using System.Buffers.Binary; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Cip; // CAPTURE DECISION: the brief gave timeout ticks 0x0E. Every libplctag capture (logix-forward-open, // logix-forward-close) carries priority/tick 0x0A, timeout ticks 0x05, so ConnectionManagerServices // uses 0x05 (brief: 0x0E -> capture: 0x05). The golden-byte tests below use 0x05 accordingly. public sealed class ConnectionManagerServicesTests { private static byte[] CapturedRequest(string file) => CaptureFile.CipPayload(CaptureFile.Load(file).First(f => f.FromClient)); private static CipReply CapturedReply(string file) => CipReply.Parse(CaptureFile.CipPayload(CaptureFile.Load(file).First(f => !f.FromClient))); [Fact] public void Forward_close_frame() { var bytes = ConnectionManagerServices.ForwardClose(0x1234, 0x1337, 0xCAFEBABE, [0x01, 0x00]); Convert.ToHexString(bytes).ShouldBe("4E02200624010A05" + "3412" + "3713" + "BEBAFECA" + "03" + "00" + "0100" + "20022401"); } [Fact] public void Forward_close_matches_capture() { var cap = CapturedRequest("logix-forward-close.hex"); var serial = BinaryPrimitives.ReadUInt16LittleEndian(cap.AsSpan(8)); var vendor = BinaryPrimitives.ReadUInt16LittleEndian(cap.AsSpan(10)); var orig = BinaryPrimitives.ReadUInt32LittleEndian(cap.AsSpan(12)); vendor.ShouldBe((ushort)0xF33D); ConnectionManagerServices.ForwardClose(serial, vendor, orig, [0x01, 0x00]).ShouldBe(cap); } [Fact] public void Large_forward_open_matches_capture() { var cap = CapturedRequest("logix-forward-open.hex"); // Values read out of the capture: T->O id @12, serial @16, vendor @18, originator serial @20, // timeout multiplier @24, RPI @28, network params @32 (0x42000FA2 -> size 4002). var p = new ForwardOpenParams( Large: true, TOConnectionId: BinaryPrimitives.ReadUInt32LittleEndian(cap.AsSpan(12)), ConnectionSerial: BinaryPrimitives.ReadUInt16LittleEndian(cap.AsSpan(16)), VendorId: BinaryPrimitives.ReadUInt16LittleEndian(cap.AsSpan(18)), OriginatorSerial: BinaryPrimitives.ReadUInt32LittleEndian(cap.AsSpan(20)), TimeoutMultiplier: cap[24], RpiMicros: BinaryPrimitives.ReadUInt32LittleEndian(cap.AsSpan(28)), ConnectionSize: 4002, Route: [0x01, 0x00]); p.RpiMicros.ShouldBe(1_000_000u); BinaryPrimitives.ReadUInt32LittleEndian(cap.AsSpan(32)).ShouldBe(0x42000FA2u); ConnectionManagerServices.ForwardOpen(p).ShouldBe(cap); } [Fact] public void Large_forward_open_reply_parses_from_capture() { var reply = ConnectionManagerServices.ParseForwardOpenReply(CapturedReply("logix-forward-open.hex")); reply.OTConnectionId.ShouldBe(0x2CDF633Fu); reply.TOConnectionId.ShouldBe(0x073F9E60u); reply.OTApiMicros.ShouldBe(1_000_000u); reply.TOApiMicros.ShouldBe(1_000_000u); } // No small Forward Open 0x54 was captured (the Large FO succeeded), so this layout is checked against // the CIP spec / the brief rather than a capture. [Fact] public void Regular_forward_open_layout_per_spec() { var bytes = ConnectionManagerServices.ForwardOpen(new ForwardOpenParams( false, 0x11223344, 0x5566, 0x7788, 0x99AABBCC, 0x01, 1_000_000, 504, [0x01, 0x00])); Convert.ToHexString(bytes).ShouldBe( "540220062401" + "0A05" + "00000000" + "44332211" + "6655" + "8877" + "CCBBAA99" + "01000000" + "40420F00" + "F843" + "40420F00" + "F843" + "A3" + "03" + "0100" + "20022401"); } [Fact] public void Regular_forward_open_network_params_encoding() { var bytes = ConnectionManagerServices.ForwardOpen(new ForwardOpenParams(false, 1, 2, 3, 4, 1, 1_000_000, 504, [])); // 504 = 0x1F8; 0x4200 | 0x1F8 = 0x43F8 -> LE F8 43 Convert.ToHexString(bytes).ShouldContain("40420F00" + "F843"); bytes[0].ShouldBe((byte)0x54); } [Fact] public void Unconnected_send_even_embedded() { byte[] embedded = [0x4C, 0x02, 0x91, 0x01, 0x41, 0x00, 0x01, 0x00]; // 8 bytes Convert.ToHexString(ConnectionManagerServices.UnconnectedSend(embedded, [0x01, 0x00])).ShouldBe( "520220062401" + "0A05" + "0800" + Convert.ToHexString(embedded) + "01" + "00" + "0100"); } [Fact] public void Unconnected_send_pads_odd_embedded() { byte[] embedded = [0x01, 0x02, 0x03]; Convert.ToHexString(ConnectionManagerServices.UnconnectedSend(embedded, [0x01, 0x00])).ShouldBe( "520220062401" + "0A05" + "0300" + "010203" + "00" + "01" + "00" + "0100"); } [Fact] public void Refused_forward_open_surfaces_statuses() { var reply = CipReply.Parse(Convert.FromHexString("DB000101" + "0901")); var ex = Should.Throw(() => ConnectionManagerServices.ParseForwardOpenReply(reply)); ex.Message.ShouldContain("0x01"); ex.Message.ShouldContain("0x0109"); } [Fact] public void Refused_forward_open_carries_the_statuses_as_structured_data() { var reply = CipReply.Parse(Convert.FromHexString("DB000102" + "09010203")); var ex = Should.Throw(() => ConnectionManagerServices.ParseForwardOpenReply(reply)); ex.GeneralStatus.ShouldBe((byte)0x01); ex.ExtendedStatus.ShouldBe(new ushort[] { 0x0109, 0x0302 }); ex.Status.ShouldBe(AbStatus.BadConnection); // general 0x01 has no specific mapping } [Fact] public void Refused_forward_open_exception_is_not_aliased_to_the_reply_array() { var reply = CipReply.Parse(Convert.FromHexString("DB000101" + "0901")); var ex = Should.Throw(() => ConnectionManagerServices.ParseForwardOpenReply(reply)); reply.ExtendedStatus[0] = 0xFFFF; ex.ExtendedStatus[0].ShouldBe((ushort)0x0109); } [Theory] [InlineData("DB000800", true)] // Service Not Supported [InlineData("DB0001010901", true)] // general 1 / extended 0x0109 [InlineData("DB0001010001", false)] // general 1 / extended 0x0100 (connection in use): no fallback [InlineData("DB000000", false)] public void Fallback_rules(string hex, bool expected) => ConnectionManagerServices.ShouldFallBackToRegularOpen(CipReply.Parse(Convert.FromHexString(hex))).ShouldBe(expected); [Theory] [InlineData(true, 0)] [InlineData(true, 0x10000)] [InlineData(false, 0)] [InlineData(false, 0x200)] public void Connection_size_out_of_range_throws(bool large, int size) => Should.Throw(() => ConnectionManagerServices.ForwardOpen( new ForwardOpenParams(large, 1, 2, 3, 4, 1, 1_000_000, size, []))); [Theory] [InlineData(true, 1)] [InlineData(true, 0xFFFF)] [InlineData(false, 1)] [InlineData(false, 0x1FF)] public void Connection_size_bounds_accepted(bool large, int size) => ConnectionManagerServices.ForwardOpen(new ForwardOpenParams(large, 1, 2, 3, 4, 1, 1_000_000, size, [])).ShouldNotBeEmpty(); }