feat(issue-225): extend backend i18n error contract to flow endpoints
This commit is contained in:
@@ -209,6 +209,8 @@ class StartRoundTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_start_round")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Only host can start round")
|
self.assertEqual(response.json()["error"], "Only host can start round")
|
||||||
|
|
||||||
def test_start_round_requires_existing_active_category_with_questions(self):
|
def test_start_round_requires_existing_active_category_with_questions(self):
|
||||||
@@ -228,6 +230,8 @@ class StartRoundTests(TestCase):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(response.json()["error_code"], "category_has_no_questions")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Category has no active questions")
|
self.assertEqual(response.json()["error"], "Category has no active questions")
|
||||||
|
|
||||||
def test_start_round_rejects_non_lobby_session(self):
|
def test_start_round_rejects_non_lobby_session(self):
|
||||||
@@ -244,6 +248,36 @@ class StartRoundTests(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Round can only be started from lobby")
|
self.assertEqual(response.json()["error"], "Round can only be started from lobby")
|
||||||
|
|
||||||
|
def test_start_round_error_localizes_to_danish(self):
|
||||||
|
self.client.login(username="other", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("lobby:start_round", kwargs={"code": self.session.code}),
|
||||||
|
data={"category_slug": self.category.slug},
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_ACCEPT_LANGUAGE="da",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_start_round")
|
||||||
|
self.assertEqual(response.json()["locale"], "da")
|
||||||
|
self.assertEqual(response.json()["error"], "Kun værten kan starte runden")
|
||||||
|
|
||||||
|
def test_start_round_error_falls_back_to_english_for_unsupported_locale(self):
|
||||||
|
self.client.login(username="other", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("lobby:start_round", kwargs={"code": self.session.code}),
|
||||||
|
data={"category_slug": self.category.slug},
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_ACCEPT_LANGUAGE="fr",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_start_round")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
|
self.assertEqual(response.json()["error"], "Only host can start round")
|
||||||
|
|
||||||
|
|
||||||
class LieSubmissionTests(TestCase):
|
class LieSubmissionTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -434,6 +468,8 @@ class MixAnswersTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(response.json()["error_code"], "host_only_mix_answers")
|
||||||
|
self.assertEqual(response.json()["locale"], "en")
|
||||||
self.assertEqual(response.json()["error"], "Only host can mix answers")
|
self.assertEqual(response.json()["error"], "Only host can mix answers")
|
||||||
|
|
||||||
def test_mix_answers_deduplicates_case_insensitive_lies(self):
|
def test_mix_answers_deduplicates_case_insensitive_lies(self):
|
||||||
|
|||||||
@@ -268,7 +268,11 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can start round"}, status=403)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("host_only_start_round", "host_only_start_round"),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LOBBY:
|
if session.status != GameSession.Status.LOBBY:
|
||||||
return api_error(
|
return api_error(
|
||||||
@@ -287,7 +291,11 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not Question.objects.filter(category=category, is_active=True).exists():
|
if not Question.objects.filter(category=category, is_active=True).exists():
|
||||||
return JsonResponse({"error": "Category has no active questions"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("category_has_no_questions", "category_has_no_questions"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
session = GameSession.objects.select_for_update().get(pk=session.pk)
|
session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
@@ -340,21 +348,41 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can show question"}, status=403)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("host_only_show_question", "host_only_show_question"),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
if session.status != GameSession.Status.LIE:
|
if session.status != GameSession.Status.LIE:
|
||||||
return JsonResponse({"error": "Question can only be shown in lie phase"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("show_question_invalid_phase", "show_question_invalid_phase"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_config = RoundConfig.objects.get(session=session, number=session.current_round)
|
round_config = RoundConfig.objects.get(session=session, number=session.current_round)
|
||||||
except RoundConfig.DoesNotExist:
|
except RoundConfig.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round config missing"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("round_config_missing", "round_config_missing"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
if RoundQuestion.objects.filter(session=session, round_number=session.current_round).exists():
|
||||||
return JsonResponse({"error": "Question already shown for this round"}, status=409)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("question_already_shown", "question_already_shown"),
|
||||||
|
status=409,
|
||||||
|
)
|
||||||
|
|
||||||
used_question_ids = RoundQuestion.objects.filter(session=session).values_list("question_id", flat=True)
|
used_question_ids = RoundQuestion.objects.filter(session=session).values_list("question_id", flat=True)
|
||||||
available_questions = Question.objects.filter(
|
available_questions = Question.objects.filter(
|
||||||
@@ -363,7 +391,11 @@ def show_question(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
).exclude(pk__in=used_question_ids)
|
).exclude(pk__in=used_question_ids)
|
||||||
|
|
||||||
if not available_questions.exists():
|
if not available_questions.exists():
|
||||||
return JsonResponse({"error": "No available questions in category"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("no_available_questions", "no_available_questions"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
question = random.choice(list(available_questions))
|
question = random.choice(list(available_questions))
|
||||||
round_question = RoundQuestion.objects.create(
|
round_question = RoundQuestion.objects.create(
|
||||||
@@ -473,13 +505,25 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
try:
|
try:
|
||||||
session = GameSession.objects.get(code=session_code)
|
session = GameSession.objects.get(code=session_code)
|
||||||
except GameSession.DoesNotExist:
|
except GameSession.DoesNotExist:
|
||||||
return JsonResponse({"error": "Session not found"}, status=404)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("session_not_found", "session_not_found"),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
if session.host_id != request.user.id:
|
if session.host_id != request.user.id:
|
||||||
return JsonResponse({"error": "Only host can mix answers"}, status=403)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("host_only_mix_answers", "host_only_mix_answers"),
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
if session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return JsonResponse({"error": "Answers can only be mixed in lie or guess phase"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
round_question = RoundQuestion.objects.get(
|
round_question = RoundQuestion.objects.get(
|
||||||
@@ -488,12 +532,20 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
round_number=session.current_round,
|
round_number=session.current_round,
|
||||||
)
|
)
|
||||||
except RoundQuestion.DoesNotExist:
|
except RoundQuestion.DoesNotExist:
|
||||||
return JsonResponse({"error": "Round question not found"}, status=404)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("round_question_not_found", "round_question_not_found"),
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
locked_session = GameSession.objects.select_for_update().get(pk=session.pk)
|
||||||
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
if locked_session.status not in {GameSession.Status.LIE, GameSession.Status.GUESS}:
|
||||||
return JsonResponse({"error": "Answers can only be mixed in lie or guess phase"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("mix_answers_invalid_phase", "mix_answers_invalid_phase"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
locked_round_question = RoundQuestion.objects.select_for_update().get(pk=round_question.pk)
|
locked_round_question = RoundQuestion.objects.select_for_update().get(pk=round_question.pk)
|
||||||
|
|
||||||
@@ -509,7 +561,11 @@ def mix_answers(request: HttpRequest, code: str, round_question_id: int) -> Json
|
|||||||
deduped_answers.append(text.strip())
|
deduped_answers.append(text.strip())
|
||||||
|
|
||||||
if len(deduped_answers) < 2:
|
if len(deduped_answers) < 2:
|
||||||
return JsonResponse({"error": "Not enough answers to mix"}, status=400)
|
return api_error(
|
||||||
|
request,
|
||||||
|
key=ERROR_CODES.get("not_enough_answers_to_mix", "not_enough_answers_to_mix"),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
random.shuffle(deduped_answers)
|
random.shuffle(deduped_answers)
|
||||||
locked_round_question.mixed_answers = deduped_answers
|
locked_round_question.mixed_answers = deduped_answers
|
||||||
|
|||||||
@@ -281,7 +281,18 @@
|
|||||||
"category_slug_required": "category_slug_required",
|
"category_slug_required": "category_slug_required",
|
||||||
"category_not_found": "category_not_found",
|
"category_not_found": "category_not_found",
|
||||||
"round_start_invalid_phase": "round_start_invalid_phase",
|
"round_start_invalid_phase": "round_start_invalid_phase",
|
||||||
"round_already_configured": "round_already_configured"
|
"round_already_configured": "round_already_configured",
|
||||||
|
"category_has_no_questions": "category_has_no_questions",
|
||||||
|
"show_question_invalid_phase": "show_question_invalid_phase",
|
||||||
|
"round_config_missing": "round_config_missing",
|
||||||
|
"question_already_shown": "question_already_shown",
|
||||||
|
"no_available_questions": "no_available_questions",
|
||||||
|
"mix_answers_invalid_phase": "mix_answers_invalid_phase",
|
||||||
|
"round_question_not_found": "round_question_not_found",
|
||||||
|
"not_enough_answers_to_mix": "not_enough_answers_to_mix",
|
||||||
|
"host_only_start_round": "host_only_start_round",
|
||||||
|
"host_only_show_question": "host_only_show_question",
|
||||||
|
"host_only_mix_answers": "host_only_mix_answers"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"session_code_required": {
|
"session_code_required": {
|
||||||
@@ -319,6 +330,50 @@
|
|||||||
"round_already_configured": {
|
"round_already_configured": {
|
||||||
"en": "Round already configured",
|
"en": "Round already configured",
|
||||||
"da": "Runden er allerede konfigureret"
|
"da": "Runden er allerede konfigureret"
|
||||||
|
},
|
||||||
|
"category_has_no_questions": {
|
||||||
|
"en": "Category has no active questions",
|
||||||
|
"da": "Kategorien har ingen aktive spørgsmål"
|
||||||
|
},
|
||||||
|
"show_question_invalid_phase": {
|
||||||
|
"en": "Question can only be shown in lie phase",
|
||||||
|
"da": "Spørgsmålet kan kun vises i løgnefasen"
|
||||||
|
},
|
||||||
|
"round_config_missing": {
|
||||||
|
"en": "Round config missing",
|
||||||
|
"da": "Rundekonfiguration mangler"
|
||||||
|
},
|
||||||
|
"question_already_shown": {
|
||||||
|
"en": "Question already shown for this round",
|
||||||
|
"da": "Spørgsmålet er allerede vist for denne runde"
|
||||||
|
},
|
||||||
|
"no_available_questions": {
|
||||||
|
"en": "No available questions in category",
|
||||||
|
"da": "Ingen tilgængelige spørgsmål i kategorien"
|
||||||
|
},
|
||||||
|
"mix_answers_invalid_phase": {
|
||||||
|
"en": "Answers can only be mixed in lie or guess phase",
|
||||||
|
"da": "Svar kan kun blandes i løgne- eller gættefasen"
|
||||||
|
},
|
||||||
|
"round_question_not_found": {
|
||||||
|
"en": "Round question not found",
|
||||||
|
"da": "Rundespørgsmål blev ikke fundet"
|
||||||
|
},
|
||||||
|
"not_enough_answers_to_mix": {
|
||||||
|
"en": "Not enough answers to mix",
|
||||||
|
"da": "Ikke nok svar at blande"
|
||||||
|
},
|
||||||
|
"host_only_start_round": {
|
||||||
|
"en": "Only host can start round",
|
||||||
|
"da": "Kun værten kan starte runden"
|
||||||
|
},
|
||||||
|
"host_only_show_question": {
|
||||||
|
"en": "Only host can show question",
|
||||||
|
"da": "Kun værten kan vise spørgsmålet"
|
||||||
|
},
|
||||||
|
"host_only_mix_answers": {
|
||||||
|
"en": "Only host can mix answers",
|
||||||
|
"da": "Kun værten kan blande svar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -345,7 +400,18 @@
|
|||||||
"category_slug_required": "start_round_failed",
|
"category_slug_required": "start_round_failed",
|
||||||
"category_not_found": "start_round_failed",
|
"category_not_found": "start_round_failed",
|
||||||
"round_start_invalid_phase": "start_round_failed",
|
"round_start_invalid_phase": "start_round_failed",
|
||||||
"round_already_configured": "start_round_failed"
|
"round_already_configured": "start_round_failed",
|
||||||
|
"host_only_start_round": "start_round_failed",
|
||||||
|
"host_only_show_question": "start_round_failed",
|
||||||
|
"host_only_mix_answers": "start_round_failed",
|
||||||
|
"category_has_no_questions": "start_round_failed",
|
||||||
|
"show_question_invalid_phase": "start_round_failed",
|
||||||
|
"round_config_missing": "start_round_failed",
|
||||||
|
"question_already_shown": "start_round_failed",
|
||||||
|
"no_available_questions": "start_round_failed",
|
||||||
|
"mix_answers_invalid_phase": "start_round_failed",
|
||||||
|
"round_question_not_found": "start_round_failed",
|
||||||
|
"not_enough_answers_to_mix": "start_round_failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user