using System; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Host.Domain; using ZB.MOM.WW.OtOpcUa.Host.Metrics; using ZB.MOM.WW.OtOpcUa.Host.Status; namespace ZB.MOM.WW.OtOpcUa.Tests.Status { /// /// Verifies how the dashboard health service classifies bridge health from connection state and metrics. /// public class HealthCheckServiceTests { private readonly HealthCheckService _sut = new(); /// /// Confirms that a disconnected runtime is reported as unhealthy. /// [Fact] public void NotConnected_ReturnsUnhealthy() { var result = _sut.CheckHealth(ConnectionState.Disconnected, null); result.Status.ShouldBe("Unhealthy"); result.Color.ShouldBe("red"); result.Message.ShouldContain("not connected"); } /// /// Confirms that a connected runtime with no metrics history is still considered healthy. /// [Fact] public void Connected_NoMetrics_ReturnsHealthy() { var result = _sut.CheckHealth(ConnectionState.Connected, null); result.Status.ShouldBe("Healthy"); result.Color.ShouldBe("green"); } /// /// Confirms that good success-rate metrics keep the service in a healthy state. /// [Fact] public void Connected_GoodMetrics_ReturnsHealthy() { using var metrics = new PerformanceMetrics(); for (var i = 0; i < 200; i++) metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10)); var result = _sut.CheckHealth(ConnectionState.Connected, metrics); result.Status.ShouldBe("Healthy"); } /// /// Confirms that poor operation success rates degrade the reported health state. /// [Fact] public void Connected_LowSuccessRate_ReturnsDegraded() { using var metrics = new PerformanceMetrics(); for (var i = 0; i < 40; i++) metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10)); for (var i = 0; i < 80; i++) metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false); var result = _sut.CheckHealth(ConnectionState.Connected, metrics); result.Status.ShouldBe("Degraded"); result.Color.ShouldBe("yellow"); } /// /// Confirms that the boolean health helper reports true when the runtime is connected. /// [Fact] public void IsHealthy_Connected_ReturnsTrue() { _sut.IsHealthy(ConnectionState.Connected, null).ShouldBe(true); } /// /// Confirms that the boolean health helper reports false when the runtime is disconnected. /// [Fact] public void IsHealthy_Disconnected_ReturnsFalse() { _sut.IsHealthy(ConnectionState.Disconnected, null).ShouldBe(false); } /// /// Confirms that the error connection state is treated as unhealthy. /// [Fact] public void Error_ReturnsUnhealthy() { var result = _sut.CheckHealth(ConnectionState.Error, null); result.Status.ShouldBe("Unhealthy"); } /// /// Confirms that the reconnecting state is treated as unhealthy while recovery is in progress. /// [Fact] public void Reconnecting_ReturnsUnhealthy() { var result = _sut.CheckHealth(ConnectionState.Reconnecting, null); result.Status.ShouldBe("Unhealthy"); } /// /// Historian enabled but plugin failed to load → Degraded with the plugin error in the message. /// [Fact] public void HistorianEnabled_PluginLoadFailed_ReturnsDegraded() { var historian = new HistorianStatusInfo { Enabled = true, PluginStatus = "LoadFailed", PluginError = "aahClientManaged.dll could not be loaded" }; var result = _sut.CheckHealth(ConnectionState.Connected, null, historian); result.Status.ShouldBe("Degraded"); result.Color.ShouldBe("yellow"); result.Message.ShouldContain("LoadFailed"); result.Message.ShouldContain("aahClientManaged.dll"); } /// /// Historian disabled is healthy regardless of plugin status string. /// [Fact] public void HistorianDisabled_ReturnsHealthy() { var historian = new HistorianStatusInfo { Enabled = false, PluginStatus = "Disabled" }; _sut.CheckHealth(ConnectionState.Connected, null, historian).Status.ShouldBe("Healthy"); } /// /// Historian enabled and plugin loaded is healthy. /// [Fact] public void HistorianEnabled_PluginLoaded_ReturnsHealthy() { var historian = new HistorianStatusInfo { Enabled = true, PluginStatus = "Loaded" }; _sut.CheckHealth(ConnectionState.Connected, null, historian).Status.ShouldBe("Healthy"); } /// /// HistoryRead operations degrade after only 11 samples with <50% success rate /// (lower threshold than the regular 100-sample rule). /// [Fact] public void HistoryReadLowSuccessRate_WithLowSampleCount_ReturnsDegraded() { using var metrics = new PerformanceMetrics(); for (var i = 0; i < 4; i++) metrics.RecordOperation("HistoryReadRaw", TimeSpan.FromMilliseconds(10)); for (var i = 0; i < 8; i++) metrics.RecordOperation("HistoryReadRaw", TimeSpan.FromMilliseconds(10), false); var result = _sut.CheckHealth(ConnectionState.Connected, metrics); result.Status.ShouldBe("Degraded"); result.Message.ShouldContain("HistoryReadRaw"); } /// /// A HistoryRead sample under the 10-sample threshold does not degrade the service. /// [Fact] public void HistoryReadLowSuccessRate_BelowThreshold_ReturnsHealthy() { using var metrics = new PerformanceMetrics(); for (var i = 0; i < 5; i++) metrics.RecordOperation("HistoryReadRaw", TimeSpan.FromMilliseconds(10), false); _sut.CheckHealth(ConnectionState.Connected, metrics).Status.ShouldBe("Healthy"); } /// /// Alarm acknowledge write failures are latched — any non-zero count degrades the service. /// [Fact] public void AlarmAckWriteFailures_AnyCount_ReturnsDegraded() { var alarms = new AlarmStatusInfo { TrackingEnabled = true, AckWriteFailures = 1 }; var result = _sut.CheckHealth(ConnectionState.Connected, null, null, alarms); result.Status.ShouldBe("Degraded"); result.Message.ShouldContain("Alarm acknowledge"); } /// /// Alarm tracking disabled ignores any failure count. /// [Fact] public void AlarmAckWriteFailures_TrackingDisabled_ReturnsHealthy() { var alarms = new AlarmStatusInfo { TrackingEnabled = false, AckWriteFailures = 99 }; _sut.CheckHealth(ConnectionState.Connected, null, null, alarms).Status.ShouldBe("Healthy"); } } }