cover the new paths, especially the ones that fail silently

خطاهای این پروژه خاموش‌اند: نامِ اشتباهِ collection یا فیلترِ نخورده نتیجه‌ی
خالی می‌دهد نه استثنا، و دستیار فقط بدتر جواب می‌دهد. این تست‌ها نگهبانِ
همان‌جاهایند.

TextbookLayoutTests بررسی می‌کند رشته‌ی هر فصل بینِ ingest و retrieval یکی
باشد و بازه‌ی صفحه‌ها پیوسته بماند. IngestBookResumeTests مطمئن می‌شود
صفحه‌ی بی‌مسئله دوبار پول نمی‌گیرد، صفحه‌ی خطادار در اجرای بعد دوباره
سراغش می‌روند، و upsertِ شکست‌خورده rollback می‌شود.
LessonContextResolutionTests نگهبانِ نرمال‌سازیِ «پایه نهم» → «ریاضی نهم»
است و اینکه مسیرِ gradeId و مسیرِ lessonId به یک collection برسند.
GradeAwarePromptTests جلوی برگشتنِ لحنِ دبستانی برای پایه‌های بالا را
می‌گیرد. FollowUpStaysGroundedTests پیامِ دومِ یک گفتگو را هم مستند نگه
می‌دارد.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 19:01:24 +03:30
parent ac1b1af398
commit 5f3aae0756
4 changed files with 859 additions and 0 deletions

View File

@@ -1493,3 +1493,70 @@ def _orig_loader():
except _U.DoesNotExist:
raise AuthenticationFailed('کاربر پیدا نشد.')
return _load
class UpdateUserProfileTests(TestCase):
"""
تست‌های PATCH /api/user/profile.
صفحه‌ی «بیا باهم آشنا بشیم» در اپ اسم می‌گیرد و تا پیش از این هیچ مسیری
برای نوشتنش نبود — پروفایل فقط خواندنی بود.
"""
def setUp(self):
self.client = APIClient()
cache.clear()
self.user = User.objects.create(device_id='device-patch-1', is_guest=True, name='')
self.token = issue_access_token(self.user.id)
def tearDown(self):
cache.clear()
def _patch(self, body, token=None):
kwargs = {'format': 'json'}
token = self.token if token is None else token
if token:
kwargs['HTTP_AUTHORIZATION'] = f'Bearer {token}'
return self.client.patch('/api/user/profile', body, **kwargs)
def test_requires_authentication(self):
self.assertEqual(self._patch({'name': 'سارا'}, token='').status_code, 401)
def test_sets_name_and_returns_the_updated_profile(self):
response = self._patch({'name': 'سارا'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['name'], 'سارا')
self.user.refresh_from_db()
self.assertEqual(self.user.name, 'سارا')
def test_partial_update_leaves_other_fields_alone(self):
self._patch({'name': 'علی', 'bio': 'عاشقِ ریاضی'})
response = self._patch({'bio': 'کتاب‌خوان'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['name'], 'علی')
self.assertEqual(response.data['bio'], 'کتاب‌خوان')
def test_creates_the_profile_row_lazily(self):
self.assertFalse(Profile.objects.filter(user_id=self.user.id).exists())
response = self._patch({'birthYear': 1391})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['birthYear'], 1391)
self.assertTrue(Profile.objects.filter(user_id=self.user.id).exists())
def test_empty_body_is_rejected(self):
self.assertEqual(self._patch({}).status_code, 400)
def test_phone_and_guest_flag_cannot_be_changed(self):
response = self._patch({'name': 'رضا', 'phone': '09120000000', 'isGuest': False})
self.assertEqual(response.status_code, 200)
self.user.refresh_from_db()
self.assertIsNone(self.user.phone)
self.assertTrue(self.user.is_guest)
def test_out_of_range_birth_year_is_rejected(self):
self.assertEqual(self._patch({'birthYear': 2026}).status_code, 400)