docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)

Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Configuration;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// WP-11: Tests for StartupValidator configuration validation.
/// Tests for StartupValidator configuration validation.
/// </summary>
public class StartupValidatorTests
{
@@ -38,18 +38,20 @@ public class StartupValidatorTests
["ScadaBridge:Database:SiteDbPath"] = "./data/scadabridge.db",
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@site-a-node1:8082",
["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@site-a-node2:8082",
// T0.3: the gRPC control plane is fail-closed, so a Site node without a preshared
// The gRPC control plane is fail-closed, so a Site node without a preshared
// key serves nothing while still looking healthy. Required at boot for that reason.
["ScadaBridge:Communication:GrpcPsk"] = "test-site-control-plane-key",
// Phase 4: gRPC (CentralControlService) is the only site→central transport, so a Site
// node must list at least one central gRPC endpoint to dial (no Akka fallback remains).
["ScadaBridge:Communication:CentralGrpcEndpoints:0"] = "http://central-a:8083",
// arch-review remediation WP1.2: the site hot-path audit writer's SQLite file has no
// The site hot-path audit writer's SQLite file has no
// default path (mirrors LocalDb:Path) — required so it lands on the mounted data volume,
// not the container's ephemeral overlayfs.
["AuditLog:SiteWriter:DatabasePath"] = "/app/data/auditlog.db",
};
/// <summary>Verifies that a valid central config passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void ValidCentralConfig_PassesValidation()
{
@@ -58,6 +60,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a valid site config passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void ValidSiteConfig_PassesValidation()
{
@@ -66,6 +70,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a site without gRPC PSK fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void SiteWithoutGrpcPsk_FailsValidation()
{
@@ -81,6 +87,8 @@ public class StartupValidatorTests
Assert.Contains("GrpcPsk", ex.Message);
}
/// <summary>Verifies that a site with a blank gRPC PSK fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void SiteWithBlankGrpcPsk_FailsValidation()
{
@@ -94,6 +102,8 @@ public class StartupValidatorTests
Assert.Contains("GrpcPsk", ex.Message);
}
/// <summary>Verifies that a central node without gRPC PSK passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void CentralWithoutGrpcPsk_PassesValidation()
{
@@ -104,6 +114,8 @@ public class StartupValidatorTests
Assert.Null(Record.Exception(() => StartupValidator.Validate(config)));
}
/// <summary>Verifies that a site without central gRPC endpoints fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void SiteWithoutCentralGrpcEndpoints_FailsValidation()
{
@@ -118,6 +130,8 @@ public class StartupValidatorTests
Assert.Contains("CentralGrpcEndpoints", ex.Message);
}
/// <summary>Verifies that a central node without central gRPC endpoints passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void CentralWithoutCentralGrpcEndpoints_PassesValidation()
{
@@ -128,10 +142,12 @@ public class StartupValidatorTests
Assert.Null(Record.Exception(() => StartupValidator.Validate(config)));
}
/// <summary>Verifies that a site without audit log database path fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void SiteWithoutAuditLogDatabasePath_FailsValidation()
{
// arch-review remediation WP1.2: SqliteAuditWriterOptions.DatabasePath has no default
// SqliteAuditWriterOptions.DatabasePath has no default
// (mirrors LocalDb:Path). An unset value used to fall back to a bare "auditlog.db"
// resolved relative to CWD — on the docker rig, the container's ephemeral overlayfs —
// so the node booted, looked healthy, and lost its pending audit backlog on every
@@ -144,6 +160,8 @@ public class StartupValidatorTests
Assert.Contains("DatabasePath", ex.Message);
}
/// <summary>Verifies that a site with a blank audit log database path fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void SiteWithBlankAuditLogDatabasePath_FailsValidation()
{
@@ -155,6 +173,8 @@ public class StartupValidatorTests
Assert.Contains("DatabasePath", ex.Message);
}
/// <summary>Verifies that a central node without audit log database path passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void CentralWithoutAuditLogDatabasePath_PassesValidation()
{
@@ -165,6 +185,8 @@ public class StartupValidatorTests
Assert.Null(Record.Exception(() => StartupValidator.Validate(config)));
}
/// <summary>Verifies that a missing role fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void MissingRole_FailsValidation()
{
@@ -176,6 +198,8 @@ public class StartupValidatorTests
Assert.Contains("Role must be 'Central' or 'Site'", ex.Message);
}
/// <summary>Verifies that an invalid role fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void InvalidRole_FailsValidation()
{
@@ -187,6 +211,8 @@ public class StartupValidatorTests
Assert.Contains("Role must be 'Central' or 'Site'", ex.Message);
}
/// <summary>Verifies that an empty hostname fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void EmptyHostname_FailsValidation()
{
@@ -198,6 +224,8 @@ public class StartupValidatorTests
Assert.Contains("NodeHostname is required", ex.Message);
}
/// <summary>Verifies that a missing hostname fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void MissingHostname_FailsValidation()
{
@@ -209,6 +237,9 @@ public class StartupValidatorTests
Assert.Contains("NodeHostname is required", ex.Message);
}
/// <summary>Verifies that an invalid port fails validation.</summary>
/// <param name="port">The port.</param>
/// <returns>The result.</returns>
[Theory]
[InlineData("0")]
[InlineData("-1")]
@@ -225,6 +256,9 @@ public class StartupValidatorTests
Assert.Contains("RemotingPort must be 1-65535", ex.Message);
}
/// <summary>Verifies that a valid port passes validation.</summary>
/// <param name="port">The port.</param>
/// <returns>The result.</returns>
[Theory]
[InlineData("1")]
[InlineData("8081")]
@@ -243,6 +277,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a site without a site id fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_MissingSiteId_FailsValidation()
{
@@ -254,6 +290,8 @@ public class StartupValidatorTests
Assert.Contains("SiteId is required for Site nodes", ex.Message);
}
/// <summary>Verifies that a central node missing configuration database fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_MissingConfigurationDb_FailsValidation()
{
@@ -265,14 +303,16 @@ public class StartupValidatorTests
Assert.Contains("ConfigurationDb connection string required for Central", ex.Message);
}
/// <summary>Verifies that a central node missing machine data database fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_MissingMachineDataDb_FailsValidation()
{
// Reverts Host-008. REQ-HOST-3/REQ-HOST-4 require MachineDataDb to be
// The host requirements require MachineDataDb to be
// validated at startup for Central nodes, and the shipped docker appsettings
// (docker/central-node-a/appsettings.Central.json and central-node-b) carry
// the key. The prior Host-008 decision (which removed the Require) is reversed
// here (#17, M2.9): a missing MachineDataDb must fail fast with a clear error.
// the key. The earlier decision (which removed the Require) is reversed
// here: a missing MachineDataDb must fail fast with a clear error.
var values = ValidCentralConfig();
values.Remove("ScadaBridge:Database:MachineDataDb");
var config = BuildConfig(values);
@@ -281,10 +321,12 @@ public class StartupValidatorTests
Assert.Contains("MachineDataDb connection string required for Central", ex.Message);
}
/// <summary>Verifies that a central node missing LDAP server fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_MissingLdapServer_FailsValidation()
{
// Task 1.4: the LDAP server key nests under Security:Ldap now. The pre-host
// The LDAP server key nests under Security:Ldap now. The pre-host
// preflight validates the nested key and still fails fast for Central.
var values = ValidCentralConfig();
values.Remove("ScadaBridge:Security:Ldap:Server");
@@ -294,6 +336,8 @@ public class StartupValidatorTests
Assert.Contains("Ldap:Server required for Central", ex.Message);
}
/// <summary>Verifies that a central node missing JWT signing key fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_MissingJwtSigningKey_FailsValidation()
{
@@ -305,6 +349,8 @@ public class StartupValidatorTests
Assert.Contains("JwtSigningKey required for Central", ex.Message);
}
/// <summary>Verifies that a central node missing API key pepper fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_MissingApiKeyPepper_FailsValidation()
{
@@ -319,6 +365,8 @@ public class StartupValidatorTests
Assert.Contains("ApiKeyPepper", ex.Message);
}
/// <summary>Verifies that a central node with a short API key pepper fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_ShortApiKeyPepper_FailsValidation()
{
@@ -331,6 +379,8 @@ public class StartupValidatorTests
Assert.Contains("ApiKeyPepper", ex.Message);
}
/// <summary>Verifies that a site does not require an API key pepper.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_ApiKeyPepper_NotRequired()
{
@@ -363,6 +413,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that fewer than two seed nodes fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void FewerThanTwoSeedNodes_FailsValidation()
{
@@ -374,6 +426,8 @@ public class StartupValidatorTests
Assert.Contains("SeedNodes must have at least 2 entries", ex.Message);
}
/// <summary>Verifies that having no seed nodes fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void NoSeedNodes_FailsValidation()
{
@@ -386,6 +440,8 @@ public class StartupValidatorTests
Assert.Contains("SeedNodes must have at least 2 entries", ex.Message);
}
/// <summary>Verifies that a peer-first seed order fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void PeerFirstSeedOrder_FailsValidation()
{
@@ -403,6 +459,8 @@ public class StartupValidatorTests
Assert.Contains("must list this node itself first", ex.Message);
}
/// <summary>Verifies that a self-first seed order on a site node passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void SelfFirstSeedOrder_OnASiteNode_PassesValidation()
{
@@ -415,6 +473,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that the self-first seed check matches on host and port, not just host.</summary>
/// <returns>The result.</returns>
[Fact]
public void SelfFirstSeed_MatchedOnHostAndPort_NotJustHost()
{
@@ -432,6 +492,9 @@ public class StartupValidatorTests
Assert.Contains("must list this node itself first", ex.Message);
}
/// <summary>Verifies that a site with an invalid gRPC port fails validation.</summary>
/// <param name="grpcPort">The grpc port.</param>
/// <returns>The result.</returns>
[Theory]
[InlineData("0")]
[InlineData("-1")]
@@ -447,6 +510,8 @@ public class StartupValidatorTests
Assert.Contains("GrpcPort must be 1-65535", ex.Message);
}
/// <summary>Verifies that a site with a valid gRPC port passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_ValidGrpcPort_PassesValidation()
{
@@ -458,6 +523,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that an invalid gRPC port is not validated on a central node.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_InvalidGrpcPort_NotValidated()
{
@@ -469,10 +536,12 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a site seed node on the gRPC port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_SeedNodeOnGrpcPort_FailsValidation()
{
// Host-004 regression: a site seed node must reference an Akka remoting
// Regression: a site seed node must reference an Akka remoting
// endpoint, never the Kestrel HTTP/2 gRPC port. A seed node whose port
// equals this node's GrpcPort would make a joining node attempt an
// Akka.Remote TCP association against the gRPC listener and fail.
@@ -485,6 +554,8 @@ public class StartupValidatorTests
Assert.Contains("must not target the gRPC port", ex.Message);
}
/// <summary>Verifies that a site seed node on the metrics port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_SeedNodeOnMetricsPort_FailsValidation()
{
@@ -501,6 +572,8 @@ public class StartupValidatorTests
Assert.Contains("must not target the metrics port", ex.Message);
}
/// <summary>Verifies that a site seed node on the default metrics port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_SeedNodeOnDefaultMetricsPort_FailsValidation()
{
@@ -515,6 +588,8 @@ public class StartupValidatorTests
Assert.Contains("must not target the metrics port", ex.Message);
}
/// <summary>Verifies that a site seed node on the default gRPC port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_SeedNodeOnDefaultGrpcPort_FailsValidation()
{
@@ -528,6 +603,8 @@ public class StartupValidatorTests
Assert.Contains("must not target the gRPC port", ex.Message);
}
/// <summary>Verifies that site seed nodes on the remoting port pass validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_SeedNodesOnRemotingPort_PassesValidation()
{
@@ -542,6 +619,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a central seed node on port 8083 passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_SeedNodeOnPort8083_PassesValidation()
{
@@ -555,10 +634,12 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a site gRPC port equal to the remoting port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_GrpcPortEqualsRemotingPort_FailsValidation()
{
// Host-007 regression: REQ-HOST-4 requires GrpcPort to differ from
// Regression: the host requirements require GrpcPort to differ from
// RemotingPort. Identical values cause Kestrel and Akka.Remote to
// contend for the same port at runtime.
var values = ValidSiteConfig();
@@ -570,6 +651,8 @@ public class StartupValidatorTests
Assert.Contains("GrpcPort must differ from RemotingPort", ex.Message);
}
/// <summary>Verifies that a site default gRPC port equal to the remoting port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_DefaultGrpcPortEqualsRemotingPort_FailsValidation()
{
@@ -583,6 +666,8 @@ public class StartupValidatorTests
Assert.Contains("GrpcPort must differ from RemotingPort", ex.Message);
}
/// <summary>Verifies that a site gRPC port that differs from the remoting port passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_GrpcPortDiffersFromRemotingPort_PassesValidation()
{
@@ -595,6 +680,9 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a site with an invalid metrics port fails validation.</summary>
/// <param name="metricsPort">The metrics port.</param>
/// <returns>The result.</returns>
[Theory]
[InlineData("0")]
[InlineData("-1")]
@@ -610,6 +698,8 @@ public class StartupValidatorTests
Assert.Contains("MetricsPort must be 1-65535", ex.Message);
}
/// <summary>Verifies that a site with a valid metrics port passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_ValidMetricsPort_PassesValidation()
{
@@ -621,10 +711,12 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a site metrics port equal to the remoting port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_MetricsPortEqualsRemotingPort_FailsValidation()
{
// Host-007 regression: the Kestrel metrics (HTTP/1.1) listener port must
// Regression: the Kestrel metrics (HTTP/1.1) listener port must
// differ from RemotingPort. Identical values cause the metrics listener
// and Akka.Remote to contend for the same port at runtime.
var values = ValidSiteConfig();
@@ -636,10 +728,12 @@ public class StartupValidatorTests
Assert.Contains("MetricsPort must differ from RemotingPort", ex.Message);
}
/// <summary>Verifies that a site metrics port equal to the gRPC port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_MetricsPortEqualsGrpcPort_FailsValidation()
{
// Host-007 regression: the metrics listener port must differ from GrpcPort.
// Regression: the metrics listener port must differ from GrpcPort.
var values = ValidSiteConfig();
values["ScadaBridge:Node:GrpcPort"] = "8083";
values["ScadaBridge:Node:MetricsPort"] = "8083";
@@ -649,6 +743,8 @@ public class StartupValidatorTests
Assert.Contains("MetricsPort must differ from GrpcPort", ex.Message);
}
/// <summary>Verifies that a site default metrics port equal to the remoting port fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_DefaultMetricsPortEqualsRemotingPort_FailsValidation()
{
@@ -668,6 +764,8 @@ public class StartupValidatorTests
Assert.Contains("MetricsPort must differ from RemotingPort", ex.Message);
}
/// <summary>Verifies that a site metrics port that differs from the remoting and gRPC ports passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void Site_MetricsPortDiffersFromRemotingAndGrpc_PassesValidation()
{
@@ -681,6 +779,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that an invalid metrics port is not validated on a central node.</summary>
/// <returns>The result.</returns>
[Fact]
public void Central_InvalidMetricsPort_NotValidated()
{
@@ -694,6 +794,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that collecting warnings warns for a two-seed central node on SQLite.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_WarnsForTwoSeedCentralOnSqlite()
{
@@ -713,6 +815,8 @@ public class StartupValidatorTests
Assert.Contains("Provider=SqlServer", warning);
}
/// <summary>Verifies that collecting warnings warns when the provider is explicitly SQLite.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_WarnsWhenProviderExplicitlySqlite()
{
@@ -726,6 +830,8 @@ public class StartupValidatorTests
Assert.Single(warnings);
}
/// <summary>Verifies that collecting warnings is silent for SQL Server.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_SilentForSqlServer()
{
@@ -739,10 +845,12 @@ public class StartupValidatorTests
Assert.Empty(warnings);
}
/// <summary>Verifies that a clustered central node on a SQLite secret store produces a warning.</summary>
/// <returns>The result.</returns>
[Fact]
public void ClusteredCentral_SqliteSecretStore_Warns()
{
// Task 5: a clustered central left on the per-node SQLite secret store silently drifts
// 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.
@@ -758,6 +866,8 @@ public class StartupValidatorTests
Assert.Contains("Secrets:Store:Provider=SqlServer", warning);
}
/// <summary>Verifies that a clustered central node on a SQL Server secret store produces no warning.</summary>
/// <returns>The result.</returns>
[Fact]
public void ClusteredCentral_SqlServerSecretStore_Silent()
{
@@ -772,7 +882,7 @@ public class StartupValidatorTests
}
/// <summary>
/// Task 11: secrets replication (the gRPC hub, and the SQL-Server ReplicatingSecretStore mode
/// Secrets replication (the gRPC hub, and the SQL-Server ReplicatingSecretStore mode
/// that preceded it) is retired in ScadaBridge — sites receive secrets exclusively via Deploy
/// Artifacts, and a replicated site store would publish DeployedSecretsMirror's local deletes
/// fleet-wide. Checked on BOTH roles: a Site used to be the gRPC hub's follower half, so the
@@ -781,10 +891,11 @@ public class StartupValidatorTests
/// because CollectWarnings never runs with this flag true on a config that also passed
/// Validate.
/// </summary>
/// <param name="value">The rejected replication flag value.</param>
[Theory]
[InlineData("true")]
[InlineData("True")]
// Review round 1, Minor #3: these are not valid booleans. GetValue<bool> used to throw a raw
// These are not valid booleans. GetValue<bool> used to throw a raw
// OptionsValidationException-style binder crash on them instead of the designed message; the
// key is read as a raw string now, so any non-"false" value is "set" and rejected the same way.
[InlineData("yes")]
@@ -803,6 +914,8 @@ public class StartupValidatorTests
Assert.Contains("central uses Secrets:Store:Provider=SqlServer", ex.Message);
}
/// <summary>Verifies that enabling secrets replication is rejected on a site.</summary>
/// <returns>The result.</returns>
[Fact]
public void ReplicationEnabled_IsRejected_OnSite()
{
@@ -816,6 +929,8 @@ public class StartupValidatorTests
"Secrets replication (the gRPC hub) is retired in ScadaBridge", ex.Message);
}
/// <summary>Verifies that disabling secrets replication passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void ReplicationDisabled_PassesValidation()
{
@@ -830,6 +945,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that a case-insensitive replication-disabled value passes validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void ReplicationDisabled_CaseInsensitive_PassesValidation()
{
@@ -843,7 +960,7 @@ public class StartupValidatorTests
}
/// <summary>
/// Deliberately stricter than <c>bool</c> parsing (review round 1, Minor #3): only an absent
/// Deliberately stricter than <c>bool</c> parsing (review feedback): only an absent
/// key or exactly (case-insensitively) <c>"false"</c> is safe. <c>"0"</c> is a legal
/// <c>bool.TryParse</c>-adjacent falsy spelling in some conventions but is NOT exempted here —
/// a leftover key this malformed should surface the retirement message, not silently pass.
@@ -862,8 +979,8 @@ public class StartupValidatorTests
}
/// <summary>
/// Review round 1, Minor #4: a leftover <c>Secrets:Replication</c>/<c>Secrets:GrpcHub</c> key
/// from before Task 11 is not itself a boot failure (Validate already rejects Enabled=true
/// A leftover <c>Secrets:Replication</c>/<c>Secrets:GrpcHub</c> key
/// from before secrets replication was retired is not itself a boot failure (Validate already rejects Enabled=true
/// outright), but every OTHER key under either section is now silently ignored — worth a
/// Warning so an operator cleans up QA/prod appsettings.
/// </summary>
@@ -882,6 +999,8 @@ public class StartupValidatorTests
&& w.Contains("the secrets hub is retired; remove them", StringComparison.Ordinal));
}
/// <summary>Verifies that collecting warnings warns for a leftover gRPC hub key.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_WarnsForLeftoverGrpcHubKey()
{
@@ -896,6 +1015,8 @@ public class StartupValidatorTests
w.Contains("Secrets:Replication / Secrets:GrpcHub settings are ignored", StringComparison.Ordinal));
}
/// <summary>Verifies that collecting warnings warns for leftover keys on a site too.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_WarnsForLeftoverKeys_OnSiteToo()
{
@@ -911,6 +1032,8 @@ public class StartupValidatorTests
w.Contains("Secrets:Replication / Secrets:GrpcHub settings are ignored", StringComparison.Ordinal));
}
/// <summary>Verifies that collecting warnings is silent for a bare replication-enabled false value.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_SilentForBareReplicationEnabledFalse()
{
@@ -928,6 +1051,8 @@ public class StartupValidatorTests
w.Contains("Secrets:Replication / Secrets:GrpcHub settings are ignored", StringComparison.Ordinal));
}
/// <summary>Verifies that collecting warnings is silent when neither section is present.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_SilentWhenNeitherSectionPresent()
{
@@ -941,6 +1066,8 @@ public class StartupValidatorTests
w.Contains("Secrets:Replication / Secrets:GrpcHub settings are ignored", StringComparison.Ordinal));
}
/// <summary>Verifies that collecting warnings is silent for a single-seed central node.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_SilentForSingleSeedCentral()
{
@@ -955,6 +1082,8 @@ public class StartupValidatorTests
Assert.Empty(warnings);
}
/// <summary>Verifies that collecting warnings is silent for a site.</summary>
/// <returns>The result.</returns>
[Fact]
public void CollectWarnings_SilentForSite()
{
@@ -965,6 +1094,8 @@ public class StartupValidatorTests
Assert.Empty(warnings);
}
/// <summary>Verifies that validation rejects an unknown API key store provider.</summary>
/// <returns>The result.</returns>
[Fact]
public void Validate_RejectsUnknownApiKeyStoreProvider()
{
@@ -976,6 +1107,8 @@ public class StartupValidatorTests
Assert.Contains("ScadaBridge:InboundApi:ApiKeyStore:Provider", ex.Message);
}
/// <summary>Verifies that validation accepts the API key store provider case-insensitively.</summary>
/// <returns>The result.</returns>
[Fact]
public void Validate_AcceptsProviderCaseInsensitively()
{
@@ -987,6 +1120,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that an unknown secret store provider fails validation.</summary>
/// <returns>The result.</returns>
[Fact]
public void UnknownSecretStoreProvider_Fails()
{
@@ -998,6 +1133,8 @@ public class StartupValidatorTests
Assert.Contains("Secrets:Store:Provider", ex.Message);
}
/// <summary>Verifies that validation accepts the secret store provider case-insensitively.</summary>
/// <returns>The result.</returns>
[Fact]
public void Validate_AcceptsSecretStoreProviderCaseInsensitively()
{
@@ -1009,6 +1146,8 @@ public class StartupValidatorTests
Assert.Null(ex);
}
/// <summary>Verifies that multiple errors are all reported.</summary>
/// <returns>The result.</returns>
[Fact]
public void MultipleErrors_AllReported()
{