Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in
accounts.0008 drops the columns and refuses to run while guest rows exist. Docs describe the current state; tests that only proved guest behaviour are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,7 +152,7 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
|
||||
(throttle_scope='bazaar_verify'): هر درخواستِ پذیرفتهشده یک تماسِ
|
||||
سرور-به-سرورِ واقعی با API بازار میزنه؛ بدونِ throttle مخصوص، یک
|
||||
کاربرِ احرازهویتشده میتونست با ارسالِ مکررِ purchaseToken بارِ زیادی
|
||||
روی سرویسِ بازار/بکاند بندازه. GuestAwareBazaarThrottle با pk کاربرِ
|
||||
روی سرویسِ بازار/بکاند بندازه. throttle با pk کاربرِ
|
||||
احرازهویتشده کلید میخوره (نه IP)، پس تغییرِ IP این محدودیت رو دور
|
||||
نمیزنه.
|
||||
"""
|
||||
@@ -160,7 +160,7 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.client = APIClient()
|
||||
self.user = User.objects.create(device_id='device-bazaar-throttle-1', is_guest=True, name='')
|
||||
self.user = User.objects.create(phone='09000000007', name='')
|
||||
self.token = issue_access_token(self.user.id)
|
||||
|
||||
def _make_order(self, key):
|
||||
@@ -211,7 +211,7 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
|
||||
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
|
||||
# کاربرِ غیرمهمان (نه self.user که مهمانه)، چون کلیدِ throttleِ
|
||||
# مهمانها عمداً بر اساسِ IP است؛ این تست فقط رفتارِ per-user رو میسنجه.
|
||||
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000751')
|
||||
registered_user = User.objects.create(name='', phone='+989120000751')
|
||||
registered_token = issue_access_token(registered_user.id)
|
||||
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '3/hour'}):
|
||||
@@ -256,29 +256,13 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
|
||||
self.assertFalse(Transaction.objects.filter(order=second_order).exists())
|
||||
|
||||
|
||||
class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
|
||||
"""
|
||||
🔒 اثبات فیکسِ امنیتی: قبلاً کلیدِ throttle_scope='bazaar_verify' برای
|
||||
هر کاربرِ احرازهویتشده (مهمان یا نه) user.pk بود. چون GuestSessionView
|
||||
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) میسازه، یک مهاجم
|
||||
میتونست با ساختنِ پیدرپیِ حسابهای مهمان، هر بار یک bucket throttle
|
||||
تازه برای تریگر کردنِ تماسِ سرور-به-سرورِ verify_purchase بگیره. حالا
|
||||
GuestAwareBazaarThrottle برای کاربرهای مهمان کلید رو بر اساسِ IP
|
||||
میسازه، پس چرخشِ حساب مهمان دیگه bucket تازه نمیده. رفتارِ throttle
|
||||
برای کاربرِ غیرمهمان (per-user، مثلِ قبل) دستنخورده مونده.
|
||||
"""
|
||||
class BazaarVerifyPurchasePerUserThrottleTests(TestCase):
|
||||
"""هر کاربرِ ثبتنامشده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمیرسد."""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.client = APIClient()
|
||||
|
||||
def _make_guest_with_order(self, device_id, key):
|
||||
guest = User.objects.create(device_id=device_id, is_guest=True, name='')
|
||||
order = Order.objects.create(
|
||||
user_id=guest.id, amount=10000, provider='bazaar',
|
||||
bazaar_product_sku='sku_one_time', idempotency_key=key,
|
||||
)
|
||||
return guest, order
|
||||
|
||||
def _verify(self, order_id, purchase_token, token):
|
||||
return self.client.post(
|
||||
@@ -288,32 +272,13 @@ class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
|
||||
HTTP_AUTHORIZATION=f'Bearer {token}',
|
||||
)
|
||||
|
||||
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '2/hour'}):
|
||||
with patch('payment.views.bazaar.verify_purchase') as mock_verify:
|
||||
mock_verify.return_value = BazaarPurchaseResult(is_valid=True, purchase_state=0)
|
||||
|
||||
guest1, order1 = self._make_guest_with_order('device-bazaar-guest-rot-1', 'order-bazaar-rot-1')
|
||||
response1 = self._verify(order1.id, 'token-rot-1', issue_access_token(guest1.id))
|
||||
self.assertEqual(response1.status_code, 200)
|
||||
|
||||
guest2, order2 = self._make_guest_with_order('device-bazaar-guest-rot-2', 'order-bazaar-rot-2')
|
||||
response2 = self._verify(order2.id, 'token-rot-2', issue_access_token(guest2.id))
|
||||
self.assertEqual(response2.status_code, 200)
|
||||
|
||||
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
|
||||
# bucket بر اساسِ IP بهاشتراک گذاشته شده، نه user.pk.
|
||||
guest3, order3 = self._make_guest_with_order('device-bazaar-guest-rot-3', 'order-bazaar-rot-3')
|
||||
response3 = self._verify(order3.id, 'token-rot-3', issue_access_token(guest3.id))
|
||||
|
||||
self.assertEqual(response3.status_code, 429)
|
||||
|
||||
def test_registered_users_keep_independent_per_user_buckets(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '1/hour'}):
|
||||
with patch('payment.views.bazaar.verify_purchase') as mock_verify:
|
||||
mock_verify.return_value = BazaarPurchaseResult(is_valid=True, purchase_state=0)
|
||||
|
||||
user_a = User.objects.create(is_guest=False, name='', phone='+989120000761')
|
||||
user_a = User.objects.create(name='', phone='+989120000761')
|
||||
order_a = Order.objects.create(
|
||||
user_id=user_a.id, amount=10000, provider='bazaar',
|
||||
bazaar_product_sku='sku_one_time', idempotency_key='order-bazaar-reg-a',
|
||||
@@ -321,7 +286,7 @@ class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
|
||||
response_a = self._verify(order_a.id, 'token-reg-a', issue_access_token(user_a.id))
|
||||
self.assertEqual(response_a.status_code, 200)
|
||||
|
||||
user_b = User.objects.create(is_guest=False, name='', phone='+989120000762')
|
||||
user_b = User.objects.create(name='', phone='+989120000762')
|
||||
order_b = Order.objects.create(
|
||||
user_id=user_b.id, amount=10000, provider='bazaar',
|
||||
bazaar_product_sku='sku_one_time', idempotency_key='order-bazaar-reg-b',
|
||||
@@ -330,26 +295,6 @@ class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
|
||||
|
||||
self.assertEqual(response_b.status_code, 200)
|
||||
|
||||
def test_blocked_guest_request_does_not_call_bazaar_or_mutate_order(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '1/hour'}):
|
||||
with patch('payment.views.bazaar.verify_purchase') as mock_verify:
|
||||
mock_verify.return_value = BazaarPurchaseResult(is_valid=True, purchase_state=0)
|
||||
|
||||
guest1, order1 = self._make_guest_with_order('device-bazaar-guest-rot-4', 'order-bazaar-rot-4')
|
||||
first_response = self._verify(order1.id, 'token-rot-4', issue_access_token(guest1.id))
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
|
||||
mock_verify.reset_mock()
|
||||
guest2, order2 = self._make_guest_with_order('device-bazaar-guest-rot-5', 'order-bazaar-rot-5')
|
||||
blocked_response = self._verify(order2.id, 'token-rot-5', issue_access_token(guest2.id))
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
mock_verify.assert_not_called()
|
||||
|
||||
order2.refresh_from_db()
|
||||
self.assertEqual(order2.status, 'pending')
|
||||
self.assertEqual(order2.bazaar_purchase_token, '')
|
||||
self.assertFalse(Transaction.objects.filter(order=order2).exists())
|
||||
|
||||
|
||||
class ZarinpalClientTests(TestCase):
|
||||
"""
|
||||
|
||||
@@ -23,7 +23,7 @@ from subscription.models import Plan, Subscription
|
||||
class _Base(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120007001', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120007001')
|
||||
self.client = APIClient()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
self.month = Plan.objects.create(title='ماهانه', price=49000, duration_days=30)
|
||||
@@ -137,7 +137,7 @@ class ReconcileTests(_Base):
|
||||
class AffiliateTests(_Base):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.referrer = User.objects.create(phone='09120007002', is_guest=False)
|
||||
self.referrer = User.objects.create(phone='09120007002')
|
||||
self.account = AffiliateAccount.objects.create(user_id=self.referrer.id, referral_code='SARA10',
|
||||
commission_rate=0.1)
|
||||
|
||||
@@ -157,11 +157,3 @@ class AffiliateTests(_Base):
|
||||
me = referrer.get('/api/affiliate/me').json()
|
||||
self.assertEqual((me['referredCount'], me['paidCount'], me['totalCommission']), (1, 1, 4900))
|
||||
|
||||
def test_guests_take_no_part(self):
|
||||
guest = User.objects.create(device_id='dev-aff', is_guest=True)
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(guest.id)}')
|
||||
self.assertEqual(client.post('/api/affiliate/track', {'referralCode': 'SARA10'},
|
||||
format='json').status_code, 403)
|
||||
self.assertEqual(client.get('/api/affiliate/me').status_code, 403)
|
||||
self.assertFalse(Conversion.objects.exists())
|
||||
|
||||
@@ -26,7 +26,7 @@ from .models import Order
|
||||
class BazaarPackTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120007101', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120007101')
|
||||
self.client = APIClient()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||||
@@ -74,7 +74,7 @@ class BazaarSkuComesFromTheOrderTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120007102', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120007102')
|
||||
self.client = APIClient()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
|
||||
@@ -105,7 +105,7 @@ class BazaarPurchaseFromTheAppTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120007103', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120007103')
|
||||
self.client = APIClient()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||||
|
||||
@@ -25,7 +25,7 @@ def _client(user):
|
||||
class InvoiceTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120007001', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120007001')
|
||||
self.client = _client(self.user)
|
||||
self.plan = Plan.objects.create(title='سالانه', price=299000, duration_days=365)
|
||||
|
||||
@@ -80,7 +80,7 @@ class InvoiceTests(TestCase):
|
||||
|
||||
def test_someone_else_sees_nothing(self):
|
||||
self.buy()
|
||||
stranger = User.objects.create(phone='09120007002', is_guest=False)
|
||||
stranger = User.objects.create(phone='09120007002')
|
||||
self.assertEqual(self.invoices(_client(stranger)), [])
|
||||
|
||||
def test_orders_from_before_the_snapshot_have_no_list_price(self):
|
||||
|
||||
@@ -27,7 +27,7 @@ from .services import OrderStateError, apply_order_payment_result, cancel_pendin
|
||||
class _Base(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120007401', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120007401')
|
||||
self.client_app = APIClient()
|
||||
self.client_app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||||
@@ -107,7 +107,7 @@ class RefundServiceTests(_Base):
|
||||
self.assertEqual(sub.status, 'canceled')
|
||||
|
||||
def test_the_referrers_commission_for_this_order_is_undone(self):
|
||||
referrer = User.objects.create(phone='09120007402', is_guest=False)
|
||||
referrer = User.objects.create(phone='09120007402')
|
||||
account = AffiliateAccount.objects.create(user_id=referrer.id, referral_code='REF1', commission_rate=0.1)
|
||||
Conversion.objects.create(affiliate_account=account, referred_user_id=self.user.id)
|
||||
order = self.paid_pack()
|
||||
|
||||
@@ -30,7 +30,7 @@ def client_for(user):
|
||||
class _Base(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120009901', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120009901')
|
||||
self.app = client_for(self.user)
|
||||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||||
|
||||
|
||||
@@ -48,24 +48,6 @@ def _redirect_to_configured_url(url: str) -> HttpResponseRedirect:
|
||||
return redirect_cls(url)
|
||||
|
||||
|
||||
class GuestAwareBazaarThrottle(ScopedRateThrottle):
|
||||
"""همون الگوی GuestAwareAIThrottle/GuestAwareDiscountThrottle/etc: چرخشِ
|
||||
حساب مهمان (هر deviceId یه pk تازه) نباید bucket تازهی bazaar_verify
|
||||
بده، چون هر درخواستِ پذیرفتهشده یه فراخوانیِ سرور-به-سرور به بازاره.
|
||||
برای مهمانها کلیدِ cache بر اساسِ IP هست، نه pk."""
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
user = request.user
|
||||
if user and user.is_authenticated and getattr(user, 'is_guest', False):
|
||||
ident = f'guest-ip:{self.get_ident(request)}'
|
||||
elif user and user.is_authenticated:
|
||||
ident = user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
||||
return self.cache_format % {'scope': self.scope, 'ident': ident}
|
||||
|
||||
|
||||
class CreateOrderView(APIView):
|
||||
"""
|
||||
POST /api/payment/orders — ساخت سفارش (فراخوانیشده از اپ Subscription)
|
||||
@@ -299,11 +281,9 @@ class BazaarVerifyPurchaseView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
# این endpoint یک فراخوانیِ سرور-به-سرورِ واقعی به API بازار (verify_purchase)
|
||||
# تریگر میکنه، برای همین throttle_scope مجزا و نرخِ سختگیرانهتر (۱۰/hour)
|
||||
# داره. کاربرهای مهمان با هر deviceId یک pk تازه میگیرن، پس
|
||||
# GuestAwareBazaarThrottle برای اونها بر اساسِ IP کلید میسازه تا چرخشِ
|
||||
# حساب bucket تازه نگیره؛ برای کاربرهای غیرمهمان رفتارِ per-user حفظ میشه.
|
||||
# داره.
|
||||
throttle_scope = 'bazaar_verify'
|
||||
throttle_classes = [GuestAwareBazaarThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method == 'GET':
|
||||
|
||||
Reference in New Issue
Block a user