Files
govoplan-campaign/tests/test_reviewed_build_mock.py
T
zemion c51fc180fb
Module Package Release / publish-packages (push) Successful in 12s
Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
2026-09-08 01:32:26 +02:00

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)