Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/RealOpcUaClientEndpointRewriteTests.cs
T
Joseph Doherty 86d129de6c fix(dcl): harden endpoint host-rewrite — IPv6 brackets + portless URLs
Code review of a1abbff7 found two edge cases: a discovery/advertised URL with no
explicit port emitted a malformed ':-1' (opc.tcp has no Uri default port), and an
IPv6 literal host could lose/double its brackets. Omit the port when neither URL
carries one; bracket an IPv6 host only when missing. +4 tests (12 total).
2026-07-23 16:58:44 -04:00

103 lines
3.5 KiB
C#

using Opc.Ua;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters;
/// <summary>
/// Covers <see cref="RealOpcUaClient.RewriteEndpointHostForReachability"/> — 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.
/// </summary>
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");
}
}