from datetime import timedelta from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from django.utils import timezone from fupogfakta.models import ( Category, GameSession, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ) User = get_user_model() class LobbyFlowTests(TestCase): def setUp(self): self.host = User.objects.create_user(username="host", password="secret123") def test_create_session_requires_login(self): response = self.client.post(reverse("lobby:create_session")) self.assertEqual(response.status_code, 302) self.assertEqual(GameSession.objects.count(), 0) def test_host_can_create_session(self): self.client.login(username="host", password="secret123") response = self.client.post(reverse("lobby:create_session")) self.assertEqual(response.status_code, 201) body = response.json() self.assertEqual(body["session"]["status"], GameSession.Status.LOBBY) self.assertEqual(len(body["session"]["code"]), 6) session = GameSession.objects.get(code=body["session"]["code"]) self.assertEqual(session.host, self.host) def test_player_can_join_with_code(self): session = GameSession.objects.create(host=self.host, code="ABCD23") response = self.client.post( reverse("lobby:join_session"), data={"code": "abcd23", "nickname": "Luna"}, content_type="application/json", ) self.assertEqual(response.status_code, 201) body = response.json() self.assertEqual(body["session"]["code"], "ABCD23") self.assertEqual(body["player"]["nickname"], "Luna") self.assertTrue(Player.objects.filter(session=session, nickname="Luna").exists()) def test_join_rejects_duplicate_nickname_case_insensitive(self): session = GameSession.objects.create(host=self.host, code="QWER12") Player.objects.create(session=session, nickname="Luna") response = self.client.post( reverse("lobby:join_session"), data={"code": "QWER12", "nickname": "lUna"}, content_type="application/json", ) self.assertEqual(response.status_code, 409) self.assertEqual(response.json()["error"], "Nickname already taken") def test_join_rejects_non_joinable_session(self): GameSession.objects.create(host=self.host, code="ZXCV98", status=GameSession.Status.FINISHED) response = self.client.post( reverse("lobby:join_session"), data={"code": "ZXCV98", "nickname": "Kai"}, content_type="application/json", ) self.assertEqual(response.status_code, 400) self.assertEqual(response.json()["error"], "Session is not joinable") def test_session_detail_returns_players(self): session = GameSession.objects.create(host=self.host, code="LMNO45") Player.objects.create(session=session, nickname="Mia", score=7) Player.objects.create(session=session, nickname="Bo", score=2) response = self.client.get(reverse("lobby:session_detail", kwargs={"code": "lmno45"})) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["session"]["players_count"], 2) self.assertEqual([p["nickname"] for p in payload["players"]], ["Bo", "Mia"]) class StartRoundTests(TestCase): def setUp(self): self.host = User.objects.create_user(username="host", password="secret123") self.other_user = User.objects.create_user(username="other", password="secret123") self.session = GameSession.objects.create(host=self.host, code="ABCD23") self.category = Category.objects.create(name="Historie", slug="historie", is_active=True) Question.objects.create( category=self.category, prompt="Hvilket år faldt muren?", correct_answer="1989", is_active=True, ) def test_host_can_start_round_with_selected_category(self): 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, 201) body = response.json() self.assertEqual(body["session"]["status"], GameSession.Status.LIE) self.assertEqual(body["round"]["number"], 1) self.assertEqual(body["round"]["category"]["slug"], self.category.slug) self.session.refresh_from_db() self.assertEqual(self.session.status, GameSession.Status.LIE) round_config = RoundConfig.objects.get(session=self.session, number=1) self.assertEqual(round_config.category, self.category) def test_start_round_requires_host(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", ) self.assertEqual(response.status_code, 403) self.assertEqual(response.json()["error"], "Only host can start round") def test_start_round_requires_existing_active_category_with_questions(self): self.client.login(username="host", password="secret123") response = self.client.post( reverse("lobby:start_round", kwargs={"code": self.session.code}), data={"category_slug": "ukendt"}, content_type="application/json", ) self.assertEqual(response.status_code, 404) empty_category = Category.objects.create(name="Sport", slug="sport", is_active=True) response = self.client.post( reverse("lobby:start_round", kwargs={"code": self.session.code}), data={"category_slug": empty_category.slug}, content_type="application/json", ) self.assertEqual(response.status_code, 400) self.assertEqual(response.json()["error"], "Category has no active questions") def test_start_round_rejects_non_lobby_session(self): self.client.login(username="host", password="secret123") self.session.status = GameSession.Status.GUESS self.session.save(update_fields=["status"]) 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"], "Round can only be started from lobby") class LieSubmissionTests(TestCase): def setUp(self): self.host = User.objects.create_user(username="host", password="secret123") self.session = GameSession.objects.create(host=self.host, code="ABCD23", status=GameSession.Status.LIE) self.category = Category.objects.create(name="Geografi", slug="geografi", is_active=True) self.question = Question.objects.create( category=self.category, prompt="Hvad er hovedstaden i Australien?", correct_answer="Canberra", is_active=True, ) RoundConfig.objects.create( session=self.session, number=1, category=self.category, lie_seconds=45, ) self.player = Player.objects.create(session=self.session, nickname="Luna") def test_host_can_show_question_and_get_lie_deadline(self): self.client.login(username="host", password="secret123") response = self.client.post(reverse("lobby:show_question", kwargs={"code": self.session.code})) self.assertEqual(response.status_code, 201) payload = response.json() self.assertEqual(payload["config"]["lie_seconds"], 45) self.assertIn("lie_deadline_at", payload["round_question"]) self.assertTrue(RoundQuestion.objects.filter(session=self.session, round_number=1).exists()) def test_player_can_submit_lie_before_deadline(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, "text": "Sydney"}, content_type="application/json", ) self.assertEqual(response.status_code, 201) self.assertTrue(LieAnswer.objects.filter(round_question=round_question, player=self.player).exists()) def test_submit_lie_rejects_after_time_window(self): round_question = RoundQuestion.objects.create( session=self.session, round_number=1, question=self.question, correct_answer=self.question.correct_answer, ) round_question.shown_at = timezone.now() - timedelta(seconds=46) round_question.save(update_fields=["shown_at"]) 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, "text": "Melbourne"}, content_type="application/json", ) self.assertEqual(response.status_code, 400) self.assertEqual(response.json()["error"], "Lie submission window has closed") def test_submit_lie_rejects_duplicate_submission(self): round_question = RoundQuestion.objects.create( session=self.session, round_number=1, question=self.question, correct_answer=self.question.correct_answer, ) LieAnswer.objects.create(round_question=round_question, player=self.player, text="Perth") 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, "text": "Brisbane"}, content_type="application/json", ) self.assertEqual(response.status_code, 409) self.assertEqual(response.json()["error"], "Lie already submitted for this player") class MixAnswersTests(TestCase): def setUp(self): self.host = User.objects.create_user(username="host", password="secret123") self.other_user = User.objects.create_user(username="other", password="secret123") self.session = GameSession.objects.create(host=self.host, code="ABCD23", status=GameSession.Status.LIE) self.category = Category.objects.create(name="Historie", slug="historie", is_active=True) self.question = Question.objects.create( category=self.category, prompt="Hvilken by er Danmarks hovedstad?", correct_answer="København", is_active=True, ) RoundConfig.objects.create(session=self.session, number=1, category=self.category) self.round_question = RoundQuestion.objects.create( session=self.session, round_number=1, question=self.question, correct_answer="København", ) self.player_one = Player.objects.create(session=self.session, nickname="Luna") self.player_two = Player.objects.create(session=self.session, nickname="Mads") def test_host_can_mix_answers_and_transition_to_guess(self): LieAnswer.objects.create(round_question=self.round_question, player=self.player_one, text="Aarhus") LieAnswer.objects.create(round_question=self.round_question, player=self.player_two, text="Odense") self.client.login(username="host", password="secret123") response = self.client.post( reverse( "lobby:mix_answers", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ) ) self.assertEqual(response.status_code, 200) payload = response.json() answer_texts = [entry["text"] for entry in payload["answers"]] self.assertEqual(set(answer_texts), {"København", "Aarhus", "Odense"}) self.assertEqual(payload["session"]["status"], GameSession.Status.GUESS) self.session.refresh_from_db() self.assertEqual(self.session.status, GameSession.Status.GUESS) def test_mix_answers_requires_host(self): self.client.login(username="other", password="secret123") response = self.client.post( reverse( "lobby:mix_answers", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ) ) self.assertEqual(response.status_code, 403) self.assertEqual(response.json()["error"], "Only host can mix answers") def test_mix_answers_deduplicates_case_insensitive_lies(self): LieAnswer.objects.create(round_question=self.round_question, player=self.player_one, text="københavn") LieAnswer.objects.create(round_question=self.round_question, player=self.player_two, text="Aarhus") self.client.login(username="host", password="secret123") response = self.client.post( reverse( "lobby:mix_answers", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ) ) self.assertEqual(response.status_code, 200) answer_texts = [entry["text"] for entry in response.json()["answers"]] self.assertEqual(set(answer_texts), {"København", "Aarhus"})