using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.ScadaBridge.Host; using ZB.MOM.WW.Secrets.Sqlite; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// Guards the content-root argument on the Layer-A pre-host ${secret:} expander /// (, called from Program.cs). /// /// /// The argument is load-bearing and its absence is SILENT. The expander composes secrets into a /// throwaway with no IHostEnvironment; without an explicit /// content root the under-content-root rule does not run, the migrator creates the store at the /// rejected path, and the boot then fails once the real host registers IHostEnvironment. /// The leftover empty database is the "the file is there, it's just empty" artifact that made the /// 2026-08-09 MxGateway outage read as corruption rather than as a deployment error. /// /// /// /// These tests must resolve and run . Resolving /// only the connection factory would make every file assertion here vacuous — that constructor /// never touches the filesystem, so no store is created whether the guard runs or not, and the /// tests would pass for the wrong reason. /// exists to prove the harness really does create files. /// /// /// /// Assertion order is deliberate: artifact first, outcome second. Asserting "it threw" /// first masks the real finding — a regression that stops the throw but still creates the store /// would report "no exception was thrown" and never mention the database sitting at the rejected /// path, which IS the defect. Verified by simulating the regression (dropping to the 3-argument /// overload) and confirming these fail on the FILE assertion, not the exception one. /// /// public class PreHostSecretsContentRootTests { private static string NewDir() => Path.Combine(Path.GetTempPath(), "prehost-secrets-" + Guid.NewGuid().ToString("N")); private static IConfiguration Config(string sqlitePath) => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Secrets:SqlitePath"] = sqlitePath, ["Secrets:MasterKey:Source"] = "Environment", ["Secrets:MasterKey:EnvVarName"] = "ZB_SECRETS_MASTER_KEY", }).Build(); /// /// Mimics the pre-host expander exactly: throwaway collection, no IHostEnvironment, /// register, then run the migrator — which is what creates the database. /// private static async Task ComposeAndMigrate(string contentRoot, string sqlitePath) { Directory.CreateDirectory(contentRoot); var services = new ServiceCollection(); services.AddLogging(); return await Record.ExceptionAsync(async () => { services.AddPreHostSqliteExpander(Config(sqlitePath), contentRoot); await using var provider = services.BuildServiceProvider(); await provider.GetRequiredService().MigrateAsync(default); }); } private static void AssertNoStoreAt(string path) { string[] leftovers = new[] { path, path + "-wal", path + "-shm" } .Where(File.Exists) .ToArray(); Assert.True( leftovers.Length == 0, "A rejected path must not be left holding a store file — an empty database at the " + "rejected path is exactly the artifact this guard exists to prevent. Found: " + string.Join(", ", leftovers)); } [Fact] public async Task Control_ValidPath_ActuallyCreatesAStore() { // Proves the harness is capable of creating a store, so that the "no file" assertions // in the other two tests are meaningful rather than vacuous. var storeDir = NewDir(); Directory.CreateDirectory(storeDir); var path = Path.Combine(storeDir, "secrets.db"); var thrown = await ComposeAndMigrate(NewDir(), path); Assert.Null(thrown); Assert.True(File.Exists(path), "the control must create a store, or the guards prove nothing"); } [Fact] public async Task PathInsideContentRoot_IsRejected_AndLeavesNoStore() { var contentRoot = NewDir(); var path = Path.Combine(contentRoot, "data", "scadabridge-secrets.db"); var thrown = await ComposeAndMigrate(contentRoot, path); AssertNoStoreAt(path); Assert.NotNull(thrown); } [Fact] public async Task RelativePath_IsRejected_AndLeavesNoStore() { // Unique name: a stale scadabridge-secrets.db from an earlier run under the old relative // default sits in the test output directory and would make this assertion lie. var path = "prehost-probe-" + Guid.NewGuid().ToString("N") + ".db"; var thrown = await ComposeAndMigrate(NewDir(), path); AssertNoStoreAt(path); Assert.NotNull(thrown); } }