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));
}
}