fix(campaign): sanitize synchronous send results
This commit is contained in:
133
tests/test_send_now_response_security.py
Normal file
133
tests/test_send_now_response_security.py
Normal 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
|
||||
Reference in New Issue
Block a user