// Ports advanced MQTT behaviors from Go reference: // golang/nats-server/server/mqtt_test.go — TestMQTTSub, TestMQTTUnsub, TestMQTTSubWithSpaces, // TestMQTTSubCaseSensitive, TestMQTTSubDups, TestMQTTParseSub, TestMQTTParseUnsub, // TestMQTTSubAck, TestMQTTPublish, TestMQTTPublishTopicErrors, TestMQTTParsePub, // TestMQTTMaxPayloadEnforced, TestMQTTCleanSession, TestMQTTDuplicateClientID, // TestMQTTConnAckFirstPacket, TestMQTTStart, TestMQTTValidateOptions, // TestMQTTPreventSubWithMQTTSubPrefix, TestMQTTConnKeepAlive, TestMQTTDontSetPinger, // TestMQTTPartial, TestMQTTSubQoS2, TestMQTTPubSubMatrix, TestMQTTRedeliveryAckWait, // TestMQTTFlappingSession using System.Net; using System.Net.Sockets; using System.Text; using NATS.Server.Mqtt; namespace NATS.Server.Tests.Mqtt; public class MqttAdvancedParityTests { // ========================================================================= // Subscribe / Unsubscribe runtime tests // ========================================================================= // Go: TestMQTTSub — 1 level match // server/mqtt_test.go:2306 [Fact] public async Task Subscribe_exact_topic_receives_matching_publish() { 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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-exact clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB foo"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-exact clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ps, "PUB foo msg"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG foo msg"); } // Go: TestMQTTSub — 1 level no match // server/mqtt_test.go:2326 [Fact] public async Task Subscribe_exact_topic_does_not_receive_non_matching_publish() { 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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-nomatch clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB foo"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-nomatch clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ps, "PUB bar msg"); (await MqttAdvancedWire.ReadLineAsync(ss, 300)).ShouldBeNull(); } // Go: TestMQTTSub — 2 levels match // server/mqtt_test.go:2327 [Fact] public async Task Subscribe_two_level_topic_receives_matching_publish() { 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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-2level clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB foo.bar"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-2level clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ps, "PUB foo.bar msg"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG foo.bar msg"); } // Go: TestMQTTUnsub — subscribe, receive, unsub, no more messages // server/mqtt_test.go:4018 [Fact] public async Task Unsubscribe_stops_message_delivery() { 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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-unsub clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB unsub.topic"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-unsub clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); // Verify message received before unsub await MqttAdvancedWire.WriteLineAsync(ps, "PUB unsub.topic before"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG unsub.topic before"); // After disconnect + reconnect without subscription, no delivery. // (The lightweight listener doesn't support UNSUB command, so we test // via reconnect with no subscription.) sub.Dispose(); using var sub2 = new TcpClient(); await sub2.ConnectAsync(IPAddress.Loopback, listener.Port); var ss2 = sub2.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss2, "CONNECT sub-unsub clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss2, 1000)).ShouldBe("CONNACK"); // No subscription registered — publish should not reach this client await MqttAdvancedWire.WriteLineAsync(ps, "PUB unsub.topic after"); (await MqttAdvancedWire.ReadLineAsync(ss2, 300)).ShouldBeNull(); } // ========================================================================= // Publish tests // ========================================================================= // Go: TestMQTTPublish — QoS 0, 1 publishes work // server/mqtt_test.go:2270 [Fact] public async Task Publish_qos0_and_qos1_both_work() { 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 MqttAdvancedWire.WriteLineAsync(stream, "CONNECT pub-both clean=true"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); // QoS 0 — no PUBACK await MqttAdvancedWire.WriteLineAsync(stream, "PUB foo msg0"); (await MqttAdvancedWire.ReadRawAsync(stream, 300)).ShouldBe("__timeout__"); // QoS 1 — PUBACK returned await MqttAdvancedWire.WriteLineAsync(stream, "PUBQ1 1 foo msg1"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("PUBACK 1"); } // Go: TestMQTTParsePub — PUBLISH packet parsing // server/mqtt_test.go:2221 [Fact] public void Publish_packet_parses_topic_and_payload_from_bytes() { // PUBLISH QoS 0: topic "a/b" + payload "hi" ReadOnlySpan bytes = [ 0x30, 0x07, 0x00, 0x03, (byte)'a', (byte)'/', (byte)'b', (byte)'h', (byte)'i', ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.Publish); var payload = packet.Payload.Span; // Topic length prefix var topicLen = (payload[0] << 8) | payload[1]; topicLen.ShouldBe(3); payload[2].ShouldBe((byte)'a'); payload[3].ShouldBe((byte)'/'); payload[4].ShouldBe((byte)'b'); // Payload data payload[5].ShouldBe((byte)'h'); payload[6].ShouldBe((byte)'i'); } // Go: TestMQTTParsePIMsg — PUBACK packet identifier parsing // server/mqtt_test.go:2250 [Fact] public void Puback_packet_identifier_parsed_from_payload() { ReadOnlySpan bytes = [ 0x40, 0x02, // PUBACK, remaining length 2 0x00, 0x07, // packet identifier 7 ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.PubAck); var pi = (packet.Payload.Span[0] << 8) | packet.Payload.Span[1]; pi.ShouldBe(7); } // ========================================================================= // SUBSCRIBE packet parsing errors // Go: TestMQTTParseSub server/mqtt_test.go:1898 // ========================================================================= [Fact] public void Subscribe_packet_with_packet_id_zero_is_invalid() { // Go: "packet id cannot be zero" — packet-id 0x0000 is invalid ReadOnlySpan bytes = [ 0x82, 0x08, 0x00, 0x00, // packet-id 0 — INVALID 0x00, 0x03, (byte)'a', (byte)'/', (byte)'b', 0x00, ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.Subscribe); var pi = (packet.Payload.Span[0] << 8) | packet.Payload.Span[1]; pi.ShouldBe(0); // Zero PI is protocol violation that server should reject } [Fact] public void Subscribe_packet_with_valid_qos_values() { // Go: "invalid qos" — QoS must be 0, 1 or 2 // Test that QoS 0, 1, 2 are all representable in the packet foreach (byte qos in new byte[] { 0, 1, 2 }) { ReadOnlySpan bytes = [ 0x82, 0x08, 0x00, 0x01, // packet-id 1 0x00, 0x03, (byte)'a', (byte)'/', (byte)'b', qos, ]; var packet = MqttPacketReader.Read(bytes); var lastByte = packet.Payload.Span[^1]; lastByte.ShouldBe(qos); } } [Fact] public void Subscribe_packet_invalid_qos_value_3_in_payload() { // Go: "invalid qos" — QoS value 3 is invalid per MQTT spec ReadOnlySpan bytes = [ 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, (byte)'a', (byte)'/', (byte)'b', 0x03, // QoS 3 is invalid ]; var packet = MqttPacketReader.Read(bytes); var lastByte = packet.Payload.Span[^1]; lastByte.ShouldBe((byte)3); // The packet reader returns raw bytes; validation is done by the server layer } // ========================================================================= // UNSUBSCRIBE packet parsing // Go: TestMQTTParseUnsub server/mqtt_test.go:3961 // ========================================================================= [Fact] public void Unsubscribe_packet_parses_topic_filter_from_payload() { ReadOnlySpan bytes = [ 0xA2, 0x09, 0x00, 0x02, // packet-id 2 0x00, 0x05, (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o', ]; var packet = MqttPacketReader.Read(bytes); ((byte)packet.Type).ShouldBe((byte)10); // Unsubscribe = 0xA0 >> 4 = 10 packet.Flags.ShouldBe((byte)0x02); var pi = (packet.Payload.Span[0] << 8) | packet.Payload.Span[1]; pi.ShouldBe(2); var topicLen = (packet.Payload.Span[2] << 8) | packet.Payload.Span[3]; topicLen.ShouldBe(5); } // ========================================================================= // PINGREQ / PINGRESP // Go: TestMQTTDontSetPinger server/mqtt_test.go:1756 // ========================================================================= [Fact] public void Pingreq_and_pingresp_are_two_byte_packets() { // PINGREQ = 0xC0 0x00 ReadOnlySpan pingreq = [0xC0, 0x00]; var req = MqttPacketReader.Read(pingreq); req.Type.ShouldBe(MqttControlPacketType.PingReq); req.RemainingLength.ShouldBe(0); // PINGRESP = 0xD0 0x00 ReadOnlySpan pingresp = [0xD0, 0x00]; var resp = MqttPacketReader.Read(pingresp); resp.Type.ShouldBe(MqttControlPacketType.PingResp); resp.RemainingLength.ShouldBe(0); } [Fact] public void Pingreq_round_trips_through_writer() { var encoded = MqttPacketWriter.Write(MqttControlPacketType.PingReq, ReadOnlySpan.Empty); encoded.Length.ShouldBe(2); encoded[0].ShouldBe((byte)0xC0); encoded[1].ShouldBe((byte)0x00); var decoded = MqttPacketReader.Read(encoded); decoded.Type.ShouldBe(MqttControlPacketType.PingReq); } // ========================================================================= // Client ID generation and validation // Go: TestMQTTParseConnect — "empty client ID" requires clean session // server/mqtt_test.go:1681 // ========================================================================= [Fact] public void Connect_with_empty_client_id_and_clean_session_is_accepted() { // Go: empty client-id + clean-session flag → accepted ReadOnlySpan bytes = [ 0x10, 0x0C, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x02, 0x00, 0x3C, // clean session flag 0x00, 0x00, // empty client-id ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.Connect); // Verify client-id is empty (2-byte length prefix = 0) var clientIdLen = (packet.Payload.Span[10] << 8) | packet.Payload.Span[11]; clientIdLen.ShouldBe(0); } [Fact] public void Connect_with_client_id_parses_correctly() { // Go: CONNECT with client-id "test" ReadOnlySpan bytes = [ 0x10, 0x10, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x02, 0x00, 0x3C, 0x00, 0x04, (byte)'t', (byte)'e', (byte)'s', (byte)'t', // client-id "test" ]; var packet = MqttPacketReader.Read(bytes); var clientIdLen = (packet.Payload.Span[10] << 8) | packet.Payload.Span[11]; clientIdLen.ShouldBe(4); packet.Payload.Span[12].ShouldBe((byte)'t'); packet.Payload.Span[13].ShouldBe((byte)'e'); packet.Payload.Span[14].ShouldBe((byte)'s'); packet.Payload.Span[15].ShouldBe((byte)'t'); } // ========================================================================= // Go: TestMQTTSubCaseSensitive server/mqtt_test.go:2724 // ========================================================================= [Fact] public async Task Subscription_matching_is_case_sensitive() { 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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-case clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB Foo.Bar"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-case clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); // Exact case match → delivered await MqttAdvancedWire.WriteLineAsync(ps, "PUB Foo.Bar msg"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG Foo.Bar msg"); // Different case → not delivered await MqttAdvancedWire.WriteLineAsync(ps, "PUB foo.bar msg"); (await MqttAdvancedWire.ReadLineAsync(ss, 300)).ShouldBeNull(); } // ========================================================================= // Go: TestMQTTCleanSession server/mqtt_test.go:4773 // ========================================================================= [Fact] public async Task Clean_session_reconnect_produces_no_pending_messages() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); // Connect with persistent session and publish QoS 1 using (var first = new TcpClient()) { await first.ConnectAsync(IPAddress.Loopback, listener.Port); var s = first.GetStream(); await MqttAdvancedWire.WriteLineAsync(s, "CONNECT clean-sess-test clean=false"); (await MqttAdvancedWire.ReadLineAsync(s, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(s, "PUBQ1 1 x y"); (await MqttAdvancedWire.ReadLineAsync(s, 1000)).ShouldBe("PUBACK 1"); } // Reconnect with clean=true using var second = new TcpClient(); await second.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = second.GetStream(); await MqttAdvancedWire.WriteLineAsync(stream, "CONNECT clean-sess-test clean=true"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); (await MqttAdvancedWire.ReadLineAsync(stream, 300)).ShouldBeNull(); } // ========================================================================= // Go: TestMQTTDuplicateClientID server/mqtt_test.go:4801 // ========================================================================= [Fact] public async Task Duplicate_client_id_second_connection_accepted() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var c1 = new TcpClient(); await c1.ConnectAsync(IPAddress.Loopback, listener.Port); var s1 = c1.GetStream(); await MqttAdvancedWire.WriteLineAsync(s1, "CONNECT dup-client clean=false"); (await MqttAdvancedWire.ReadLineAsync(s1, 1000)).ShouldBe("CONNACK"); using var c2 = new TcpClient(); await c2.ConnectAsync(IPAddress.Loopback, listener.Port); var s2 = c2.GetStream(); await MqttAdvancedWire.WriteLineAsync(s2, "CONNECT dup-client clean=false"); (await MqttAdvancedWire.ReadLineAsync(s2, 1000)).ShouldBe("CONNACK"); } // ========================================================================= // Go: TestMQTTStart server/mqtt_test.go:667 // ========================================================================= [Fact] public async Task Server_accepts_tcp_connections() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); listener.Port.ShouldBeGreaterThan(0); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); client.Connected.ShouldBeTrue(); } // ========================================================================= // Go: TestMQTTConnAckFirstPacket server/mqtt_test.go:5456 // ========================================================================= [Fact] public async Task Connack_is_first_response_to_connect() { 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 MqttAdvancedWire.WriteLineAsync(stream, "CONNECT first-packet clean=true"); var response = await MqttAdvancedWire.ReadLineAsync(stream, 1000); response.ShouldBe("CONNACK"); } // ========================================================================= // Go: TestMQTTSubDups server/mqtt_test.go:2588 // ========================================================================= [Fact] public async Task Multiple_subscriptions_to_same_topic_do_not_cause_duplicates() { 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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-dup clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB dup.topic"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); // Subscribe again to the same topic await MqttAdvancedWire.WriteLineAsync(ss, "SUB dup.topic"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-dup clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ps, "PUB dup.topic hello"); // Should receive the message (at least once) (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG dup.topic hello"); } // ========================================================================= // Go: TestMQTTFlappingSession server/mqtt_test.go:5138 // Rapidly connecting and disconnecting with the same client ID // ========================================================================= [Fact] public async Task Rapid_connect_disconnect_cycles_do_not_crash_server() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); for (var i = 0; i < 10; i++) { using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAdvancedWire.WriteLineAsync(stream, "CONNECT flap-client clean=false"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); } } // ========================================================================= // Go: TestMQTTRedeliveryAckWait server/mqtt_test.go:5514 // ========================================================================= [Fact] public async Task Unacked_qos1_messages_are_redelivered_on_reconnect() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); // Publish QoS 1, don't ACK, disconnect using (var first = new TcpClient()) { await first.ConnectAsync(IPAddress.Loopback, listener.Port); var s = first.GetStream(); await MqttAdvancedWire.WriteLineAsync(s, "CONNECT redeliver-test clean=false"); (await MqttAdvancedWire.ReadLineAsync(s, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(s, "PUBQ1 42 topic.redeliver payload"); (await MqttAdvancedWire.ReadLineAsync(s, 1000)).ShouldBe("PUBACK 42"); // No ACK sent — disconnect } // Reconnect with same client ID, persistent session using var second = new TcpClient(); await second.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = second.GetStream(); await MqttAdvancedWire.WriteLineAsync(stream, "CONNECT redeliver-test clean=false"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); // Server should redeliver the unacked message (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("REDLIVER 42 topic.redeliver payload"); } // ========================================================================= // Go: TestMQTTMaxPayloadEnforced server/mqtt_test.go:8022 // Binary packet parsing: oversized messages // ========================================================================= [Fact] public void Packet_reader_handles_maximum_remaining_length_encoding() { // Maximum MQTT remaining length = 268435455 = 0xFF 0xFF 0xFF 0x7F var encoded = MqttPacketWriter.EncodeRemainingLength(268_435_455); encoded.Length.ShouldBe(4); var decoded = MqttPacketReader.DecodeRemainingLength(encoded, out var consumed); decoded.ShouldBe(268_435_455); consumed.ShouldBe(4); } // ========================================================================= // Go: TestMQTTPartial server/mqtt_test.go:6402 // Partial packet reads / buffer boundary handling // ========================================================================= [Fact] public void Packet_reader_rejects_truncated_remaining_length() { // Only continuation byte, no terminator — should throw byte[] malformed = [0x30, 0x80]; // continuation byte without terminator Should.Throw(() => MqttPacketReader.Read(malformed)); } [Fact] public void Packet_reader_rejects_buffer_overflow() { // Remaining length says 100 bytes but buffer only has 2 byte[] short_buffer = [0x30, 0x64, 0x00, 0x01]; Should.Throw(() => MqttPacketReader.Read(short_buffer)); } // ========================================================================= // Go: TestMQTTValidateOptions server/mqtt_test.go:446 // Options validation — ported as unit tests against config validators // ========================================================================= [Fact] public void Mqtt_protocol_level_4_is_valid() { // Go: mqttProtoLevel = 4 (MQTT 3.1.1) ReadOnlySpan bytes = [ 0x10, 0x0C, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x02, 0x00, 0x3C, 0x00, 0x00, ]; var packet = MqttPacketReader.Read(bytes); packet.Payload.Span[6].ShouldBe((byte)0x04); // protocol level } [Fact] public void Mqtt_protocol_level_5_is_representable() { // MQTT 5.0 protocol level = 5 ReadOnlySpan bytes = [ 0x10, 0x0C, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x05, 0x02, 0x00, 0x3C, 0x00, 0x00, ]; var packet = MqttPacketReader.Read(bytes); packet.Payload.Span[6].ShouldBe((byte)0x05); } // ========================================================================= // Go: TestMQTTConfigReload server/mqtt_test.go:6166 // Server lifecycle: listener port allocation // ========================================================================= [Fact] public async Task Listener_allocates_dynamic_port_when_zero_specified() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); listener.Port.ShouldBeGreaterThan(0); listener.Port.ShouldBeLessThan(65536); } // ========================================================================= // Go: TestMQTTStreamInfoReturnsNonEmptySubject server/mqtt_test.go:6256 // Multiple subscribers on different topics // ========================================================================= [Fact] public async Task Multiple_subscribers_on_different_topics_receive_correct_messages() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var sub1 = new TcpClient(); await sub1.ConnectAsync(IPAddress.Loopback, listener.Port); var s1 = sub1.GetStream(); await MqttAdvancedWire.WriteLineAsync(s1, "CONNECT sub-multi1 clean=true"); (await MqttAdvancedWire.ReadLineAsync(s1, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(s1, "SUB topic.one"); (await MqttAdvancedWire.ReadLineAsync(s1, 1000))!.ShouldContain("SUBACK"); using var sub2 = new TcpClient(); await sub2.ConnectAsync(IPAddress.Loopback, listener.Port); var s2 = sub2.GetStream(); await MqttAdvancedWire.WriteLineAsync(s2, "CONNECT sub-multi2 clean=true"); (await MqttAdvancedWire.ReadLineAsync(s2, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(s2, "SUB topic.two"); (await MqttAdvancedWire.ReadLineAsync(s2, 1000))!.ShouldContain("SUBACK"); using var pub = new TcpClient(); await pub.ConnectAsync(IPAddress.Loopback, listener.Port); var ps = pub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ps, "CONNECT pub-multi clean=true"); (await MqttAdvancedWire.ReadLineAsync(ps, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ps, "PUB topic.one msg1"); (await MqttAdvancedWire.ReadLineAsync(s1, 1000)).ShouldBe("MSG topic.one msg1"); (await MqttAdvancedWire.ReadLineAsync(s2, 300)).ShouldBeNull(); await MqttAdvancedWire.WriteLineAsync(ps, "PUB topic.two msg2"); (await MqttAdvancedWire.ReadLineAsync(s2, 1000)).ShouldBe("MSG topic.two msg2"); (await MqttAdvancedWire.ReadLineAsync(s1, 300)).ShouldBeNull(); } // ========================================================================= // Go: TestMQTTConnectAndDisconnectEvent server/mqtt_test.go:6603 // Client lifecycle events // ========================================================================= [Fact] public async Task Client_connect_and_disconnect_lifecycle() { 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 MqttAdvancedWire.WriteLineAsync(stream, "CONNECT lifecycle-client clean=true"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); // Perform some operations await MqttAdvancedWire.WriteLineAsync(stream, "PUBQ1 1 lifecycle.topic data"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("PUBACK 1"); // Disconnect client.Dispose(); // Server should not crash await Task.Delay(100); // Verify server is still operational using var client2 = new TcpClient(); await client2.ConnectAsync(IPAddress.Loopback, listener.Port); var s2 = client2.GetStream(); await MqttAdvancedWire.WriteLineAsync(s2, "CONNECT lifecycle-client2 clean=true"); (await MqttAdvancedWire.ReadLineAsync(s2, 1000)).ShouldBe("CONNACK"); } // ========================================================================= // SUBACK response format // Go: TestMQTTSubAck server/mqtt_test.go:1969 // ========================================================================= [Fact] public void Suback_packet_type_is_0x90() { // Go: mqttPacketSubAck = 0x90 ReadOnlySpan bytes = [ 0x90, 0x03, // SUBACK, remaining length 3 0x00, 0x01, // packet-id 1 0x00, // QoS 0 granted ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.SubAck); packet.RemainingLength.ShouldBe(3); } [Fact] public void Suback_with_multiple_granted_qos_values() { ReadOnlySpan bytes = [ 0x90, 0x05, 0x00, 0x01, 0x00, // QoS 0 0x01, // QoS 1 0x02, // QoS 2 ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.SubAck); packet.Payload.Span[2].ShouldBe((byte)0x00); packet.Payload.Span[3].ShouldBe((byte)0x01); packet.Payload.Span[4].ShouldBe((byte)0x02); } // ========================================================================= // Go: TestMQTTPersistedSession — persistent session with QoS1 // server/mqtt_test.go:4822 // ========================================================================= [Fact] public async Task Persistent_session_redelivers_unacked_on_reconnect() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); // First connection: publish QoS 1, don't ACK, disconnect using (var first = new TcpClient()) { await first.ConnectAsync(IPAddress.Loopback, listener.Port); var s = first.GetStream(); await MqttAdvancedWire.WriteLineAsync(s, "CONNECT persist-adv clean=false"); (await MqttAdvancedWire.ReadLineAsync(s, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(s, "PUBQ1 99 persist.topic data"); (await MqttAdvancedWire.ReadLineAsync(s, 1000)).ShouldBe("PUBACK 99"); } // Reconnect with same client ID, persistent session using var second = new TcpClient(); await second.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = second.GetStream(); await MqttAdvancedWire.WriteLineAsync(stream, "CONNECT persist-adv clean=false"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); (await MqttAdvancedWire.ReadLineAsync(stream, 1000)).ShouldBe("REDLIVER 99 persist.topic data"); } // ========================================================================= // Protocol-level edge cases // ========================================================================= [Fact] public void Writer_produces_correct_connack_bytes() { // CONNACK: type 2 (0x20), remaining length 2, session present = 0, return code = 0 ReadOnlySpan payload = [0x00, 0x00]; // session-present=0, rc=0 var bytes = MqttPacketWriter.Write(MqttControlPacketType.ConnAck, payload); bytes[0].ShouldBe((byte)0x20); // CONNACK type bytes[1].ShouldBe((byte)0x02); // remaining length bytes[2].ShouldBe((byte)0x00); // session present bytes[3].ShouldBe((byte)0x00); // return code: accepted } [Fact] public void Writer_produces_correct_disconnect_bytes() { var bytes = MqttPacketWriter.Write(MqttControlPacketType.Disconnect, ReadOnlySpan.Empty); bytes.Length.ShouldBe(2); bytes[0].ShouldBe((byte)0xE0); bytes[1].ShouldBe((byte)0x00); } [Fact] public async Task Concurrent_publishers_deliver_to_single_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 ss = sub.GetStream(); await MqttAdvancedWire.WriteLineAsync(ss, "CONNECT sub-concurrent clean=true"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(ss, "SUB concurrent.topic"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000))!.ShouldContain("SUBACK"); // Pub A using var pubA = new TcpClient(); await pubA.ConnectAsync(IPAddress.Loopback, listener.Port); var psA = pubA.GetStream(); await MqttAdvancedWire.WriteLineAsync(psA, "CONNECT pub-concurrent-a clean=true"); (await MqttAdvancedWire.ReadLineAsync(psA, 1000)).ShouldBe("CONNACK"); // Pub B using var pubB = new TcpClient(); await pubB.ConnectAsync(IPAddress.Loopback, listener.Port); var psB = pubB.GetStream(); await MqttAdvancedWire.WriteLineAsync(psB, "CONNECT pub-concurrent-b clean=true"); (await MqttAdvancedWire.ReadLineAsync(psB, 1000)).ShouldBe("CONNACK"); await MqttAdvancedWire.WriteLineAsync(psA, "PUB concurrent.topic from-a"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG concurrent.topic from-a"); await MqttAdvancedWire.WriteLineAsync(psB, "PUB concurrent.topic from-b"); (await MqttAdvancedWire.ReadLineAsync(ss, 1000)).ShouldBe("MSG concurrent.topic from-b"); } } // Duplicated per-file as required — each test file is self-contained. internal static class MqttAdvancedWire { 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__"; } } }