using System; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Host.Domain; using ZB.MOM.WW.OtOpcUa.Host.GalaxyRepository; using ZB.MOM.WW.OtOpcUa.Host.Metrics; using ZB.MOM.WW.OtOpcUa.Host.Status; using ZB.MOM.WW.OtOpcUa.Tests.Helpers; namespace ZB.MOM.WW.OtOpcUa.Tests.Status { /// /// Verifies the HTML, JSON, and health snapshots generated for the operator status dashboard. /// public class StatusReportServiceTests { /// /// Confirms that the generated HTML contains every dashboard panel expected by operators. /// [Fact] public void GenerateHtml_ContainsAllPanels() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("Connection"); html.ShouldContain("Health"); html.ShouldContain("Subscriptions"); html.ShouldContain("Galaxy Info"); html.ShouldContain("Operations"); } /// /// Confirms that the generated HTML includes the configured auto-refresh meta tag. /// [Fact] public void GenerateHtml_ContainsMetaRefresh() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("meta http-equiv='refresh' content='10'"); } /// /// Confirms that the connection panel renders the current runtime connection state. /// [Fact] public void GenerateHtml_ConnectionPanel_ShowsState() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("Connected"); } /// /// Confirms that the Galaxy panel renders the bridged Galaxy name. /// [Fact] public void GenerateHtml_GalaxyPanel_ShowsName() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("TestGalaxy"); } /// /// Confirms that the operations table renders the expected performance metric headers. /// [Fact] public void GenerateHtml_OperationsTable_ShowsHeaders() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("Count"); html.ShouldContain("Success Rate"); html.ShouldContain("Avg (ms)"); html.ShouldContain("Min (ms)"); html.ShouldContain("Max (ms)"); html.ShouldContain("P95 (ms)"); } /// /// The dashboard title shows the service version inline so operators can identify the deployed /// build without scrolling, and the standalone footer panel is gone. /// [Fact] public void GenerateHtml_Title_ShowsVersion_NoFooter() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("

LmxOpcUa Status Dashboard"); html.ShouldContain("class='version'"); html.ShouldNotContain("

Footer

"); html.ShouldNotContain("Generated:"); } /// /// Confirms that the generated JSON includes the major dashboard sections. /// [Fact] public void GenerateJson_Deserializes() { var sut = CreateService(); var json = sut.GenerateJson(); json.ShouldNotBeNullOrWhiteSpace(); json.ShouldContain("Connection"); json.ShouldContain("Health"); json.ShouldContain("Subscriptions"); json.ShouldContain("Galaxy"); json.ShouldContain("Operations"); json.ShouldContain("Historian"); json.ShouldContain("Alarms"); json.ShouldContain("Footer"); } /// /// The dashboard JSON exposes the historian plugin status so operators can distinguish /// "disabled by config" from "plugin crashed on load." /// [Fact] public void GenerateJson_Historian_IncludesPluginStatus() { var sut = CreateService(); var json = sut.GenerateJson(); json.ShouldContain("PluginStatus"); json.ShouldContain("PluginPath"); } /// /// The dashboard JSON exposes alarm counters so operators can see transition/ack activity. /// [Fact] public void GenerateJson_Alarms_IncludesCounters() { var sut = CreateService(); var json = sut.GenerateJson(); json.ShouldContain("TrackingEnabled"); json.ShouldContain("TransitionCount"); json.ShouldContain("AckWriteFailures"); } /// /// The Historian and Alarms panels render in the HTML dashboard. /// [Fact] public void GenerateHtml_IncludesHistorianAndAlarmPanels() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("

Historian

"); html.ShouldContain("

Alarms

"); } /// /// The Endpoints panel renders in the HTML dashboard even when no server host has been set, /// so operators can tell the OPC UA server has not started. /// [Fact] public void GenerateHtml_IncludesEndpointsPanel() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("

Endpoints

"); html.ShouldContain("OPC UA server not started"); } /// /// The dashboard JSON surfaces the alarm filter counters so monitoring clients can verify scope. /// [Fact] public void GenerateJson_Alarms_IncludesFilterCounters() { var sut = CreateService(); var json = sut.GenerateJson(); json.ShouldContain("FilterEnabled"); json.ShouldContain("FilterPatternCount"); json.ShouldContain("FilterIncludedObjectCount"); json.ShouldContain("FilterPatterns"); } /// /// With no filter configured, the Alarms panel renders an explicit "disabled" line so operators /// know all alarm-bearing objects are being tracked. /// [Fact] public void GenerateHtml_AlarmsPanel_FilterDisabled_ShowsDisabledLine() { var sut = CreateService(); var html = sut.GenerateHtml(); html.ShouldContain("Filter: disabled"); } /// /// The dashboard JSON surfaces the Endpoints section with base-address and security-profile slots /// so monitoring clients can read them programmatically. /// [Fact] public void GenerateJson_Endpoints_IncludesBaseAddressesAndSecurityProfiles() { var sut = CreateService(); var json = sut.GenerateJson(); json.ShouldContain("Endpoints"); json.ShouldContain("BaseAddresses"); json.ShouldContain("SecurityProfiles"); json.ShouldContain("UserTokenPolicies"); } /// /// The /api/health payload exposes Historian and Alarms component status. /// [Fact] public void GetHealthData_Components_IncludeHistorianAndAlarms() { var sut = CreateService(); var data = sut.GetHealthData(); data.Components.Historian.ShouldNotBeNullOrEmpty(); data.Components.Alarms.ShouldNotBeNullOrEmpty(); } /// /// Confirms that the report service reports healthy when the runtime connection is up. /// [Fact] public void IsHealthy_WhenConnected_ReturnsTrue() { var sut = CreateService(); sut.IsHealthy().ShouldBe(true); } /// /// Confirms that the report service reports unhealthy when the runtime connection is down. /// [Fact] public void IsHealthy_WhenDisconnected_ReturnsFalse() { var mxClient = new FakeMxAccessClient { State = ConnectionState.Disconnected }; var sut = new StatusReportService(new HealthCheckService(), 10); sut.SetComponents(mxClient, null, null, null); sut.IsHealthy().ShouldBe(false); } [Fact] public void GetHealthData_WhenConnected_ReturnsHealthyStatus() { var sut = CreateService(); var data = sut.GetHealthData(); data.Status.ShouldBe("Healthy"); data.Components.MxAccess.ShouldBe("Connected"); data.Components.Database.ShouldBe("Connected"); } [Fact] public void GetHealthData_WhenDisconnected_ReturnsUnhealthyStatus() { var mxClient = new FakeMxAccessClient { State = ConnectionState.Disconnected }; var galaxyStats = new GalaxyRepositoryStats { DbConnected = false }; var sut = new StatusReportService(new HealthCheckService(), 10); sut.SetComponents(mxClient, null, galaxyStats, null); var data = sut.GetHealthData(); data.Status.ShouldBe("Unhealthy"); data.ServiceLevel.ShouldBe((byte)0); data.Components.MxAccess.ShouldBe("Disconnected"); data.Components.Database.ShouldBe("Disconnected"); } [Fact] public void GetHealthData_NoRedundancy_ServiceLevel255WhenHealthy() { var sut = CreateService(); var data = sut.GetHealthData(); data.RedundancyEnabled.ShouldBe(false); data.ServiceLevel.ShouldBe((byte)255); data.RedundancyRole.ShouldBeNull(); data.RedundancyMode.ShouldBeNull(); } [Fact] public void GetHealthData_WithRedundancy_IncludesRoleAndServiceLevel() { var sut = CreateServiceWithRedundancy("Primary"); var data = sut.GetHealthData(); data.RedundancyEnabled.ShouldBe(true); data.RedundancyRole.ShouldBe("Primary"); data.RedundancyMode.ShouldBe("Warm"); data.ServiceLevel.ShouldBe((byte)200); } [Fact] public void GetHealthData_SecondaryRole_LowerServiceLevel() { var sut = CreateServiceWithRedundancy("Secondary"); var data = sut.GetHealthData(); data.ServiceLevel.ShouldBe((byte)150); } [Fact] public void GetHealthData_ContainsUptime() { var sut = CreateService(); var data = sut.GetHealthData(); data.Uptime.ShouldNotBeNullOrWhiteSpace(); } [Fact] public void GetHealthData_ContainsTimestamp() { var sut = CreateService(); var data = sut.GetHealthData(); data.Timestamp.ShouldBeGreaterThan(DateTime.UtcNow.AddMinutes(-1)); } [Fact] public void GenerateHealthJson_ContainsExpectedFields() { var sut = CreateService(); var json = sut.GenerateHealthJson(); json.ShouldContain("Status"); json.ShouldContain("ServiceLevel"); json.ShouldContain("Components"); json.ShouldContain("MxAccess"); json.ShouldContain("Database"); json.ShouldContain("OpcUaServer"); json.ShouldContain("Uptime"); } [Fact] public void GenerateHealthHtml_ContainsStatusBadge() { var sut = CreateService(); var html = sut.GenerateHealthHtml(); html.ShouldContain("HEALTHY"); html.ShouldContain("SERVICE LEVEL"); html.ShouldContain("255"); } [Fact] public void GenerateHealthHtml_ContainsComponentCards() { var sut = CreateService(); var html = sut.GenerateHealthHtml(); html.ShouldContain("MXAccess"); html.ShouldContain("Galaxy Database"); html.ShouldContain("OPC UA Server"); } [Fact] public void GenerateHealthHtml_WithRedundancy_ShowsRoleAndMode() { var sut = CreateServiceWithRedundancy("Primary"); var html = sut.GenerateHealthHtml(); html.ShouldContain("Primary"); html.ShouldContain("Warm"); } [Fact] public void GenerateHealthHtml_ContainsAutoRefresh() { var sut = CreateService(); var html = sut.GenerateHealthHtml(); html.ShouldContain("meta http-equiv='refresh' content='10'"); } /// /// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data. /// /// A configured status report service for dashboard assertions. private static StatusReportService CreateService() { var mxClient = new FakeMxAccessClient(); using var metrics = new PerformanceMetrics(); metrics.RecordOperation("Read", TimeSpan.FromMilliseconds(10)); metrics.RecordOperation("Write", TimeSpan.FromMilliseconds(20)); var galaxyStats = new GalaxyRepositoryStats { GalaxyName = "TestGalaxy", DbConnected = true, LastDeployTime = new DateTime(2024, 6, 1), ObjectCount = 42, AttributeCount = 200, LastRebuildTime = DateTime.UtcNow }; var sut = new StatusReportService(new HealthCheckService(), 10); sut.SetComponents(mxClient, metrics, galaxyStats, null); return sut; } private static StatusReportService CreateServiceWithRedundancy(string role) { var mxClient = new FakeMxAccessClient(); var galaxyStats = new GalaxyRepositoryStats { GalaxyName = "TestGalaxy", DbConnected = true }; var redundancyConfig = new RedundancyConfiguration { Enabled = true, Mode = "Warm", Role = role, ServiceLevelBase = 200 }; var sut = new StatusReportService(new HealthCheckService(), 10); sut.SetComponents(mxClient, null, galaxyStats, null, null, redundancyConfig, "urn:test:instance1"); return sut; } } }