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));
}
}