fix(campaign): sanitize synchronous send results

This commit is contained in:
2026-07-22 09:21:44 +02:00
parent 1225802c5d
commit f095a3e2c7
3 changed files with 241 additions and 5 deletions

View File

@@ -0,0 +1,133 @@
from __future__ import annotations
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import Mock, patch
from govoplan_campaign.backend import router
from govoplan_campaign.backend.schemas import SendCampaignNowRequest
from govoplan_campaign.backend.sending.jobs import SendCampaignNowResult
def test_send_now_omits_provider_and_recipient_text_from_response_and_audit() -> None:
campaign = SimpleNamespace(id="campaign-1", current_version_id="version-1")
version = SimpleNamespace(
id="version-1",
raw_json={},
locked_at=datetime(2026, 7, 22, tzinfo=UTC),
validation_summary={
"ok": True,
"error_count": 0,
"warning_count": 1,
"issues": [{"message": "provider-secret validation detail"}],
},
build_summary={
"built_count": 2,
"build_failed_count": 0,
"ready_count": 2,
"warning_count": 1,
"messages": [{"recipient": "refused-recipient@example.test"}],
"build_token": "provider-secret-build-token",
},
)
delivery = SendCampaignNowResult(
campaign_id="campaign-1",
version_id="version-1",
attempted_count=2,
sent_count=1,
failed_count=1,
outcome_unknown_count=0,
skipped_count=0,
preflight_count=2,
synchronous_send_policy={
"max_recipient_jobs": 25,
"source": "deployment_default",
"deployment_max_recipient_jobs": 25,
"tenant_max_recipient_jobs": None,
"provider_diagnostic": "provider-secret-policy",
},
results=[
{
"job_id": "job-failed",
"status": "failed",
"attempt_number": 1,
"message": "provider-secret smtp.internal.example",
"recipient_email": "private@example.test",
},
{
"job_id": "job-refused",
"status": "smtp_accepted",
"attempt_number": 1,
"message": (
"SMTP accepted 1/2 envelope recipient(s); refused recipients: "
'{"refused-recipient@example.test": [550, "not allowed"]}'
),
},
],
)
principal = SimpleNamespace(tenant_id="tenant-1", user=SimpleNamespace(id="user-1"), api_key=None)
audit = Mock()
with (
patch.object(router, "_get_campaign_for_principal", return_value=campaign),
patch.object(router, "_require_permission"),
patch.object(router, "_get_campaign_for_tenant", return_value=campaign),
patch.object(router, "_get_version_for_tenant", return_value=version),
patch.object(router, "_require_mail_profile_use_if_needed"),
patch.object(router, "is_user_locked_version", return_value=False),
patch.object(router, "send_campaign_now", return_value=delivery),
patch.object(router, "audit_from_principal", audit),
):
response = router.send_campaign_now_endpoint(
"campaign-1",
SendCampaignNowRequest(),
session=Mock(),
principal=principal, # type: ignore[arg-type]
)
payload = response.model_dump(mode="json")["result"]
assert payload["results"] == [
{"job_id": "job-failed", "status": "failed", "attempt_number": 1},
{"job_id": "job-refused", "status": "smtp_accepted", "attempt_number": 1},
]
assert payload["validation"] == {"ok": True, "error_count": 0, "warning_count": 1}
assert payload["build"] == {
"built_count": 2,
"build_failed_count": 0,
"ready_count": 2,
"warning_count": 1,
}
assert payload["synchronous_send_policy"] == {
"max_recipient_jobs": 25,
"source": "deployment_default",
"deployment_max_recipient_jobs": 25,
"tenant_max_recipient_jobs": None,
}
audit_details = audit.call_args.kwargs["details"]
assert set(audit_details) == {
"campaign_id",
"version_id",
"attempted_count",
"sent_count",
"failed_count",
"outcome_unknown_count",
"skipped_count",
"preflight_count",
"delivery_mode",
"dry_run",
"synchronous_send_policy",
}
assert "results" not in audit_details
assert "validation" not in audit_details
assert "build" not in audit_details
serialized = repr({"response": payload, "audit": audit_details})
for forbidden in (
"provider-secret",
"smtp.internal.example",
"private@example.test",
"refused-recipient@example.test",
"not allowed",
):
assert forbidden not in serialized