security: seal Campaign delivery inputs

This commit is contained in:
2026-07-21 17:53:43 +02:00
parent 9f4eab07f6
commit 50c509d161
8 changed files with 350 additions and 44 deletions

View File

@@ -218,10 +218,11 @@ def test_imap_claim_migration_preserves_and_renumbers_duplicate_attempts() -> No
]
def test_imap_reconciliation_requires_an_evidence_note() -> None:
@pytest.mark.parametrize("decision", ["smtp_accepted", "not_sent", "imap_appended", "imap_not_appended"])
def test_reconciliation_requires_an_evidence_note(decision: str) -> None:
with pytest.raises(ValueError, match="evidence note"):
CampaignResolveOutcomeRequest(
decision="imap_not_appended",
decision=decision, # type: ignore[arg-type]
note=" ",
)
@@ -299,7 +300,11 @@ def test_imap_reconciliation_preserves_attempt_and_only_not_appended_is_retryabl
files().mark_job_attachment_uses_sent.assert_not_called()
def test_imap_reconciliation_rejects_a_retryable_or_completed_state() -> None:
@pytest.mark.parametrize(
"imap_status",
[JobImapStatus.APPENDING.value, JobImapStatus.FAILED.value, JobImapStatus.APPENDED.value],
)
def test_imap_reconciliation_rejects_live_retryable_or_completed_state(imap_status: str) -> None:
campaign = SimpleNamespace(id="campaign-1")
job = SimpleNamespace(
id="job-1",
@@ -307,7 +312,7 @@ def test_imap_reconciliation_rejects_a_retryable_or_completed_state() -> None:
campaign_id="campaign-1",
campaign_version_id="version-1",
send_status=JobSendStatus.SMTP_ACCEPTED.value,
imap_status=JobImapStatus.FAILED.value,
imap_status=imap_status,
)
session = MagicMock()
session.get.return_value = job
@@ -326,10 +331,37 @@ def test_imap_reconciliation_rejects_a_retryable_or_completed_state() -> None:
)
@pytest.mark.parametrize("send_status", [JobSendStatus.CLAIMED.value, JobSendStatus.SENDING.value])
def test_smtp_reconciliation_rejects_a_live_worker_state(send_status: str) -> None:
campaign = SimpleNamespace(id="campaign-1")
job = SimpleNamespace(
id="job-1",
tenant_id="tenant-1",
campaign_id="campaign-1",
campaign_version_id="version-1",
send_status=send_status,
)
session = MagicMock()
session.get.return_value = job
with patch(
"govoplan_campaign.backend.sending.jobs._get_campaign_for_tenant",
return_value=campaign,
):
with pytest.raises(QueueingError, match="does not require reconciliation"):
reconcile_job_outcome(
session,
tenant_id="tenant-1",
campaign_id="campaign-1",
job_id="job-1",
decision="not_sent",
note="Checked provider logs.",
)
@pytest.mark.parametrize(
("decision", "note"),
[
("smtp_accepted", None),
("smtp_accepted", "SMTP provider evidence checked."),
("imap_appended", "Mailbox evidence checked."),
],
)