feat(site): deployed_secrets (LocalDb-replicated ciphertext) + node-local mirrored_secrets ledger

This commit is contained in:
Joseph Doherty
2026-09-28 18:36:49 -04:00
parent 437b02a43c
commit a35ee663c8
8 changed files with 662 additions and 22 deletions
@@ -104,6 +104,21 @@ public class SiteLocalDbCdcRegistrationTests : IDisposable
Assert.DoesNotContain(triggers, t => t.StartsWith("__localdb_smtp_configurations_", StringComparison.Ordinal));
}
[Fact]
public void ReplicatedNode_InstallsNoCaptureTriggersOnTheNodeLocalMirroredSecretsLedger()
{
// mirrored_secrets records which names THIS node has mirrored into its own secret
// store — per-node fact, not site-wide state. Capturing it would replicate one node's
// mirroring progress onto the other, so it must stay uncaptured even on a fully
// replicated node.
var db = BuildDatabase(Config(apiKey: "cdc-test-key"));
var triggers = CaptureTriggers(db);
Assert.DoesNotContain(triggers, t => t.StartsWith("__localdb_mirrored_secrets_", StringComparison.Ordinal));
Assert.DoesNotContain("mirrored_secrets", db.ReplicatedTables.Keys);
}
[Fact]
public void UnreplicatedNode_DropsTriggersLeftByAPreviouslyReplicatedBuild()
{
@@ -196,13 +211,18 @@ public class SiteLocalDbCdcRegistrationTests : IDisposable
// ---- helpers ----------------------------------------------------------------------
/// <summary>The ten replicated tables plus the two deliberately-unregistered ones.</summary>
/// <summary>
/// The eleven replicated tables, plus the two deliberately-unregistered central-only
/// notification tables, plus the node-local (also deliberately-unregistered)
/// <c>mirrored_secrets</c> ledger.
/// </summary>
private static readonly string[] AllSiteTables =
[
"OperationTracking", "site_events", "sf_messages", "deployed_configurations",
"static_attribute_overrides", "shared_scripts", "external_systems",
"database_connections", "data_connection_definitions", "native_alarm_state",
"notification_lists", "smtp_configurations",
"deployed_secrets",
"notification_lists", "smtp_configurations", "mirrored_secrets",
];
private static readonly string[] ReplicatedTables =
@@ -210,6 +230,7 @@ public class SiteLocalDbCdcRegistrationTests : IDisposable
"OperationTracking", "site_events", "sf_messages", "deployed_configurations",
"static_attribute_overrides", "shared_scripts", "external_systems",
"database_connections", "data_connection_definitions", "native_alarm_state",
"deployed_secrets",
];
private static void AssertCaptureTriggersCoverTheReplicatedTables(ILocalDb db)
@@ -15,7 +15,8 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <c>RegisterReplicated</c> time, which on a site node is a BOOT failure of the replicating
/// pair. This test moves that discovery from the first replicated boot to the build: it walks
/// the real site schema (every table <c>SiteLocalDbSetup.OnReady</c> creates) and asserts none
/// of the ten <see cref="SiteLocalDbSetup.ReplicatedTables"/> has such an inbound FK.
/// of the eleven <see cref="SiteLocalDbSetup.ReplicatedTables"/> (incl. <c>deployed_secrets</c>,
/// added for site-secrets artifact deploy) has such an inbound FK.
/// </summary>
/// <remarks>
/// The schema is built with an UNREPLICATED config on purpose: a replicated one would make
@@ -245,7 +245,7 @@ public class SiteLocalDbWiringTests : IDisposable
var tables = TableNames(db);
// One from each Phase 1 schema, then every Phase 2 table.
// One from each Phase 1 schema, then every Phase 2 table plus the site-secrets tables.
Assert.Contains("OperationTracking", tables);
Assert.Contains("site_events", tables);
Assert.Contains("sf_messages", tables);
@@ -254,6 +254,7 @@ public class SiteLocalDbWiringTests : IDisposable
"deployed_configurations", "static_attribute_overrides", "shared_scripts",
"external_systems", "database_connections", "notification_lists",
"data_connection_definitions", "smtp_configurations", "native_alarm_state",
"deployed_secrets", "mirrored_secrets",
})
{
Assert.Contains(table, tables);
@@ -261,19 +262,21 @@ public class SiteLocalDbWiringTests : IDisposable
}
[Fact]
public void Site_LocalDb_RegistersExactlyTheTenReplicatedTables()
public void Site_LocalDb_RegistersExactlyTheElevenReplicatedTables()
{
// The Phase 2 cutover. This is an EXACTLY check rather than a Contains in both
// directions, and both directions are load-bearing.
// The Phase 2 cutover, extended by site-secrets artifact deploy's deployed_secrets.
// This is an EXACTLY check rather than a Contains in both directions, and both
// directions are load-bearing.
//
// Too few: the table silently stops replicating. Storage still works, every other
// test still passes, and the pair just quietly diverges — the failure mode Phase 1
// existed to end.
//
// Too many: notification_lists and smtp_configurations must NOT be here. They are
// permanently empty by design, and registering them would open a standing
// replication channel whose only historical payload was plaintext SMTP passwords.
// A Contains-based test would never catch that.
// Too many: notification_lists, smtp_configurations, and mirrored_secrets must NOT
// be here. notification_lists/smtp_configurations are permanently empty by design
// (their only historical payload was a plaintext SMTP password); mirrored_secrets is
// node-local fact (which names THIS node has mirrored), not site-wide state. A
// Contains-based test would never catch either.
var db = _host.Services.GetRequiredService<ILocalDb>();
Assert.Equal(
@@ -281,8 +284,9 @@ public class SiteLocalDbWiringTests : IDisposable
// Ordinal order: '_' (0x5F) sorts before 'b' (0x62), so
// data_connection_definitions precedes database_connections.
"OperationTracking", "data_connection_definitions", "database_connections",
"deployed_configurations", "external_systems", "native_alarm_state",
"sf_messages", "shared_scripts", "site_events", "static_attribute_overrides",
"deployed_configurations", "deployed_secrets", "external_systems",
"native_alarm_state", "sf_messages", "shared_scripts", "site_events",
"static_attribute_overrides",
],
db.ReplicatedTables.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray());
}
@@ -300,6 +304,17 @@ public class SiteLocalDbWiringTests : IDisposable
Assert.DoesNotContain("smtp_configurations", db.ReplicatedTables.Keys);
}
[Fact]
public void Site_LocalDb_DoesNotReplicateTheNodeLocalMirroredSecretsLedger()
{
// mirrored_secrets exists (SiteStorageSchema creates it) but must never be captured:
// it records which names THIS node has mirrored into its own secret store, which is
// per-node fact, not site-wide state — see SiteLocalDbSetup.ReplicatedTables.
var db = _host.Services.GetRequiredService<ILocalDb>();
Assert.DoesNotContain("mirrored_secrets", db.ReplicatedTables.Keys);
}
[Fact]
public void Site_LocalDb_ReplicatedPhase2TablesHaveTheirExpectedPrimaryKeys()
{
@@ -318,6 +333,7 @@ public class SiteLocalDbWiringTests : IDisposable
Assert.Equal(
["instance_unique_name", "source_canonical_name", "source_reference"],
db.ReplicatedTables["native_alarm_state"].PkColumns);
Assert.Equal(["name"], db.ReplicatedTables["deployed_secrets"].PkColumns);
}
private static HashSet<string> TableNames(ILocalDb db)