168 lines
5.5 KiB
Python
168 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from govoplan_campaign.backend.reports.campaigns import (
|
|
_job_evidence_row,
|
|
_latest_by_job_id,
|
|
_load_delivery_info,
|
|
generate_campaign_report,
|
|
)
|
|
|
|
|
|
def _dt() -> datetime:
|
|
return datetime(2026, 7, 8, 12, 30, tzinfo=UTC)
|
|
|
|
|
|
def test_job_evidence_row_contains_transport_and_message_evidence() -> None:
|
|
job = SimpleNamespace(
|
|
id="job-1",
|
|
campaign_id="campaign-1",
|
|
campaign_version_id="version-1",
|
|
entry_index=0,
|
|
entry_id="recipient-1",
|
|
recipient_email="recipient@govoplan.test",
|
|
subject="Evidence test",
|
|
message_id_header="<message-1@govoplan.test>",
|
|
eml_storage_key="eml/job-1.eml",
|
|
eml_local_path="/tmp/job-1.eml",
|
|
eml_size_bytes=1234,
|
|
eml_sha256="abc123",
|
|
build_status="built",
|
|
validation_status="ready",
|
|
queue_status="queued",
|
|
send_status="smtp_accepted",
|
|
imap_status="appended",
|
|
attempt_count=2,
|
|
queued_at=_dt(),
|
|
claimed_at=None,
|
|
smtp_started_at=_dt(),
|
|
outcome_unknown_at=None,
|
|
sent_at=_dt(),
|
|
last_error=None,
|
|
resolved_recipients={
|
|
"from": [{"name": "Sender", "email": "sender@govoplan.test"}],
|
|
"to": [{"name": "Recipient", "email": "recipient@govoplan.test"}],
|
|
"cc": [],
|
|
"bcc": [],
|
|
"reply_to": [{"email": "reply@govoplan.test"}],
|
|
},
|
|
resolved_attachments=[
|
|
{
|
|
"matches": [{"filename": "notice.pdf"}, "/tmp/evidence.xlsx"],
|
|
"zip_filename": "bundle.zip",
|
|
"message_filenames": ["notice.pdf"],
|
|
}
|
|
],
|
|
issues_snapshot=[],
|
|
created_at=_dt(),
|
|
updated_at=_dt(),
|
|
)
|
|
smtp = SimpleNamespace(
|
|
job_id="job-1",
|
|
attempt_number=2,
|
|
status="success",
|
|
smtp_status_code=250,
|
|
smtp_response="2.0.0 queued",
|
|
error_type=None,
|
|
error_message=None,
|
|
started_at=_dt(),
|
|
finished_at=_dt(),
|
|
)
|
|
imap = SimpleNamespace(
|
|
job_id="job-1",
|
|
attempt_number=1,
|
|
status="appended",
|
|
folder="Sent",
|
|
error_message=None,
|
|
created_at=_dt(),
|
|
updated_at=_dt(),
|
|
)
|
|
|
|
row = _job_evidence_row(job, latest_smtp=smtp, latest_imap=imap)
|
|
|
|
assert row["campaign_id"] == "campaign-1"
|
|
assert row["campaign_version_id"] == "version-1"
|
|
assert row["message_id_header"] == "<message-1@govoplan.test>"
|
|
assert row["from"] == "Sender <sender@govoplan.test>"
|
|
assert row["to"] == "Recipient <recipient@govoplan.test>"
|
|
assert row["reply_to"] == "reply@govoplan.test"
|
|
assert "bundle.zip" in row["attachment_names"]
|
|
assert "notice.pdf" in row["attachment_names"]
|
|
assert row["latest_smtp_status_code"] == 250
|
|
assert "latest_smtp_response" not in row
|
|
assert "latest_smtp_error_type" not in row
|
|
assert "latest_smtp_error_message" not in row
|
|
assert row["latest_imap_status"] == "appended"
|
|
assert row["latest_imap_folder"] == "Sent"
|
|
assert "latest_imap_error_message" not in row
|
|
assert "eml_storage_key" not in row
|
|
assert "eml_local_path" not in row
|
|
|
|
|
|
def test_latest_by_job_id_keeps_highest_attempt_number() -> None:
|
|
attempts = [
|
|
SimpleNamespace(job_id="job-1", attempt_number=1),
|
|
SimpleNamespace(job_id="job-1", attempt_number=3),
|
|
SimpleNamespace(job_id="job-2", attempt_number=2),
|
|
]
|
|
|
|
latest = _latest_by_job_id(attempts)
|
|
|
|
assert latest["job-1"].attempt_number == 3
|
|
assert latest["job-2"].attempt_number == 2
|
|
|
|
|
|
def test_invalid_legacy_snapshot_never_echoes_validation_details() -> None:
|
|
version = SimpleNamespace(
|
|
execution_snapshot={
|
|
"snapshot_version": "legacy",
|
|
"smtp": {"host": "smtp.internal.example", "password": "provider-secret"},
|
|
},
|
|
execution_snapshot_hash=None,
|
|
execution_snapshot_at=None,
|
|
)
|
|
|
|
delivery = _load_delivery_info(version, [])
|
|
|
|
assert delivery["load_error"] == "Execution snapshot is invalid; rebuild the selected version before delivery."
|
|
assert "provider-secret" not in repr(delivery)
|
|
assert "smtp.internal.example" not in repr(delivery)
|
|
|
|
|
|
def test_aggregate_report_omits_recipient_level_failures_by_default() -> None:
|
|
campaign = SimpleNamespace(id="campaign-1")
|
|
version = SimpleNamespace(id="version-1")
|
|
with (
|
|
patch("govoplan_campaign.backend.reports.campaigns._get_campaign", return_value=campaign),
|
|
patch("govoplan_campaign.backend.reports.campaigns._selected_version", return_value=version),
|
|
patch("govoplan_campaign.backend.reports.campaigns._report_jobs", return_value=[]),
|
|
patch(
|
|
"govoplan_campaign.backend.reports.campaigns._campaign_report_payload",
|
|
return_value={"cards": {}},
|
|
) as payload,
|
|
):
|
|
report = generate_campaign_report(
|
|
object(), # type: ignore[arg-type]
|
|
tenant_id="tenant-1",
|
|
campaign_id="campaign-1",
|
|
)
|
|
|
|
assert report == {"cards": {}}
|
|
assert payload.call_args.kwargs["include_recent_failures"] is False
|
|
|
|
|
|
class CampaignReportProjectionTests(unittest.TestCase):
|
|
def test_evidence_projection(self) -> None:
|
|
test_job_evidence_row_contains_transport_and_message_evidence()
|
|
|
|
def test_latest_attempt_projection(self) -> None:
|
|
test_latest_by_job_id_keeps_highest_attempt_number()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|