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");
}
}