import unittest from dataclasses import dataclass from datetime import datetime, timezone from dns_sync.adapters import ( CERTIFICATES, DNS_RECORDS, PROXY_HOSTS, Capability, CapabilityOperation, Endpoint, NormalizedObject, ObjectReference, Observation, PlanOperation, ) from dns_sync.reconciliation import ( PlanningConflict, build_endpoint_plans, ) NOW = datetime(2026, 9, 19, 22, 30, tzinfo=timezone.utc) FIOS = "verizon://192.168.1.1" SECONDARY = "test-dns://192.168.1.2" @dataclass class FakeAdapter: endpoint: Endpoint capabilities: frozenset[Capability] def adapter( plugin: str, address: str, *apply_types: str, ) -> FakeAdapter: return FakeAdapter( endpoint=Endpoint(plugin, address), capabilities=frozenset( Capability( object_type, CapabilityOperation.APPLY, ) for object_type in apply_types ), ) def dns_object( hostname: str, ip: str, local_key: str | None = None, ) -> NormalizedObject: return NormalizedObject( object_type=DNS_RECORDS, logical_key=hostname, local_key=local_key, payload={ "hostname": hostname, "ip": ip, "record_type": "A", "wildcard": False, }, ) def observed( endpoint_id: str, object: NormalizedObject, ) -> Observation: return Observation( endpoint_id=endpoint_id, object=object, observed_at=NOW, ) class EndpointPlanningTests(unittest.TestCase): def test_builds_separate_capability_filtered_plans(self): desired = [ dns_object("add.example.com", "192.168.1.101"), dns_object("keep.example.com", "192.168.1.101"), dns_object("update.example.com", "192.168.1.101"), NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "expires_on": "2026-12-15", }, ), ] current = [ observed( FIOS, dns_object( "keep.example.com", "192.168.1.101", local_key="41", ), ), observed( FIOS, dns_object( "update.example.com", "192.168.1.50", local_key="42", ), ), observed( FIOS, dns_object( "unmanaged.example.com", "192.168.1.77", local_key="43", ), ), ] adapters = [ adapter("verizon", "192.168.1.1", DNS_RECORDS), adapter("test-dns", "192.168.1.2", DNS_RECORDS), FakeAdapter( endpoint=Endpoint("npm", "192.168.1.3:81"), capabilities=frozenset( { Capability( CERTIFICATES, CapabilityOperation.DISCOVER, ) } ), ), ] plans = build_endpoint_plans( adapters, desired, current, ) self.assertEqual( tuple(plan.endpoint_id for plan in plans), (SECONDARY, FIOS), ) secondary_actions = { action.desired.logical_key: action.operation for action in plans[0].actions } self.assertEqual( set(secondary_actions.values()), {PlanOperation.CREATE}, ) fios_actions = { action.desired.logical_key: action.operation for action in plans[1].actions } self.assertEqual( fios_actions, { "add.example.com": PlanOperation.CREATE, "keep.example.com": PlanOperation.NOOP, "update.example.com": PlanOperation.UPDATE, }, ) planned_keys = { action.desired.logical_key for plan in plans for action in plan.actions } self.assertNotIn("app.example.com", planned_keys) self.assertNotIn("unmanaged.example.com", planned_keys) def test_plan_orders_referenced_object_before_dependent(self): endpoint = Endpoint("generic", "node.example:443") generic_adapter = FakeAdapter( endpoint=endpoint, capabilities=frozenset( { Capability( CERTIFICATES, CapabilityOperation.APPLY, ), Capability( PROXY_HOSTS, CapabilityOperation.APPLY, ), } ), ) certificate = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], }, ) proxy_host = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "192.168.1.50", "forward_port": 8080, "enabled": True, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="app.example.com", ), ), ) plans = build_endpoint_plans( [generic_adapter], [ proxy_host, certificate, ], [], ) self.assertEqual(len(plans), 1) self.assertEqual( tuple( action.desired.object_type for action in plans[0].actions ), ( CERTIFICATES, PROXY_HOSTS, ), ) def test_plan_dependency_cycle_fails_before_plan_is_returned(self): endpoint = Endpoint("generic", "node.example:443") object_a = "object_a" object_b = "object_b" generic_adapter = adapter( "generic", "node.example:443", object_a, object_b, ) first = NormalizedObject( object_type=object_a, logical_key="a", payload={}, references=( ObjectReference( relation="b", object_type=object_b, logical_key="b", ), ), ) second = NormalizedObject( object_type=object_b, logical_key="b", payload={}, references=( ObjectReference( relation="a", object_type=object_a, logical_key="a", ), ), ) with self.assertRaisesRegex( RuntimeError, "dependency cycle", ): build_endpoint_plans( [generic_adapter], [first, second], [], ) def test_certificate_provider_difference_is_noop_when_portable_state_matches(self): endpoint = Endpoint("npm", "node-two:81") npm_adapter = FakeAdapter( endpoint=endpoint, capabilities=frozenset( { Capability( CERTIFICATES, CapabilityOperation.APPLY, ), } ), ) desired = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "provider": "letsencrypt", "nice_name": "source", "valid_from": "2026-09-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ) current = Observation( endpoint_id=endpoint.identity, object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="84", payload={ "domain_names": ["app.example.com"], "provider": "other", "nice_name": "imported copy", "valid_from": "2026-09-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ), observed_at=NOW, ) plans = build_endpoint_plans( [npm_adapter], [desired], [current], ) self.assertEqual( plans[0].actions[0].operation, PlanOperation.NOOP, ) def test_proxy_reference_change_is_update_even_when_payload_matches(self): endpoint = Endpoint("npm", "node-two:81") npm_adapter = FakeAdapter( endpoint=endpoint, capabilities=frozenset( { Capability( PROXY_HOSTS, CapabilityOperation.APPLY, ), } ), ) desired = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "backend.internal", "forward_port": 8080, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="new.example.com", ), ), ) observations = [ Observation( endpoint_id=endpoint.identity, object=NormalizedObject( object_type=CERTIFICATES, logical_key="old.example.com", local_key="67", payload={ "domain_names": ["old.example.com"], }, ), observed_at=NOW, ), Observation( endpoint_id=endpoint.identity, object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload=dict(desired.payload), references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key="67", ), ), ), observed_at=NOW, ), ] plans = build_endpoint_plans( [npm_adapter], [desired], observations, ) self.assertEqual( plans[0].actions[0].operation, PlanOperation.UPDATE, ) def test_planner_logicalizes_matching_local_reference_to_noop(self): endpoint = Endpoint("npm", "node-two:81") npm_adapter = FakeAdapter( endpoint=endpoint, capabilities=frozenset( { Capability( PROXY_HOSTS, CapabilityOperation.APPLY, ), } ), ) desired = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "backend.internal", "forward_port": 8080, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="app.example.com", ), ), ) observations = [ Observation( endpoint_id=endpoint.identity, object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="84", payload={ "domain_names": ["app.example.com"], }, ), observed_at=NOW, ), Observation( endpoint_id=endpoint.identity, object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload=dict(desired.payload), references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key="84", ), ), ), observed_at=NOW, ), ] plans = build_endpoint_plans( [npm_adapter], [desired], observations, ) self.assertEqual( plans[0].actions[0].operation, PlanOperation.NOOP, ) self.assertEqual( plans[0].actions[0].current.references, desired.references, ) def test_older_certificate_is_replace_and_dependent_proxy_is_update(self): endpoint = Endpoint( "generic", "node.example:443", ) generic_adapter = adapter( "generic", "node.example:443", CERTIFICATES, PROXY_HOSTS, ) desired_certificate = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "valid_from": "2026-09-20T00:00:00+00:00", "expires_on": "2026-12-20T00:00:00+00:00", }, ) desired_proxy = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "forward_scheme": "https", "forward_host": "backend.internal", "forward_port": 443, "enabled": True, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="app.example.com", ), ), ) current_certificate = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="67", payload={ "domain_names": ["app.example.com"], "valid_from": "2026-08-20T00:00:00+00:00", "expires_on": "2026-11-20T00:00:00+00:00", }, ) current_proxy = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload=dict(desired_proxy.payload), references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key="67", ), ), ) plans = build_endpoint_plans( [generic_adapter], [ desired_certificate, desired_proxy, ], [ observed( endpoint.identity, current_certificate, ), observed( endpoint.identity, current_proxy, ), ], ) self.assertEqual(len(plans), 1) actions = plans[0].actions self.assertEqual( [ ( action.operation, action.desired.object_type, ) for action in actions ], [ ( PlanOperation.REPLACE, CERTIFICATES, ), ( PlanOperation.UPDATE, PROXY_HOSTS, ), ], ) self.assertEqual( actions[1].reason, "referenced object is being replaced", ) def test_restart_duplicate_certificate_uses_desired_copy_and_rebinds_proxy(self): endpoint = Endpoint( "generic", "node.example:443", ) generic_adapter = adapter( "generic", "node.example:443", CERTIFICATES, PROXY_HOSTS, ) desired_certificate = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "provider": "letsencrypt", "valid_from": "2026-09-20T00:00:00+00:00", "expires_on": "2026-12-20T00:00:00+00:00", }, ) desired_proxy = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "forward_scheme": "https", "forward_host": "backend.internal", "forward_port": 443, "enabled": True, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="app.example.com", ), ), ) old_certificate = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="67", payload={ "domain_names": ["app.example.com"], "provider": "other", "valid_from": "2026-08-20T00:00:00+00:00", "expires_on": "2026-11-20T00:00:00+00:00", }, ) new_certificate = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="84", payload={ "domain_names": ["app.example.com"], "provider": "other", "valid_from": "2026-09-20T00:00:00+00:00", "expires_on": "2026-12-20T00:00:00+00:00", }, ) current_proxy = NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload=dict(desired_proxy.payload), references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key="67", ), ), ) plans = build_endpoint_plans( [generic_adapter], [ desired_certificate, desired_proxy, ], [ observed( endpoint.identity, old_certificate, ), observed( endpoint.identity, new_certificate, ), observed( endpoint.identity, current_proxy, ), ], ) self.assertEqual(len(plans), 1) plan = plans[0] by_type = { action.desired.object_type: action for action in plan.actions } self.assertEqual( by_type[CERTIFICATES].operation, PlanOperation.NOOP, ) self.assertEqual( by_type[CERTIFICATES].current.local_key, "84", ) self.assertEqual( by_type[PROXY_HOSTS].operation, PlanOperation.UPDATE, ) self.assertEqual( by_type[PROXY_HOSTS].reason, "referenced local object is being retired", ) self.assertEqual( tuple( retirement.object.local_key for retirement in plan.retirements ), ("67",), ) def test_equivalent_duplicate_preserves_letsencrypt_copy(self): endpoint = Endpoint( "generic", "node.example:443", ) generic_adapter = adapter( "generic", "node.example:443", CERTIFICATES, ) desired = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "valid_from": "2026-09-20T00:00:00+00:00", "expires_on": "2026-12-20T00:00:00+00:00", }, ) imported = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="90", payload={ "domain_names": ["app.example.com"], "provider": "other", "valid_from": "2026-09-20T00:00:00+00:00", "expires_on": "2026-12-20T00:00:00+00:00", }, ) native = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="12", payload={ "domain_names": ["app.example.com"], "provider": "letsencrypt", "valid_from": "2026-09-20T00:00:00+00:00", "expires_on": "2026-12-20T00:00:00+00:00", }, ) plans = build_endpoint_plans( [generic_adapter], [desired], [ observed(endpoint.identity, imported), observed(endpoint.identity, native), ], ) action = plans[0].actions[0] self.assertEqual( action.operation, PlanOperation.NOOP, ) self.assertEqual( action.current.local_key, "12", ) self.assertEqual( tuple( retirement.object.local_key for retirement in plans[0].retirements ), ("90",), ) def test_duplicate_endpoint_objects_fail_without_payload_output(self): desired = [ dns_object("app.example.com", "192.168.1.101") ] current = [ observed( FIOS, dns_object( "app.example.com", "192.168.1.50", local_key="41", ), ), observed( FIOS, dns_object( "app.example.com", "203.0.113.77", local_key="42", ), ), ] with self.assertRaises(PlanningConflict) as raised: build_endpoint_plans( [adapter("verizon", "192.168.1.1", DNS_RECORDS)], desired, current, ) message = str(raised.exception) self.assertIn(FIOS, message) self.assertIn("app.example.com", message) self.assertNotIn("192.168.1.50", message) self.assertNotIn("203.0.113.77", message) def test_duplicate_desired_identity_is_rejected(self): duplicate = dns_object( "app.example.com", "192.168.1.101", ) with self.assertRaisesRegex( RuntimeError, "Duplicate desired object identity", ): build_endpoint_plans( [adapter("verizon", "192.168.1.1", DNS_RECORDS)], [duplicate, duplicate], [], ) if __name__ == "__main__": unittest.main()