using ScadaLink.Commons.Types.Enums; namespace ScadaLink.HealthMonitoring.Tests; public class SiteHealthCollectorTests { private readonly SiteHealthCollector _collector = new(); [Fact] public void CollectReport_ReturnsZeroCounters_WhenNoErrorsRecorded() { var report = _collector.CollectReport("site-1"); Assert.Equal("site-1", report.SiteId); Assert.Equal(0, report.ScriptErrorCount); Assert.Equal(0, report.AlarmEvaluationErrorCount); Assert.Equal(0, report.DeadLetterCount); } [Fact] public void IncrementScriptError_AccumulatesBetweenReports() { _collector.IncrementScriptError(); _collector.IncrementScriptError(); _collector.IncrementScriptError(); var report = _collector.CollectReport("site-1"); Assert.Equal(3, report.ScriptErrorCount); } [Fact] public void IncrementAlarmError_AccumulatesBetweenReports() { _collector.IncrementAlarmError(); _collector.IncrementAlarmError(); var report = _collector.CollectReport("site-1"); Assert.Equal(2, report.AlarmEvaluationErrorCount); } [Fact] public void IncrementDeadLetter_AccumulatesBetweenReports() { _collector.IncrementDeadLetter(); var report = _collector.CollectReport("site-1"); Assert.Equal(1, report.DeadLetterCount); } [Fact] public void CollectReport_ResetsCounters_AfterCollection() { _collector.IncrementScriptError(); _collector.IncrementAlarmError(); _collector.IncrementDeadLetter(); var first = _collector.CollectReport("site-1"); Assert.Equal(1, first.ScriptErrorCount); Assert.Equal(1, first.AlarmEvaluationErrorCount); Assert.Equal(1, first.DeadLetterCount); var second = _collector.CollectReport("site-1"); Assert.Equal(0, second.ScriptErrorCount); Assert.Equal(0, second.AlarmEvaluationErrorCount); Assert.Equal(0, second.DeadLetterCount); } [Fact] public void UpdateConnectionHealth_ReflectedInReport() { _collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected); _collector.UpdateConnectionHealth("opc-2", ConnectionHealth.Disconnected); var report = _collector.CollectReport("site-1"); Assert.Equal(2, report.DataConnectionStatuses.Count); Assert.Equal(ConnectionHealth.Connected, report.DataConnectionStatuses["opc-1"]); Assert.Equal(ConnectionHealth.Disconnected, report.DataConnectionStatuses["opc-2"]); } [Fact] public void ConnectionHealth_NotResetAfterCollect() { _collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected); _collector.CollectReport("site-1"); var second = _collector.CollectReport("site-1"); Assert.Single(second.DataConnectionStatuses); Assert.Equal(ConnectionHealth.Connected, second.DataConnectionStatuses["opc-1"]); } [Fact] public void RemoveConnection_RemovesFromReport() { _collector.UpdateConnectionHealth("opc-1", ConnectionHealth.Connected); _collector.UpdateTagResolution("opc-1", 10, 8); _collector.RemoveConnection("opc-1"); var report = _collector.CollectReport("site-1"); Assert.Empty(report.DataConnectionStatuses); Assert.Empty(report.TagResolutionCounts); } [Fact] public void UpdateTagResolution_ReflectedInReport() { _collector.UpdateTagResolution("opc-1", 50, 45); var report = _collector.CollectReport("site-1"); Assert.Single(report.TagResolutionCounts); Assert.Equal(50, report.TagResolutionCounts["opc-1"].TotalSubscribed); Assert.Equal(45, report.TagResolutionCounts["opc-1"].SuccessfullyResolved); } [Fact] public void StoreAndForwardBufferDepths_IsEmptyPlaceholder() { var report = _collector.CollectReport("site-1"); Assert.Empty(report.StoreAndForwardBufferDepths); } [Fact] public void CollectReport_IncludesUtcTimestamp() { var before = DateTimeOffset.UtcNow; var report = _collector.CollectReport("site-1"); var after = DateTimeOffset.UtcNow; Assert.InRange(report.ReportTimestamp, before, after); } /// /// HealthMonitoring-016 regression: /// must stamp ReportTimestamp from an injected /// (consistent with the rest of the module), not directly from /// DateTimeOffset.UtcNow, so the report timestamp is deterministically /// testable against a known instant. /// [Fact] public void CollectReport_StampsTimestamp_FromInjectedTimeProvider() { var fixedInstant = new DateTimeOffset(2026, 5, 17, 9, 30, 0, TimeSpan.Zero); var collector = new SiteHealthCollector(new TestTimeProvider(fixedInstant)); var report = collector.CollectReport("site-1"); Assert.Equal(fixedInstant, report.ReportTimestamp); } [Fact] public void CollectReport_SequenceNumberIsZero_CallerAssignsIt() { var report = _collector.CollectReport("site-1"); Assert.Equal(0, report.SequenceNumber); } // HealthMonitoring-009 regression: the remaining collector setters had no // "reflected in report" coverage. The following tests verify each setter's // value reaches CollectReport output. [Fact] public void SetClusterNodes_ReflectedInReport() { var nodes = new List { new("node-a", true, "Active"), new("node-b", true, "Standby") }; _collector.SetClusterNodes(nodes); var report = _collector.CollectReport("site-1"); Assert.NotNull(report.ClusterNodes); Assert.Equal(2, report.ClusterNodes!.Count); Assert.Equal("node-a", report.ClusterNodes[0].Hostname); } [Fact] public void SetInstanceCounts_ReflectedInReport() { _collector.SetInstanceCounts(deployed: 10, enabled: 7, disabled: 3); var report = _collector.CollectReport("site-1"); Assert.Equal(10, report.DeployedInstanceCount); Assert.Equal(7, report.EnabledInstanceCount); Assert.Equal(3, report.DisabledInstanceCount); } [Fact] public void SetParkedMessageCount_ReflectedInReport() { _collector.SetParkedMessageCount(42); var report = _collector.CollectReport("site-1"); Assert.Equal(42, report.ParkedMessageCount); } [Fact] public void SetNodeHostname_ReflectedInReport() { _collector.SetNodeHostname("site-host-1"); var report = _collector.CollectReport("site-1"); Assert.Equal("site-host-1", report.NodeHostname); } [Fact] public void SetActiveNode_ReflectedInNodeRole() { _collector.SetActiveNode(true); Assert.Equal("Active", _collector.CollectReport("site-1").NodeRole); Assert.True(_collector.IsActiveNode); _collector.SetActiveNode(false); Assert.Equal("Standby", _collector.CollectReport("site-1").NodeRole); Assert.False(_collector.IsActiveNode); } [Fact] public void UpdateTagQuality_ReflectedInReport() { _collector.UpdateTagQuality("opc-1", good: 80, bad: 15, uncertain: 5); var report = _collector.CollectReport("site-1"); Assert.NotNull(report.DataConnectionTagQuality); var quality = report.DataConnectionTagQuality!["opc-1"]; Assert.Equal(80, quality.Good); Assert.Equal(15, quality.Bad); Assert.Equal(5, quality.Uncertain); } [Fact] public void UpdateConnectionEndpoint_ReflectedInReport() { _collector.UpdateConnectionEndpoint("opc-1", "opc.tcp://plc-1:4840"); var report = _collector.CollectReport("site-1"); Assert.NotNull(report.DataConnectionEndpoints); Assert.Equal("opc.tcp://plc-1:4840", report.DataConnectionEndpoints!["opc-1"]); } [Fact] public void SetStoreAndForwardDepths_ReflectedInReport() { _collector.SetStoreAndForwardDepths(new Dictionary { ["ExternalSystem"] = 5, ["Notification"] = 2 }); var report = _collector.CollectReport("site-1"); Assert.Equal(5, report.StoreAndForwardBufferDepths["ExternalSystem"]); Assert.Equal(2, report.StoreAndForwardBufferDepths["Notification"]); } [Fact] public async Task ThreadSafety_ConcurrentIncrements() { const int iterations = 10_000; var tasks = new[] { Task.Run(() => { for (int i = 0; i < iterations; i++) _collector.IncrementScriptError(); }), Task.Run(() => { for (int i = 0; i < iterations; i++) _collector.IncrementAlarmError(); }), Task.Run(() => { for (int i = 0; i < iterations; i++) _collector.IncrementDeadLetter(); }) }; await Task.WhenAll(tasks); var report = _collector.CollectReport("site-1"); Assert.Equal(iterations, report.ScriptErrorCount); Assert.Equal(iterations, report.AlarmEvaluationErrorCount); Assert.Equal(iterations, report.DeadLetterCount); } }