Remove silent fallbacks found in the 2026-10-03 audit
- /api/ai/ask: the stub answer is reachable only under DEBUG or RAG_FORCE_MOCK. Without RAG keys on the server it used to return 200 with "this is a stub, set OPENAI_API_KEY" and spend the student's quota; now 503. - /api/home: the `grade` alias is gone and sending it is a 400 instead of a silent fallback to the user's own grade. No client sent it. - activate_subscription logs ERROR when the paid order's plan no longer exists (it silently became 30 days). - resolve_grade_title logs instead of `except: pass`; curriculum_io no longer reads an `intro_text` key no file contains. - CLAUDE.md: "No silent fallbacks" rules for future agents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,7 +92,7 @@ def parse(raw: bytes) -> list[dict]:
|
||||
'lessons': [
|
||||
{
|
||||
'title': _text(lesson.get('title'), 'title', f'{nwhere} › نوع {li + 1}'),
|
||||
'intro_text': (lesson.get('introText') or lesson.get('intro_text') or ''),
|
||||
'intro_text': (lesson.get('introText') or ''),
|
||||
'example': str(lesson.get('example') or ''),
|
||||
'steps': [str(x) for x in (lesson.get('steps') or []) if str(x).strip()][:12],
|
||||
}
|
||||
|
||||
@@ -587,7 +587,7 @@ class HomeViewScalabilityTests(TestCase):
|
||||
|
||||
def _home(self, grade_id):
|
||||
return self.client.get(
|
||||
f'/api/home?grade={grade_id}', HTTP_AUTHORIZATION=f'Bearer {self.token}')
|
||||
f'/api/home?gradeId={grade_id}', HTTP_AUTHORIZATION=f'Bearer {self.token}')
|
||||
|
||||
def test_query_count_is_flat_regardless_of_node_count(self):
|
||||
g_small = self._grade_with_nodes(2)
|
||||
@@ -726,7 +726,7 @@ class ReadThrottleTests(TestCase):
|
||||
codes = set()
|
||||
for _ in range(122):
|
||||
codes.add(self.client.get(
|
||||
f'/api/home?grade={self.grade.id}',
|
||||
f'/api/home?gradeId={self.grade.id}',
|
||||
HTTP_AUTHORIZATION=f'Bearer {self.token}').status_code)
|
||||
self.assertIn(429, codes)
|
||||
|
||||
@@ -774,7 +774,7 @@ class HomeGradeFallbackTests(TestCase):
|
||||
self.user.grade_id = self.grade.id
|
||||
self.user.save(update_fields=['grade_id'])
|
||||
|
||||
response = self._home(f'?grade={other.id}')
|
||||
response = self._home(f'?gradeId={other.id}')
|
||||
|
||||
self.assertEqual(response.data['units'][0]['title'], 'فصل مثلثات')
|
||||
|
||||
@@ -785,7 +785,7 @@ class HomeGradeFallbackTests(TestCase):
|
||||
self.assertEqual(response.data['units'], [])
|
||||
|
||||
def test_unknown_explicit_grade_is_still_404(self):
|
||||
self.assertEqual(self._home('?grade=999999').status_code, 404)
|
||||
self.assertEqual(self._home('?gradeId=999999').status_code, 404)
|
||||
|
||||
def test_a_deleted_saved_grade_degrades_to_an_empty_map(self):
|
||||
# پایهای که کاربر ذخیره کرده حذف شده — این خطای کاربر نیست.
|
||||
|
||||
@@ -142,8 +142,8 @@ class HomeGradeParamTests(TestCase):
|
||||
def test_grade_id_selects_that_grade(self):
|
||||
self.assertEqual(self._titles(f'?gradeId={self.b.id}'), ['فصلِ نهم'])
|
||||
|
||||
def test_the_old_grade_name_still_works(self):
|
||||
self.assertEqual(self._titles(f'?grade={self.b.id}'), ['فصلِ نهم'])
|
||||
def test_the_removed_grade_name_is_a_400_not_a_silent_fallback(self):
|
||||
self.assertEqual(self.client.get(f'/api/home?grade={self.b.id}', **self.auth).status_code, 400)
|
||||
|
||||
def test_without_a_parameter_the_users_own_grade_is_used(self):
|
||||
self.assertEqual(self._titles(), ['فصلِ هفتم'])
|
||||
|
||||
@@ -253,7 +253,7 @@ def _node_status(lesson_ids, completed_ids, is_first_unlocked_candidate, quiz_co
|
||||
|
||||
|
||||
class HomeView(APIView):
|
||||
"""GET /api/home?grade — نقشهی مهارتها + آمار (ادغامشده)"""
|
||||
"""GET /api/home?gradeId — نقشهی مهارتها + آمار (ادغامشده)"""
|
||||
|
||||
# userId همیشه از request.user.id (توکن معتبر) گرفته میشود، نه ورودی کلاینت (IDOR).
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -265,8 +265,6 @@ class HomeView(APIView):
|
||||
OpenApiParameter('gradeId', OpenApiTypes.INT, required=False,
|
||||
description='پایهای که نقشهاش خواسته میشود؛ '
|
||||
'بدونِ آن، پایهی ذخیرهشدهی خودِ کاربر.'),
|
||||
OpenApiParameter('grade', OpenApiTypes.INT, required=False,
|
||||
deprecated=True, description='نامِ قدیمیِ gradeId.'),
|
||||
],
|
||||
responses=HomeResponseSerializer,
|
||||
tags=['Curriculum (content)'],
|
||||
@@ -278,13 +276,12 @@ class HomeView(APIView):
|
||||
# خودِ کاربر نادیده میماند؛ نتیجه این بود که اپ در «آشنایی» پایه را ثبت
|
||||
# میکرد و بعد صفحهی خانه خالی برمیگشت. حالا پارامتر اولویت دارد (برای
|
||||
# مرورِ پایهی دیگر) و در نبودش پایهی خودِ کاربر ملاک است.
|
||||
# هر دو نام پذیرفته میشوند. نامِ درست `gradeId` است — با بقیهی API
|
||||
# یکی است (profile و /user/grade هم gradeId میگویند) — ولی نسخهی
|
||||
# اولِ این اندپوینت `grade` بود و کلاینتهایی با همان بیرون رفتهاند.
|
||||
# پذیرفتنِ هر دو ارزان است؛ نپذیرفتن یعنی انتخابِ پایه بیصدا نادیده
|
||||
# گرفته میشود و کاربر نقشهی پایهی دیگری را میبیند.
|
||||
grade_param = (request.query_params.get('gradeId')
|
||||
or request.query_params.get('grade'))
|
||||
# فقط `gradeId` (همنام با بقیهی API). نامِ قدیمیِ `grade` در ۲۰۲۶-۱۰-۰۳
|
||||
# حذف شد: تنها کلاینت (MathApp_frontend) آن را نمیفرستد. فرستادنش ۴۰۰ است،
|
||||
# نه نادیده گرفتنِ بیصدا (که کاربر را به نقشهی پایهی خودش میبرد).
|
||||
if 'grade' in request.query_params:
|
||||
raise ValidationError({'grade': 'این پارامتر حذف شده؛ gradeId بفرستید.'})
|
||||
grade_param = request.query_params.get('gradeId')
|
||||
if grade_param:
|
||||
try:
|
||||
grade_id = int(grade_param)
|
||||
|
||||
Reference in New Issue
Block a user