using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
///
/// lmxopcua#834 (follow-up to #815 / #836): the warn-only compatibility window is over. A RUNNING OpcUaClient
/// driver whose stored config carries an out-of-namespace credential reference, or a user-certificate path outside
/// the node's certificate directory, now refuses it at resolve time in the session open
/// (, before any store / environment read and before any endpoint is
/// dialled) — exactly as save, deploy, Test Connect and browse already did.
///
[Trait("Category", "Unit")]
public sealed class OpcUaClientDriverCredentialScopeTests
{
private const string PlainPassword = "resolved-plaintext";
private static OpcUaClientDriverOptions Username(string password) => new()
{
EndpointUrl = "opc.tcp://127.0.0.1:1",
AuthType = OpcUaAuthType.Username,
Username = "plant-user",
Password = password,
};
///
/// Out-of-namespace values that the pre-#834 running driver resolved with the old semantics: a secret:
/// outside opcuaclient/, a case-variant SECRET: prefix, and an env: outside
/// OTOPCUA_OPCUACLIENT_ (which it sent verbatim as a literal password).
///
[Theory]
[InlineData("secret:otopcua/configdb-password")]
[InlineData("SECRET:opcuaclient/inst/password")]
[InlineData("env:NOT_A_REF")]
public async Task A_running_driver_refuses_an_out_of_namespace_password_ref_before_reading_or_dialling(string reference)
{
var store = new CountingResolver();
using var driver = new OpcUaClientDriver(Username(reference), "ouc-834", secretResolver: store);
var opened = 0;
driver.OpenSessionForTest = (_, _) =>
{
opened++;
throw new InvalidOperationException("no server in this test");
};
var ex = await Should.ThrowAsync(
() => driver.InitializeAsync("{}", TestContext.Current.CancellationToken));
ex.Message.ShouldContain("Password");
ex.Message.ShouldNotContain(PlainPassword);
store.Calls.ShouldBe(0);
opened.ShouldBe(0, "the credential is refused before any endpoint is dialled");
driver.GetHealth().State.ShouldBe(DriverState.Faulted);
}
/// lmxopcua#836: a fully-qualified (pre-#836) user-certificate path is refused, not loaded.
[Fact]
public async Task A_running_driver_refuses_an_absolute_user_certificate_path()
{
var abs = Path.Combine(Path.GetTempPath(), $"ouc-834-{Guid.NewGuid():N}.pfx");
var options = new OpcUaClientDriverOptions
{
EndpointUrl = "opc.tcp://127.0.0.1:1",
AuthType = OpcUaAuthType.Certificate,
UserCertificatePath = abs,
UserCertificatePassword = "pw",
};
using var driver = new OpcUaClientDriver(options, "ouc-834-cert", secretResolver: new CountingResolver());
var opened = 0;
driver.OpenSessionForTest = (_, _) =>
{
opened++;
throw new InvalidOperationException("no server in this test");
};
await Should.ThrowAsync(
() => driver.InitializeAsync("{}", TestContext.Current.CancellationToken));
opened.ShouldBe(0);
}
/// Positive control: a namespaced reference resolves and the session open is reached.
[Fact]
public async Task A_namespaced_password_ref_resolves_and_the_session_open_is_reached()
{
var store = new CountingResolver();
using var driver = new OpcUaClientDriver(
Username("secret:opcuaclient/inst/password"), "ouc-834-ok", secretResolver: store);
var opened = 0;
driver.OpenSessionForTest = (_, _) =>
{
opened++;
throw new InvalidOperationException("no server in this test");
};
await Should.ThrowAsync(
() => driver.InitializeAsync("{}", TestContext.Current.CancellationToken));
store.Calls.ShouldBe(1);
opened.ShouldBe(1);
}
private sealed class CountingResolver : ISecretResolver
{
public int Calls { get; private set; }
public Task GetAsync(SecretName name, CancellationToken ct)
{
Calls++;
return Task.FromResult(PlainPassword);
}
}
}