feat(mqtt): implement PUBLISH QoS 0, SUBSCRIBE, and UNSUBSCRIBE handlers

Add ParsePub, ParseSubsOrUnsubs, ProcessPub (QoS 0), ProcessSubs,
ProcessUnsubs, EnqueueSubAck, and EnqueueUnsubAck to MqttPacketHandlers.
Wire PUB/SUB/UNSUB dispatch cases in MqttParser. Add ReadSlice to
MqttReader for raw payload extraction. 18 new unit tests covering
parsing, flags, error cases, QoS downgrade, and full flow. 1 new
integration test verifying SUBSCRIBE handshake over TCP.
This commit is contained in:
Joseph Doherty
2026-03-01 16:04:37 -05:00
parent 95cf20b00b
commit 715367b9ea
7 changed files with 947 additions and 21 deletions

View File

@@ -24,7 +24,7 @@ public sealed class MqttParserTests
/// </summary>
private static ClientConnection CreateMqttClient()
{
var c = new ClientConnection(ClientKind.Client);
var c = new ClientConnection(ClientKind.Client, nc: new MemoryStream());
c.InitMqtt(new MqttHandler());
return c;
}
@@ -164,37 +164,38 @@ public sealed class MqttParserTests
[Fact]
public void Parse_SingleByteRemainingLength_ShouldWork()
{
// SUB packet with remaining length = 5 (single byte < 128)
// After CONNECT is received, a SUB packet should parse the remaining length correctly.
// SUBSCRIBE with remaining length = 6 (single byte < 128).
// Proves single-byte remaining length decoding works.
var c = CreateMqttClient();
c.Flags |= ClientFlags.ConnectReceived;
// SUBSCRIBE: type=0x82, remaining len=5, then 5 bytes of payload
var buf = new byte[] { 0x82, 0x05, 0x00, 0x01, 0x00, 0x01, 0x74 };
// SUBSCRIBE: type=0x82, remlen=6, PI=1, filter="t" (len=1), QoS=0
var buf = new byte[] { 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, (byte)'t', 0x00 };
var err = MqttParser.Parse(c, buf, buf.Length);
// Will hit NotImplementedException for SUBSCRIBE — that's fine, it proves parsing worked.
err.ShouldNotBeNull();
err.ShouldBeOfType<NotImplementedException>();
err.ShouldBeNull();
}
[Fact]
public void Parse_TwoByteRemainingLength_ShouldWork()
{
// PUBLISH QoS 0 with remaining length = 200 → encoded as [0xC8, 0x01].
// Proves two-byte remaining length decoding works.
var c = CreateMqttClient();
c.Flags |= ClientFlags.ConnectReceived;
// Remaining length = 200 → encoded as [0xC8, 0x01]
// (200 & 0x7F) | 0x80 = 0xC8, 200 >> 7 = 1 → 0x01
// type(1) + remlen(2) + payload(200) = 203 bytes total.
var buf = new byte[203];
buf[0] = MqttPacket.Pub;
buf[0] = MqttPacket.Pub; // 0x30, QoS 0
buf[1] = 0xC8;
buf[2] = 0x01;
// Remaining 200 bytes are zero (payload).
// Topic "t": length prefix (2 bytes) + 1 byte.
buf[3] = 0x00;
buf[4] = 0x01;
buf[5] = (byte)'t';
// Bytes 6..202 are zero (197-byte payload).
var err = MqttParser.Parse(c, buf, buf.Length);
err.ShouldNotBeNull();
err.ShouldBeOfType<NotImplementedException>(); // PUBLISH not yet implemented
err.ShouldBeNull();
}
// =========================================================================