diff --git a/src/ccma/services/welcome_mail.py b/src/ccma/services/welcome_mail.py index 6ef0d41..08c95f3 100644 --- a/src/ccma/services/welcome_mail.py +++ b/src/ccma/services/welcome_mail.py @@ -141,6 +141,9 @@ def generate_and_send_welcome_mail( ) -> GeneratedWelcomeMail: if delivery_mode not in {"local", "send", "drafts"}: raise RepositoryError(f"Unbekannter Versandmodus: {delivery_mode}") + # The mail is archived in the member file, so a read-only store rules the whole + # path out -- said here, before anything is rendered or handed to a mail server. + repository.assert_writable() member = repository.get_member(member_id) if not member.email.strip(): raise RepositoryError("Für das Mitglied ist keine E-Mail-Adresse hinterlegt.") diff --git a/tests/test_read_only_store.py b/tests/test_read_only_store.py index 49b0335..534f092 100644 --- a/tests/test_read_only_store.py +++ b/tests/test_read_only_store.py @@ -8,6 +8,8 @@ from ccma.domain.models import ContributionData from ccma.services.documents import DocumentService from ccma.services.housekeeper import Housekeeper from ccma.services.reminder_mail import generate_and_send_reminder_mail +from ccma.services.sepa_mail import generate_debit_mails +from ccma.services.welcome_mail import generate_and_send_welcome_mail from ccma.storage.repository import MemberRepository, ReadOnlyStoreError # Root ignores the permission bits this test relies on, so the read-only mount it @@ -32,6 +34,11 @@ def read_only_store(tmp_path): member = repository.create_member( first_name="Ada", last_name="Lovelace", birth_date="1990-01-01" ) + # A complete member: without an address the mail services would bail out early + # for that reason, and the write they are actually being tested for is never + # reached. + member.email = "ada@example.org" + repository.save_member(member) repository.save_contributions( member.member_id, ContributionData( @@ -126,6 +133,53 @@ def test_documents_and_mails_refuse_before_touching_the_member_file(read_only_st sender_email="verwaltung@example.org", signature="Der Vorstand", ) + with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"): + generate_and_send_welcome_mail( + repository, + member.member_id, + delivery_mode="local", + output_path=tmp_path / "Willkommen.eml", + sender_name="Verwaltung", + sender_email="verwaltung@example.org", + signature="Der Vorstand", + ) + with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"): + generate_debit_mails( + repository, + [], + collection_date=date(2026, 9, 1), + delivery_mode="local", + output_directory=tmp_path / "sepa", + sender_name="Verwaltung", + sender_email="verwaltung@example.org", + signature="Der Vorstand", + ) + + +def test_the_welcome_mail_writes_nothing_at_all_when_refused(read_only_store, tmp_path): + """The refusal has to come before rendering, sending and archiving -- not out of + the failing write at the end, by which point the mail would already be out.""" + repository, member = read_only_store + export_path = tmp_path / "Willkommen.eml" + + with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"): + generate_and_send_welcome_mail( + repository, + member.member_id, + delivery_mode="local", + output_path=export_path, + sender_name="Verwaltung", + sender_email="verwaltung@example.org", + signature="Der Vorstand", + ) + + assert not export_path.exists() + files = repository.members_root / member.member_id / "files" + assert not (files / "documents" / "Willkommen").exists() + assert all( + event.event_type != "welcome_email_sent" + for event in repository.get_events(member.member_id) + ) def test_uninitialized_read_only_store_reports_why_it_cannot_be_opened(tmp_path):