""" بازبینیِ دومِ ۲۰۲۶-۰۹-۲۵: ایرادهایی که بازتولید و بسته شدند. هر تست همان سناریویی را اجرا می‌کند که پیش‌تر خراب بود. ایرادهایی که عمداً باز ماندند (تصمیمِ محصولی یا خطرِ شکستنِ فرانت) در docs/security-before-production.md و کامنتِ همان کدها ثبت‌اند. """ from unittest.mock import MagicMock, patch from django.core.cache import cache from django.test import TestCase, override_settings from django.utils import timezone 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, Unit from discount.models import Offer from payment.models import Order from payment.services import apply_order_payment_result from subscription.models import Plan, Subscription def _client(user): client = APIClient() client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}') return client class _Base(TestCase): def setUp(self): cache.clear() self.user = User.objects.create(phone='09120008001') self.client = _client(self.user) self.year = Plan.objects.create(title='سالانه', price=299000, duration_days=365) def tearDown(self): cache.clear() def buy(self, client=None): response = (client or self.client).post( '/api/subscription/purchase', {'planId': str(self.year.id)}, format='json') self.assertEqual(response.status_code, 200, response.content) return Order.objects.get(pk=response.json()['orderId']) def pay(self, order, ref): apply_order_payment_result(order=order, gateway='zarinpal', gateway_ref_id=ref, new_status='paid', event='test') class OfferNeverShortensSubscription(_Base): def test_trial_code_after_a_paid_year_keeps_the_year(self): self.pay(self.buy(), ref='R1') before = Subscription.objects.get(user_id=self.user.id).expires_at Offer.objects.create(name='هفته رایگان', kind='trial', duration_days=7, code='WEEK') response = self.client.post('/api/discount/offers/redeem', {'code': 'WEEK'}, format='json') self.assertEqual(response.status_code, 200, response.content) self.assertEqual(Subscription.objects.get(user_id=self.user.id).expires_at, before) def test_trial_still_works_for_a_user_without_subscription(self): Offer.objects.create(name='هفته رایگان', kind='trial', duration_days=7, code='WEEK') self.client.post('/api/discount/offers/redeem', {'code': 'WEEK'}, format='json') sub = Subscription.objects.get(user_id=self.user.id) self.assertEqual(sub.status, 'active') self.assertIn((sub.expires_at - timezone.now()).days, (6, 7)) def test_free_full_still_upgrades_a_paid_year_to_unlimited(self): self.pay(self.buy(), ref='R3') Offer.objects.create(name='رایگانِ کامل', kind='free_full', code='FULL') self.client.post('/api/discount/offers/redeem', {'code': 'FULL'}, format='json') self.assertIsNone(Subscription.objects.get(user_id=self.user.id).expires_at) @override_settings(ZARINPAL_MERCHANT_ID='x' * 36, ZARINPAL_REDIRECT_URL='https://app.example/') class ZarinpalErrorBodyReturnsToApp(_Base): def test_documented_error_body_redirects_instead_of_500(self): order = self.buy() Order.objects.filter(pk=order.pk).update(gateway_authority='A' * 36) fake = MagicMock(status_code=200) fake.json.return_value = {'data': [], 'errors': {'code': -51, 'message': 'failed'}} with patch('payment.zarinpal.requests.post', return_value=fake): response = APIClient().get('/api/payment/zarinpal/callback', {'Authority': 'A' * 36, 'Status': 'OK'}) self.assertEqual(response.status_code, 302) self.assertIn('status=failed', response['Location']) class WeeklyReportCountsIslands(TestCase): # تا ۲۰۲۶-۱۰-۰۱ همین سناریو گزارشِ والد را از /api/family/weekly-report می‌خواند؛ # آن اپ حذف شد و گزارش حالا در حسابِ خودِ دانش‌آموز است. def test_island_quiz_xp_reaches_the_weekly_report(self): cache.clear() grade = Grade.objects.create(title='پایه نهم', icon='', color='') node = Node.objects.create(unit=Unit.objects.create(grade=grade, title='فصل', order=1), title='جزیره', order=1) child = User.objects.create(phone='09120008002', grade_id=grade.id) child_client = _client(child) for i in range(4): question = Question.objects.create( lesson=Lesson.objects.create(node=node, title=f'درس {i}'), type='multiple_choice', prompt=f'سوال {i}') right = Choice.objects.create(question=question, value='الف', is_correct=True) Choice.objects.create(question=question, value='ب', is_correct=False) child_client.post(f'/api/questions/{question.id}/answer', {'selectedOptionId': right.id}, format='json') child_client.post(f'/api/nodes/{node.id}/quiz/complete', {'correctCount': 4, 'totalQuestions': 4}, format='json') week = child_client.get('/api/user/weekly-report').json()['thisWeek'] self.assertEqual(week['xp'], 40) self.assertEqual(week['islands'], 1) self.assertEqual(week['lessons'], 0) self.assertEqual((week['questions'], week['correct']), (4, 4)) class AskWithNonNumericQuestionId(TestCase): def test_answer_is_saved_not_500(self): cache.clear() user = User.objects.create(phone='09120008004') response = _client(user).post('/api/ai/ask', {'userQuestion': 'کسر چیست؟', 'questionId': 'q-12'}, format='json') self.assertEqual(response.status_code, 200, response.content) from ai_tutor.models import Message self.assertIsNone(Message.objects.get(pk=response.json()['messageId']).question_id)