feat: Wave 6 batch 1 — monitoring, config reload, client protocol, MQTT, leaf node tests
Port 405 new test methods across 5 subsystems for Go parity: - Monitoring: 102 tests (varz, connz, routez, subsz, stacksz) - Leaf Nodes: 85 tests (connection, forwarding, loop detection, subject filter, JetStream) - MQTT Bridge: 86 tests (advanced, auth, retained messages, topic mapping, will messages) - Client Protocol: 73 tests (connection handling, protocol violations, limits) - Config Reload: 59 tests (hot reload, option changes, permission updates) Total: 1,678 tests passing, 0 failures, 3 skipped
This commit is contained in:
264
tests/NATS.Server.Tests/Mqtt/MqttWillMessageParityTests.cs
Normal file
264
tests/NATS.Server.Tests/Mqtt/MqttWillMessageParityTests.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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<byte> 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<byte> 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<byte> 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<string?> ReadLineAsync(NetworkStream stream, int timeoutMs)
|
||||
{
|
||||
using var timeout = new CancellationTokenSource(timeoutMs);
|
||||
var bytes = new List<byte>();
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user