from __future__ import annotations import unittest from govoplan_policy.backend.retention import ( PrivacyRetentionPolicy, PrivacyRetentionPolicyPatch, PrivacyPolicyError, _parent_privacy_policy_scope_id, _privacy_policy_patch_payload, _required_privacy_policy_scope_id, ) from govoplan_policy.backend.hierarchy import ( PolicyRestrictionRule, simulate_hierarchical_policy_change, validate_hierarchical_policy_patch, ) class PolicyHierarchyTests(unittest.TestCase): def test_privacy_policy_parent_scope_ids_follow_scope_precedence(self) -> None: self.assertEqual( "tenant-1", _parent_privacy_policy_scope_id(tenant_id="tenant-1", scope_type="tenant", scope_id=None), ) self.assertIsNone(_parent_privacy_policy_scope_id(tenant_id="tenant-1", scope_type="user", scope_id=None)) self.assertIsNone(_parent_privacy_policy_scope_id(tenant_id="tenant-1", scope_type="group", scope_id=None)) self.assertEqual( "campaign-1", _parent_privacy_policy_scope_id(tenant_id="tenant-1", scope_type="campaign", scope_id="campaign-1"), ) def test_privacy_policy_patch_payload_validates_and_removes_unset_fields(self) -> None: payload = _privacy_policy_patch_payload({"audit_detail_level": "minimal", "generated_eml_retention_days": None}) self.assertEqual({"audit_detail_level": "minimal"}, payload) def test_privacy_policy_models_extend_shared_schema_contract(self) -> None: policy = PrivacyRetentionPolicy.model_validate({"generated_eml_retention_days": ""}) patch = PrivacyRetentionPolicyPatch.model_validate({"allow_lower_level_limits": {"audit_detail_level": False}}) self.assertIsNone(policy.generated_eml_retention_days) self.assertEqual({"audit_detail_level": False}, patch.allow_lower_level_limits) def test_required_privacy_policy_scope_id_reports_missing_scope(self) -> None: with self.assertRaises(PrivacyPolicyError) as captured: _required_privacy_policy_scope_id("group", None) self.assertEqual("Group privacy policy requires scope_id", str(captured.exception)) def test_parent_locks_block_lower_level_field_changes_and_limit_reenable(self) -> None: issues = validate_hierarchical_policy_patch( parent_policy={"retention_days": 30}, patch={"retention_days": 20, "allow_lower_level_limits": {"retention_days": True}}, field_keys=("retention_days",), parent_allow_lower_level_limits={"retention_days": False}, ) self.assertEqual(["field_locked_by_parent", "lower_level_limit_locked_by_parent"], [issue.code for issue in issues]) self.assertEqual(["retention_days", "retention_days"], [issue.field for issue in issues]) def test_restriction_rules_block_less_restrictive_patch(self) -> None: issues = validate_hierarchical_policy_patch( parent_policy={"retention_days": 30}, patch={"retention_days": 45}, field_keys=("retention_days",), parent_allow_lower_level_limits={"retention_days": True}, restriction_rules=( PolicyRestrictionRule( field="retention_days", is_more_restrictive_or_equal=lambda parent, requested: int(requested) <= int(parent), less_restrictive_message="retention_days cannot be widened", ), ), ) self.assertEqual(1, len(issues)) self.assertEqual("less_restrictive_than_parent", issues[0].code) self.assertEqual("retention_days cannot be widened", issues[0].message) def test_policy_simulation_returns_decision_payload(self) -> None: simulation = simulate_hierarchical_policy_change( parent_policy={"retention_days": 30}, current_policy={"retention_days": 30}, patch={"retention_days": 45}, field_keys=("retention_days",), parent_allow_lower_level_limits={"retention_days": True}, restriction_rules=( PolicyRestrictionRule( field="retention_days", is_more_restrictive_or_equal=lambda parent, requested: int(requested) <= int(parent), less_restrictive_message="retention_days cannot be widened", ), ), ) self.assertFalse(simulation.allowed) self.assertEqual(("retention_days",), simulation.changed_fields) self.assertIsNotNone(simulation.decision) self.assertEqual(("retention_days",), simulation.decision.requirements) self.assertEqual("Requested policy change is blocked by parent policy constraints.", simulation.decision.reason) if __name__ == "__main__": unittest.main()