using Mbproxy.Proxy; using Xunit; namespace Mbproxy.Tests.Proxy; /// /// Unit tests for header parsing and frame-length helpers. /// All tests are pure in-memory; no network, no simulator required. /// [Trait("Category", "Unit")] public sealed class MbapFrameTests { // ── 1. TryParseHeader — too-short buffers ──────────────────────────────────────────── [Fact] public void TryParseHeader_TooShort_ReturnsFalse() { // A buffer of only 6 bytes is one byte short of the 7-byte header. byte[] buf = [0x00, 0x01, 0x00, 0x00, 0x00, 0x06]; bool result = MbapFrame.TryParseHeader(buf, out _, out _, out _, out _); Assert.False(result, "Buffer shorter than 7 bytes must return false."); } [Fact] public void TryParseHeader_EmptyBuffer_ReturnsFalse() { bool result = MbapFrame.TryParseHeader(ReadOnlySpan.Empty, out _, out _, out _, out _); Assert.False(result); } // ── 2. TryParseHeader — valid frame parses all fields ────────────────────────────── [Fact] public void TryParseHeader_ValidFrame_ParsesAllFields() { // TxId=0x0042, ProtocolId=0x0000, Length=0x0006, UnitId=0x01 byte[] header = [0x00, 0x42, 0x00, 0x00, 0x00, 0x06, 0x01]; bool ok = MbapFrame.TryParseHeader(header, out ushort txId, out ushort protocolId, out ushort length, out byte unitId); Assert.True(ok); Assert.Equal(0x0042, txId); Assert.Equal(0x0000, protocolId); Assert.Equal(6, length); Assert.Equal(1, unitId); } // ── 3. Non-zero ProtocolId still parses (PLC's job to reject it) ───────────────── [Fact] public void TryParseHeader_ProtocolId_NotZero_StillParses() { // ProtocolId = 0x0001 (non-standard but we don't filter it). byte[] header = [0x00, 0x01, 0x00, 0x01, 0x00, 0x06, 0xFF]; bool ok = MbapFrame.TryParseHeader(header, out _, out ushort protocolId, out _, out _); Assert.True(ok); Assert.Equal(0x0001, protocolId); } // ── 4. TotalFrameLength — known good values ────────────────────────────────────── [Fact] public void TotalFrameLength_LengthField7_Returns13() { // 6 fixed prefix bytes + 7 = 13 Assert.Equal(13, MbapFrame.TotalFrameLength(7)); } [Fact] public void TotalFrameLength_LengthFieldMax_Returns_LengthFieldPlus6() { // The formula is always lengthField + 6. ushort max = ushort.MaxValue; // 65535 Assert.Equal(max + 6, MbapFrame.TotalFrameLength(max)); } // ── 5. Round-trip: FC03 read-holding-registers request ─────────────────────────── [Fact] public void RoundTrip_FC03_ReadHoldingRegisters_Request_ParsesCorrectly() { // FC03 request: TxId=1, ProtocolId=0, Length=6, UnitId=1, FC=0x03, Start=0x0430, Qty=0x0001 byte[] frame = [ 0x00, 0x01, // TxId = 1 0x00, 0x00, // ProtocolId = 0 0x00, 0x06, // Length = 6 0x01, // UnitId = 1 0x03, // FC 03 0x04, 0x30, // Start address = 0x0430 (decimal 1072) 0x00, 0x01, // Quantity = 1 ]; bool ok = MbapFrame.TryParseHeader(frame.AsSpan(0, 7), out ushort txId, out ushort protocolId, out ushort length, out byte unitId); Assert.True(ok); Assert.Equal(1, txId); Assert.Equal(0, protocolId); Assert.Equal(6, length); Assert.Equal(1, unitId); // Total frame = 6 + length = 12 bytes Assert.Equal(12, MbapFrame.TotalFrameLength(length)); Assert.Equal(frame.Length, MbapFrame.TotalFrameLength(length)); } // ── 6. Round-trip: FC16 write-multiple-registers request ───────────────────────── [Fact] public void RoundTrip_FC16_WriteMultipleRegisters_ParsesCorrectly() { // FC16 request: TxId=5, ProtocolId=0, Length=11, UnitId=1 // FC=0x10, Start=0x00C8 (200), Qty=2, ByteCount=4, Data=[0x00,0x0A, 0x00,0x14] byte[] frame = [ 0x00, 0x05, // TxId = 5 0x00, 0x00, // ProtocolId = 0 0x00, 0x0B, // Length = 11 0x01, // UnitId = 1 0x10, // FC 16 0x00, 0xC8, // Start address = 200 0x00, 0x02, // Quantity = 2 0x04, // Byte count = 4 0x00, 0x0A, // Register 200 = 10 0x00, 0x14, // Register 201 = 20 ]; bool ok = MbapFrame.TryParseHeader(frame.AsSpan(0, 7), out ushort txId, out _, out ushort length, out byte unitId); Assert.True(ok); Assert.Equal(5, txId); Assert.Equal(11, length); Assert.Equal(1, unitId); // Total frame = 6 + 11 = 17 Assert.Equal(17, MbapFrame.TotalFrameLength(length)); Assert.Equal(frame.Length, MbapFrame.TotalFrameLength(length)); } // ── 7. Length < 2 — parsed but unusual (callers' responsibility) ─────────────────── [Fact] public void TryParseHeader_LengthLessThan2_ParsedButUnusual() { // length=1 means only a UnitId byte follows the 6-byte prefix; PDU body = 0 bytes. // The proxy does not reject this — that is the PLC's job. We parse and pass through. byte[] header = [0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01]; bool ok = MbapFrame.TryParseHeader(header, out _, out _, out ushort length, out _); Assert.True(ok, "Header with length=1 should still parse; the proxy does not validate length semantics."); Assert.Equal(1, length); // TotalFrameLength still returns 6 + length = 7 (header only, no PDU body). Assert.Equal(7, MbapFrame.TotalFrameLength(length)); } // ── 8. Exactly 7 bytes — boundary case ───────────────────────────────────────────── [Fact] public void TryParseHeader_ExactlySevenBytes_ParsesOk() { byte[] header = [0xFF, 0xFE, 0x00, 0x00, 0x00, 0x06, 0x02]; bool ok = MbapFrame.TryParseHeader(header, out ushort txId, out _, out _, out byte unitId); Assert.True(ok); Assert.Equal(0xFFFE, txId); Assert.Equal(2, unitId); } }