Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Configuration/SecretsPreHostContentRootSourceTests.cs
T
Joseph Doherty b037cc7ac4
v2-ci / build (push) Successful in 4m42s
v2-ci / unit-tests (push) Failing after 15m19s
chore(secrets): adopt ZB.MOM.WW.Secrets 0.6.2 — fail-closed store-path validation
Four-minor jump 0.2.3 → 0.6.2. The release line exists because a deploy
renaming the app directory took the MxGateway credential store with it
(2026-08-09, two silent days): Secrets:SqlitePath must now be absolute AND
outside the content root, rejected at boot rather than relocated. 0.6.2 over
0.6.1 is message-only (the rejection text now teaches the mounted-volume
case and forecloses the override hunt) — the live rig evidence below was
gathered at 0.6.1 and the validator behavior is identical.

Adoption changes beyond the pins:

- appsettings.json drops the relative "otopcua-secrets.db" (now boot-fatal);
  dev gets the library's absolute per-user default, deployments supply their
  own (wonder already pins an absolute out-of-root path in the service env).
- Program.cs's pre-host ${secret:} expander now calls the FOUR-argument
  AddZbSecrets with builder.Environment.ContentRootPath. This call site is
  the actual fix, not the pin: the throwaway container has no
  IHostEnvironment, and without the explicit root the under-content-root
  rule is skipped at exactly the moment the migrator creates the store file
  (the 0.6.0 defect). Do NOT simplify back to the 3-argument overload — it
  compiles and silently reopens the gap. Pinned by a Program.cs source-scan
  test AND a runtime falsification control that RUNS the migration and
  demonstrably creates the store at the rejected path under the 3-arg form.
- docker-dev: Secrets__SqlitePath moves /app/data → /data via a SECOND mount
  of the same per-node volume (same file at the volume root, so seeded
  stores carry over) because /app is the container's content root and the
  new rule correctly rejects it. LocalDb stays on /app/data (secrets-only
  rule).
- SecretsStorePathValidationTests: positive-control probes, artifact-first
  (relative and in-content-root paths must FAIL leaving no file; outside-
  root passes; the no-explicit-root control proves the file assertions can
  fail) — a clean boot is equally consistent with the validator never being
  wired.

Live-verified on the six-node docker-dev rig (at 0.6.1): all nodes boot
under the new validation with stores at /data; an AdminUI-written secret
converged into BOTH central nodes' independent stores; site-a held only its
own pair's secrets (Phase 6 pair-local isolation intact). Replicator wire
behavior unchanged 0.3.0–0.6.x per CHANGELOG; registration-graph pins and
the full suite pass.
2026-08-11 09:06:43 -04:00

49 lines
2.4 KiB
C#

using System.Text.RegularExpressions;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
/// <summary>
/// Pins the one <c>AddZbSecrets</c> call that no executable test can reach: the pre-host
/// <c>${secret:}</c> expander in <c>Program.cs</c> (top-level statements). That call composes the
/// secrets subsystem in a throwaway <c>ServiceCollection</c> with no <c>IHostEnvironment</c>, so
/// it MUST pass the content root explicitly (the Secrets 0.6.1 four-argument overload) — dropped
/// back to the three-argument form, the under-content-root store rule silently vanishes at
/// exactly the moment the migrator creates the store file, and a rejected path gains an empty
/// database before the real host aborts (the 0.6.0 defect; the runtime half of this pin is
/// <c>SecretsStorePathValidationTests.Without_the_explicit_content_root_the_prehost_hole_reopens</c>
/// in Host.IntegrationTests).
/// </summary>
public sealed class SecretsPreHostContentRootSourceTests
{
[Fact]
public void The_prehost_expander_passes_the_content_root_explicitly()
{
var source = ReadProgramSource();
// The throwaway container's AddZbSecrets call, allowing for formatting drift but requiring
// the third argument to be the builder's content root.
Regex.IsMatch(
source,
@"new\s+ServiceCollection\(\)\s*\.\s*AddZbSecrets\(\s*builder\.Configuration\s*,\s*""Secrets""\s*,\s*builder\.Environment\.ContentRootPath\s*\)",
RegexOptions.Singleline)
.ShouldBeTrue(
"Program.cs's pre-host secrets expander must call the four-argument AddZbSecrets "
+ "overload with builder.Environment.ContentRootPath — the throwaway container has no "
+ "IHostEnvironment, and without the explicit root the under-content-root store rule "
+ "is skipped while the migrator creates the store file");
}
private static string ReadProgramSource()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx")))
dir = dir.Parent!;
dir.ShouldNotBeNull();
return File.ReadAllText(Path.Combine(
dir.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.Host", "Program.cs"));
}
}