feat: define policy impact subject contract
This commit is contained in:
@@ -6219,6 +6219,121 @@ class ApiSmokeTests(unittest.TestCase):
|
||||
self.assertEqual(campaign_source["path"], f"campaign:{campaign_id}")
|
||||
self.assertIn("allow_campaign_profiles", campaign_source["applied_fields"])
|
||||
|
||||
def test_policy_impact_preview_is_bounded_audited_and_linked_to_commit(self) -> None:
|
||||
headers, _ = self._login()
|
||||
proposed_policy = {"visible_surface_ids": []}
|
||||
|
||||
preview = self.client.post(
|
||||
"/api/v1/admin/policy-impact/preview",
|
||||
headers=headers,
|
||||
json={
|
||||
"policy_family": "view",
|
||||
"scope_type": "tenant",
|
||||
"proposed_policy": proposed_policy,
|
||||
"populations": [
|
||||
{
|
||||
"provider_id": "views",
|
||||
"selector": {
|
||||
"include_views": False,
|
||||
"include_surfaces": True,
|
||||
},
|
||||
"limit": 10,
|
||||
}
|
||||
],
|
||||
"include_details": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, preview.status_code, preview.text)
|
||||
preview_payload = preview.json()
|
||||
self.assertEqual(10, preview_payload["counts"]["newly_denied"])
|
||||
self.assertEqual("truncated", preview_payload["populations"][0]["state"])
|
||||
self.assertEqual(10, len(preview_payload["effects"]))
|
||||
|
||||
unchanged = self.client.get(
|
||||
"/api/v1/admin/view-policies/tenant",
|
||||
headers=headers,
|
||||
)
|
||||
self.assertEqual(200, unchanged.status_code, unchanged.text)
|
||||
self.assertEqual({}, unchanged.json()["policy"])
|
||||
|
||||
committed = self.client.put(
|
||||
"/api/v1/admin/view-policies/tenant",
|
||||
headers=headers,
|
||||
json={
|
||||
"policy": proposed_policy,
|
||||
"impact_preview_id": preview_payload["preview_id"],
|
||||
"impact_proposal_hash": preview_payload["proposal_hash"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, committed.status_code, committed.text)
|
||||
self.assertEqual([], committed.json()["policy"]["visible_surface_ids"])
|
||||
|
||||
from govoplan_audit.backend.db.models import AuditLog
|
||||
|
||||
with SessionLocal() as session:
|
||||
audit_rows = (
|
||||
session.query(AuditLog)
|
||||
.filter(
|
||||
AuditLog.action.in_(
|
||||
("policy.impact_previewed", "view_policy.updated")
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
by_action = {row.action: row for row in audit_rows}
|
||||
self.assertEqual(
|
||||
preview_payload["proposal_hash"],
|
||||
by_action["policy.impact_previewed"].details["proposal_hash"],
|
||||
)
|
||||
self.assertEqual(
|
||||
preview_payload["preview_id"],
|
||||
by_action["view_policy.updated"].details["impact_preview_id"],
|
||||
)
|
||||
|
||||
stale = self.client.put(
|
||||
"/api/v1/admin/view-policies/tenant",
|
||||
headers=headers,
|
||||
json={
|
||||
"policy": {"allow_edit": False},
|
||||
"impact_preview_id": preview_payload["preview_id"],
|
||||
"impact_proposal_hash": preview_payload["proposal_hash"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(409, stale.status_code, stale.text)
|
||||
self.assertEqual("policy_impact_preview_stale", stale.json()["detail"]["code"])
|
||||
|
||||
inherited_preview = self.client.post(
|
||||
"/api/v1/admin/policy-impact/preview",
|
||||
headers=headers,
|
||||
json={
|
||||
"policy_family": "view",
|
||||
"scope_type": "tenant",
|
||||
"proposed_policy": {},
|
||||
"populations": [
|
||||
{
|
||||
"provider_id": "views",
|
||||
"selector": {
|
||||
"include_views": False,
|
||||
"include_surfaces": True,
|
||||
},
|
||||
"limit": 10,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, inherited_preview.status_code, inherited_preview.text)
|
||||
inherited_payload = inherited_preview.json()
|
||||
removed = self.client.delete(
|
||||
"/api/v1/admin/view-policies/tenant",
|
||||
headers=headers,
|
||||
params={
|
||||
"impact_preview_id": inherited_payload["preview_id"],
|
||||
"impact_proposal_hash": inherited_payload["proposal_hash"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, removed.status_code, removed.text)
|
||||
self.assertEqual({}, removed.json()["policy"])
|
||||
|
||||
def test_campaign_scoped_mail_profile_policy_is_enforced(self) -> None:
|
||||
headers, _ = self._login()
|
||||
created = self.client.post(
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.policy import (
|
||||
CAPABILITY_POLICY_IMPACT_SUBJECT_PREFIX,
|
||||
PolicyImpactPopulationRequest,
|
||||
PolicyImpactSubject,
|
||||
PolicyImpactSubjectBatch,
|
||||
PolicyImpactSubjectProvider,
|
||||
policy_impact_subject_provider,
|
||||
)
|
||||
|
||||
|
||||
class _Provider:
|
||||
provider_id = "example"
|
||||
supported_policy_families = ("view",)
|
||||
|
||||
def collect_policy_impact_subjects(
|
||||
self,
|
||||
session: object | None = None,
|
||||
*,
|
||||
request: PolicyImpactPopulationRequest,
|
||||
) -> PolicyImpactSubjectBatch:
|
||||
del session, request
|
||||
return PolicyImpactSubjectBatch(
|
||||
provider_id=self.provider_id,
|
||||
subjects=(
|
||||
PolicyImpactSubject(
|
||||
module_id="example",
|
||||
resource_type="record",
|
||||
resource_id="record-1",
|
||||
action="view",
|
||||
),
|
||||
),
|
||||
total_available=1,
|
||||
)
|
||||
|
||||
|
||||
class _Registry:
|
||||
def __init__(self, capability: object) -> None:
|
||||
self._capability = capability
|
||||
|
||||
def has_capability(self, name: str) -> bool:
|
||||
return name == f"{CAPABILITY_POLICY_IMPACT_SUBJECT_PREFIX}example"
|
||||
|
||||
def capability(self, name: str) -> object:
|
||||
del name
|
||||
return self._capability
|
||||
|
||||
|
||||
class PolicyImpactContractTests(unittest.TestCase):
|
||||
def test_provider_contract_is_runtime_checkable_and_discoverable(self) -> None:
|
||||
provider = _Provider()
|
||||
self.assertIsInstance(provider, PolicyImpactSubjectProvider)
|
||||
self.assertIs(
|
||||
policy_impact_subject_provider(_Registry(provider), "example"),
|
||||
provider,
|
||||
)
|
||||
|
||||
def test_population_and_provider_batches_are_bounded(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "between 1 and 500"):
|
||||
PolicyImpactPopulationRequest(
|
||||
tenant_id="tenant-1",
|
||||
policy_family="view",
|
||||
limit=501,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "at most 500"):
|
||||
PolicyImpactSubjectBatch(
|
||||
provider_id="example",
|
||||
subjects=tuple(
|
||||
PolicyImpactSubject(
|
||||
module_id="example",
|
||||
resource_type="record",
|
||||
resource_id=str(index),
|
||||
action="view",
|
||||
)
|
||||
for index in range(501)
|
||||
),
|
||||
)
|
||||
|
||||
def test_unavailable_batches_explain_the_gap(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "need an explanation"):
|
||||
PolicyImpactSubjectBatch(
|
||||
provider_id="example",
|
||||
state="unavailable",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user