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:
2026-10-05 18:20:10 +03:30
parent 8835e01595
commit 663252a260
61 changed files with 357 additions and 1518 deletions

View File

@@ -20,7 +20,7 @@ class PurchaseIdempotencyRetryTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-purchase-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000042', name='')
self.token = issue_access_token(self.user.id)
self.plan = Plan.objects.create(
title='ماهانه', price=100000, currency='IRT', duration_days=30,
@@ -77,7 +77,7 @@ class PurchaseExternalCallNotUnderLockTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-purchase-lock-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000043', name='')
self.token = issue_access_token(self.user.id)
self.plan = Plan.objects.create(
title='ماهانه', price=100000, currency='IRT', duration_days=30,
@@ -115,7 +115,7 @@ class PurchaseOrderCreationFailureCleanupTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-purchase-cleanup-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000044', name='')
self.token = issue_access_token(self.user.id)
self.plan = Plan.objects.create(
title='ماهانه', price=100000, currency='IRT', duration_days=30,
@@ -171,7 +171,7 @@ class PurchaseFailureCleanupConcurrencyRegressionTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-purchase-race-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000045', name='')
self.token = issue_access_token(self.user.id)
self.plan = Plan.objects.create(
title='ماهانه', price=100000, currency='IRT', duration_days=30,
@@ -257,7 +257,7 @@ class PurchaseIdenticalIntermediateStateRaceRegressionTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-purchase-race-identical-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000046', name='')
self.token = issue_access_token(self.user.id)
self.plan = Plan.objects.create(
title='ماهانه', price=100000, currency='IRT', duration_days=30,
@@ -314,7 +314,7 @@ class PurchaseThrottleTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-purchase-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000047', name='')
self.token = issue_access_token(self.user.id)
self.plan = Plan.objects.create(
title='ماهانه', price=100000, currency='IRT', duration_days=30,
@@ -343,7 +343,7 @@ class PurchaseThrottleTests(TestCase):
}
)
def test_requests_within_the_limit_still_succeed(self):
# ⚠️ مثلِ GuestSessionThrottleTests: override_settings به‌تنهایی روی
# override_settings به‌تنهایی روی
# ScopedRateThrottle.THROTTLE_RATES (که یک‌بار موقعِ import گرفته
# شده) اثر نداره؛ برای اینکه نرخِ واقعاً محدودشده اعمال بشه، مستقیماً
# دیکشنریِ زنده‌ی THROTTLE_RATES رو patch می‌کنیم.
@@ -362,10 +362,8 @@ class PurchaseThrottleTests(TestCase):
self.assertEqual(blocked_response.status_code, 429)
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# کاربرِ غیرمهمان لازمه، نه self.user، چون کلیدِ bucketِ مهمان‌ها
# عمداً بر اساسِ IP است (نگاه کنید به PurchaseGuestThrottleBypassTests)
# و این تست رفتارِ per-user برای ثبت‌نام‌شده‌ها را می‌سنجد.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000501')
# کلیدِ throttle شناسه‌ی کاربر است، نه IP؛ تغییرِ IP سطلِ تازه نمی‌دهد.
registered_user = User.objects.create(name='', phone='+989120000501')
registered_token = issue_access_token(registered_user.id)
def _purchase_as(idempotency_key, remote_addr):
@@ -413,19 +411,8 @@ class PurchaseThrottleTests(TestCase):
self.assertEqual(subscription.version, snapshot['version'])
class PurchaseGuestThrottleBypassTests(TestCase):
"""
🔒 اثبات فیکسِ امنیتی: قبلاً کلیدِ throttle_scope='purchase' برای هر
کاربرِ احرازهویت‌شده (مهمان یا نه) user.pk بود. چون GuestSessionView
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) می‌سازه، یک مهاجم
می‌تونست با ساختنِ پی‌درپیِ حساب‌های مهمان، هر بار یک bucket throttle
تازه برای سقفِ ۱۰/ساعتِ purchase بگیره. این endpoint حساسِ مالیه: هر
درخواستِ پذیرفته‌شده Subscription می‌سازه/آپدیت می‌کنه و create_order()
رو صدا می‌زنه. حالا GuestAwarePurchaseThrottle برای کاربرهای مهمان
کلید رو بر اساسِ IP (همون get_ident که guest_session هم استفاده
می‌کنه) می‌سازه، پس چرخشِ حساب مهمان دیگه bucket تازه نمی‌ده. رفتارِ
throttle برای کاربرِ غیرمهمان (per-user، مثلِ قبل) دست‌نخورده مونده.
"""
class PurchasePerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
from django.core.cache import cache
@@ -444,79 +431,21 @@ class PurchaseGuestThrottleBypassTests(TestCase):
HTTP_IDEMPOTENCY_KEY=idempotency_key,
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
# سه حساب مهمانِ متفاوت (شبیه‌سازیِ چرخشِ حساب برای دورزدنِ throttle)،
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'purchase': '2/hour'}):
guest1 = User.objects.create(device_id='device-purchase-guest-rot-1', is_guest=True, name='')
response1 = self._purchase('key-guest-rot-1', issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-purchase-guest-rot-2', is_guest=True, name='')
response2 = self._purchase('key-guest-rot-2', issue_access_token(guest2.id))
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk — یعنی
# سقفِ ۲/ساعت هنوز اعمال می‌شه، صرف‌نظر از تعدادِ حساب‌های
# مهمانِ ساخته‌شده.
guest3 = User.objects.create(device_id='device-purchase-guest-rot-3', is_guest=True, name='')
response3 = self._purchase('key-guest-rot-3', issue_access_token(guest3.id))
self.assertEqual(response3.status_code, 429)
def test_rotated_guest_account_cannot_get_a_fresh_bucket_after_limit(self):
# حتی وقتی حسابِ مهمانِ جدید به سقف رسیده، یک حسابِ مهمانِ دیگر
# (چهارمین pk) هم نباید bucket تازه‌ای بگیره — سقف هنوز به IP
# مشترک متصل است.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'purchase': '1/hour'}):
guest1 = User.objects.create(device_id='device-purchase-guest-rot-4', is_guest=True, name='')
response1 = self._purchase('key-guest-rot-4', issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-purchase-guest-rot-5', is_guest=True, name='')
blocked = self._purchase('key-guest-rot-5', issue_access_token(guest2.id))
self.assertEqual(blocked.status_code, 429)
guest3 = User.objects.create(device_id='device-purchase-guest-rot-6', is_guest=True, name='')
still_blocked = self._purchase('key-guest-rot-6', issue_access_token(guest3.id))
self.assertEqual(still_blocked.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن — throttle
# برای اونا هنوز بر اساسِ user.pk است، نه IP.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'purchase': '1/hour'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120000601')
user_a = User.objects.create(name='', phone='+989120000601')
response_a = self._purchase('key-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='+989120000602')
user_b = User.objects.create(name='', phone='+989120000602')
response_b = self._purchase('key-reg-b', issue_access_token(user_b.id))
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_does_not_mutate_subscription_or_call_purchase_logic(self):
# درخواستِ بلاک‌شده (از حسابِ مهمانِ چرخشی) نباید Subscription/Order
# بسازه یا create_order() رو صدا بزنه — throttle باید *قبل* از
# رسیدن به منطقِ purchase رد کنه، حتی با Idempotency-Key معتبر.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'purchase': '1/hour'}):
guest1 = User.objects.create(device_id='device-purchase-guest-rot-7', is_guest=True, name='')
first_response = self._purchase('key-guest-rot-7', issue_access_token(guest1.id))
self.assertEqual(first_response.status_code, 200)
# حسابِ مهمانِ تازه‌ی دوم باید بلاک بشه (bucket مشترکِ IP از
# قبل مصرف شده) و نباید Subscription/Order برای آن بسازد.
guest2 = User.objects.create(device_id='device-purchase-guest-rot-8', is_guest=True, name='')
with patch('subscription.views.create_order') as mock_create_order:
blocked_response = self._purchase('key-guest-rot-8', issue_access_token(guest2.id))
self.assertEqual(blocked_response.status_code, 429)
mock_create_order.assert_not_called()
self.assertFalse(Subscription.objects.filter(user_id=guest2.id).exists())
self.assertEqual(Order.objects.filter(user_id=guest2.id).count(), 0)
class PlanCardContractTests(TestCase):
"""