143 lines
8.1 KiB
Python
143 lines
8.1 KiB
Python
"""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"<frozen-{job.entry_index}@example.test>"
|
|
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"]) == "<frozen-1@example.test>"
|
|
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)
|