feat: add durable mail delivery outbox
This commit is contained in:
294
tests/test_delivery_outbox.py
Normal file
294
tests/test_delivery_outbox.py
Normal file
@@ -0,0 +1,294 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy import Column, String, Table, create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_mail.backend.db.models import (
|
||||
MailDeliveryAttempt,
|
||||
MailDeliveryCommand,
|
||||
MailDeliveryReconciliation,
|
||||
MailServerProfile,
|
||||
)
|
||||
from govoplan_mail.backend.delivery_outbox import (
|
||||
MailDeliveryIdempotencyConflict,
|
||||
delivery_command_diagnostics,
|
||||
dispatch_due,
|
||||
purge_expired,
|
||||
submit_delivery_command,
|
||||
utcnow,
|
||||
)
|
||||
from govoplan_mail.backend.sending.smtp import SmtpSendError
|
||||
|
||||
|
||||
class MailDeliveryOutboxTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine(
|
||||
"sqlite+pysqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
access_users = Base.metadata.tables.get("access_users")
|
||||
if access_users is None:
|
||||
access_users = Table(
|
||||
"access_users",
|
||||
Base.metadata,
|
||||
Column("id", String(36), primary_key=True),
|
||||
)
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=[
|
||||
access_users,
|
||||
MailServerProfile.__table__,
|
||||
MailDeliveryCommand.__table__,
|
||||
MailDeliveryAttempt.__table__,
|
||||
MailDeliveryReconciliation.__table__,
|
||||
],
|
||||
)
|
||||
self.SessionLocal = sessionmaker(
|
||||
bind=self.engine,
|
||||
class_=Session,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
with self.SessionLocal() as session:
|
||||
session.add(
|
||||
MailServerProfile(
|
||||
id="profile-1",
|
||||
tenant_id="tenant-1",
|
||||
scope_type="tenant",
|
||||
scope_id="tenant-1",
|
||||
name="Delivery",
|
||||
slug="delivery",
|
||||
smtp_config={"host": "smtp.example.test", "port": 25},
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
self.audit = patch(
|
||||
"govoplan_mail.backend.delivery_outbox.audit_event"
|
||||
)
|
||||
self.audit.start()
|
||||
self.addCleanup(self.audit.stop)
|
||||
self.addCleanup(self.engine.dispose)
|
||||
|
||||
def _submit(
|
||||
self,
|
||||
session: Session,
|
||||
*,
|
||||
key: str = "request-1",
|
||||
recipients: list[str] | None = None,
|
||||
) -> dict[str, object]:
|
||||
return submit_delivery_command(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
command_type="campaign_report",
|
||||
source_module="campaigns",
|
||||
source_resource_type="campaign",
|
||||
source_resource_id="campaign-1",
|
||||
source_version_id="version-1",
|
||||
idempotency_key=key,
|
||||
profile_id="profile-1",
|
||||
message_bytes=b"Subject: report\r\n\r\ncontent",
|
||||
envelope_from="sender@example.test",
|
||||
envelope_recipients=recipients or ["recipient@example.test"],
|
||||
from_header="Sender <sender@example.test>",
|
||||
expected_smtp_transport_revision="revision-1",
|
||||
created_by_user_id=None,
|
||||
)
|
||||
|
||||
def test_submission_is_idempotent_and_conflicts_on_changed_intent(self) -> None:
|
||||
with self.SessionLocal() as session:
|
||||
first = self._submit(session)
|
||||
session.commit()
|
||||
repeated = self._submit(session)
|
||||
|
||||
self.assertEqual(first["id"], repeated["id"])
|
||||
self.assertTrue(repeated["duplicate"])
|
||||
with self.assertRaises(MailDeliveryIdempotencyConflict):
|
||||
self._submit(
|
||||
session,
|
||||
recipients=["different@example.test"],
|
||||
)
|
||||
|
||||
def test_attempt_is_committed_before_effect_and_accepted_is_not_resent(self) -> None:
|
||||
observed: dict[str, object] = {}
|
||||
with self.SessionLocal() as session:
|
||||
command_id = str(self._submit(session)["id"])
|
||||
session.commit()
|
||||
|
||||
def provider_effect(session: Session, **_kwargs):
|
||||
command = session.get(MailDeliveryCommand, command_id)
|
||||
attempt = session.query(MailDeliveryAttempt).one()
|
||||
observed.update(
|
||||
command_status=command.status,
|
||||
effect_started=command.effect_started_at is not None,
|
||||
attempt_status=attempt.status,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
accepted_count=1,
|
||||
refused_recipients={},
|
||||
)
|
||||
|
||||
with (
|
||||
self.SessionLocal() as session,
|
||||
patch(
|
||||
"govoplan_mail.backend.delivery_outbox.send_campaign_email_bytes",
|
||||
side_effect=provider_effect,
|
||||
) as send,
|
||||
):
|
||||
result = dispatch_due(session, worker_id="worker-1")
|
||||
repeated = dispatch_due(session, worker_id="worker-2")
|
||||
|
||||
self.assertEqual(result["accepted"], 1)
|
||||
self.assertEqual(repeated["selected"], 0)
|
||||
self.assertEqual(send.call_count, 1)
|
||||
self.assertEqual(
|
||||
observed,
|
||||
{
|
||||
"command_status": "in_progress",
|
||||
"effect_started": True,
|
||||
"attempt_status": "in_progress",
|
||||
},
|
||||
)
|
||||
|
||||
def test_partial_refusal_is_terminal_and_diagnostics_are_separate(self) -> None:
|
||||
with self.SessionLocal() as session:
|
||||
command_id = str(
|
||||
self._submit(
|
||||
session,
|
||||
recipients=["accepted@example.test", "blocked@example.test"],
|
||||
)["id"]
|
||||
)
|
||||
session.commit()
|
||||
with patch(
|
||||
"govoplan_mail.backend.delivery_outbox.send_campaign_email_bytes",
|
||||
return_value=SimpleNamespace(
|
||||
accepted_count=1,
|
||||
refused_recipients={
|
||||
"blocked@example.test": {
|
||||
"status_code": 550,
|
||||
"classification": "permanent",
|
||||
"message": "Permanent recipient rejection",
|
||||
}
|
||||
},
|
||||
),
|
||||
):
|
||||
result = dispatch_due(session)
|
||||
command = session.get(MailDeliveryCommand, command_id)
|
||||
assert command is not None
|
||||
self.assertEqual(result["partially_refused"], 1)
|
||||
self.assertNotIn("blocked@example.test", repr({
|
||||
"status": command.status,
|
||||
"summary": command.refusal_summary,
|
||||
}))
|
||||
diagnostics = delivery_command_diagnostics(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
command_id=command_id,
|
||||
)
|
||||
self.assertIn(
|
||||
"blocked@example.test",
|
||||
diagnostics["refused_recipients"],
|
||||
)
|
||||
|
||||
def test_unknown_outcome_is_never_automatically_retried(self) -> None:
|
||||
with self.SessionLocal() as session:
|
||||
command_id = str(self._submit(session)["id"])
|
||||
session.commit()
|
||||
with patch(
|
||||
"govoplan_mail.backend.delivery_outbox.send_campaign_email_bytes",
|
||||
side_effect=SmtpSendError(
|
||||
"unknown",
|
||||
outcome_unknown=True,
|
||||
),
|
||||
) as send:
|
||||
first = dispatch_due(session)
|
||||
second = dispatch_due(session)
|
||||
command = session.get(MailDeliveryCommand, command_id)
|
||||
|
||||
assert command is not None
|
||||
self.assertEqual(first["outcome_unknown"], 1)
|
||||
self.assertEqual(second["selected"], 0)
|
||||
self.assertEqual(command.status, "outcome_unknown")
|
||||
self.assertEqual(send.call_count, 1)
|
||||
|
||||
def test_audit_failure_after_acceptance_cannot_make_command_retryable(self) -> None:
|
||||
with self.SessionLocal() as session:
|
||||
command_id = str(self._submit(session)["id"])
|
||||
session.commit()
|
||||
|
||||
with (
|
||||
self.SessionLocal() as session,
|
||||
patch(
|
||||
"govoplan_mail.backend.delivery_outbox.send_campaign_email_bytes",
|
||||
return_value=SimpleNamespace(
|
||||
accepted_count=1,
|
||||
refused_recipients={},
|
||||
),
|
||||
) as send,
|
||||
patch(
|
||||
"govoplan_mail.backend.delivery_outbox.audit_event",
|
||||
side_effect=RuntimeError("audit unavailable"),
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, "audit unavailable"):
|
||||
dispatch_due(session)
|
||||
|
||||
with self.SessionLocal() as session:
|
||||
command = session.get(MailDeliveryCommand, command_id)
|
||||
assert command is not None
|
||||
self.assertEqual(command.status, "accepted")
|
||||
self.assertEqual(dispatch_due(session)["selected"], 0)
|
||||
self.assertEqual(send.call_count, 1)
|
||||
|
||||
def test_stale_effect_started_command_becomes_unknown_without_send(self) -> None:
|
||||
with self.SessionLocal() as session:
|
||||
command_id = str(self._submit(session)["id"])
|
||||
session.commit()
|
||||
command = session.get(MailDeliveryCommand, command_id)
|
||||
assert command is not None
|
||||
command.status = "in_progress"
|
||||
command.attempt_count = 1
|
||||
command.claimed_at = utcnow() - timedelta(hours=1)
|
||||
command.effect_started_at = utcnow() - timedelta(hours=1)
|
||||
session.add(
|
||||
MailDeliveryAttempt(
|
||||
command_id=command.id,
|
||||
attempt_number=1,
|
||||
status="in_progress",
|
||||
started_at=utcnow() - timedelta(hours=1),
|
||||
effect_started_at=utcnow() - timedelta(hours=1),
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
with patch(
|
||||
"govoplan_mail.backend.delivery_outbox.send_campaign_email_bytes"
|
||||
) as send:
|
||||
result = dispatch_due(session)
|
||||
self.assertEqual(result["outcome_unknown"], 1)
|
||||
self.assertEqual(command.status, "outcome_unknown")
|
||||
send.assert_not_called()
|
||||
|
||||
def test_expired_payload_is_minimized_but_evidence_remains(self) -> None:
|
||||
with self.SessionLocal() as session:
|
||||
command_id = str(self._submit(session)["id"])
|
||||
session.commit()
|
||||
command = session.get(MailDeliveryCommand, command_id)
|
||||
assert command is not None
|
||||
command.expires_at = utcnow() - timedelta(seconds=1)
|
||||
session.commit()
|
||||
self.assertEqual(purge_expired(session), {"purged": 1})
|
||||
session.refresh(command)
|
||||
self.assertIsNone(command.message_encrypted)
|
||||
self.assertIsNone(command.envelope_recipients_encrypted)
|
||||
self.assertIsNotNone(command.payload_purged_at)
|
||||
self.assertEqual(command.message_sha256, command.message_sha256)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user