merge: rebase canonical reveal flow onto main
This commit is contained in:
223
lobby/tests.py
223
lobby/tests.py
@@ -100,6 +100,21 @@ class LobbyFlowTests(TestCase):
|
||||
self.assertEqual(response.status_code, 409)
|
||||
self.assertEqual(response.json()["error"], "Nickname already taken")
|
||||
|
||||
def test_player_can_join_during_scoreboard_phase(self):
|
||||
session = GameSession.objects.create(host=self.host, code="ZXCV97", status=GameSession.Status.SCOREBOARD)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("lobby:join_session"),
|
||||
data={"code": "ZXCV97", "nickname": "Kai"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
body = response.json()
|
||||
self.assertEqual(body["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||
self.assertEqual(body["player"]["nickname"], "Kai")
|
||||
self.assertTrue(Player.objects.filter(session=session, nickname="Kai").exists())
|
||||
|
||||
def test_join_rejects_non_joinable_session(self):
|
||||
GameSession.objects.create(host=self.host, code="ZXCV98", status=GameSession.Status.FINISHED)
|
||||
|
||||
@@ -360,9 +375,9 @@ class LieSubmissionTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error_code"], "lie_submission_window_closed")
|
||||
self.assertEqual(response.json()["error_code"], "lie_submission_closed")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Lie submission window has closed.")
|
||||
self.assertEqual(response.json()["error"], "Lie submission window has closed")
|
||||
|
||||
def test_submit_lie_rejects_duplicate_submission(self):
|
||||
round_question = RoundQuestion.objects.create(
|
||||
@@ -385,7 +400,7 @@ class LieSubmissionTests(TestCase):
|
||||
self.assertEqual(response.status_code, 409)
|
||||
self.assertEqual(response.json()["error_code"], "lie_already_submitted")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Lie has already been submitted for this player.")
|
||||
self.assertEqual(response.json()["error"], "Lie already submitted for this player")
|
||||
|
||||
def test_submit_lie_requires_session_token(self):
|
||||
round_question = RoundQuestion.objects.create(
|
||||
@@ -407,7 +422,7 @@ class LieSubmissionTests(TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error_code"], "session_token_required")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Session token is required.")
|
||||
self.assertEqual(response.json()["error"], "session_token is required")
|
||||
|
||||
def test_submit_lie_rejects_invalid_session_token(self):
|
||||
round_question = RoundQuestion.objects.create(
|
||||
@@ -429,7 +444,30 @@ class LieSubmissionTests(TestCase):
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "invalid_player_session_token")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Player session token is invalid.")
|
||||
self.assertEqual(response.json()["error"], "Invalid player session token")
|
||||
|
||||
def test_submit_lie_uses_danish_locale_payload_from_accept_language(self):
|
||||
round_question = RoundQuestion.objects.create(
|
||||
session=self.session,
|
||||
round_number=1,
|
||||
question=self.question,
|
||||
correct_answer=self.question.correct_answer,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"lobby:submit_lie",
|
||||
kwargs={"code": self.session.code, "round_question_id": round_question.id},
|
||||
),
|
||||
data={"player_id": self.player.id, "session_token": "invalid-token", "text": "Sydney"},
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT_LANGUAGE="da-DK,da;q=0.9,en;q=0.1",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "invalid_player_session_token")
|
||||
self.assertEqual(response.json()["locale"], "da")
|
||||
self.assertEqual(response.json()["error"], "Ugyldigt spiller-session-token")
|
||||
|
||||
class MixAnswersTests(TestCase):
|
||||
def setUp(self):
|
||||
@@ -592,7 +630,7 @@ class GuessSubmissionTests(TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error_code"], "guess_submission_invalid_phase")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Guess submission is only allowed in guess phase.")
|
||||
self.assertEqual(response.json()["error"], "Guess submission is only allowed in guess phase")
|
||||
|
||||
def test_submit_guess_rejects_unknown_answer(self):
|
||||
response = self.client.post(
|
||||
@@ -606,7 +644,8 @@ class GuessSubmissionTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error_code"], "selected_answer_invalid")
|
||||
self.assertEqual(response.json()["error"], "Selected answer is not part of this round.")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Selected answer is not part of this round")
|
||||
|
||||
def test_submit_guess_rejects_duplicate_submission(self):
|
||||
Guess.objects.create(round_question=self.round_question, player=self.player, selected_text="Mars", is_correct=True)
|
||||
@@ -622,7 +661,8 @@ class GuessSubmissionTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 409)
|
||||
self.assertEqual(response.json()["error_code"], "guess_already_submitted")
|
||||
self.assertEqual(response.json()["error"], "Guess has already been submitted for this player.")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Guess already submitted for this player")
|
||||
|
||||
def test_submit_guess_rejects_after_deadline(self):
|
||||
self.round_question.shown_at = timezone.now() - timedelta(seconds=76)
|
||||
@@ -638,8 +678,9 @@ class GuessSubmissionTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error_code"], "guess_submission_window_closed")
|
||||
self.assertEqual(response.json()["error"], "Guess submission window has closed.")
|
||||
self.assertEqual(response.json()["error_code"], "guess_submission_closed")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Guess submission window has closed")
|
||||
|
||||
|
||||
|
||||
@@ -654,7 +695,9 @@ class GuessSubmissionTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "Session token is required.")
|
||||
self.assertEqual(response.json()["error_code"], "session_token_required")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "session_token is required")
|
||||
|
||||
def test_submit_guess_rejects_invalid_session_token(self):
|
||||
response = self.client.post(
|
||||
@@ -668,7 +711,8 @@ class GuessSubmissionTests(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "invalid_player_session_token")
|
||||
self.assertEqual(response.json()["error"], "Player session token is invalid.")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Invalid player session token")
|
||||
|
||||
|
||||
class ScoreCalculationTests(TestCase):
|
||||
@@ -795,7 +839,9 @@ class ScoreCalculationTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error"], "Only the host can calculate scores.")
|
||||
self.assertEqual(response.json()["error_code"], "host_only_calculate_scores")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Only host can calculate scores")
|
||||
|
||||
def test_calculate_scores_rejects_duplicate_calculation(self):
|
||||
Guess.objects.create(round_question=self.round_question, player=self.player_one, selected_text="Tennis", is_correct=True)
|
||||
@@ -816,18 +862,21 @@ class ScoreCalculationTests(TestCase):
|
||||
|
||||
self.assertEqual(first.status_code, 200)
|
||||
self.assertEqual(second.status_code, 409)
|
||||
self.assertEqual(second.json()["error"], "Scores have already been calculated for this round question.")
|
||||
self.assertEqual(second.json()["error_code"], "scores_already_calculated")
|
||||
self.assertEqual(second.json()["locale"], "en")
|
||||
self.assertEqual(second.json()["error"], "Scores already calculated for this round question")
|
||||
|
||||
|
||||
class RevealRoundFlowTests(TestCase):
|
||||
def setUp(self):
|
||||
self.host = User.objects.create_user(username="host_reveal", password="secret123")
|
||||
self.other_user = User.objects.create_user(username="other_reveal", password="secret123")
|
||||
self.session = GameSession.objects.create(host=self.host, code="RVL123", status=GameSession.Status.SCOREBOARD)
|
||||
self.session = GameSession.objects.create(host=self.host, code="RVL123", status=GameSession.Status.REVEAL)
|
||||
self.player_one = Player.objects.create(session=self.session, nickname="Luna", score=9)
|
||||
self.player_two = Player.objects.create(session=self.session, nickname="Mads", score=3)
|
||||
|
||||
def test_host_can_get_reveal_scoreboard(self):
|
||||
@patch("lobby.views.sync_broadcast_phase_event")
|
||||
def test_host_can_get_reveal_scoreboard(self, mock_sync_broadcast_phase_event):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
|
||||
response = self.client.get(
|
||||
@@ -841,6 +890,20 @@ class RevealRoundFlowTests(TestCase):
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||
self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"])
|
||||
mock_sync_broadcast_phase_event.assert_called_once_with(
|
||||
self.session.code,
|
||||
"phase.scoreboard",
|
||||
{
|
||||
"leaderboard": [
|
||||
{"id": self.player_one.id, "nickname": "Luna", "score": 9},
|
||||
{"id": self.player_two.id, "nickname": "Mads", "score": 3},
|
||||
],
|
||||
"current_round": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.session.refresh_from_db()
|
||||
self.assertEqual(self.session.status, GameSession.Status.SCOREBOARD)
|
||||
|
||||
def test_reveal_scoreboard_requires_host(self):
|
||||
self.client.login(username="other_reveal", password="secret123")
|
||||
@@ -849,15 +912,32 @@ class RevealRoundFlowTests(TestCase):
|
||||
reverse(
|
||||
"lobby:reveal_scoreboard",
|
||||
kwargs={"code": self.session.code},
|
||||
)
|
||||
),
|
||||
HTTP_ACCEPT_LANGUAGE="fr",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error"], "Only the host can view the scoreboard.")
|
||||
self.assertEqual(response.json()["error_code"], "host_only_view_scoreboard")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Only host can view scoreboard")
|
||||
|
||||
def test_host_can_finish_game_from_scoreboard(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, 200)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["session"]["status"], GameSession.Status.SCOREBOARD)
|
||||
self.assertEqual([item["nickname"] for item in payload["leaderboard"]], ["Luna", "Mads"])
|
||||
|
||||
@patch("lobby.views.sync_broadcast_phase_event")
|
||||
def test_host_can_finish_game_from_scoreboard(self, _mock_sync_broadcast_phase_event):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"lobby:finish_game",
|
||||
@@ -881,11 +961,14 @@ class RevealRoundFlowTests(TestCase):
|
||||
reverse(
|
||||
"lobby:finish_game",
|
||||
kwargs={"code": self.session.code},
|
||||
)
|
||||
),
|
||||
HTTP_ACCEPT_LANGUAGE="da",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error"], "Only the host can finish the game.")
|
||||
self.assertEqual(response.json()["error_code"], "host_only_finish_game")
|
||||
self.assertEqual(response.json()["locale"], "da")
|
||||
self.assertEqual(response.json()["error"], "Kun værten kan afslutte spillet")
|
||||
|
||||
def test_finish_game_rejects_wrong_phase(self):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
@@ -896,14 +979,19 @@ class RevealRoundFlowTests(TestCase):
|
||||
reverse(
|
||||
"lobby:finish_game",
|
||||
kwargs={"code": self.session.code},
|
||||
)
|
||||
),
|
||||
HTTP_ACCEPT_LANGUAGE="fr",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "Game can only be finished from scoreboard phase.")
|
||||
self.assertEqual(response.json()["error_code"], "finish_game_invalid_phase")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Game can only be finished from scoreboard phase")
|
||||
|
||||
def test_host_can_start_next_round_from_scoreboard(self):
|
||||
@patch("lobby.views.sync_broadcast_phase_event")
|
||||
def test_host_can_start_next_round_from_scoreboard(self, _mock_sync_broadcast_phase_event):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
self.client.get(reverse("lobby:reveal_scoreboard", kwargs={"code": self.session.code}))
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
@@ -921,6 +1009,50 @@ class RevealRoundFlowTests(TestCase):
|
||||
self.assertEqual(self.session.status, GameSession.Status.LOBBY)
|
||||
self.assertEqual(self.session.current_round, 2)
|
||||
|
||||
def test_start_next_round_requires_host(self):
|
||||
self.session.status = GameSession.Status.SCOREBOARD
|
||||
self.session.save(update_fields=["status"])
|
||||
self.client.login(username="other_reveal", password="secret123")
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"lobby:start_next_round",
|
||||
kwargs={"code": self.session.code},
|
||||
),
|
||||
HTTP_ACCEPT_LANGUAGE="fr",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json(), {
|
||||
"error": "Only host can start next round",
|
||||
"error_code": "host_only_start_next_round",
|
||||
"locale": "en",
|
||||
})
|
||||
|
||||
@patch("lobby.views.sync_broadcast_phase_event")
|
||||
def test_reveal_scoreboard_allows_repeated_reads_after_promotion(self, mock_sync_broadcast_phase_event):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
|
||||
first_response = self.client.get(
|
||||
reverse(
|
||||
"lobby:reveal_scoreboard",
|
||||
kwargs={"code": self.session.code},
|
||||
)
|
||||
)
|
||||
second_response = self.client.get(
|
||||
reverse(
|
||||
"lobby:reveal_scoreboard",
|
||||
kwargs={"code": self.session.code},
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
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"])
|
||||
self.assertEqual(mock_sync_broadcast_phase_event.call_count, 1)
|
||||
|
||||
def test_start_next_round_rejects_wrong_phase(self):
|
||||
self.client.login(username="host_reveal", password="secret123")
|
||||
self.session.status = GameSession.Status.GUESS
|
||||
@@ -930,11 +1062,30 @@ class RevealRoundFlowTests(TestCase):
|
||||
reverse(
|
||||
"lobby:start_next_round",
|
||||
kwargs={"code": self.session.code},
|
||||
)
|
||||
),
|
||||
HTTP_ACCEPT_LANGUAGE="da",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "Next round can only start from scoreboard phase.")
|
||||
self.assertEqual(response.json()["error_code"], "next_round_invalid_phase")
|
||||
self.assertEqual(response.json()["locale"], "da")
|
||||
self.assertEqual(response.json()["error"], "Næste runde kan kun starte fra scoreboard-fasen")
|
||||
|
||||
def test_reveal_scoreboard_unsupported_locale_falls_back_to_en_deterministically(self):
|
||||
self.client.login(username="other_reveal", password="secret123")
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"lobby:reveal_scoreboard",
|
||||
kwargs={"code": self.session.code},
|
||||
),
|
||||
HTTP_ACCEPT_LANGUAGE="fr-FR,fr;q=0.9",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.json()["error_code"], "host_only_view_scoreboard")
|
||||
self.assertEqual(response.json()["locale"], "en")
|
||||
self.assertEqual(response.json()["error"], "Only host can view scoreboard")
|
||||
|
||||
class UiScreenTests(TestCase):
|
||||
def setUp(self):
|
||||
@@ -1150,6 +1301,17 @@ class UiScreenTests(TestCase):
|
||||
self.assertContains(response, "data-wpp-shell-route=\"/host/guess/round-1\"")
|
||||
self.assertContains(response, "data-wpp-shell-kind=\"host\"")
|
||||
|
||||
def test_host_screen_template_registers_scoreboard_shell_route(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
|
||||
response = self.client.get(reverse("lobby:host_screen"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
'var HOST_SHELL_ROUTES={lobby:"lobby",lie:"lie",guess:"guess",reveal:"reveal",scoreboard:"scoreboard",finished:"finished"};',
|
||||
)
|
||||
|
||||
@override_settings(USE_SPA_UI=True)
|
||||
def test_host_screen_deeplink_normalizes_redundant_slashes_when_feature_flag_enabled(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
@@ -1496,6 +1658,15 @@ class I18nResolverTests(TestCase):
|
||||
self.assertEqual(result, "missing_key")
|
||||
self.assertTrue(any("i18n key missing in shared catalog" in entry for entry in logs.output))
|
||||
|
||||
def test_missing_backend_error_code_is_logged_with_context(self):
|
||||
from lobby.i18n import resolve_error_key
|
||||
|
||||
with self.assertLogs("lobby.i18n", level="WARNING") as logs:
|
||||
result = resolve_error_key("missing_code")
|
||||
|
||||
self.assertEqual(result, "missing_code")
|
||||
self.assertTrue(any("i18n error code missing in shared catalog" in entry for entry in logs.output))
|
||||
|
||||
def test_missing_locale_translation_falls_back_to_default_locale(self):
|
||||
with patch(
|
||||
"lobby.i18n.lobby_i18n_error_messages",
|
||||
|
||||
Reference in New Issue
Block a user