Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-09-08 01:32:26 +02:00
parent 1b32427813
commit c51fc180fb
111 changed files with 6905 additions and 980 deletions
+67 -1
View File
@@ -8,6 +8,7 @@ import pytest
from fastapi import HTTPException
from govoplan_campaign.backend import router as campaign_api
from govoplan_core.tenancy.scope import Tenant
from govoplan_campaign.backend.routes import delivery as router
from govoplan_campaign.backend.delivery_policy import (
CampaignDeliveryPolicyError,
@@ -20,8 +21,14 @@ from govoplan_campaign.backend.db.models import (
JobSendStatus,
JobValidationStatus,
)
from govoplan_campaign.backend.integrations import (
MailProfileError,
SmtpConfigurationError,
SmtpSendError,
)
from govoplan_campaign.backend.sending.jobs import (
QueueCampaignResult,
SendJobError,
SynchronousSendRejected,
_ensure_synchronous_send_count_allowed,
_pause_jobs_after_systemic_smtp_failure,
@@ -39,7 +46,7 @@ class _PolicySession:
self.tenant = SimpleNamespace(settings=settings or {})
def get(self, _model, _id):
return self.tenant
return self.tenant if _model is Tenant else None
def _version() -> SimpleNamespace:
@@ -218,6 +225,65 @@ def test_post_queue_growth_is_rejected_before_batch_or_provider_preflight() -> N
batch_preflight.assert_not_called()
@pytest.mark.parametrize(
("error", "reason", "message", "fails_on_exit"),
[
(MailProfileError("private credential detail"), "mail_profile_preflight_failed", "credential selection", False),
(SmtpConfigurationError("private credential detail"), "smtp_configuration_preflight_failed", "SMTP configuration", False),
(SmtpSendError("private credential detail", reason_code="smtp_authentication_failed"), "smtp_authentication_failed", "SMTP authentication failed", False),
(SmtpSendError("private credential detail", reason_code="smtp_preflight_rejected"), "smtp_preflight_rejected", "server rejected", False),
(SmtpSendError("private credential detail", reason_code="private provider code"), "smtp_connectivity_unavailable", "connectivity", False),
(OSError("private credential detail"), "smtp_connectivity_unavailable", "connectivity", False),
(OSError("private credential detail"), None, "Messages may already have been sent", True),
],
)
def test_smtp_preflight_errors_distinguish_policy_configuration_and_connection_without_secrets(
error: Exception, reason: str | None, message: str, fails_on_exit: bool,
) -> None:
session = Mock()
campaign = SimpleNamespace(id="campaign-1")
job = _job("one", queue_status="queued", send_status="queued")
policy = effective_synchronous_send_policy(_PolicySession(), tenant_id="tenant-1", environ={})
@contextmanager
def rejected_connection():
if fails_on_exit:
yield SimpleNamespace(connection_count=1, reconnect_count=0)
raise error
with (
patch("govoplan_campaign.backend.sending.jobs._get_campaign_for_tenant", return_value=campaign),
patch("govoplan_campaign.backend.sending.jobs._get_current_version", return_value=_version()),
patch("govoplan_campaign.backend.sending.jobs._ensure_version_validated_and_locked"),
patch("govoplan_campaign.backend.sending.jobs._ensure_campaign_execution_snapshot"),
patch("govoplan_campaign.backend.sending.jobs.effective_synchronous_send_policy", return_value=policy),
patch("govoplan_campaign.backend.sending.jobs._campaign_jobs_for_queue", return_value=[job]),
patch("govoplan_campaign.backend.sending.jobs.queue_campaign_jobs") as queue,
patch("govoplan_campaign.backend.sending.jobs._campaign_jobs_for_version", return_value=[job]),
patch("govoplan_campaign.backend.sending.jobs._preflight_synchronous_send_batch", return_value={"one": Mock()}),
patch("govoplan_campaign.backend.sending.jobs._synchronous_smtp_batch_manager", return_value=rejected_connection()),
patch("govoplan_campaign.backend.sending.jobs._deliver_job_with_recovery", return_value=SimpleNamespace(status="smtp_accepted", as_dict=lambda: {"status": "smtp_accepted"})) as deliver,
pytest.raises(SendJobError if fails_on_exit else SynchronousSendRejected) as rejected,
):
send_campaign_now(session, tenant_id="tenant-1", campaign_id=campaign.id)
assert message in str(rejected.value)
assert "private" not in str(rejected.value)
assert queue.call_args.kwargs["commit_queue"] is False
session.rollback.assert_called_once_with()
if fails_on_exit:
assert "no message was sent" not in str(rejected.value)
session.commit.assert_called_once_with()
deliver.assert_called_once()
else:
assert rejected.value.reason == reason
assert "no message was sent" in str(rejected.value)
assert "private" not in str(rejected.value.audit_details())
assert rejected.value.eligible_count == 1
session.commit.assert_not_called()
deliver.assert_not_called()
@pytest.mark.parametrize(
("workers_available", "expected_mode", "expected_enqueued"),
((False, "database_queue", 0), (True, "worker_queue", 1)),