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 LessonCompleteImpossibleCountTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-complete-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000011', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -79,7 +79,7 @@ class LessonCompleteRatioIgnoresClientTotalQuestionsTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='device-complete-2', is_guest=True, name='')
self.user = User.objects.create(phone='09000000012', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه دوم', icon='icon', color='#fff')
@@ -130,7 +130,7 @@ class QuestionAnswerThrottleTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-answer-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000013', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -173,13 +173,8 @@ class QuestionAnswerThrottleTests(TestCase):
self.assertEqual(blocked_response.status_code, 429)
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده می‌شه (نه self.user که
# مهمان است)، چون طبقِ فیکسِ GuestAwareContentThrottle حالا کلیدِ
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
# QuestionAnswerGuestThrottleBypassTests پایین‌تر) — این تست فقط
# رفتارِ per-user (نه IP) برای کاربرهایِ ثبت‌نام‌شده رو تضمین
# می‌کنه که باید دقیقاً مثلِ قبل دست‌نخورده بمونه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000701')
# کلیدِ throttle شناسه‌ی کاربر است، نه IP؛ تغییرِ IP سطلِ تازه نمی‌دهد.
registered_user = User.objects.create(name='', phone='+989120000701')
registered_token = issue_access_token(registered_user.id)
def _answer_as(remote_addr):
@@ -253,7 +248,7 @@ class LessonCompleteThrottleTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-complete-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000014', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -294,13 +289,8 @@ class LessonCompleteThrottleTests(TestCase):
self.assertEqual(blocked_response.status_code, 429)
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده می‌شه (نه self.user که
# مهمان است)، چون طبقِ فیکسِ GuestAwareContentThrottle حالا کلیدِ
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
# LessonCompleteGuestThrottleBypassTests پایین‌تر) — این تست فقط
# رفتارِ per-user (نه IP) برای کاربرهایِ ثبت‌نام‌شده رو تضمین
# می‌کنه که باید دقیقاً مثلِ قبل دست‌نخورده بمونه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000702')
# کلیدِ throttle شناسه‌ی کاربر است، نه IP؛ تغییرِ IP سطلِ تازه نمی‌دهد.
registered_user = User.objects.create(name='', phone='+989120000702')
registered_token = issue_access_token(registered_user.id)
def _complete_as(remote_addr):
@@ -358,10 +348,8 @@ class LessonCompleteThrottleTests(TestCase):
self.assertEqual(progress_after.last_activity_at, progress_snapshot['last_activity_at'])
class QuestionAnswerGuestThrottleBypassTests(TestCase):
"""اثباتِ فیکسِ throttle: چرخشِ حساب مهمان (هر deviceId یه pk تازه) نباید
bucket تازه‌ی question_answer بده — GuestAwareContentThrottle برای
مهمان‌ها کلید رو بر اساسِ IP می‌سازه، نه user.pk."""
class QuestionAnswerPerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
from django.core.cache import cache
@@ -387,58 +375,23 @@ class QuestionAnswerGuestThrottleBypassTests(TestCase):
HTTP_AUTHORIZATION=f'Bearer {token}',
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
# سه حساب مهمانِ متفاوت (شبیه‌سازیِ چرخشِ حساب برای دورزدنِ throttle)،
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'question_answer': '2/min'}):
guest1 = User.objects.create(device_id='device-answer-guest-rot-1', is_guest=True, name='')
response1 = self._answer(issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-answer-guest-rot-2', is_guest=True, name='')
response2 = self._answer(issue_access_token(guest2.id))
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk.
guest3 = User.objects.create(device_id='device-answer-guest-rot-3', is_guest=True, name='')
response3 = self._answer(issue_access_token(guest3.id))
self.assertEqual(response3.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'question_answer': '1/min'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120000711')
user_a = User.objects.create(name='', phone='+989120000711')
response_a = self._answer(issue_access_token(user_a.id))
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120000712')
user_b = User.objects.create(name='', phone='+989120000712')
response_b = self._answer(issue_access_token(user_b.id))
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_creates_no_answer_or_hearts_change(self):
# درخواستِ بلاک‌شده (از حسابِ مهمانِ چرخشی) نباید UserAnswer بسازه
# یا هارت/progress رو تغییر بده.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'question_answer': '1/min'}):
guest1 = User.objects.create(device_id='device-answer-guest-rot-4', is_guest=True, name='')
first_response = self._answer(issue_access_token(guest1.id))
self.assertEqual(first_response.status_code, 200)
guest2 = User.objects.create(device_id='device-answer-guest-rot-5', is_guest=True, name='')
blocked_response = self._answer(issue_access_token(guest2.id))
self.assertEqual(blocked_response.status_code, 429)
self.assertEqual(UserAnswer.objects.filter(user_id=guest2.id).count(), 0)
self.assertFalse(UserHearts.objects.filter(user_id=guest2.id).exists())
class LessonCompleteGuestThrottleBypassTests(TestCase):
"""اثباتِ فیکسِ throttle: چرخشِ حساب مهمان نباید bucket تازه‌ی
lesson_complete بده — GuestAwareContentThrottle برای مهمان‌ها کلید رو
بر اساسِ IP می‌سازه، نه user.pk."""
class LessonCompletePerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
from django.core.cache import cache
@@ -462,54 +415,20 @@ class LessonCompleteGuestThrottleBypassTests(TestCase):
HTTP_AUTHORIZATION=f'Bearer {token}',
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
# سه حساب مهمانِ متفاوت (شبیه‌سازیِ چرخشِ حساب برای دورزدنِ throttle)،
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'lesson_complete': '2/hour'}):
guest1 = User.objects.create(device_id='device-complete-guest-rot-1', is_guest=True, name='')
response1 = self._complete(issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-complete-guest-rot-2', is_guest=True, name='')
response2 = self._complete(issue_access_token(guest2.id))
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk.
guest3 = User.objects.create(device_id='device-complete-guest-rot-3', is_guest=True, name='')
response3 = self._complete(issue_access_token(guest3.id))
self.assertEqual(response3.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'lesson_complete': '1/hour'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120000721')
user_a = User.objects.create(name='', phone='+989120000721')
response_a = self._complete(issue_access_token(user_a.id))
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120000722')
user_b = User.objects.create(name='', phone='+989120000722')
response_b = self._complete(issue_access_token(user_b.id))
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_creates_no_completion_or_reward_side_effects(self):
# درخواستِ بلاک‌شده (از حسابِ مهمانِ چرخشی) نباید UserLessonCompletion
# بسازه یا UserStats/NodeProgress رو تغییر بده.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'lesson_complete': '1/hour'}):
guest1 = User.objects.create(device_id='device-complete-guest-rot-4', is_guest=True, name='')
first_response = self._complete(issue_access_token(guest1.id))
self.assertEqual(first_response.status_code, 200)
guest2 = User.objects.create(device_id='device-complete-guest-rot-5', is_guest=True, name='')
blocked_response = self._complete(issue_access_token(guest2.id))
self.assertEqual(blocked_response.status_code, 429)
self.assertEqual(UserLessonCompletion.objects.filter(user_id=guest2.id).count(), 0)
self.assertFalse(UserStats.objects.filter(user_id=guest2.id).exists())
self.assertFalse(NodeProgress.objects.filter(user_id=guest2.id, node=self.node).exists())
class QuestionAnswerMascotMoodTests(TestCase):
"""پاسخِ /answer باید mascotMood بده: درست → happy، غلط → encourage
@@ -519,7 +438,7 @@ class QuestionAnswerMascotMoodTests(TestCase):
from django.core.cache import cache
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-mascot-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000015', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='i', color='#fff')
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
@@ -574,7 +493,7 @@ class HomeViewScalabilityTests(TestCase):
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-home-scale', is_guest=True, name='')
self.user = User.objects.create(phone='09000000016', name='')
self.token = issue_access_token(self.user.id)
def _grade_with_nodes(self, n_nodes):
@@ -661,7 +580,7 @@ class UnlimitedHeartsSubscriptionTests(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create(device_id='dev-unlimited-hearts', is_guest=True, name='')
self.user = User.objects.create(phone='09000000017', name='')
self.token = issue_access_token(self.user.id)
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
@@ -717,7 +636,7 @@ class ReadThrottleTests(TestCase):
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-read-throttle', is_guest=True, name='')
self.user = User.objects.create(phone='09000000018', name='')
self.token = issue_access_token(self.user.id)
self.grade = Grade.objects.create(title='g', icon='i', color='#fff')
@@ -742,7 +661,7 @@ class HomeGradeFallbackTests(TestCase):
def setUp(self):
_django_cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='dev-home-fallback', is_guest=True, name='')
self.user = User.objects.create(phone='09000000019', name='')
self.token = issue_access_token(self.user.id)
self.grade = Grade.objects.create(title='پایه نهم', icon='grade-9', color='#FF9800')
@@ -805,7 +724,7 @@ class NodeProgressUnitParamTests(TestCase):
from accounts.authentication import issue_access_token
from accounts.models import User
user = User.objects.create(device_id='unit-param', is_guest=True)
user = User.objects.create(phone='09000000020')
response = APIClient().get('/api/progress/nodes?unit=abc',
HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
self.assertEqual(response.status_code, 400)