""" GET /api/user/weekly-report — گزارشی که فرانت با عنوانِ «گزارشِ والدین» نشان می‌دهد. هر تست یک راهِ شکستن را امتحان می‌کند: دیدنِ دیتای کاربرِ دیگر، افتادنِ فعالیت در روز یا هفته‌ی غلط (مرزِ نیمه‌شبِ تهران)، و دو بار شمرده شدنِ درسِ تکراری. """ from datetime import datetime, timedelta from django.core.cache import cache from django.test import SimpleTestCase, TestCase from django.utils import timezone from rest_framework.test import APIClient from accounts.authentication import issue_access_token from accounts.models import User from ai_tutor.models import Conversation, Message from content.models import ( Choice, Grade, Lesson, Node, NodeProgress, Question, Unit, UserAnswer, UserLessonCompletion, ) from .report import estimate_minutes class EstimateMinutesTests(SimpleTestCase): def at(self, *minutes): base = datetime(2026, 10, 1, 10, 0) return [base + timedelta(minutes=m) for m in minutes] def test_no_activity_is_zero(self): self.assertEqual(estimate_minutes([]), 0) def test_a_single_answer_counts_one_minute(self): self.assertEqual(estimate_minutes(self.at(0)), 1) def test_close_events_are_one_continuous_session(self): self.assertEqual(estimate_minutes(self.at(0, 3, 5)), 6) def test_a_long_break_is_not_counted_as_study(self): # دو نشستِ جدا: ۱ + ۲ دقیقه + ۱ (شروعِ نشستِ دوم)؛ دو ساعتِ وسط شمرده نمی‌شود. self.assertEqual(estimate_minutes(self.at(0, 2, 120)), 4) def test_order_does_not_matter(self): self.assertEqual(estimate_minutes(self.at(5, 0, 3)), estimate_minutes(self.at(0, 3, 5))) class WeeklyReportApiTests(TestCase): def setUp(self): cache.clear() self.user = User.objects.create(phone='09120009001') self.other = User.objects.create(phone='09120009002') grade = Grade.objects.create(title='پایه نهم', icon='', color='') node = Node.objects.create(unit=Unit.objects.create(grade=grade, title='فصل', order=1), title='جزیره', order=1) self.lesson = Lesson.objects.create(node=node, title='درس') self.question = Question.objects.create(lesson=self.lesson, type='multiple_choice', prompt='؟') self.right = Choice.objects.create(question=self.question, value='الف', is_correct=True) self.wrong = Choice.objects.create(question=self.question, value='ب', is_correct=False) def tearDown(self): cache.clear() def client_for(self, user): client = APIClient() client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}') return client def answer(self, user, correct, when): row = UserAnswer.objects.create(user_id=user.id, question=self.question, choice=self.right if correct else self.wrong, correct=correct) UserAnswer.objects.filter(pk=row.pk).update(created_at=when) def report(self, user=None, **params): response = self.client_for(user or self.user).get('/api/user/weekly-report', params) self.assertEqual(response.status_code, 200, response.content) return response.json() def test_requires_a_token(self): self.assertIn(APIClient().get('/api/user/weekly-report').status_code, (401, 403)) def test_empty_account_gets_fourteen_zero_days(self): data = self.report() self.assertEqual(len(data['days']), 7) self.assertEqual(len(data['previousDays']), 7) self.assertEqual(data['days'][-1]['date'], timezone.localdate().isoformat()) self.assertEqual(data['thisWeek']['minutes'], 0) self.assertEqual(data['lastWeek']['activeDays'], 0) def test_only_the_callers_own_activity_is_reported(self): now = timezone.now() self.answer(self.other, True, now) self.answer(self.other, True, now) # پارامترِ userId (مثلِ /api/progress/nodes) نادیده گرفته می‌شود. data = self.report(userId=self.other.id) self.assertEqual(data['thisWeek']['questions'], 0) self.assertEqual(self.report(self.other)['thisWeek']['questions'], 2) def test_this_week_and_last_week_are_split_at_seven_days(self): now = timezone.now() self.answer(self.user, True, now) self.answer(self.user, False, now - timedelta(days=8)) self.answer(self.user, True, now - timedelta(days=20)) # بیرون از هر دو پنجره data = self.report() self.assertEqual((data['thisWeek']['questions'], data['thisWeek']['correct']), (1, 1)) self.assertEqual((data['lastWeek']['questions'], data['lastWeek']['correct']), (1, 0)) def test_day_follows_tehran_midnight_not_utc(self): # ۲۲:۰۰ UTC = ۰۱:۳۰ بامدادِ روزِ بعد در تهران. today = timezone.localdate() yesterday_utc_night = timezone.make_aware( datetime.combine(today - timedelta(days=1), datetime.min.time()) + timedelta(hours=22), timezone=timezone.get_fixed_timezone(0), ) self.answer(self.user, True, yesterday_utc_night) data = self.report() self.assertEqual(data['days'][-1]['questions'], 1) self.assertEqual(data['days'][-2]['questions'], 0) def test_repeating_a_lesson_counts_it_once(self): for _ in range(2): UserLessonCompletion.objects.create(user_id=self.user.id, lesson=self.lesson, correct_count=1, stars=3, xp_earned=10) week = self.report()['thisWeek'] self.assertEqual(week['lessons'], 1) self.assertEqual(week['xp'], 20) def test_tutor_questions_are_counted_but_not_its_answers(self): conversation = Conversation.objects.create(user_id=self.user.id) Message.objects.create(conversation=conversation, role='user', text='سوال') Message.objects.create(conversation=conversation, role='assistant', text='جواب') other = Conversation.objects.create(user_id=self.other.id) Message.objects.create(conversation=other, role='user', text='سوالِ دیگری') week = self.report()['thisWeek'] self.assertEqual(week['aiQuestions'], 1) self.assertEqual(week['activeDays'], 1) class NodeProgressIsOwnOnlyTests(TestCase): """تا ۲۰۲۶-۱۰-۰۱ والدِ وصل‌شده با ?userId= پیشرفتِ فرزند را می‌دید؛ پارامتر حذف شد.""" def setUp(self): cache.clear() self.user = User.objects.create(phone='09120009003') self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}') def test_someone_elses_id_returns_only_your_own_progress(self): other = User.objects.create(phone='09120009004') node = Node.objects.create( unit=Unit.objects.create(grade=Grade.objects.create(title='پایه', icon='', color=''), title='فصل', order=1), title='جزیره', order=1) NodeProgress.objects.create(user_id=other.id, node=node, status='in_progress') response = self.client.get(f'/api/progress/nodes?userId={other.id}') self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['progress'], []) def test_junk_parameter_is_not_a_server_error(self): self.assertEqual(self.client.get('/api/progress/nodes?userId=abc').status_code, 200)