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:
@@ -250,6 +250,104 @@ public sealed class ServerBootTests : IDisposable
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end: CONNECT → SUBSCRIBE → verify SUBACK over the wire.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MqttBoot_SubscribeHandshake_ShouldReceiveSubAck()
|
||||
{
|
||||
var opts = new ServerOptions
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 0,
|
||||
Mqtt = { Port = -1, Host = "127.0.0.1" },
|
||||
};
|
||||
|
||||
var (server, err) = NatsServer.NewServer(opts);
|
||||
err.ShouldBeNull();
|
||||
server.ShouldNotBeNull();
|
||||
|
||||
try
|
||||
{
|
||||
server!.Start();
|
||||
var mqttAddr = server.MqttAddr();
|
||||
mqttAddr.ShouldNotBeNull();
|
||||
|
||||
using var tcp = new System.Net.Sockets.TcpClient();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await tcp.ConnectAsync(mqttAddr!.Address, mqttAddr.Port, cts.Token);
|
||||
|
||||
var stream = tcp.GetStream();
|
||||
|
||||
// 1. CONNECT → CONNACK
|
||||
var connectPacket = BuildMqttConnectPacket("sub-test");
|
||||
await stream.WriteAsync(connectPacket, cts.Token);
|
||||
await stream.FlushAsync(cts.Token);
|
||||
|
||||
var connack = new byte[4];
|
||||
await ReadExactAsync(stream, connack, cts.Token);
|
||||
connack[0].ShouldBe((byte)0x20); // CONNACK
|
||||
connack[3].ShouldBe((byte)0x00); // Accepted
|
||||
|
||||
// 2. SUBSCRIBE to "test/sub" QoS 0 → SUBACK
|
||||
var subscribePacket = BuildMqttSubscribePacket(packetId: 1, topic: "test/sub", qos: 0);
|
||||
await stream.WriteAsync(subscribePacket, cts.Token);
|
||||
await stream.FlushAsync(cts.Token);
|
||||
|
||||
// SUBACK: [0x90] [0x03] [PI high] [PI low] [granted QoS]
|
||||
var suback = new byte[5];
|
||||
await ReadExactAsync(stream, suback, cts.Token);
|
||||
suback[0].ShouldBe((byte)0x90); // SUBACK
|
||||
suback[1].ShouldBe((byte)0x03); // remaining length = 3
|
||||
suback[2].ShouldBe((byte)0x00); // PI high
|
||||
suback[3].ShouldBe((byte)0x01); // PI low = 1
|
||||
suback[4].ShouldBe((byte)0x00); // granted QoS 0
|
||||
}
|
||||
finally
|
||||
{
|
||||
server!.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Builds a minimal MQTT SUBSCRIBE packet.</summary>
|
||||
private static byte[] BuildMqttSubscribePacket(ushort packetId, string topic, byte qos)
|
||||
{
|
||||
var topicBytes = System.Text.Encoding.UTF8.GetBytes(topic);
|
||||
var payload = new List<byte>();
|
||||
payload.Add((byte)(packetId >> 8));
|
||||
payload.Add((byte)(packetId & 0xFF));
|
||||
payload.Add((byte)(topicBytes.Length >> 8));
|
||||
payload.Add((byte)(topicBytes.Length & 0xFF));
|
||||
payload.AddRange(topicBytes);
|
||||
payload.Add(qos);
|
||||
|
||||
var result = new List<byte>();
|
||||
result.Add(0x82); // SUBSCRIBE + flags 0x02
|
||||
var remLen = payload.Count;
|
||||
do
|
||||
{
|
||||
var b = (byte)(remLen & 0x7F);
|
||||
remLen >>= 7;
|
||||
if (remLen > 0) b |= 0x80;
|
||||
result.Add(b);
|
||||
} while (remLen > 0);
|
||||
result.AddRange(payload);
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Reads exactly <paramref name="buffer"/>.Length bytes from the stream.</summary>
|
||||
private static async Task ReadExactAsync(System.Net.Sockets.NetworkStream stream, byte[] buffer, CancellationToken ct)
|
||||
{
|
||||
var totalRead = 0;
|
||||
while (totalRead < buffer.Length)
|
||||
{
|
||||
var n = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead), ct);
|
||||
if (n == 0) break;
|
||||
totalRead += n;
|
||||
}
|
||||
totalRead.ShouldBe(buffer.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that Shutdown() after Start() completes cleanly.
|
||||
/// Uses DontListen to skip TCP binding — tests lifecycle only.
|
||||
|
||||
Reference in New Issue
Block a user