import unittest from datetime import datetime, timezone from dns_sync.adapters import ( CERTIFICATES, DNS_RECORDS, PROXY_HOSTS, NormalizedObject, ObjectReference, Observation, ) from dns_sync.reconciliation import ( ReconciliationConflict, group_observations, logicalize_observation_references, reconcile_observations, ) NOW = datetime(2026, 9, 19, 22, 0, tzinfo=timezone.utc) def dns_observation( endpoint_id: str, hostname: str, ip: str, local_key: str, ) -> Observation: return Observation( endpoint_id=endpoint_id, object=NormalizedObject( object_type=DNS_RECORDS, logical_key=hostname, local_key=local_key, payload={ "hostname": hostname, "ip": ip, "record_type": "A", "wildcard": False, }, ), observed_at=NOW, ) class ObservationGroupingTests(unittest.TestCase): def test_groups_by_type_and_logical_identity(self): observations = [ dns_observation( "npm://192.168.1.4:81", "app.example.com", "192.168.1.101", "22", ), dns_observation( "npm://192.168.1.3:81", "app.example.com", "192.168.1.101", "12", ), dns_observation( "npm://192.168.1.3:81", "other.example.com", "192.168.1.101", "13", ), ] grouped = group_observations(observations) self.assertEqual( tuple(grouped), ( (DNS_RECORDS, "app.example.com"), (DNS_RECORDS, "other.example.com"), ), ) self.assertEqual( tuple( item.endpoint_id for item in grouped[ (DNS_RECORDS, "app.example.com") ] ), ( "npm://192.168.1.3:81", "npm://192.168.1.4:81", ), ) class DnsUnionPolicyTests(unittest.TestCase): def test_union_keeps_every_unique_hostname(self): observations = [ dns_observation( "npm://192.168.1.3:81", "one.example.com", "192.168.1.101", "12", ), dns_observation( "npm://192.168.1.4:81", "two.example.com", "192.168.1.101", "22", ), ] desired = reconcile_observations(observations) self.assertEqual( tuple(item.logical_key for item in desired), ("one.example.com", "two.example.com"), ) self.assertTrue( all(item.local_key is None for item in desired) ) def test_identical_observations_converge_to_one_object(self): observations = [ dns_observation( "npm://192.168.1.3:81", "app.example.com", "192.168.1.101", "12", ), dns_observation( "npm://192.168.1.4:81", "app.example.com", "192.168.1.101", "22", ), ] desired = reconcile_observations(observations) self.assertEqual(len(desired), 1) self.assertEqual( desired[0].payload["ip"], "192.168.1.101", ) self.assertIsNone(desired[0].local_key) def test_conflicting_values_fail_without_exposing_payload(self): observations = [ dns_observation( "npm://node-one:81", "app.example.com", "192.168.1.101", "12", ), dns_observation( "npm://node-two:81", "app.example.com", "203.0.113.77", "22", ), ] with self.assertRaises(ReconciliationConflict) as raised: reconcile_observations(observations) message = str(raised.exception) self.assertIn("app.example.com", message) self.assertIn("npm://node-one:81", message) self.assertIn("npm://node-two:81", message) self.assertNotIn("192.168.1.101", message) self.assertNotIn("203.0.113.77", message) def test_missing_policy_fails_explicitly(self): observation = Observation( endpoint_id="generic://node-one", object=NormalizedObject( object_type="unknown_objects", logical_key="app.example.com", local_key="67", payload={}, ), observed_at=NOW, ) with self.assertRaisesRegex( RuntimeError, "No reconciliation policy", ): reconcile_observations([observation]) def test_local_certificate_references_become_logical_references(self): observations = [ Observation( endpoint_id="npm://node-one:81", object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="67", payload={ "domain_names": ["app.example.com"], "valid_from": "2026-09-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ), observed_at=NOW, ), Observation( endpoint_id="npm://node-one:81", object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "backend.internal", "forward_port": 8080, "enabled": True, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key="67", ), ), ), observed_at=NOW, ), ] logicalized = logicalize_observation_references( observations ) proxy = next( row.object for row in logicalized if row.object.object_type == PROXY_HOSTS ) self.assertEqual( proxy.references, ( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="app.example.com", ), ), ) def test_unresolved_local_reference_fails_closed(self): observation = Observation( endpoint_id="npm://node-one:81", object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "backend.internal", "forward_port": 8080, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key="999", ), ), ), observed_at=NOW, ) with self.assertRaisesRegex( ReconciliationConflict, "Unresolved endpoint-local reference", ): reconcile_observations([observation]) def test_certificate_policy_selects_newest_expiry(self): observations = [ Observation( endpoint_id="npm://node-one:81", object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="10", payload={ "domain_names": ["app.example.com"], "provider": "other", "nice_name": "older", "valid_from": "2026-06-01T00:00:00+00:00", "expires_on": "2026-09-01T00:00:00+00:00", }, ), observed_at=NOW, ), Observation( endpoint_id="npm://node-two:81", object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="44", payload={ "domain_names": ["app.example.com"], "provider": "letsencrypt", "nice_name": "newer", "valid_from": "2026-09-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ), observed_at=NOW, ), ] desired = reconcile_observations(observations) self.assertEqual(len(desired), 1) self.assertEqual( desired[0].payload["expires_on"], "2026-12-01T00:00:00+00:00", ) self.assertEqual( desired[0].payload["provider"], "letsencrypt", ) self.assertIsNone(desired[0].local_key) def test_certificate_same_expiry_conflict_fails_closed(self): observations = [ Observation( endpoint_id="npm://node-one:81", object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="10", payload={ "domain_names": ["app.example.com"], "valid_from": "2026-08-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ), observed_at=NOW, ), Observation( endpoint_id="npm://node-two:81", object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key="44", payload={ "domain_names": ["app.example.com"], "valid_from": "2026-09-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ), observed_at=NOW, ), ] with self.assertRaisesRegex( ReconciliationConflict, "newest expiry", ): reconcile_observations(observations) def test_proxy_consensus_ignores_different_endpoint_local_certificate_ids(self): observations = [] for endpoint_id, certificate_id, proxy_id in ( ("npm://node-one:81", "67", "12"), ("npm://node-two:81", "84", "31"), ): observations.extend( [ Observation( endpoint_id=endpoint_id, object=NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key=certificate_id, payload={ "domain_names": ["app.example.com"], "valid_from": "2026-09-01T00:00:00+00:00", "expires_on": "2026-12-01T00:00:00+00:00", }, ), observed_at=NOW, ), Observation( endpoint_id=endpoint_id, object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key=proxy_id, payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "backend.internal", "forward_port": 8080, "enabled": True, }, references=( ObjectReference( relation="certificate", object_type=CERTIFICATES, local_key=certificate_id, ), ), ), observed_at=NOW, ), ] ) desired = reconcile_observations(observations) proxy = next( object for object in desired if object.object_type == PROXY_HOSTS ) self.assertEqual( proxy.references, ( ObjectReference( relation="certificate", object_type=CERTIFICATES, logical_key="app.example.com", ), ), ) def test_proxy_consensus_rejects_backend_drift_without_payload_output(self): observations = [ Observation( endpoint_id="npm://node-one:81", object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="12", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "one.internal", "forward_port": 8080, }, ), observed_at=NOW, ), Observation( endpoint_id="npm://node-two:81", object=NormalizedObject( object_type=PROXY_HOSTS, logical_key="app.example.com", local_key="31", payload={ "domain_names": ["app.example.com"], "forward_scheme": "http", "forward_host": "two.internal", "forward_port": 8080, }, ), observed_at=NOW, ), ] with self.assertRaises(ReconciliationConflict) as raised: reconcile_observations(observations) message = str(raised.exception) self.assertIn("app.example.com", message) self.assertIn("npm://node-one:81", message) self.assertIn("npm://node-two:81", message) self.assertNotIn("one.internal", message) self.assertNotIn("two.internal", message) def test_explicit_empty_policy_registry_uses_no_defaults(self): observation = dns_observation( "npm://node-one:81", "app.example.com", "192.168.1.101", "12", ) with self.assertRaisesRegex(RuntimeError, "No reconciliation policy"): reconcile_observations([observation], policies={}) if __name__ == "__main__": unittest.main()