122 lines
5.1 KiB
Python
122 lines
5.1 KiB
Python
|
|
"""ITs for `apps.billboard.tax.log_tax_debit` — the @taxman-authored
|
||
|
|
"Debits & credits" ledger (user-spec 2026-05-26).
|
||
|
|
|
||
|
|
Mirrors the shape of `apps.drama.tests.integrated.test_note_brief` for
|
||
|
|
`Note.grant_if_new` — each spend appends a Line + spawns a Brief on the
|
||
|
|
user's single TAX_LEDGER Post."""
|
||
|
|
|
||
|
|
from django.test import TestCase
|
||
|
|
|
||
|
|
from apps.billboard.models import Brief, Line, Post, TAX_LEDGER_POST_TITLE
|
||
|
|
from apps.billboard.tax import (
|
||
|
|
TAX_DEBIT_TEMPLATES,
|
||
|
|
log_tax_debit,
|
||
|
|
)
|
||
|
|
from apps.lyric.models import User, get_or_create_taxman
|
||
|
|
|
||
|
|
|
||
|
|
class LogTaxDebitTest(TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.user = User.objects.create(email="tax@test.io")
|
||
|
|
|
||
|
|
def test_free_draw_locked_creates_post_line_brief(self):
|
||
|
|
post, line, brief = log_tax_debit(self.user, "free_draw_locked")
|
||
|
|
# Post created with the canonical title + correct kind
|
||
|
|
self.assertEqual(post.owner, self.user)
|
||
|
|
self.assertEqual(post.kind, Post.KIND_TAX_LEDGER)
|
||
|
|
self.assertEqual(post.title, TAX_LEDGER_POST_TITLE)
|
||
|
|
# Line authored by @taxman + admin_solicited
|
||
|
|
self.assertEqual(line.post, post)
|
||
|
|
self.assertEqual(line.author, get_or_create_taxman())
|
||
|
|
self.assertTrue(line.admin_solicited)
|
||
|
|
# Brief points at the Post + Line w. correct kind
|
||
|
|
self.assertEqual(brief.owner, self.user)
|
||
|
|
self.assertEqual(brief.post, post)
|
||
|
|
self.assertEqual(brief.line, line)
|
||
|
|
self.assertEqual(brief.kind, Brief.KIND_TAX_LEDGER)
|
||
|
|
self.assertEqual(brief.title, TAX_LEDGER_POST_TITLE)
|
||
|
|
|
||
|
|
def test_paid_draw_locked_uses_paid_template_text(self):
|
||
|
|
_, line, _ = log_tax_debit(self.user, "paid_draw_locked")
|
||
|
|
self.assertIn("PAID DRAW", line.text)
|
||
|
|
# `GATE VIEW` is wrapped in a `.btn-pri-name` span for inline styling
|
||
|
|
# parity w. the actual button label (user-spec 2026-05-26), so the
|
||
|
|
# raw text has HTML between "depositing a Token in " + "GATE VIEW".
|
||
|
|
self.assertIn("depositing a Token in", line.text)
|
||
|
|
self.assertIn("GATE VIEW", line.text)
|
||
|
|
|
||
|
|
def test_free_draw_locked_uses_free_template_text(self):
|
||
|
|
_, line, _ = log_tax_debit(self.user, "free_draw_locked")
|
||
|
|
self.assertIn("FREE DRAW", line.text)
|
||
|
|
self.assertIn("24h from the production of this log", line.text)
|
||
|
|
|
||
|
|
def test_two_spends_share_one_post_with_two_lines(self):
|
||
|
|
"""Like Note unlocks: one Post per user, growing thread of Lines."""
|
||
|
|
log_tax_debit(self.user, "free_draw_locked")
|
||
|
|
log_tax_debit(self.user, "paid_draw_locked")
|
||
|
|
posts = Post.objects.filter(owner=self.user, kind=Post.KIND_TAX_LEDGER)
|
||
|
|
self.assertEqual(posts.count(), 1)
|
||
|
|
self.assertEqual(posts.first().lines.count(), 2)
|
||
|
|
|
||
|
|
def test_two_free_draws_produce_distinct_lines(self):
|
||
|
|
"""Each spend produces a UNIQUE Line — text is timestamp-prefixed so
|
||
|
|
a second identical-slug spend doesn't collide with `unique_together
|
||
|
|
= (post, text)`."""
|
||
|
|
log_tax_debit(self.user, "free_draw_locked")
|
||
|
|
log_tax_debit(self.user, "free_draw_locked")
|
||
|
|
post = Post.objects.get(owner=self.user, kind=Post.KIND_TAX_LEDGER)
|
||
|
|
line_texts = list(post.lines.values_list("text", flat=True))
|
||
|
|
self.assertEqual(len(line_texts), 2)
|
||
|
|
self.assertNotEqual(line_texts[0], line_texts[1])
|
||
|
|
|
||
|
|
def test_unknown_slug_raises(self):
|
||
|
|
with self.assertRaises(KeyError):
|
||
|
|
log_tax_debit(self.user, "no_such_slug")
|
||
|
|
|
||
|
|
def test_template_keys_cover_both_known_slugs(self):
|
||
|
|
self.assertIn("free_draw_locked", TAX_DEBIT_TEMPLATES)
|
||
|
|
self.assertIn("paid_draw_locked", TAX_DEBIT_TEMPLATES)
|
||
|
|
|
||
|
|
|
||
|
|
class UnsolicitedLineGuardTest(TestCase):
|
||
|
|
"""post_save guard nukes any Line saved on a TAX_LEDGER Post w.o.
|
||
|
|
admin_solicited=True — mirrors the NOTE_UNLOCK defense-in-depth."""
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.user = User.objects.create(email="guard@test.io")
|
||
|
|
self.taxman = get_or_create_taxman()
|
||
|
|
self.post = Post.objects.create(
|
||
|
|
owner=self.user,
|
||
|
|
kind=Post.KIND_TAX_LEDGER,
|
||
|
|
title=TAX_LEDGER_POST_TITLE,
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_unsolicited_line_on_tax_ledger_gets_deleted(self):
|
||
|
|
Line.objects.create(
|
||
|
|
post=self.post, text="impostor", author=self.taxman,
|
||
|
|
admin_solicited=False,
|
||
|
|
)
|
||
|
|
self.assertEqual(self.post.lines.count(), 0)
|
||
|
|
|
||
|
|
def test_solicited_line_on_tax_ledger_survives(self):
|
||
|
|
Line.objects.create(
|
||
|
|
post=self.post, text="legit", author=self.taxman,
|
||
|
|
admin_solicited=True,
|
||
|
|
)
|
||
|
|
self.assertEqual(self.post.lines.count(), 1)
|
||
|
|
|
||
|
|
|
||
|
|
class TaxmanReservedUsernameTest(TestCase):
|
||
|
|
"""`taxman` joins `adman` as a reserved system-author handle."""
|
||
|
|
|
||
|
|
def test_taxman_is_reserved(self):
|
||
|
|
from apps.lyric.models import is_reserved_username
|
||
|
|
self.assertTrue(is_reserved_username("taxman"))
|
||
|
|
self.assertTrue(is_reserved_username("TAXMAN")) # case-insensitive
|
||
|
|
|
||
|
|
def test_get_or_create_taxman_is_idempotent(self):
|
||
|
|
a = get_or_create_taxman()
|
||
|
|
b = get_or_create_taxman()
|
||
|
|
self.assertEqual(a.pk, b.pk)
|
||
|
|
self.assertEqual(a.email, "taxman@earthmanrpg.local")
|