using Opc.Ua; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters; /// /// Covers — the fix for OPC UA /// servers (e.g. OtOpcUa v3) that advertise an unroutable base address (a 0.0.0.0 wildcard bind or /// an internal container/NAT hostname). The session must dial the host the operator reached, not /// the advertised one, while preserving the advertised scheme + path. /// public class RealOpcUaClientEndpointRewriteTests { private static string? Rewrite(string advertised, string discovery) { var ep = new EndpointDescription(advertised); RealOpcUaClient.RewriteEndpointHostForReachability(ep, discovery); return ep.EndpointUrl; } [Fact] public void Rewrites_0_0_0_0_advertisement_to_the_reachable_host_preserving_path() { Assert.Equal( "opc.tcp://otopcua-dev-site-a-1-1:4840/OtOpcUa", Rewrite("opc.tcp://0.0.0.0:4840/OtOpcUa", "opc.tcp://otopcua-dev-site-a-1-1:4840/OtOpcUa")); } [Fact] public void Rewrites_internal_nat_hostname_to_the_reachable_host() { Assert.Equal( "opc.tcp://plc.example.com:4840/OtOpcUa", Rewrite("opc.tcp://be5667fab9da:4840/OtOpcUa", "opc.tcp://plc.example.com:4840/OtOpcUa")); } [Fact] public void Rewrites_port_too_when_the_advertised_port_differs() { Assert.Equal( "opc.tcp://gateway:51210/OtOpcUa", Rewrite("opc.tcp://0.0.0.0:4840/OtOpcUa", "opc.tcp://gateway:51210/OtOpcUa")); } [Fact] public void Leaves_an_already_reachable_advertisement_untouched() { // opc-plc advertises its real container name — must not be rewritten. Assert.Equal( "opc.tcp://scadabridge-opcua:50000", Rewrite("opc.tcp://scadabridge-opcua:50000", "opc.tcp://scadabridge-opcua:50000")); } [Fact] public void Handles_authority_only_advertisement_without_appending_a_trailing_slash() { Assert.Equal( "opc.tcp://host-b:4840", Rewrite("opc.tcp://0.0.0.0:4840", "opc.tcp://host-b:4840")); } [Fact] public void Re_wraps_an_ipv6_reachable_host_in_brackets() { Assert.Equal( "opc.tcp://[::1]:4840/OtOpcUa", Rewrite("opc.tcp://0.0.0.0:4840/OtOpcUa", "opc.tcp://[::1]:4840/OtOpcUa")); } [Fact] public void Emits_no_port_when_neither_url_carries_one() { // opc.tcp has no default port, so Uri.Port is -1 on both — must not emit ":-1". Assert.Equal( "opc.tcp://myhost/UaServer", Rewrite("opc.tcp://0.0.0.0/UaServer", "opc.tcp://myhost/UaServer")); } [Fact] public void Uses_the_reachable_port_when_the_advertisement_omits_one() { Assert.Equal( "opc.tcp://myhost:4840/UaServer", Rewrite("opc.tcp://0.0.0.0/UaServer", "opc.tcp://myhost:4840/UaServer")); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData("not-a-uri")] public void Leaves_endpoint_unchanged_when_discovery_url_is_unusable(string discovery) { Assert.Equal("opc.tcp://0.0.0.0:4840/OtOpcUa", Rewrite("opc.tcp://0.0.0.0:4840/OtOpcUa", discovery)); } [Fact] public void Null_endpoint_is_a_no_op() { // Must not throw. RealOpcUaClient.RewriteEndpointHostForReachability(null, "opc.tcp://host:4840"); } }