using ZB.MOM.WW.Overview.Polling; namespace ZB.MOM.WW.Overview.Tests; /// /// The store is the cache-first seam: a page reads it and never probes. These tests pin that it /// always has something to serve and that subscribers hear about replacements. /// public class OverviewSnapshotStoreTests { private static OverviewSnapshot SnapshotAt(int second) => new(new DateTimeOffset(2026, 7, 24, 12, 0, second, TimeSpan.Zero), []); [Fact] public void Current_BeforeAnyPublish_IsEmptyNotNull() { // A page can render before the poller's first sweep finishes; a null here would be a // NullReferenceException on the landing page of a monitoring dashboard. var store = new OverviewSnapshotStore(); Assert.NotNull(store.Current); Assert.Empty(store.Current.Applications); } [Fact] public void Publish_ReplacesCurrent() { var store = new OverviewSnapshotStore(); var snapshot = SnapshotAt(1); store.Publish(snapshot); Assert.Same(snapshot, store.Current); } [Fact] public void Publish_NotifiesSubscribersWithTheNewSnapshot() { var store = new OverviewSnapshotStore(); OverviewSnapshot? received = null; store.Changed += s => received = s; var snapshot = SnapshotAt(2); store.Publish(snapshot); Assert.Same(snapshot, received); } [Fact] public void Publish_AfterUnsubscribe_DoesNotNotify() { // Blazor circuits come and go; a handler that outlives its component would call // StateHasChanged on a disposed renderer on every sweep. var store = new OverviewSnapshotStore(); var calls = 0; void Handler(OverviewSnapshot _) => calls++; store.Changed += Handler; store.Publish(SnapshotAt(1)); store.Changed -= Handler; store.Publish(SnapshotAt(2)); Assert.Equal(1, calls); } [Fact] public void Publish_Null_Throws() { Assert.Throws(() => new OverviewSnapshotStore().Publish(null!)); } [Fact] public async Task Current_ReadDuringPublish_ObservesOneWholeSnapshot() { // The tearing guarantee: a reader always sees a complete graph, never a half-swapped one. var store = new OverviewSnapshotStore(); var a = SnapshotAt(1); var b = SnapshotAt(2); store.Publish(a); using var stop = new CancellationTokenSource(); var reader = Task.Run(() => { while (!stop.IsCancellationRequested) { var seen = store.Current; Assert.True(ReferenceEquals(seen, a) || ReferenceEquals(seen, b)); } }); for (var i = 0; i < 1_000; i++) store.Publish(i % 2 == 0 ? b : a); await stop.CancelAsync(); await reader; } }