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>
155 lines
7.8 KiB
Python
155 lines
7.8 KiB
Python
"""
|
||
حفاظهای بازبینیِ امنیتیِ پیش از تحویل (محتوا، پنل و ورودیها). هر تست رفتاری را نگه میدارد که
|
||
پیشتر غلط بود.
|
||
"""
|
||
import io
|
||
import json
|
||
|
||
from django.contrib.auth.models import Group, User as AuthUser
|
||
from django.core.cache import cache
|
||
from django.core.management import call_command
|
||
from django.test import TestCase
|
||
from rest_framework.test import APIClient
|
||
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import User
|
||
from content.models import Choice, Grade, Lesson, Node, Question, QuestionReport, Unit, UserAnswer
|
||
|
||
|
||
class SupervisorCannotEditContentTests(TestCase):
|
||
"""admin_view فقط is_staff را میسنجد؛ صفحههای ورودِ فایل اجازهی مدل را جدا چک نمیکردند."""
|
||
|
||
def setUp(self):
|
||
cache.clear()
|
||
call_command('setup_supervisor_group', stdout=io.StringIO())
|
||
sup = AuthUser.objects.create_user('sup', 's@x.c', 'pw12345!', is_staff=True)
|
||
sup.groups.add(Group.objects.get(name='Supervisor'))
|
||
self.client.force_login(sup)
|
||
|
||
grade = Grade.objects.create(title='G', icon='', color='', number=55)
|
||
unit = Unit.objects.create(grade=grade, title='U', order=1)
|
||
node = Node.objects.create(unit=unit, title='N', order=1)
|
||
self.lesson = Lesson.objects.create(node=node, title='L', intro_text='x')
|
||
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')
|
||
UserAnswer.objects.create(user_id=self.student.id, question=self.q, choice=ok, correct=True)
|
||
|
||
def test_question_import_with_replace_is_refused(self):
|
||
f = io.BytesIO(json.dumps([{
|
||
'lessonId': self.lesson.id, 'prompt': 'HACKED', 'type': 'multiple_choice',
|
||
'choices': ['a', 'b'], 'correct': 0,
|
||
}]).encode())
|
||
f.name = 'q.json'
|
||
r = self.client.post('/admin/content/question/import/', {'file': f, 'replace': 'on'})
|
||
self.assertEqual(r.status_code, 403)
|
||
self.assertEqual(list(Question.objects.filter(lesson=self.lesson).values_list('prompt', flat=True)), ['p?'])
|
||
self.assertEqual(UserAnswer.objects.filter(user_id=self.student.id).count(), 1)
|
||
|
||
def test_curriculum_import_and_content_browser_are_refused(self):
|
||
self.assertEqual(self.client.get('/admin/content/grade/import-curriculum/').status_code, 403)
|
||
self.assertEqual(self.client.get('/admin/content/question/import/').status_code, 403)
|
||
self.assertEqual(self.client.get(f'/admin/content/question/browse/questions/{self.lesson.id}/').status_code, 403)
|
||
|
||
def test_mark_resolved_action_is_refused(self):
|
||
report = QuestionReport.objects.create(question=self.q, reason='wrong_answer', user_id=self.student.id)
|
||
self.client.post('/admin/content/questionreport/', {'action': 'mark_resolved', '_selected_action': [report.pk]})
|
||
report.refresh_from_db()
|
||
self.assertNotEqual(report.status, 'resolved')
|
||
|
||
|
||
class NonAsciiDigitsAre400Tests(TestCase):
|
||
"""'²'.isdigit() هم True است ولی int('²') خطا میدهد؛ این ورودیها ۵۰۰ میدادند."""
|
||
|
||
def setUp(self):
|
||
cache.clear()
|
||
user = User.objects.create(phone='09000000010')
|
||
self.app = APIClient()
|
||
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||
|
||
def test_character_and_unit_ids(self):
|
||
self.assertEqual(self.app.post('/api/characters/select', {'characterId': 'abc'}, format='json').status_code, 400)
|
||
self.assertEqual(self.app.post('/api/characters/select', {'characterId': '²'}, format='json').status_code, 400)
|
||
self.assertEqual(self.app.get('/api/progress/nodes', {'unit': '²'}).status_code, 400)
|
||
|
||
def test_ticket_text_is_capped(self):
|
||
r = self.app.post('/api/support/bug-reports', {'title': 't', 'description': 'x' * 5001}, format='json')
|
||
self.assertEqual(r.status_code, 400)
|
||
ok = self.app.post('/api/support/bug-reports', {'title': 't', 'description': 'x' * 5000}, format='json')
|
||
self.assertIn(ok.status_code, (200, 201))
|
||
|
||
|
||
class JunkGradeIdsDoNotFlushCacheTests(TestCase):
|
||
"""
|
||
هر ?gradeId=ِ ناموجود در /api/home یک کلیدِ کش میساخت؛ ۴۰۰ عددِ ساختگی LocMemCache را پر و
|
||
شمارندهی سقفِ ورودِ ادمین را پاک میکرد (یازدهمین تلاش دوباره ۲۰۰ میگرفت، نه ۴۲۹).
|
||
"""
|
||
|
||
def setUp(self):
|
||
from unittest import mock
|
||
|
||
from rest_framework.throttling import ScopedRateThrottle
|
||
|
||
cache.clear()
|
||
self.addCleanup(cache.clear)
|
||
# فقط برای سرعتِ تست؛ با سقفِ واقعی (۱۲۰ در دقیقه) همین کار حدودِ سه دقیقه طول میکشید.
|
||
patcher = mock.patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'read': '100000/min'})
|
||
patcher.start()
|
||
self.addCleanup(patcher.stop)
|
||
user = User.objects.create(phone='09120009911')
|
||
self.app = APIClient()
|
||
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||
|
||
def test_admin_login_limit_survives_junk_grade_ids(self):
|
||
anon = APIClient()
|
||
for _ in range(10):
|
||
anon.post('/admin/login/', {'username': 'x', 'password': 'y'})
|
||
self.assertEqual(anon.post('/admin/login/', {'username': 'x', 'password': 'y'}).status_code, 429)
|
||
for n in range(1000, 1400):
|
||
self.assertEqual(self.app.get('/api/home', {'gradeId': n}).status_code, 404)
|
||
self.assertEqual(anon.post('/admin/login/', {'username': 'x', 'password': 'y'}).status_code, 429)
|
||
|
||
def test_unknown_grade_is_not_cached_and_new_grade_still_loads(self):
|
||
self.app.get('/api/home', {'gradeId': 777})
|
||
self.assertFalse(any('grade:777' in k for k in cache._cache)) # LocMemCache در تستها
|
||
grade = Grade.objects.create(title='تازه', icon='', color='')
|
||
self.assertEqual(self.app.get('/api/home', {'gradeId': grade.id}).status_code, 200)
|
||
|
||
|
||
class ProgressNodesInputAndQueriesTests(TestCase):
|
||
"""?unit= بزرگتر از INTEGERِ SQLite ۵۰۰ میداد؛ lessonsTotal برای هر ردیف یک کوئریِ جدا بود."""
|
||
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(phone='09120009912')
|
||
self.app = APIClient()
|
||
self.app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||
|
||
def test_huge_unit_is_400(self):
|
||
self.assertEqual(self.app.get('/api/progress/nodes', {'unit': '9' * 25}).status_code, 400)
|
||
|
||
def test_query_count_does_not_grow_with_rows(self):
|
||
from django.db import connection
|
||
from django.test.utils import CaptureQueriesContext
|
||
|
||
from content.models import NodeProgress
|
||
|
||
unit = Unit.objects.create(grade=Grade.objects.create(title='g', icon='', color=''), title='u', order=1)
|
||
|
||
def add_nodes(start, n):
|
||
for i in range(start, start + n):
|
||
node = Node.objects.create(unit=unit, title=f'n{i}', order=i)
|
||
Lesson.objects.create(node=node, title='l1')
|
||
Lesson.objects.create(node=node, title='l2')
|
||
NodeProgress.objects.create(user_id=self.user.id, node=node)
|
||
|
||
add_nodes(0, 2)
|
||
with CaptureQueriesContext(connection) as small:
|
||
self.app.get('/api/progress/nodes')
|
||
add_nodes(2, 8)
|
||
with CaptureQueriesContext(connection) as big:
|
||
r = self.app.get('/api/progress/nodes')
|
||
self.assertEqual(len(big), len(small))
|
||
self.assertEqual([row['lessonsTotal'] for row in r.json()['progress']], [2] * 10)
|