import json import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] CRM_PACK = REPO_ROOT / "salesforce-crm-standard" EXPECTED_POLICY_OBJECTS = { "RecordType", "IdentitySyncSetting", "IdentitySyncDeactivationTicket", "FlowDefinition", "Flow", "ProcessDefinition", "ProcessNode", "ProcessAction", } PUBLIC_WRITE_SURFACES = [ "singleCreate", "singleUpdate", "singleDelete", "externalIDUpsert", "collectionCreate", "collectionUpdate", "collectionDelete", "collectionUpsert", "treeCreate", ] def load_json(path: Path): with path.open(encoding="utf-8") as handle: return json.load(handle) class RecordMutationPolicyContractsTest(unittest.TestCase): def test_record_mutation_policies_are_locked_pack_components(self): manifest = load_json(CRM_PACK / "pack.json") components = [ component for component in manifest.get("components", []) if component.get("kind") == "object_record_mutation_policy" ] by_key = {component["businessKey"]: component for component in components} self.assertEqual(set(by_key), EXPECTED_POLICY_OBJECTS) for object_api_name, component in by_key.items(): with self.subTest(object=object_api_name): self.assertEqual(component["manageability"], "locked") self.assertNotIn("payload", component) path = CRM_PACK / component["path"] self.assertTrue(path.exists(), path) payload = load_json(path) self.assertEqual(payload["objectApiName"], object_api_name) self.assertIn("recordMutation", payload) self.assertGreater( len(payload["recordMutation"].get("deny", [])), 0, ) if object_api_name in { "IdentitySyncSetting", "IdentitySyncDeactivationTicket", }: self.assertEqual( component.get("dependsOn"), [ { "kind": "object_spec", "businessKey": "standard_objects_spec:crm", } ], ) def test_record_mutation_policies_do_not_live_in_object_specs(self): specs = load_json(CRM_PACK / "objects" / "standard_objects_spec.json") offenders = [ spec["apiName"] for spec in specs if spec.get("accessPolicy", {}).get("recordMutation") ] self.assertEqual(offenders, []) def test_process_definition_policy_closes_direct_metadata_and_legacy_alert_activation(self): payload = load_json( CRM_PACK / "policies" / "process_definition_record_mutation_denials.json" ) denials = payload["recordMutation"]["deny"] self.assertTrue( any( "singleCreate" in denial.get("surfaces", []) and "field" not in denial for denial in denials ), "ProcessDefinition direct singleCreate must stay closed; child materialization is pack/deploy owned", ) self.assertTrue( any( denial.get("surfaces") == ["singleUpdate"] and denial.get("field") == "Metadata" and denial.get("when", {}).get("fieldPresent", {}).get("field") == "Metadata" for denial in denials ), "ProcessDefinition.Metadata direct update must be closed by metadata-owned policy", ) activation_denials = [ denial for denial in denials if denial.get("surfaces") == ["singleUpdate"] and denial.get("field") in {"State", "IsActive"} ] self.assertEqual(len(activation_denials), 2) for denial in activation_denials: guarded = [ condition["fieldPresentEquals"] for condition in denial.get("when", {}).get("allOf", []) if "fieldPresentEquals" in condition ] self.assertEqual(len(guarded), 1) self.assertEqual(guarded[0]["field"], denial["field"]) related = [ condition["relatedExists"] for condition in denial.get("when", {}).get("allOf", []) if "relatedExists" in condition ] self.assertEqual(len(related), 1) self.assertEqual(related[0]["objectApiName"], "ProcessAction") self.assertEqual(related[0]["parentField"], "ProcessDefinitionId") self.assertEqual( related[0]["match"], [ { "field": "ActionType", "value": "Alert", "caseInsensitive": True, } ], ) def test_process_definition_policy_is_scoped_to_public_write_surfaces(self): payload = load_json( CRM_PACK / "policies" / "process_definition_record_mutation_denials.json" ) denials = payload["recordMutation"]["deny"] public_write_surfaces = { "singleCreate", "singleUpdate", "singleDelete", "externalIDUpsert", "collectionCreate", "collectionUpdate", "collectionDelete", "collectionUpsert", "treeCreate", } for denial in denials: surfaces = denial.get("surfaces", []) self.assertGreater( len(surfaces), 0, "ProcessDefinition deny rules must stay surface-scoped so deploy/source-backed authoring is not globally closed", ) self.assertLessEqual( set(surfaces), public_write_surfaces, f"unexpected non-public write surface in ProcessDefinition policy: {surfaces}", ) self.assertNotIn("metadataDeploy", surfaces) self.assertNotIn("sourceBackedApprovalProcess", surfaces) def test_approval_child_policies_close_every_public_write_surface(self): for object_api_name, filename in { "IdentitySyncSetting": "identity_sync_setting_record_mutation_denials.json", "IdentitySyncDeactivationTicket": "identity_sync_deactivation_ticket_record_mutation_denials.json", "ProcessNode": "process_node_record_mutation_denials.json", "ProcessAction": "process_action_record_mutation_denials.json", }.items(): with self.subTest(object=object_api_name): payload = load_json(CRM_PACK / "policies" / filename) denials = payload["recordMutation"]["deny"] self.assertEqual(len(denials), 1) self.assertEqual(denials[0]["surfaces"], PUBLIC_WRITE_SURFACES) self.assertNotIn("field", denials[0]) self.assertNotIn("when", denials[0]) def test_flow_policies_close_public_writes_without_fake_field_scope(self): for object_api_name, filename in { "Flow": "flow_record_mutation_denials.json", "FlowDefinition": "flow_definition_record_mutation_denials.json", }.items(): with self.subTest(object=object_api_name): payload = load_json(CRM_PACK / "policies" / filename) denials = payload["recordMutation"]["deny"] self.assertEqual(len(denials), 1) self.assertEqual( denials[0]["surfaces"], [ "singleCreate", "singleUpdate", "externalIDUpsert", "collectionCreate", "collectionUpdate", "collectionUpsert", "treeCreate", ], ) self.assertNotIn( "field", denials[0], f"{object_api_name} write closure is object-level; do not invent a same-name field", ) if __name__ == "__main__": unittest.main()