using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Cip; public sealed class CipPathTests { [Theory] [InlineData("1,0", "0100")] [InlineData("", "")] [InlineData("1,0,2,10.0.0.5", "0100" + "1208" + "31302E302E302E35")] [InlineData("2,10.0.0.55", "1209" + "31302E302E302E3535" + "00")] [InlineData("15,3", "0F0F0003")] [InlineData("16,10.0.0.5", "1F08" + "1000" + "31302E302E302E35")] public void Encodes_route(string path, string hex) => Convert.ToHexString(CipPath.EncodeRoute(path)).ShouldBe(hex); [Fact] public void Odd_route_parts_throw() => Should.Throw(() => CipPath.EncodeRoute("1,0,2")); [Theory] [InlineData("TestDINT", "9108546573744449 4E54")] [InlineData("Abc", "910341626300")] [InlineData("Program:Main.Arr[3].Val", "910C50726F6772616D3A4D61696E" + "910341727200" + "2803" + "910356616C00")] [InlineData("Big[300]", "910342696700" + "29002C01")] [InlineData("Huge[70000]", "910448756765" + "2A00" + "70110100")] [InlineData("Grid[1,2]", "910447726964" + "2801" + "2802")] public void Encodes_symbolic(string name, string hex) => Convert.ToHexString(CipPath.EncodeSymbolic(name)).ShouldBe(hex.Replace(" ", "")); [Fact] public void Symbolic_rejects_bit_selector() => Should.Throw(() => CipPath.EncodeSymbolic("Tag.5")); /// Gitea #570: names that used to be encoded silently wrong (a non-ASCII name as '?', an over-long /// one with a wrapped length byte, an empty member as a zero-length segment) or that threw an unrelated /// exception (an unterminated subscript) are refused as invalid arguments before any I/O. [Theory] [InlineData("Tëmp")] [InlineData("A..B")] [InlineData("Arr[1")] public void Symbolic_rejects_names_it_cannot_encode_faithfully(string name) => Should.Throw(() => CipPath.EncodeSymbolic(name)); [Fact] public void Symbolic_rejects_a_member_longer_than_its_length_byte() { Should.Throw(() => CipPath.EncodeSymbolic(new string('A', 256))); CipPath.EncodeSymbolic(new string('A', 255))[1].ShouldBe((byte)255); // the boundary still encodes } [Fact] public void Splits_bit_selector() { CipPath.SplitBitSelector("Tag.5").ShouldBe(("Tag", 5)); CipPath.SplitBitSelector("Tag.Member").ShouldBe(("Tag.Member", null)); CipPath.SplitBitSelector("Tag").ShouldBe(("Tag", null)); } [Theory] [InlineData(2, 1u, "20022401")] [InlineData(0x6B, 0x1234u, "206B25003412")] [InlineData(0x300, 1u, "21000003" + "2401")] [InlineData(2, 70000u, "2002" + "2600" + "70110100")] public void Encodes_class_instance(ushort cls, uint inst, string hex) => Convert.ToHexString(CipPath.EncodeClassInstance(cls, inst)).ShouldBe(hex); [Theory] [InlineData("-1,0")] // negative port [InlineData("65536,0")] // port does not fit a u16 [InlineData("x,0")] // non-numeric port [InlineData("1,é.example")] // non-ASCII link public void Malformed_route_throws_ArgumentException(string path) => Should.Throw(() => CipPath.EncodeRoute(path)); [Fact] public void Route_link_over_255_characters_throws() { Should.Throw(() => CipPath.EncodeRoute("1," + new string('a', 256))); CipPath.EncodeRoute("1," + new string('a', 255)).Length.ShouldBe(2 + 255 + 1); } [Fact] public void Route_over_255_words_throws() => Should.Throw(() => CipPath.EncodeRoute(string.Join(",", Enumerable.Repeat("1,0", 256)))); [Theory] [InlineData("")] [InlineData("A..B")] // empty member [InlineData(".A")] [InlineData("A[1")] // unterminated subscript [InlineData("A[x]")] // non-numeric subscript [InlineData("A[-1]")] [InlineData("A[]")] [InlineData("A[1]B")] // junk after the subscript [InlineData("Tä")] // non-ASCII [InlineData("A[99999999999]")] // subscript overflows u32 [InlineData("Tag.99999999999")] // bit selector overflows int public void Malformed_tag_names_throw_ArgumentException(string name) => Should.Throw(() => CipPath.EncodeSymbolic(name)); [Fact] public void Tag_member_over_255_characters_throws() { Should.Throw(() => CipPath.EncodeSymbolic(new string('a', 256))); CipPath.EncodeSymbolic(new string('a', 255)).Length.ShouldBe(2 + 255 + 1); } [Theory] [InlineData(1)] // odd [InlineData(3)] [InlineData(512)] // 256 words: size byte would wrap public void WithPathSize_rejects_odd_and_oversize_paths(int length) => Should.Throw(() => CipPath.WithPathSize(new byte[length])); [Fact] public void WithPathSize_accepts_the_255_word_maximum() => CipPath.WithPathSize(new byte[510])[0].ShouldBe((byte)255); [Fact] public void Well_known_paths_are_immutable_spans() { CipPath.MessageRouter.ToArray().ShouldBe(new byte[] { 0x20, 0x02, 0x24, 0x01 }); CipPath.ConnectionManager.ToArray().ShouldBe(new byte[] { 0x20, 0x06, 0x24, 0x01 }); } [Fact] public void Prefixes_path_size_in_words() => Convert.ToHexString(CipPath.WithPathSize([0x20, 0x02, 0x24, 0x01])).ShouldBe("0220022401"); }