Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/PreHostSecretsContentRootTests.cs
T
Joseph Doherty 513fa1aa16 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.
2026-10-07 15:05:49 -04:00

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