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