using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters; namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters; /// /// Gitea #812 — pins the Client CLI/UI endpoint-URL rewrite () case by case, /// reusing the #806 rewrite cases of OpcUaClientEndpointRewriteTests (Driver.OpcUaClient.Tests). These were /// written and run green against Client.Shared's own pre-#812 copy of the rule, BEFORE it was swapped for the shared /// OpcUaEndpointUrlRewriter, so they are the "behaviour stays identical" proof for the swap. /// public sealed class EndpointSelectorRewriteParityTests { private static string Select(string? advertised, string configured) { var ep = new EndpointDescription { EndpointUrl = advertised, SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None, }; return EndpointSelector.SelectBest([ep], configured, MessageSecurityMode.None).EndpointUrl!; } [Theory] // #806 cases (exact strings shared with the driver's rewriter tests). [InlineData("opc.tcp://internal-host:4840/OtOpcUa", "opc.tcp://10.1.2.3:54840/OtOpcUa", "opc.tcp://10.1.2.3:54840/OtOpcUa")] [InlineData("opc.tcp://otopcua-sdk1-browser-nat-parity:50000/", "opc.tcp://127.0.0.1:32772", "opc.tcp://127.0.0.1:32772/")] [InlineData("opc.tcp://plc-a:4840/Server/Ua", "opc.tcp://gateway:6000/", "opc.tcp://gateway:6000/Server/Ua")] [InlineData("opc.tcp://127.0.0.1:4840/OtOpcUa", "opc.tcp://127.0.0.1:61000/OtOpcUa", "opc.tcp://127.0.0.1:61000/OtOpcUa")] [InlineData("opc.tcp://plc-a:4840/Server", "opc.tcp://plc-a:4840/Server", "opc.tcp://plc-a:4840/Server")] [InlineData("opc.tcp://internal-host:62541/Server", "opc.tcp://gateway/Server", "opc.tcp://gateway:62541/Server")] [InlineData("opc.tcp://internal-host:4840/OtOpcUa", "opc.tcp://[fd00::1]:54840/OtOpcUa", "opc.tcp://[fd00::1]:54840/OtOpcUa")] [InlineData("opc.tcp://[fd00::7]:4840/Ua", "opc.tcp://gateway:6000", "opc.tcp://gateway:6000/Ua")] [InlineData("https://srv:443/ua", "opc.tcp://srv:4840/ua", "https://srv:443/ua")] // The #813 NAT fixture's shape: private .invalid host AND a different internal port. [InlineData("opc.tcp://otopcua-opc-plc-nat-private.invalid:50000", "opc.tcp://10.100.0.35:50010", "opc.tcp://10.100.0.35:50010/")] // Client.Shared-specific: a no-op rewrite returns the advertised string VERBATIM (no trailing slash added) … [InlineData("opc.tcp://server:4840", "opc.tcp://server:4840", "opc.tcp://server:4840")] // … and a case-only host difference is not a rewrite (DNS is case-insensitive; the advertised spelling stays). [InlineData("opc.tcp://PLC-A:4840/Server", "OPC.TCP://plc-a:4840/", "opc.tcp://PLC-A:4840/Server")] [InlineData("opc.tcp://plc-a:4840/Server", "opc.tcp://PLC-A:4840/", "opc.tcp://plc-a:4840/Server")] // A real rewrite keeps the advertised path's percent-escaping intact. [InlineData("opc.tcp://a:4840/x%20y?q=1", "opc.tcp://b:1/", "opc.tcp://b:1/x%20y?q=1")] // #819: an IPv6 zone index on the configured host is KEPT, verbatim (it used to be dropped — UriBuilder behaviour, // preserved deliberately through #812 — leaving link-local endpoints unroutable). [InlineData("opc.tcp://internal-host:4840/OtOpcUa", "opc.tcp://[fe80::1%25eth0]:4841", "opc.tcp://[fe80::1%25eth0]:4841/OtOpcUa")] [InlineData("opc.tcp://internal-host:4840/OtOpcUa", "opc.tcp://[fe80::1%eth0]:4841", "opc.tcp://[fe80::1%eth0]:4841/OtOpcUa")] public void SelectBest_rewrites_exactly_as_before(string advertised, string configured, string expected) { Select(advertised, configured).ShouldBe(expected); } /// /// Client.Shared fails LOUDLY on an unusable URL, where the driver/browser rewriter falls back to the configured /// URL. The CLI/UI is an interactive diagnostic tool talking to whatever the operator typed, so a server /// advertising garbage is reported rather than papered over. Kept as call-site validation in #812. /// [Fact] public void SelectBest_throws_on_an_endpoint_without_a_url() { Should.Throw(() => Select(null, "opc.tcp://srv:4840/ua")) .Message.ShouldContain("no URL"); } [Theory] [InlineData("")] [InlineData("not a url")] public void SelectBest_throws_on_an_unparseable_advertised_url(string advertised) { Should.Throw(() => Select(advertised, "opc.tcp://srv:4840/ua")); } [Fact] public void SelectBest_throws_on_an_unparseable_configured_url() { Should.Throw(() => Select("opc.tcp://a:4840/x", "not a url")); } }