Fence Campaign delivery effects in recovery ledger

This commit is contained in:
2026-08-03 03:48:03 +02:00
parent dd09b06c47
commit 9da03090a7
7 changed files with 821 additions and 39 deletions
+28
View File
@@ -0,0 +1,28 @@
from __future__ import annotations
import pytest
from govoplan_core.core import runtime_coordination
from govoplan_core.core.runtime_coordination import (
RuntimeIdentity,
bind_process_runtime_identity,
)
@pytest.fixture(scope="session", autouse=True)
def _bind_test_runtime_identity():
previous = runtime_coordination._process_runtime_identity
bind_process_runtime_identity(
RuntimeIdentity(
installation_id="campaign-tests",
node_id="campaign-test-process",
incarnation="campaign-test-incarnation",
role="test",
software_version="test",
composition_hash="a" * 64,
)
)
try:
yield
finally:
bind_process_runtime_identity(previous)
+119
View File
@@ -0,0 +1,119 @@
from __future__ import annotations
from unittest.mock import Mock
import pytest
from govoplan_core.core.recovery import RecoveryStatus
from govoplan_campaign.backend.sending import jobs
@pytest.mark.parametrize(
("outcome", "expected_method", "expected_status"),
[
("succeeded", "succeed", None),
("failed", "reject", None),
("outcome_unknown", "unresolved", RecoveryStatus.OUTCOME_UNKNOWN),
("recovery_required", "unresolved", RecoveryStatus.RECOVERY_REQUIRED),
],
)
def test_delivery_recovery_maps_verified_job_state(
monkeypatch,
outcome: str,
expected_method: str,
expected_status: RecoveryStatus | None,
) -> None:
evidence = {
"verified": outcome != "recovery_required",
"checks": {"job_state_reloaded": True},
"send_status": outcome,
}
monkeypatch.setattr(
jobs,
"_job_delivery_recovery_evidence",
lambda _job_id: (outcome, evidence),
)
operation = Mock()
jobs._finish_job_delivery_recovery(operation, job_id="job-1")
method = getattr(operation, expected_method)
method.assert_called_once()
if expected_status is not None:
assert method.call_args.kwargs["status"] == expected_status
for other in {"succeed", "reject", "unresolved"} - {expected_method}:
getattr(operation, other).assert_not_called()
@pytest.mark.parametrize(
("outcome", "expected_method", "expected_status"),
[
("succeeded", "succeed", None),
("failed", "reject", None),
("outcome_unknown", "unresolved", RecoveryStatus.OUTCOME_UNKNOWN),
("recovery_required", "unresolved", RecoveryStatus.RECOVERY_REQUIRED),
],
)
def test_imap_recovery_maps_verified_append_state(
monkeypatch,
outcome: str,
expected_method: str,
expected_status: RecoveryStatus | None,
) -> None:
evidence = {
"verified": outcome != "recovery_required",
"checks": {"job_state_reloaded": True},
"imap_status": outcome,
}
monkeypatch.setattr(
jobs,
"_imap_recovery_evidence",
lambda _job_id: (outcome, evidence),
)
operation = Mock()
jobs._finish_imap_append_recovery(operation, job_id="job-1")
method = getattr(operation, expected_method)
method.assert_called_once()
if expected_status is not None:
assert method.call_args.kwargs["status"] == expected_status
for other in {"succeed", "reject", "unresolved"} - {expected_method}:
getattr(operation, other).assert_not_called()
@pytest.mark.parametrize(
("outcome", "expected_method", "expected_status"),
[
("succeeded", "succeed", None),
("failed", "reject", None),
("outcome_unknown", "unresolved", RecoveryStatus.OUTCOME_UNKNOWN),
("recovery_required", "unresolved", RecoveryStatus.RECOVERY_REQUIRED),
],
)
def test_single_action_recovery_maps_verified_action_state(
monkeypatch,
outcome: str,
expected_method: str,
expected_status: RecoveryStatus | None,
) -> None:
evidence = {
"verified": outcome != "recovery_required",
"checks": {"action_state_reloaded": True},
"action_status": outcome,
}
monkeypatch.setattr(
jobs,
"_single_action_recovery_evidence",
lambda _action_id: (outcome, evidence),
)
operation = Mock()
jobs._finish_single_action_delivery_recovery(operation, action_id="action-1")
method = getattr(operation, expected_method)
method.assert_called_once()
if expected_status is not None:
assert method.call_args.kwargs["status"] == expected_status
for other in {"succeed", "reject", "unresolved"} - {expected_method}:
getattr(operation, other).assert_not_called()
+5
View File
@@ -159,6 +159,11 @@ def test_post_provider_persistence_failure_freezes_imap_retry() -> None:
patch("govoplan_campaign.backend.sending.jobs._load_eml_bytes_for_job", return_value=b"message"),
patch("govoplan_campaign.backend.sending.jobs._claim_job_for_imap_append", return_value="claim-1"),
patch("govoplan_campaign.backend.sending.jobs._record_imap_attempt_start", return_value=attempt),
patch(
"govoplan_campaign.backend.sending.jobs._begin_imap_append_recovery",
return_value=SimpleNamespace(replayed=False, operation=MagicMock()),
),
patch("govoplan_campaign.backend.sending.jobs._finish_imap_append_recovery"),
patch("govoplan_campaign.backend.sending.jobs.mail_integration", return_value=Mail()),
patch(
"govoplan_campaign.backend.sending.jobs._record_imap_append_success",
+14
View File
@@ -170,6 +170,13 @@ class CampaignSingleMessageActionTests(unittest.TestCase):
"govoplan_campaign.backend.sending.jobs.mail_integration",
return_value=mail,
),
patch(
"govoplan_campaign.backend.sending.jobs._begin_single_action_delivery_recovery",
return_value=SimpleNamespace(replayed=False, operation=Mock()),
),
patch(
"govoplan_campaign.backend.sending.jobs._finish_single_action_delivery_recovery"
),
patch(
"govoplan_campaign.backend.sending.jobs.audit_event"
),
@@ -269,6 +276,13 @@ class CampaignSingleMessageActionTests(unittest.TestCase):
"govoplan_campaign.backend.sending.jobs.mail_integration",
return_value=mail,
),
patch(
"govoplan_campaign.backend.sending.jobs._begin_single_action_delivery_recovery",
return_value=SimpleNamespace(replayed=False, operation=Mock()),
),
patch(
"govoplan_campaign.backend.sending.jobs._finish_single_action_delivery_recovery"
),
patch(
"govoplan_campaign.backend.sending.jobs.files_integration"
) as files,