mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
generate_and_send_reminder_mail marked a reminder as "sent" before attempting SMTP send/IMAP append. If the connection, login, or append failed, the reminder stayed persisted as sent even though no mail was ever delivered or filed, so it could not be corrected and resent. Wrap content generation, archiving, and delivery in a try/except that calls the existing revert_reminder_sent on any failure and re-raises, putting the reminder back into "draft" for all three delivery modes.
304 lines
10 KiB
Python
304 lines
10 KiB
Python
from contextlib import contextmanager
|
|
from email.parser import BytesParser
|
|
from email.policy import default
|
|
|
|
import pytest
|
|
from test_reminders import _overdue_claim_repository
|
|
|
|
from ccma.services.reminder_mail import generate_and_send_reminder_mail
|
|
from ccma.storage.repository import RepositoryError
|
|
|
|
|
|
def test_reminder_mail_is_archived_and_marks_reminder_sent(tmp_path):
|
|
repository, member = _overdue_claim_repository(tmp_path / "store")
|
|
member.email = "reminder@example.org"
|
|
repository.save_member(member)
|
|
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)
|
|
reminder = repository.create_reminder_draft(
|
|
member.member_id,
|
|
"claim-1",
|
|
level=1,
|
|
name="Zahlungserinnerung",
|
|
payment_deadline_days=14,
|
|
detail="Bitte Mitgliedsnummer angeben.",
|
|
)
|
|
export_path = tmp_path / "Zahlungserinnerung.eml"
|
|
|
|
generated = generate_and_send_reminder_mail(
|
|
repository,
|
|
member.member_id,
|
|
"claim-1",
|
|
reminder["reminder_id"],
|
|
delivery_mode="local",
|
|
output_path=export_path,
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
assert generated.export_path.read_bytes() == generated.archive_path.read_bytes()
|
|
message = BytesParser(policy=default).parsebytes(generated.export_path.read_bytes())
|
|
assert message["To"] == "reminder@example.org"
|
|
assert "Zahlungserinnerung" in message["Subject"]
|
|
assert "100.00 Euro" in message.get_content()
|
|
assert "DE98670505050038907751" in message.get_content()
|
|
data = repository.get_contributions(member.member_id)
|
|
assert data.reminders[0]["status"] == "sent"
|
|
assert data.reminders[0]["payment_deadline"]
|
|
assert repository.get_events(member.member_id)[-1].event_type == "reminder_email_sent"
|
|
assert generated.archive_path.parent.name == "Mahnungen"
|
|
|
|
|
|
def test_reminder_mail_includes_fee_in_open_balance(tmp_path):
|
|
repository, member = _overdue_claim_repository(tmp_path / "store")
|
|
member.email = "reminder@example.org"
|
|
repository.save_member(member)
|
|
reminder = repository.create_reminder_draft(
|
|
member.member_id,
|
|
"claim-1",
|
|
level=1,
|
|
name="Erste Mahnung",
|
|
payment_deadline_days=14,
|
|
items=[{"description": "Mahngebühr", "amount": "5.00"}],
|
|
)
|
|
|
|
generated = generate_and_send_reminder_mail(
|
|
repository,
|
|
member.member_id,
|
|
"claim-1",
|
|
reminder["reminder_id"],
|
|
delivery_mode="local",
|
|
output_path=tmp_path / "Mahnung.eml",
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
message = BytesParser(policy=default).parsebytes(generated.export_path.read_bytes())
|
|
assert "Offener Betrag: 105.00 Euro" in message.get_content()
|
|
assert "Mahngebühr: 5.00 Euro" in message.get_content()
|
|
|
|
|
|
def test_reminder_mail_send_mode_delivers_via_smtp_without_local_file(tmp_path, monkeypatch):
|
|
import ccma.services.reminder_mail as reminder_mail_module
|
|
|
|
repository, member = _overdue_claim_repository(tmp_path / "store")
|
|
member.email = "reminder@example.org"
|
|
repository.save_member(member)
|
|
repository.save_email_settings(
|
|
delivery_mode="send",
|
|
smtp_host="mail.example.org",
|
|
smtp_port=587,
|
|
smtp_security="none",
|
|
smtp_username="",
|
|
smtp_password="",
|
|
imap_host="",
|
|
imap_port=993,
|
|
imap_security="ssl",
|
|
imap_username="",
|
|
imap_password="",
|
|
imap_drafts_folder="",
|
|
)
|
|
reminder = repository.create_reminder_draft(
|
|
member.member_id,
|
|
"claim-1",
|
|
level=1,
|
|
name="Zahlungserinnerung",
|
|
payment_deadline_days=14,
|
|
)
|
|
|
|
sent = []
|
|
fake_smtp_session = contextmanager(lambda settings: iter([object()]))
|
|
monkeypatch.setattr(reminder_mail_module, "smtp_session", fake_smtp_session)
|
|
monkeypatch.setattr(
|
|
reminder_mail_module, "send_via_smtp", lambda client, content: sent.append(content)
|
|
)
|
|
|
|
generated = generate_and_send_reminder_mail(
|
|
repository,
|
|
member.member_id,
|
|
"claim-1",
|
|
reminder["reminder_id"],
|
|
delivery_mode="send",
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
assert generated.export_path is None
|
|
assert generated.archive_path.is_file()
|
|
assert len(sent) == 1
|
|
data = repository.get_contributions(member.member_id)
|
|
assert data.reminders[0]["status"] == "sent"
|
|
|
|
|
|
def test_reminder_mail_send_mode_also_copies_to_sent_folder_when_enabled(tmp_path, monkeypatch):
|
|
import ccma.services.reminder_mail as reminder_mail_module
|
|
|
|
repository, member = _overdue_claim_repository(tmp_path / "store")
|
|
member.email = "reminder@example.org"
|
|
repository.save_member(member)
|
|
repository.save_email_settings(
|
|
delivery_mode="send",
|
|
smtp_host="mail.example.org",
|
|
smtp_port=587,
|
|
smtp_security="none",
|
|
smtp_username="",
|
|
smtp_password="",
|
|
imap_host="mail.example.org",
|
|
imap_port=993,
|
|
imap_security="ssl",
|
|
imap_username="",
|
|
imap_password="",
|
|
imap_drafts_folder="",
|
|
imap_sent_enabled=True,
|
|
imap_sent_folder="INBOX.Gesendet",
|
|
)
|
|
reminder = repository.create_reminder_draft(
|
|
member.member_id,
|
|
"claim-1",
|
|
level=1,
|
|
name="Zahlungserinnerung",
|
|
payment_deadline_days=14,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
reminder_mail_module, "smtp_session", contextmanager(lambda settings: iter(["smtp-client"]))
|
|
)
|
|
monkeypatch.setattr(reminder_mail_module, "send_via_smtp", lambda client, content: None)
|
|
monkeypatch.setattr(
|
|
reminder_mail_module, "imap_session", contextmanager(lambda settings: iter(["imap-client"]))
|
|
)
|
|
ensured = []
|
|
appended = []
|
|
monkeypatch.setattr(
|
|
reminder_mail_module, "ensure_imap_folder", lambda client, folder: ensured.append((client, folder))
|
|
)
|
|
monkeypatch.setattr(
|
|
reminder_mail_module,
|
|
"append_message",
|
|
lambda client, content, *, folder, flags: appended.append((client, folder, flags)),
|
|
)
|
|
|
|
generate_and_send_reminder_mail(
|
|
repository,
|
|
member.member_id,
|
|
"claim-1",
|
|
reminder["reminder_id"],
|
|
delivery_mode="send",
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
assert ensured == [("imap-client", "INBOX.Gesendet")]
|
|
assert appended == [("imap-client", "INBOX.Gesendet", r"(\Seen)")]
|
|
|
|
|
|
def test_reminder_mail_send_failure_reverts_sent_status(tmp_path, monkeypatch):
|
|
import ccma.services.reminder_mail as reminder_mail_module
|
|
|
|
repository, member = _overdue_claim_repository(tmp_path / "store")
|
|
member.email = "reminder@example.org"
|
|
repository.save_member(member)
|
|
repository.save_email_settings(
|
|
delivery_mode="send",
|
|
smtp_host="mail.example.org",
|
|
smtp_port=587,
|
|
smtp_security="none",
|
|
smtp_username="",
|
|
smtp_password="",
|
|
imap_host="",
|
|
imap_port=993,
|
|
imap_security="ssl",
|
|
imap_username="",
|
|
imap_password="",
|
|
imap_drafts_folder="",
|
|
)
|
|
reminder = repository.create_reminder_draft(
|
|
member.member_id,
|
|
"claim-1",
|
|
level=1,
|
|
name="Zahlungserinnerung",
|
|
payment_deadline_days=14,
|
|
items=[{"description": "Mahngebühr", "amount": "5.00"}],
|
|
)
|
|
|
|
@contextmanager
|
|
def _fake_smtp_session(settings):
|
|
yield "smtp-client"
|
|
|
|
monkeypatch.setattr(reminder_mail_module, "smtp_session", _fake_smtp_session)
|
|
|
|
def _failing_send(client, content):
|
|
raise RepositoryError("E-Mail konnte nicht versandt werden: connection reset")
|
|
|
|
monkeypatch.setattr(reminder_mail_module, "send_via_smtp", _failing_send)
|
|
|
|
with pytest.raises(RepositoryError):
|
|
generate_and_send_reminder_mail(
|
|
repository,
|
|
member.member_id,
|
|
"claim-1",
|
|
reminder["reminder_id"],
|
|
delivery_mode="send",
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
data = repository.get_contributions(member.member_id)
|
|
stored_reminder = data.reminders[0]
|
|
assert stored_reminder["status"] == "draft"
|
|
assert not stored_reminder.get("fee_item_ids")
|
|
assert repository.get_events(member.member_id)[-1].event_type == "reminder_sent_reverted"
|
|
_data, claim = repository.get_claim(member.member_id, "claim-1")
|
|
assert not any(
|
|
item.get("description") == "Mahngebühr" for item in claim.get("items", [])
|
|
)
|
|
|
|
|
|
def test_reminder_mail_local_write_failure_reverts_sent_status(tmp_path, monkeypatch):
|
|
repository, member = _overdue_claim_repository(tmp_path / "store")
|
|
member.email = "reminder@example.org"
|
|
repository.save_member(member)
|
|
reminder = repository.create_reminder_draft(
|
|
member.member_id,
|
|
"claim-1",
|
|
level=1,
|
|
name="Zahlungserinnerung",
|
|
payment_deadline_days=14,
|
|
)
|
|
|
|
import ccma.services.reminder_mail as reminder_mail_module
|
|
|
|
def _boom(*args, **kwargs):
|
|
raise OSError("disk full")
|
|
|
|
monkeypatch.setattr(reminder_mail_module.os, "replace", _boom)
|
|
|
|
with pytest.raises(OSError):
|
|
generate_and_send_reminder_mail(
|
|
repository,
|
|
member.member_id,
|
|
"claim-1",
|
|
reminder["reminder_id"],
|
|
delivery_mode="local",
|
|
output_path=tmp_path / "Zahlungserinnerung.eml",
|
|
sender_name="Verwaltung C3MA",
|
|
sender_email="verwaltung@example.org",
|
|
signature="Der Vorstand",
|
|
)
|
|
|
|
data = repository.get_contributions(member.member_id)
|
|
assert data.reminders[0]["status"] == "draft"
|