From 97b366d1e9ee487f776ada96fd9a8f96e9486db4 Mon Sep 17 00:00:00 2001 From: DEV-bot Date: Sun, 15 Mar 2026 08:05:21 +0000 Subject: [PATCH] fix(gameplay): make scoreboard reads idempotent --- lobby/tests.py | 16 ++++++++++------ lobby/views.py | 9 +++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lobby/tests.py b/lobby/tests.py index d11b541..5b01b5d 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -813,15 +813,17 @@ class RevealRoundFlowTests(TestCase): self.assertEqual(response.status_code, 403) self.assertEqual(response.json()["error"], "Only host can view scoreboard") - def test_reveal_scoreboard_rejects_scoreboard_phase(self): + def test_reveal_scoreboard_is_idempotent_in_scoreboard_phase(self): self.session.status = GameSession.Status.SCOREBOARD self.session.save(update_fields=["status"]) self.client.login(username="host_reveal", password="secret123") response = self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code})) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()["error"], "Scoreboard is only available in reveal phase") + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD) + self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"]) def test_host_can_finish_game_from_scoreboard(self): self.client.login(username="host_reveal", password="secret123") @@ -891,7 +893,7 @@ class RevealRoundFlowTests(TestCase): self.assertEqual(self.session.status, GameSession.Status.LOBBY) self.assertEqual(self.session.current_round, 2) - def test_reveal_scoreboard_rejects_repeated_reads_after_promotion(self): + def test_reveal_scoreboard_allows_repeated_reads_after_promotion(self): self.client.login(username="host_reveal", password="secret123") first_response = self.client.get( @@ -908,8 +910,10 @@ class RevealRoundFlowTests(TestCase): ) self.assertEqual(first_response.status_code, 200) - self.assertEqual(second_response.status_code, 400) - self.assertEqual(second_response.json()["error"], "Scoreboard is only available in reveal phase") + self.assertEqual(second_response.status_code, 200) + self.assertEqual(first_response.json()["session"]["status"], GameSession.Status.SCOREBOARD) + self.assertEqual(second_response.json()["session"]["status"], GameSession.Status.SCOREBOARD) + self.assertEqual([item["nickname"] for item in second_response.json()["leaderboard"]], ["Luna", "Mads"]) def test_start_next_round_rejects_wrong_phase(self): self.client.login(username="host_reveal", password="secret123") diff --git a/lobby/views.py b/lobby/views.py index 0aa1ea6..e8ba0f1 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -714,11 +714,12 @@ def reveal_scoreboard(request: HttpRequest, code: str) -> JsonResponse: with transaction.atomic(): locked_session = GameSession.objects.select_for_update().get(pk=session.pk) - if locked_session.status != GameSession.Status.REVEAL: - return JsonResponse({"error": "Scoreboard is only available in reveal phase"}, status=400) + if locked_session.status not in {GameSession.Status.REVEAL, GameSession.Status.SCOREBOARD}: + return JsonResponse({"error": "Scoreboard is only available in reveal/scoreboard phase"}, status=400) - locked_session.status = GameSession.Status.SCOREBOARD - locked_session.save(update_fields=["status"]) + if locked_session.status == GameSession.Status.REVEAL: + locked_session.status = GameSession.Status.SCOREBOARD + locked_session.save(update_fields=["status"]) leaderboard = list( Player.objects.filter(session=session)