110 lines
5.6 KiB
Python
110 lines
5.6 KiB
Python
from django.test import TestCase
|
||
from rest_framework.test import APIClient
|
||
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import User
|
||
|
||
from .models import Grade, Lesson, Node, Question, Unit
|
||
|
||
|
||
class LessonCompleteImpossibleCountTests(TestCase):
|
||
"""
|
||
🔒 تستِ رگرسیونِ فیکسِ امنیتی: LessonCompleteView قبلاً correctCount رو
|
||
بدونِ مقایسه با تعدادِ واقعیِ سوالهای درس میپذیرفت، پس یک کلاینتِ
|
||
مخرب میتونست با correctCount بزرگتر از تعدادِ واقعیِ سوالها،
|
||
xp/coins متورم بگیره. این تست هم ردِ درخواستهای غیرممکن رو، هم
|
||
رفتارِ صحیحِ تکمیل با مقادیرِ معتبر رو تضمین میکنه.
|
||
"""
|
||
|
||
def setUp(self):
|
||
self.client = APIClient()
|
||
self.user = User.objects.create(device_id='device-complete-1', is_guest=True, name='')
|
||
self.token = issue_access_token(self.user.id)
|
||
|
||
grade = Grade.objects.create(title='پایه اول', icon='icon', color='#fff')
|
||
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
|
||
node = Node.objects.create(unit=unit, title='گره ۱', order=1)
|
||
self.lesson = Lesson.objects.create(node=node, title='درس ۱', intro_text='...')
|
||
for i in range(3):
|
||
Question.objects.create(
|
||
lesson=self.lesson, type='multiple_choice', prompt=f'سوال {i}',
|
||
)
|
||
|
||
def _complete(self, correct_count, total_questions):
|
||
return self.client.post(
|
||
f'/api/lessons/{self.lesson.id}/complete',
|
||
{'correctCount': correct_count, 'totalQuestions': total_questions},
|
||
format='json',
|
||
HTTP_AUTHORIZATION=f'Bearer {self.token}',
|
||
)
|
||
|
||
def test_correct_count_exceeding_actual_questions_is_rejected(self):
|
||
# فقط ۳ سوال واقعی در این درس هست؛ کلاینت ادعای ۱۰۰ تای درست از
|
||
# ۱۰ تای کل رو میکنه.
|
||
response = self._complete(correct_count=100, total_questions=10)
|
||
self.assertEqual(response.status_code, 400)
|
||
self.assertIn('correctCount', response.data)
|
||
|
||
def test_negative_correct_count_is_rejected(self):
|
||
response = self._complete(correct_count=-1, total_questions=3)
|
||
self.assertEqual(response.status_code, 400)
|
||
|
||
def test_zero_total_questions_is_rejected(self):
|
||
response = self._complete(correct_count=1, total_questions=0)
|
||
self.assertEqual(response.status_code, 400)
|
||
|
||
def test_valid_completion_still_grants_expected_reward(self):
|
||
# ۳ از ۳ سوالِ واقعیِ همین درس، درست — باید ۳ ستاره و xp کامل بده.
|
||
response = self._complete(correct_count=3, total_questions=3)
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(response.data['stars'], 3)
|
||
self.assertEqual(response.data['xpEarned'], 30)
|
||
self.assertEqual(response.data['coinsEarned'], 15)
|
||
|
||
|
||
class LessonCompleteRatioIgnoresClientTotalQuestionsTests(TestCase):
|
||
"""
|
||
🔒 تستِ رگرسیونِ فیکسِ دوم: قبلاً ratio از totalQuestions ارسالیی
|
||
خودِ کلاینت محاسبه میشد، نه از تعدادِ واقعیِ سوالهای درس. یعنی حتی
|
||
بعد از سقفزدنِ correctCount، کلاینت میتونست با totalQuestions=1
|
||
(بهجای ۳ سوالِ واقعی)، ratio رو مصنوعاً ۱۰۰٪ کنه و ۳ ستاره/coins
|
||
کامل بگیره درحالیکه فقط ۱ از ۳ سوال رو درست جواب داده. حالا ratio
|
||
همیشه بر اساسِ actual_question_count محاسبه میشه.
|
||
"""
|
||
|
||
def setUp(self):
|
||
self.client = APIClient()
|
||
self.user = User.objects.create(device_id='device-complete-2', is_guest=True, name='')
|
||
self.token = issue_access_token(self.user.id)
|
||
|
||
grade = Grade.objects.create(title='پایه دوم', icon='icon', color='#fff')
|
||
unit = Unit.objects.create(grade=grade, title='فصل ۲', order=1)
|
||
node = Node.objects.create(unit=unit, title='گره ۲', order=1)
|
||
self.lesson = Lesson.objects.create(node=node, title='درس ۲', intro_text='...')
|
||
for i in range(3):
|
||
Question.objects.create(
|
||
lesson=self.lesson, type='multiple_choice', prompt=f'سوال {i}',
|
||
)
|
||
|
||
def _complete(self, correct_count, total_questions):
|
||
return self.client.post(
|
||
f'/api/lessons/{self.lesson.id}/complete',
|
||
{'correctCount': correct_count, 'totalQuestions': total_questions},
|
||
format='json',
|
||
HTTP_AUTHORIZATION=f'Bearer {self.token}',
|
||
)
|
||
|
||
def test_manipulated_total_questions_does_not_yield_full_stars(self):
|
||
# درسِ ۳-سوالی؛ کلاینت فقط ۱ درست داشته ولی totalQuestions=1
|
||
# میفرسته تا وانمود کنه ۱۰۰٪ جواب داده.
|
||
response = self._complete(correct_count=1, total_questions=1)
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertNotEqual(response.data['stars'], 3)
|
||
|
||
def test_ratio_is_based_on_actual_lesson_question_count(self):
|
||
# ۱ از ۳ سوالِ واقعی = ratio نزدیکِ ۰.۳۳ → طبقِ آستانههای موجود
|
||
# (>=0.9 → 3, >=0.6 → 2, >0 → 1)، باید دقیقاً ۱ ستاره بده.
|
||
response = self._complete(correct_count=1, total_questions=1)
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(response.data['stars'], 1)
|