2026-03-13 17:31:52 -04:00
|
|
|
|
from datetime import timedelta
|
2026-03-13 18:37:19 -04:00
|
|
|
|
from django.db.models import Q
|
2026-03-13 00:31:17 -04:00
|
|
|
|
from django.test import TestCase
|
2026-03-13 17:31:52 -04:00
|
|
|
|
from django.urls import reverse
|
2026-03-14 22:00:16 -04:00
|
|
|
|
from django.utils import timezone
|
2026-03-13 00:31:17 -04:00
|
|
|
|
|
2026-03-13 17:31:52 -04:00
|
|
|
|
from apps.lyric.models import Token, User
|
2026-03-25 01:30:18 -04:00
|
|
|
|
from apps.epic.models import (
|
|
|
|
|
|
DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard,
|
|
|
|
|
|
debit_token, select_token, sig_deck_cards, sig_seat_order, active_sig_seat,
|
|
|
|
|
|
)
|
2026-03-13 00:31:17 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoomCreationTest(TestCase):
|
|
|
|
|
|
def test_creating_a_room_generates_six_gate_slots(self):
|
|
|
|
|
|
owner = User.objects.create(email="founder@example.com")
|
|
|
|
|
|
room = Room.objects.create(name="Test Room", owner=owner)
|
|
|
|
|
|
self.assertEqual(GateSlot.objects.filter(room=room).count(), 6)
|
2026-03-13 17:31:52 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DebitTokenTest(TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.owner = User.objects.create(email="founder@example.com")
|
|
|
|
|
|
self.room = Room.objects.create(
|
|
|
|
|
|
name="Test Room",
|
|
|
|
|
|
owner=self.owner,
|
|
|
|
|
|
renewal_period=timedelta(days=7)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
|
|
|
|
|
|
def test_debit_free_token_consumes_token_and_fills_slot(self):
|
|
|
|
|
|
free_token = Token.objects.get(user=self.owner, token_type=Token.FREE)
|
|
|
|
|
|
debit_token(self.owner, self.slot, free_token)
|
|
|
|
|
|
self.assertFalse(Token.objects.filter(pk=free_token.pk).exists())
|
|
|
|
|
|
self.slot.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.slot.status, GateSlot.FILLED)
|
|
|
|
|
|
self.assertEqual(self.slot.gamer, self.owner)
|
|
|
|
|
|
|
|
|
|
|
|
def test_debit_coin_does_not_consume_token(self):
|
|
|
|
|
|
coin_token = Token.objects.get(user=self.owner, token_type=Token.COIN)
|
|
|
|
|
|
debit_token(self.owner, self.slot, coin_token)
|
|
|
|
|
|
self.assertTrue(Token.objects.filter(pk=coin_token.pk).exists())
|
|
|
|
|
|
self.slot.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.slot.status, GateSlot.FILLED)
|
|
|
|
|
|
self.assertEqual(self.slot.gamer, self.owner)
|
|
|
|
|
|
|
2026-03-13 22:51:42 -04:00
|
|
|
|
def test_debit_fills_last_slot_and_opens_gate(self):
|
|
|
|
|
|
for i in range(2, 7):
|
|
|
|
|
|
gamer = 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()
|
|
|
|
|
|
free_token = Token.objects.get(user=self.owner, token_type=Token.FREE)
|
|
|
|
|
|
debit_token(self.owner, self.slot, free_token)
|
|
|
|
|
|
self.room.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.room.gate_status, Room.OPEN)
|
|
|
|
|
|
|
2026-03-13 17:31:52 -04:00
|
|
|
|
|
|
|
|
|
|
class CoinTokenInUseTest(TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.owner = User.objects.create(email="founder@example.com")
|
|
|
|
|
|
self.room = Room.objects.create(
|
|
|
|
|
|
name="Dragon's Den",
|
|
|
|
|
|
owner=self.owner,
|
|
|
|
|
|
renewal_period=timedelta(days=7),
|
|
|
|
|
|
)
|
|
|
|
|
|
self.slot = self.room.gate_slots.get(slot_number=1)
|
|
|
|
|
|
self.coin = Token.objects.get(user=self.owner, token_type=Token.COIN)
|
|
|
|
|
|
debit_token(self.owner, self.slot, self.coin)
|
|
|
|
|
|
self.coin.refresh_from_db()
|
|
|
|
|
|
|
|
|
|
|
|
def test_coin_tooltip_expiry_shows_next_ready_date(self):
|
|
|
|
|
|
expected_date = self.coin.next_ready_at.strftime("%Y-%m-%d")
|
|
|
|
|
|
self.assertIn(expected_date, self.coin.tooltip_expiry())
|
|
|
|
|
|
|
|
|
|
|
|
def test_coin_tooltip_room_html_contains_anchor(self):
|
|
|
|
|
|
room_url = reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
|
|
|
|
|
html = self.coin.tooltip_room_html()
|
|
|
|
|
|
self.assertIn(f'href="{room_url}"', html)
|
|
|
|
|
|
self.assertIn(self.room.name, html)
|
2026-03-13 18:37:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 22:00:16 -04:00
|
|
|
|
class SelectTokenTest(TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
|
self.other_room = Room.objects.create(name="Other Room", owner=self.user)
|
|
|
|
|
|
self.coin = Token.objects.get(user=self.user, token_type=Token.COIN)
|
|
|
|
|
|
|
|
|
|
|
|
def test_returns_coin_when_available(self):
|
|
|
|
|
|
token = select_token(self.user)
|
|
|
|
|
|
self.assertEqual(token.token_type, Token.COIN)
|
|
|
|
|
|
|
|
|
|
|
|
def test_returns_free_token_when_coin_in_use(self):
|
|
|
|
|
|
self.coin.current_room = self.other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
token = select_token(self.user)
|
|
|
|
|
|
self.assertEqual(token.token_type, Token.FREE)
|
|
|
|
|
|
|
|
|
|
|
|
def test_free_token_selection_is_fefo(self):
|
|
|
|
|
|
self.coin.current_room = self.other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
Token.objects.filter(user=self.user, token_type=Token.FREE).delete()
|
|
|
|
|
|
soon = Token.objects.create(
|
|
|
|
|
|
user=self.user, token_type=Token.FREE,
|
|
|
|
|
|
expires_at=timezone.now() + timedelta(days=2),
|
|
|
|
|
|
)
|
|
|
|
|
|
Token.objects.create(
|
|
|
|
|
|
user=self.user, token_type=Token.FREE,
|
|
|
|
|
|
expires_at=timezone.now() + timedelta(days=6),
|
|
|
|
|
|
)
|
|
|
|
|
|
token = select_token(self.user)
|
|
|
|
|
|
self.assertEqual(token.pk, soon.pk)
|
|
|
|
|
|
|
|
|
|
|
|
def test_returns_tithe_when_coin_in_use_and_no_free_tokens(self):
|
|
|
|
|
|
self.coin.current_room = self.other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
Token.objects.filter(user=self.user, token_type=Token.FREE).delete()
|
|
|
|
|
|
tithe = Token.objects.create(user=self.user, token_type=Token.TITHE)
|
|
|
|
|
|
token = select_token(self.user)
|
|
|
|
|
|
self.assertEqual(token.pk, tithe.pk)
|
|
|
|
|
|
|
|
|
|
|
|
def test_returns_none_when_all_depleted(self):
|
|
|
|
|
|
self.coin.current_room = self.other_room
|
|
|
|
|
|
self.coin.save()
|
|
|
|
|
|
Token.objects.filter(user=self.user, token_type=Token.FREE).delete()
|
|
|
|
|
|
token = select_token(self.user)
|
|
|
|
|
|
self.assertIsNone(token)
|
|
|
|
|
|
|
|
|
|
|
|
def test_returns_pass_for_staff(self):
|
|
|
|
|
|
self.user.is_staff = True
|
|
|
|
|
|
self.user.save()
|
|
|
|
|
|
pass_token = Token.objects.create(user=self.user, token_type=Token.PASS)
|
|
|
|
|
|
token = select_token(self.user)
|
|
|
|
|
|
self.assertEqual(token.token_type, Token.PASS)
|
|
|
|
|
|
|
|
|
|
|
|
|
Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
2026-03-17 00:24:23 -04:00
|
|
|
|
class RoomTableStatusTest(TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.owner = User.objects.create(email="founder@test.io")
|
|
|
|
|
|
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_status_defaults_to_blank(self):
|
|
|
|
|
|
self.room.refresh_from_db()
|
|
|
|
|
|
self.assertFalse(self.room.table_status)
|
|
|
|
|
|
|
|
|
|
|
|
def test_room_has_role_select_constant(self):
|
|
|
|
|
|
self.assertEqual(Room.ROLE_SELECT, "ROLE_SELECT")
|
|
|
|
|
|
|
|
|
|
|
|
def test_room_has_sig_select_constant(self):
|
|
|
|
|
|
self.assertEqual(Room.SIG_SELECT, "SIG_SELECT")
|
|
|
|
|
|
|
|
|
|
|
|
def test_room_has_in_game_constant(self):
|
|
|
|
|
|
self.assertEqual(Room.IN_GAME, "IN_GAME")
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_status_accepts_role_select(self):
|
|
|
|
|
|
self.room.table_status = Room.ROLE_SELECT
|
|
|
|
|
|
self.room.save()
|
|
|
|
|
|
self.room.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.room.table_status, Room.ROLE_SELECT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TableSeatModelTest(TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.owner = User.objects.create(email="founder@test.io")
|
|
|
|
|
|
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_seat_can_be_created(self):
|
|
|
|
|
|
seat = TableSeat.objects.create(
|
|
|
|
|
|
room=self.room,
|
|
|
|
|
|
gamer=self.owner,
|
|
|
|
|
|
slot_number=1,
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(seat.slot_number, 1)
|
|
|
|
|
|
self.assertIsNone(seat.role)
|
|
|
|
|
|
self.assertFalse(seat.role_revealed)
|
|
|
|
|
|
self.assertIsNone(seat.seat_position)
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_seat_role_choices_cover_all_six(self):
|
|
|
|
|
|
role_codes = [c[0] for c in TableSeat.ROLE_CHOICES]
|
|
|
|
|
|
for code in ["PC", "BC", "SC", "AC", "NC", "EC"]:
|
|
|
|
|
|
self.assertIn(code, role_codes)
|
|
|
|
|
|
|
|
|
|
|
|
def test_partner_map_pairs_are_mutual(self):
|
|
|
|
|
|
for a, b in [(TableSeat.PC, TableSeat.BC), (TableSeat.SC, TableSeat.AC), (TableSeat.NC, TableSeat.EC)]:
|
|
|
|
|
|
self.assertEqual(TableSeat.PARTNER_MAP[a], b)
|
|
|
|
|
|
self.assertEqual(TableSeat.PARTNER_MAP[b], a)
|
|
|
|
|
|
|
|
|
|
|
|
def test_room_table_seats_reverse_relation(self):
|
|
|
|
|
|
TableSeat.objects.create(room=self.room, gamer=self.owner, slot_number=1)
|
|
|
|
|
|
self.assertEqual(self.room.table_seats.count(), 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 18:37:19 -04:00
|
|
|
|
class RoomInviteTest(TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.founder = User.objects.create(email="founder@example.com")
|
|
|
|
|
|
self.room = Room.objects.create(name="Dragon's Den", owner=self.founder)
|
|
|
|
|
|
|
|
|
|
|
|
def test_founder_can_invite_by_email(self):
|
|
|
|
|
|
invite = RoomInvite.objects.create(
|
|
|
|
|
|
room=self.room,
|
|
|
|
|
|
inviter=self.founder,
|
|
|
|
|
|
invitee_email="friend@example.com",
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(invite.status, RoomInvite.PENDING)
|
|
|
|
|
|
|
|
|
|
|
|
def test_invited_room_appears_in_my_games_queryset(self):
|
|
|
|
|
|
friend = User.objects.create(email="friend@example.com")
|
|
|
|
|
|
RoomInvite.objects.create(
|
|
|
|
|
|
room=self.room,
|
|
|
|
|
|
inviter=self.founder,
|
|
|
|
|
|
invitee_email=friend.email,
|
|
|
|
|
|
)
|
|
|
|
|
|
rooms = Room.objects.filter(
|
|
|
|
|
|
Q(owner=friend) |
|
|
|
|
|
|
Q(gate_slots__gamer=friend) |
|
|
|
|
|
|
Q(invites__invitee_email=friend.email, invites__status=RoomInvite.PENDING)
|
|
|
|
|
|
).distinct()
|
|
|
|
|
|
self.assertIn(self.room, rooms)
|
2026-03-25 01:30:18 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Significator deck helpers ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _full_sig_room(name="Sig Room", role_order=None):
|
|
|
|
|
|
"""Return (room, gamers, earthman) with all 6 seats filled, roles assigned,
|
2026-03-25 01:50:06 -04:00
|
|
|
|
table_status=SIG_SELECT, and every gamer's equipped_deck set to Earthman.
|
|
|
|
|
|
Uses get_or_create for DeckVariant — migration data persists in TestCase."""
|
2026-03-25 01:30:18 -04:00
|
|
|
|
if role_order is None:
|
|
|
|
|
|
role_order = SIG_SEAT_ORDER[:]
|
2026-03-25 01:50:06 -04:00
|
|
|
|
earthman, _ = DeckVariant.objects.get_or_create(
|
|
|
|
|
|
slug="earthman",
|
|
|
|
|
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
2026-03-25 01:30:18 -04:00
|
|
|
|
)
|
|
|
|
|
|
owner = User.objects.create(email="founder@sig.io")
|
|
|
|
|
|
gamers = [owner]
|
|
|
|
|
|
for i in range(2, 7):
|
|
|
|
|
|
gamers.append(User.objects.create(email=f"g{i}@sig.io"))
|
|
|
|
|
|
for gamer in gamers:
|
|
|
|
|
|
gamer.equipped_deck = earthman
|
|
|
|
|
|
gamer.save(update_fields=["equipped_deck"])
|
|
|
|
|
|
room = Room.objects.create(name=name, owner=owner)
|
|
|
|
|
|
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.table_status = Room.SIG_SELECT
|
|
|
|
|
|
room.save()
|
|
|
|
|
|
return room, gamers, earthman
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SigDeckCompositionTest(TestCase):
|
|
|
|
|
|
"""sig_deck_cards(room) returns exactly 36 cards with correct suit/arcana split."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.room, self.gamers, self.earthman = _full_sig_room()
|
|
|
|
|
|
|
|
|
|
|
|
def test_sig_deck_returns_36_cards(self):
|
|
|
|
|
|
cards = sig_deck_cards(self.room)
|
|
|
|
|
|
self.assertEqual(len(cards), 36)
|
|
|
|
|
|
|
|
|
|
|
|
def test_sc_ac_contribute_court_cards_of_swords_and_cups(self):
|
|
|
|
|
|
cards = sig_deck_cards(self.room)
|
|
|
|
|
|
sc_ac = [c for c in cards if c.suit in ("SWORDS", "CUPS")]
|
|
|
|
|
|
# M/J/Q/K × 2 suits × 2 roles = 16
|
|
|
|
|
|
self.assertEqual(len(sc_ac), 16)
|
|
|
|
|
|
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac))
|
|
|
|
|
|
|
|
|
|
|
|
def test_pc_bc_contribute_court_cards_of_wands_and_pentacles(self):
|
|
|
|
|
|
cards = sig_deck_cards(self.room)
|
|
|
|
|
|
pc_bc = [c for c in cards if c.suit in ("WANDS", "PENTACLES")]
|
|
|
|
|
|
self.assertEqual(len(pc_bc), 16)
|
|
|
|
|
|
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc))
|
|
|
|
|
|
|
|
|
|
|
|
def test_nc_ec_contribute_schiz_and_chancellor(self):
|
|
|
|
|
|
cards = sig_deck_cards(self.room)
|
|
|
|
|
|
major = [c for c in cards if c.arcana == "MAJOR"]
|
|
|
|
|
|
self.assertEqual(len(major), 4)
|
|
|
|
|
|
self.assertEqual(sorted(c.number for c in major), [0, 0, 1, 1])
|
|
|
|
|
|
|
|
|
|
|
|
def test_each_card_appears_twice_once_per_pile(self):
|
|
|
|
|
|
"""18 unique card specs × 2 (levity + gravity) = 36 total."""
|
|
|
|
|
|
cards = sig_deck_cards(self.room)
|
|
|
|
|
|
slugs = [c.slug for c in cards]
|
|
|
|
|
|
unique_slugs = set(slugs)
|
|
|
|
|
|
self.assertEqual(len(unique_slugs), 18)
|
|
|
|
|
|
self.assertTrue(all(slugs.count(s) == 2 for s in unique_slugs))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SigSeatOrderTest(TestCase):
|
|
|
|
|
|
"""sig_seat_order() and active_sig_seat() return seats in PC→NC→EC→SC→AC→BC order."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
# Assign roles in reverse of canonical order to prove reordering works
|
|
|
|
|
|
self.room, self.gamers, _ = _full_sig_room(
|
|
|
|
|
|
name="Order Room",
|
|
|
|
|
|
role_order=["BC", "AC", "SC", "EC", "NC", "PC"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_sig_seat_order_returns_canonical_role_sequence(self):
|
|
|
|
|
|
seats = sig_seat_order(self.room)
|
|
|
|
|
|
self.assertEqual([s.role for s in seats], SIG_SEAT_ORDER)
|
|
|
|
|
|
|
|
|
|
|
|
def test_active_sig_seat_is_first_seat_without_significator(self):
|
|
|
|
|
|
seat = active_sig_seat(self.room)
|
|
|
|
|
|
self.assertEqual(seat.role, "PC")
|
|
|
|
|
|
|
|
|
|
|
|
def test_active_sig_seat_advances_after_significator_set(self):
|
|
|
|
|
|
pc_seat = TableSeat.objects.get(room=self.room, role="PC")
|
|
|
|
|
|
earthman = DeckVariant.objects.get(slug="earthman")
|
|
|
|
|
|
card = TarotCard.objects.filter(deck_variant=earthman, arcana="MINOR").first()
|
|
|
|
|
|
pc_seat.significator = card
|
|
|
|
|
|
pc_seat.save()
|
|
|
|
|
|
seat = active_sig_seat(self.room)
|
|
|
|
|
|
self.assertEqual(seat.role, "NC")
|
|
|
|
|
|
|
|
|
|
|
|
def test_active_sig_seat_is_none_when_all_chosen(self):
|
|
|
|
|
|
earthman = DeckVariant.objects.get(slug="earthman")
|
|
|
|
|
|
cards = list(TarotCard.objects.filter(deck_variant=earthman))
|
|
|
|
|
|
for i, seat in enumerate(TableSeat.objects.filter(room=self.room)):
|
|
|
|
|
|
seat.significator = cards[i]
|
|
|
|
|
|
seat.save()
|
|
|
|
|
|
self.assertIsNone(active_sig_seat(self.room))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SigCardFieldTest(TestCase):
|
|
|
|
|
|
"""TableSeat.significator FK to TarotCard — default null, assignable."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
2026-03-25 01:50:06 -04:00
|
|
|
|
earthman, _ = DeckVariant.objects.get_or_create(
|
|
|
|
|
|
slug="earthman",
|
|
|
|
|
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
2026-03-25 01:30:18 -04:00
|
|
|
|
)
|
2026-03-25 01:50:06 -04:00
|
|
|
|
self.card = TarotCard.objects.get(
|
2026-03-25 01:30:18 -04:00
|
|
|
|
deck_variant=earthman, arcana="MINOR", suit="WANDS", number=11,
|
|
|
|
|
|
)
|
|
|
|
|
|
owner = User.objects.create(email="owner@test.io")
|
|
|
|
|
|
room = Room.objects.create(name="Field Test", owner=owner)
|
|
|
|
|
|
self.seat = TableSeat.objects.create(room=room, gamer=owner, slot_number=1, role="PC")
|
|
|
|
|
|
|
|
|
|
|
|
def test_significator_defaults_to_none(self):
|
|
|
|
|
|
self.assertIsNone(self.seat.significator)
|
|
|
|
|
|
|
|
|
|
|
|
def test_significator_can_be_assigned(self):
|
|
|
|
|
|
self.seat.significator = self.card
|
|
|
|
|
|
self.seat.save()
|
|
|
|
|
|
self.seat.refresh_from_db()
|
|
|
|
|
|
self.assertEqual(self.seat.significator, self.card)
|
|
|
|
|
|
|
|
|
|
|
|
def test_significator_nullable_on_delete(self):
|
|
|
|
|
|
self.seat.significator = self.card
|
|
|
|
|
|
self.seat.save()
|
|
|
|
|
|
self.card.delete()
|
|
|
|
|
|
self.seat.refresh_from_db()
|
|
|
|
|
|
self.assertIsNone(self.seat.significator)
|