test(client): pin Client.Shared EndpointSelector rewrite behaviour before unifying it (#812)
Reuses the #806 rewrite cases plus the Client.Shared-specific edges (verbatim no-op, case-only host difference, escaped path, loud failure on unusable URLs). Green against the pre-#812 copy: 343/343.
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
|||||||
|
using Opc.Ua;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gitea #812 — pins the Client CLI/UI endpoint-URL rewrite (<see cref="EndpointSelector.SelectBest"/>) case by case,
|
||||||
|
/// reusing the #806 rewrite cases of <c>OpcUaClientEndpointRewriteTests</c> (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
|
||||||
|
/// <c>OpcUaEndpointUrlRewriter</c>, so they are the "behaviour stays identical" proof for the swap.
|
||||||
|
/// </summary>
|
||||||
|
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")]
|
||||||
|
// An IPv6 zone index on the configured host is dropped (UriBuilder behaviour, same in both copies).
|
||||||
|
[InlineData("opc.tcp://internal-host:4840/OtOpcUa", "opc.tcp://[fe80::1%25eth0]:4841", "opc.tcp://[fe80::1]:4841/OtOpcUa")]
|
||||||
|
public void SelectBest_rewrites_exactly_as_before(string advertised, string configured, string expected)
|
||||||
|
{
|
||||||
|
Select(advertised, configured).ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void SelectBest_throws_on_an_endpoint_without_a_url()
|
||||||
|
{
|
||||||
|
Should.Throw<InvalidOperationException>(() => 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<UriFormatException>(() => Select(advertised, "opc.tcp://srv:4840/ua"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectBest_throws_on_an_unparseable_configured_url()
|
||||||
|
{
|
||||||
|
Should.Throw<UriFormatException>(() => Select("opc.tcp://a:4840/x", "not a url"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user