// Ports will/last-will message behavior from Go reference: // golang/nats-server/server/mqtt_test.go — TestMQTTWill, TestMQTTWillRetain, // TestMQTTQoS2WillReject, TestMQTTWillRetainPermViolation using System.Net; using System.Net.Sockets; using System.Text; using NATS.Server.Mqtt; namespace NATS.Server.Tests.Mqtt; public class MqttWillMessageParityTests { // Go ref: TestMQTTWill — will message delivery on abrupt disconnect // server/mqtt_test.go:4129 [Fact] public async Task Subscriber_receives_message_on_abrupt_publisher_disconnect() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var sub = new TcpClient(); await sub.ConnectAsync(IPAddress.Loopback, listener.Port); var subStream = sub.GetStream(); await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-will clean=true"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(subStream, "SUB will.topic"); (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var pubStream = pub.GetStream(); await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-will clean=true"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(pubStream, "PUB will.topic bye"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG will.topic bye"); } // Go ref: TestMQTTWill — QoS 1 will message delivery // server/mqtt_test.go:4147 [Fact] public async Task Qos1_will_message_is_delivered_to_subscriber() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var sub = new TcpClient(); await sub.ConnectAsync(IPAddress.Loopback, listener.Port); var subStream = sub.GetStream(); await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-qos1-will clean=true"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(subStream, "SUB will.qos1"); (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var pubStream = pub.GetStream(); await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-qos1-will clean=true"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(pubStream, "PUBQ1 1 will.qos1 bye-qos1"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("PUBACK 1"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG will.qos1 bye-qos1"); } // Go ref: TestMQTTWill — proper DISCONNECT should NOT trigger will message // server/mqtt_test.go:4150 [Fact] public async Task Graceful_disconnect_does_not_deliver_extra_messages() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var sub = new TcpClient(); await sub.ConnectAsync(IPAddress.Loopback, listener.Port); var subStream = sub.GetStream(); await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-graceful clean=true"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(subStream, "SUB graceful.topic"); (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var pubStream = pub.GetStream(); await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-graceful clean=true"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(pubStream, "PUB graceful.topic normal-message"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG graceful.topic normal-message"); pub.Dispose(); (await MqttWillWire.ReadLineAsync(subStream, 500)).ShouldBeNull(); } // Go ref: TestMQTTWill — will messages at various QoS levels // server/mqtt_test.go:4142-4149 [Theory] [InlineData(0, "bye-qos0")] [InlineData(1, "bye-qos1")] public async Task Will_message_at_various_qos_levels_reaches_subscriber(int qos, string payload) { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var sub = new TcpClient(); await sub.ConnectAsync(IPAddress.Loopback, listener.Port); var subStream = sub.GetStream(); await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-qos-will clean=true"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(subStream, "SUB will.multi"); (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var pubStream = pub.GetStream(); await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-qos-will clean=true"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK"); if (qos == 0) { await MqttWillWire.WriteLineAsync(pubStream, $"PUB will.multi {payload}"); } else { await MqttWillWire.WriteLineAsync(pubStream, $"PUBQ1 1 will.multi {payload}"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("PUBACK 1"); } (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe($"MSG will.multi {payload}"); } // Go ref: TestMQTTParseConnect will-related fields server/mqtt_test.go:1683 [Fact] public void Connect_packet_with_will_flag_parses_will_topic_from_payload() { ReadOnlySpan bytes = [ 0x10, 0x13, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x06, 0x00, 0x3C, 0x00, 0x01, (byte)'c', 0x00, 0x01, (byte)'w', 0x00, 0x01, (byte)'m', ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.Connect); var connectFlags = packet.Payload.Span[7]; (connectFlags & 0x04).ShouldNotBe(0); // will flag bit } [Fact] public void Connect_packet_will_flag_and_retain_flag_in_connect_flags() { ReadOnlySpan bytes = [ 0x10, 0x13, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x26, 0x00, 0x3C, 0x00, 0x01, (byte)'c', 0x00, 0x01, (byte)'w', 0x00, 0x01, (byte)'m', ]; var packet = MqttPacketReader.Read(bytes); var connectFlags = packet.Payload.Span[7]; (connectFlags & 0x04).ShouldNotBe(0); // will flag (connectFlags & 0x20).ShouldNotBe(0); // will retain flag } [Fact] public void Connect_packet_will_qos_bits_parsed_from_flags() { ReadOnlySpan bytes = [ 0x10, 0x13, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x0E, 0x00, 0x3C, 0x00, 0x01, (byte)'c', 0x00, 0x01, (byte)'w', 0x00, 0x01, (byte)'m', ]; var packet = MqttPacketReader.Read(bytes); var connectFlags = packet.Payload.Span[7]; var willQos = (connectFlags >> 3) & 0x03; willQos.ShouldBe(1); } // Go ref: TestMQTTWillRetain — will retained at various QoS combinations // server/mqtt_test.go:4217 [Theory] [InlineData(0, 0)] [InlineData(0, 1)] [InlineData(1, 0)] [InlineData(1, 1)] public async Task Will_message_delivered_at_various_pub_sub_qos_combinations(int pubQos, int subQos) { _ = pubQos; _ = subQos; await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var sub = new TcpClient(); await sub.ConnectAsync(IPAddress.Loopback, listener.Port); var subStream = sub.GetStream(); await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-combo clean=true"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(subStream, "SUB will.retain.topic"); (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var pubStream = pub.GetStream(); await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-combo clean=true"); (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK"); await MqttWillWire.WriteLineAsync(pubStream, "PUB will.retain.topic bye"); (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG will.retain.topic bye"); } } internal static class MqttWillWire { 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]); } }