using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.LocalDb.Replication; using ZB.MOM.WW.OtOpcUa.Host.Health; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// LocalDb Phase 1 (Task 10) — the replication health check. /// /// /// The load-bearing case is the default-OFF one: a plain driver node with no peer and no /// listener must report Healthy, or every unreplicated node in the fleet degrades the /// moment this check ships. Beyond that, "connected" is not sufficient for healthy — an unknown /// backlog (a failed oplog poll) must not read as zero. /// public sealed class LocalDbReplicationHealthCheckTests { // ---- Pure decision core (table-tested) -------------------------------------------------- [Fact] public void Unconfigured_IsHealthy_SoDefaultOffNodesDoNotDegrade() { LocalDbReplicationDecision.Evaluate( peerConfigured: false, connected: false, oplogBacklog: null, degradedThreshold: 100_000) .Status.ShouldBe(HealthStatus.Healthy); } [Fact] public void ConfiguredButNotConnected_IsDegraded() { LocalDbReplicationDecision.Evaluate( peerConfigured: true, connected: false, oplogBacklog: null, degradedThreshold: 100_000) .Status.ShouldBe(HealthStatus.Degraded); } [Fact] public void ConnectedButBacklogUnknown_IsDegraded() { // null backlog = the oplog poll failed. Reporting Healthy here would call a pair that cannot // read its own oplog perfectly fine. LocalDbReplicationDecision.Evaluate( peerConfigured: true, connected: true, oplogBacklog: null, degradedThreshold: 100_000) .Status.ShouldBe(HealthStatus.Degraded); } [Fact] public void ConnectedWithSmallBacklog_IsHealthy() { LocalDbReplicationDecision.Evaluate( peerConfigured: true, connected: true, oplogBacklog: 12, degradedThreshold: 100_000) .Status.ShouldBe(HealthStatus.Healthy); } [Fact] public void ConnectedWithBacklogOverThreshold_IsDegraded() { // A backlog past the threshold means the peer is falling behind faster than it drains — // connected, but not keeping up. LocalDbReplicationDecision.Evaluate( peerConfigured: true, connected: true, oplogBacklog: 100_001, degradedThreshold: 100_000) .Status.ShouldBe(HealthStatus.Degraded); } // ---- IHealthCheck wrapper --------------------------------------------------------------- [Fact] public async Task Wrapper_WithNoSyncStatusRegistered_IsHealthy() { // Admin-only graphs never register the replication engine. The check is registered // unconditionally (AddOtOpcUaHealth takes no role argument), so it must no-op to Healthy // rather than throw or degrade when LocalDb is simply absent. var check = new LocalDbReplicationHealthCheck( syncStatus: null, options: Options.Create(new ReplicationOptions()), syncListenPort: 0); var result = await check.CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken); result.Status.ShouldBe(HealthStatus.Healthy); } [Fact] public async Task Wrapper_ListenerConfiguredButNotConnected_IsDegraded() { // A passive node (no PeerAddress) still counts as "replication configured" when it is // listening — otherwise a passive node that never gets dialled would look healthy while // silently accepting nothing. var check = new LocalDbReplicationHealthCheck( syncStatus: new StubSyncStatus { Connected = false }, options: Options.Create(new ReplicationOptions { PeerAddress = "" }), syncListenPort: 9001); var result = await check.CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken); result.Status.ShouldBe(HealthStatus.Degraded); } [Fact] public async Task Wrapper_PeerConfiguredAndConnectedAndDraining_IsHealthy() { var check = new LocalDbReplicationHealthCheck( syncStatus: new StubSyncStatus { Connected = true, OplogBacklog = 3 }, options: Options.Create(new ReplicationOptions { PeerAddress = "https://peer:9001" }), syncListenPort: 9001); var result = await check.CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken); result.Status.ShouldBe(HealthStatus.Healthy); } private sealed class StubSyncStatus : ISyncStatus { public bool Connected { get; init; } public string? PeerNodeId { get; init; } public DateTimeOffset? LastSyncUtc { get; init; } public long? OplogBacklog { get; init; } public long ConnectionAttempts { get; init; } } }