feat(host): StartupValidator warns when a clustered central keeps per-node inbound API keys; rejects an unknown ApiKeyStore:Provider at boot; Program.cs logs CollectWarnings once Serilog is up

Claude-Session: https://claude.ai/code/session_013L3XdmVii9i5sY4FChU8pB
This commit is contained in:
Joseph Doherty
2026-09-02 23:26:34 -04:00
parent 06be0b9e64
commit d6fd8c400c
3 changed files with 123 additions and 1 deletions
@@ -694,6 +694,91 @@ public class StartupValidatorTests
Assert.Null(ex);
}
[Fact]
public void CollectWarnings_WarnsForTwoSeedCentralOnSqlite()
{
// 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());
var warnings = StartupValidator.CollectWarnings(config);
var warning = Assert.Single(warnings);
Assert.Contains("2-node", warning);
Assert.Contains("SQLite", warning);
Assert.Contains("Provider=SqlServer", warning);
}
[Fact]
public void CollectWarnings_WarnsWhenProviderExplicitlySqlite()
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "sqlite";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
Assert.Single(warnings);
}
[Fact]
public void CollectWarnings_SilentForSqlServer()
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "SqlServer";
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
Assert.Empty(warnings);
}
[Fact]
public void CollectWarnings_SilentForSingleSeedCentral()
{
// CollectWarnings is independent of Validate's own >=2-seeds requirement — it should
// simply not fire the failover warning when there is nothing to fail over to.
var values = ValidCentralConfig();
values.Remove("ScadaBridge:Cluster:SeedNodes:1");
var config = BuildConfig(values);
var warnings = StartupValidator.CollectWarnings(config);
Assert.Empty(warnings);
}
[Fact]
public void CollectWarnings_SilentForSite()
{
var config = BuildConfig(ValidSiteConfig());
var warnings = StartupValidator.CollectWarnings(config);
Assert.Empty(warnings);
}
[Fact]
public void Validate_RejectsUnknownApiKeyStoreProvider()
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "Postgres";
var config = BuildConfig(values);
var ex = Assert.Throws<InvalidOperationException>(() => StartupValidator.Validate(config));
Assert.Contains("ScadaBridge:InboundApi:ApiKeyStore:Provider", ex.Message);
}
[Fact]
public void Validate_AcceptsProviderCaseInsensitively()
{
var values = ValidCentralConfig();
values["ScadaBridge:InboundApi:ApiKeyStore:Provider"] = "SQLSERVER";
var config = BuildConfig(values);
var ex = Record.Exception(() => StartupValidator.Validate(config));
Assert.Null(ex);
}
[Fact]
public void MultipleErrors_AllReported()
{