mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
The placeholder chooser showed repeat blocks as "{{#claims}} … {{/claims}}" and
inserted exactly that on double-click -- the ellipsis is a label, so the saved
template rendered a literal "…" per claim instead of the claim.
Chooser rows are data now: each carries the text it reads as and, separately,
the snippet it inserts. A block contributes a complete, ready-to-edit block with
a sample line built from its own placeholders, and lands on a line of its own
when the cursor sits behind existing text.
Covered from both sides: the snippets are checked against the template validator
and renderer, and a UI test drives the real dialog -- insert, save, send -- and
asserts the mail carries actual claim lines. The Tk tests share one root (the
icon library binds its images to the first one) and skip without a display.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
import pytest
|
|
|
|
from ccma.domain.mail_templates import (
|
|
MAIL_TEMPLATES,
|
|
MailTemplate,
|
|
MailTemplateError,
|
|
default_mail_template,
|
|
parse_mail_template,
|
|
placeholder_entries,
|
|
render_mail_template,
|
|
serialize_mail_template,
|
|
validate_mail_template,
|
|
)
|
|
from ccma.storage.repository import MemberRepository, RepositoryError
|
|
|
|
|
|
def test_subject_header_round_trips_through_file_format() -> None:
|
|
text = serialize_mail_template("Hallo {{member.first_name}}", "Zeile eins\nZeile zwei")
|
|
|
|
template = parse_mail_template(text)
|
|
|
|
assert text.startswith("Betreff: Hallo {{member.first_name}}\n\n")
|
|
assert template == MailTemplate("Hallo {{member.first_name}}", "Zeile eins\nZeile zwei")
|
|
|
|
|
|
def test_shipped_defaults_only_use_declared_placeholders() -> None:
|
|
for spec in MAIL_TEMPLATES:
|
|
template = default_mail_template(spec.key)
|
|
validate_mail_template(spec.key, template.subject, template.body)
|
|
|
|
|
|
def test_rendering_fills_values_and_expands_repeat_blocks() -> None:
|
|
template = MailTemplate(
|
|
"Beitrag für {{member.first_name}}",
|
|
"Hallo {{member.first_name}},\n"
|
|
"{{#claims}}- {{claim.title}}: {{claim.amount}}{{/claims}}\n"
|
|
"Summe: {{claims.total}}",
|
|
)
|
|
|
|
rendered = render_mail_template(
|
|
template,
|
|
{"member.first_name": "Ada", "claims.total": "75.00"},
|
|
{
|
|
"claims": [
|
|
{"claim.title": "Aufnahmegebühr", "claim.amount": "15.00"},
|
|
{"claim.title": "Mitgliedsbeitrag", "claim.amount": "60.00"},
|
|
]
|
|
},
|
|
)
|
|
|
|
assert rendered.subject == "Beitrag für Ada"
|
|
assert rendered.body == (
|
|
"Hallo Ada,\n- Aufnahmegebühr: 15.00\n- Mitgliedsbeitrag: 60.00\nSumme: 75.00"
|
|
)
|
|
|
|
|
|
def test_line_holding_only_an_empty_placeholder_is_dropped() -> None:
|
|
template = MailTemplate("Betreff", "Erste Zeile\n{{reminder.hint}}\nLetzte Zeile")
|
|
|
|
assert render_mail_template(template, {"reminder.hint": ""}).body == "Erste Zeile\nLetzte Zeile"
|
|
assert (
|
|
render_mail_template(template, {"reminder.hint": "Hinweis: X"}).body
|
|
== "Erste Zeile\nHinweis: X\nLetzte Zeile"
|
|
)
|
|
|
|
|
|
def test_unknown_placeholder_is_rejected_while_editing() -> None:
|
|
with pytest.raises(MailTemplateError, match="member.favourite_colour"):
|
|
validate_mail_template("welcome", "Betreff", "Hallo {{member.favourite_colour}}")
|
|
|
|
|
|
def test_store_copies_are_seeded_editable_and_resettable(tmp_path) -> None:
|
|
repository = MemberRepository(tmp_path)
|
|
repository.initialize()
|
|
path = repository.mail_templates_root / "willkommen.txt"
|
|
assert path.is_file()
|
|
|
|
repository.save_mail_template("welcome", subject="Moin {{member.first_name}}", body="Kurz und knapp.")
|
|
assert repository.get_mail_template("welcome") == MailTemplate(
|
|
"Moin {{member.first_name}}", "Kurz und knapp."
|
|
)
|
|
assert "Moin {{member.first_name}}" in path.read_text(encoding="utf-8")
|
|
|
|
repository.reset_mail_template("welcome")
|
|
assert repository.get_mail_template("welcome") == default_mail_template("welcome")
|
|
|
|
|
|
def test_missing_template_file_falls_back_to_shipped_default(tmp_path) -> None:
|
|
repository = MemberRepository(tmp_path)
|
|
repository.initialize()
|
|
(repository.mail_templates_root / "mahnung.txt").unlink()
|
|
|
|
template = repository.get_mail_template("reminder")
|
|
|
|
assert template == default_mail_template("reminder")
|
|
assert (repository.mail_templates_root / "mahnung.txt").is_file()
|
|
|
|
|
|
def test_saving_a_broken_template_is_refused(tmp_path) -> None:
|
|
repository = MemberRepository(tmp_path)
|
|
repository.initialize()
|
|
|
|
with pytest.raises(RepositoryError, match="Platzhalter"):
|
|
repository.save_mail_template("reminder", subject="Mahnung", body="Hallo {{member.nonsense}}")
|
|
with pytest.raises(RepositoryError, match="Betreff"):
|
|
repository.save_mail_template("reminder", subject=" ", body="Hallo")
|
|
|
|
|
|
def test_block_rows_insert_a_working_block_not_its_description() -> None:
|
|
entries = placeholder_entries("welcome")
|
|
block = next(entry for entry in entries if entry.label.startswith("{{#claims}}"))
|
|
|
|
# The label may abbreviate the block, the inserted snippet may not.
|
|
assert block.label == "{{#claims}} … {{/claims}}"
|
|
assert block.snippet.startswith("{{#claims}}\n")
|
|
assert block.snippet.rstrip().endswith("{{/claims}}")
|
|
assert "{{claim.description}}" in block.snippet
|
|
assert "…" not in block.snippet
|
|
assert all("…" not in entry.snippet for entry in entries)
|
|
|
|
|
|
def test_inserted_block_validates_and_renders_one_line_per_claim() -> None:
|
|
block = next(
|
|
entry for entry in placeholder_entries("welcome") if entry.label.startswith("{{#claims}}")
|
|
)
|
|
body = f"Hallo {{{{member.first_name}}}},\n\n{block.snippet}\nSumme: {{{{claims.total}}}}"
|
|
|
|
validate_mail_template("welcome", "Betreff", body)
|
|
rendered = render_mail_template(
|
|
MailTemplate("Betreff", body),
|
|
{"member.first_name": "Ada", "claims.total": "75.00"},
|
|
{
|
|
"claims": [
|
|
{
|
|
"claim.description": "Aufnahmegebühr",
|
|
"claim.due_date": "17.09.2026",
|
|
"claim.balance": "15.00",
|
|
},
|
|
{
|
|
"claim.description": "Mitgliedsbeitrag 2. Halbjahr 2026",
|
|
"claim.due_date": "17.09.2026",
|
|
"claim.balance": "60.00",
|
|
},
|
|
]
|
|
},
|
|
)
|
|
|
|
assert "Aufnahmegebühr (fällig 17.09.2026): 15.00 Euro" in rendered.body
|
|
assert "Mitgliedsbeitrag 2. Halbjahr 2026 (fällig 17.09.2026): 60.00 Euro" in rendered.body
|
|
assert "…" not in rendered.body
|
|
|
|
|
|
def test_every_block_snippet_is_valid_in_its_own_template() -> None:
|
|
for spec in MAIL_TEMPLATES:
|
|
for block in spec.blocks:
|
|
validate_mail_template(spec.key, "Betreff", block.snippet)
|