513fa1aa16
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.
127 lines
5.6 KiB
C#
127 lines
5.6 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Guards the content-root argument on the Layer-A pre-host <c>${secret:}</c> expander
|
|
/// (<see cref="SecretsRegistration.AddPreHostSqliteExpander"/>, called from <c>Program.cs</c>).
|
|
///
|
|
/// <para>
|
|
/// The argument is load-bearing and its absence is SILENT. The expander composes secrets into a
|
|
/// throwaway <see cref="ServiceCollection"/> with no <c>IHostEnvironment</c>; 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 <c>IHostEnvironment</c>.
|
|
/// 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.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <b>These tests must resolve and run <see cref="SqliteSecretsStoreMigrator"/>.</b> 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. <see cref="Control_ValidPath_ActuallyCreatesAStore"/>
|
|
/// exists to prove the harness really does create files.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <b>Assertion order is deliberate: artifact first, outcome second.</b> 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.
|
|
/// </para>
|
|
/// </summary>
|
|
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<string, string?>
|
|
{
|
|
["Secrets:SqlitePath"] = sqlitePath,
|
|
["Secrets:MasterKey:Source"] = "Environment",
|
|
["Secrets:MasterKey:EnvVarName"] = "ZB_SECRETS_MASTER_KEY",
|
|
}).Build();
|
|
|
|
/// <summary>
|
|
/// Mimics the pre-host expander exactly: throwaway collection, no IHostEnvironment,
|
|
/// register, then run the migrator — which is what creates the database.
|
|
/// </summary>
|
|
private static async Task<Exception?> 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<SqliteSecretsStoreMigrator>().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));
|
|
}
|
|
|
|
/// <summary>Verifies, as a control, that a valid path actually creates a secrets store.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>Verifies that a path inside the content root is rejected and leaves no store behind.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that a relative path is rejected and leaves no store behind.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
}
|