using GatewayKeepalive.Core; namespace GatewayKeepalive.Tests; internal static class JsonStoreTests { public static async Task AtomicReplacementAsync() { using var temp = new TempDirectory(); var nested = Path.Combine(temp.DirectoryPath, "nested"); var store = new JsonStore(nested); var first = new SwitchJournal( Fixtures.Baseline(), Fixtures.Baseline() with { Dns = new DnsConfiguration(false, ["192.168.6.2"], []) }, "Prepared", new DateTimeOffset(2026, 2, 3, 4, 5, 6, TimeSpan.Zero)); const string file = "switch-journal.json"; store.Write(file, first); var read = store.Read(file); Check.True(read is not null, "A written journal should be readable"); Check.Equal(first.Stage, read!.Stage, "String data should round trip"); Check.Equal(first.StartedAt, read.StartedAt, "DateTimeOffset should round trip"); Check.True(NetworkComparison.SameConfiguration(first.Before, read.Before), "Nested before snapshot should round trip"); Check.True(NetworkComparison.SameConfiguration(first.Intended, read.Intended), "Nested intended snapshot should round trip"); Check.Equal(0, Directory.GetFiles(nested, "*.tmp").Length, "Successful write must clean its temporary file"); var second = first with { Stage = "Verified" }; store.Write(file, second); Check.Equal("Verified", store.Read(file)?.Stage, "The target should contain the replacement"); Check.Equal("Prepared", store.Read(file + ".bak")?.Stage, "The previous complete file should be retained as a backup"); Check.Equal(0, Directory.GetFiles(nested, "*.tmp").Length, "Replacement must clean its temporary file"); var beforeFailure = File.ReadAllText(Path.Combine(nested, file)); var backupBeforeFailure = File.ReadAllText(Path.Combine(nested, file + ".bak")); var failure = await Check.ThrowsAnyAsync( () => Task.Run(() => store.Write(file, new UnserializablePayload())), "A serialization failure should be observable"); Check.Contains("intentional serialization fault", failure.ToString(), "The expected serialization fault should surface"); Check.Equal(beforeFailure, File.ReadAllText(Path.Combine(nested, file)), "A failed write must leave the target byte-for-byte intact"); Check.Equal(backupBeforeFailure, File.ReadAllText(Path.Combine(nested, file + ".bak")), "A failed write must leave the backup intact"); Check.Equal(0, Directory.GetFiles(nested, "*.tmp").Length, "A failed write must delete its temporary file"); var empty = Path.Combine(nested, "empty.json"); File.WriteAllText(empty, string.Empty); Check.Throws(() => store.Read("empty.json"), "Malformed JSON must be rejected"); } private sealed class UnserializablePayload { public string Value => throw new InvalidOperationException("intentional serialization fault"); } }