using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests; /// /// Gitea #806 — the AdminUI browse picker used the endpoint URL the server ADVERTISED in GetEndpoints verbatim, so /// behind NAT / a container port publish it dialled the server's private hostname + internal port and failed DNS /// (opc.tcp://otopcua-sdk1-browser-nat-parity:50000/) where the runtime driver, which rewrites to the /// configured host/port, connected. The browser now selects through the driver's shared /// ; these tests pin the browse seam's behaviour, including that /// the security choice is not altered by the rewrite. /// [Trait("Category", "Unit")] public sealed class OpcUaClientBrowserEndpointSelectionTests { private static EndpointDescription Ep(string url, MessageSecurityMode mode, string policyUri, byte[]? cert = null) => new() { EndpointUrl = url, SecurityMode = mode, SecurityPolicyUri = policyUri, ServerCertificate = cert is null ? default : new ByteString(cert), }; [Fact] public void Advertised_private_host_and_port_are_replaced_by_the_configured_ones() { var advertised = new[] { Ep("opc.tcp://otopcua-sdk1-browser-nat-parity:50000/", MessageSecurityMode.None, SecurityPolicies.None) }; var picked = OpcUaClientDriverBrowser.SelectBrowseEndpoint( advertised, "opc.tcp://127.0.0.1:32772", OpcUaSecurityPolicy.None, OpcUaSecurityMode.None); picked.EndpointUrl.ShouldBe("opc.tcp://127.0.0.1:32772/"); } [Fact] public void The_advertised_path_is_kept() { var advertised = new[] { Ep("opc.tcp://internal:4840/Quickstarts/ReferenceServer", MessageSecurityMode.None, SecurityPolicies.None) }; OpcUaClientDriverBrowser.SelectBrowseEndpoint( advertised, "opc.tcp://gateway.example:14840/", OpcUaSecurityPolicy.None, OpcUaSecurityMode.None) .EndpointUrl.ShouldBe("opc.tcp://gateway.example:14840/Quickstarts/ReferenceServer"); } [Fact] public void The_browser_and_the_driver_choose_the_same_url() { const string advertisedUrl = "opc.tcp://internal-host:4840/OtOpcUa"; const string configured = "opc.tcp://[fd00::1]:54840"; var picked = OpcUaClientDriverBrowser.SelectBrowseEndpoint( [Ep(advertisedUrl, MessageSecurityMode.None, SecurityPolicies.None)], configured, OpcUaSecurityPolicy.None, OpcUaSecurityMode.None); picked.EndpointUrl.ShouldBe(OpcUaEndpointUrlRewriter.Rewrite(advertisedUrl, configured)); picked.EndpointUrl.ShouldBe("opc.tcp://[fd00::1]:54840/OtOpcUa"); } [Fact] public void Security_selection_and_server_certificate_are_unchanged_by_the_rewrite() { using var rsa = RSA.Create(2048); using var cert = new CertificateRequest("CN=srv", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) .CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); var der = cert.RawData; var advertised = new[] { Ep("opc.tcp://internal:50000/", MessageSecurityMode.None, SecurityPolicies.None), Ep("opc.tcp://internal:50000/", MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, der), Ep("opc.tcp://internal:50000/", MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15, der), Ep("opc.tcp://internal:50000/", MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256, der), }; var picked = OpcUaClientDriverBrowser.SelectBrowseEndpoint( advertised, "opc.tcp://10.100.0.35:32772", OpcUaSecurityPolicy.Basic256Sha256, OpcUaSecurityMode.SignAndEncrypt); picked.ShouldBeSameAs(advertised[3]); picked.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256); picked.SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt); picked.ServerCertificate.ToArray().ShouldBe(der); picked.EndpointUrl.ShouldBe("opc.tcp://10.100.0.35:32772/"); } [Fact] public void An_unadvertised_policy_mode_pair_still_fails_loudly() { var advertised = new[] { Ep("opc.tcp://internal:50000/", MessageSecurityMode.None, SecurityPolicies.None) }; var ex = Should.Throw(() => OpcUaClientDriverBrowser.SelectBrowseEndpoint( advertised, "opc.tcp://127.0.0.1:32772", OpcUaSecurityPolicy.Basic256Sha256, OpcUaSecurityMode.SignAndEncrypt)); ex.Message.ShouldContain("Basic256Sha256"); ex.Message.ShouldContain("None/None"); } [Fact] public void A_different_transport_scheme_is_left_alone() { var advertised = new[] { Ep("https://internal:443/ua", MessageSecurityMode.None, SecurityPolicies.None) }; OpcUaClientDriverBrowser.SelectBrowseEndpoint( advertised, "opc.tcp://127.0.0.1:32772", OpcUaSecurityPolicy.None, OpcUaSecurityMode.None) .EndpointUrl.ShouldBe("https://internal:443/ua"); } }