Files
python-tdd/src/apps/epic/tests/unit/test_tasks.py
Disco DeDisco 758c9c5377 COVERAGE: patch 91% → 96%+ — 603 tests, tasks.py at 100%
New/extended tests across billboard, dashboard, drama, epic, gameboard,
and lyric to cover previously untested branches: dev_login view, scroll
position endpoints, sky preview error paths, drama to_prose/to_activity
branches, consumer broadcast handlers, tarot deck draw/shuffle, astrology
model __str__, character model, sig reserve/ready/confirm views, natus
preview/save views, and the full tasks.py countdown scheduler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:23:28 -04:00

205 lines
8.9 KiB
Python

from unittest.mock import patch, MagicMock
from django.test import TestCase
from apps.epic.models import Room, SigReservation, TableSeat, TarotCard
from apps.lyric.models import User
from apps.epic.tasks import (
_cache_key, cancel_polarity_confirm, schedule_polarity_confirm,
)
class CacheKeyTest(TestCase):
def test_cache_key_format(self):
self.assertEqual(_cache_key("room-1", "levity"), "sig_countdown_room-1_levity")
class CancelPolarityConfirmTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="owner@tasks.io")
self.room = Room.objects.create(name="R", owner=self.user)
def test_cancel_with_no_timer_is_a_noop(self):
cancel_polarity_confirm(str(self.room.id), SigReservation.LEVITY)
def test_cancel_clears_cache_entry(self):
from django.core.cache import cache
key = _cache_key(str(self.room.id), SigReservation.LEVITY)
cache.set(key, "sometoken", timeout=60)
cancel_polarity_confirm(str(self.room.id), SigReservation.LEVITY)
self.assertIsNone(cache.get(key))
@patch("apps.epic.tasks._timers")
def test_cancel_calls_timer_cancel_when_present(self, mock_timers):
mock_timer = MagicMock()
key = f"{self.room.id}_levity"
mock_timers.pop.return_value = mock_timer
cancel_polarity_confirm(str(self.room.id), SigReservation.LEVITY)
mock_timer.cancel.assert_called_once()
class FireFunctionTest(TestCase):
"""Tests for the _fire() callback executed by threading.Timer."""
def setUp(self):
self.owner = User.objects.create(email="owner@fire.io")
self.room = Room.objects.create(name="R", owner=self.owner)
self.room.table_status = Room.SIG_SELECT
self.room.save()
roles = ["PC", "NC", "SC"]
self.gamers = [self.owner]
for i, role in enumerate(roles):
if i == 0:
TableSeat.objects.create(room=self.room, gamer=self.owner, slot_number=1, role=role)
else:
g = User.objects.create(email=f"g{i}@fire.io")
self.gamers.append(g)
TableSeat.objects.create(room=self.room, gamer=g, slot_number=i+1, role=role)
# Gravity seats (no significators needed for levity test)
grav_roles = ["BC", "EC", "AC"]
for i, role in enumerate(grav_roles, start=4):
g = User.objects.create(email=f"grav{i}@fire.io")
TableSeat.objects.create(room=self.room, gamer=g, slot_number=i, role=role)
def _set_token(self):
from django.core.cache import cache
import uuid
token = str(uuid.uuid4())
cache.set(_cache_key(str(self.room.id), SigReservation.LEVITY), token, 120)
return token
@patch("apps.epic.tasks._group_send")
def test_fire_does_nothing_if_token_mismatch(self, mock_send):
from apps.epic.tasks import _fire
self._set_token()
_fire(str(self.room.id), SigReservation.LEVITY, "wrong-token")
mock_send.assert_not_called()
@patch("apps.epic.tasks._group_send")
def test_fire_does_nothing_if_room_not_sig_select(self, mock_send):
from apps.epic.tasks import _fire
token = self._set_token()
self.room.table_status = Room.ROLE_SELECT
self.room.save()
_fire(str(self.room.id), SigReservation.LEVITY, token)
mock_send.assert_not_called()
@patch("apps.epic.tasks._group_send")
def test_fire_does_nothing_if_fewer_than_3_ready(self, mock_send):
from apps.epic.tasks import _fire
token = self._set_token()
cards = list(TarotCard.objects.all()[:2])
seats = list(TableSeat.objects.filter(room=self.room, role__in=["PC", "NC"]))
for i, seat in enumerate(seats):
SigReservation.objects.create(
room=self.room, gamer=seat.gamer, card=cards[i],
polarity=SigReservation.LEVITY, seat=seat, ready=True,
)
_fire(str(self.room.id), SigReservation.LEVITY, token)
mock_send.assert_not_called()
@patch("apps.epic.tasks._group_send")
def test_fire_assigns_significators_and_broadcasts_when_all_ready(self, mock_send):
from apps.epic.tasks import _fire
token = self._set_token()
cards = list(TarotCard.objects.all()[:3])
seats = list(TableSeat.objects.filter(room=self.room, role__in=["PC", "NC", "SC"]))
for i, seat in enumerate(seats):
SigReservation.objects.create(
room=self.room, gamer=seat.gamer, card=cards[i],
polarity=SigReservation.LEVITY, seat=seat, ready=True,
)
_fire(str(self.room.id), SigReservation.LEVITY, token)
self.assertTrue(mock_send.called)
levity_seats = TableSeat.objects.filter(room=self.room, role__in=["PC", "NC", "SC"])
for i, seat in enumerate(levity_seats):
seat.refresh_from_db()
self.assertEqual(seat.significator, cards[i])
def test_fire_does_nothing_for_nonexistent_room(self):
from apps.epic.tasks import _fire
from django.core.cache import cache
fake_id = "00000000-0000-0000-0000-000000000000"
token = "known-token"
cache.set(_cache_key(fake_id, SigReservation.LEVITY), token, 60)
_fire(fake_id, SigReservation.LEVITY, token)
@patch("apps.epic.tasks._group_send")
def test_fire_does_nothing_if_all_sigs_already_assigned(self, mock_send):
from apps.epic.tasks import _fire
token = self._set_token()
cards = list(TarotCard.objects.all()[:3])
seats = list(TableSeat.objects.filter(room=self.room, role__in=["PC", "NC", "SC"]))
for i, seat in enumerate(seats):
seat.significator = cards[i]
seat.save(update_fields=["significator"])
_fire(str(self.room.id), SigReservation.LEVITY, token)
mock_send.assert_not_called()
@patch("apps.epic.tasks._group_send")
def test_fire_broadcasts_pick_sky_when_all_polarity_sigs_assigned(self, mock_send):
"""When both levity AND gravity seats all have significators, fire() triggers SKY_SELECT."""
from apps.epic.tasks import _fire
token = self._set_token()
cards = list(TarotCard.objects.all()[:6])
# Give gravity seats significators so the all-assigned check passes
gravity_seats = list(TableSeat.objects.filter(room=self.room, role__in=["BC", "EC", "AC"]))
for i, seat in enumerate(gravity_seats):
seat.significator = cards[i]
seat.save(update_fields=["significator"])
# Create ready levity reservations (different cards from gravity)
levity_seats = list(TableSeat.objects.filter(room=self.room, role__in=["PC", "NC", "SC"]))
levity_cards = cards[3:6]
for i, seat in enumerate(levity_seats):
SigReservation.objects.create(
room=self.room, gamer=seat.gamer, card=levity_cards[i],
polarity=SigReservation.LEVITY, seat=seat, ready=True,
)
_fire(str(self.room.id), SigReservation.LEVITY, token)
call_types = [c.args[1]["type"] for c in mock_send.call_args_list]
self.assertIn("polarity_room_done", call_types)
self.assertIn("pick_sky_available", call_types)
self.room.refresh_from_db()
self.assertEqual(self.room.table_status, "SKY_SELECT")
class SchedulePolarityConfirmTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="owner@schedule.io")
self.room = Room.objects.create(name="R", owner=self.user)
def test_schedule_sets_cache_token(self):
from django.core.cache import cache
schedule_polarity_confirm(str(self.room.id), SigReservation.LEVITY, 60)
token = cache.get(_cache_key(str(self.room.id), SigReservation.LEVITY))
self.assertIsNotNone(token)
def test_schedule_registers_timer(self):
from apps.epic.tasks import _timers
key = f"{self.room.id}_{SigReservation.LEVITY}"
schedule_polarity_confirm(str(self.room.id), SigReservation.LEVITY, 60)
self.assertIn(key, _timers)
_timers[key].cancel() # clean up
def test_schedule_cancels_prior_timer_before_scheduling(self):
from apps.epic.tasks import _timers
schedule_polarity_confirm(str(self.room.id), SigReservation.LEVITY, 60)
key = f"{self.room.id}_{SigReservation.LEVITY}"
first_timer = _timers.get(key)
schedule_polarity_confirm(str(self.room.id), SigReservation.LEVITY, 60)
second_timer = _timers.get(key)
self.assertIsNotNone(second_timer)
self.assertIsNot(first_timer, second_timer)
second_timer.cancel()
class GroupSendTest(TestCase):
@patch("apps.epic.tasks.async_to_sync")
def test_group_send_calls_async_to_sync(self, mock_a2s):
from apps.epic.tasks import _group_send
mock_fn = MagicMock()
mock_a2s.return_value = mock_fn
_group_send("room-abc", {"type": "test"})
mock_a2s.assert_called_once()
mock_fn.assert_called_once_with("room_room-abc", {"type": "test"})