"""Mock accepted exceptions using real frozen jobs/EML, never new live effects.""" from __future__ import annotations import copy import hashlib from email import policy from email.message import EmailMessage from types import SimpleNamespace from unittest.mock import Mock, patch import pytest from test_incremental_review_persistence import review, _save from govoplan_campaign.backend.campaign.models import DeliveryConfig from govoplan_campaign.backend.db.models import CampaignJob from govoplan_campaign.backend.dev import mock_campaign from govoplan_campaign.backend.persistence.campaigns import CampaignPersistenceError from govoplan_campaign.backend.sending.execution import ExecutionSnapshotError, create_execution_snapshot @pytest.fixture def frozen(review, tmp_path): review.version.raw_json = {**review.version.raw_json, "server": {"mail_profile_id": "profile-1"}} jobs = review.session.query(CampaignJob).order_by(CampaignJob.entry_index).all() for job in jobs: job.subject = f"Frozen subject {job.entry_index}" job.resolved_recipients = {"from": {"email": "sender@example.test"}, "to": [{"email": f"recipient-{job.entry_index}@example.test"}]} job.issues_snapshot = [{**item, "severity": "warning", "message": "Explicit attachment exception"} for item in job.issues_snapshot] message = EmailMessage() message["From"] = "sender@example.test" message["To"] = f"recipient-{job.entry_index}@example.test" message["Subject"] = job.subject message["Date"] = "Mon, 07 Sep 2026 09:00:00 +0200" message["Message-ID"] = f"" message.set_content(f"Frozen reviewed body {job.entry_index}") payload = message.as_bytes(policy=policy.SMTP) path = tmp_path / f"message-{job.entry_index}.eml" path.write_bytes(payload) job.eml_local_path = str(path) job.eml_size_bytes = len(payload) job.eml_sha256 = hashlib.sha256(payload).hexdigest() job.message_id_header = str(message["Message-ID"]) review.session.flush() payload, digest = create_execution_snapshot(review.version, mail_profile_id="profile-1", smtp_transport_revision="smtp-1", imap_transport_revision="imap-1", delivery=DeliveryConfig(), jobs=jobs, build_summary=review.version.build_summary) review.version.execution_snapshot = payload review.version.execution_snapshot_hash = digest review.session.commit() _save(review, ids=(1, 2), complete=True) mailbox = Mock() mailbox.consume_fail_next_smtp.return_value = False mailbox.consume_fail_next_imap.return_value = False mailbox.get_failures.return_value = {} mailbox.record_smtp_delivery.return_value = SimpleNamespace(id="mock-1") mailbox.record_imap_append.return_value = SimpleNamespace(id="mock-imap-1") mailbox.list_records.return_value = [] with ( patch.object(mock_campaign, "_mock_mailbox", return_value=mailbox), patch("govoplan_campaign.backend.sending.execution.files_integration", return_value=SimpleNamespace(available=False)), patch("govoplan_campaign.backend.sending.execution.assert_archive_encryption_allowed", return_value=SimpleNamespace(policy_hash="archive-policy")), patch("govoplan_campaign.backend.sending.execution.profile_delivery_summary", return_value={"smtp_transport_revision": "smtp-1", "imap_transport_revision": "imap-1"}) as transport, ): yield SimpleNamespace(**vars(review), jobs=jobs, mailbox=mailbox, transport=transport) def _run(frozen, **options): return mock_campaign.run_mock_campaign_send(frozen.session, tenant_id="tenant-1", campaign_id="campaign-1", version_id="version-1", use_reviewed_build=True, include_needs_review=False, send=True, **options) def test_mock_reuses_reviewed_frozen_bytes_and_decisions_without_reasking_or_mutating_campaign(frozen): original = copy.deepcopy(frozen.version.editor_state) revision = frozen.version.edit_revision with patch.object(mock_campaign, "_build_mock_campaign_run", side_effect=AssertionError("Do not rebuild reviewed messages")): result = _run(frozen) assert result["send"]["sent_count"] == 2 and result["send"]["skipped_count"] == 0 assert result["use_reviewed_build"] is True and result["build"]["review_satisfied"] is True assert [step["status"] for step in result["steps"]] == ["ok", "ok", "ok", "ok"] assert frozen.mailbox.record_smtp_delivery.call_count == 2 captured = frozen.mailbox.record_smtp_delivery.call_args_list[0].args[0] assert str(captured["Message-ID"]) == "" assert "Frozen reviewed body 1" in captured.get_content() frozen.session.expire_all() assert frozen.version.editor_state == original and frozen.version.edit_revision == revision assert all(job.send_status == "not_queued" for job in frozen.jobs) @pytest.mark.parametrize("mutation", ["partial", "build", "issue", "reason_evidence", "configuration", "snapshot", "bytes", "transport", "imap"]) def test_invalid_frozen_review_stops_before_mock_capture_or_mailbox_clear(frozen, mutation): if mutation == "partial": state = copy.deepcopy(frozen.version.editor_state) state["review_send"]["inspection_complete"] = False frozen.version.editor_state = state elif mutation == "build": frozen.version.build_summary = {"build_token": "different-build", "built_count": 2} elif mutation == "issue": frozen.jobs[1].issues_snapshot = [{"code": "new-hard-block", "source": "attachments", "behavior": "block", "severity": "error", "message": "Missing required attachment"}] elif mutation == "reason_evidence": state = copy.deepcopy(frozen.version.editor_state) state["review_send"]["issue_decisions"][0]["issue_fingerprint"] = "f" * 64 frozen.version.editor_state = state elif mutation == "configuration": frozen.version.raw_json = {**frozen.version.raw_json, "template": {"subject": "Unreviewed changed subject"}} elif mutation == "snapshot": frozen.version.execution_snapshot = None elif mutation == "bytes": from pathlib import Path Path(frozen.jobs[1].eml_local_path).write_bytes(b"changed bytes") else: frozen.transport.return_value = {"smtp_transport_revision": "changed" if mutation == "transport" else "smtp-1", "imap_transport_revision": "changed" if mutation == "imap" else "imap-1"} frozen.session.flush() with pytest.raises((mock_campaign.MockCampaignSendError, ExecutionSnapshotError, CampaignPersistenceError)): _run(frozen, clear_mailbox=True) frozen.mailbox.record_smtp_delivery.assert_not_called() frozen.mailbox.record_imap_append.assert_not_called() frozen.mailbox.clear_records.assert_not_called() def test_policy_dropped_frozen_message_stays_skipped_even_with_ask_evidence(frozen): excluded = frozen.jobs[1] excluded.validation_status = "excluded" excluded.send_status = "skipped" excluded.imap_status = "skipped" payload, digest = create_execution_snapshot(frozen.version, mail_profile_id="profile-1", smtp_transport_revision="smtp-1", imap_transport_revision="imap-1", delivery=DeliveryConfig(), jobs=frozen.jobs, build_summary=frozen.version.build_summary) frozen.version.execution_snapshot = payload frozen.version.execution_snapshot_hash = digest state = copy.deepcopy(frozen.version.editor_state) state["review_send"]["reviewed_message_keys"] = ["entry-1"] state["review_send"]["issue_decisions"] = state["review_send"]["issue_decisions"][:1] frozen.version.editor_state = state frozen.session.commit() result = _run(frozen) assert result["send"]["sent_count"] == 1 and result["send"]["skipped_count"] == 1 assert frozen.mailbox.record_smtp_delivery.call_count == 1 def test_default_authoring_mock_still_uses_transient_preview(frozen): with patch.object(mock_campaign, "_build_mock_campaign_run", side_effect=RuntimeError("authoring preview path")): with pytest.raises(RuntimeError, match="authoring preview path"): mock_campaign.run_mock_campaign_send(frozen.session, tenant_id="tenant-1", campaign_id="campaign-1", send=False)