import sqlite3 import tempfile import unittest from datetime import datetime, timezone from pathlib import Path from dns_sync.adapters import CERTIFICATES, NormalizedObject from dns_sync.persistence import SQLiteStateStore ENDPOINT = "npm://192.168.1.3:81" T1 = datetime(2026, 9, 19, 21, 0, tzinfo=timezone.utc) T2 = datetime(2026, 9, 19, 21, 5, tzinfo=timezone.utc) T3 = datetime(2026, 9, 19, 21, 10, tzinfo=timezone.utc) def certificate( expires_on: str, local_key: str | None = None, ) -> NormalizedObject: return NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", local_key=local_key, payload={ "domain_names": ["app.example.com"], "expires_on": expires_on, }, ) class DesiredStatePersistenceTests(unittest.TestCase): def test_snapshot_round_trips_and_can_be_cleared(self): desired = certificate("2026-12-15T00:00:00+00:00") with SQLiteStateStore(":memory:") as store: store.replace_desired_state( CERTIFICATES, [desired], T1, ) loaded = store.load_desired_state(CERTIFICATES) store.replace_desired_state( CERTIFICATES, [], T2, ) cleared = store.load_desired_state(CERTIFICATES) self.assertEqual(len(loaded), 1) self.assertEqual(loaded[0].object, desired) self.assertEqual(loaded[0].selected_at, T1) self.assertEqual(cleared, []) def test_endpoint_local_identity_is_rejected(self): with SQLiteStateStore(":memory:") as store: with self.assertRaisesRegex( RuntimeError, "endpoint-local key", ): store.replace_desired_state( CERTIFICATES, [certificate("new", local_key="67")], T1, ) def test_sensitive_payload_is_rejected_before_snapshot_changes(self): safe = certificate("safe") unsafe = NormalizedObject( object_type=CERTIFICATES, logical_key="app.example.com", payload={ "domain_names": ["app.example.com"], "nested": { "private_key": "must-never-persist", }, }, ) with SQLiteStateStore(":memory:") as store: store.replace_desired_state( CERTIFICATES, [safe], T1, ) with self.assertRaisesRegex( RuntimeError, "sensitive field", ): store.replace_desired_state( CERTIFICATES, [unsafe], T2, ) loaded = store.load_desired_state(CERTIFICATES) self.assertEqual(len(loaded), 1) self.assertEqual(loaded[0].object, safe) self.assertEqual(loaded[0].selected_at, T1) class AppliedStatePersistenceTests(unittest.TestCase): def test_apply_requires_successful_verification(self): first = certificate("first", local_key="91") second = certificate("second", local_key="91") with SQLiteStateStore(":memory:") as store: store.record_applied(ENDPOINT, first, T1) before = store.load_applied_state( ENDPOINT, CERTIFICATES, )[0] store.record_verification_success( ENDPOINT, first, T2, ) verified = store.load_applied_state( ENDPOINT, CERTIFICATES, )[0] store.record_applied(ENDPOINT, second, T3) reapplied = store.load_applied_state( ENDPOINT, CERTIFICATES, )[0] self.assertFalse(before.is_verified) self.assertIsNone(before.verified_at) self.assertTrue(verified.is_verified) self.assertEqual(verified.verified_at, T2) self.assertEqual(verified.verification_attempted_at, T2) self.assertEqual( reapplied.object.payload["expires_on"], "second", ) self.assertEqual(reapplied.applied_at, T3) self.assertFalse(reapplied.is_verified) self.assertIsNone(reapplied.verification_attempted_at) self.assertIsNone(reapplied.verified_at) def test_stale_payload_cannot_verify_current_applied_state(self): old = certificate("old", local_key="91") current = certificate("current", local_key="91") with SQLiteStateStore(":memory:") as store: store.record_applied(ENDPOINT, current, T1) with self.assertRaisesRegex( RuntimeError, "does not match", ): store.record_verification_success( ENDPOINT, old, T2, ) state = store.load_applied_state( ENDPOINT, CERTIFICATES, )[0] self.assertFalse(state.is_verified) self.assertIsNone(state.verification_attempted_at) self.assertIsNone(state.verified_at) def test_failed_verification_is_not_reported_as_verified(self): applied = certificate("current", local_key="91") with SQLiteStateStore(":memory:") as store: store.record_applied(ENDPOINT, applied, T1) store.record_verification_success( ENDPOINT, applied, T2, ) store.record_verification_failure( ENDPOINT, applied, "target no longer matches", T3, ) state = store.load_applied_state( ENDPOINT, CERTIFICATES, )[0] self.assertFalse(state.is_verified) self.assertEqual(state.verified_at, T2) self.assertEqual(state.verification_attempted_at, T3) self.assertEqual( state.verification_error, "target no longer matches", ) class SchemaMigrationTests(unittest.TestCase): def test_version_one_database_is_migrated_to_version_two(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "state.sqlite3" connection = sqlite3.connect(path) connection.execute( "CREATE TABLE schema_meta (version INTEGER NOT NULL)" ) connection.execute( "INSERT INTO schema_meta(version) VALUES (1)" ) connection.commit() connection.close() with SQLiteStateStore(path) as store: version = store.connection.execute( "SELECT version FROM schema_meta" ).fetchone()[0] tables = { row[0] for row in store.connection.execute( """ SELECT name FROM sqlite_master WHERE type = 'table' """ ) } self.assertEqual(version, 2) self.assertIn("desired_state", tables) self.assertIn("applied_state", tables) if __name__ == "__main__": unittest.main()