100 lines
4.5 KiB
Python
100 lines
4.5 KiB
Python
|
|
"""UT for the `SeaInvite` status machine + derived properties — Phase A1 of
|
||
|
|
[[my-sea-invite-voice-blueprint]].
|
||
|
|
|
||
|
|
`SeaInvite` is the single source of truth for the my-sea bud-invite
|
||
|
|
relationship. These tests pin the four derived properties the OK/BYE render,
|
||
|
|
seat-2 glow, spectator guard, and voice window all read from:
|
||
|
|
|
||
|
|
expires_at — created_at + 24h
|
||
|
|
is_expired — PENDING only, past expiry, AND no token deposited
|
||
|
|
voice_active — voice_until set + in the future
|
||
|
|
is_present — ACCEPTED + token deposited + not yet LEFT
|
||
|
|
|
||
|
|
All assertions run on unsaved instances (no FK / DB touched), so this is a
|
||
|
|
`SimpleTestCase` — the properties are pure functions of the row's own fields.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from datetime import timedelta
|
||
|
|
|
||
|
|
from django.test import SimpleTestCase
|
||
|
|
from django.utils import timezone
|
||
|
|
|
||
|
|
from apps.gameboard.models import SeaInvite
|
||
|
|
|
||
|
|
|
||
|
|
class SeaInviteStatusMachineTest(SimpleTestCase):
|
||
|
|
def _invite(self, **kwargs):
|
||
|
|
"""Build an unsaved SeaInvite with PENDING/now defaults overridable."""
|
||
|
|
defaults = {"status": SeaInvite.PENDING, "created_at": timezone.now()}
|
||
|
|
defaults.update(kwargs)
|
||
|
|
return SeaInvite(**defaults)
|
||
|
|
|
||
|
|
# ── status default ───────────────────────────────────────────────────
|
||
|
|
def test_default_status_is_pending(self):
|
||
|
|
self.assertEqual(SeaInvite().status, SeaInvite.PENDING)
|
||
|
|
|
||
|
|
# ── expires_at ───────────────────────────────────────────────────────
|
||
|
|
def test_expires_at_is_24h_after_created(self):
|
||
|
|
created = timezone.now()
|
||
|
|
inv = self._invite(created_at=created)
|
||
|
|
self.assertEqual(inv.expires_at, created + timedelta(hours=24))
|
||
|
|
|
||
|
|
# ── is_expired ───────────────────────────────────────────────────────
|
||
|
|
def test_is_expired_true_for_stale_pending_without_deposit(self):
|
||
|
|
inv = self._invite(created_at=timezone.now() - timedelta(hours=25))
|
||
|
|
self.assertTrue(inv.is_expired)
|
||
|
|
|
||
|
|
def test_is_expired_false_within_window(self):
|
||
|
|
inv = self._invite(created_at=timezone.now() - timedelta(hours=1))
|
||
|
|
self.assertFalse(inv.is_expired)
|
||
|
|
|
||
|
|
def test_deposit_makes_invite_non_expiring(self):
|
||
|
|
# Per user-spec: once a token is deposited the invite never expires,
|
||
|
|
# even though it's well past the 24h PENDING window.
|
||
|
|
inv = self._invite(
|
||
|
|
created_at=timezone.now() - timedelta(hours=25),
|
||
|
|
token_deposited_at=timezone.now() - timedelta(hours=20),
|
||
|
|
)
|
||
|
|
self.assertFalse(inv.is_expired)
|
||
|
|
|
||
|
|
def test_only_pending_invites_can_expire(self):
|
||
|
|
inv = self._invite(
|
||
|
|
status=SeaInvite.ACCEPTED,
|
||
|
|
created_at=timezone.now() - timedelta(hours=25),
|
||
|
|
)
|
||
|
|
self.assertFalse(inv.is_expired)
|
||
|
|
|
||
|
|
# ── voice_active ─────────────────────────────────────────────────────
|
||
|
|
def test_voice_active_true_when_voice_until_in_future(self):
|
||
|
|
inv = self._invite(voice_until=timezone.now() + timedelta(hours=1))
|
||
|
|
self.assertTrue(inv.voice_active)
|
||
|
|
|
||
|
|
def test_voice_active_false_when_none(self):
|
||
|
|
self.assertFalse(self._invite().voice_active)
|
||
|
|
|
||
|
|
def test_voice_active_false_when_past(self):
|
||
|
|
inv = self._invite(voice_until=timezone.now() - timedelta(minutes=1))
|
||
|
|
self.assertFalse(inv.voice_active)
|
||
|
|
|
||
|
|
# ── is_present ───────────────────────────────────────────────────────
|
||
|
|
def test_is_present_true_when_accepted_deposited_not_left(self):
|
||
|
|
inv = self._invite(
|
||
|
|
status=SeaInvite.ACCEPTED,
|
||
|
|
token_deposited_at=timezone.now(),
|
||
|
|
)
|
||
|
|
self.assertTrue(inv.is_present)
|
||
|
|
|
||
|
|
def test_is_present_false_without_deposit(self):
|
||
|
|
inv = self._invite(status=SeaInvite.ACCEPTED)
|
||
|
|
self.assertFalse(inv.is_present)
|
||
|
|
|
||
|
|
def test_is_present_false_after_left(self):
|
||
|
|
# left_at set (status still ACCEPTED here) isolates the `left_at is
|
||
|
|
# None` clause of the guard.
|
||
|
|
inv = self._invite(
|
||
|
|
status=SeaInvite.ACCEPTED,
|
||
|
|
token_deposited_at=timezone.now() - timedelta(hours=1),
|
||
|
|
left_at=timezone.now(),
|
||
|
|
)
|
||
|
|
self.assertFalse(inv.is_present)
|