Files
govoplan-mail/tests/test_bounce_processing.py
T
2026-07-31 22:48:07 +02:00

187 lines
6.0 KiB
Python

from __future__ import annotations
import unittest
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.bounce_processing import (
SqlMailBounceProcessingProvider,
parse_delivery_status,
)
from govoplan_mail.backend.db.models import (
MailBounceObservation,
MailBounceSource,
MailDeliveryCommand,
MailServerProfile,
)
from govoplan_mail.backend.delivery_outbox import submit_delivery_command
DSN = b"""From: Mail Delivery Subsystem <mailer-daemon@example.test>
To: sender@example.test
Date: Fri, 31 Jul 2026 12:00:00 +0000
Subject: Delivery Status Notification (Failure)
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status; boundary="dsn"
--dsn
Content-Type: text/plain; charset=utf-8
Delivery failed.
--dsn
Content-Type: message/delivery-status
Reporting-MTA: dns; mx.example.test
Original-Message-ID: <outgoing-1@example.test>
Final-Recipient: rfc822; recipient@example.test
Action: failed
Status: 5.1.1
Remote-MTA: dns; destination.example.test
Diagnostic-Code: smtp; 550 mailbox unavailable
Last-Attempt-Date: Fri, 31 Jul 2026 11:59:00 +0000
--dsn
Content-Type: message/rfc822
Message-ID: <outgoing-1@example.test>
From: sender@example.test
To: recipient@example.test
Subject: Original
Body
--dsn--
"""
class MailBounceProcessingTests(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__,
MailBounceSource.__table__,
MailBounceObservation.__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_delivery = patch(
"govoplan_mail.backend.delivery_outbox.audit_event"
)
self.audit_bounce = patch(
"govoplan_mail.backend.bounce_processing.audit_event"
)
self.audit_delivery.start()
self.audit_bounce.start()
self.addCleanup(self.audit_delivery.stop)
self.addCleanup(self.audit_bounce.stop)
self.addCleanup(self.engine.dispose)
def test_parser_extracts_structured_recipient_outcome(self) -> None:
reports = parse_delivery_status(DSN)
self.assertEqual(1, len(reports))
self.assertEqual("recipient@example.test", reports[0]["recipient"])
self.assertEqual("failed", reports[0]["action"])
self.assertEqual("5.1.1", reports[0]["status_code"])
self.assertEqual(
"<outgoing-1@example.test>",
reports[0]["original_message_id"],
)
def test_processing_correlates_and_is_idempotent(self) -> None:
provider = SqlMailBounceProcessingProvider()
with self.SessionLocal() as session:
command = 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="delivery-1",
profile_id="profile-1",
message_bytes=(
b"Message-ID: <outgoing-1@example.test>\r\n"
b"Subject: Original\r\n\r\nBody"
),
envelope_from="sender@example.test",
envelope_recipients=["recipient@example.test"],
from_header="sender@example.test",
expected_smtp_transport_revision="revision-1",
)
first = provider.process_raw_message(
session,
tenant_id="tenant-1",
profile_id="profile-1",
folder="INBOX",
uid="42",
raw_message=DSN,
)
second = provider.process_raw_message(
session,
tenant_id="tenant-1",
profile_id="profile-1",
folder="INBOX",
uid="42",
raw_message=DSN,
)
self.assertEqual(command["id"], first[0].command_id)
self.assertTrue(first[0].matched)
self.assertTrue(first[0].permanent)
self.assertEqual(first[0].id, second[0].id)
self.assertEqual(1, session.query(MailBounceObservation).count())
self.assertEqual(
"pending",
session.get(MailDeliveryCommand, command["id"]).status,
)
def test_ordinary_mail_is_not_misclassified(self) -> None:
self.assertEqual(
(),
parse_delivery_status(
b"From: person@example.test\r\nSubject: Hello\r\n\r\nNot a DSN"
),
)
if __name__ == "__main__":
unittest.main()