using System.Net; using System.Net.Sockets; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// The per-driver Transport switch: config parsing, driver factory selection and the probe. [Trait("Category", "Unit")] public sealed class AbLegacyTransportSwitchTests { private const string Device = """{"hostAddress":"ab://10.0.0.1:44818/1,0","plcFamily":"Slc500"}"""; private static string Config(string? transportJson) => $$"""{{{(transportJson is null ? "" : $"\"transport\":{transportJson},")}}"devices":[{{Device}}]}"""; /// Transport values select the factory type; absent defaults to libplctag. [Theory] [InlineData("\"Native\"", AbLegacyTransport.Native)] [InlineData("\"native\"", AbLegacyTransport.Native)] [InlineData("\"Libplctag\"", AbLegacyTransport.Libplctag)] [InlineData(null, AbLegacyTransport.Libplctag)] public void Config_transport_selects_the_factory(string? json, AbLegacyTransport expected) { AbLegacyDriverFactoryExtensions.ParseOptions("ab1", Config(json)).Transport.ShouldBe(expected); var driver = AbLegacyDriverFactoryExtensions.CreateInstance("ab1", Config(json)); if (expected == AbLegacyTransport.Native) driver.TagFactory.ShouldBeOfType(); else driver.TagFactory.ShouldBeOfType(); } /// A bad value fails the way an existing bad enum field (PlcFamily) does. [Fact] public void Bad_transport_fails_like_a_bad_PlcFamily() { var bad = Should.Throw( () => AbLegacyDriverFactoryExtensions.ParseOptions("ab1", Config("\"Bogus\""))); bad.Message.ShouldContain("unknown Transport 'Bogus'"); var existing = Should.Throw(() => AbLegacyDriverFactoryExtensions.ParseOptions( "ab1", """{"devices":[{"hostAddress":"ab://10.0.0.1/1,0","plcFamily":"Bogus"}]}""")); existing.GetType().ShouldBe(bad.GetType()); existing.Message.ShouldContain("unknown PlcFamily 'Bogus'"); } /// An explicitly injected factory wins over the option. [Fact] public void Injected_factory_wins_over_the_option() { var fake = new FakeAbLegacyTagFactory(); var driver = new AbLegacyDriver(new AbLegacyDriverOptions { Transport = AbLegacyTransport.Native }, "ab1", fake); driver.TagFactory.ShouldBeSameAs(fake); } /// /// Reinit does NOT swap transports (by design): the factory is constructor-held, so a changed /// transport in the reinit JSON is inert. A deploy recreates the instance instead (#516). /// [Fact] public async Task Reinit_with_a_changed_transport_keeps_the_constructed_transport() { var driver = AbLegacyDriverFactoryExtensions.CreateInstance("ab1", Config("\"Libplctag\"")); var before = driver.TagFactory; await driver.ReinitializeAsync(Config("\"Native\""), TestContext.Current.CancellationToken); driver.TagFactory.ShouldBeSameAs(before); driver.TagFactory.ShouldBeOfType(); } /// /// The probe's Phase 1 TCP preflight rejects a refused port before any runtime exists, so the native /// path is proven against a listener that accepts TCP and hangs up: only a runtime that really opens /// an EtherNet/IP session reports the native transport's own failure text. /// [Fact] public async Task Probe_with_Native_uses_the_native_path() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; var accept = Task.Run(async () => { while (true) { try { (await listener.AcceptTcpClientAsync(TestContext.Current.CancellationToken)).Dispose(); } catch { return; } } }); string Cfg(string t) => $$"""{"transport":"{{t}}","devices":[{"hostAddress":"ab://127.0.0.1:{{port}}/1,0"}]}"""; var probe = new AbLegacyDriverProbe(); var native = await probe.ProbeAsync(Cfg("Native"), TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken); var lib = await probe.ProbeAsync(Cfg("Libplctag"), TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken); listener.Stop(); native.Ok.ShouldBeFalse(); native.Message.ShouldNotBeNull(); // "Connection lost" is the managed EncapsulationClient's wording; libplctag reports a bare status name. native.Message!.ShouldContain("PCCC handshake failed: Connection lost"); lib.Message.ShouldNotContain("Connection lost"); } /// The probe deserialises with its own string-enum converter; a bad transport is a config error. [Fact] public async Task Probe_with_a_bad_transport_reports_invalid_config() { var r = await new AbLegacyDriverProbe().ProbeAsync( Config("\"Bogus\""), TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken); r.Ok.ShouldBeFalse(); r.Message!.ShouldContain("Config JSON is invalid"); } }