feat: harden shared platform contracts
This commit is contained in:
@@ -132,6 +132,24 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 201, response.text)
|
||||
return str(response.json()["id"])
|
||||
|
||||
def _campaign_version_precondition(
|
||||
self,
|
||||
headers: dict[str, str],
|
||||
campaign_id: str,
|
||||
version_id: str,
|
||||
) -> tuple[dict[str, str], int]:
|
||||
response = self.client.get(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
etag = response.headers.get("etag")
|
||||
self.assertIsNotNone(etag)
|
||||
return (
|
||||
{**headers, "If-Match": str(etag)},
|
||||
int(response.json()["edit_revision"]),
|
||||
)
|
||||
|
||||
def test_recipient_import_mapping_profiles_are_db_backed(self) -> None:
|
||||
headers, _ = self._login()
|
||||
payload = {
|
||||
@@ -950,6 +968,33 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
user_always = {item["id"]: item for item in user_docs_payload["layers"]["always"]["documentation"]}
|
||||
self.assertEqual(user_always["docs.configured-system-documentation"]["translation_locale"], "de")
|
||||
|
||||
sources = self.client.get("/api/v1/docs/sources", headers=headers)
|
||||
self.assertEqual(sources.status_code, 200, sources.text)
|
||||
source_payload = sources.json()
|
||||
self.assertEqual(source_payload["total"], len(source_payload["items"]))
|
||||
manifest_source = next(
|
||||
item
|
||||
for item in source_payload["items"]
|
||||
if item["id"] == "docs.manifest"
|
||||
)
|
||||
self.assertEqual(manifest_source["kind"], "manifest")
|
||||
self.assertNotIn("inspection", manifest_source)
|
||||
|
||||
inspected_source = self.client.get(
|
||||
manifest_source["inspection_url"],
|
||||
headers=headers,
|
||||
)
|
||||
self.assertEqual(inspected_source.status_code, 200, inspected_source.text)
|
||||
self.assertEqual(
|
||||
inspected_source.json()["inspection"]["module_id"],
|
||||
"docs",
|
||||
)
|
||||
unknown_source = self.client.get(
|
||||
"/api/v1/docs/sources/docs.unknown",
|
||||
headers=headers,
|
||||
)
|
||||
self.assertEqual(unknown_source.status_code, 404, unknown_source.text)
|
||||
|
||||
platform_status = self.client.get("/api/v1/platform/status", headers=headers)
|
||||
self.assertEqual(platform_status.status_code, 200, platform_status.text)
|
||||
i18n_status = platform_status.json()["i18n"]
|
||||
@@ -2747,7 +2792,10 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
archive.writestr("reports/february.txt", "February report")
|
||||
payload.seek(0)
|
||||
|
||||
with patch("govoplan_files.backend.router._read_limited_upload", side_effect=AssertionError("ZIP upload should not be fully buffered")):
|
||||
with patch(
|
||||
"govoplan_files.backend.routes.uploads._read_limited_upload",
|
||||
side_effect=AssertionError("ZIP upload should not be fully buffered"),
|
||||
):
|
||||
response = self.client.post(
|
||||
"/api/v1/files/upload-zip",
|
||||
headers=headers,
|
||||
@@ -2890,10 +2938,18 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertTrue(full_payload["full"])
|
||||
self.assertEqual(full_payload["current_version"]["id"], version_id)
|
||||
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
version_id,
|
||||
)
|
||||
autosaved = self.client.post(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/autosave",
|
||||
headers=headers,
|
||||
json={"current_step": "fields"},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"current_step": "fields",
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(autosaved.status_code, 200, autosaved.text)
|
||||
self.assertEqual(autosaved.json()["current_step"], "fields")
|
||||
@@ -3038,10 +3094,18 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(temporary.json()["user_lock_state"], "temporary")
|
||||
self.assertTrue(temporary.json()["user_locked_at"])
|
||||
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
version_id,
|
||||
)
|
||||
blocked_update = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
json={"current_step": "fields"},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"current_step": "fields",
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(blocked_update.status_code, 409, blocked_update.text)
|
||||
|
||||
@@ -3052,10 +3116,18 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(unlocked.status_code, 200, unlocked.text)
|
||||
self.assertIsNone(unlocked.json()["user_lock_state"])
|
||||
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
version_id,
|
||||
)
|
||||
updated = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
json={"current_step": "fields"},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"current_step": "fields",
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(updated.status_code, 200, updated.text)
|
||||
self.assertEqual(updated.json()["current_step"], "fields")
|
||||
@@ -3116,18 +3188,34 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertNotEqual(second_version_id, first_version_id)
|
||||
self.assertEqual(copied.json()["campaign"]["current_version_id"], second_version_id)
|
||||
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
first_version_id,
|
||||
)
|
||||
historical_update = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}",
|
||||
headers=headers,
|
||||
json={"current_step": "fields"},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"current_step": "fields",
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(historical_update.status_code, 409, historical_update.text)
|
||||
self.assertIn("Historical campaign versions are read-only", historical_update.text)
|
||||
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
second_version_id,
|
||||
)
|
||||
second_update = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}",
|
||||
headers=headers,
|
||||
json={"current_step": "fields"},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"current_step": "fields",
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(second_update.status_code, 200, second_update.text)
|
||||
|
||||
@@ -4613,8 +4701,14 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
)
|
||||
updated = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}",
|
||||
headers=headers,
|
||||
json={"campaign_json": campaign_json},
|
||||
headers={
|
||||
**headers,
|
||||
"If-Match": str(version_response.headers["etag"]),
|
||||
},
|
||||
json={
|
||||
"campaign_json": campaign_json,
|
||||
"base_revision": version_response.json()["edit_revision"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(updated.status_code, 200, updated.text)
|
||||
validated = self.client.post(
|
||||
@@ -5883,10 +5977,18 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(allowed_write.status_code, 200, allowed_write.text)
|
||||
changed_config = version_detail.json()["raw_json"]
|
||||
changed_config["entries"]["inline"][0]["to"][0]["email"] = "changed@example.org"
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
reader_headers,
|
||||
campaign_id,
|
||||
version_id,
|
||||
)
|
||||
denied_recipient_edit = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=reader_headers,
|
||||
json={"campaign_json": changed_config},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"campaign_json": changed_config,
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(denied_recipient_edit.status_code, 403, denied_recipient_edit.text)
|
||||
self.assertEqual(self.client.get(f"/api/v1/campaigns/{campaign_id}", headers=other_headers).status_code, 403)
|
||||
@@ -6030,18 +6132,29 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
}
|
||||
blocked_inline = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
json={"campaign_json": inline_json},
|
||||
headers={**headers, "If-Match": str(detail.headers["etag"])},
|
||||
json={
|
||||
"campaign_json": inline_json,
|
||||
"base_revision": detail.json()["edit_revision"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(blocked_inline.status_code, 422, blocked_inline.text)
|
||||
self.assertIn("may only reference", blocked_inline.json()["detail"])
|
||||
|
||||
reusable_json = detail.json()["raw_json"]
|
||||
reusable_json["server"] = {"mail_profile_id": tenant_profile_id}
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
version_id,
|
||||
)
|
||||
allowed_reusable = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
json={"campaign_json": reusable_json},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"campaign_json": reusable_json,
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(allowed_reusable.status_code, 200, allowed_reusable.text)
|
||||
|
||||
@@ -6080,8 +6193,11 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
raw_json["server"] = {"mail_profile_id": profile_id}
|
||||
denied_update = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
json={"campaign_json": raw_json},
|
||||
headers={**headers, "If-Match": str(detail.headers["etag"])},
|
||||
json={
|
||||
"campaign_json": raw_json,
|
||||
"base_revision": detail.json()["edit_revision"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(denied_update.status_code, 422, denied_update.text)
|
||||
|
||||
@@ -6091,10 +6207,18 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
json={"policy": {"allowed_profile_ids": [profile_id]}},
|
||||
)
|
||||
self.assertEqual(allowed_policy.status_code, 200, allowed_policy.text)
|
||||
mutation_headers, base_revision = self._campaign_version_precondition(
|
||||
headers,
|
||||
campaign_id,
|
||||
version_id,
|
||||
)
|
||||
allowed_update = self.client.put(
|
||||
f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
|
||||
headers=headers,
|
||||
json={"campaign_json": raw_json},
|
||||
headers=mutation_headers,
|
||||
json={
|
||||
"campaign_json": raw_json,
|
||||
"base_revision": base_revision,
|
||||
},
|
||||
)
|
||||
self.assertEqual(allowed_update.status_code, 200, allowed_update.text)
|
||||
|
||||
|
||||
@@ -250,6 +250,38 @@ class CoreEventTests(unittest.TestCase):
|
||||
self.assertIn("frame-ancestors 'none'", response.headers["Content-Security-Policy"])
|
||||
self.assertEqual("max-age=86400", response.headers["Strict-Transport-Security"])
|
||||
|
||||
def test_slow_request_log_excludes_query_headers_and_cookies(self) -> None:
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/slow")
|
||||
def slow() -> dict[str, bool]:
|
||||
return {"ok": True}
|
||||
|
||||
with patch("govoplan_core.server.fastapi._slow_request_threshold_ms", return_value=0.0001):
|
||||
app = create_govoplan_app(
|
||||
title="request log redaction test",
|
||||
version="test",
|
||||
registry=PlatformRegistry(),
|
||||
api_router=router,
|
||||
)
|
||||
|
||||
with self.assertLogs("govoplan.request", level="WARNING") as captured:
|
||||
with TestClient(app) as client:
|
||||
response = client.get(
|
||||
"/slow?token=query-secret",
|
||||
headers={
|
||||
"Authorization": "Bearer header-secret",
|
||||
"Cookie": "govoplan_session=cookie-secret",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(200, response.status_code, response.text)
|
||||
rendered = "\n".join(captured.output)
|
||||
self.assertIn("path=/slow", rendered)
|
||||
self.assertNotIn("query-secret", rendered)
|
||||
self.assertNotIn("header-secret", rendered)
|
||||
self.assertNotIn("cookie-secret", rendered)
|
||||
|
||||
def test_production_app_startup_rejects_an_unvalidated_environment(self) -> None:
|
||||
with patch.dict("os.environ", {"APP_ENV": "production"}, clear=True), self.assertRaisesRegex(
|
||||
RuntimeError,
|
||||
|
||||
@@ -3,7 +3,10 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationConfigurationDecision,
|
||||
DocumentationConfigurationProviderRegistration,
|
||||
DocumentationCondition,
|
||||
DocumentationSourceDefinition,
|
||||
DocumentationTopic,
|
||||
ModuleManifest,
|
||||
user_workflow_scope_condition_issues,
|
||||
@@ -67,6 +70,52 @@ class DocumentationTopicContractTests(unittest.TestCase):
|
||||
self.assertEqual(user_workflow_scope_condition_issues(user_reference), ())
|
||||
registry_for(scoped, admin_workflow, user_reference).validate()
|
||||
|
||||
def test_documentation_configuration_and_source_extensions_are_validated(self) -> None:
|
||||
resolver = lambda _context, keys: { # noqa: E731
|
||||
key: DocumentationConfigurationDecision(key=key, state="enabled")
|
||||
for key in keys
|
||||
}
|
||||
registry = PlatformRegistry()
|
||||
registry.register(ModuleManifest(
|
||||
id="example",
|
||||
name="Example",
|
||||
version="1.0.0",
|
||||
documentation_configuration_providers=(
|
||||
DocumentationConfigurationProviderRegistration(
|
||||
keys=("example.feature",),
|
||||
resolve=resolver,
|
||||
),
|
||||
),
|
||||
documentation_sources=(
|
||||
DocumentationSourceDefinition(
|
||||
id="example.handbook",
|
||||
kind="repository",
|
||||
label="Example handbook",
|
||||
),
|
||||
),
|
||||
))
|
||||
|
||||
registry.validate()
|
||||
|
||||
duplicate = PlatformRegistry()
|
||||
duplicate.register(ModuleManifest(
|
||||
id="example",
|
||||
name="Example",
|
||||
version="1.0.0",
|
||||
documentation_configuration_providers=(
|
||||
DocumentationConfigurationProviderRegistration(
|
||||
keys=("example.feature",),
|
||||
resolve=resolver,
|
||||
),
|
||||
DocumentationConfigurationProviderRegistration(
|
||||
keys=("example.feature",),
|
||||
resolve=resolver,
|
||||
),
|
||||
),
|
||||
))
|
||||
with self.assertRaisesRegex(RegistryError, "duplicate documentation configuration key"):
|
||||
duplicate.validate()
|
||||
|
||||
|
||||
def registry_for(*topics: DocumentationTopic) -> PlatformRegistry:
|
||||
registry = PlatformRegistry()
|
||||
|
||||
85
tests/test_mail_delivery_worker.py
Normal file
85
tests/test_mail_delivery_worker.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from govoplan_core.celery_app import (
|
||||
celery,
|
||||
dispatch_mail_outbox,
|
||||
purge_mail_outbox,
|
||||
)
|
||||
|
||||
|
||||
class _Provider:
|
||||
def dispatch_due(self, session, **kwargs):
|
||||
return {"session": session, **kwargs}
|
||||
|
||||
def purge_expired(self, session, **kwargs):
|
||||
return {"session": session, **kwargs}
|
||||
|
||||
|
||||
class MailDeliveryWorkerTests(unittest.TestCase):
|
||||
def test_dispatch_uses_optional_provider_boundary(self) -> None:
|
||||
session = object()
|
||||
|
||||
@contextmanager
|
||||
def session_scope():
|
||||
yield session
|
||||
|
||||
database = SimpleNamespace(SessionLocal=session_scope)
|
||||
with (
|
||||
patch("govoplan_core.db.session.get_database", return_value=database),
|
||||
patch(
|
||||
"govoplan_core.celery_app._mail_delivery_outbox",
|
||||
return_value=_Provider(),
|
||||
),
|
||||
):
|
||||
result = dispatch_mail_outbox.run("tenant-1", 7)
|
||||
|
||||
self.assertIs(result["session"], session)
|
||||
self.assertEqual(result["tenant_id"], "tenant-1")
|
||||
self.assertEqual(result["limit"], 7)
|
||||
|
||||
def test_purge_uses_optional_provider_boundary(self) -> None:
|
||||
session = object()
|
||||
|
||||
@contextmanager
|
||||
def session_scope():
|
||||
yield session
|
||||
|
||||
database = SimpleNamespace(SessionLocal=session_scope)
|
||||
with (
|
||||
patch("govoplan_core.db.session.get_database", return_value=database),
|
||||
patch(
|
||||
"govoplan_core.celery_app._mail_delivery_outbox",
|
||||
return_value=_Provider(),
|
||||
),
|
||||
):
|
||||
result = purge_mail_outbox.run(19)
|
||||
|
||||
self.assertIs(result["session"], session)
|
||||
self.assertEqual(result["limit"], 19)
|
||||
|
||||
def test_worker_routes_and_schedules_are_declared(self) -> None:
|
||||
self.assertEqual(
|
||||
celery.conf.task_routes["govoplan.mail.dispatch_outbox"],
|
||||
{"queue": "mail"},
|
||||
)
|
||||
self.assertEqual(
|
||||
celery.conf.task_routes["govoplan.mail.purge_outbox"],
|
||||
{"queue": "mail"},
|
||||
)
|
||||
self.assertEqual(
|
||||
celery.conf.beat_schedule["mail-outbox-every-five-seconds"]["task"],
|
||||
"govoplan.mail.dispatch_outbox",
|
||||
)
|
||||
self.assertEqual(
|
||||
celery.conf.beat_schedule["mail-outbox-retention-daily"]["task"],
|
||||
"govoplan.mail.purge_outbox",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
175
tests/test_optimistic_concurrency.py
Normal file
175
tests/test_optimistic_concurrency.py
Normal file
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import Integer, String, create_engine
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from govoplan_core.core.concurrency import (
|
||||
MissingPreconditionError,
|
||||
RevisionConflictError,
|
||||
assert_revision_precondition,
|
||||
claim_revision,
|
||||
if_match_matches,
|
||||
strong_resource_etag,
|
||||
three_way_merge,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
|
||||
|
||||
class _RevisionFixture(Base):
|
||||
__tablename__ = "test_revision_fixture"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
edit_revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||
value: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
|
||||
class OptimisticConcurrencyTests(unittest.TestCase):
|
||||
def test_strong_etags_and_if_match_use_strong_comparison(self) -> None:
|
||||
etag = strong_resource_etag("campaign_version", "version-1", 3)
|
||||
|
||||
self.assertTrue(if_match_matches(etag, etag))
|
||||
self.assertTrue(if_match_matches(f'"other", {etag}', etag))
|
||||
self.assertFalse(if_match_matches(f"W/{etag}", etag))
|
||||
self.assertNotEqual(
|
||||
etag,
|
||||
strong_resource_etag("campaign_version", "version-1", 4),
|
||||
)
|
||||
with self.assertRaises(MissingPreconditionError):
|
||||
assert_revision_precondition(
|
||||
None,
|
||||
resource_type="campaign_version",
|
||||
resource_id="version-1",
|
||||
submitted_base_revision=3,
|
||||
)
|
||||
|
||||
def test_compare_and_set_allows_only_one_writer_for_a_revision(self) -> None:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
_RevisionFixture.__table__.create(engine)
|
||||
session_factory = sessionmaker(bind=engine, class_=Session)
|
||||
with session_factory() as session:
|
||||
session.add(_RevisionFixture(id="resource-1", value="base"))
|
||||
session.commit()
|
||||
next_revision = claim_revision(
|
||||
session,
|
||||
model=_RevisionFixture,
|
||||
filters=(_RevisionFixture.id == "resource-1",),
|
||||
revision_attribute="edit_revision",
|
||||
expected_revision=1,
|
||||
resource_type="fixture",
|
||||
resource_id="resource-1",
|
||||
)
|
||||
session.commit()
|
||||
self.assertEqual(next_revision, 2)
|
||||
|
||||
with self.assertRaises(RevisionConflictError) as raised:
|
||||
claim_revision(
|
||||
session,
|
||||
model=_RevisionFixture,
|
||||
filters=(_RevisionFixture.id == "resource-1",),
|
||||
revision_attribute="edit_revision",
|
||||
expected_revision=1,
|
||||
resource_type="fixture",
|
||||
resource_id="resource-1",
|
||||
)
|
||||
self.assertEqual(raised.exception.current_revision, 2)
|
||||
engine.dispose()
|
||||
|
||||
def test_disjoint_map_and_stable_id_collection_changes_merge(self) -> None:
|
||||
base = {
|
||||
"campaign": {"name": "Original", "description": "Base"},
|
||||
"entries": {
|
||||
"inline": [
|
||||
{"id": "one", "name": "One", "active": True},
|
||||
{"id": "two", "name": "Two", "active": True},
|
||||
]
|
||||
},
|
||||
}
|
||||
local = {
|
||||
**base,
|
||||
"campaign": {"name": "Local", "description": "Base"},
|
||||
"entries": {
|
||||
"inline": [
|
||||
{"id": "one", "name": "One", "active": False},
|
||||
{"id": "two", "name": "Two", "active": True},
|
||||
]
|
||||
},
|
||||
}
|
||||
current = {
|
||||
**base,
|
||||
"campaign": {"name": "Original", "description": "Remote"},
|
||||
"entries": {
|
||||
"inline": [
|
||||
{"id": "one", "name": "One", "active": True},
|
||||
{"id": "two", "name": "Remote Two", "active": True},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
result = three_way_merge(base, local, current)
|
||||
|
||||
self.assertTrue(result.merged)
|
||||
self.assertEqual(result.value["campaign"], {"name": "Local", "description": "Remote"})
|
||||
self.assertEqual(result.value["entries"]["inline"][0]["active"], False)
|
||||
self.assertEqual(result.value["entries"]["inline"][1]["name"], "Remote Two")
|
||||
|
||||
def test_same_path_unkeyed_collection_and_protected_edits_conflict(self) -> None:
|
||||
same_path = three_way_merge(
|
||||
{"name": "Base"},
|
||||
{"name": "Local"},
|
||||
{"name": "Remote"},
|
||||
)
|
||||
self.assertEqual(same_path.conflicts[0].kind, "same_path_changed")
|
||||
|
||||
unkeyed = three_way_merge(
|
||||
{"values": ["a"]},
|
||||
{"values": ["a", "local"]},
|
||||
{"values": ["a", "remote"]},
|
||||
)
|
||||
self.assertEqual(unkeyed.conflicts[0].kind, "unkeyed_collection")
|
||||
|
||||
protected = three_way_merge(
|
||||
{"workflow_state": "draft", "name": "Base"},
|
||||
{"workflow_state": "ready", "name": "Base"},
|
||||
{"workflow_state": "draft", "name": "Remote"},
|
||||
protected_paths=("/workflow_state",),
|
||||
)
|
||||
self.assertEqual(protected.conflicts[0].kind, "protected_path")
|
||||
|
||||
def test_delete_versus_edit_and_conflicting_reorder_remain_explicit(self) -> None:
|
||||
base = [{"id": "one", "value": "1"}, {"id": "two", "value": "2"}]
|
||||
delete_vs_edit = three_way_merge(
|
||||
base,
|
||||
[{"id": "two", "value": "2"}],
|
||||
[{"id": "one", "value": "remote"}, {"id": "two", "value": "2"}],
|
||||
)
|
||||
self.assertEqual(delete_vs_edit.conflicts[0].kind, "delete_vs_edit")
|
||||
|
||||
reordered = three_way_merge(
|
||||
[
|
||||
{"id": "one"},
|
||||
{"id": "two"},
|
||||
{"id": "three"},
|
||||
],
|
||||
[
|
||||
{"id": "two"},
|
||||
{"id": "one"},
|
||||
{"id": "three"},
|
||||
],
|
||||
[
|
||||
{"id": "one"},
|
||||
{"id": "three"},
|
||||
{"id": "two"},
|
||||
],
|
||||
)
|
||||
self.assertEqual(reordered.conflicts[0].kind, "collection_reorder")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.postbox import (
|
||||
@@ -11,6 +12,7 @@ from govoplan_core.core.postbox import (
|
||||
CAPABILITY_POSTBOX_MESSAGES,
|
||||
PostboxAccessProvider,
|
||||
PostboxDeliveryProvider,
|
||||
PostboxDeliveryReceiptSummaryRef,
|
||||
PostboxDirectoryProvider,
|
||||
PostboxEvidenceProvider,
|
||||
PostboxMessagesProvider,
|
||||
@@ -69,6 +71,17 @@ class _PostboxProvider:
|
||||
del session, tenant_id, message_id, attachment
|
||||
return None
|
||||
|
||||
def delivery_receipt_summaries(
|
||||
self,
|
||||
session,
|
||||
*,
|
||||
tenant_id,
|
||||
producer_module,
|
||||
delivery_ids,
|
||||
):
|
||||
del session, tenant_id, producer_module, delivery_ids
|
||||
return {}
|
||||
|
||||
|
||||
class PostboxContractTests(unittest.TestCase):
|
||||
def test_protocols_and_registry_helpers_resolve_without_module_imports(self) -> None:
|
||||
@@ -78,6 +91,16 @@ class PostboxContractTests(unittest.TestCase):
|
||||
self.assertIsInstance(provider, PostboxMessagesProvider)
|
||||
self.assertIsInstance(provider, PostboxDeliveryProvider)
|
||||
self.assertIsInstance(provider, PostboxEvidenceProvider)
|
||||
self.assertEqual(
|
||||
"delivery-1",
|
||||
PostboxDeliveryReceiptSummaryRef(
|
||||
delivery_id="delivery-1",
|
||||
message_id="message-1",
|
||||
postbox_id="postbox-1",
|
||||
delivery_status="accepted",
|
||||
accepted_at=datetime.now(timezone.utc),
|
||||
).delivery_id,
|
||||
)
|
||||
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
|
||||
35
tests/test_security_redaction.py
Normal file
35
tests/test_security_redaction.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_core.security.redaction import contains_plain_secret, redact_secret_values
|
||||
|
||||
|
||||
class SecurityRedactionTests(unittest.TestCase):
|
||||
def test_structured_payload_redaction_covers_nested_container_shapes(self) -> None:
|
||||
payload = {
|
||||
"request": {
|
||||
"Authorization": "Bearer must-not-leak",
|
||||
"headers": [{"x-api-key": "must-not-leak"}],
|
||||
},
|
||||
"records": (
|
||||
{"clientSecret": "must-not-leak", "status": "ok"},
|
||||
{"credential_ref": "credential-1"},
|
||||
),
|
||||
}
|
||||
|
||||
redacted = redact_secret_values(payload)
|
||||
|
||||
self.assertEqual("<redacted>", redacted["request"]["Authorization"]) # type: ignore[index]
|
||||
self.assertEqual("<redacted>", redacted["request"]["headers"][0]["x-api-key"]) # type: ignore[index]
|
||||
self.assertEqual("<redacted>", redacted["records"][0]["clientSecret"]) # type: ignore[index]
|
||||
self.assertEqual("<redacted>", redacted["records"][1]["credential_ref"]) # type: ignore[index]
|
||||
self.assertNotIn("must-not-leak", repr(redacted))
|
||||
|
||||
def test_plain_secret_detection_descends_into_lists_and_tuples(self) -> None:
|
||||
self.assertTrue(contains_plain_secret([{"metadata": ({"private-key": "secret"},)}]))
|
||||
self.assertFalse(contains_plain_secret([{"credential_ref": "credential-1"}]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user