Files
python-tdd/src/apps/lyric/tests/integrated/test_models.py

368 lines
15 KiB
Python
Raw Normal View History

import uuid
from django.contrib import auth
from django.test import TestCase
from django.utils import timezone
from apps.drama.models import Note
from apps.lyric.models import LoginToken, PaymentMethod, Token, User, Wallet
class UserModelTest(TestCase):
def test_model_is_configured_for_django_auth(self):
self.assertEqual(auth.get_user_model(), User)
def test_user_is_valid_with_email_only(self):
user = User(email="a@b.cde")
user.set_unusable_password()
user.full_clean() # should not raise
def test_id_is_primary_key(self):
user = User(id="123")
self.assertEqual(user.pk, "123")
def test_user_can_have_a_username(self):
user = User.objects.create(email="a@b.cde")
user.username = "stardust"
user.save()
self.assertEqual(User.objects.get(pk=user.pk).username, "stardust")
def test_searchable_defaults_to_false(self):
user = User.objects.create(email="a@b.cde")
self.assertFalse(user.searchable)
def test_active_title_is_null_by_default(self):
user = User.objects.create(email="a@b.cde")
self.assertIsNone(user.active_title)
def test_active_title_can_be_set_to_a_note(self):
user = User.objects.create(email="a@b.cde")
note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
user.active_title = note
user.save(update_fields=["active_title"])
user.refresh_from_db()
self.assertEqual(user.active_title, note)
def test_clearing_note_nullifies_active_title(self):
user = User.objects.create(email="a@b.cde")
note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
user.active_title = note
user.save(update_fields=["active_title"])
note.delete()
user.refresh_from_db()
self.assertIsNone(user.active_title)
Baltimorean: post-attribution titles now read "Baltimorean" not "Ard!" — TDD User caught a missed surface on iPhone after the May-18b rename pass (1ccb045): the post.html `.post-attribution` spans still rendered "@disco the Ard!" instead of "@disco the Baltimorean" — six callsites across the post header (author / invitee / shared-self / created-by) plus `_my_buds_applet_item.html`'s bud row body. Same shape on display: navbar DON greeting is the only surface that should keep "Ayo, Ard!", per the May-18b architectural decision ; root cause: `User.active_title_display` at lyric/models.py:152 returned `self.active_title.display_title` ("Ard!" for Baltimorean) instead of `self.active_title.display_name` ("Baltimorean"). The Sprint-18b rename pass swapped the inline `attr_combo` + Brief.title to use `display_name`, but missed this property which is the indirection layer for `.post-attribution` callsites. Navbar uses `{{ user.active_title.display_title }}` directly (no helper-property indirection) so it stays at "Ard!" — that's the intended single Ard! surface ; fix: one-line swap in `active_title_display` from `display_title` to `display_name`. For stargazer / schizo / nomad these two are equal (the Note model's `display_name` property at drama/models.py:262 falls through to `display_title` unless the slug has an override in `_NOTE_DISPLAY[slug]["display_name"]`) — Baltimorean is the only current override w. `{"display_name": "Baltimorean"}`. So this is no-op for every non-Baltimorean Note ; TDD trail: +3 UTs in apps.lyric.tests.integrated.test_models.UserModelTest: `test_active_title_display_returns_earthman_when_no_note_donned` (smoke), `test_active_title_display_uses_display_name_not_display_title` (pins the Baltimorean override path — went red 'Ard! != Baltimorean' before the fix), `test_active_title_display_falls_through_to_display_title_for_non_overridden_slugs` (pins the no-op path for stargazer). Red → green confirmed. Surfaces auto-affected: post.html post-attribution × 5 callsites + `_my_buds_applet_item.html` bud row body (all use `{{ user.active_title_display }}`) ; 1008 IT/UT green in 46s (+3 from 1005) Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:42:58 -04:00
def test_active_title_display_returns_earthman_when_no_note_donned(self):
user = User.objects.create(email="a@b.cde")
self.assertEqual(user.active_title_display, "Earthman")
def test_active_title_display_uses_display_name_not_display_title(self):
"""Inline attributions like `.post-attribution` should render the
Note's `display_name`, not its `display_title` — the only divergence
today is Baltimorean (display_title="Ard!" navbar-flair, display_name
="Baltimorean" everywhere else). Pinning here so future Notes that
override one field but not the other don't surprise the post page."""
user = User.objects.create(email="ard@b.cde")
note = Note.objects.create(user=user, slug="baltimorean", earned_at=timezone.now())
user.active_title = note
user.save(update_fields=["active_title"])
self.assertEqual(user.active_title_display, "Baltimorean")
def test_active_title_display_falls_through_to_display_title_for_non_overridden_slugs(self):
"""Stargazer / Schizo / Nomad don't override display_name, so
active_title_display returns the same string display_title would
the rename pattern is no-op for these surfaces."""
user = User.objects.create(email="star@b.cde")
note = Note.objects.create(user=user, slug="stargazer", earned_at=timezone.now())
user.active_title = note
user.save(update_fields=["active_title"])
self.assertEqual(user.active_title_display, note.display_title)
class LoginTokenModelTest(TestCase):
def test_links_user_with_autogen_uid(self):
login_token1 = LoginToken.objects.create(email="a@b.cde")
login_token2 = LoginToken.objects.create(email="v@w.xyz")
self.assertNotEqual(login_token1.pk, login_token2.pk)
self.assertIsInstance(login_token1.pk, uuid.UUID)
class UserManagerTest(TestCase):
def test_create_superuser_sets_is_staff_and_is_superuser(self):
user = User.objects.create_superuser(
email="admin@example.com",
password="correct-password",
)
self.assertTrue(user.is_staff)
self.assertTrue(user.is_superuser)
def test_create_superuser_sets_usable_password(self):
user = User.objects.create_superuser(
email="admin@example.com",
password="correct-password",
)
self.assertTrue(user.check_password("correct-password"))
class UserPaletteTest(TestCase):
def test_palette_field_defaults_to_palette_default(self):
user = User.objects.create(email="a@b.cde")
self.assertEqual(user.palette, "palette-default")
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD - User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns - drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their" - SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched - new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio - card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses - dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE - _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question - billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
class UserPronounsTest(TestCase):
def test_pronouns_default_to_pluralism(self):
user = User.objects.create(email="they@test.io")
self.assertEqual(user.pronouns, "pluralism")
def test_pronouns_choices_include_all_five_options(self):
from apps.lyric.models import PRONOUN_CHOICES
keys = [k for (k, _) in PRONOUN_CHOICES]
self.assertEqual(
keys,
["pluralism", "bawlmorese", "misogyny", "misandry", "misanthropy"],
)
def test_pluralism_resolves_to_they_them_their(self):
user = User.objects.create(email="plural@test.io")
self.assertEqual(user.pronoun_subj, "they")
self.assertEqual(user.pronoun_obj, "them")
self.assertEqual(user.pronoun_poss, "their")
def test_bawlmorese_resolves_to_yo_yo_yos(self):
user = User.objects.create(email="yo@test.io", pronouns="bawlmorese")
self.assertEqual(user.pronoun_subj, "yo")
self.assertEqual(user.pronoun_obj, "yo")
self.assertEqual(user.pronoun_poss, "yos")
def test_misogyny_resolves_to_he_him_his(self):
user = User.objects.create(email="he@test.io", pronouns="misogyny")
self.assertEqual(user.pronoun_subj, "he")
self.assertEqual(user.pronoun_obj, "him")
self.assertEqual(user.pronoun_poss, "his")
def test_misandry_resolves_to_she_her_hers(self):
user = User.objects.create(email="she@test.io", pronouns="misandry")
self.assertEqual(user.pronoun_subj, "she")
self.assertEqual(user.pronoun_obj, "her")
self.assertEqual(user.pronoun_poss, "hers")
def test_misanthropy_resolves_to_it_it_its(self):
user = User.objects.create(email="it@test.io", pronouns="misanthropy")
self.assertEqual(user.pronoun_subj, "it")
self.assertEqual(user.pronoun_obj, "it")
self.assertEqual(user.pronoun_poss, "its")
class WalletCreationTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="capman@test.io")
def test_wallet_is_created_for_new_user(self):
self.assertTrue(Wallet.objects.filter(user=self.user).exists())
def test_new_wallet_has_144_writs(self):
wallet = Wallet.objects.get(user = self.user)
self.assertEqual(wallet.writs, 144)
def test_new_wallet_has_0_esteem(self):
wallet = Wallet.objects.get(user=self.user)
self.assertEqual(wallet.esteem, 0)
class TokenCreationTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="capman@test.io")
def test_coin_on_a_string_created_for_new_user(self):
self.assertTrue(
Token.objects.filter(user=self.user, token_type=Token.COIN).exists()
)
def test_free_token_created_for_new_user(self):
self.assertTrue(
Token.objects.filter(user=self.user, token_type=Token.FREE).exists()
)
def test_coin_on_a_string_has_no_expiry(self):
coin = Token.objects.get(user=self.user, token_type=Token.COIN)
self.assertIsNone(coin.expires_at)
def test_free_token_has_expiry_within_7_days(self):
free = Token.objects.get(user=self.user, token_type=Token.FREE)
self.assertIsNotNone(free.expires_at)
delta = free.expires_at - timezone.now()
self.assertLessEqual(delta.days, 7)
self.assertGreater(delta.total_seconds(), 0)
def test_no_pass_token_for_regular_user(self):
self.assertFalse(
Token.objects.filter(user=self.user, token_type=Token.PASS).exists()
)
class SuperuserTokenCreationTest(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(
email="admin@test.io", password="secret"
)
def test_pass_token_created_for_superuser(self):
self.assertTrue(
Token.objects.filter(user=self.user, token_type=Token.PASS).exists()
)
def test_superuser_also_gets_coin_and_free_token(self):
self.assertTrue(
Token.objects.filter(user=self.user, token_type=Token.COIN).exists()
)
self.assertTrue(
Token.objects.filter(user=self.user, token_type=Token.FREE).exists()
)
class SuperuserNoteGrantTest(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(
email="admin@test.io", password="secret"
)
def test_superuser_gets_super_schizo_note(self):
from apps.drama.models import Note
self.assertTrue(Note.objects.filter(user=self.user, slug="super-schizo").exists())
def test_superuser_gets_super_nomad_note(self):
from apps.drama.models import Note
self.assertTrue(Note.objects.filter(user=self.user, slug="super-nomad").exists())
def test_regular_user_does_not_get_super_notes(self):
from apps.drama.models import Note
regular = User.objects.create(email="regular@test.io")
self.assertFalse(Note.objects.filter(user=regular, slug="super-schizo").exists())
self.assertFalse(Note.objects.filter(user=regular, slug="super-nomad").exists())
class WalletTooltipTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="wallet@test.io")
self.wallet = Wallet.objects.get(user=self.user)
def test_tooltip_name(self):
self.assertEqual(self.wallet.tooltip_name(), "Wallet")
def test_tooltip_description(self):
self.wallet.writs = 144
self.wallet.esteem = 12
self.assertEqual(self.wallet.tooltip_description(), "144 writs · 12 esteem")
def test_tooltip_shoptalk_returns_none(self):
self.assertIsNone(self.wallet.tooltip_shoptalk())
def test_tooltip_expiry_returns_none(self):
self.assertIsNone(self.wallet.tooltip_expiry())
def test_tooltip_text(self):
self.assertEqual(self.wallet.tooltip_text(), "Wallet: 144 writs · 0 esteem")
class TokenTooltipTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="tokens@test.io")
def test_tithe_tooltip_description(self):
tithe = Token.objects.create(user=self.user, token_type=Token.TITHE)
self.assertEqual(tithe.tooltip_description(), "+ Writ bonus")
def test_tooltip_description_empty_fallback(self):
# token_type other than COIN/FREE/TITHE hits the bare return ""
token = Token(user=self.user, token_type="")
self.assertEqual(token.tooltip_description(), "")
def test_tooltip_expiry_empty_when_no_expiry_and_not_coin(self):
free = Token.objects.get(user=self.user, token_type=Token.FREE)
free.expires_at = None
self.assertEqual(free.tooltip_expiry(), "")
def test_tooltip_shoptalk_none_for_free_coin(self):
free = Token.objects.get(user=self.user, token_type=Token.FREE)
self.assertEqual(free.tooltip_shoptalk(), "a spot of good fortune")
def test_tooltip_room_html_returns_empty_when_no_room(self):
token = Token.objects.get(user=self.user, token_type=Token.COIN)
self.assertEqual(token.tooltip_room_html(), "")
class EquippedTrinketTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="equip@test.io", is_staff=True)
self.pass_token = self.user.tokens.get(token_type=Token.PASS)
def test_normal_user_equipped_trinket_defaults_to_coin(self):
user = User.objects.create(email="noequip@test.io")
coin = user.tokens.get(token_type=Token.COIN)
self.assertEqual(user.equipped_trinket, coin)
def test_staff_user_equipped_trinket_defaults_to_pass(self):
self.assertEqual(self.user.equipped_trinket, self.pass_token)
def test_equipped_trinket_can_be_set_to_pass(self):
self.user.equipped_trinket = self.pass_token
self.user.save(update_fields=["equipped_trinket"])
self.assertEqual(
User.objects.get(pk=self.user.pk).equipped_trinket, self.pass_token
)
def test_equipped_trinket_can_be_set_to_carte(self):
carte = Token.objects.create(user=self.user, token_type=Token.CARTE)
self.user.equipped_trinket = carte
self.user.save(update_fields=["equipped_trinket"])
self.assertEqual(
User.objects.get(pk=self.user.pk).equipped_trinket, carte
)
def test_equipped_trinket_can_be_cleared(self):
self.user.equipped_trinket = self.pass_token
self.user.save(update_fields=["equipped_trinket"])
self.user.equipped_trinket = None
self.user.save(update_fields=["equipped_trinket"])
self.assertIsNone(User.objects.get(pk=self.user.pk).equipped_trinket)
class CarteTokenCreationTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="carte@test.io")
def test_carte_token_can_be_created(self):
token = Token.objects.create(user=self.user, token_type=Token.CARTE)
self.assertEqual(Token.objects.get(pk=token.pk).token_type, Token.CARTE)
def test_carte_has_no_expiry(self):
token = Token.objects.create(user=self.user, token_type=Token.CARTE)
self.assertIsNone(token.expires_at)
class EquippedDeckTest(TestCase):
def test_new_user_gets_earthman_as_default_deck(self):
from apps.epic.models import DeckVariant
earthman = DeckVariant.objects.get(slug="earthman")
user = User.objects.create(email="deck@test.io")
user.refresh_from_db()
self.assertEqual(user.equipped_deck, earthman)
def test_fiorentine_is_not_auto_assigned_to_new_users(self):
from apps.epic.models import DeckVariant
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
user = User.objects.create(email="deck2@test.io")
user.refresh_from_db()
self.assertNotEqual(user.equipped_deck, fiorentine)
def test_equipped_deck_can_be_switched(self):
from apps.epic.models import DeckVariant
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
user = User.objects.create(email="deck3@test.io")
user.equipped_deck = fiorentine
user.save(update_fields=["equipped_deck"])
self.assertEqual(User.objects.get(pk=user.pk).equipped_deck, fiorentine)
class PaymentMethodTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="pay@test.io")
def test_str(self):
pm = PaymentMethod(user=self.user, stripe_pm_id="pm_123", last4="4242", brand="Visa")
self.assertEqual(str(pm), "Visa ....4242")