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
@@ -196,6 +196,77 @@ public class SecretsReplicationWiringTests
Assert.False(SecretsRegistration.UsesGrpcHub(config));
}
// ---------------------------------------------------------------------------------------
// Secrets:Store:Provider=SqlServer — a clustered central sharing ONE SQL secret store
// directly, without standing up the gRPC hub (task 5). Independent of Replication/Mode.
// ---------------------------------------------------------------------------------------
[Fact]
public void Central_StoreProviderSqlServer_UsesSharedSqlStore_WithoutHub()
{
IConfiguration config = BuildConfig(
("Secrets:Store:Provider", "SqlServer"),
("Secrets:SqlServer:ConnectionString", DummyConnectionString));
using ServiceProvider provider = BuildProvider(config, SecretsNodeRole.Central);
Assert.IsType<SqlServerSecretStore>(provider.GetRequiredService<ISecretStore>());
// No hub: neither the fail-closed bearer interceptor nor the follower sweep is wired,
// and UsesGrpcHub — the single predicate MapScadaBridgeSecretsHub also checks — is false.
Assert.Null(provider.GetService<SecretsHubAuthInterceptor>());
Assert.DoesNotContain(provider.GetServices<IHostedService>(), IsGrpcHubSweepService);
Assert.False(SecretsRegistration.UsesGrpcHub(config));
}
[Fact]
public void Central_StoreProviderSqlServer_BlankConnectionString_Throws()
{
var services = new ServiceCollection();
services.AddLogging();
IConfiguration config = BuildConfig(("Secrets:Store:Provider", "SqlServer"));
var ex = Assert.Throws<InvalidOperationException>(
() => services.AddScadaBridgeSecrets(config, SecretsNodeRole.Central));
// Same helper, same message style as the Grpc-mode central check.
Assert.Contains(
SecretsRegistration.HubConnectionStringKey, ex.Message, StringComparison.Ordinal);
}
[Fact]
public void Central_StoreProviderSqlServer_SecretReferenceConnectionString_Throws()
{
var services = new ServiceCollection();
services.AddLogging();
IConfiguration config = BuildConfig(
("Secrets:Store:Provider", "SqlServer"),
("Secrets:SqlServer:ConnectionString", "${secret:hub-connection-string}"));
var ex = Assert.Throws<InvalidOperationException>(
() => services.AddScadaBridgeSecrets(config, SecretsNodeRole.Central));
Assert.Contains(
SecretsRegistration.HubConnectionStringKey, ex.Message, StringComparison.Ordinal);
Assert.Contains("${secret:", ex.Message, StringComparison.Ordinal);
Assert.DoesNotContain("hub-connection-string", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void Site_IgnoresStoreProvider()
{
// A Site node ignores Secrets:Store:Provider entirely and always keeps its local SQLite
// store — even with the value set to SqlServer and no connection string configured at
// all, which would fail closed on Central.
IConfiguration config = BuildConfig(("Secrets:Store:Provider", "SqlServer"));
using ServiceProvider provider = BuildProvider(config, SecretsNodeRole.Site);
Assert.IsType<SqliteSecretStore>(provider.GetRequiredService<ISecretStore>());
}
// ---------------------------------------------------------------------------------------
// SQL-Server mode — regression pins. Behaviour must be byte-for-byte what it was before the
// mode key existed, both when the mode is left unset and when it is named explicitly.