from email.parser import BytesParser from email.policy import default import pytest tk = pytest.importorskip("tkinter") from ccma.config import AppConfig # noqa: E402 from ccma.domain.mail_templates import placeholder_entries # noqa: E402 from ccma.services.welcome_mail import generate_and_send_welcome_mail # noqa: E402 from ccma.storage.repository import MemberRepository # noqa: E402 # One root for the whole session: the icon library binds its images to the first # Tk instance, so tearing a root down between tests would invalidate them. @pytest.fixture(scope="session") def tk_root(): from ccma.ui.theme import load_theme try: root = tk.Tk() except tk.TclError as exc: # headless CI has no display to build widgets on pytest.skip(f"kein Display verfügbar: {exc}") root.withdraw() load_theme(root, "dark") yield root root.destroy() @pytest.fixture def repository(tmp_path): repository = MemberRepository(tmp_path / "store") repository.initialize() organization = repository.get_configuration()["organization"] organization.update( { "name": "Chaos Computer Club Mannheim e.V.", "email": "verwaltung@example.org", "iban": "DE98670505050038907751", "bic": "MANSDE66XXX", } ) repository.save_organization(organization) return repository def _open_options(tk_root, repository): from ccma.ui.options_dialog import OptionsDialog dialog = OptionsDialog(tk_root, AppConfig(store_path=str(repository.root)), repository) tk_root.update() return dialog def _block_row(dialog): for row in dialog.mail_template_placeholders.get_children(): label = str(dialog.mail_template_placeholders.item(row, "values")[0]) if label.startswith("{{#claims}}"): return row raise AssertionError("Der Wiederholungsblock fehlt in der Platzhalterliste.") def test_double_click_on_a_block_is_wired_up(tk_root, repository): dialog = _open_options(tk_root, repository) try: assert dialog.mail_template_placeholders.bind("") assert dialog.mail_template_snippets[_block_row(dialog)] == next( entry.snippet for entry in placeholder_entries("welcome") if entry.label.startswith("{{#claims}}") ) finally: dialog.grab_release() dialog.destroy() def test_inserting_a_block_puts_editable_template_code_into_the_body(tk_root, repository): dialog = _open_options(tk_root, repository) try: dialog.mail_template_body.delete("1.0", "end") dialog.mail_template_body.insert("1.0", "Offen sind aktuell:") dialog.mail_template_placeholders.selection_set(_block_row(dialog)) dialog._insert_mail_placeholder() tk_root.update() body = dialog.mail_template_body.get("1.0", "end-1c") finally: dialog.grab_release() dialog.destroy() assert "…" not in body assert "{{#claims}}" in body and "{{/claims}}" in body assert "{{claim.description}}" in body # The block was appended behind existing text and still starts on its own line. assert body.splitlines()[0] == "Offen sind aktuell:" assert body.splitlines()[1] == "{{#claims}}" def test_edited_template_survives_saving_and_produces_real_lines_in_the_mail( tk_root, repository, tmp_path ): member = repository.create_member( first_name="Ada", last_name="Lovelace", birth_date="1990-01-01" ) member.email = "ada@example.org" member.accepted_at = "2026-08-20" repository.save_member(member) repository.create_manual_claim( member.member_id, title="Aufnahmegebühr", amount="15.00", due_date="2026-09-17" ) dialog = _open_options(tk_root, repository) try: dialog.mail_template_subject_var.set("Willkommen, {{member.first_name}}") dialog.mail_template_body.delete("1.0", "end") dialog.mail_template_body.insert("1.0", "Offen sind aktuell:\n") dialog.mail_template_placeholders.selection_set(_block_row(dialog)) dialog._insert_mail_placeholder() tk_root.update() dialog._save_mail_templates() finally: dialog.grab_release() dialog.destroy() generated = generate_and_send_welcome_mail( repository, member.member_id, delivery_mode="local", output_path=tmp_path / "Willkommen.eml", sender_name="Verwaltung C3MA", sender_email="verwaltung@example.org", signature="Der Vorstand", ) content = BytesParser(policy=default).parsebytes( generated.export_path.read_bytes() ).get_content() assert "Aufnahmegebühr (fällig 17.09.2026): 15.00 Euro" in content assert "…" not in content