using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Diagnostics.HealthChecks; using Xunit; using ZB.MOM.WW.LmxProxy.Host.Domain; using ZB.MOM.WW.LmxProxy.Host.Health; using HealthCheckService = ZB.MOM.WW.LmxProxy.Host.Health.HealthCheckService; using ZB.MOM.WW.LmxProxy.Host.Metrics; using ZB.MOM.WW.LmxProxy.Host.Subscriptions; namespace ZB.MOM.WW.LmxProxy.Host.Tests.Health { public class HealthCheckServiceTests { private class FakeScadaClient : IScadaClient { public bool IsConnected { get; set; } = true; public ConnectionState ConnectionState { get; set; } = ConnectionState.Connected; public DateTime ConnectedSince => DateTime.UtcNow; public int ReconnectCount => 0; public event EventHandler? ConnectionStateChanged; public Task ConnectAsync(CancellationToken ct = default) => Task.CompletedTask; public Task DisconnectAsync(CancellationToken ct = default) => Task.CompletedTask; public Task ReadAsync(string address, CancellationToken ct = default) => Task.FromResult(Vtq.Good(42.0)); public Task> ReadBatchAsync(IEnumerable addresses, CancellationToken ct = default) => Task.FromResult>(new Dictionary()); public Task WriteAsync(string address, object value, CancellationToken ct = default) => Task.CompletedTask; public Task WriteBatchAsync(IReadOnlyDictionary values, CancellationToken ct = default) => Task.CompletedTask; public Task<(bool flagReached, int elapsedMs)> WriteBatchAndWaitAsync( IReadOnlyDictionary values, string flagTag, object flagValue, int timeoutMs, int pollIntervalMs, CancellationToken ct = default) => Task.FromResult((false, 0)); public Task UnsubscribeByAddressAsync(IEnumerable addresses) => Task.CompletedTask; public Task SubscribeAsync(IEnumerable addresses, Action callback, CancellationToken ct = default) => Task.FromResult(new FakeHandle()); public ValueTask DisposeAsync() => default; internal void FireEvent() => ConnectionStateChanged?.Invoke(this, null!); private class FakeHandle : IAsyncDisposable { public ValueTask DisposeAsync() => default; } } [Fact] public async Task ReturnsHealthy_WhenConnectedAndNormalMetrics() { var client = new FakeScadaClient { IsConnected = true, ConnectionState = ConnectionState.Connected }; using var sm = new SubscriptionManager(client); using var pm = new PerformanceMetrics(); pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true); var svc = new HealthCheckService(client, sm, pm); var result = await svc.CheckHealthAsync(new HealthCheckContext()); result.Status.Should().Be(HealthStatus.Healthy); } [Fact] public async Task ReturnsUnhealthy_WhenNotConnected() { var client = new FakeScadaClient { IsConnected = false, ConnectionState = ConnectionState.Disconnected }; using var sm = new SubscriptionManager(client); using var pm = new PerformanceMetrics(); var svc = new HealthCheckService(client, sm, pm); var result = await svc.CheckHealthAsync(new HealthCheckContext()); result.Status.Should().Be(HealthStatus.Unhealthy); result.Description.Should().Contain("not connected"); } [Fact] public async Task ReturnsDegraded_WhenSuccessRateBelow50Percent() { var client = new FakeScadaClient { IsConnected = true }; using var sm = new SubscriptionManager(client); using var pm = new PerformanceMetrics(); // Record 200 operations with 40% success rate for (int i = 0; i < 80; i++) pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true); for (int i = 0; i < 120; i++) pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false); var svc = new HealthCheckService(client, sm, pm); var result = await svc.CheckHealthAsync(new HealthCheckContext()); result.Status.Should().Be(HealthStatus.Degraded); result.Description.Should().Contain("success rate"); } [Fact] public async Task ReturnsDegraded_WhenClientCountOver100() { var client = new FakeScadaClient { IsConnected = true }; using var sm = new SubscriptionManager(client); using var pm = new PerformanceMetrics(); // Create 101 subscriptions to exceed the threshold for (int i = 0; i < 101; i++) { using var cts = new CancellationTokenSource(); await sm.SubscribeAsync("client-" + i, new[] { "tag1" }, cts.Token); } var svc = new HealthCheckService(client, sm, pm); var result = await svc.CheckHealthAsync(new HealthCheckContext()); result.Status.Should().Be(HealthStatus.Degraded); result.Description.Should().Contain("client count"); } [Fact] public async Task DoesNotFlagLowSuccessRate_Under100Operations() { var client = new FakeScadaClient { IsConnected = true }; using var sm = new SubscriptionManager(client); using var pm = new PerformanceMetrics(); // Record 50 operations with 0% success rate (under 100 threshold) for (int i = 0; i < 50; i++) pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false); var svc = new HealthCheckService(client, sm, pm); var result = await svc.CheckHealthAsync(new HealthCheckContext()); result.Status.Should().Be(HealthStatus.Healthy); } } }