fix(lobby): avoid orphaned round configs on round start
All checks were successful
CI / test-and-quality (pull_request) Successful in 3m6s
CI / test-and-quality (push) Successful in 3m8s

This commit is contained in:
2026-03-16 04:22:45 +00:00
parent 624bcd602b
commit 242aeaacd6
2 changed files with 50 additions and 9 deletions

View File

@@ -308,6 +308,23 @@ class StartRoundTests(TestCase):
self.assertEqual(response.json()["locale"], "en") 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")
@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): class LieSubmissionTests(TestCase):
def setUp(self): def setUp(self):
@@ -1329,6 +1346,26 @@ class RevealRoundFlowTests(TestCase):
self.assertEqual(response.json()["locale"], "da") self.assertEqual(response.json()["locale"], "da")
self.assertEqual(response.json()["error"], "Næste runde kan kun starte fra scoreboard-fasen") 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): def test_reveal_scoreboard_unsupported_locale_falls_back_to_en_deterministically(self):
self.client.login(username="other_reveal", password="secret123") self.client.login(username="other_reveal", password="secret123")

View File

@@ -528,23 +528,25 @@ def start_round(request: HttpRequest, code: str) -> JsonResponse:
status=400, status=400,
) )
round_config, created = RoundConfig.objects.get_or_create( if RoundConfig.objects.filter(session=session, number=session.current_round).exists():
session=session,
number=session.current_round,
defaults={"category": category},
)
if not created:
return api_error( return api_error(
request, request,
code="round_already_configured", code="round_already_configured",
status=409, status=409,
) )
round_config = RoundConfig(
session=session,
number=session.current_round,
category=category,
)
try: try:
round_question = _select_round_question(session, round_config) round_question = _select_round_question(session, round_config)
except ValueError as exc: except ValueError as exc:
return api_error(request, code=str(exc), status=400) return api_error(request, code=str(exc), status=400)
round_config.save()
session.status = GameSession.Status.LIE session.status = GameSession.Status.LIE
session.save(update_fields=["status"]) session.save(update_fields=["status"])
@@ -1101,22 +1103,24 @@ def start_next_round(request: HttpRequest, code: str) -> JsonResponse:
if previous_round_config is None: if previous_round_config is None:
return api_error(request, code="round_config_missing", status=400) return api_error(request, code="round_config_missing", status=400)
locked_session.current_round += 1 next_round_number = locked_session.current_round + 1
next_round_config = RoundConfig.objects.create( next_round_config = RoundConfig(
session=locked_session, session=locked_session,
number=locked_session.current_round, number=next_round_number,
category=previous_round_config.category, category=previous_round_config.category,
lie_seconds=previous_round_config.lie_seconds, lie_seconds=previous_round_config.lie_seconds,
guess_seconds=previous_round_config.guess_seconds, guess_seconds=previous_round_config.guess_seconds,
points_correct=previous_round_config.points_correct, points_correct=previous_round_config.points_correct,
points_bluff=previous_round_config.points_bluff, points_bluff=previous_round_config.points_bluff,
) )
locked_session.current_round = next_round_number
try: try:
round_question = _select_round_question(locked_session, next_round_config) round_question = _select_round_question(locked_session, next_round_config)
except ValueError as exc: except ValueError as exc:
return api_error(request, code=str(exc), status=400) return api_error(request, code=str(exc), status=400)
next_round_config.save()
locked_session.status = GameSession.Status.LIE locked_session.status = GameSession.Status.LIE
locked_session.save(update_fields=["current_round", "status"]) locked_session.save(update_fields=["current_round", "status"])