Wraps the 4 HistoryRead overrides and OnAlarmAcknowledge with PerformanceMetrics.BeginOperation, adds alarm counters to LmxNodeManager, publishes a structured HistorianPluginOutcome from HistorianPluginLoader, and extends HealthCheckService with plugin-load, history-read, and alarm-ack-failure degradation rules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
367 lines
12 KiB
C#
367 lines
12 KiB
C#
using System;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Status;
|
|
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
|
|
|
namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
|
{
|
|
/// <summary>
|
|
/// Verifies the HTML, JSON, and health snapshots generated for the operator status dashboard.
|
|
/// </summary>
|
|
public class StatusReportServiceTests
|
|
{
|
|
/// <summary>
|
|
/// Confirms that the generated HTML contains every dashboard panel expected by operators.
|
|
/// </summary>
|
|
[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");
|
|
html.ShouldContain("Footer");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the generated HTML includes the configured auto-refresh meta tag.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateHtml_ContainsMetaRefresh()
|
|
{
|
|
var sut = CreateService();
|
|
var html = sut.GenerateHtml();
|
|
html.ShouldContain("meta http-equiv='refresh' content='10'");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the connection panel renders the current runtime connection state.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateHtml_ConnectionPanel_ShowsState()
|
|
{
|
|
var sut = CreateService();
|
|
var html = sut.GenerateHtml();
|
|
html.ShouldContain("Connected");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the Galaxy panel renders the bridged Galaxy name.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateHtml_GalaxyPanel_ShowsName()
|
|
{
|
|
var sut = CreateService();
|
|
var html = sut.GenerateHtml();
|
|
html.ShouldContain("TestGalaxy");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the operations table renders the expected performance metric headers.
|
|
/// </summary>
|
|
[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)");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the footer renders timestamp and version information.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateHtml_Footer_ContainsTimestampAndVersion()
|
|
{
|
|
var sut = CreateService();
|
|
var html = sut.GenerateHtml();
|
|
html.ShouldContain("Generated:");
|
|
html.ShouldContain("Version:");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the generated JSON includes the major dashboard sections.
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The dashboard JSON exposes the historian plugin status so operators can distinguish
|
|
/// "disabled by config" from "plugin crashed on load."
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateJson_Historian_IncludesPluginStatus()
|
|
{
|
|
var sut = CreateService();
|
|
var json = sut.GenerateJson();
|
|
|
|
json.ShouldContain("PluginStatus");
|
|
json.ShouldContain("PluginPath");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The dashboard JSON exposes alarm counters so operators can see transition/ack activity.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateJson_Alarms_IncludesCounters()
|
|
{
|
|
var sut = CreateService();
|
|
var json = sut.GenerateJson();
|
|
|
|
json.ShouldContain("TrackingEnabled");
|
|
json.ShouldContain("TransitionCount");
|
|
json.ShouldContain("AckWriteFailures");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Historian and Alarms panels render in the HTML dashboard.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GenerateHtml_IncludesHistorianAndAlarmPanels()
|
|
{
|
|
var sut = CreateService();
|
|
var html = sut.GenerateHtml();
|
|
|
|
html.ShouldContain("<h2>Historian</h2>");
|
|
html.ShouldContain("<h2>Alarms</h2>");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The /api/health payload exposes Historian and Alarms component status.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetHealthData_Components_IncludeHistorianAndAlarms()
|
|
{
|
|
var sut = CreateService();
|
|
var data = sut.GetHealthData();
|
|
|
|
data.Components.Historian.ShouldNotBeNullOrEmpty();
|
|
data.Components.Alarms.ShouldNotBeNullOrEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the report service reports healthy when the runtime connection is up.
|
|
/// </summary>
|
|
[Fact]
|
|
public void IsHealthy_WhenConnected_ReturnsTrue()
|
|
{
|
|
var sut = CreateService();
|
|
sut.IsHealthy().ShouldBe(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the report service reports unhealthy when the runtime connection is down.
|
|
/// </summary>
|
|
[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'");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data.
|
|
/// </summary>
|
|
/// <returns>A configured status report service for dashboard assertions.</returns>
|
|
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;
|
|
}
|
|
}
|
|
} |