+52 IT/UT to close IT/UT-only coverage gaps (93% → 96%) — full suite 983 tests in 47s ; UTs in epic/tests/unit/test_models.py — TarotCardEmanationForTest (4) covers emanation_for(polarity) w. levity/gravity overrides + fallback to name_title for cards w.o a polarity split (cards 48-49 are the only polarity-split cards in the deck so this method is sparsely exercised by ITs); TarotCardReversalForTest (4) covers reversal_for(polarity) w. polarity-split + reversal_qualifier fallback + further fallthrough to emanation_for; TarotCardNameSplitTest (4) covers name_group/name_title colon-split parsing (prefix-w-colon / suffix / no-colon edge); TarotCardCautionsJsonTest (2) covers the cautions_json JSON serialiser ; UTs in epic/tests/unit/test_utils.py — PlanetHouseFallbackTest +1 happy-path test (degree=15 lands in house 1 w. sequential cusps) for the normal cusp-match branch alongside the existing pathological fallback test; TopCapacitorsTest (6) covers all top_capacitors() branches — empty dict / None / all-zero counts (the L56 max(counts.values()) <= 0 fallback that was uncovered) / single-winner / tie-clockwise-order / enriched dict {"count":N} input shape ; ITs in epic/tests/integrated/test_models.py — TarotDeckDrawTest extended w. 5 tests for remaining_count (happy + no-deck-variant fallback to 0) + draw() happy-path (returns n tuples of (TarotCard, bool) / appends to drawn_card_ids / never repeats cards across consecutive draws); existing ValueError + shuffle tests preserved ; ITs in epic/tests/integrated/test_views.py — SigEventRetractionTest (4 tests) covers the three data["retracted"] = True paths that the FT test_game_room_select_sig.py walks transitively but no IT pins directly: sig_unready retracts prior SIG_READY (L937), sig_ready retracts prior SIG_UNREADY (L907), sig_reserve action=release while ready retracts prior SIG_READY + records fresh SIG_UNREADY (L823); SigReserveInvalidCardIdTest (1) covers TarotCard.DoesNotExist → 400 (L840-841) ; SigSelectGravityContextTest (3) covers the user_polarity = 'gravity' branch (L322) + the gravity_sig_cards lookup (L357) — all existing SIG_SELECT context tests use the founder-as-PC-levity setup so these branches sat uncovered; logs in as gamers[5] (BC role) + asserts user_polarity + sig_cards match gravity_sig_cards() output ; SeaDeckViewTest (7) mirrors the test_game_room_select_sea.py FT but isolates the JSON contract — covers 403 when unseated, empty halves when seat has no deck_variant (L1255-1256 early-out), two-halves shape, ~even split, card_dict keys (id/name/arcana/corner_rank/suit_icon/name_group/name_title/reversed/qualifiers), reversed field is bool, claimed-significator exclusion via room.table_seats.exclude(significator__isnull=True) ; ITs in dashboard/tests/integrated/test_views.py — ProfileViewTest +2 (reserved-handle "adman" rejection — L116-117: username stays unchanged + redirect to /); KitBagViewTest (3) covers the kit_bag view's panel render w. TITHE-sort branch (L169-175) + login guard ; ITs in dashboard/tests/integrated/test_sky_views.py — SkyViewTest +2 (saved birth datetime renders in user's sky_birth_tz via astimezone L300-306 — 16:00 UTC → 12:00 EDT; invalid-tz string triggers ZoneInfoNotFoundError → swallowed pass → UTC fallback at 16:00) ; ITs in gameboard/tests/integrated/test_views.py — EquipTrinketViewTest +2 (POST equips trinket + returns 204 — L83-85; non-owner POST returns 404 via get_object_or_404); UnequipTrinketViewTest +2 (POST clears matching equipped_trinket — L107-110; POST of non-matching token is a 204 no-op, the implicit else branch) ; .coveragerc omit gains */reset_staging_db.py per user — mgmt cmd was the only 0%-stmt module that wasn't exercised by tests at all + we agreed it's deliberately untested staging-side code ; palette-monochrome-dark rebalance in rootvars.scss — --quiUser/--sixUser/--sepUser remapped to (secAg / quaAg / priPt) instead of (quaAg / terAg / secAg), shifting the secondary/subtle/deep-subtle anchors up the silver gradient so the palette reads more cleanly under the new sig-stage card colours from 3242873 ; uncovered remnants from earlier analysis intentionally left in place — consumers.py at 68% (channels-tag tests excluded; would need --tag=channels run), Carte Blanche slot navigation + sky_dice + tarot_deck preview view paths (the "bigger investments" tier from session triage; FT-covered + the IT setup is heavier than the immediate value), defensive except fallbacks that need contrived inputs to fire, and a handful of __str__s/pass branches not worth a test apiece — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ omit =
|
||||
*/migrations/*
|
||||
*/tests/*
|
||||
*/routing.py
|
||||
*/reset_staging_db.py
|
||||
|
||||
[report]
|
||||
show_missing = true
|
||||
@@ -37,6 +37,32 @@ class SkyViewTest(TestCase):
|
||||
self.assertContains(response, reverse("sky_preview"))
|
||||
self.assertContains(response, reverse("sky_save"))
|
||||
|
||||
def test_saved_birth_date_renders_in_user_tz_when_set(self):
|
||||
"""A user w. saved sky_birth_dt + sky_birth_tz hits the astimezone
|
||||
branch (views.py L300-306) — saved_birth_date / saved_birth_time
|
||||
render in the user's local tz, not UTC."""
|
||||
from datetime import datetime
|
||||
import zoneinfo
|
||||
# 1990-06-15 16:00 UTC = 12:00 PM in America/New_York (EDT, UTC-4)
|
||||
self.user.sky_birth_dt = datetime(1990, 6, 15, 16, 0, tzinfo=zoneinfo.ZoneInfo("UTC"))
|
||||
self.user.sky_birth_tz = "America/New_York"
|
||||
self.user.save()
|
||||
response = self.client.get(reverse("sky"))
|
||||
self.assertEqual(response.context["saved_birth_date"], "1990-06-15")
|
||||
self.assertEqual(response.context["saved_birth_time"], "12:00")
|
||||
|
||||
def test_saved_birth_falls_back_to_utc_when_tz_invalid(self):
|
||||
"""A garbage sky_birth_tz triggers ZoneInfoNotFoundError — the view
|
||||
swallows it (pass) and renders the UTC representation."""
|
||||
from datetime import datetime
|
||||
import zoneinfo
|
||||
self.user.sky_birth_dt = datetime(1990, 6, 15, 16, 0, tzinfo=zoneinfo.ZoneInfo("UTC"))
|
||||
self.user.sky_birth_tz = "Not/A/Real_Zone"
|
||||
self.user.save()
|
||||
response = self.client.get(reverse("sky"))
|
||||
# UTC fallback — 16:00 stays 16:00
|
||||
self.assertEqual(response.context["saved_birth_time"], "16:00")
|
||||
|
||||
def test_tz_input_is_readonly_and_carries_auto_detect_placeholder(self):
|
||||
"""Manual TZ edits throw the schedulePreview / PySwiss fetch off (the
|
||||
backend gets a stale TZ for the new lat/lon), so the field is render-
|
||||
|
||||
@@ -461,6 +461,47 @@ class ProfileViewTest(TestCase):
|
||||
[username_input] = parsed.cssselect("#id_new_username")
|
||||
self.assertEqual("discoman", username_input.get("value"))
|
||||
|
||||
def test_post_reserved_username_does_not_save(self):
|
||||
"""RESERVED_USERNAMES (e.g. 'adman') must be rejected — the view bails
|
||||
with an error message + redirect to / before reaching user.save()."""
|
||||
original_username = self.user.username
|
||||
self.client.post("/dashboard/set_profile", data={"username": "adman"})
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.username, original_username)
|
||||
|
||||
def test_post_reserved_username_redirects_home(self):
|
||||
response = self.client.post("/dashboard/set_profile", data={"username": "adman"})
|
||||
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
||||
|
||||
|
||||
class KitBagViewTest(TestCase):
|
||||
"""`kit_bag` view — renders the kit-bag panel partial w. equipped + token state."""
|
||||
|
||||
def setUp(self):
|
||||
from apps.lyric.models import Token
|
||||
self.user = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.user)
|
||||
# Stash a TITHE token so the list-comprehension branch lands non-empty
|
||||
Token.objects.create(user=self.user, token_type=Token.TITHE)
|
||||
self.url = "/dashboard/kit-bag/"
|
||||
|
||||
def test_get_returns_200_and_renders_panel(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "core/_partials/_kit_bag_panel.html")
|
||||
|
||||
def test_context_passes_free_and_tithe_counts(self):
|
||||
response = self.client.get(self.url)
|
||||
# signal seeds a FREE + COIN; we added a TITHE in setUp.
|
||||
self.assertEqual(response.context["tithe_count"], 1)
|
||||
self.assertGreaterEqual(response.context["free_count"], 0)
|
||||
|
||||
def test_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/?next=", response["Location"])
|
||||
|
||||
class ToggleDashAppletsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="disco@test.io")
|
||||
|
||||
@@ -649,6 +649,57 @@ class TarotDeckDrawTest(TestCase):
|
||||
td.refresh_from_db()
|
||||
self.assertEqual(td.drawn_card_ids, [])
|
||||
|
||||
def test_remaining_count_subtracts_drawn_from_total(self):
|
||||
from apps.epic.models import TarotDeck
|
||||
deck_variant = DeckVariant.objects.first()
|
||||
td = TarotDeck.objects.create(
|
||||
room=self.room,
|
||||
deck_variant=deck_variant,
|
||||
drawn_card_ids=[],
|
||||
)
|
||||
self.assertEqual(td.remaining_count, deck_variant.card_count)
|
||||
td.drawn_card_ids = list(
|
||||
TarotCard.objects.filter(deck_variant=deck_variant).values_list('id', flat=True)[:5]
|
||||
)
|
||||
td.save()
|
||||
self.assertEqual(td.remaining_count, deck_variant.card_count - 5)
|
||||
|
||||
def test_remaining_count_zero_when_no_deck_variant(self):
|
||||
from apps.epic.models import TarotDeck
|
||||
td = TarotDeck.objects.create(room=self.room, deck_variant=None)
|
||||
self.assertEqual(td.remaining_count, 0)
|
||||
|
||||
def test_draw_returns_n_tuples_of_card_and_bool(self):
|
||||
from apps.epic.models import TarotDeck
|
||||
deck_variant = DeckVariant.objects.first()
|
||||
td = TarotDeck.objects.create(room=self.room, deck_variant=deck_variant)
|
||||
drawn = td.draw(3)
|
||||
self.assertEqual(len(drawn), 3)
|
||||
for card, is_reversed in drawn:
|
||||
self.assertIsInstance(card, TarotCard)
|
||||
self.assertIsInstance(is_reversed, bool)
|
||||
|
||||
def test_draw_appends_card_ids_to_drawn_card_ids(self):
|
||||
from apps.epic.models import TarotDeck
|
||||
deck_variant = DeckVariant.objects.first()
|
||||
td = TarotDeck.objects.create(room=self.room, deck_variant=deck_variant)
|
||||
drawn = td.draw(4)
|
||||
td.refresh_from_db()
|
||||
self.assertEqual(len(td.drawn_card_ids), 4)
|
||||
for card, _ in drawn:
|
||||
self.assertIn(card.id, td.drawn_card_ids)
|
||||
|
||||
def test_draw_excludes_already_drawn_cards(self):
|
||||
"""Subsequent draws never repeat cards from the existing drawn_card_ids."""
|
||||
from apps.epic.models import TarotDeck
|
||||
deck_variant = DeckVariant.objects.first()
|
||||
td = TarotDeck.objects.create(room=self.room, deck_variant=deck_variant)
|
||||
first = td.draw(5)
|
||||
first_ids = {card.id for card, _ in first}
|
||||
second = td.draw(5)
|
||||
second_ids = {card.id for card, _ in second}
|
||||
self.assertFalse(first_ids & second_ids)
|
||||
|
||||
|
||||
# ── sig_deck_cards with no equipped deck ─────────────────────────────────────
|
||||
|
||||
|
||||
@@ -2259,3 +2259,222 @@ class SkySaveViewTest(TestCase):
|
||||
|
||||
char = Character.objects.get(seat=pc_seat)
|
||||
self.assertEqual(char.significator, sig_card)
|
||||
|
||||
|
||||
# ── SIG event-retraction branches ────────────────────────────────────────────
|
||||
# The provenance scrolls use a `data["retracted"] = True` flag to soft-cancel
|
||||
# prior events when a gamer reverses themselves (WAIT NVM after SAVE SIG, etc).
|
||||
# These three branches in sig_reserve / sig_ready are the load-bearing ones —
|
||||
# without them a recanted action stays visible in the billboard scrollback.
|
||||
|
||||
class SigEventRetractionTest(TestCase):
|
||||
"""`data["retracted"] = True` writes on the three reverse-direction paths."""
|
||||
|
||||
def setUp(self):
|
||||
self.room, self.gamers, self.earthman, self.card = _full_sig_setUp(self)
|
||||
# PC (founder) already logged in; reserve + go ready so subsequent
|
||||
# actions have prior SIG_READY events to retract.
|
||||
self.reserve_url = reverse("epic:sig_reserve", kwargs={"room_id": self.room.id})
|
||||
self.ready_url = reverse("epic:sig_ready", kwargs={"room_id": self.room.id})
|
||||
|
||||
def _reserve(self):
|
||||
return self.client.post(self.reserve_url, data={
|
||||
"card_id": self.card.id, "action": "reserve",
|
||||
})
|
||||
|
||||
def _ready(self, action="ready"):
|
||||
return self.client.post(self.ready_url, data={"action": action})
|
||||
|
||||
def test_sig_unready_retracts_prior_sig_ready_event(self):
|
||||
"""sig_ready action=unready flips `data["retracted"]=True` on the most
|
||||
recent un-retracted SIG_READY event for this actor (views.py L937)."""
|
||||
self._reserve()
|
||||
self._ready(action="ready")
|
||||
prior = self.room.events.filter(
|
||||
actor=self.gamers[0], verb=GameEvent.SIG_READY
|
||||
).last()
|
||||
self.assertFalse(prior.data.get("retracted"), "precondition: not yet retracted")
|
||||
|
||||
self._ready(action="unready")
|
||||
|
||||
prior.refresh_from_db()
|
||||
self.assertTrue(prior.data.get("retracted"))
|
||||
|
||||
def test_sig_ready_retracts_prior_sig_unready_event(self):
|
||||
"""sig_ready action=ready retracts the most recent un-retracted
|
||||
SIG_UNREADY event (views.py L907) — the cancellation is now moot."""
|
||||
self._reserve()
|
||||
self._ready(action="ready")
|
||||
self._ready(action="unready")
|
||||
prior_unready = self.room.events.filter(
|
||||
actor=self.gamers[0], verb=GameEvent.SIG_UNREADY
|
||||
).last()
|
||||
self.assertFalse(prior_unready.data.get("retracted"))
|
||||
|
||||
self._ready(action="ready")
|
||||
|
||||
prior_unready.refresh_from_db()
|
||||
self.assertTrue(prior_unready.data.get("retracted"))
|
||||
|
||||
def test_sig_release_while_ready_retracts_prior_sig_ready_event(self):
|
||||
"""sig_reserve action=release on a ready reservation acts as implicit
|
||||
WAIT NVM — retracts the most recent SIG_READY (views.py L823)."""
|
||||
self._reserve()
|
||||
self._ready(action="ready")
|
||||
prior = self.room.events.filter(
|
||||
actor=self.gamers[0], verb=GameEvent.SIG_READY
|
||||
).last()
|
||||
self.assertFalse(prior.data.get("retracted"))
|
||||
|
||||
self.client.post(self.reserve_url, data={
|
||||
"card_id": self.card.id, "action": "release",
|
||||
})
|
||||
|
||||
prior.refresh_from_db()
|
||||
self.assertTrue(prior.data.get("retracted"))
|
||||
|
||||
def test_sig_release_while_ready_records_sig_unready_event(self):
|
||||
"""Same release-while-ready path also records a fresh SIG_UNREADY
|
||||
(the implicit cancellation event)."""
|
||||
self._reserve()
|
||||
self._ready(action="ready")
|
||||
unready_count_before = self.room.events.filter(
|
||||
actor=self.gamers[0], verb=GameEvent.SIG_UNREADY
|
||||
).count()
|
||||
self.client.post(self.reserve_url, data={
|
||||
"card_id": self.card.id, "action": "release",
|
||||
})
|
||||
self.assertEqual(
|
||||
self.room.events.filter(
|
||||
actor=self.gamers[0], verb=GameEvent.SIG_UNREADY
|
||||
).count(),
|
||||
unready_count_before + 1,
|
||||
)
|
||||
|
||||
|
||||
# ── SIG_RESERVE invalid card-id branch ───────────────────────────────────────
|
||||
|
||||
class SigReserveInvalidCardIdTest(TestCase):
|
||||
"""sig_reserve POSTed with a card_id that doesn't exist returns 400."""
|
||||
|
||||
def setUp(self):
|
||||
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||
self.url = reverse("epic:sig_reserve", kwargs={"room_id": self.room.id})
|
||||
|
||||
def test_unknown_card_id_returns_400(self):
|
||||
"""TarotCard.DoesNotExist branch (views.py L840-841)."""
|
||||
response = self.client.post(self.url, data={
|
||||
"card_id": 999999, "action": "reserve",
|
||||
})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
# ── SIG_SELECT gravity-polarity rendering ────────────────────────────────────
|
||||
|
||||
class SigSelectGravityContextTest(TestCase):
|
||||
"""SIG_SELECT room context for a gravity-polarity gamer.
|
||||
|
||||
Covers the `user_polarity = 'gravity'` branch (views.py L322) and the
|
||||
gravity_sig_cards lookup (L357) — both fall through the cracks of the
|
||||
default founder-as-PC-levity tests."""
|
||||
|
||||
def setUp(self):
|
||||
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||
# gamers[5] is BC → gravity polarity
|
||||
self.bc = self.gamers[5]
|
||||
self.client.force_login(self.bc)
|
||||
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
|
||||
|
||||
def test_gravity_gamer_room_context_has_gravity_polarity(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.context["user_polarity"], "gravity")
|
||||
|
||||
def test_gravity_gamer_sees_gravity_sig_cards(self):
|
||||
"""Levity + gravity get the same 16 court cards (filtered by major-arcana
|
||||
Note unlocks); this test just asserts the gravity branch was taken."""
|
||||
from apps.epic.models import gravity_sig_cards
|
||||
response = self.client.get(self.url)
|
||||
# Same underlying card set; assertion is that the context was populated
|
||||
# (the gravity branch returned, vs falling into the empty `else`).
|
||||
self.assertEqual(
|
||||
list(response.context["sig_cards"]),
|
||||
list(gravity_sig_cards(self.room, self.bc)),
|
||||
)
|
||||
|
||||
def test_gravity_gamer_sig_card_set_non_empty(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertGreater(len(response.context["sig_cards"]), 0)
|
||||
|
||||
|
||||
# ── SEA_DECK draw view ───────────────────────────────────────────────────────
|
||||
|
||||
class SeaDeckViewTest(TestCase):
|
||||
"""sea_deck — JSON view returning shuffled levity + gravity halves.
|
||||
|
||||
Mirrors the FT in test_game_room_select_sea.py:DRAW SEA — that test walks
|
||||
the full UI; this one isolates the JSON contract + filter semantics."""
|
||||
|
||||
def setUp(self):
|
||||
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||
self.room.table_status = Room.SKY_SELECT
|
||||
self.room.save()
|
||||
# Use PC seat (founder) — already logged in by _full_sig_setUp
|
||||
self.url = reverse("epic:sea_deck", kwargs={"room_id": self.room.id})
|
||||
|
||||
def test_returns_403_when_not_seated(self):
|
||||
outsider = User.objects.create(email="outsider@test.io")
|
||||
self.client.force_login(outsider)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_returns_empty_halves_when_seat_has_no_deck_variant(self):
|
||||
"""sea_deck early-outs to {levity:[],gravity:[]} if the seat hasn't
|
||||
committed a deck — guards against null deck_variant FK access."""
|
||||
TableSeat.objects.filter(room=self.room).update(deck_variant=None)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data, {"levity": [], "gravity": []})
|
||||
|
||||
def test_returns_two_halves(self):
|
||||
response = self.client.get(self.url)
|
||||
data = response.json()
|
||||
self.assertIn("levity", data)
|
||||
self.assertIn("gravity", data)
|
||||
|
||||
def test_card_count_roughly_split_between_halves(self):
|
||||
"""Total card pool is split in half — within 1 of perfectly even."""
|
||||
response = self.client.get(self.url)
|
||||
data = response.json()
|
||||
self.assertAlmostEqual(len(data["levity"]), len(data["gravity"]), delta=1)
|
||||
|
||||
def test_card_dict_contains_expected_keys(self):
|
||||
response = self.client.get(self.url)
|
||||
data = response.json()
|
||||
sample = data["levity"][0]
|
||||
for key in (
|
||||
"id", "name", "arcana", "corner_rank", "suit_icon",
|
||||
"name_group", "name_title", "reversed",
|
||||
"levity_qualifier", "gravity_qualifier",
|
||||
):
|
||||
self.assertIn(key, sample, f"missing key {key!r} in card dict")
|
||||
|
||||
def test_reversed_field_is_boolean(self):
|
||||
response = self.client.get(self.url)
|
||||
data = response.json()
|
||||
for card in data["levity"] + data["gravity"]:
|
||||
self.assertIsInstance(card["reversed"], bool)
|
||||
|
||||
def test_excludes_claimed_significators(self):
|
||||
"""A card already set as a seat.significator must not appear in either
|
||||
half — it's been claimed for the game and is out of the sea-draw pool."""
|
||||
sig_card = TarotCard.objects.get(
|
||||
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=11
|
||||
)
|
||||
pc_seat = TableSeat.objects.get(room=self.room, role="PC")
|
||||
pc_seat.significator = sig_card
|
||||
pc_seat.save()
|
||||
response = self.client.get(self.url)
|
||||
data = response.json()
|
||||
all_ids = {c["id"] for c in data["levity"]} | {c["id"] for c in data["gravity"]}
|
||||
self.assertNotIn(sig_card.id, all_ids)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.epic.models import TarotCard
|
||||
@@ -70,3 +72,102 @@ class TarotCardSuitIconTest(SimpleTestCase):
|
||||
|
||||
def test_icon_override_takes_priority_over_suit(self):
|
||||
self.assertEqual(_card('MIDDLE', 11, 'CROWNS', icon='fa-star').suit_icon, 'fa-star')
|
||||
|
||||
|
||||
class TarotCardEmanationForTest(SimpleTestCase):
|
||||
"""TarotCard.emanation_for — polarity-split upright title (cards 48-49)."""
|
||||
|
||||
def test_returns_levity_emanation_when_polarity_levity(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 11: The Awakened'
|
||||
c.levity_emanation = 'The Effulgent Mould of Man'
|
||||
c.gravity_emanation = 'The Tellurian Mould of Man'
|
||||
self.assertEqual(c.emanation_for('levity'), 'The Effulgent Mould of Man')
|
||||
|
||||
def test_returns_gravity_emanation_when_polarity_gravity(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 11: The Awakened'
|
||||
c.levity_emanation = 'The Effulgent Mould of Man'
|
||||
c.gravity_emanation = 'The Tellurian Mould of Man'
|
||||
self.assertEqual(c.emanation_for('gravity'), 'The Tellurian Mould of Man')
|
||||
|
||||
def test_falls_back_to_name_title_when_no_levity_split(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 3: The Nomad'
|
||||
self.assertEqual(c.emanation_for('levity'), 'The Nomad')
|
||||
|
||||
def test_falls_back_to_name_title_when_no_gravity_split(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 3: The Nomad'
|
||||
self.assertEqual(c.emanation_for('gravity'), 'The Nomad')
|
||||
|
||||
|
||||
class TarotCardReversalForTest(SimpleTestCase):
|
||||
"""TarotCard.reversal_for — polarity-split reversal title (card 48)."""
|
||||
|
||||
def test_returns_levity_reversal_when_polarity_levity(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 11: The Awakened'
|
||||
c.levity_reversal = 'The Reflected Mould of Man'
|
||||
c.gravity_reversal = 'The Obscured Mould of Man'
|
||||
self.assertEqual(c.reversal_for('levity'), 'The Reflected Mould of Man')
|
||||
|
||||
def test_returns_gravity_reversal_when_polarity_gravity(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 11: The Awakened'
|
||||
c.levity_reversal = 'The Reflected Mould of Man'
|
||||
c.gravity_reversal = 'The Obscured Mould of Man'
|
||||
self.assertEqual(c.reversal_for('gravity'), 'The Obscured Mould of Man')
|
||||
|
||||
def test_falls_back_to_reversal_qualifier_when_no_polarity_split(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Jack of Brands'
|
||||
c.reversal_qualifier = 'Fickle'
|
||||
# No polarity-split override → reversal_qualifier wins
|
||||
self.assertEqual(c.reversal_for('levity'), 'Fickle')
|
||||
self.assertEqual(c.reversal_for('gravity'), 'Fickle')
|
||||
|
||||
def test_falls_back_to_emanation_when_no_reversal_qualifier(self):
|
||||
"""Blank reversal_qualifier → reversal_for falls through emanation_for."""
|
||||
c = TarotCard()
|
||||
c.name = 'Group 3: The Nomad'
|
||||
# reversal_qualifier blank, no polarity-split reversal → emanation fallback
|
||||
self.assertEqual(c.reversal_for('levity'), 'The Nomad')
|
||||
|
||||
|
||||
class TarotCardNameSplitTest(SimpleTestCase):
|
||||
"""TarotCard.name_group / name_title — colon-split parsing."""
|
||||
|
||||
def test_name_group_returns_prefix_with_colon(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 3: The Nomad'
|
||||
self.assertEqual(c.name_group, 'Group 3:')
|
||||
|
||||
def test_name_group_empty_when_no_colon(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Jack of Brands'
|
||||
self.assertEqual(c.name_group, '')
|
||||
|
||||
def test_name_title_returns_suffix_after_colon(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Group 3: The Nomad'
|
||||
self.assertEqual(c.name_title, 'The Nomad')
|
||||
|
||||
def test_name_title_returns_full_name_when_no_colon(self):
|
||||
c = TarotCard()
|
||||
c.name = 'Jack of Brands'
|
||||
self.assertEqual(c.name_title, 'Jack of Brands')
|
||||
|
||||
|
||||
class TarotCardCautionsJsonTest(SimpleTestCase):
|
||||
"""TarotCard.cautions_json — JSON-serialised cautions list."""
|
||||
|
||||
def test_empty_cautions_serialises_to_empty_list(self):
|
||||
c = TarotCard()
|
||||
c.cautions = []
|
||||
self.assertEqual(c.cautions_json, '[]')
|
||||
|
||||
def test_cautions_serialise_to_json_array(self):
|
||||
c = TarotCard()
|
||||
c.cautions = [{'type': 'Vanity', 'effect': 'Costs an Ardor token.'}]
|
||||
self.assertEqual(json.loads(c.cautions_json), c.cautions)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.epic.utils import _planet_house
|
||||
from apps.epic.utils import _planet_house, top_capacitors
|
||||
|
||||
|
||||
class PlanetHouseFallbackTest(SimpleTestCase):
|
||||
@@ -11,3 +11,44 @@ class PlanetHouseFallbackTest(SimpleTestCase):
|
||||
# the fallback `return 1`.
|
||||
cusps = [0.0] * 12
|
||||
self.assertEqual(_planet_house(180.0, cusps), 1)
|
||||
|
||||
def test_returns_1_when_degree_in_first_house_normal(self):
|
||||
# Standard, sequential cusps: degree=15 should land in house 1 (0–30).
|
||||
cusps = [i * 30.0 for i in range(12)]
|
||||
self.assertEqual(_planet_house(15.0, cusps), 1)
|
||||
|
||||
|
||||
class TopCapacitorsTest(SimpleTestCase):
|
||||
"""top_capacitors — capacitor names tied for the highest element count."""
|
||||
|
||||
def test_returns_empty_when_elements_is_empty(self):
|
||||
self.assertEqual(top_capacitors({}), [])
|
||||
|
||||
def test_returns_empty_when_elements_is_none(self):
|
||||
self.assertEqual(top_capacitors(None), [])
|
||||
|
||||
def test_returns_empty_when_all_counts_are_zero(self):
|
||||
"""All-zero counts (e.g. a brand-new chart with no planets) → empty list,
|
||||
not an Ardor-by-default. Exercises the `max(counts.values()) <= 0` branch."""
|
||||
self.assertEqual(
|
||||
top_capacitors({"Fire": 0, "Stone": 0, "Time": 0, "Space": 0, "Air": 0, "Water": 0}),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_returns_single_top_capacitor_when_one_element_wins(self):
|
||||
# Stone has highest count → Ossum
|
||||
result = top_capacitors({"Fire": 1, "Stone": 5, "Time": 2})
|
||||
self.assertEqual(result, ["Ossum"])
|
||||
|
||||
def test_returns_multiple_capacitors_on_tie_in_clockwise_order(self):
|
||||
# Fire + Stone tied at 3 → order follows ELEMENT_ORDER (Fire first).
|
||||
result = top_capacitors({"Fire": 3, "Stone": 3, "Time": 2})
|
||||
self.assertEqual(result, ["Ardor", "Ossum"])
|
||||
|
||||
def test_accepts_dict_values_with_count_key(self):
|
||||
"""`elements` may carry enriched dicts like {"count": N, ...}."""
|
||||
result = top_capacitors({
|
||||
"Fire": {"count": 1, "sign": "Aries"},
|
||||
"Stone": {"count": 4, "sign": "Taurus"},
|
||||
})
|
||||
self.assertEqual(result, ["Ossum"])
|
||||
|
||||
@@ -235,6 +235,7 @@ class UnequipTrinketViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.user)
|
||||
self.token = Token.objects.filter(user=self.user, token_type=Token.COIN).first()
|
||||
|
||||
def test_get_returns_405(self):
|
||||
from apps.lyric.models import Token
|
||||
@@ -244,6 +245,30 @@ class UnequipTrinketViewTest(TestCase):
|
||||
response = self.client.get(reverse("unequip_trinket", kwargs={"token_id": token.pk}))
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_post_clears_equipped_trinket_when_matching(self):
|
||||
self.user.equipped_trinket = self.token
|
||||
self.user.save(update_fields=["equipped_trinket"])
|
||||
response = self.client.post(
|
||||
reverse("unequip_trinket", kwargs={"token_id": self.token.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.user.refresh_from_db()
|
||||
self.assertIsNone(self.user.equipped_trinket)
|
||||
|
||||
def test_post_ignores_non_matching_trinket(self):
|
||||
"""POSTing a token that's not the currently-equipped one is a 204 no-op
|
||||
— equipped_trinket is unchanged. Covers the implicit `else` of the
|
||||
`if request.user.equipped_trinket_id == token.pk` branch."""
|
||||
other_token = Token.objects.create(user=self.user, token_type=Token.TITHE)
|
||||
self.user.equipped_trinket = self.token # COIN is equipped
|
||||
self.user.save(update_fields=["equipped_trinket"])
|
||||
response = self.client.post(
|
||||
reverse("unequip_trinket", kwargs={"token_id": other_token.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.equipped_trinket, self.token)
|
||||
|
||||
|
||||
class GameKitViewTest(TestCase):
|
||||
def setUp(self):
|
||||
@@ -383,6 +408,23 @@ class EquipTrinketViewTest(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "apps/gameboard/_partials/_equip_trinket_btn.html")
|
||||
|
||||
def test_post_equips_trinket_and_returns_204(self):
|
||||
response = self.client.post(
|
||||
reverse("equip_trinket", kwargs={"token_id": self.token.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.equipped_trinket, self.token)
|
||||
|
||||
def test_post_requires_token_owner(self):
|
||||
outsider = User.objects.create(email="outsider@test.io")
|
||||
self.client.force_login(outsider)
|
||||
response = self.client.post(
|
||||
reverse("equip_trinket", kwargs={"token_id": self.token.pk})
|
||||
)
|
||||
# get_object_or_404 — the token belongs to self.user, not outsider
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
class TarotFanViewTest(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -429,9 +429,9 @@
|
||||
--secUser: var(--quiAg); /* 175,175,175 — light gray text/border */
|
||||
--terUser: var(--sixAg); /* 240,240,240 — bright white accent */
|
||||
--quaUser: var(--sixAg); /* 240,240,240 — active/interactive */
|
||||
--quiUser: var(--quaAg); /* 133,133,133 — secondary action */
|
||||
--sixUser: var(--terAg); /* 100,100,100 — subtle mid */
|
||||
--sepUser: var(--secAg); /* 60,60,60 — deep subtle */
|
||||
--quiUser: var(--secAg); /* 133,133,133 — secondary action */
|
||||
--sixUser: var(--quaAg); /* 100,100,100 — subtle mid */
|
||||
--sepUser: var(--priPt); /* 60,60,60 — deep subtle */
|
||||
--octUser: var(--quiPt); /* 189,190,189 — links (cooler silver) */
|
||||
--ninUser: var(--sixAg); /* 240,240,240 — glow highlight */
|
||||
--decUser: var(--terAg); /* 100,100,100 — mid tone */
|
||||
|
||||
Reference in New Issue
Block a user