130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
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()
|