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

@@ -29,7 +29,7 @@ class AskIdempotencyFollowUpsTests(TestCase):
# بینِ تست‌ها پاک نمی‌شه) می‌تونه از یک تستِ قبلی باقی بمونه.
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ask-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000021', name='')
self.token = issue_access_token(self.user.id)
def _ask(self, idempotency_key):
@@ -83,7 +83,7 @@ class AskAIGenerationFailureTests(TestCase):
# ⚠️ افزوده شده: نگاه کنید به توضیحِ مشابه در AskIdempotencyFollowUpsTests.setUp
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ask-fail-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000022', name='')
self.token = issue_access_token(self.user.id)
def _ask(self, idempotency_key=None):
@@ -177,7 +177,7 @@ class AskAIConcurrentIdempotencyKeyTests(TestCase):
# ⚠️ افزوده شده: نگاه کنید به توضیحِ مشابه در AskIdempotencyFollowUpsTests.setUp
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ask-race-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000023', name='')
self.token = issue_access_token(self.user.id)
def test_losing_request_cleans_up_and_returns_winners_result(self):
@@ -254,9 +254,9 @@ class AskAIConversationApiContractTests(TestCase):
# ⚠️ افزوده شده: نگاه کنید به توضیحِ مشابه در AskIdempotencyFollowUpsTests.setUp
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-contract-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000024', name='')
self.token = issue_access_token(self.user.id)
self.other_user = User.objects.create(device_id='device-contract-2', is_guest=True, name='')
self.other_user = User.objects.create(phone='09000000025', name='')
self.other_token = issue_access_token(self.other_user.id)
def test_no_conversation_id_creates_new_conversation(self):
@@ -348,7 +348,7 @@ class GenerationBoundaryContextTests(TestCase):
# فرستاده شدن به مرزِ تولید برسن — این metadataی لازم برای
# RAG/Qdrantِ آینده‌ست، نه تاریخچه.
client = APIClient()
user = User.objects.create(device_id='device-boundary-grade-1', is_guest=True, name='')
user = User.objects.create(phone='09000000026', name='')
token = issue_access_token(user.id)
with patch(
@@ -376,7 +376,7 @@ class GenerationBoundaryContextTests(TestCase):
# پیاده نشده، context باید None بمونه (فقط gradeId/lessonId/سوال
# مجازن).
client = APIClient()
user = User.objects.create(device_id='device-boundary-1', is_guest=True, name='')
user = User.objects.create(phone='09000000027', name='')
token = issue_access_token(user.id)
conversation = Conversation.objects.create(user_id=user.id)
@@ -432,7 +432,7 @@ class AskAIViewThrottleTests(TestCase):
def setUp(self):
self.client = APIClient()
cache.clear()
self.user = User.objects.create(device_id='device-ai-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000028', name='')
self.token = issue_access_token(self.user.id)
def _ask(self, idempotency_key):
@@ -456,18 +456,8 @@ class AskAIViewThrottleTests(TestCase):
self.assertEqual(throttled_response.status_code, 429)
class AskAIViewGuestThrottleBypassTests(TestCase):
"""
🔒 اثبات فیکسِ امنیتی: قبلاً کلیدِ throttle_scope='ai_ask' برای هر
کاربرِ احرازهویت‌شده (مهمان یا نه) user.pk بود. چون GuestSessionView
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) می‌سازه، یک
مهاجم می‌تونست با ساختنِ پی‌درپیِ حساب‌های مهمان، هر بار یک bucket
throttle تازه برای AskAIView بگیره. حالا GuestAwareAIThrottle برای
کاربرهای مهمان کلید رو بر اساسِ IP (همون get_ident که guest_session
هم استفاده می‌کنه) می‌سازه، پس چرخشِ حساب مهمان دیگه bucket تازه
نمی‌ده. رفتارِ throttle برای کاربرِ غیرمهمان (per-user، مثلِ قبل)
دست‌نخورده مونده.
"""
class AskAIViewPerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
cache.clear()
@@ -482,35 +472,17 @@ class AskAIViewGuestThrottleBypassTests(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, {'ai_ask': '2/hour'}):
guest1 = User.objects.create(device_id='device-guest-rot-1', is_guest=True, name='')
response1 = self._ask(issue_access_token(guest1.id), 'guest-rot-key-1')
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-guest-rot-2', is_guest=True, name='')
response2 = self._ask(issue_access_token(guest2.id), 'guest-rot-key-2')
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk.
guest3 = User.objects.create(device_id='device-guest-rot-3', is_guest=True, name='')
response3 = self._ask(issue_access_token(guest3.id), 'guest-rot-key-3')
self.assertEqual(response3.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن — throttle
# برای اونا هنوز بر اساسِ user.pk است، نه IP.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'ai_ask': '1/hour'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120000101')
user_a = User.objects.create(name='', phone='+989120000101')
response_a = self._ask(issue_access_token(user_a.id), 'user-a-key-1')
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120000102')
user_b = User.objects.create(name='', phone='+989120000102')
response_b = self._ask(issue_access_token(user_b.id), 'user-b-key-1')
self.assertEqual(response_b.status_code, 200)
@@ -522,7 +494,7 @@ class StubNeverReachesProductionTests(TestCase):
@override_settings(DEBUG=False, OPENAI_API_KEY='', QDRANT_API_KEY='', RAG_FORCE_MOCK=False)
def test_missing_keys_are_a_503_and_cost_nothing(self):
cache.clear()
user = User.objects.create(device_id='device-no-keys', is_guest=True, name='')
user = User.objects.create(phone='09000000029', name='')
response = APIClient().post(
'/api/ai/ask', {'userQuestion': '۲+۲ چند است؟'}, format='json',
HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}',
@@ -539,7 +511,7 @@ class GlobalDailyCapTests(TestCase):
cache.clear()
codes = []
for i in range(3):
user = User.objects.create(device_id=f'cap-{i}', is_guest=True, name='')
user = User.objects.create(phone=f'0900{i:07d}', name='')
codes.append(APIClient().post(
'/api/ai/ask', {'userQuestion': f'۲+{i} چند است؟'}, format='json',
HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}',