feat(mqtt): implement QoS 1 PUBACK and QoS 2 PUBREC/PUBREL/PUBCOMP handlers

QoS 1: deliver message then send PUBACK. QoS 2: store in-memory
pending PUBREL, send PUBREC; on PUBREL deliver and send PUBCOMP.
Wire all four PI-packet dispatches (PUBACK/PUBREC/PUBREL/PUBCOMP)
in parser. Add QoS2Pending dictionary to MqttHandler for in-memory
QoS 2 tracking. 6 new tests for QoS 1/2 flows including full
QoS 2 handshake.
This commit is contained in:
Joseph Doherty
2026-03-01 16:08:28 -05:00
parent 715367b9ea
commit b465d095e3
5 changed files with 307 additions and 22 deletions

View File

@@ -174,14 +174,14 @@ public sealed class MqttPubSubTests
}
[Fact]
public void Parser_PublishQoS1_ShouldReturnNotImplemented()
public void Parser_PublishQoS1_ShouldSendPubAck()
{
var c = CreateConnectedMqttClient();
// PUBLISH QoS 1: type=0x32, topic="t", PI=1, payload="x"
// PUBLISH QoS 1: type=0x32, topic="t", PI=5, payload="x"
var data = new List<byte>();
data.Add(0x00); data.Add(0x01); data.Add((byte)'t'); // topic
data.Add(0x00); data.Add(0x01); // PI = 1
data.Add(0x00); data.Add(0x05); // PI = 5
data.Add((byte)'x'); // payload
var buf = new List<byte>();
@@ -190,8 +190,146 @@ public sealed class MqttPubSubTests
buf.AddRange(data);
var err = MqttParser.Parse(c, buf.ToArray(), buf.Count);
err.ShouldNotBeNull();
err.ShouldBeOfType<NotImplementedException>();
err.ShouldBeNull();
// Verify PUBACK: [0x40] [0x02] [PI high] [PI low]
var ms = GetStream(c);
var written = ms.ToArray();
written.Length.ShouldBe(4);
written[0].ShouldBe(MqttPacket.PubAck); // 0x40
written[1].ShouldBe((byte)0x02);
written[2].ShouldBe((byte)0x00); // PI high
written[3].ShouldBe((byte)0x05); // PI low
}
[Fact]
public void Parser_PublishQoS2_ShouldSendPubRec()
{
var c = CreateConnectedMqttClient();
// PUBLISH QoS 2: type=0x34, topic="t", PI=10, payload="y"
var data = new List<byte>();
data.Add(0x00); data.Add(0x01); data.Add((byte)'t'); // topic
data.Add(0x00); data.Add(0x0A); // PI = 10
data.Add((byte)'y'); // payload
var buf = new List<byte>();
buf.Add((byte)(MqttPacket.Pub | MqttPubFlag.QoS2)); // 0x34
buf.Add((byte)data.Count);
buf.AddRange(data);
var err = MqttParser.Parse(c, buf.ToArray(), buf.Count);
err.ShouldBeNull();
// Verify PUBREC: [0x50] [0x02] [PI high] [PI low]
var ms = GetStream(c);
var written = ms.ToArray();
written.Length.ShouldBe(4);
written[0].ShouldBe(MqttPacket.PubRec); // 0x50
written[1].ShouldBe((byte)0x02);
written[2].ShouldBe((byte)0x00); // PI high
written[3].ShouldBe((byte)0x0A); // PI low
// Message should be stored in QoS2Pending.
c.Mqtt!.QoS2Pending.ShouldContainKey((ushort)10);
}
[Fact]
public void Parser_QoS2_FullHandshake_PubRecPubRelPubComp()
{
var c = CreateConnectedMqttClient();
// Step 1: PUBLISH QoS 2 → PUBREC
var pubData = new List<byte>();
pubData.Add(0x00); pubData.Add(0x01); pubData.Add((byte)'t');
pubData.Add(0x00); pubData.Add(0x07); // PI = 7
pubData.AddRange(Encoding.UTF8.GetBytes("qos2msg"));
var pubBuf = new List<byte>();
pubBuf.Add((byte)(MqttPacket.Pub | MqttPubFlag.QoS2));
pubBuf.Add((byte)pubData.Count);
pubBuf.AddRange(pubData);
var err = MqttParser.Parse(c, pubBuf.ToArray(), pubBuf.Count);
err.ShouldBeNull();
c.Mqtt!.QoS2Pending.ShouldContainKey((ushort)7);
// Reset stream to capture only PUBCOMP.
var ms = GetStream(c);
ms.SetLength(0);
// Step 2: PUBREL from client → PUBCOMP
// PUBREL: [0x62] [0x02] [PI high] [PI low]
var pubrelBuf = new byte[] { 0x62, 0x02, 0x00, 0x07 };
err = MqttParser.Parse(c, pubrelBuf, pubrelBuf.Length);
err.ShouldBeNull();
// Verify PUBCOMP was sent.
var written = ms.ToArray();
written.Length.ShouldBe(4);
written[0].ShouldBe(MqttPacket.PubComp); // 0x70
written[1].ShouldBe((byte)0x02);
written[2].ShouldBe((byte)0x00);
written[3].ShouldBe((byte)0x07);
// Message should be removed from QoS2Pending.
c.Mqtt.QoS2Pending.ShouldNotContainKey((ushort)7);
}
[Fact]
public void Parser_PubAck_ShouldRemoveFromPending()
{
var c = CreateConnectedMqttClient();
// Pre-populate pending.
c.Mqtt!.Pending[(ushort)3] = null;
c.Mqtt.Pending.ShouldContainKey((ushort)3);
// PUBACK: [0x40] [0x02] [0x00] [0x03]
var buf = new byte[] { 0x40, 0x02, 0x00, 0x03 };
var err = MqttParser.Parse(c, buf, buf.Length);
err.ShouldBeNull();
c.Mqtt.Pending.ShouldNotContainKey((ushort)3);
}
[Fact]
public void Parser_PubRec_ShouldSendPubRel()
{
var c = CreateConnectedMqttClient();
// Pre-populate pending.
c.Mqtt!.Pending[(ushort)9] = null;
// PUBREC: [0x50] [0x02] [0x00] [0x09]
var buf = new byte[] { 0x50, 0x02, 0x00, 0x09 };
var err = MqttParser.Parse(c, buf, buf.Length);
err.ShouldBeNull();
// Should have sent PUBREL: [0x62] [0x02] [0x00] [0x09]
var ms = GetStream(c);
var written = ms.ToArray();
written.Length.ShouldBe(4);
written[0].ShouldBe((byte)0x62); // PUBREL with bit 1 set
written[3].ShouldBe((byte)0x09);
// Pending should be cleared.
c.Mqtt.Pending.ShouldNotContainKey((ushort)9);
}
[Fact]
public void Parser_PubComp_ShouldRemoveFromPending()
{
var c = CreateConnectedMqttClient();
c.Mqtt!.Pending[(ushort)15] = null;
// PUBCOMP: [0x70] [0x02] [0x00] [0x0F]
var buf = new byte[] { 0x70, 0x02, 0x00, 0x0F };
var err = MqttParser.Parse(c, buf, buf.Length);
err.ShouldBeNull();
c.Mqtt.Pending.ShouldNotContainKey((ushort)15);
}
// =========================================================================