Implement task 006 site config versioning and audit events

This commit is contained in:
2026-05-21 19:13:19 +03:00
parent cf84400118
commit ff1a2f78c0
23 changed files with 1878 additions and 32 deletions
@@ -121,6 +121,35 @@ class AuthAuthorizationPublicApiTest(unittest.TestCase):
version_activate_response.text,
)
def test_editor_cannot_rollback_script_config_version(self) -> None:
site_response = self.client.post(
"/api/sites",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._site_payload("rollback_denied_blog"),
)
self.assertEqual(201, site_response.status_code, site_response.text)
site_id = site_response.json()["site"]["id"]
first_version = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._script_version_payload(),
)
self.assertEqual(201, first_version.status_code, first_version.text)
version_id = first_version.json()["version"]["id"]
activate_response = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions/{version_id}/activate",
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
)
rollback_response = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions/{version_id}/rollback",
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
)
self.assertEqual(403, activate_response.status_code, activate_response.text)
self.assertEqual(403, rollback_response.status_code, rollback_response.text)
def test_admin_can_create_and_activate_script_config_version(self) -> None:
site_response = self.client.post(
"/api/sites",
@@ -147,6 +176,121 @@ class AuthAuthorizationPublicApiTest(unittest.TestCase):
self.assertEqual(200, activate_response.status_code, activate_response.text)
self.assertEqual("ACTIVE", activate_response.json()["version"]["status"])
def test_admin_can_get_site_by_id(self) -> None:
site_response = self.client.post(
"/api/sites",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._site_payload("admin_get_site_by_id_blog"),
)
self.assertEqual(201, site_response.status_code, site_response.text)
site_id = site_response.json()["site"]["id"]
get_response = self.client.get(
f"/api/sites/{site_id}",
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
)
self.assertEqual(200, get_response.status_code, get_response.text)
self.assertEqual(site_id, get_response.json()["site"]["id"])
def test_admin_can_create_version_with_rollback_target(self) -> None:
site_response = self.client.post(
"/api/sites",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._site_payload("rollback_target_blog"),
)
self.assertEqual(201, site_response.status_code, site_response.text)
site_id = site_response.json()["site"]["id"]
baseline = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._script_version_payload(diff={"summary": "baseline"}),
)
self.assertEqual(201, baseline.status_code, baseline.text)
baseline_id = baseline.json()["version"]["id"]
create_response = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._script_version_payload(
diff={"summary": "rollback-aware"},
rollback_target_version_id=baseline_id,
activate=True,
),
)
self.assertEqual(201, create_response.status_code, create_response.text)
created = create_response.json()["version"]
self.assertEqual("ACTIVE", created["status"])
self.assertEqual("rollback-aware", created["diff"].get("summary"))
self.assertEqual(baseline_id, created["rollback_target_version_id"])
versions_response = self.client.get(
f"/api/sites/{site_id}/publishing-config/versions",
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
)
self.assertEqual(200, versions_response.status_code, versions_response.text)
self.assertEqual(2, len(versions_response.json()["versions"]))
def test_admin_can_rollback_script_config_version(self) -> None:
site_response = self.client.post(
"/api/sites",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._site_payload("rollback_action_blog"),
)
self.assertEqual(201, site_response.status_code, site_response.text)
site_id = site_response.json()["site"]["id"]
active = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._script_version_payload(
diff={"summary": "initial active"},
activate=True,
),
)
self.assertEqual(201, active.status_code, active.text)
active_id = active.json()["version"]["id"]
next_version = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
json=self._script_version_payload(diff={"summary": "next version"}),
)
self.assertEqual(201, next_version.status_code, next_version.text)
next_id = next_version.json()["version"]["id"]
self.client.post(
f"/api/sites/{site_id}/publishing-config/versions/{next_id}/activate",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
)
rollback_response = self.client.post(
f"/api/sites/{site_id}/publishing-config/versions/{active_id}/rollback",
headers={DEMO_USER_EMAIL_HEADER: DEMO_ADMIN_EMAIL},
)
self.assertEqual(200, rollback_response.status_code, rollback_response.text)
self.assertEqual(active_id, rollback_response.json()["version"]["id"])
self.assertEqual(
"ACTIVE",
rollback_response.json()["version"]["status"],
)
self.assertEqual(
next_id,
rollback_response.json()["version"]["rollback_target_version_id"],
)
get_response = self.client.get(
f"/api/sites/{site_id}",
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
)
self.assertEqual(200, get_response.status_code, get_response.text)
self.assertEqual(
active_id,
get_response.json()["site"]["active_script_config_version_id"],
)
def test_editor_can_create_articles_and_approve_plan_review(self) -> None:
sites_response = self.client.get(
"/api/sites",
@@ -215,12 +359,22 @@ class AuthAuthorizationPublicApiTest(unittest.TestCase):
},
}
def _script_version_payload(self) -> dict[str, object]:
return {
def _script_version_payload(
self,
diff: dict[str, object] | None = None,
rollback_target_version_id: str | None = None,
activate: bool = False,
) -> dict[str, object]:
payload = {
"publishing_yaml": "target: editorial_ops\nrepository:\n branch: main\n",
"transform_script": "export function transformArticle(article) { return article; }\n",
"diff": {"summary": "Task 004 authorization fixture."},
"diff": diff or {"summary": "Task 004 authorization fixture."},
}
if rollback_target_version_id is not None:
payload["rollback_target_version_id"] = rollback_target_version_id
if activate:
payload["activate"] = True
return payload
if __name__ == "__main__":
@@ -0,0 +1,134 @@
from __future__ import annotations
import os
import tempfile
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
import unittest
from uuid import UUID
BACKEND_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(BACKEND_ROOT))
@contextmanager
def repository_dsn() -> Iterator[str]:
configured = os.environ.get("PIPELINE_TEST_DATABASE_DSN")
if configured:
yield configured
return
with tempfile.TemporaryDirectory() as directory:
yield f"sqlite:///{Path(directory) / 'pipeline-audit.db'}"
class ScriptConfigAuditTest(unittest.TestCase):
def test_rollback_writes_audit_event(self) -> None:
with repository_dsn() as dsn:
from src.application.auth import get_current_user_by_email
from src.application.seed_data import seed_reference_data
from src.application.site_config import (
create_script_config_version,
create_target_site,
rollback_script_config_version,
)
from src.domain.contracts import (
PublishingRules,
ScriptConfigVersionCreateRequest,
TargetSiteConfigCreateRequest,
)
from src.infrastructure.repositories import open_backend_repository
repository = open_backend_repository(dsn)
repository.setup()
seed_reference_data(repository)
admin = get_current_user_by_email(repository, "admin@example.com")
site = create_target_site(
repository,
TargetSiteConfigCreateRequest(
name="Audit Test Site",
slug="audit-site",
publishing_type="git_next",
default_language="en",
brand_voice="Audit-focused publishing profile.",
audience="QA operations team.",
seo_rules={"primary_keyword_required": True},
visual_rules={"hero_style": "technical"},
source_rules={"minimum_sources": 1},
publishing_rules=PublishingRules(
repository_url="git@github.com:example/audit-site.git",
production_branch="main",
content_format="mdx",
content_path_template="content/articles/{slug}.mdx",
asset_path_template="public/articles/{slug}/{filename}",
frontmatter_mapping={"title": "title"},
dry_run_renderer="next-mdx",
),
active_script_config_version_id=None,
),
).site
initial = create_script_config_version(
repository,
site_id=site.id,
current_user=admin,
request=self._script_version_payload(summary="initial", activate=True),
).version
candidate = create_script_config_version(
repository,
site_id=site.id,
current_user=admin,
request=self._script_version_payload(
summary="candidate",
activate=True,
rollback_target_version_id=initial.id,
),
).version
rolled_back = rollback_script_config_version(
repository,
site_id=site.id,
version_id=initial.id,
current_user=admin,
)
self.assertEqual(str(initial.id), str(rolled_back.version.id))
rollback_events = [
event
for event in repository.script_config_version_events.list_for_site(site.id)
if event["event_type"] == "SCRIPT_CONFIG_VERSION_ROLLBACK"
]
self.assertEqual(1, len(rollback_events))
rollback_event = rollback_events[0]
self.assertEqual(str(site.id), str(rollback_event["target_site_id"]))
self.assertEqual(str(initial.id), str(rollback_event["version_id"]))
self.assertEqual(str(admin.id), str(rollback_event["actor_user_id"]))
self.assertEqual(
str(candidate.id),
rollback_event["payload"]["rollback_target_version_id"],
)
active_site = repository.target_sites.get_by_id(site.id)
self.assertEqual(initial.id, active_site.active_script_config_version_id)
def _script_version_payload(
self,
summary: str,
activate: bool = False,
rollback_target_version_id: UUID | None = None,
) -> ScriptConfigVersionCreateRequest:
payload = ScriptConfigVersionCreateRequest(
publishing_yaml="target:\n repo: audit\n",
transform_script="export function transformArticle(article) { return article; }",
diff={"summary": summary},
activate=activate,
)
if rollback_target_version_id is not None:
payload.rollback_target_version_id = rollback_target_version_id
return payload
if __name__ == "__main__":
unittest.main()