feat(mqtt): implement CONNECT/CONNACK/DISCONNECT packet handlers

Implement Task 3 of MQTT orchestration:
- Create MqttPacketHandlers.cs with ParseConnect(), ProcessConnect(), EnqueueConnAck(), HandleDisconnect()
- Wire CONNECT and DISCONNECT dispatch in MqttParser.cs
- Parse CONNECT: protocol name/level, flags, keep-alive, client ID, will, auth
- Send CONNACK (4-byte fixed packet with return code)
- DISCONNECT clears will message and closes connection cleanly
- Auto-generate client ID for empty ID + clean session
- Validate reserved bit, will flags, username/password consistency
- Add Reader field to MqttHandler for per-connection parsing
- 11 unit tests for CONNECT parsing and processing
- 1 end-to-end integration test: TCP → CONNECT → CONNACK over the wire
This commit is contained in:
Joseph Doherty
2026-03-01 15:48:22 -05:00
parent 2e2ffee41a
commit 95cf20b00b
6 changed files with 768 additions and 15 deletions

View File

@@ -56,16 +56,39 @@ public sealed class MqttParserTests
}
[Fact]
public void Parse_ConnectFirst_ShouldNotRejectAsNonConnect()
public void Parse_ConnectFirst_ShouldAcceptConnect()
{
var c = CreateMqttClient();
// CONNECT packet (minimal): type=0x10, remaining len=0
// This will hit the "not yet implemented" but NOT the "first packet" error.
var buf = new byte[] { MqttPacket.Connect, 0x00 };
var err = MqttParser.Parse(c, buf, buf.Length);
err.ShouldNotBeNull(); // Will be NotImplementedException
err.ShouldBeOfType<NotImplementedException>();
err.Message.ShouldContain("CONNECT not yet implemented");
// Use a MemoryStream so CONNACK can be written.
typeof(ClientConnection)
.GetField("_nc", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
.SetValue(c, new MemoryStream());
// Build a valid CONNECT packet.
var payload = new List<byte>();
payload.AddRange(new byte[] { 0x00, 0x04 }); // protocol name length
payload.AddRange(System.Text.Encoding.UTF8.GetBytes("MQTT"));
payload.Add(0x04); // level
payload.Add(0x02); // flags: clean session
payload.AddRange(new byte[] { 0x00, 0x3C }); // keep alive = 60
payload.AddRange(new byte[] { 0x00, 0x04 }); // client id length
payload.AddRange(System.Text.Encoding.UTF8.GetBytes("test"));
var buf = new List<byte> { MqttPacket.Connect };
// Remaining length
var remLen = payload.Count;
do
{
var b = (byte)(remLen & 0x7F);
remLen >>= 7;
if (remLen > 0) b |= 0x80;
buf.Add(b);
} while (remLen > 0);
buf.AddRange(payload);
var err = MqttParser.Parse(c, buf.ToArray(), buf.Count);
err.ShouldBeNull("CONNECT should be accepted, not rejected as non-CONNECT");
(c.Flags & ClientFlags.ConnectReceived).ShouldNotBe((ClientFlags)0);
}
[Fact]