mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
Merge branch 'dev' into feature/mail-templates
dev gained the delivery/rollback semantics for the dunning and SEPA mails while the mail texts were being moved into templates here. Both services keep dev's flow -- roll back only while nothing has gone out, log the event even when archiving fails afterwards -- and render their content from the template. The welcome mail follows the same rule now, including its archiving-failure path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,11 @@ 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):
|
||||
@@ -200,3 +202,214 @@ def test_reminder_mail_send_mode_also_copies_to_sent_folder_when_enabled(tmp_pat
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_reminder_mail_archiving_failure_after_smtp_success_does_not_revert(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)
|
||||
monkeypatch.setattr(reminder_mail_module, "send_via_smtp", lambda client, content: None)
|
||||
|
||||
real_replace = reminder_mail_module.os.replace
|
||||
|
||||
def _flaky_replace(src, dst):
|
||||
# Only the final archive move (into the "Mahnungen" folder) should fail --
|
||||
# the mail has already been handed to send_via_smtp by that point.
|
||||
if str(dst).endswith(".eml") and "Mahnungen" in str(dst):
|
||||
raise OSError("disk full")
|
||||
return real_replace(src, dst)
|
||||
|
||||
monkeypatch.setattr(reminder_mail_module.os, "replace", _flaky_replace)
|
||||
|
||||
with pytest.raises(RepositoryError, match="zugestellt"):
|
||||
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"] == "sent", "mail was already sent -- must not be un-sent"
|
||||
assert stored_reminder.get("fee_item_ids")
|
||||
events = repository.get_events(member.member_id)
|
||||
assert events[-1].event_type == "reminder_email_sent"
|
||||
assert events[-1].data["archive_error"]
|
||||
assert "document" not in events[-1].references
|
||||
|
||||
|
||||
def test_reminder_mail_archive_dir_creation_failure_reverts_sent_status(tmp_path, monkeypatch):
|
||||
from pathlib import 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="Zahlungserinnerung",
|
||||
payment_deadline_days=14,
|
||||
items=[{"description": "Mahngebühr", "amount": "5.00"}],
|
||||
)
|
||||
|
||||
original_mkdir = Path.mkdir
|
||||
|
||||
def _flaky_mkdir(self, *args, **kwargs):
|
||||
if self.name == "Mahnungen":
|
||||
raise OSError("permission denied")
|
||||
return original_mkdir(self, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(Path, "mkdir", _flaky_mkdir)
|
||||
|
||||
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)
|
||||
stored_reminder = data.reminders[0]
|
||||
assert stored_reminder["status"] == "draft"
|
||||
assert not stored_reminder.get("fee_item_ids")
|
||||
|
||||
@@ -388,3 +388,288 @@ def test_generated_mail_send_mode_copies_to_sent_folder_reusing_one_imap_connect
|
||||
assert len(imap_connections) == 1, "one IMAP connection should be reused for the whole batch"
|
||||
assert ensured == ["INBOX.Gesendet"]
|
||||
assert appended == [("imap-client", "INBOX.Gesendet", r"(\Seen)")] * 2
|
||||
|
||||
|
||||
def test_generated_mail_archiving_failure_after_smtp_success_keeps_batch_going(tmp_path, monkeypatch):
|
||||
import ccma.services.sepa_mail as sepa_mail_module
|
||||
|
||||
repository, ada = _repository(tmp_path)
|
||||
second = repository.create_member(first_name="Grace", last_name="Hopper", member_number="C3-43")
|
||||
second.status = "active"
|
||||
second.email = "grace@example.org"
|
||||
second.account_holder = "Grace Hopper"
|
||||
second.iban = "DE89370400440532013000"
|
||||
second.bic = "COBADEFFXXX"
|
||||
second.mandate_reference = "MANDAT-43"
|
||||
second.mandate_signed_at = "2025-01-10"
|
||||
second.mandate_active = True
|
||||
repository.save_member(second)
|
||||
repository.save_contributions(
|
||||
second.member_id,
|
||||
ContributionData(
|
||||
claims=[
|
||||
{
|
||||
"claim_id": "due",
|
||||
"title": "Mitgliedsbeitrag 2026",
|
||||
"amount": "150.00",
|
||||
"due_date": "2026-01-31",
|
||||
"status": "open",
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
organization = repository.get_configuration()["organization"]
|
||||
organization.update(
|
||||
{
|
||||
"name": "Chaos Computer Club Mannheim e.V.",
|
||||
"email": "verwaltung@example.org",
|
||||
"iban": "DE89370400440532013000",
|
||||
"bic": "COBADEFFXXX",
|
||||
"creditor_id": "DE98ZZZ09999999999",
|
||||
}
|
||||
)
|
||||
repository.save_organization(organization)
|
||||
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="",
|
||||
)
|
||||
debits, _warnings = pending_direct_debits(repository, due_until=date(2026, 12, 31))
|
||||
assert len(debits) == 2
|
||||
|
||||
monkeypatch.setattr(
|
||||
sepa_mail_module, "smtp_session", contextmanager(lambda settings: iter(["smtp-client"]))
|
||||
)
|
||||
sent = []
|
||||
monkeypatch.setattr(sepa_mail_module, "send_via_smtp", lambda client, content: sent.append(content))
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
original_write_bytes = Path.write_bytes
|
||||
|
||||
def _flaky_write_bytes(self, data):
|
||||
if "documents" in str(self) and "C3-43" in self.name:
|
||||
raise OSError("disk full")
|
||||
return original_write_bytes(self, data)
|
||||
|
||||
monkeypatch.setattr(Path, "write_bytes", _flaky_write_bytes)
|
||||
|
||||
generated, warnings = generate_debit_mails(
|
||||
repository,
|
||||
debits,
|
||||
collection_date=date(2026, 8, 3),
|
||||
delivery_mode="send",
|
||||
sender_name="Verwaltung C3MA",
|
||||
sender_email="verwaltung@example.org",
|
||||
signature="Der Vorstand",
|
||||
)
|
||||
|
||||
# Both mails were handed to SMTP -- a broken archive write for one of them must
|
||||
# not swallow the other, nor pretend the failing one was never sent.
|
||||
assert len(sent) == 2
|
||||
assert len(generated) == 2
|
||||
assert len(warnings) == 1
|
||||
assert "C3-43" in warnings[0] or "Grace" in warnings[0]
|
||||
|
||||
failing = next(g for g in generated if g.member_id == second.member_id)
|
||||
assert not failing.archive_path.is_file()
|
||||
succeeding = next(g for g in generated if g.member_id == ada.member_id)
|
||||
assert succeeding.archive_path.is_file()
|
||||
|
||||
grace_event = repository.get_events(second.member_id)[-1]
|
||||
assert grace_event.event_type == "sepa_notification_generated"
|
||||
assert grace_event.data["archive_error"]
|
||||
assert "document" not in grace_event.references
|
||||
ada_event = repository.get_events(ada.member_id)[-1]
|
||||
assert "document" in ada_event.references
|
||||
|
||||
|
||||
def test_generated_mail_smtp_failure_for_one_debit_does_not_abort_the_batch(tmp_path, monkeypatch):
|
||||
import ccma.services.sepa_mail as sepa_mail_module
|
||||
from ccma.storage.repository import RepositoryError
|
||||
|
||||
repository, ada = _repository(tmp_path)
|
||||
second = repository.create_member(first_name="Grace", last_name="Hopper", member_number="C3-43")
|
||||
second.status = "active"
|
||||
second.email = "grace@example.org"
|
||||
second.account_holder = "Grace Hopper"
|
||||
second.iban = "DE89370400440532013000"
|
||||
second.bic = "COBADEFFXXX"
|
||||
second.mandate_reference = "MANDAT-43"
|
||||
second.mandate_signed_at = "2025-01-10"
|
||||
second.mandate_active = True
|
||||
repository.save_member(second)
|
||||
repository.save_contributions(
|
||||
second.member_id,
|
||||
ContributionData(
|
||||
claims=[
|
||||
{
|
||||
"claim_id": "due",
|
||||
"title": "Mitgliedsbeitrag 2026",
|
||||
"amount": "150.00",
|
||||
"due_date": "2026-01-31",
|
||||
"status": "open",
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
organization = repository.get_configuration()["organization"]
|
||||
organization.update(
|
||||
{
|
||||
"name": "Chaos Computer Club Mannheim e.V.",
|
||||
"email": "verwaltung@example.org",
|
||||
"iban": "DE89370400440532013000",
|
||||
"bic": "COBADEFFXXX",
|
||||
"creditor_id": "DE98ZZZ09999999999",
|
||||
}
|
||||
)
|
||||
repository.save_organization(organization)
|
||||
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="",
|
||||
)
|
||||
debits, _warnings = pending_direct_debits(repository, due_until=date(2026, 12, 31))
|
||||
assert len(debits) == 2
|
||||
|
||||
monkeypatch.setattr(
|
||||
sepa_mail_module, "smtp_session", contextmanager(lambda settings: iter(["smtp-client"]))
|
||||
)
|
||||
sent = []
|
||||
|
||||
def _flaky_send(client, content):
|
||||
if b"ada@example.org" in content:
|
||||
raise RepositoryError("E-Mail konnte nicht versandt werden: connection reset")
|
||||
sent.append(content)
|
||||
|
||||
monkeypatch.setattr(sepa_mail_module, "send_via_smtp", _flaky_send)
|
||||
|
||||
generated, warnings = generate_debit_mails(
|
||||
repository,
|
||||
debits,
|
||||
collection_date=date(2026, 8, 3),
|
||||
delivery_mode="send",
|
||||
sender_name="Verwaltung C3MA",
|
||||
sender_email="verwaltung@example.org",
|
||||
signature="Der Vorstand",
|
||||
)
|
||||
|
||||
assert len(sent) == 1
|
||||
assert len(generated) == 1
|
||||
assert generated[0].member_id == second.member_id
|
||||
assert len(warnings) == 1
|
||||
assert "connection reset" in warnings[0]
|
||||
ada_events = repository.get_events(ada.member_id)
|
||||
assert all(event.event_type != "sepa_notification_generated" for event in ada_events)
|
||||
|
||||
|
||||
def test_generated_mail_archive_dir_creation_failure_for_one_debit_does_not_abort_the_batch(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
from pathlib import Path
|
||||
|
||||
repository, ada = _repository(tmp_path)
|
||||
second = repository.create_member(first_name="Grace", last_name="Hopper", member_number="C3-43")
|
||||
second.status = "active"
|
||||
second.email = "grace@example.org"
|
||||
second.account_holder = "Grace Hopper"
|
||||
second.iban = "DE89370400440532013000"
|
||||
second.bic = "COBADEFFXXX"
|
||||
second.mandate_reference = "MANDAT-43"
|
||||
second.mandate_signed_at = "2025-01-10"
|
||||
second.mandate_active = True
|
||||
repository.save_member(second)
|
||||
repository.save_contributions(
|
||||
second.member_id,
|
||||
ContributionData(
|
||||
claims=[
|
||||
{
|
||||
"claim_id": "due",
|
||||
"title": "Mitgliedsbeitrag 2026",
|
||||
"amount": "150.00",
|
||||
"due_date": "2026-01-31",
|
||||
"status": "open",
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
organization = repository.get_configuration()["organization"]
|
||||
organization.update(
|
||||
{
|
||||
"name": "Chaos Computer Club Mannheim e.V.",
|
||||
"email": "verwaltung@example.org",
|
||||
"iban": "DE89370400440532013000",
|
||||
"bic": "COBADEFFXXX",
|
||||
"creditor_id": "DE98ZZZ09999999999",
|
||||
}
|
||||
)
|
||||
repository.save_organization(organization)
|
||||
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="",
|
||||
)
|
||||
debits, _warnings = pending_direct_debits(repository, due_until=date(2026, 12, 31))
|
||||
assert len(debits) == 2
|
||||
|
||||
import ccma.services.sepa_mail as sepa_mail_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
sepa_mail_module, "smtp_session", contextmanager(lambda settings: iter(["smtp-client"]))
|
||||
)
|
||||
sent = []
|
||||
monkeypatch.setattr(sepa_mail_module, "send_via_smtp", lambda client, content: sent.append(content))
|
||||
|
||||
original_mkdir = Path.mkdir
|
||||
|
||||
def _flaky_mkdir(self, *args, **kwargs):
|
||||
if self.name == "SEPA" and second.member_id in str(self):
|
||||
raise OSError("permission denied")
|
||||
return original_mkdir(self, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(Path, "mkdir", _flaky_mkdir)
|
||||
|
||||
generated, warnings = generate_debit_mails(
|
||||
repository,
|
||||
debits,
|
||||
collection_date=date(2026, 8, 3),
|
||||
delivery_mode="send",
|
||||
sender_name="Verwaltung C3MA",
|
||||
sender_email="verwaltung@example.org",
|
||||
signature="Der Vorstand",
|
||||
)
|
||||
|
||||
# Grace's archive directory could not even be created -- nothing was sent to her,
|
||||
# so she must NOT show up as delivered, but Ada's mail must still go out.
|
||||
assert len(sent) == 1
|
||||
assert len(generated) == 1
|
||||
assert generated[0].member_id == ada.member_id
|
||||
assert len(warnings) == 1
|
||||
assert "Grace" in warnings[0] or "C3-43" in warnings[0]
|
||||
|
||||
@@ -219,3 +219,51 @@ def test_member_without_email_is_refused(tmp_path):
|
||||
sender_email="verwaltung@example.org",
|
||||
signature="Der Vorstand",
|
||||
)
|
||||
|
||||
|
||||
def test_archiving_failure_after_smtp_success_still_logs_the_sent_mail(tmp_path, monkeypatch):
|
||||
import ccma.services.welcome_mail as welcome_mail_module
|
||||
|
||||
repository, member = _new_member_repository(tmp_path)
|
||||
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="",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
welcome_mail_module, "smtp_session", contextmanager(lambda settings: iter(["client"]))
|
||||
)
|
||||
monkeypatch.setattr(welcome_mail_module, "send_via_smtp", lambda client, content: None)
|
||||
real_replace = welcome_mail_module.os.replace
|
||||
|
||||
def _flaky_replace(src, dst):
|
||||
# Only the final move into the member file fails -- the mail is out by then.
|
||||
if str(dst).endswith(".eml") and "Willkommen" in str(dst):
|
||||
raise OSError("disk full")
|
||||
return real_replace(src, dst)
|
||||
|
||||
monkeypatch.setattr(welcome_mail_module.os, "replace", _flaky_replace)
|
||||
|
||||
with pytest.raises(RepositoryError, match="zugestellt"):
|
||||
generate_and_send_welcome_mail(
|
||||
repository,
|
||||
member.member_id,
|
||||
delivery_mode="send",
|
||||
sender_name="Verwaltung C3MA",
|
||||
sender_email="verwaltung@example.org",
|
||||
signature="Der Vorstand",
|
||||
)
|
||||
|
||||
event = repository.get_events(member.member_id)[-1]
|
||||
assert event.event_type == "welcome_email_sent"
|
||||
assert event.data["archive_error"]
|
||||
assert "document" not in event.references
|
||||
|
||||
Reference in New Issue
Block a user