feat: govern shared campaign artifact storage
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from govoplan_core.core.object_storage import (
|
||||
LocalFilesystemStorageBackend,
|
||||
StorageBackendError,
|
||||
)
|
||||
from govoplan_campaign.backend.persistence.campaigns import (
|
||||
CampaignPersistenceError,
|
||||
_persist_built_eml_artifacts,
|
||||
)
|
||||
from govoplan_campaign.backend.retention import _apply_eml_retention
|
||||
from govoplan_campaign.backend.sending.jobs import _load_eml_bytes_for_job
|
||||
|
||||
|
||||
class _FailingStorage:
|
||||
name = "test"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.objects: dict[str, bytes] = {}
|
||||
self.deleted: list[str] = []
|
||||
|
||||
def put_bytes(self, key: str, data: bytes, **_kwargs) -> None:
|
||||
if self.objects:
|
||||
raise StorageBackendError("object service unavailable")
|
||||
self.objects[key] = data
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
self.deleted.append(key)
|
||||
self.objects.pop(key, None)
|
||||
|
||||
|
||||
def test_partial_campaign_artifact_write_is_compensated(tmp_path: Path) -> None:
|
||||
first = tmp_path / "first.eml"
|
||||
second = tmp_path / "second.eml"
|
||||
first.write_bytes(b"Message-ID: <first@example.test>\r\n\r\nfirst")
|
||||
second.write_bytes(b"Message-ID: <second@example.test>\r\n\r\nsecond")
|
||||
messages = [
|
||||
SimpleNamespace(
|
||||
draft=SimpleNamespace(
|
||||
entry_index=1,
|
||||
eml_path=str(first),
|
||||
eml_size_bytes=None,
|
||||
)
|
||||
),
|
||||
SimpleNamespace(
|
||||
draft=SimpleNamespace(
|
||||
entry_index=2,
|
||||
eml_path=str(second),
|
||||
eml_size_bytes=None,
|
||||
)
|
||||
),
|
||||
]
|
||||
storage = _FailingStorage()
|
||||
|
||||
with pytest.raises(CampaignPersistenceError, match="could not be persisted"):
|
||||
_persist_built_eml_artifacts(
|
||||
storage=storage, # type: ignore[arg-type]
|
||||
tenant_id="tenant-1",
|
||||
campaign_id="campaign-1",
|
||||
version_id="version-1",
|
||||
build_id="build-1",
|
||||
built_messages=messages,
|
||||
)
|
||||
|
||||
assert storage.objects == {}
|
||||
assert len(storage.deleted) == 1
|
||||
|
||||
|
||||
def test_worker_loads_and_verifies_campaign_eml_from_object_storage(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
payload = b"Message-ID: <message@example.test>\r\n\r\nbody"
|
||||
storage = LocalFilesystemStorageBackend(tmp_path)
|
||||
storage.put_bytes("campaign-artifacts/message.eml", payload)
|
||||
job = SimpleNamespace(
|
||||
id="job-1",
|
||||
eml_storage_key="campaign-artifacts/message.eml",
|
||||
eml_local_path=None,
|
||||
eml_size_bytes=len(payload),
|
||||
eml_sha256=hashlib.sha256(payload).hexdigest(),
|
||||
message_id_header="<message@example.test>",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"govoplan_campaign.backend.sending.jobs.configured_storage_backend",
|
||||
return_value=storage,
|
||||
):
|
||||
assert _load_eml_bytes_for_job(job) == payload
|
||||
|
||||
|
||||
def test_retention_preserves_reference_when_object_store_is_unavailable() -> None:
|
||||
job = SimpleNamespace(
|
||||
campaign_id="campaign-1",
|
||||
updated_at=datetime.now(timezone.utc) - timedelta(days=10),
|
||||
queue_status="draft",
|
||||
send_status="smtp_accepted",
|
||||
imap_status="appended",
|
||||
eml_local_path=None,
|
||||
eml_storage_key="campaign-artifacts/message.eml",
|
||||
)
|
||||
session = MagicMock()
|
||||
session.query.return_value.filter.return_value.order_by.return_value.all.return_value = [
|
||||
job
|
||||
]
|
||||
storage = MagicMock()
|
||||
storage.exists.side_effect = StorageBackendError("object service unavailable")
|
||||
|
||||
result = _apply_eml_retention(
|
||||
session,
|
||||
dry_run=False,
|
||||
now=datetime.now(timezone.utc),
|
||||
policy_for_campaign_id=lambda _campaign_id: SimpleNamespace(
|
||||
generated_eml_retention_days=1
|
||||
),
|
||||
storage=storage,
|
||||
)
|
||||
|
||||
assert result["delete_failed"] == 1
|
||||
assert result["metadata_cleared"] == 0
|
||||
assert job.eml_storage_key == "campaign-artifacts/message.eml"
|
||||
session.add.assert_not_called()
|
||||
Reference in New Issue
Block a user