feat(secrets): Secrets:Store:Provider=SqlServer — central pair shares one SQL secret store without the hub

This commit is contained in:
Joseph Doherty
2026-09-28 17:44:14 -04:00
parent ebc55c5f72
commit 35235ecfed
5 changed files with 202 additions and 10 deletions
@@ -699,7 +699,11 @@ public class StartupValidatorTests
{
// Design 2026-09-02: the inbound API key store is per-node SQLite by default, so a
// 2+-node central cluster is one failover away from rejecting every inbound client.
var config = BuildConfig(ValidCentralConfig());
// Secrets:Store:Provider is set to SqlServer here to isolate this warning from the
// independent one added for task 5 (ClusteredCentral_SqliteSecretStore_Warns below).
var values = ValidCentralConfig();
values["Secrets:Store:Provider"] = "SqlServer";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
@@ -714,6 +718,7 @@ public class StartupValidatorTests
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "sqlite";
values["Secrets:Store:Provider"] = "SqlServer";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
@@ -726,6 +731,39 @@ public class StartupValidatorTests
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "SqlServer";
values["Secrets:Store:Provider"] = "SqlServer";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
Assert.Empty(warnings);
}
[Fact]
public void ClusteredCentral_SqliteSecretStore_Warns()
{
// Task 5: a clustered central left on the per-node SQLite secret store silently drifts
// the moment anyone edits a secret on only one node, then ships an inconsistent set to
// sites. ApiKeyStore:Provider is set to SqlServer here to isolate this warning from the
// pre-existing one above.
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "SqlServer";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
var warning = Assert.Single(warnings);
Assert.Contains("clustered central", warning);
Assert.Contains("SQLite", warning);
Assert.Contains("Secrets:Store:Provider=SqlServer", warning);
}
[Fact]
public void ClusteredCentral_SqlServerSecretStore_Silent()
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "SqlServer";
values["Secrets:Store:Provider"] = "SqlServer";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
@@ -779,6 +817,28 @@ public class StartupValidatorTests
Assert.Null(ex);
}
[Fact]
public void UnknownSecretStoreProvider_Fails()
{
var values = ValidCentralConfig();
values["Secrets:Store:Provider"] = "Postgres";
var config = BuildConfig(values);
var ex = Assert.Throws<InvalidOperationException>(() => StartupValidator.Validate(config));
Assert.Contains("Secrets:Store:Provider", ex.Message);
}
[Fact]
public void Validate_AcceptsSecretStoreProviderCaseInsensitively()
{
var values = ValidCentralConfig();
values["Secrets:Store:Provider"] = "sqlserver";
var config = BuildConfig(values);
var ex = Record.Exception(() => StartupValidator.Validate(config));
Assert.Null(ex);
}
[Fact]
public void MultipleErrors_AllReported()
{