Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import call, patch
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import Column, String, Table, create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_campaign.backend import route_support
|
||||
from govoplan_campaign.backend.routes import attachments as attachment_routes
|
||||
@@ -17,11 +19,14 @@ from govoplan_campaign.backend.campaign.mail_profile_boundary import (
|
||||
campaign_mail_profile_id,
|
||||
)
|
||||
from govoplan_campaign.backend.campaign.models import DeliveryConfig
|
||||
from govoplan_campaign.backend.db.models import Campaign, CampaignIssue, CampaignVersion
|
||||
from govoplan_campaign.backend.persistence.campaigns import CampaignPersistenceError, load_campaign_config_from_json
|
||||
from govoplan_campaign.backend.persistence.versions import update_campaign_version
|
||||
from govoplan_campaign.backend.integrations import MailCampaignIntegration
|
||||
from govoplan_campaign.backend.persistence.versions import _updated_runtime_json, update_campaign_version
|
||||
from govoplan_campaign.backend.integrations import MailCampaignIntegration, MailProfileError
|
||||
from govoplan_campaign.backend.sending.execution import ExecutionSnapshotError, create_execution_snapshot, ensure_execution_snapshot
|
||||
from govoplan_campaign.backend.schemas import CampaignVersionUpdateRequest
|
||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
def _campaign_json(server: dict[str, object] | None = None) -> dict[str, object]:
|
||||
@@ -237,6 +242,133 @@ def test_editing_a_legacy_record_requires_an_explicit_profile_migration() -> Non
|
||||
assert legacy_raw["server"]["smtp"]["password"] == "secret" # type: ignore[index]
|
||||
|
||||
|
||||
def test_explicit_legacy_migration_accepts_an_unchanged_authorized_profile_reference() -> None:
|
||||
legacy = _campaign_json({
|
||||
"mail_profile_id": "profile-1",
|
||||
"smtp": {"host": "smtp.example.test", "password": "legacy-secret"},
|
||||
"imap": None,
|
||||
"credentials": {},
|
||||
"inherit_smtp_credentials": True,
|
||||
"inherit_imap_credentials": True,
|
||||
})
|
||||
submitted = _campaign_json({"mail_profile_id": "profile-1"})
|
||||
integration = SimpleNamespace(assert_campaign_mail_policy_allows_json=Mock())
|
||||
session = object()
|
||||
with patch("govoplan_campaign.backend.persistence.versions.mail_integration", return_value=integration):
|
||||
result = _updated_runtime_json(
|
||||
session, # type: ignore[arg-type]
|
||||
tenant_id="tenant-1",
|
||||
campaign=SimpleNamespace(id="campaign-1"),
|
||||
version=SimpleNamespace(raw_json=legacy),
|
||||
raw_json=submitted,
|
||||
source_base_path=None,
|
||||
migrate_legacy_mail_settings=True,
|
||||
)
|
||||
|
||||
assert result == submitted
|
||||
assert result is not submitted
|
||||
assert campaign_mail_profile_boundary_violations(result) == ()
|
||||
assert "legacy-secret" not in repr(result)
|
||||
assert legacy["server"]["smtp"]["password"] == "legacy-secret" # type: ignore[index]
|
||||
integration.assert_campaign_mail_policy_allows_json.assert_called_once_with(
|
||||
session, tenant_id="tenant-1", raw_json=result, campaign_id="campaign-1"
|
||||
)
|
||||
|
||||
|
||||
def test_explicit_legacy_migration_requires_a_profile_and_preserves_mail_authorization() -> None:
|
||||
legacy = _campaign_json({"smtp": {"password": "legacy-secret"}})
|
||||
integration = SimpleNamespace(assert_campaign_mail_policy_allows_json=Mock(
|
||||
side_effect=MailProfileError("Mail profile is not authorized for this campaign")
|
||||
))
|
||||
with patch("govoplan_campaign.backend.persistence.versions.mail_integration", return_value=integration):
|
||||
for submitted, expected_error in (
|
||||
(_campaign_json(), CampaignPersistenceError),
|
||||
(_campaign_json({"mail_profile_id": "unauthorized-profile"}), MailProfileError),
|
||||
(_campaign_json({"mail_profile_id": "profile-1", "smtp": {}}), CampaignMailProfileBoundaryError),
|
||||
):
|
||||
with pytest.raises(expected_error):
|
||||
_updated_runtime_json(
|
||||
object(), # type: ignore[arg-type]
|
||||
tenant_id="tenant-1",
|
||||
campaign=SimpleNamespace(id="campaign-1"),
|
||||
version=SimpleNamespace(raw_json=legacy),
|
||||
raw_json=submitted,
|
||||
source_base_path=None,
|
||||
migrate_legacy_mail_settings=True,
|
||||
)
|
||||
assert integration.assert_campaign_mail_policy_allows_json.call_count == 1
|
||||
assert legacy["server"]["smtp"]["password"] == "legacy-secret" # type: ignore[index]
|
||||
|
||||
|
||||
def test_persisted_legacy_draft_can_migrate_then_save_other_content_without_changing_history() -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
for table_name in ("access_users", "access_groups"):
|
||||
if table_name not in Base.metadata.tables:
|
||||
Table(table_name, Base.metadata, Column("id", String(36), primary_key=True))
|
||||
Base.metadata.create_all(engine, tables=[
|
||||
Base.metadata.tables["access_users"],
|
||||
Base.metadata.tables["access_groups"],
|
||||
ChangeSequenceEntry.__table__,
|
||||
Campaign.__table__,
|
||||
CampaignVersion.__table__,
|
||||
CampaignIssue.__table__,
|
||||
])
|
||||
legacy = _campaign_json({
|
||||
"mail_profile_id": "profile-1",
|
||||
"smtp": {"host": "old.example.test", "password": "historical-secret"},
|
||||
"imap": None,
|
||||
"credentials": {},
|
||||
"inherit_smtp_credentials": True,
|
||||
"inherit_imap_credentials": True,
|
||||
})
|
||||
integration = SimpleNamespace(assert_campaign_mail_policy_allows_json=Mock())
|
||||
try:
|
||||
with Session(engine) as session, patch(
|
||||
"govoplan_campaign.backend.persistence.versions.mail_integration", return_value=integration
|
||||
):
|
||||
campaign = Campaign(id="campaign-1", tenant_id="tenant-1", external_id="campaign-1", name="Campaign", current_version_id="draft-1")
|
||||
historical = CampaignVersion(id="history-1", campaign_id=campaign.id, version_number=1, raw_json=legacy, workflow_state="final")
|
||||
draft = CampaignVersion(id="draft-1", campaign_id=campaign.id, version_number=2, raw_json=legacy, editor_state={"created_from": "minimal_campaign"})
|
||||
session.add_all((campaign, historical, draft))
|
||||
session.commit()
|
||||
original_revision = draft.edit_revision
|
||||
assert draft.mail_profile_migration_required
|
||||
|
||||
migrated = update_campaign_version(
|
||||
session,
|
||||
tenant_id="tenant-1", campaign_id=campaign.id, version_id=draft.id,
|
||||
raw_json=_campaign_json({"mail_profile_id": "profile-1"}),
|
||||
editor_state={"created_from": "minimal_campaign"},
|
||||
expected_revision=original_revision,
|
||||
migrate_legacy_mail_settings=True,
|
||||
autosave=True,
|
||||
)
|
||||
session.expire_all()
|
||||
persisted = session.get(CampaignVersion, migrated.id)
|
||||
assert persisted is not None
|
||||
assert not persisted.mail_profile_migration_required
|
||||
assert persisted.raw_json["server"] == {"mail_profile_id": "profile-1"}
|
||||
assert persisted.edit_revision > original_revision
|
||||
assert persisted.autosaved_at is not None
|
||||
assert "historical-secret" not in repr(persisted.raw_json)
|
||||
assert session.get(CampaignVersion, historical.id).raw_json == legacy
|
||||
|
||||
edited = _campaign_json({"mail_profile_id": "profile-1"})
|
||||
edited["template"]["subject"] = "Edited after migration" # type: ignore[index]
|
||||
updated = update_campaign_version(
|
||||
session,
|
||||
tenant_id="tenant-1", campaign_id=campaign.id, version_id=persisted.id,
|
||||
raw_json=edited,
|
||||
expected_revision=persisted.edit_revision,
|
||||
)
|
||||
session.expire_all()
|
||||
assert session.get(CampaignVersion, updated.id).raw_json["template"]["subject"] == "Edited after migration"
|
||||
assert session.get(CampaignVersion, historical.id).raw_json == legacy
|
||||
assert integration.assert_campaign_mail_policy_allows_json.call_count == 1
|
||||
finally:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_fork_inherited_profile_requires_mail_profile_use_scope() -> None:
|
||||
principal = SimpleNamespace(
|
||||
tenant_id="tenant-1",
|
||||
|
||||
Reference in New Issue
Block a user