using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; [Trait("Category", "Unit")] public sealed class OpcUaClientFailoverTests { [Fact] public void ResolveEndpointCandidates_prefers_EndpointUrls_when_provided() { var opts = new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://fallback:4840", EndpointUrls = ["opc.tcp://primary:4840", "opc.tcp://backup:4841"], }; var list = OpcUaClientDriver.ResolveEndpointCandidates(opts); list.Count.ShouldBe(2); list[0].ShouldBe("opc.tcp://primary:4840"); list[1].ShouldBe("opc.tcp://backup:4841"); } [Fact] public void ResolveEndpointCandidates_falls_back_to_single_EndpointUrl_when_list_empty() { var opts = new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://only:4840" }; var list = OpcUaClientDriver.ResolveEndpointCandidates(opts); list.Count.ShouldBe(1); list[0].ShouldBe("opc.tcp://only:4840"); } [Fact] public void ResolveEndpointCandidates_empty_list_treated_as_fallback_to_EndpointUrl() { // Explicit empty list should still fall back to the single-URL shortcut rather than // producing a zero-candidate sweep that would immediately throw with no URLs tried. var opts = new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://single:4840", EndpointUrls = [], }; OpcUaClientDriver.ResolveEndpointCandidates(opts).Count.ShouldBe(1); } [Fact] public void HostName_uses_first_candidate_before_connect() { var opts = new OpcUaClientDriverOptions { EndpointUrls = ["opc.tcp://primary:4840", "opc.tcp://backup:4841"], }; using var drv = new OpcUaClientDriver(opts, "opcua-host"); drv.HostName.ShouldBe("opc.tcp://primary:4840", "pre-connect the dashboard should show the first candidate URL so operators can link back"); } [Fact] public async Task Initialize_against_all_unreachable_endpoints_throws_AggregateException_listing_each() { // Port 1 + port 2 + port 3 on loopback are all guaranteed closed (TCP RST immediate). // Failover sweep should attempt all three and throw AggregateException naming each URL // so operators see exactly which candidates were tried. var opts = new OpcUaClientDriverOptions { EndpointUrls = ["opc.tcp://127.0.0.1:1", "opc.tcp://127.0.0.1:2", "opc.tcp://127.0.0.1:3"], PerEndpointConnectTimeout = TimeSpan.FromMilliseconds(500), Timeout = TimeSpan.FromMilliseconds(500), AutoAcceptCertificates = true, }; using var drv = new OpcUaClientDriver(opts, "opcua-failover"); var ex = await Should.ThrowAsync(async () => await drv.InitializeAsync("{}", TestContext.Current.CancellationToken)); ex.Message.ShouldContain("127.0.0.1:1"); ex.Message.ShouldContain("127.0.0.1:2"); ex.Message.ShouldContain("127.0.0.1:3"); drv.GetHealth().State.ShouldBe(DriverState.Faulted); } }