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:
125
content/tests.py
125
content/tests.py
@@ -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)
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from accounts.models import User
|
||||
from accounts.testing import guest_session
|
||||
from accounts.authentication import issue_access_token
|
||||
from content.models import Choice, Grade, Lesson, Node, Question, Unit
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ class BasicLearningFlowTests(TestCase):
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||
|
||||
def test_a_new_user_can_go_from_signup_to_finishing_a_lesson(self):
|
||||
# ۱. حسابِ مهمانِ قدیمی (ورودِ مهمان حذف شده؛ این ردیفها هنوز هستند).
|
||||
session = guest_session('flow-device-1')
|
||||
# ۱. کاربرِ ثبتنامشده با شماره (توکنِ ورود).
|
||||
session = {'token': issue_access_token(User.objects.create(phone='09000000101', name='').id)}
|
||||
self._auth(session['token'])
|
||||
|
||||
# ۲. صفحهی «بیا باهم آشنا بشیم»: اسم + پایه.
|
||||
@@ -110,7 +110,7 @@ class BasicLearningFlowTests(TestCase):
|
||||
def test_completing_twice_with_the_same_key_does_not_double_count(self):
|
||||
from content.models import UserLessonCompletion
|
||||
|
||||
session = guest_session('flow-device-2')
|
||||
session = {'token': issue_access_token(User.objects.create(phone='09000000102', name='').id)}
|
||||
self._auth(session['token'])
|
||||
self.client.post('/api/user/grade', {'gradeId': self.grade.id}, format='json')
|
||||
self.client.post(
|
||||
@@ -144,7 +144,7 @@ class BasicLearningFlowTests(TestCase):
|
||||
بهروز میشود. بدونِ دانستنِ این، توسعهدهندهی فرانت دنبالِ باگی
|
||||
میگردد که وجود ندارد.
|
||||
"""
|
||||
session = guest_session('flow-device-4')
|
||||
session = {'token': issue_access_token(User.objects.create(phone='09000000104', name='').id)}
|
||||
self._auth(session['token'])
|
||||
self.client.post('/api/user/grade', {'gradeId': self.grade.id}, format='json')
|
||||
|
||||
@@ -162,7 +162,7 @@ class BasicLearningFlowTests(TestCase):
|
||||
self.assertGreater(self.client.get('/api/user/summary').data['xp'], 0)
|
||||
|
||||
def test_the_map_is_empty_but_healthy_before_a_grade_is_chosen(self):
|
||||
session = guest_session('flow-device-3')
|
||||
session = {'token': issue_access_token(User.objects.create(phone='09000000103', name='').id)}
|
||||
self._auth(session['token'])
|
||||
|
||||
home = self.client.get('/api/home')
|
||||
@@ -191,7 +191,7 @@ class LessonWithoutQuestionsTests(TestCase):
|
||||
)
|
||||
Choice.objects.create(question=question, value='الف', is_correct=True)
|
||||
|
||||
user = User.objects.create(device_id='empty-lesson', is_guest=True, grade_id=grade.id)
|
||||
user = User.objects.create(phone='09000000008', grade_id=grade.id)
|
||||
from accounts.authentication import issue_access_token
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||||
|
||||
@@ -315,7 +315,7 @@ class LessonCompletionNotificationTests(TestCase):
|
||||
self.correct = Choice.objects.create(question=question, value='الف', is_correct=True)
|
||||
self.question = question
|
||||
|
||||
self.user = User.objects.create(device_id='notif-on-complete', is_guest=True, grade_id=grade.id)
|
||||
self.user = User.objects.create(phone='09000000009', grade_id=grade.id)
|
||||
from accounts.authentication import issue_access_token
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class SkillMapTests(TestCase):
|
||||
self.assertEqual(rows['ویدیوی توان']['kind'], 'video')
|
||||
self.assertEqual(rows['ویدیوی توان']['meta'], '12 دقیقه')
|
||||
|
||||
def test_done_is_false_for_a_guest_and_true_after_completion(self):
|
||||
def test_done_is_false_when_signed_out_and_true_after_completion(self):
|
||||
self.assertFalse(any(r['done'] for r in self._lessons()['lessons']))
|
||||
|
||||
UserLessonCompletion.objects.create(user_id=self.user.id, lesson=self.quiz,
|
||||
|
||||
@@ -32,7 +32,7 @@ def _client(user):
|
||||
class _World:
|
||||
def build(self, phone):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone=phone, is_guest=False)
|
||||
self.user = User.objects.create(phone=phone)
|
||||
self.client = _client(self.user)
|
||||
grade = Grade.objects.create(title='پایه نهم', icon='', color='')
|
||||
self.node = Node.objects.create(unit=Unit.objects.create(grade=grade, title='فصل', order=1),
|
||||
|
||||
@@ -33,7 +33,7 @@ class SupervisorCannotEditContentTests(TestCase):
|
||||
self.q = Question.objects.create(lesson=self.lesson, type='multiple_choice', prompt='p?')
|
||||
ok = Choice.objects.create(question=self.q, value='1', is_correct=True)
|
||||
Choice.objects.create(question=self.q, value='2', is_correct=False)
|
||||
self.student = User.objects.create(phone='09120009999', is_guest=False)
|
||||
self.student = User.objects.create(phone='09120009999')
|
||||
UserAnswer.objects.create(user_id=self.student.id, question=self.q, choice=ok, correct=True)
|
||||
|
||||
def test_question_import_with_replace_is_refused(self):
|
||||
@@ -64,7 +64,7 @@ class NonAsciiDigitsAre400Tests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
user = User.objects.create(device_id='digits', is_guest=True)
|
||||
user = User.objects.create(phone='09000000010')
|
||||
self.app = APIClient()
|
||||
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||||
|
||||
@@ -97,7 +97,7 @@ class JunkGradeIdsDoNotFlushCacheTests(TestCase):
|
||||
patcher = mock.patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'read': '100000/min'})
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
user = User.objects.create(phone='09120009911', is_guest=False)
|
||||
user = User.objects.create(phone='09120009911')
|
||||
self.app = APIClient()
|
||||
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||||
|
||||
@@ -122,7 +122,7 @@ class ProgressNodesInputAndQueriesTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.user = User.objects.create(phone='09120009912', is_guest=False)
|
||||
self.user = User.objects.create(phone='09120009912')
|
||||
self.app = APIClient()
|
||||
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||||
|
||||
|
||||
@@ -49,27 +49,6 @@ from .serializers import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuestAwareContentThrottle(ScopedRateThrottle):
|
||||
"""
|
||||
مشترک با GuestAwareAIThrottle/GuestAwareDiscountThrottle
|
||||
و مشابههاش: چون GuestSessionView با هر deviceId جدید یک pk تازه میسازه،
|
||||
throttleِ استانداردِ بر اساسِ user.pk با چرخشِ حسابِ مهمان دور زده میشه —
|
||||
درحالیکه answer/complete اثرِ جانبیِ ماندگار (هارت، xp/coins/progress) دارن.
|
||||
برای همین فقط کاربرهای مهمان با 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}
|
||||
|
||||
|
||||
# --- کشِ دادهی شبهِثابتِ برنامهی درسی -------------------------------------
|
||||
# گریدها و درختِ unit/node/lesson تقریباً هیچوقت عوض نمیشوند ولی روی هر بار
|
||||
# باز شدنِ اپ خوانده میشوند. یک نسخهکلیدِ مشترک نگه میداریم که با هر تغییرِ
|
||||
@@ -172,7 +151,7 @@ class GradesListView(APIView):
|
||||
"""GET /api/grades — لیست پایهها"""
|
||||
|
||||
throttle_scope = 'read'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(responses=GradesListResponseSerializer, tags=['Curriculum (content)'])
|
||||
def get(self, request):
|
||||
@@ -260,7 +239,7 @@ class HomeView(APIView):
|
||||
# userId همیشه از request.user.id (توکن معتبر) گرفته میشود، نه ورودی کلاینت (IDOR).
|
||||
permission_classes = [IsAuthenticated]
|
||||
throttle_scope = 'read'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
@@ -438,9 +417,9 @@ class QuestionAnswerView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
# Idempotency-Key فقط جلوی retry با همون کلید رو میگیره، نه حجمِ درخواستهای
|
||||
# *متفاوت* که هرکدوم UserAnswer جدید میسازن و هارت/NodeProgress رو تغییر میدن؛
|
||||
# برای همون GuestAwareContentThrottle لازمه (نگاه کنید به تعریفش بالا).
|
||||
# برای همین throttle لازمه.
|
||||
throttle_scope = 'question_answer'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=AnswerRequestSerializer,
|
||||
@@ -556,11 +535,11 @@ class LessonCompleteView(APIView):
|
||||
# میتونست بهجای هر کاربرِ دیگری xp/coins ثبت کنه.
|
||||
permission_classes = [IsAuthenticated]
|
||||
# Idempotency-Key فقط جلوی retry با همون کلید رو میگیره؛ برای همینه که
|
||||
# GuestAwareContentThrottle لازمه (نگاه کنید به تعریفش بالا). نرخ در
|
||||
# throttle لازمه. نرخ در
|
||||
# settings (`lesson_complete`، الان ۱۲۰/hour) کمتر از question_answer است
|
||||
# چون این endpoint رکوردِ ماندگار میسازه.
|
||||
throttle_scope = 'lesson_complete'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=LessonCompleteRequestSerializer,
|
||||
@@ -823,7 +802,7 @@ class NodeQuizView(APIView):
|
||||
"""
|
||||
|
||||
throttle_scope = 'read'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
DEFAULT_SIZE = 4
|
||||
MAX_SIZE = 20
|
||||
@@ -909,7 +888,7 @@ class NodeQuizCompleteView(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
throttle_scope = 'node_complete'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=NodeQuizCompleteRequestSerializer,
|
||||
@@ -1047,7 +1026,7 @@ class NodeLessonsView(APIView):
|
||||
"""
|
||||
|
||||
throttle_scope = 'read'
|
||||
throttle_classes = [GuestAwareContentThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(responses=NodeLessonsResponseSerializer, tags=['Curriculum (content)'])
|
||||
def get(self, request, node_id: int):
|
||||
|
||||
Reference in New Issue
Block a user