From 242aeaacd6b02c1908ab69e2e10bcd8abee28f3c Mon Sep 17 00:00:00 2001 From: DEV-bot Date: Mon, 16 Mar 2026 04:22:45 +0000 Subject: [PATCH] fix(lobby): avoid orphaned round configs on round start --- lobby/tests.py | 37 +++++++++++++++++++++++++++++++++++++ lobby/views.py | 22 +++++++++++++--------- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/lobby/tests.py b/lobby/tests.py index 2a64f4f..dead0e8 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -308,6 +308,23 @@ class StartRoundTests(TestCase): self.assertEqual(response.json()["locale"], "en") self.assertEqual(response.json()["error"], "Only host can start round") + @patch("lobby.views._select_round_question", side_effect=ValueError("no_available_questions")) + def test_start_round_does_not_persist_round_config_when_question_selection_fails(self, _mock_select_round_question): + self.client.login(username="host", 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", + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error_code"], "no_available_questions") + self.session.refresh_from_db() + self.assertEqual(self.session.status, GameSession.Status.LOBBY) + self.assertFalse(RoundConfig.objects.filter(session=self.session, number=1).exists()) + self.assertFalse(RoundQuestion.objects.filter(session=self.session, round_number=1).exists()) + class LieSubmissionTests(TestCase): def setUp(self): @@ -1329,6 +1346,26 @@ class RevealRoundFlowTests(TestCase): self.assertEqual(response.json()["locale"], "da") self.assertEqual(response.json()["error"], "Næste runde kan kun starte fra scoreboard-fasen") + def test_start_next_round_does_not_persist_round_config_when_question_selection_fails(self): + self.client.login(username="host_reveal", password="secret123") + self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code})) + self.next_question.delete() + + response = self.client.post( + reverse( + "lobby:start_next_round", + kwargs={"code": self.session.code}, + ) + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error_code"], "no_available_questions") + self.session.refresh_from_db() + self.assertEqual(self.session.status, GameSession.Status.SCOREBOARD) + self.assertEqual(self.session.current_round, 1) + self.assertFalse(RoundConfig.objects.filter(session=self.session, number=2).exists()) + self.assertFalse(RoundQuestion.objects.filter(session=self.session, round_number=2).exists()) + def test_reveal_scoreboard_unsupported_locale_falls_back_to_en_deterministically(self): self.client.login(username="other_reveal", password="secret123") diff --git a/lobby/views.py b/lobby/views.py index f092fc9..31024fa 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -528,23 +528,25 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse: status=400, ) - round_config, created = RoundConfig.objects.get_or_create( - session=session, - number=session.current_round, - defaults={"category": category}, - ) - if not created: + if RoundConfig.objects.filter(session=session, number=session.current_round).exists(): return api_error( request, code="round_already_configured", status=409, ) + round_config = RoundConfig( + session=session, + number=session.current_round, + category=category, + ) + try: round_question = _select_round_question(session, round_config) except ValueError as exc: return api_error(request, code=str(exc), status=400) + round_config.save() session.status = GameSession.Status.LIE session.save(update_fields=["status"]) @@ -1101,22 +1103,24 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse: if previous_round_config is None: return api_error(request, code="round_config_missing", status=400) - locked_session.current_round += 1 - next_round_config = RoundConfig.objects.create( + next_round_number = locked_session.current_round + 1 + next_round_config = RoundConfig( session=locked_session, - number=locked_session.current_round, + number=next_round_number, category=previous_round_config.category, lie_seconds=previous_round_config.lie_seconds, guess_seconds=previous_round_config.guess_seconds, points_correct=previous_round_config.points_correct, points_bluff=previous_round_config.points_bluff, ) + locked_session.current_round = next_round_number try: round_question = _select_round_question(locked_session, next_round_config) except ValueError as exc: return api_error(request, code=str(exc), status=400) + next_round_config.save() locked_session.status = GameSession.Status.LIE locked_session.save(update_fields=["current_round", "status"])