// Ports QoS delivery behavior from Go reference: // golang/nats-server/server/mqtt_test.go — TestMQTTPublish, TestMQTTSubQoS1, TestMQTTParsePub using System.Net; using System.Net.Sockets; using System.Text; using NATS.Server.Mqtt; namespace NATS.Server.Tests.Mqtt; public class MqttQosDeliveryParityTests { // Go ref: TestMQTTPublish — QoS 0 is fire-and-forget; publisher sends PUB and receives no PUBACK. [Fact] public async Task Qos0_publish_is_fire_and_forget_no_puback_returned() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttQosWire.WriteLineAsync(stream, "CONNECT qos0-client clean=false"); (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); // PUB is QoS 0 — no PUBACK should come back await MqttQosWire.WriteLineAsync(stream, "PUB sensors.temp 25"); // Server must not send anything back for QoS 0 (await MqttQosWire.ReadRawAsync(stream, 200)).ShouldBe("__timeout__"); } // Go ref: TestMQTTSubQoS1 — QoS 1 publisher receives PUBACK; subscriber on matching topic receives MSG. [Fact] public async Task Qos1_publish_with_subscriber_delivers_message_to_subscriber() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); // Set up subscriber first using var sub = new TcpClient(); await sub.ConnectAsync(IPAddress.Loopback, listener.Port); var subStream = sub.GetStream(); await MqttQosWire.WriteLineAsync(subStream, "CONNECT sub-client clean=false"); (await MqttQosWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK"); await MqttQosWire.WriteLineAsync(subStream, "SUB sensors.temp"); var subAck = await MqttQosWire.ReadLineAsync(subStream, 1000); subAck.ShouldNotBeNull(); subAck.ShouldContain("SUBACK"); // Publisher sends QoS 1 using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var pubStream = pub.GetStream(); await MqttQosWire.WriteLineAsync(pubStream, "CONNECT pub-client clean=false"); (await MqttQosWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK"); await MqttQosWire.WriteLineAsync(pubStream, "PUBQ1 3 sensors.temp 72"); // Publisher receives PUBACK (await MqttQosWire.ReadLineAsync(pubStream, 1000)).ShouldBe("PUBACK 3"); // Subscriber receives the published message (await MqttQosWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG sensors.temp 72"); } // Go ref: TestMQTTSubQoS1 — QoS 1 PUBACK is sent by the server regardless of whether any subscriber exists. [Fact] public async Task Qos1_publish_without_subscriber_still_returns_puback_to_publisher() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttQosWire.WriteLineAsync(stream, "CONNECT lonely-publisher clean=false"); (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); // Publish QoS 1 with no subscribers registered await MqttQosWire.WriteLineAsync(stream, "PUBQ1 9 nowhere.topic hello"); // Server must still acknowledge the publish (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("PUBACK 9"); } // Go ref: TestMQTTSubQoS1 — each QoS 1 publish carries a distinct packet identifier assigned by the sender. [Fact] public async Task Multiple_qos1_publishes_use_incrementing_packet_ids() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttQosWire.WriteLineAsync(stream, "CONNECT multi-pub-client clean=false"); (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); // Send three QoS 1 publishes with consecutive packet IDs await MqttQosWire.WriteLineAsync(stream, "PUBQ1 1 sensor.a alpha"); (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("PUBACK 1"); await MqttQosWire.WriteLineAsync(stream, "PUBQ1 2 sensor.b beta"); (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("PUBACK 2"); await MqttQosWire.WriteLineAsync(stream, "PUBQ1 3 sensor.c gamma"); (await MqttQosWire.ReadLineAsync(stream, 1000)).ShouldBe("PUBACK 3"); } } // Duplicated per-file as required — each test file is self-contained. internal static class MqttQosWire { public static async Task WriteLineAsync(NetworkStream stream, string line) { var bytes = Encoding.UTF8.GetBytes(line + "\n"); await stream.WriteAsync(bytes); await stream.FlushAsync(); } public static async Task ReadLineAsync(NetworkStream stream, int timeoutMs) { using var timeout = new CancellationTokenSource(timeoutMs); var bytes = new List(); var one = new byte[1]; try { while (true) { var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token); if (read == 0) return null; if (one[0] == (byte)'\n') break; if (one[0] != (byte)'\r') bytes.Add(one[0]); } } catch (OperationCanceledException) { return null; } return Encoding.UTF8.GetString([.. bytes]); } public static async Task ReadRawAsync(NetworkStream stream, int timeoutMs) { using var timeout = new CancellationTokenSource(timeoutMs); var one = new byte[1]; try { var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token); if (read == 0) return null; return Encoding.UTF8.GetString(one, 0, read); } catch (OperationCanceledException) { return "__timeout__"; } } }