feat: define bulk governance projection contract
This commit is contained in:
@@ -5506,6 +5506,67 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(deleted_delta.status_code, 200, deleted_delta.text)
|
||||
self.assertTrue(any(item["id"] == template_id and item["resource_type"] == "governance_template" for item in deleted_delta.json()["deleted"]))
|
||||
|
||||
def test_governance_template_bulk_synchronization_previews_and_applies(self) -> None:
|
||||
headers, login = self._login()
|
||||
tenant_id = login["tenant"]["id"]
|
||||
approver_headers = self._create_system_approver(
|
||||
headers,
|
||||
tenant_id=tenant_id,
|
||||
email="governance-sync-approver@example.local",
|
||||
)
|
||||
create_payload = {
|
||||
"kind": "role",
|
||||
"slug": "governance-sync",
|
||||
"name": "Governance Sync",
|
||||
"description": "Bulk reconciliation contract",
|
||||
"permissions": ["admin:roles:read"],
|
||||
"is_active": True,
|
||||
"assignments": [{"tenant_id": tenant_id, "mode": "required"}],
|
||||
}
|
||||
change_request_id = self._approved_configuration_change(
|
||||
headers,
|
||||
approver_headers,
|
||||
key="governance_templates",
|
||||
value=create_payload,
|
||||
target={"kind": "role", "slug": "governance-sync"},
|
||||
)
|
||||
created = self.client.post(
|
||||
"/api/v1/admin/system/governance-templates",
|
||||
headers=headers,
|
||||
json={**create_payload, "change_request_id": change_request_id},
|
||||
)
|
||||
self.assertEqual(201, created.status_code, created.text)
|
||||
template_id = created.json()["id"]
|
||||
|
||||
preview = self.client.post(
|
||||
"/api/v1/admin/system/governance-templates/synchronize",
|
||||
headers=headers,
|
||||
json={"template_ids": [template_id], "dry_run": True},
|
||||
)
|
||||
self.assertEqual(200, preview.status_code, preview.text)
|
||||
self.assertTrue(preview.json()["dry_run"])
|
||||
self.assertEqual({"unchanged": 1}, preview.json()["counts"])
|
||||
self.assertEqual("1", preview.json()["version"])
|
||||
self.assertEqual("admin.bulk-synchronization", preview.json()["outcomes"][0]["provenance"]["source"])
|
||||
|
||||
applied = self.client.post(
|
||||
"/api/v1/admin/system/governance-templates/synchronize",
|
||||
headers=headers,
|
||||
json={"template_ids": [template_id], "dry_run": False},
|
||||
)
|
||||
self.assertEqual(200, applied.status_code, applied.text)
|
||||
self.assertFalse(applied.json()["dry_run"])
|
||||
self.assertEqual({"unchanged": 1}, applied.json()["counts"])
|
||||
|
||||
audit = self.client.get(
|
||||
"/api/v1/admin/audit",
|
||||
headers=headers,
|
||||
params={"all_tenants": True, "limit": 500},
|
||||
)
|
||||
actions = {item["action"] for item in audit.json()["items"]}
|
||||
self.assertIn("governance_template.synchronization_previewed", actions)
|
||||
self.assertIn("governance_template.synchronized", actions)
|
||||
|
||||
def test_module_installer_history_supports_cursor_windows(self) -> None:
|
||||
from govoplan_core.core.module_installer import default_installer_runtime_dir
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.access import (
|
||||
AccessGovernanceProjectionV1,
|
||||
GovernanceProjectionBatch,
|
||||
GovernanceProjectionCommand,
|
||||
GovernanceProjectionResult,
|
||||
GovernanceTemplateMaterialization,
|
||||
)
|
||||
|
||||
|
||||
def _command(assignment_id: str) -> GovernanceProjectionCommand:
|
||||
return GovernanceProjectionCommand(
|
||||
assignment_id=assignment_id,
|
||||
operation="upsert",
|
||||
template=GovernanceTemplateMaterialization(
|
||||
template_id="template-1",
|
||||
kind="role",
|
||||
tenant_id=f"tenant-{assignment_id}",
|
||||
slug="reader",
|
||||
name="Reader",
|
||||
),
|
||||
provenance={"source": "contract-test"},
|
||||
)
|
||||
|
||||
|
||||
class _Projection:
|
||||
def reconcile(self, session: object, batch: GovernanceProjectionBatch) -> GovernanceProjectionResult:
|
||||
del session
|
||||
return GovernanceProjectionResult(
|
||||
operation_id=batch.operation_id,
|
||||
outcomes=(),
|
||||
dry_run=batch.dry_run,
|
||||
)
|
||||
|
||||
|
||||
class GovernanceProjectionContractTests(unittest.TestCase):
|
||||
def test_protocol_is_runtime_checkable(self) -> None:
|
||||
self.assertIsInstance(_Projection(), AccessGovernanceProjectionV1)
|
||||
|
||||
def test_batch_rejects_duplicate_assignment_ids(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "unique"):
|
||||
GovernanceProjectionBatch(
|
||||
operation_id="duplicate",
|
||||
commands=(_command("same"), _command("same")),
|
||||
)
|
||||
|
||||
def test_batch_enforces_bounds(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "between 1 and 500"):
|
||||
GovernanceProjectionBatch(operation_id="empty", commands=())
|
||||
with self.assertRaisesRegex(ValueError, "between 1 and 500"):
|
||||
GovernanceProjectionBatch(
|
||||
operation_id="large",
|
||||
commands=tuple(_command(str(index)) for index in range(501)),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user