513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
196 lines
8.1 KiB
C#
196 lines
8.1 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Observability validation.
|
|
/// Verifies structured logs contain SiteId/NodeHostname/NodeRole,
|
|
/// correlation IDs flow through request chains, and health dashboard shows all metric types.
|
|
/// </summary>
|
|
public class ObservabilityTests : IClassFixture<ScadaBridgeWebApplicationFactory>
|
|
{
|
|
private readonly ScadaBridgeWebApplicationFactory _factory;
|
|
|
|
/// <summary>Initializes the test class with the shared web application factory.</summary>
|
|
/// <param name="factory">The shared web application factory.</param>
|
|
public ObservabilityTests(ScadaBridgeWebApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
}
|
|
|
|
/// <summary>Verifies that the Serilog output template includes the required node fields.</summary>
|
|
[Fact]
|
|
public void StructuredLog_SerilogTemplate_IncludesRequiredFields()
|
|
{
|
|
// The Serilog output template from Program.cs must include NodeRole and NodeHostname.
|
|
var template = "[{Timestamp:HH:mm:ss} {Level:u3}] [{NodeRole}/{NodeHostname}] {Message:lj}{NewLine}{Exception}";
|
|
|
|
Assert.Contains("{NodeRole}", template);
|
|
Assert.Contains("{NodeHostname}", template);
|
|
Assert.Contains("{Timestamp", template);
|
|
Assert.Contains("{Level", template);
|
|
}
|
|
|
|
/// <summary>Verifies that the Serilog enrichment is configured with the site id.</summary>
|
|
[Fact]
|
|
public void SerilogEnrichment_SiteId_Configured()
|
|
{
|
|
// Program.cs enriches all log entries with SiteId, NodeHostname, NodeRole.
|
|
// These are set from configuration and Serilog's Enrich.WithProperty().
|
|
// Verify the enrichment properties are the ones we expect.
|
|
var expectedProperties = new[] { "SiteId", "NodeHostname", "NodeRole" };
|
|
|
|
foreach (var prop in expectedProperties)
|
|
{
|
|
// Structural check: these property names must be present in the logging pipeline
|
|
Assert.False(string.IsNullOrEmpty(prop));
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that every message contract carries a correlation id.</summary>
|
|
[Fact]
|
|
public void CorrelationId_MessageContracts_AllHaveCorrelationId()
|
|
{
|
|
// Verify that key message contracts include a CorrelationId field
|
|
// for request/response tracing through the system.
|
|
|
|
// DeployInstanceCommand has DeploymentId (serves as correlation)
|
|
var deployCmd = new Commons.Messages.Deployment.DeployInstanceCommand(
|
|
"dep-1", "inst-1", "rev-1", "{}", "admin", DateTimeOffset.UtcNow);
|
|
Assert.NotEmpty(deployCmd.DeploymentId);
|
|
|
|
// ScriptCallRequest has CorrelationId
|
|
var scriptCall = new Commons.Messages.ScriptExecution.ScriptCallRequest(
|
|
"OnTrigger", new Dictionary<string, object?>(), 0, "corr-123");
|
|
Assert.Equal("corr-123", scriptCall.CorrelationId);
|
|
|
|
// ScriptCallResult has CorrelationId
|
|
var scriptResult = new Commons.Messages.ScriptExecution.ScriptCallResult(
|
|
"corr-123", true, 42, null);
|
|
Assert.Equal("corr-123", scriptResult.CorrelationId);
|
|
|
|
// Lifecycle commands have CommandId
|
|
var disableCmd = new Commons.Messages.Lifecycle.DisableInstanceCommand(
|
|
"cmd-456", "inst-1", DateTimeOffset.UtcNow);
|
|
Assert.Equal("cmd-456", disableCmd.CommandId);
|
|
}
|
|
|
|
/// <summary>Verifies that every metric type is represented in the site health report.</summary>
|
|
[Fact]
|
|
public void HealthDashboard_AllMetricTypes_RepresentedInReport()
|
|
{
|
|
// The SiteHealthReport must carry all metric types for the health dashboard.
|
|
var report = new SiteHealthReport(
|
|
SiteId: "site-01",
|
|
SequenceNumber: 42,
|
|
ReportTimestamp: DateTimeOffset.UtcNow,
|
|
DataConnectionStatuses: new Dictionary<string, ConnectionHealth>
|
|
{
|
|
["opc-ua-1"] = ConnectionHealth.Connected,
|
|
["opc-ua-2"] = ConnectionHealth.Disconnected
|
|
},
|
|
TagResolutionCounts: new Dictionary<string, TagResolutionStatus>
|
|
{
|
|
["opc-ua-1"] = new(75, 72),
|
|
["opc-ua-2"] = new(50, 0)
|
|
},
|
|
ScriptErrorCount: 3,
|
|
AlarmEvaluationErrorCount: 1,
|
|
StoreAndForwardBufferDepths: new Dictionary<string, int>
|
|
{
|
|
["ext-system"] = 15,
|
|
["notification"] = 2
|
|
},
|
|
DeadLetterCount: 5,
|
|
DeployedInstanceCount: 0,
|
|
EnabledInstanceCount: 0,
|
|
DisabledInstanceCount: 0);
|
|
|
|
// Metric type 1: Data connection health
|
|
Assert.Equal(2, report.DataConnectionStatuses.Count);
|
|
Assert.Equal(ConnectionHealth.Connected, report.DataConnectionStatuses["opc-ua-1"]);
|
|
Assert.Equal(ConnectionHealth.Disconnected, report.DataConnectionStatuses["opc-ua-2"]);
|
|
|
|
// Metric type 2: Tag resolution
|
|
Assert.Equal(75, report.TagResolutionCounts["opc-ua-1"].TotalSubscribed);
|
|
Assert.Equal(72, report.TagResolutionCounts["opc-ua-1"].SuccessfullyResolved);
|
|
|
|
// Metric type 3: Script errors
|
|
Assert.Equal(3, report.ScriptErrorCount);
|
|
|
|
// Metric type 4: Alarm evaluation errors
|
|
Assert.Equal(1, report.AlarmEvaluationErrorCount);
|
|
|
|
// Metric type 5: S&F buffer depths
|
|
Assert.Equal(15, report.StoreAndForwardBufferDepths["ext-system"]);
|
|
Assert.Equal(2, report.StoreAndForwardBufferDepths["notification"]);
|
|
|
|
// Metric type 6: Dead letters
|
|
Assert.Equal(5, report.DeadLetterCount);
|
|
}
|
|
|
|
/// <summary>Verifies that a registered site is marked online by the health aggregator.</summary>
|
|
[Fact]
|
|
public void HealthAggregator_SiteRegistration_MarkedOnline()
|
|
{
|
|
var options = Options.Create(new HealthMonitoringOptions
|
|
{
|
|
OfflineTimeout = TimeSpan.FromSeconds(60)
|
|
});
|
|
|
|
var aggregator = new CentralHealthAggregator(
|
|
options, NullLogger<CentralHealthAggregator>.Instance);
|
|
|
|
// Register a site
|
|
aggregator.ProcessReport(new SiteHealthReport(
|
|
"site-01", 1, DateTimeOffset.UtcNow,
|
|
new Dictionary<string, ConnectionHealth>(),
|
|
new Dictionary<string, TagResolutionStatus>(),
|
|
0, 0, new Dictionary<string, int>(), 0, 0, 0, 0));
|
|
|
|
var state = aggregator.GetSiteState("site-01");
|
|
Assert.NotNull(state);
|
|
Assert.True(state!.IsOnline);
|
|
|
|
// Update with a newer report
|
|
aggregator.ProcessReport(new SiteHealthReport(
|
|
"site-01", 2, DateTimeOffset.UtcNow,
|
|
new Dictionary<string, ConnectionHealth>(),
|
|
new Dictionary<string, TagResolutionStatus>(),
|
|
3, 0, new Dictionary<string, int>(), 0, 0, 0, 0));
|
|
|
|
state = aggregator.GetSiteState("site-01");
|
|
Assert.Equal(2, state!.LastSequenceNumber);
|
|
Assert.Equal(3, state.LatestReport!.ScriptErrorCount);
|
|
}
|
|
|
|
/// <summary>Verifies that health report sequence numbers increase monotonically.</summary>
|
|
[Fact]
|
|
public void HealthReport_SequenceNumbers_Monotonic()
|
|
{
|
|
// Sequence numbers must be monotonically increasing per site.
|
|
// The aggregator should reject stale reports.
|
|
var options = Options.Create(new HealthMonitoringOptions());
|
|
var aggregator = new CentralHealthAggregator(
|
|
options, NullLogger<CentralHealthAggregator>.Instance);
|
|
|
|
for (var seq = 1; seq <= 10; seq++)
|
|
{
|
|
aggregator.ProcessReport(new SiteHealthReport(
|
|
"site-01", seq, DateTimeOffset.UtcNow,
|
|
new Dictionary<string, ConnectionHealth>(),
|
|
new Dictionary<string, TagResolutionStatus>(),
|
|
seq, 0, new Dictionary<string, int>(), 0, 0, 0, 0));
|
|
}
|
|
|
|
var state = aggregator.GetSiteState("site-01");
|
|
Assert.Equal(10, state!.LastSequenceNumber);
|
|
Assert.Equal(10, state.LatestReport!.ScriptErrorCount);
|
|
}
|
|
}
|