from __future__ import annotations from contextlib import AbstractContextManager from datetime import datetime, timezone from types import SimpleNamespace from unittest.mock import Mock import pytest from govoplan_core.core.recovery import RecoveryStatus from govoplan_campaign.backend import retention class _EvidenceSession(AbstractContextManager): def __init__(self, job: object | None) -> None: self.job = job def __enter__(self): return self def __exit__(self, *_args) -> None: return None def get(self, _model, _object_id): return self.job def _recovery(*, storage, storage_key: str | None, local_path: str | None): return retention._GeneratedArtifactRecovery( operation=Mock(), job_id="job-1", storage_key=storage_key, local_path=local_path, storage=storage, ) @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_retention_recovery_maps_verified_artifact_state( monkeypatch, outcome: str, expected_method: str, expected_status: RecoveryStatus | None, ) -> None: recovery = _recovery( storage=Mock(), storage_key="campaigns/message.eml", local_path=None, ) monkeypatch.setattr( retention, "_generated_artifact_recovery_evidence", lambda _recovery: ( outcome, { "verified": outcome != "outcome_unknown", "checks": {"artifact_locations_probed": True}, }, ), ) retention._finish_generated_artifact_recovery(recovery) method = getattr(recovery.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(recovery.operation, other).assert_not_called() @pytest.mark.parametrize( ("object_exists", "metadata_key", "expected"), [ (False, None, "succeeded"), (True, "campaigns/message.eml", "failed"), (False, "campaigns/message.eml", "recovery_required"), (True, None, "recovery_required"), ], ) def test_retention_recovery_compares_storage_and_database_independently( monkeypatch, object_exists: bool, metadata_key: str | None, expected: str, ) -> None: storage = Mock() storage.exists.return_value = object_exists job = SimpleNamespace( eml_storage_key=metadata_key, eml_local_path=None, ) monkeypatch.setattr( retention, "get_database", lambda: SimpleNamespace( SessionLocal=lambda: _EvidenceSession(job), ), ) outcome, evidence = retention._generated_artifact_recovery_evidence( _recovery( storage=storage, storage_key="campaigns/message.eml", local_path=None, ) ) assert outcome == expected assert evidence["verified"] is True def test_campaign_retention_commits_before_recovery_verification(monkeypatch) -> None: events: list[str] = [] session = Mock() session.commit.side_effect = lambda: events.append("commit") recovery = Mock() monkeypatch.setattr(retention, "_apply_raw_json_retention", lambda *_args, **_kwargs: {}) monkeypatch.setattr(retention, "_apply_report_detail_retention", lambda *_args, **_kwargs: {}) def apply_eml(*_args, **kwargs): kwargs["recovery_operations"].append(recovery) return {"metadata_cleared": 1} monkeypatch.setattr(retention, "_apply_eml_retention", apply_eml) monkeypatch.setattr( retention, "_finish_generated_artifact_recoveries", lambda _recoveries: events.append("verify"), ) result = retention.apply_campaign_retention( session, dry_run=False, now=datetime.now(timezone.utc), policy_for_campaign_id=lambda _campaign_id: object(), ) assert result["generated_eml"] == {"metadata_cleared": 1} assert events == ["commit", "verify"] session.rollback.assert_not_called()