0289d51137
A DeployedSecretsMirror failure on the STANDBY node (e.g. a master-key mismatch
that stops a deployed secret decrypting there) was only logged on that node:
site health reports are sent by the active node only, so central never saw it.
- New 12th LocalDb-replicated table secret_mirror_status (node_name PK,
failures_json of {SecretName, Message}, checked_at; no FKs). Each node's
mirror writes only its own row, and only when its failure set changes plus
once per start-up (every write replicates via CDC). Best-effort: a failed
write is a Warning and is retried next pass. Node key = INodeIdentityProvider
NodeName, falling back to the machine name.
- SecretMirrorStatusReporter (SiteRuntime, every node, 30 s) reads every row
into ISiteHealthCollector.SetSecretMirrorStatuses -> additive nullable
SiteHealthReport.SecretMirrorStatuses (null = no data).
- Wire: SiteHealthReportDto.secret_mirror_statuses = 33 (presence wrapper so
null != empty); vendored CentralControl.cs regenerated; mapper both ways.
- Central UI Health card: 'Secret mirror failing' badge + compact per-node
node / secret / message list.
- Tests: mirror status write-on-change/first-pass/failure isolation, storage
round-trip, reporter/collector, DTO mapper round-trip incl. null/empty,
bUnit, replicated-table set (twelve) + FK guard wording.
46 lines
1.6 KiB
C#
46 lines
1.6 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
|
|
|
/// <summary>
|
|
/// Review finding M1 — <c>SiteHealthReport.SecretMirrorStatuses</c> carries every site node's
|
|
/// deployed-secrets mirror failures (the standby's included) on the active node's report.
|
|
/// </summary>
|
|
public class SecretMirrorStatusHealthFieldTests
|
|
{
|
|
[Fact]
|
|
public void CollectReport_NeverTold_LeavesTheFieldNull()
|
|
{
|
|
Assert.Null(new SiteHealthCollector().CollectReport("TestSite").SecretMirrorStatuses);
|
|
}
|
|
|
|
[Fact]
|
|
public void CollectReport_CarriesTheLatestSnapshot_AndDoesNotDrainIt()
|
|
{
|
|
var collector = new SiteHealthCollector();
|
|
collector.SetSecretMirrorStatuses(
|
|
[
|
|
new SecretMirrorNodeStatus("node-a", DateTimeOffset.UtcNow, []),
|
|
new SecretMirrorNodeStatus(
|
|
"node-b", DateTimeOffset.UtcNow, [new SecretMirrorFailure("S1", "did not decrypt")]),
|
|
]);
|
|
|
|
var first = collector.CollectReport("TestSite").SecretMirrorStatuses!;
|
|
var second = collector.CollectReport("TestSite").SecretMirrorStatuses!;
|
|
|
|
Assert.Equal(["node-a", "node-b"], first.Select(s => s.NodeName));
|
|
Assert.Equal(2, second.Count); // point-in-time, not interval-drained
|
|
Assert.Equal("S1", Assert.Single(first[1].Failures).SecretName);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetNull_ResetsToNoData()
|
|
{
|
|
var collector = new SiteHealthCollector();
|
|
collector.SetSecretMirrorStatuses([]);
|
|
collector.SetSecretMirrorStatuses(null);
|
|
|
|
Assert.Null(collector.CollectReport("TestSite").SecretMirrorStatuses);
|
|
}
|
|
}
|