mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
Refuse the welcome mail on a read-only store before it renders anything
generate_and_send_welcome_mail() arrived with the mail templates, after the read-only guards were added to the other services, and never got one. On a read-only store it therefore rendered the mail, could hand it to the mail server, and only failed when it tried to create the archive directory in the member file -- surfacing a PermissionError instead of the ReadOnlyStoreError every other write path reports. The guard now sits at the top, next to the delivery-mode check, so nothing is rendered, sent or written. The read-only test covers this path (and the SEPA batch alongside it) and asserts that nothing at all was left behind: no export file, no archive directory, no "sent" event. Its member carries an e-mail address now -- without one the mail services bail out for that reason, and the write the test exists for is never reached. Note that the SEPA CSV/XML export keeps writing without a guard on purpose: it writes to a path the board picks outside the store, which a read-only store has no say over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a841c9bdd
commit
e87f859101
@@ -141,6 +141,9 @@ def generate_and_send_welcome_mail(
|
|||||||
) -> GeneratedWelcomeMail:
|
) -> GeneratedWelcomeMail:
|
||||||
if delivery_mode not in {"local", "send", "drafts"}:
|
if delivery_mode not in {"local", "send", "drafts"}:
|
||||||
raise RepositoryError(f"Unbekannter Versandmodus: {delivery_mode}")
|
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)
|
member = repository.get_member(member_id)
|
||||||
if not member.email.strip():
|
if not member.email.strip():
|
||||||
raise RepositoryError("Für das Mitglied ist keine E-Mail-Adresse hinterlegt.")
|
raise RepositoryError("Für das Mitglied ist keine E-Mail-Adresse hinterlegt.")
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from ccma.domain.models import ContributionData
|
|||||||
from ccma.services.documents import DocumentService
|
from ccma.services.documents import DocumentService
|
||||||
from ccma.services.housekeeper import Housekeeper
|
from ccma.services.housekeeper import Housekeeper
|
||||||
from ccma.services.reminder_mail import generate_and_send_reminder_mail
|
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
|
from ccma.storage.repository import MemberRepository, ReadOnlyStoreError
|
||||||
|
|
||||||
# Root ignores the permission bits this test relies on, so the read-only mount it
|
# 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(
|
member = repository.create_member(
|
||||||
first_name="Ada", last_name="Lovelace", birth_date="1990-01-01"
|
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(
|
repository.save_contributions(
|
||||||
member.member_id,
|
member.member_id,
|
||||||
ContributionData(
|
ContributionData(
|
||||||
@@ -126,6 +133,53 @@ def test_documents_and_mails_refuse_before_touching_the_member_file(read_only_st
|
|||||||
sender_email="verwaltung@example.org",
|
sender_email="verwaltung@example.org",
|
||||||
signature="Der Vorstand",
|
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):
|
def test_uninitialized_read_only_store_reports_why_it_cannot_be_opened(tmp_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user