using System.Net; using System.Net.Sockets; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// The per-driver Transport switch: config parsing, driver factory selection and the probe. [Trait("Category", "Unit")] public sealed class AbCipTransportSwitchTests { private const string Device = """{"hostAddress":"ab://10.0.0.1:44818/1,0","plcFamily":"ControlLogix"}"""; private static string Config(string? transportJson) => $$"""{{{(transportJson is null ? "" : $"\"transport\":{transportJson},")}}"devices":[{{Device}}]}"""; /// Transport values select the factory trio; absent defaults to libplctag. [Theory] [InlineData("\"Native\"", AbCipTransport.Native)] [InlineData("\"native\"", AbCipTransport.Native)] [InlineData("\"Libplctag\"", AbCipTransport.Libplctag)] [InlineData(null, AbCipTransport.Libplctag)] public void Config_transport_selects_the_factory(string? json, AbCipTransport expected) { AbCipDriverFactoryExtensions.ParseOptions("ab1", Config(json)).Transport.ShouldBe(expected); var driver = AbCipDriverFactoryExtensions.CreateInstance("ab1", Config(json)); if (expected == AbCipTransport.Native) driver.TagFactory.ShouldBeOfType(); else driver.TagFactory.ShouldBeOfType(); } /// The factory trio matches the transport for all three factory kinds. [Fact] public void Factories_For_returns_a_matching_trio() { var n = AbCipTransportFactories.For(AbCipTransport.Native); n.Tags.ShouldBeOfType(); n.Enumerators.ShouldBeOfType(); n.Templates.ShouldBeOfType(); var l = AbCipTransportFactories.For(AbCipTransport.Libplctag); l.Tags.ShouldBeOfType(); l.Enumerators.ShouldBeOfType(); l.Templates.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( () => AbCipDriverFactoryExtensions.ParseOptions("ab1", Config("\"Bogus\""))); bad.Message.ShouldContain("unknown Transport 'Bogus'"); var existing = Should.Throw(() => AbCipDriverFactoryExtensions.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 FakeAbCipTagFactory(); var driver = new AbCipDriver(new AbCipDriverOptions { Transport = AbCipTransport.Native }, "ab1", fake); driver.TagFactory.ShouldBeSameAs(fake); } /// /// Reinit does NOT swap transports (by design): the factories are 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 = AbCipDriverFactoryExtensions.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; _ = Task.Run(async () => { while (true) { try { (await listener.AcceptTcpClientAsync(TestContext.Current.CancellationToken)).Dispose(); } catch { return; } } }, TestContext.Current.CancellationToken); string Cfg(string t) => $$"""{"transport":"{{t}}","devices":[{"hostAddress":"ab://127.0.0.1:{{port}}/1,0"}]}"""; var probe = new AbCipDriverProbe(); 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(); native.Message!.ShouldContain("CIP 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 AbCipDriverProbe().ProbeAsync( Config("\"Bogus\""), TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken); r.Ok.ShouldBeFalse(); r.Message!.ShouldContain("Config JSON is invalid"); } }