namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// Host-003: appsettings.Central.json no longer commits database connection /// strings — they are externalised to environment variables. Tests that exercise the /// full Program startup pipeline against the real SQL provider must therefore /// supply the local dev connection string the way a deployment would: via an /// environment variable (Program's configuration builder calls /// AddEnvironmentVariables()). /// /// Also supplies ScadaBridge__InboundApi__ApiKeyPepper so the Central-role /// StartupValidator preflight (added in 1fcc4f5) does not fail for tests that set /// DOTNET_ENVIRONMENT=Central without an explicit pepper env var. /// /// Also supplies ScadaBridge__Database__MachineDataDb so the Central-role /// StartupValidator preflight (reverts Host-008, REQ-HOST-3/4, M2.9 #17) does not /// fail for tests that set DOTNET_ENVIRONMENT=Central without an explicit /// MachineDataDb env var. /// /// G-4 (secrets adoption): appsettings.Central.json now carries /// ${secret:...} references for the ConfigurationDb connection string, the /// LDAP service-account password and the JWT signing key. The pre-host secrets /// expander (Program.cs) resolves those before StartupValidator runs and fails /// closed on an unseeded reference. Supplying the concrete values as whole-key /// environment overrides (the sanctioned rollback path) makes the expander skip /// the tokens entirely — so tests that boot the real Program pipeline do /// not need a seeded secrets store. All vars are restored on Dispose so tests /// stay isolated. /// /// Every var below is process-wide, and the reader — Program's /// AddEnvironmentVariables() — runs at an unpredictable point during host boot. /// Two live instances therefore cannot be allowed to overlap: one's Dispose would /// restore/clear a var while the other's boot is still reading it, and the fail-closed /// secrets expander turns that into a SecretNotFoundException. Serialization via /// is what enforces the invariant; the overlap check in /// the constructor makes a regression fail deterministically instead of intermittently. /// internal sealed class CentralDbTestEnvironment : IDisposable { // Local dev/test database — same credentials the infra docker-compose stack uses. // This is a test fixture value, not a committed production secret. private const string ConfigurationDb = "Server=localhost,1433;Database=ScadaBridgeConfig;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true"; private const string ConfigKey = "ScadaBridge__Database__ConfigurationDb"; private const string MachineDataDb = "Server=localhost,1433;Database=ScadaBridgeMachineData;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true"; private const string MachineDataKey = "ScadaBridge__Database__MachineDataDb"; // Test-only pepper — satisfies the ≥16-char StartupValidator requirement without // committing a real secret. The env-var name uses the double-underscore delimiter // so AddEnvironmentVariables() maps it to ScadaBridge:InboundApi:ApiKeyPepper. internal const string TestPepper = "test-pepper-01234567890"; private const string PepperKey = "ScadaBridge__InboundApi__ApiKeyPepper"; // G-4: whole-key overrides for the two remaining ${secret:...} config references // in appsettings.Central.json (the LDAP service-account password and JWT signing // key). Test-only placeholder values — non-empty, and the JWT key clears the // ≥32-char StartupValidator length rule. Applied via env so the pre-host expander // sees the concrete value and never evaluates the token. private const string LdapServiceAccountPassword = "test-ldap-service-account-password"; private const string LdapPasswordKey = "ScadaBridge__Security__Ldap__ServiceAccountPassword"; private const string JwtSigningKey = "test-signing-key-must-be-at-least-32-chars-long!"; private const string JwtSigningKeyKey = "ScadaBridge__Security__JwtSigningKey"; private readonly string? _previousConfig; private readonly string? _previousMachineData; private readonly string? _previousPepper; private readonly string? _previousLdapPassword; private readonly string? _previousJwtSigningKey; /// /// Number of live instances. Only ever 0 or 1 while every consuming fixture is a /// member of ; see the overlap check below. /// private static int _liveCount; public CentralDbTestEnvironment() { if (Interlocked.Increment(ref _liveCount) != 1) { Interlocked.Decrement(ref _liveCount); throw new InvalidOperationException( $"Two {nameof(CentralDbTestEnvironment)} instances are live at once, so one fixture's " + "teardown can clear the process-wide environment variables another fixture's host boot " + $"is reading. Add the offending test class to the \"{HostBootCollection.Name}\" collection " + $"([Collection({nameof(HostBootCollection)}.Name)]) so xUnit runs it sequentially with the " + "other host-boot fixtures."); } _previousConfig = Environment.GetEnvironmentVariable(ConfigKey); Environment.SetEnvironmentVariable(ConfigKey, ConfigurationDb); _previousMachineData = Environment.GetEnvironmentVariable(MachineDataKey); Environment.SetEnvironmentVariable(MachineDataKey, MachineDataDb); _previousPepper = Environment.GetEnvironmentVariable(PepperKey); Environment.SetEnvironmentVariable(PepperKey, TestPepper); _previousLdapPassword = Environment.GetEnvironmentVariable(LdapPasswordKey); Environment.SetEnvironmentVariable(LdapPasswordKey, LdapServiceAccountPassword); _previousJwtSigningKey = Environment.GetEnvironmentVariable(JwtSigningKeyKey); Environment.SetEnvironmentVariable(JwtSigningKeyKey, JwtSigningKey); } public void Dispose() { Environment.SetEnvironmentVariable(ConfigKey, _previousConfig); Environment.SetEnvironmentVariable(MachineDataKey, _previousMachineData); Environment.SetEnvironmentVariable(PepperKey, _previousPepper); Environment.SetEnvironmentVariable(LdapPasswordKey, _previousLdapPassword); Environment.SetEnvironmentVariable(JwtSigningKeyKey, _previousJwtSigningKey); Interlocked.Decrement(ref _liveCount); } }