from datetime import timedelta from unittest.mock import patch from django.test import TestCase from django.urls import reverse from django.utils import timezone from apps.lyric.models import Token, User from apps.epic.models import ( DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard, ) class RoomCreationViewTest(TestCase): def setUp(self): self.user = User.objects.create(email="founder@test.io") self.client.force_login(self.user) def test_post_creates_room_and_redirects_to_gatekeeper(self): response = self.client.post( reverse("epic:create_room"), data={"name": "Test Room"}, ) room = Room.objects.get(owner=self.user) self.assertRedirects( response, reverse( "epic:gatekeeper", args=[room.id], ) ) def test_post_requires_login(self): self.client.logout() response = self.client.post( reverse("epic:create_room"), data={"name": "Test Room"}, ) def test_create_room_get_redirects_to_gameboard(self): response = self.client.get(reverse("epic:create_room")) self.assertRedirects(response, "/gameboard/") class MyGamesContextTest(TestCase): def setUp(self): self.user = User.objects.create(email="gamer@example.com") self.client.force_login(self.user) def test_gameboard_context_includes_owned_rooms(self): room = Room.objects.create(name="Durango", owner=self.user) response = self.client.get("/gameboard/") self.assertIn(room, response.context["my_games"]) def test_gameboard_context_includes_rooms_with_filled_slot(self): other = User.objects.create(email="friend@example.com") room = Room.objects.create(name="Their Room", owner=other) slot = room.gate_slots.get(slot_number=2) slot.gamer = self.user slot.status = "FILLED" slot.save() response = self.client.get("/gameboard/") self.assertIn(room, response.context["my_games"]) class GateStatusViewTest(TestCase): def setUp(self): self.owner = User.objects.create(email="founder@test.io") self.client.force_login(self.owner) self.room = Room.objects.create(name="Test Room", owner=self.owner) def test_gate_status_returns_launch_btn_when_open(self): self.room.gate_status = Room.OPEN self.room.save() response = self.client.get(reverse("epic:gate_status", kwargs={"room_id": self.room.id})) self.assertEqual(response.status_code, 200) self.assertContains(response, "launch-game-btn") def test_gate_status_returns_partial_when_gathering(self): response = self.client.get( reverse("epic:gate_status", kwargs={"room_id": self.room.id}) ) self.assertEqual(response.status_code, 200) self.assertContains(response, "gate-modal") class DropTokenViewTest(TestCase): def setUp(self): self.gamer = User.objects.create(email="gamer@test.io") self.client.force_login(self.gamer) owner = User.objects.create(email="owner@test.io") self.room = Room.objects.create(name="Test Room", owner=owner) def test_drop_token_reserves_lowest_empty_slot(self): self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id})) slot = self.room.gate_slots.get(slot_number=1) self.assertEqual(slot.status, GateSlot.RESERVED) self.assertEqual(slot.gamer, self.gamer) def test_drop_token_skips_already_filled_slots(self): other = User.objects.create(email="other@test.io") slot1 = self.room.gate_slots.get(slot_number=1) slot1.gamer = other slot1.status = GateSlot.FILLED slot1.save() self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id})) slot2 = self.room.gate_slots.get(slot_number=2) self.assertEqual(slot2.status, GateSlot.RESERVED) self.assertEqual(slot2.gamer, self.gamer) def test_drop_token_blocked_when_another_slot_reserved(self): other = User.objects.create(email="other@test.io") slot1 = self.room.gate_slots.get(slot_number=1) slot1.gamer = other slot1.status = GateSlot.RESERVED slot1.reserved_at = timezone.now() slot1.save() self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id})) # Slot 2 should remain EMPTY — lock held by other user slot2 = self.room.gate_slots.get(slot_number=2) self.assertEqual(slot2.status, GateSlot.EMPTY) def test_drop_token_blocked_when_user_already_has_filled_slot(self): slot1 = self.room.gate_slots.get(slot_number=1) slot1.gamer = self.gamer slot1.status = GateSlot.FILLED slot1.save() self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id})) slot2 = self.room.gate_slots.get(slot_number=2) self.assertEqual(slot2.status, GateSlot.EMPTY) def test_drop_token_sets_reserved_at(self): self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id})) slot = self.room.gate_slots.get(slot_number=1) self.assertIsNotNone(slot.reserved_at) def test_drop_token_redirects_to_gatekeeper(self): response = self.client.post( reverse("epic:drop_token", kwargs={"room_id": self.room.id}) ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) class ConfirmTokenViewTest(TestCase): def setUp(self): self.gamer = User.objects.create(email="gamer@test.io") self.client.force_login(self.gamer) owner = User.objects.create(email="owner@test.io") self.room = Room.objects.create(name="Test Room", owner=owner) self.slot = self.room.gate_slots.get(slot_number=1) self.slot.gamer = self.gamer self.slot.status = GateSlot.RESERVED self.slot.reserved_at = timezone.now() self.slot.save() Token.objects.create(user=self.gamer, token_type=Token.FREE) def test_confirm_marks_slot_filled(self): self.client.post( reverse("epic:confirm_token", kwargs={"room_id": self.room.id}) ) self.slot.refresh_from_db() self.assertEqual(self.slot.status, GateSlot.FILLED) def test_confirm_sets_gate_open_when_all_slots_filled(self): # Fill slots 2–6 via ORM for i in range(2, 7): other = User.objects.create(email=f"g{i}@test.io") s = self.room.gate_slots.get(slot_number=i) s.gamer = other s.status = GateSlot.FILLED s.save() self.client.post( reverse("epic:confirm_token", kwargs={"room_id": self.room.id}) ) self.room.refresh_from_db() self.assertEqual(self.room.gate_status, Room.OPEN) def test_confirm_redirects_to_gatekeeper(self): response = self.client.post( reverse("epic:confirm_token", kwargs={"room_id": self.room.id}) ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) def test_confirm_does_nothing_without_reserved_slot(self): self.slot.status = GateSlot.EMPTY self.slot.gamer = None self.slot.save() self.client.post( reverse("epic:confirm_token", kwargs={"room_id": self.room.id}) ) self.slot.refresh_from_db() self.assertEqual(self.slot.status, GateSlot.EMPTY) class ReturnTokenViewTest(TestCase): def setUp(self): self.gamer = User.objects.create(email="gamer@test.io") self.client.force_login(self.gamer) owner = User.objects.create(email="owner@test.io") self.room = Room.objects.create(name="Test Room", owner=owner) self.slot = self.room.gate_slots.get(slot_number=1) self.slot.gamer = self.gamer self.slot.status = GateSlot.RESERVED self.slot.reserved_at = timezone.now() self.slot.save() def test_return_clears_reserved_slot(self): self.client.post( reverse("epic:return_token", kwargs={"room_id": self.room.id}) ) self.slot.refresh_from_db() self.assertEqual(self.slot.status, GateSlot.EMPTY) self.assertIsNone(self.slot.gamer) self.assertIsNone(self.slot.reserved_at) def test_return_after_confirm_clears_filled_slot(self): self.slot.status = GateSlot.FILLED self.slot.save() self.client.post( reverse("epic:return_token", kwargs={"room_id": self.room.id}) ) self.slot.refresh_from_db() self.assertEqual(self.slot.status, GateSlot.EMPTY) self.assertIsNone(self.slot.gamer) def test_return_redirects_to_gatekeeper(self): response = self.client.post( reverse("epic:return_token", kwargs={"room_id": self.room.id}) ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) def test_return_restores_coin_token(self): coin = Token.objects.get(user=self.gamer, token_type=Token.COIN) coin.current_room = self.room coin.next_ready_at = timezone.now() + timedelta(days=7) coin.save() self.slot.status = GateSlot.FILLED self.slot.debited_token_type = Token.COIN self.slot.save() self.client.post( reverse("epic:return_token", kwargs={"room_id": self.room.id}) ) coin.refresh_from_db() self.assertIsNone(coin.current_room) self.assertIsNone(coin.next_ready_at) def test_return_restores_free_token(self): Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete() expires = timezone.now() + timedelta(days=3) self.slot.status = GateSlot.FILLED self.slot.debited_token_type = Token.FREE self.slot.debited_token_expires_at = expires self.slot.save() self.client.post( reverse("epic:return_token", kwargs={"room_id": self.room.id}) ) restored = Token.objects.filter(user=self.gamer, token_type=Token.FREE).first() self.assertIsNotNone(restored) self.assertEqual(restored.expires_at, expires) def test_return_restores_tithe_token(self): self.slot.status = GateSlot.FILLED self.slot.debited_token_type = Token.TITHE self.slot.save() self.client.post( reverse("epic:return_token", kwargs={"room_id": self.room.id}) ) self.assertTrue( Token.objects.filter(user=self.gamer, token_type=Token.TITHE).exists() ) class DropTokenAvailabilityViewTest(TestCase): def setUp(self): self.gamer = User.objects.create(email="gamer@test.io") self.client.force_login(self.gamer) owner = User.objects.create(email="owner@test.io") self.room = Room.objects.create(name="Test Room", owner=owner) self.other_room = Room.objects.create(name="Other Room", owner=owner) self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN) def test_drop_reserves_slot_when_tokens_available(self): self.client.post(reverse("epic:drop_token", kwargs={"room_id": self.room.id})) slot = self.room.gate_slots.get(slot_number=1) self.assertEqual(slot.status, GateSlot.RESERVED) # token not debited yet — that happens at confirm self.coin.refresh_from_db() self.assertIsNone(self.coin.current_room) def test_drop_returns_402_when_all_tokens_depleted(self): self.coin.current_room = self.other_room self.coin.save() Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete() response = self.client.post( reverse("epic:drop_token", kwargs={"room_id": self.room.id}) ) self.assertEqual(response.status_code, 402) class ConfirmTokenPriorityViewTest(TestCase): def setUp(self): self.gamer = User.objects.create(email="gamer@test.io") self.client.force_login(self.gamer) owner = User.objects.create(email="owner@test.io") self.room = Room.objects.create(name="Test Room", owner=owner) self.other_room = Room.objects.create(name="Other Room", owner=owner) self.slot = self.room.gate_slots.get(slot_number=1) self.slot.gamer = self.gamer self.slot.status = GateSlot.RESERVED self.slot.reserved_at = timezone.now() self.slot.save() self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN) def test_confirm_leases_coin_to_room(self): self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id})) self.coin.refresh_from_db() self.assertEqual(self.coin.current_room, self.room) self.assertTrue(Token.objects.filter(pk=self.coin.pk).exists()) def test_confirm_uses_free_token_when_coin_in_use(self): self.coin.current_room = self.other_room self.coin.save() self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id})) self.assertEqual( Token.objects.filter(user=self.gamer, token_type=Token.FREE).count(), 0 ) self.coin.refresh_from_db() self.assertEqual(self.coin.current_room, self.other_room) def test_confirm_uses_tithe_when_free_tokens_exhausted(self): self.coin.current_room = self.other_room self.coin.save() Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete() tithe = Token.objects.create(user=self.gamer, token_type=Token.TITHE) self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id})) self.assertFalse(Token.objects.filter(pk=tithe.pk).exists()) def test_pass_not_consumed_and_coin_not_leased(self): self.gamer.is_staff = True self.gamer.save() pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS) self.client.post(reverse("epic:confirm_token", kwargs={"room_id": self.room.id})) self.assertTrue(Token.objects.filter(pk=pass_token.pk).exists()) self.coin.refresh_from_db() self.assertIsNone(self.coin.current_room) class RoleSelectRenderingTest(TestCase): def setUp(self): self.founder = User.objects.create(email="founder@test.io") self.room = Room.objects.create(name="Test Room", owner=self.founder) self.gamers = [self.founder] for i in range(2, 7): self.gamers.append(User.objects.create(email=f"g{i}@test.io")) for i, gamer in enumerate(self.gamers, start=1): slot = self.room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() self.room.gate_status = Room.OPEN self.room.table_status = Room.ROLE_SELECT self.room.save() for i, gamer in enumerate(self.gamers, start=1): TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i) def test_room_view_includes_card_stack_when_role_select(self): self.client.force_login(self.founder) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, "card-stack") def test_card_stack_eligible_for_slot1_gamer(self): self.client.force_login(self.founder) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, 'data-state="eligible"') def test_card_stack_ineligible_for_slot2_gamer(self): self.client.force_login(self.gamers[1]) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, 'data-state="ineligible"') def test_card_stack_ineligible_shows_fa_ban(self): self.client.force_login(self.gamers[1]) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, "fa-ban") def test_card_stack_eligible_omits_fa_ban(self): self.client.force_login(self.founder) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertNotContains(response, "fa-ban") def test_gatekeeper_overlay_absent_when_role_select(self): self.client.force_login(self.founder) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertNotContains(response, "gate-overlay") def test_six_table_seats_rendered(self): self.client.force_login(self.founder) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, "table-seat", count=6) def test_active_table_seat_has_active_class(self): self.client.force_login(self.founder) # slot 1 is active response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, 'class="table-seat active"') def test_inactive_table_seat_lacks_active_class(self): self.client.force_login(self.founder) response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) # Slots 2–6 are not active, so at least one plain table-seat exists self.assertContains(response, 'class="table-seat"') def test_card_stack_has_data_user_slots_for_eligible_gamer(self): self.client.force_login(self.founder) # founder is slot 1 only response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, 'data-user-slots="1"') def test_card_stack_has_data_user_slots_for_ineligible_gamer(self): self.client.force_login(self.gamers[1]) # slot 2 gamer response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, 'data-user-slots="2"') class PickRolesViewTest(TestCase): def setUp(self): self.founder = User.objects.create(email="founder@test.io") self.client.force_login(self.founder) self.room = Room.objects.create(name="Test Room", owner=self.founder) for i in range(1, 7): gamer = self.founder if i == 1 else User.objects.create(email=f"g{i}@test.io") slot = self.room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() self.room.gate_status = Room.OPEN self.room.save() def test_pick_roles_transitions_room_to_role_select(self): self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id})) self.room.refresh_from_db() self.assertEqual(self.room.table_status, Room.ROLE_SELECT) def test_pick_roles_creates_one_table_seat_per_filled_slot(self): self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id})) self.assertEqual(TableSeat.objects.filter(room=self.room).count(), 6) def test_pick_roles_table_seats_carry_gamer_and_slot_number(self): self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id})) seat = TableSeat.objects.get(room=self.room, slot_number=1) self.assertEqual(seat.gamer, self.founder) def test_only_open_room_can_start_role_select(self): self.room.gate_status = Room.GATHERING self.room.save() self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id})) self.room.refresh_from_db() self.assertIsNone(self.room.table_status) def test_pick_roles_requires_login(self): self.client.logout() response = self.client.post( reverse("epic:pick_roles", kwargs={"room_id": self.room.id}) ) self.assertEqual(response.status_code, 302) self.assertIn("/accounts/login/", response.url) def test_pick_roles_redirects_to_room(self): response = self.client.post( reverse("epic:pick_roles", kwargs={"room_id": self.room.id}) ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) def test_pick_roles_notifies_channel_layer(self): with patch("apps.epic.views._notify_role_select_start") as mock_notify: self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id})) mock_notify.assert_called_once_with(self.room.id) def test_pick_roles_idempotent_no_duplicate_seats(self): url = reverse("epic:pick_roles", kwargs={"room_id": self.room.id}) self.client.post(url) self.client.post(url) # second call must be a no-op self.assertEqual(TableSeat.objects.filter(room=self.room).count(), 6) class SelectRoleViewTest(TestCase): def setUp(self): self.founder = User.objects.create(email="founder@test.io") self.room = Room.objects.create(name="Test Room", owner=self.founder) self.gamers = [self.founder] for i in range(2, 7): self.gamers.append(User.objects.create(email=f"g{i}@test.io")) for i, gamer in enumerate(self.gamers, start=1): slot = self.room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() self.room.gate_status = Room.OPEN self.room.table_status = Room.ROLE_SELECT self.room.save() for i, gamer in enumerate(self.gamers, start=1): TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i) self.client.force_login(self.founder) def test_select_role_records_choice(self): self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) seat = TableSeat.objects.get(room=self.room, slot_number=1) self.assertEqual(seat.role, "PC") def test_select_role_wrong_turn_makes_no_change(self): self.client.force_login(self.gamers[1]) # slot 2 — not their turn self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "BC"}, ) seat = TableSeat.objects.get(room=self.room, slot_number=2) self.assertIsNone(seat.role) def test_turn_advances_after_selection(self): self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) next_active = TableSeat.objects.filter( room=self.room, role__isnull=True ).order_by("slot_number").first() self.assertEqual(next_active.slot_number, 2) def test_all_selected_sets_sig_select(self): roles = ["PC", "BC", "SC", "AC", "NC"] for i, role in enumerate(roles): seat = TableSeat.objects.get(room=self.room, slot_number=i + 1) seat.role = role seat.save() self.client.force_login(self.gamers[5]) # slot 6 — last self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "EC"}, ) self.room.refresh_from_db() self.assertEqual(self.room.table_status, Room.SIG_SELECT) def test_select_role_notifies_turn_changed(self): with patch("apps.epic.views._notify_turn_changed") as mock_notify: self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) mock_notify.assert_called_once_with(self.room.id) def test_select_role_notifies_roles_revealed_when_last(self): roles = ["PC", "BC", "SC", "AC", "NC"] for i, role in enumerate(roles): seat = TableSeat.objects.get(room=self.room, slot_number=i + 1) seat.role = role seat.save() self.client.force_login(self.gamers[5]) with patch("apps.epic.views._notify_roles_revealed") as mock_notify: self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "EC"}, ) mock_notify.assert_called_once_with(self.room.id) def test_select_role_requires_login(self): self.client.logout() response = self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) self.assertEqual(response.status_code, 302) self.assertIn("/accounts/login/", response.url) def test_select_role_returns_ok(self): response = self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) self.assertEqual(response.status_code, 200) def test_select_role_returns_409_for_duplicate_role(self): TableSeat.objects.filter(room=self.room, slot_number=2).update(role="BC") response = self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "BC"}, ) self.assertEqual(response.status_code, 409) def test_select_role_redirects_when_not_role_select_phase(self): self.room.table_status = None self.room.save() response = self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) def test_select_role_redirects_for_invalid_role_code(self): response = self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "BOGUS"}, ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) def test_same_gamer_cannot_double_pick_sequentially(self): """A second POST from the active gamer — after their role has been saved — must redirect rather than assign a second role.""" self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "PC"}, ) response = self.client.post( reverse("epic:select_role", kwargs={"room_id": self.room.id}), data={"role": "BC"}, ) self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) self.assertEqual( TableSeat.objects.filter(room=self.room, role__isnull=False).count(), 1 ) class RevealPhaseRenderingTest(TestCase): def setUp(self): self.founder = User.objects.create(email="founder@test.io") self.room = Room.objects.create(name="Test Room", owner=self.founder) gamers = [self.founder] for i in range(2, 7): gamers.append(User.objects.create(email=f"g{i}@test.io")) roles = ["PC", "BC", "SC", "AC", "NC", "EC"] for i, (gamer, role) in enumerate(zip(gamers, roles), start=1): TableSeat.objects.create( room=self.room, gamer=gamer, slot_number=i, role=role, role_revealed=True, ) self.room.gate_status = Room.OPEN self.room.table_status = Room.SIG_SELECT self.room.save() self.client.force_login(self.founder) def test_face_up_role_cards_rendered_when_sig_select(self): response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, "face-up") def test_inv_role_card_slot_present(self): response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, "id_inv_role_card") def test_partner_indicator_present_when_sig_select(self): response = self.client.get( reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) ) self.assertContains(response, "partner-indicator") class RoomActionsViewTest(TestCase): def setUp(self): self.owner = User.objects.create(email="owner@test.io") self.gamer = User.objects.create(email="gamer@test.io") self.room = Room.objects.create(name="Test Room", owner=self.owner) self.slot = self.room.gate_slots.get(slot_number=2) self.slot.gamer = self.gamer self.slot.status = "FILLED" self.slot.save() RoomInvite.objects.create( room=self.room, inviter=self.owner, invitee_email=self.gamer.email ) def test_owner_delete_removes_room(self): self.client.force_login(self.owner) self.client.post(reverse("epic:delete_room", kwargs={"room_id": self.room.id})) self.assertFalse(Room.objects.filter(pk=self.room.pk).exists()) def test_non_owner_delete_does_not_remove_room(self): self.client.force_login(self.gamer) self.client.post(reverse("epic:delete_room", kwargs={"room_id": self.room.id})) self.assertTrue(Room.objects.filter(pk=self.room.pk).exists()) def test_delete_redirects_to_gameboard(self): self.client.force_login(self.owner) response = self.client.post( reverse("epic:delete_room", kwargs={"room_id": self.room.id}) ) self.assertRedirects(response, "/gameboard/") def test_abandon_clears_slot(self): self.client.force_login(self.gamer) self.client.post(reverse("epic:abandon_room", kwargs={"room_id": self.room.id})) self.slot.refresh_from_db() self.assertEqual(self.slot.status, "EMPTY") self.assertIsNone(self.slot.gamer) def test_abandon_deletes_pending_invite(self): self.client.force_login(self.gamer) self.client.post(reverse("epic:abandon_room", kwargs={"room_id": self.room.id})) self.assertFalse( RoomInvite.objects.filter( room=self.room, invitee_email=self.gamer.email ).exists() ) def test_abandon_redirects_to_gameboard(self): self.client.force_login(self.gamer) response = self.client.post( reverse("epic:abandon_room", kwargs={"room_id": self.room.id}) ) self.assertRedirects(response, "/gameboard/") class ReleaseSlotViewTest(TestCase): def setUp(self): self.gamer = User.objects.create(email="gamer@test.io") self.client.force_login(self.gamer) owner = User.objects.create(email="owner@test.io") self.room = Room.objects.create(name="Test Room", owner=owner) self.slot = self.room.gate_slots.get(slot_number=1) self.slot.gamer = self.gamer self.slot.status = GateSlot.FILLED self.slot.debited_token_type = Token.CARTE self.slot.save() def test_release_slot_downgrades_open_room_to_gathering(self): self.room.gate_status = Room.OPEN self.room.save() self.client.post( reverse("epic:release_slot", kwargs={"room_id": self.room.id}), data={"slot_number": self.slot.slot_number}, ) self.room.refresh_from_db() self.assertEqual(self.room.gate_status, Room.GATHERING) # ── Significator Selection ──────────────────────────────────────────────────── SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"] def _full_sig_setUp(test_case, role_order=None): """Populate test_case with a SIG_SELECT room; return (room, gamers, earthman, card_in_deck). Uses get_or_create for DeckVariant — migration data persists in TestCase.""" if role_order is None: role_order = SIG_SEAT_ORDER[:] earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) founder = User.objects.create(email="founder@test.io") gamers = [founder] for i in range(2, 7): gamers.append(User.objects.create(email=f"g{i}@test.io")) for gamer in gamers: gamer.equipped_deck = earthman gamer.save(update_fields=["equipped_deck"]) room = Room.objects.create(name="Sig Room", owner=founder) for i, (gamer, role) in enumerate(zip(gamers, role_order), start=1): slot = room.gate_slots.get(slot_number=i) slot.gamer = gamer slot.status = GateSlot.FILLED slot.save() TableSeat.objects.create( room=room, gamer=gamer, slot_number=i, role=role, role_revealed=True, ) room.gate_status = Room.OPEN room.table_status = Room.SIG_SELECT room.save() card_in_deck = TarotCard.objects.get( deck_variant=earthman, arcana="MINOR", suit="WANDS", number=11 ) test_case.client.force_login(founder) return room, gamers, earthman, card_in_deck class SigSelectRenderingTest(TestCase): """Gate view at SIG_SELECT renders the Significator deck.""" def setUp(self): self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self) self.url = reverse("epic:gatekeeper", kwargs={"room_id": self.room.id}) def test_sig_deck_element_present(self): response = self.client.get(self.url) self.assertContains(response, "id_sig_deck") def test_sig_deck_contains_36_sig_cards(self): response = self.client.get(self.url) self.assertEqual(response.content.decode().count('class="sig-card"'), 36) def test_seats_rendered_in_pc_nc_ec_sc_ac_bc_order(self): response = self.client.get(self.url) content = response.content.decode() positions = {role: content.find(f'data-role="{role}"') for role in SIG_SEAT_ORDER} # Every role must appear self.assertTrue(all(pos != -1 for pos in positions.values())) # Rendered in canonical sequence ordered = sorted(SIG_SEAT_ORDER, key=lambda r: positions[r]) self.assertEqual(ordered, SIG_SEAT_ORDER) def test_sig_deck_not_present_during_role_select(self): self.room.table_status = Room.ROLE_SELECT self.room.save() response = self.client.get(self.url) self.assertNotContains(response, "id_sig_deck") class SelectSigCardViewTest(TestCase): """select_sig view — records choice, enforces turn order, rejects bad input.""" def setUp(self): self.room, self.gamers, self.earthman, self.card = _full_sig_setUp(self) # Founder is slot 1, role=PC — active first in canonical order self.url = reverse("epic:select_sig", kwargs={"room_id": self.room.id}) def _post(self, card_id=None, client=None): c = client or self.client return c.post(self.url, data={"card_id": card_id or self.card.id}) def test_select_sig_records_choice_on_active_seat(self): self._post() seat = TableSeat.objects.get(room=self.room, role="PC") self.assertEqual(seat.significator, self.card) def test_select_sig_returns_200(self): response = self._post() self.assertEqual(response.status_code, 200) def test_select_sig_wrong_turn_makes_no_change(self): # Gamer 2 is NC — not their turn yet self.client.force_login(self.gamers[1]) self._post() seat = TableSeat.objects.get(room=self.room, role="NC") self.assertIsNone(seat.significator) def test_select_sig_wrong_turn_returns_403(self): self.client.force_login(self.gamers[1]) response = self._post() self.assertEqual(response.status_code, 403) def test_select_sig_card_not_in_deck_returns_400(self): # Create a pip card (number=5) — not in the sig deck (only court 11–14 + major 0–1) other = TarotCard.objects.create( deck_variant=self.earthman, arcana="MINOR", suit="WANDS", number=5, name="Five of Wands Test", slug="five-of-wands-test", keywords_upright=[], keywords_reversed=[], ) response = self._post(card_id=other.id) self.assertEqual(response.status_code, 400) def test_select_sig_card_already_taken_returns_409(self): # Another seat already holds this card as their significator nc_seat = TableSeat.objects.get(room=self.room, role="NC") nc_seat.significator = self.card nc_seat.save() response = self._post() self.assertEqual(response.status_code, 409) def test_select_sig_advances_active_seat_to_nc(self): self._post() from apps.epic.models import active_sig_seat seat = active_sig_seat(self.room) self.assertEqual(seat.role, "NC") def test_select_sig_notifies_ws(self): with patch("apps.epic.views._notify_sig_selected") as mock_notify: self._post() mock_notify.assert_called_once_with(self.room.id) def test_select_sig_requires_login(self): self.client.logout() response = self._post() self.assertEqual(response.status_code, 302) self.assertIn("/accounts/login/", response.url) def test_select_sig_wrong_phase_redirects(self): self.room.table_status = Room.ROLE_SELECT self.room.save() response = self._post() self.assertRedirects( response, reverse("epic:gatekeeper", args=[self.room.id]) ) def test_select_sig_last_choice_does_not_advance_to_none(self): """After all 6 significators chosen, active_sig_seat() is None — no unhandled AttributeError in the view.""" cards = list(TarotCard.objects.filter(deck_variant=self.earthman, arcana="MINOR")) seats_in_order = list( TableSeat.objects.filter(room=self.room).order_by("slot_number") ) # Assign all but the last (BC) manually for seat, card in zip(seats_in_order[:-1], cards): seat.significator = card seat.save() # BC gamer POSTs the final choice bc_seat = TableSeat.objects.get(room=self.room, role="BC") self.client.force_login(bc_seat.gamer) last_card = TarotCard.objects.filter( deck_variant=self.earthman, arcana="MAJOR", number=0 ).first() response = self.client.post(self.url, data={"card_id": last_card.id}) self.assertIn(response.status_code, (200, 302))