using Microsoft.Extensions.Logging.Abstractions; namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests; /// /// Deferred-work register row 80 — must /// distinguish "replication is not configured on this node" (null, no reading) from /// "replication is configured and the peer session is down" (false, a genuine fault). /// /// /// The replication engine is registered unconditionally on every site node and sits inert /// without a peer, so ISyncStatus answers Connected=false / OplogBacklog=0 /// on a site that was never meant to replicate. Passing that straight through made rig /// site-b/site-c publish "disconnected, empty backlog" — a healthy unreplicated site reading /// as a broken replicated one, and central's null branches (conditional KPI emission, /// sustained-badge anchor exclusion, per register row 33) never receiving the null they exist /// for. The gate is supplied by the Host from SiteLocalDbSetup.ReplicationIsConfigured, /// mirroring SiteEventLogReplicationCheck. /// public class LocalDbReplicationStatusReporterTests { private static LocalDbReplicationStatusReporter Build( SiteHealthCollector collector, bool replicationConfigured, bool connected = false, long? backlog = 0L) => new( connectedProvider: () => connected, oplogBacklogProvider: () => backlog, replicationConfiguredProvider: () => replicationConfigured, collector: collector, logger: NullLogger.Instance, refreshInterval: TimeSpan.FromHours(1)); [Fact] public void Probe_ReplicationUnconfigured_PublishesNullForBothFields() { // The row-80 regression. The inert engine's false/0 must NOT reach the report. var collector = new SiteHealthCollector(); using var reporter = Build(collector, replicationConfigured: false); reporter.Probe(); var report = collector.CollectReport("TestSite"); Assert.Null(report.LocalDbReplicationConnected); Assert.Null(report.LocalDbOplogBacklog); } [Fact] public void Probe_ReplicationUnconfigured_DoesNotReadTheSyncStatusProviders() { // The gate short-circuits rather than reading and discarding: an unconfigured node // should not be polling a replication engine it never uses. Also guards against a // future provider that throws when replication is off. var connectedReads = 0; var backlogReads = 0; var collector = new SiteHealthCollector(); using var reporter = new LocalDbReplicationStatusReporter( connectedProvider: () => { connectedReads++; return false; }, oplogBacklogProvider: () => { backlogReads++; return 0L; }, replicationConfiguredProvider: () => false, collector: collector, logger: NullLogger.Instance, refreshInterval: TimeSpan.FromHours(1)); reporter.Probe(); Assert.Equal(0, connectedReads); Assert.Equal(0, backlogReads); } [Fact] public void Probe_ReplicationConfiguredButDisconnected_PublishesFalse_NotNull() { // The other half of the contract, and the reason the fix is a gate rather than a // blanket "never report false": on a node that IS configured to replicate, a down // peer session is a real fault and must stay visible as false. var collector = new SiteHealthCollector(); using var reporter = Build( collector, replicationConfigured: true, connected: false, backlog: 4200L); reporter.Probe(); var report = collector.CollectReport("TestSite"); Assert.False(report.LocalDbReplicationConnected); Assert.Equal(4200L, report.LocalDbOplogBacklog); } [Fact] public void Probe_ReplicationConfiguredAndConnected_PublishesTrue() { var collector = new SiteHealthCollector(); using var reporter = Build( collector, replicationConfigured: true, connected: true, backlog: 0L); reporter.Probe(); var report = collector.CollectReport("TestSite"); Assert.True(report.LocalDbReplicationConnected); Assert.Equal(0L, report.LocalDbOplogBacklog); } [Fact] public void Probe_ReplicationConfigured_PreservesANullBacklogAsUnknown() { // A failed oplog poll on a REPLICATED node is "unknown", never 0 — rendering it as a // healthy empty backlog would report a pair that cannot read its own oplog as fine. // Connected stays false (the session really is down); only the backlog is unknown. var collector = new SiteHealthCollector(); using var reporter = Build( collector, replicationConfigured: true, connected: false, backlog: null); reporter.Probe(); var report = collector.CollectReport("TestSite"); Assert.False(report.LocalDbReplicationConnected); Assert.Null(report.LocalDbOplogBacklog); } [Fact] public void Probe_ProviderThrows_LeavesThePreviousSnapshotIntact() { // Health reporting must never take the node down, and a failed probe must not // silently downgrade a good reading to null. var shouldThrow = false; var collector = new SiteHealthCollector(); using var reporter = new LocalDbReplicationStatusReporter( connectedProvider: () => shouldThrow ? throw new InvalidOperationException("sync status unavailable") : true, oplogBacklogProvider: () => 7L, replicationConfiguredProvider: () => true, collector: collector, logger: NullLogger.Instance, refreshInterval: TimeSpan.FromHours(1)); reporter.Probe(); shouldThrow = true; reporter.Probe(); var report = collector.CollectReport("TestSite"); Assert.True(report.LocalDbReplicationConnected); Assert.Equal(7L, report.LocalDbOplogBacklog); } [Fact] public void Probe_GateFlippedOn_StopsReportingNull() { // Turning replication on for a previously-unreplicated site is a both-nodes-together // restart in practice, but the gate is read per probe, so pin that it is not latched. var configured = false; var collector = new SiteHealthCollector(); using var reporter = new LocalDbReplicationStatusReporter( connectedProvider: () => true, oplogBacklogProvider: () => 12L, replicationConfiguredProvider: () => configured, collector: collector, logger: NullLogger.Instance, refreshInterval: TimeSpan.FromHours(1)); reporter.Probe(); Assert.Null(collector.CollectReport("TestSite").LocalDbReplicationConnected); configured = true; reporter.Probe(); var report = collector.CollectReport("TestSite"); Assert.True(report.LocalDbReplicationConnected); Assert.Equal(12L, report.LocalDbOplogBacklog); } [Fact] public void Constructor_NullReplicationConfiguredProvider_Throws() { Assert.Throws(() => new LocalDbReplicationStatusReporter( connectedProvider: () => false, oplogBacklogProvider: () => 0L, replicationConfiguredProvider: null!, collector: new SiteHealthCollector(), logger: NullLogger.Instance)); } }