feat: add governed postbox delivery and report hardening
This commit is contained in:
289
tests/test_postbox_delivery_orchestration.py
Normal file
289
tests/test_postbox_delivery_orchestration.py
Normal file
@@ -0,0 +1,289 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from govoplan_campaign.backend.campaign.models import DeliveryChannelPolicy
|
||||
from govoplan_campaign.backend.db.models import JobSendStatus
|
||||
from govoplan_campaign.backend.sending.jobs import (
|
||||
SendJobResult,
|
||||
_MailChannelOutcome,
|
||||
_final_multichannel_status,
|
||||
_send_claimed_multichannel_job,
|
||||
)
|
||||
from govoplan_campaign.backend.sending.postbox_delivery import (
|
||||
PostboxChannelOutcome,
|
||||
)
|
||||
|
||||
|
||||
def _context():
|
||||
return SimpleNamespace(
|
||||
message_bytes=b"message",
|
||||
snapshot=SimpleNamespace(
|
||||
delivery=SimpleNamespace(
|
||||
postbox=SimpleNamespace(classification="internal")
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _job():
|
||||
return SimpleNamespace(id="job-1")
|
||||
|
||||
|
||||
class _Session:
|
||||
def __init__(self, job) -> None:
|
||||
self.job = job
|
||||
|
||||
def get(self, _model, _id):
|
||||
return self.job
|
||||
|
||||
def add(self, _value) -> None:
|
||||
return None
|
||||
|
||||
def commit(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class PostboxFallbackOrchestrationTests(unittest.TestCase):
|
||||
def test_mail_unknown_never_starts_postbox_fallback(self) -> None:
|
||||
job = _job()
|
||||
expected = SendJobResult(
|
||||
job_id=job.id,
|
||||
status=JobSendStatus.OUTCOME_UNKNOWN.value,
|
||||
attempt_number=1,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._deliver_mail_channel",
|
||||
return_value=_MailChannelOutcome(outcome_unknown=True),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._postbox_outcome_from_attempts",
|
||||
return_value=PostboxChannelOutcome(),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs.deliver_campaign_job_to_postboxes"
|
||||
) as deliver_postbox,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._finalize_multichannel_job",
|
||||
return_value=expected,
|
||||
),
|
||||
):
|
||||
result = _send_claimed_multichannel_job(
|
||||
_Session(job), # type: ignore[arg-type]
|
||||
job=job, # type: ignore[arg-type]
|
||||
claim_token="claim-1",
|
||||
context=_context(), # type: ignore[arg-type]
|
||||
channel_policy=DeliveryChannelPolicy.MAIL_THEN_POSTBOX,
|
||||
use_rate_limit=False,
|
||||
enqueue_imap_task=False,
|
||||
)
|
||||
|
||||
self.assertIs(result, expected)
|
||||
deliver_postbox.assert_not_called()
|
||||
|
||||
def test_mail_preacceptance_rejection_starts_postbox_fallback(self) -> None:
|
||||
job = _job()
|
||||
postbox_outcome = PostboxChannelOutcome(accepted=1)
|
||||
with (
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._deliver_mail_channel",
|
||||
return_value=_MailChannelOutcome(rejected_permanent=True),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._postbox_outcome_from_attempts",
|
||||
return_value=PostboxChannelOutcome(),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs.deliver_campaign_job_to_postboxes",
|
||||
return_value=postbox_outcome,
|
||||
) as deliver_postbox,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._finalize_multichannel_job",
|
||||
return_value=SendJobResult(
|
||||
job_id=job.id,
|
||||
status=JobSendStatus.POSTBOX_ACCEPTED.value,
|
||||
attempt_number=1,
|
||||
),
|
||||
),
|
||||
):
|
||||
_send_claimed_multichannel_job(
|
||||
_Session(job), # type: ignore[arg-type]
|
||||
job=job, # type: ignore[arg-type]
|
||||
claim_token="claim-1",
|
||||
context=_context(), # type: ignore[arg-type]
|
||||
channel_policy=DeliveryChannelPolicy.MAIL_THEN_POSTBOX,
|
||||
use_rate_limit=False,
|
||||
enqueue_imap_task=False,
|
||||
)
|
||||
|
||||
deliver_postbox.assert_called_once()
|
||||
|
||||
def test_accepted_postbox_fallback_prevents_later_mail_retry(self) -> None:
|
||||
job = _job()
|
||||
with (
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._postbox_outcome_from_attempts",
|
||||
return_value=PostboxChannelOutcome(
|
||||
accepted=1,
|
||||
rejected_temporary=1,
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._deliver_mail_channel"
|
||||
) as deliver_mail,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs.deliver_campaign_job_to_postboxes",
|
||||
return_value=PostboxChannelOutcome(accepted=2),
|
||||
) as deliver_postbox,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._finalize_multichannel_job",
|
||||
return_value=SendJobResult(
|
||||
job_id=job.id,
|
||||
status=JobSendStatus.POSTBOX_ACCEPTED.value,
|
||||
attempt_number=3,
|
||||
),
|
||||
),
|
||||
):
|
||||
_send_claimed_multichannel_job(
|
||||
_Session(job), # type: ignore[arg-type]
|
||||
job=job, # type: ignore[arg-type]
|
||||
claim_token="claim-1",
|
||||
context=_context(), # type: ignore[arg-type]
|
||||
channel_policy=DeliveryChannelPolicy.MAIL_THEN_POSTBOX,
|
||||
use_rate_limit=False,
|
||||
enqueue_imap_task=False,
|
||||
)
|
||||
|
||||
deliver_mail.assert_not_called()
|
||||
deliver_postbox.assert_called_once()
|
||||
|
||||
def test_unknown_postbox_fallback_prevents_every_later_effect(self) -> None:
|
||||
job = _job()
|
||||
prior = PostboxChannelOutcome(outcome_unknown=1)
|
||||
with (
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._postbox_outcome_from_attempts",
|
||||
return_value=prior,
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._deliver_mail_channel"
|
||||
) as deliver_mail,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs.deliver_campaign_job_to_postboxes"
|
||||
) as deliver_postbox,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._finalize_multichannel_job",
|
||||
return_value=SendJobResult(
|
||||
job_id=job.id,
|
||||
status=JobSendStatus.OUTCOME_UNKNOWN.value,
|
||||
attempt_number=1,
|
||||
),
|
||||
) as finalize,
|
||||
):
|
||||
_send_claimed_multichannel_job(
|
||||
_Session(job), # type: ignore[arg-type]
|
||||
job=job, # type: ignore[arg-type]
|
||||
claim_token="claim-1",
|
||||
context=_context(), # type: ignore[arg-type]
|
||||
channel_policy=DeliveryChannelPolicy.MAIL_THEN_POSTBOX,
|
||||
use_rate_limit=False,
|
||||
enqueue_imap_task=False,
|
||||
)
|
||||
|
||||
deliver_mail.assert_not_called()
|
||||
deliver_postbox.assert_not_called()
|
||||
self.assertIs(finalize.call_args.kwargs["postbox"], prior)
|
||||
|
||||
def test_postbox_acceptance_stops_mail_fallback(self) -> None:
|
||||
job = _job()
|
||||
with (
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs.deliver_campaign_job_to_postboxes",
|
||||
return_value=PostboxChannelOutcome(
|
||||
accepted=1,
|
||||
rejected_permanent=1,
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._deliver_mail_channel"
|
||||
) as deliver_mail,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._finalize_multichannel_job",
|
||||
return_value=SendJobResult(
|
||||
job_id=job.id,
|
||||
status=JobSendStatus.PARTIALLY_ACCEPTED.value,
|
||||
attempt_number=2,
|
||||
),
|
||||
),
|
||||
):
|
||||
_send_claimed_multichannel_job(
|
||||
_Session(job), # type: ignore[arg-type]
|
||||
job=job, # type: ignore[arg-type]
|
||||
claim_token="claim-1",
|
||||
context=_context(), # type: ignore[arg-type]
|
||||
channel_policy=DeliveryChannelPolicy.POSTBOX_THEN_MAIL,
|
||||
use_rate_limit=False,
|
||||
enqueue_imap_task=False,
|
||||
)
|
||||
|
||||
deliver_mail.assert_not_called()
|
||||
|
||||
def test_all_postbox_rejections_start_mail_fallback(self) -> None:
|
||||
job = _job()
|
||||
with (
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs.deliver_campaign_job_to_postboxes",
|
||||
return_value=PostboxChannelOutcome(
|
||||
rejected_temporary=1,
|
||||
rejected_permanent=1,
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._deliver_mail_channel",
|
||||
return_value=_MailChannelOutcome(accepted=True),
|
||||
) as deliver_mail,
|
||||
patch(
|
||||
"govoplan_campaign.backend.sending.jobs._finalize_multichannel_job",
|
||||
return_value=SendJobResult(
|
||||
job_id=job.id,
|
||||
status=JobSendStatus.SMTP_ACCEPTED.value,
|
||||
attempt_number=1,
|
||||
),
|
||||
),
|
||||
):
|
||||
_send_claimed_multichannel_job(
|
||||
_Session(job), # type: ignore[arg-type]
|
||||
job=job, # type: ignore[arg-type]
|
||||
claim_token="claim-1",
|
||||
context=_context(), # type: ignore[arg-type]
|
||||
channel_policy=DeliveryChannelPolicy.POSTBOX_THEN_MAIL,
|
||||
use_rate_limit=False,
|
||||
enqueue_imap_task=False,
|
||||
)
|
||||
|
||||
deliver_mail.assert_called_once()
|
||||
|
||||
def test_explicit_dual_delivery_reports_partial_and_unknown(self) -> None:
|
||||
self.assertEqual(
|
||||
JobSendStatus.PARTIALLY_ACCEPTED.value,
|
||||
_final_multichannel_status(
|
||||
channel_policy=DeliveryChannelPolicy.MAIL_AND_POSTBOX,
|
||||
mail=_MailChannelOutcome(accepted=True),
|
||||
postbox=PostboxChannelOutcome(rejected_permanent=1),
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
JobSendStatus.OUTCOME_UNKNOWN.value,
|
||||
_final_multichannel_status(
|
||||
channel_policy=DeliveryChannelPolicy.MAIL_AND_POSTBOX,
|
||||
mail=_MailChannelOutcome(accepted=True),
|
||||
postbox=PostboxChannelOutcome(outcome_unknown=1),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user