Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/RawTreeServiceSecretRefRoundTripTests.cs
T
Joseph Doherty b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via
VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask,
SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass
TestContext.Current.CancellationToken. Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 08:57:33 -05:00

111 lines
5.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
using ZB.MOM.WW.OtOpcUa.Configuration;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// <summary>
/// Secrets-adoption (G-2c) regression guard: the AdminUI driver-config editor persists <c>DriverConfig</c>
/// as an OPAQUE JSON string through <see cref="RawTreeService"/> (see <c>UpdateDriverAsync</c> /
/// <c>CreateDriverAsync</c> — <c>entity.DriverConfig = driverConfigJson</c> verbatim, and
/// <c>LoadDriverForEditAsync</c> reads it back verbatim). A <c>secret:</c>/<c>env:</c>/<c>file:</c>
/// reference stored in a driver secret field (OpcUaClient <c>Password</c>/<c>UserCertificatePassword</c>,
/// Galaxy <c>Gateway.ApiKeySecretRef</c>) MUST survive save→reload as the literal ref string — the save
/// path must never resolve-then-resave cleartext (which would re-leak the secret into the config store and
/// defeat G-2). Secret resolution belongs ONLY at driver-instantiation / Test-connect time (Tasks 7-8),
/// never on the AdminUI persistence path — and indeed <see cref="RawTreeService"/> takes no secret resolver
/// (its only dependency is the DbContext factory), so resolution is structurally impossible here.
/// <para>
/// Reuses the shared <see cref="RawTreeTestDb"/> InMemory fixture, matching the WP3 driver-config tests.
/// </para>
/// </summary>
[Trait("Category", "Unit")]
public sealed class RawTreeServiceSecretRefRoundTripTests
{
private static (RawTreeService Service, string DbName) Seeded()
{
var name = RawTreeTestDb.SeedFresh();
return (new RawTreeService(RawTreeTestDb.Factory(name)), name);
}
private static byte[] RowVersionOf(string dbName, string driverId)
{
using var db = RawTreeTestDb.CreateNamed(dbName);
return db.DriverInstances.Single(d => d.DriverInstanceId == driverId).RowVersion;
}
[Fact]
public async Task UpdateDriver_preserves_OpcUaClient_secret_refs_verbatim_on_save_and_reload()
{
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, RawTreeTestDb.FolderedDriverId);
// The editor emits the raw ref strings the user typed — they must NOT be resolved on save.
const string config =
"{\"Password\":\"secret:opcua/x/pw\",\"UserCertificatePassword\":\"secret:opcua/x/certpw\"}";
var result = await service.UpdateDriverAsync(
RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
// Reload through the same seam the modal uses.
var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId, TestContext.Current.CancellationToken);
reloaded.ShouldNotBeNull();
// Byte-identical: nothing on the save path rewrote the string.
reloaded!.DriverConfig.ShouldBe(config);
// The literal refs survived (not resolved to cleartext, not stripped).
reloaded.DriverConfig.ShouldContain("secret:opcua/x/pw");
reloaded.DriverConfig.ShouldContain("secret:opcua/x/certpw");
// And the raw column agrees with the projection.
using var db = RawTreeTestDb.CreateNamed(dbName);
db.DriverInstances.Single(d => d.DriverInstanceId == RawTreeTestDb.FolderedDriverId)
.DriverConfig.ShouldBe(config);
}
[Fact]
public async Task CreateDriver_preserves_Galaxy_ApiKeySecretRef_verbatim_on_save_and_reload()
{
var (service, dbName) = Seeded();
const string config = "{\"Gateway\":{\"ApiKeySecretRef\":\"secret:galaxy/x/apikey\"}}";
var created = await service.CreateDriverAsync(
RawTreeTestDb.ClusterId, RawTreeTestDb.RootFolderId, "galaxy-1", "Galaxy", config, TestContext.Current.CancellationToken);
created.Ok.ShouldBeTrue();
created.CreatedId.ShouldNotBeNull();
var reloaded = await service.LoadDriverForEditAsync(created.CreatedId!, TestContext.Current.CancellationToken);
reloaded.ShouldNotBeNull();
reloaded!.DriverConfig.ShouldBe(config);
reloaded.DriverConfig.ShouldContain("secret:galaxy/x/apikey");
using var db = RawTreeTestDb.CreateNamed(dbName);
db.DriverInstances.Single(d => d.DriverInstanceId == created.CreatedId)
.DriverConfig.ShouldBe(config);
}
[Fact]
public async Task UpdateDriver_preserves_env_and_file_ref_forms_verbatim()
{
var (service, dbName) = Seeded();
var rv = RowVersionOf(dbName, RawTreeTestDb.FolderedDriverId);
const string config =
"{\"Password\":\"env:OPCUA_PW\",\"UserCertificatePassword\":\"file:/run/secrets/certpw\"}";
var result = await service.UpdateDriverAsync(
RawTreeTestDb.FolderedDriverId, "modbus-1", config, null, rv, TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
var reloaded = await service.LoadDriverForEditAsync(RawTreeTestDb.FolderedDriverId, TestContext.Current.CancellationToken);
reloaded!.DriverConfig.ShouldBe(config);
reloaded.DriverConfig.ShouldContain("env:OPCUA_PW");
reloaded.DriverConfig.ShouldContain("file:/run/secrets/certpw");
}
}