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);
}
}