using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.Secrets.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests; /// /// Review 2026-10-21 R06:O-5 — the AdminUI browse refused Certificate auth "in v1", so a /// certificate-authenticated upstream could not be browsed at all. The browse now builds its identity through /// the same the runtime driver uses (incl. the secret: resolution /// of the credential fields, fail-closed), so it presents exactly the credentials the deployed driver will. /// [Trait("Category", "Unit")] public sealed class OpcUaClientBrowserCertificateAuthTests { private sealed class OneSecret(string name, string value) : ISecretResolver { public Task GetAsync(SecretName secretName, CancellationToken ct) => Task.FromResult(secretName.Value == name ? value : null); } private static string WritePfx(string password) { using var rsa = RSA.Create(2048); var req = new CertificateRequest("CN=OpcUaClientBrowserCertAuthTests", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); var path = Path.Combine(Path.GetTempPath(), $"opcua-browse-cert-{Guid.NewGuid():N}.pfx"); File.WriteAllBytes(path, cert.Export(X509ContentType.Pfx, password)); return path; } /// Certificate auth is no longer refused: a missing cert path fails on the certificate itself. [Fact] public async Task OpenAsync_with_certificate_auth_fails_on_the_certificate_not_a_refusal() { var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":"Certificate"}"""; var ex = await Should.ThrowAsync( () => new OpcUaClientDriverBrowser().OpenAsync(json, TestContext.Current.CancellationToken)); ex.Message.ShouldContain("UserCertificatePath"); ex.Message.ShouldNotContain("does not support"); } /// A valid PFX whose password is a secret: ref resolves through the injected store. [Fact] public async Task A_certificate_identity_is_built_with_its_secret_password_resolved() { var path = WritePfx("s3cret-pw"); try { var opts = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Certificate, UserCertificatePath = path, UserCertificatePassword = "secret:opcua/browse-cert", }; var identity = await OpcUaClientDriverBrowser.BuildBrowseUserIdentityAsync( opts, new OneSecret("opcua/browse-cert", "s3cret-pw"), TestContext.Current.CancellationToken); identity.TokenType.ShouldBe(UserTokenType.Certificate); } finally { try { File.Delete(path); } catch { /* best-effort */ } } } /// With no secret store, a secret: ref fails closed instead of being sent as the literal. [Fact] public async Task A_secret_ref_with_no_secret_store_fails_closed() { var opts = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Username, Username = "op", Password = "secret:opcua/pw", }; var ex = await Should.ThrowAsync(() => OpcUaClientDriverBrowser.BuildBrowseUserIdentityAsync(opts, null, TestContext.Current.CancellationToken)); ex.Message.ShouldContain("fail-closed"); } /// Positive control: a literal password is used as-is (no store needed). [Fact] public async Task A_literal_username_password_needs_no_store() { var opts = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Username, Username = "op", Password = "pw" }; var identity = await OpcUaClientDriverBrowser.BuildBrowseUserIdentityAsync(opts, null, TestContext.Current.CancellationToken); identity.TokenType.ShouldBe(UserTokenType.UserName); } }