2026-03-09 01:07:16 -04:00
|
|
|
import lxml.html
|
|
|
|
|
|
|
|
|
|
from django.test import TestCase
|
2026-03-09 21:13:35 -04:00
|
|
|
from django.urls import reverse
|
2026-03-09 01:07:16 -04:00
|
|
|
|
2026-03-09 21:13:35 -04:00
|
|
|
from apps.applets.models import Applet, UserApplet
|
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
|
|
|
from apps.epic.models import DeckVariant
|
2026-03-19 00:00:00 -04:00
|
|
|
from apps.lyric.models import Token, User
|
2026-03-09 01:07:16 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class GameboardViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
2026-03-09 21:13:35 -04:00
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
2026-03-09 01:07:16 -04:00
|
|
|
self.client.force_login(self.user)
|
2026-03-09 21:13:35 -04:00
|
|
|
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
|
|
|
|
|
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
|
|
|
|
|
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
|
2026-03-09 01:07:16 -04:00
|
|
|
response = self.client.get("/gameboard/")
|
|
|
|
|
self.parsed = lxml.html.fromstring(response.content)
|
|
|
|
|
|
|
|
|
|
def test_gameboard_requires_login(self):
|
|
|
|
|
self.client.logout()
|
|
|
|
|
response = self.client.get("/gameboard/")
|
|
|
|
|
self.assertRedirects(
|
|
|
|
|
response, "/?next=/gameboard/", fetch_redirect_response=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_gameboard_renders(self):
|
|
|
|
|
response = self.client.get("/gameboard/")
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
|
|
|
|
|
def test_gameboard_shows_my_games_applet(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_applet_my_games")
|
|
|
|
|
|
|
|
|
|
def test_gameboard_shows_new_game_applet(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_applet_new_game")
|
|
|
|
|
|
2026-03-09 21:52:54 -04:00
|
|
|
def test_gameboard_shows_game_kit(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_game_kit")
|
2026-03-09 01:07:16 -04:00
|
|
|
|
|
|
|
|
def test_gameboard_shows_game_gear(self):
|
2026-03-09 21:13:35 -04:00
|
|
|
[_] = self.parsed.cssselect(".gear-btn")
|
2026-03-09 01:07:16 -04:00
|
|
|
|
|
|
|
|
def test_my_games_has_no_game_items_for_new_user(self):
|
|
|
|
|
game_items = self.parsed.cssselect("#id_applet_my_games .game-item")
|
|
|
|
|
self.assertEqual(len(game_items), 0)
|
|
|
|
|
|
|
|
|
|
def test_game_kit_has_coin_on_a_string(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_has_free_token(self):
|
2026-03-15 16:57:24 -04:00
|
|
|
[_] = self.parsed.cssselect("#id_game_kit #id_kit_free_token")
|
2026-03-09 01:07:16 -04:00
|
|
|
|
2026-03-24 21:52:57 -04:00
|
|
|
def test_game_kit_shows_deck_variant_cards(self):
|
|
|
|
|
decks = self.parsed.cssselect("#id_game_kit .deck-variant")
|
|
|
|
|
self.assertGreater(len(decks), 0)
|
|
|
|
|
# Earthman deck (seeded by migration) should have its own card
|
|
|
|
|
[_] = self.parsed.cssselect("#id_game_kit #id_kit_earthman_deck")
|
2026-03-09 01:07:16 -04:00
|
|
|
|
|
|
|
|
def test_game_kit_has_dice_set_placeholder(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set")
|
2026-03-09 21:13:35 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToggleGameAppletsViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
self.new_game, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
|
|
|
|
|
)
|
|
|
|
|
self.my_games, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
|
|
|
|
|
)
|
|
|
|
|
self.url = reverse("toggle_game_applets")
|
|
|
|
|
|
|
|
|
|
def test_unauthenticated_user_is_redirected(self):
|
|
|
|
|
self.client.logout()
|
|
|
|
|
response = self.client.post(self.url)
|
|
|
|
|
self.assertRedirects(
|
|
|
|
|
response, f"/?next={self.url}", fetch_redirect_response=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_unchecked_applet_gets_user_applet_with_visible_false(self):
|
|
|
|
|
self.client.post(self.url, {"applets": ["new-game"]})
|
|
|
|
|
ua = UserApplet.objects.get(user=self.user, applet=self.my_games)
|
|
|
|
|
self.assertFalse(ua.visible)
|
|
|
|
|
|
|
|
|
|
def test_redirects_on_normal_post(self):
|
|
|
|
|
response = self.client.post(self.url, {"applets": ["new-game", "my-games"]})
|
|
|
|
|
self.assertRedirects(
|
|
|
|
|
response, reverse("gameboard"), fetch_redirect_response=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_returns_200_on_htmx_post(self):
|
|
|
|
|
response = self.client.post(
|
|
|
|
|
self.url,
|
|
|
|
|
{"applets": ["new-game", "my-games"]},
|
|
|
|
|
HTTP_HX_REQUEST="true",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
|
|
|
|
|
def test_does_not_affect_dash_applets(self):
|
|
|
|
|
dash_applet, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="username", defaults={"name": "Username", "context": "dashboard"}
|
|
|
|
|
)
|
|
|
|
|
self.client.post(self.url, {"applets": ["new-game", "my-games"]})
|
|
|
|
|
self.assertFalse(UserApplet.objects.filter(user=self.user, applet=dash_applet).exists())
|
2026-03-19 00:00:00 -04:00
|
|
|
|
|
|
|
|
|
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
|
|
|
class EquipDeckViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
self.deck = DeckVariant.objects.first()
|
|
|
|
|
|
|
|
|
|
def test_get_returns_405(self):
|
|
|
|
|
response = self.client.get(reverse("equip_deck", kwargs={"deck_id": self.deck.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 405)
|
|
|
|
|
|
|
|
|
|
def test_post_equips_deck(self):
|
|
|
|
|
response = self.client.post(reverse("equip_deck", kwargs={"deck_id": self.deck.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 204)
|
|
|
|
|
self.user.refresh_from_db()
|
|
|
|
|
self.assertEqual(self.user.equipped_deck, self.deck)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnequipDeckViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
self.deck = DeckVariant.objects.first()
|
|
|
|
|
self.user.equipped_deck = self.deck
|
|
|
|
|
self.user.save(update_fields=["equipped_deck"])
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
|
|
|
|
|
def test_get_returns_405(self):
|
|
|
|
|
response = self.client.get(reverse("unequip_deck", kwargs={"deck_id": self.deck.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 405)
|
|
|
|
|
|
|
|
|
|
def test_post_clears_equipped_deck_when_matches(self):
|
|
|
|
|
response = self.client.post(reverse("unequip_deck", kwargs={"deck_id": self.deck.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 204)
|
|
|
|
|
self.user.refresh_from_db()
|
|
|
|
|
self.assertIsNone(self.user.equipped_deck)
|
|
|
|
|
|
|
|
|
|
def test_post_ignores_non_matching_deck(self):
|
|
|
|
|
other_deck = DeckVariant.objects.exclude(pk=self.deck.pk).first()
|
|
|
|
|
if other_deck is None:
|
|
|
|
|
self.skipTest("Only one deck variant in DB")
|
|
|
|
|
self.client.post(reverse("unequip_deck", kwargs={"deck_id": other_deck.pk}))
|
|
|
|
|
self.user.refresh_from_db()
|
|
|
|
|
self.assertEqual(self.user.equipped_deck, self.deck)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnequipTrinketViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
|
|
|
|
|
def test_get_returns_405(self):
|
|
|
|
|
from apps.lyric.models import Token
|
|
|
|
|
token = Token.objects.filter(user=self.user).first()
|
|
|
|
|
if token is None:
|
|
|
|
|
self.skipTest("No token for user")
|
|
|
|
|
response = self.client.get(reverse("unequip_trinket", kwargs={"token_id": token.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 405)
|
|
|
|
|
|
|
|
|
|
|
2026-04-04 13:49:48 -04:00
|
|
|
class GameKitViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
Applet.objects.get_or_create(slug="gk-trinkets", defaults={"name": "Trinkets", "context": "game-kit"})
|
|
|
|
|
Applet.objects.get_or_create(slug="gk-tokens", defaults={"name": "Tokens", "context": "game-kit"})
|
|
|
|
|
Applet.objects.get_or_create(slug="gk-decks", defaults={"name": "Card Decks", "context": "game-kit"})
|
|
|
|
|
Applet.objects.get_or_create(slug="gk-dice", defaults={"name": "Dice Sets", "context": "game-kit"})
|
|
|
|
|
response = self.client.get("/gameboard/game-kit/")
|
|
|
|
|
self.parsed = lxml.html.fromstring(response.content)
|
|
|
|
|
|
|
|
|
|
def test_game_kit_requires_login(self):
|
|
|
|
|
self.client.logout()
|
|
|
|
|
response = self.client.get("/gameboard/game-kit/")
|
|
|
|
|
self.assertRedirects(response, "/?next=/gameboard/game-kit/", fetch_redirect_response=False)
|
|
|
|
|
|
|
|
|
|
def test_game_kit_shows_gear_btn(self):
|
|
|
|
|
[_] = self.parsed.cssselect(".gear-btn")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_shows_applet_menu(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_game_kit_menu")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_applet_menu_has_trinkets_checkbox(self):
|
|
|
|
|
[inp] = self.parsed.cssselect("#id_game_kit_menu input[value='gk-trinkets']")
|
|
|
|
|
self.assertEqual(inp.get("type"), "checkbox")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_applet_menu_has_tokens_checkbox(self):
|
|
|
|
|
[inp] = self.parsed.cssselect("#id_game_kit_menu input[value='gk-tokens']")
|
|
|
|
|
self.assertEqual(inp.get("type"), "checkbox")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_applet_menu_has_decks_checkbox(self):
|
|
|
|
|
[inp] = self.parsed.cssselect("#id_game_kit_menu input[value='gk-decks']")
|
|
|
|
|
self.assertEqual(inp.get("type"), "checkbox")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_applet_menu_has_dice_checkbox(self):
|
|
|
|
|
[inp] = self.parsed.cssselect("#id_game_kit_menu input[value='gk-dice']")
|
|
|
|
|
self.assertEqual(inp.get("type"), "checkbox")
|
|
|
|
|
|
|
|
|
|
def test_game_kit_sections_container_present(self):
|
|
|
|
|
[_] = self.parsed.cssselect("#id_gk_sections_container")
|
|
|
|
|
|
|
|
|
|
def test_all_sections_visible_by_default(self):
|
|
|
|
|
sections = self.parsed.cssselect("#id_gk_sections_container section")
|
|
|
|
|
self.assertEqual(len(sections), 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToggleGameKitSectionsViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.user = User.objects.create(email="gamer@test.io")
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
self.trinkets, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="gk-trinkets", defaults={"name": "Trinkets", "context": "game-kit"}
|
|
|
|
|
)
|
|
|
|
|
self.tokens, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="gk-tokens", defaults={"name": "Tokens", "context": "game-kit"}
|
|
|
|
|
)
|
|
|
|
|
self.decks, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="gk-decks", defaults={"name": "Card Decks", "context": "game-kit"}
|
|
|
|
|
)
|
|
|
|
|
self.dice, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="gk-dice", defaults={"name": "Dice Sets", "context": "game-kit"}
|
|
|
|
|
)
|
|
|
|
|
self.url = reverse("toggle_game_kit_sections")
|
|
|
|
|
|
|
|
|
|
def test_unauthenticated_user_is_redirected(self):
|
|
|
|
|
self.client.logout()
|
|
|
|
|
response = self.client.post(self.url)
|
|
|
|
|
self.assertRedirects(response, f"/?next={self.url}", fetch_redirect_response=False)
|
|
|
|
|
|
|
|
|
|
def test_unchecked_section_gets_user_applet_with_visible_false(self):
|
|
|
|
|
self.client.post(self.url, {"applets": ["gk-trinkets"]})
|
|
|
|
|
ua = UserApplet.objects.get(user=self.user, applet=self.tokens)
|
|
|
|
|
self.assertFalse(ua.visible)
|
|
|
|
|
|
|
|
|
|
def test_redirects_on_normal_post(self):
|
|
|
|
|
response = self.client.post(self.url, {"applets": ["gk-trinkets", "gk-tokens"]})
|
|
|
|
|
self.assertRedirects(response, reverse("game_kit"), fetch_redirect_response=False)
|
|
|
|
|
|
|
|
|
|
def test_returns_200_on_htmx_post(self):
|
|
|
|
|
response = self.client.post(
|
|
|
|
|
self.url,
|
|
|
|
|
{"applets": ["gk-trinkets", "gk-tokens"]},
|
|
|
|
|
HTTP_HX_REQUEST="true",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
|
|
|
|
|
def test_does_not_affect_gameboard_applets(self):
|
|
|
|
|
gb_applet, _ = Applet.objects.get_or_create(
|
|
|
|
|
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
|
|
|
|
|
)
|
|
|
|
|
self.client.post(self.url, {"applets": ["gk-trinkets"]})
|
|
|
|
|
self.assertFalse(UserApplet.objects.filter(user=self.user, applet=gb_applet).exists())
|
|
|
|
|
|
|
|
|
|
def test_hidden_section_absent_from_htmx_response(self):
|
|
|
|
|
response = self.client.post(
|
|
|
|
|
self.url,
|
|
|
|
|
{"applets": ["gk-trinkets"]},
|
|
|
|
|
HTTP_HX_REQUEST="true",
|
|
|
|
|
)
|
|
|
|
|
parsed = lxml.html.fromstring(response.content)
|
|
|
|
|
sections = parsed.cssselect("section")
|
|
|
|
|
self.assertEqual(len(sections), 1)
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 00:00:00 -04:00
|
|
|
class EquipTrinketViewTest(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_trinket_button_partial(self):
|
|
|
|
|
response = self.client.get(
|
|
|
|
|
reverse("equip_trinket", kwargs={"token_id": self.token.pk})
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
self.assertTemplateUsed(response, "apps/gameboard/_partials/_equip_trinket_btn.html")
|
2026-03-24 22:57:12 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TarotFanViewTest(TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
from apps.epic.models import DeckVariant
|
|
|
|
|
self.earthman = DeckVariant.objects.get(slug="earthman")
|
|
|
|
|
self.fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
|
|
|
|
self.user = User.objects.create(email="fan@test.io")
|
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
|
|
|
|
|
|
def test_returns_fan_partial_for_unlocked_deck(self):
|
|
|
|
|
response = self.client.get(reverse("tarot_fan", kwargs={"deck_id": self.earthman.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
|
self.assertTemplateUsed(response, "apps/gameboard/_partials/_tarot_fan.html")
|
|
|
|
|
|
|
|
|
|
def test_returns_403_for_locked_deck(self):
|
|
|
|
|
response = self.client.get(reverse("tarot_fan", kwargs={"deck_id": self.fiorentine.pk}))
|
|
|
|
|
self.assertEqual(response.status_code, 403)
|