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)