using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters; namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters; /// /// Regression tests for the pure best-endpoint selection logic extracted from /// (Client.Shared-011). The selector picks the best /// endpoint matching the requested security mode (preferring Basic256Sha256) and rewrites /// the discovered endpoint URL hostname to match the operator-supplied URL so internal DNS /// hostnames in discovery responses do not leak into the session. /// public class EndpointSelectorTests { private static EndpointDescription Ep(MessageSecurityMode mode, string policy, string url) { return new EndpointDescription { EndpointUrl = url, SecurityMode = mode, SecurityPolicyUri = policy, }; } /// /// Verifies that the selector returns the only endpoint matching the requested /// security mode even when other endpoints with different modes are present. /// [Fact] public void SelectBest_PicksMatchingSecurityMode() { var endpoints = new[] { Ep(MessageSecurityMode.None, SecurityPolicies.None, "opc.tcp://server:4840"), Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840"), Ep(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840"), }; var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign); best.SecurityMode.ShouldBe(MessageSecurityMode.Sign); best.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256); } /// /// Verifies that when multiple endpoints match the requested mode, Basic256Sha256 wins /// over weaker policies — even when Basic256Sha256 is not the first encountered. /// [Fact] public void SelectBest_PrefersBasic256Sha256WhenMultipleMatch() { var endpoints = new[] { Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15, "opc.tcp://server:4840"), Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840"), Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256, "opc.tcp://server:4840"), }; var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign); best.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256); } /// /// Verifies that the selector falls back to the first matching endpoint when no /// Basic256Sha256 endpoint is advertised for the requested security mode. /// [Fact] public void SelectBest_FallsBackToFirstMatchWhenNoBasic256Sha256() { var endpoints = new[] { Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15, "opc.tcp://server:4840"), Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256, "opc.tcp://server:4840"), }; var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign); best.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic128Rsa15); } /// /// Verifies that no matching endpoint produces an InvalidOperationException whose /// message lists the available security mode/policy combinations to aid diagnosis. /// [Fact] public void SelectBest_NoMatchingMode_ThrowsWithDiagnostic() { var endpoints = new[] { Ep(MessageSecurityMode.None, SecurityPolicies.None, "opc.tcp://server:4840"), }; var ex = Should.Throw(() => EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.SignAndEncrypt)); ex.Message.ShouldContain("SignAndEncrypt"); ex.Message.ShouldContain("None"); // available endpoint listed in the message } /// /// Verifies that the selector rewrites the discovery-returned hostname to the /// operator-supplied hostname so internal DNS names in the response do not leak /// into the resulting session. /// [Fact] public void SelectBest_RewritesHostToMatchRequestedUrl() { var endpoints = new[] { Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, "opc.tcp://internal-host:4840/UA/Server"), }; var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://external-host:4840", MessageSecurityMode.Sign); new Uri(best.EndpointUrl).Host.ShouldBe("external-host"); } /// /// Verifies that when the discovery host already matches the requested host the /// endpoint URL is left untouched. /// [Fact] public void SelectBest_HostsMatch_LeavesUrlUnchanged() { var endpoints = new[] { Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840/UA/Server"), }; var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign); best.EndpointUrl.ShouldBe("opc.tcp://server:4840/UA/Server"); } /// /// Verifies that a null endpoints argument throws ArgumentNullException rather than /// producing a confusing downstream NullReferenceException. /// [Fact] public void SelectBest_NullEndpoints_Throws() { Should.Throw(() => EndpointSelector.SelectBest(null!, "opc.tcp://server:4840", MessageSecurityMode.None)); } /// /// Verifies that an empty endpointUrl produces ArgumentException so the caller gets /// a clear contract violation rather than a downstream UriFormatException. /// [Fact] public void SelectBest_EmptyEndpointUrl_Throws() { Should.Throw(() => EndpointSelector.SelectBest(Array.Empty(), "", MessageSecurityMode.None)); } }