Files
natsdotnet/tests/NATS.Server.Tests/Mqtt/MqttAuthParityTests.cs
Joseph Doherty 9554d53bf5 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
2026-02-23 21:40:29 -05:00

368 lines
13 KiB
C#

// Ports MQTT authentication behavior from Go reference:
// golang/nats-server/server/mqtt_test.go — TestMQTTBasicAuth, TestMQTTTokenAuth,
// TestMQTTAuthTimeout, TestMQTTUsersAuth, TestMQTTNoAuthUser,
// TestMQTTConnectNotFirstPacket, TestMQTTSecondConnect, TestMQTTParseConnect,
// TestMQTTConnKeepAlive
using System.Net;
using System.Net.Sockets;
using System.Text;
using NATS.Server.Auth;
using NATS.Server.Mqtt;
namespace NATS.Server.Tests.Mqtt;
public class MqttAuthParityTests
{
// Go ref: TestMQTTBasicAuth — correct credentials accepted
// server/mqtt_test.go:1159
[Fact]
public async Task Correct_mqtt_credentials_connect_accepted()
{
await using var listener = new MqttListener(
"127.0.0.1", 0,
requiredUsername: "mqtt",
requiredPassword: "client");
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-ok clean=true user=mqtt pass=client");
(await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
}
// Go ref: TestMQTTBasicAuth — wrong credentials rejected
[Fact]
public async Task Wrong_mqtt_credentials_connect_rejected()
{
await using var listener = new MqttListener(
"127.0.0.1", 0,
requiredUsername: "mqtt",
requiredPassword: "client");
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-fail clean=true user=wrong pass=client");
var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
response.ShouldNotBeNull();
response!.ShouldContain("ERR");
}
// Go ref: TestMQTTBasicAuth — wrong password rejected
[Fact]
public async Task Wrong_password_connect_rejected()
{
await using var listener = new MqttListener(
"127.0.0.1", 0,
requiredUsername: "mqtt",
requiredPassword: "secret");
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-badpass clean=true user=mqtt pass=wrong");
var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
response.ShouldNotBeNull();
response!.ShouldContain("ERR");
}
// Go ref: TestMQTTBasicAuth — no auth configured, any credentials accepted
[Fact]
public async Task No_auth_configured_connects_without_credentials()
{
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT no-auth-client clean=true");
(await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
}
[Fact]
public async Task No_auth_configured_accepts_any_credentials()
{
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT any-creds clean=true user=whatever pass=doesntmatter");
(await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
}
// =========================================================================
// Go: TestMQTTTokenAuth — ValidateMqttCredentials tests
// server/mqtt_test.go:1307
// =========================================================================
[Fact]
public void ValidateMqttCredentials_returns_true_when_no_auth_configured()
{
AuthService.ValidateMqttCredentials(null, null, null, null).ShouldBeTrue();
AuthService.ValidateMqttCredentials(null, null, "anything", "anything").ShouldBeTrue();
AuthService.ValidateMqttCredentials(string.Empty, string.Empty, null, null).ShouldBeTrue();
}
[Fact]
public void ValidateMqttCredentials_returns_true_for_matching_credentials()
{
AuthService.ValidateMqttCredentials("mqtt", "client", "mqtt", "client").ShouldBeTrue();
}
[Fact]
public void ValidateMqttCredentials_returns_false_for_wrong_username()
{
AuthService.ValidateMqttCredentials("mqtt", "client", "wrong", "client").ShouldBeFalse();
}
[Fact]
public void ValidateMqttCredentials_returns_false_for_wrong_password()
{
AuthService.ValidateMqttCredentials("mqtt", "client", "mqtt", "wrong").ShouldBeFalse();
}
[Fact]
public void ValidateMqttCredentials_returns_false_for_null_credentials_when_auth_configured()
{
AuthService.ValidateMqttCredentials("mqtt", "client", null, null).ShouldBeFalse();
}
[Fact]
public void ValidateMqttCredentials_case_sensitive_comparison()
{
AuthService.ValidateMqttCredentials("MQTT", "Client", "mqtt", "client").ShouldBeFalse();
AuthService.ValidateMqttCredentials("MQTT", "Client", "MQTT", "Client").ShouldBeTrue();
}
// =========================================================================
// Go: TestMQTTUsersAuth — multiple users
// server/mqtt_test.go:1466
// =========================================================================
[Fact]
public async Task Multiple_clients_with_different_credentials_authenticate_independently()
{
await using var listener = new MqttListener(
"127.0.0.1", 0,
requiredUsername: "admin",
requiredPassword: "password");
using var cts = new CancellationTokenSource();
await listener.StartAsync(cts.Token);
using var client1 = new TcpClient();
await client1.ConnectAsync(IPAddress.Loopback, listener.Port);
var s1 = client1.GetStream();
await MqttAuthWire.WriteLineAsync(s1, "CONNECT user1 clean=true user=admin pass=password");
(await MqttAuthWire.ReadLineAsync(s1, 1000)).ShouldBe("CONNACK");
using var client2 = new TcpClient();
await client2.ConnectAsync(IPAddress.Loopback, listener.Port);
var s2 = client2.GetStream();
await MqttAuthWire.WriteLineAsync(s2, "CONNECT user2 clean=true user=admin pass=wrong");
var response = await MqttAuthWire.ReadLineAsync(s2, 1000);
response.ShouldNotBeNull();
response!.ShouldContain("ERR");
await MqttAuthWire.WriteLineAsync(s1, "PUBQ1 1 auth.test ok");
(await MqttAuthWire.ReadLineAsync(s1, 1000)).ShouldBe("PUBACK 1");
}
// =========================================================================
// Go: TestMQTTConnKeepAlive server/mqtt_test.go:1741
// =========================================================================
[Fact]
public async Task Keepalive_timeout_disconnects_idle_client()
{
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT keepalive-client clean=true keepalive=1");
(await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
await Task.Delay(2500);
var result = await MqttAuthWire.ReadRawAsync(stream, 500);
(result == null || result == "__timeout__").ShouldBeTrue();
}
// =========================================================================
// Go: TestMQTTParseConnect — username/password flags
// server/mqtt_test.go:1661
// =========================================================================
[Fact]
public void Connect_packet_with_username_flag_has_username_in_payload()
{
ReadOnlySpan<byte> bytes =
[
0x10, 0x10,
0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
0x04, 0x82, 0x00, 0x3C,
0x00, 0x01, (byte)'c',
0x00, 0x01, (byte)'u',
];
var packet = MqttPacketReader.Read(bytes);
packet.Type.ShouldBe(MqttControlPacketType.Connect);
var connectFlags = packet.Payload.Span[7];
(connectFlags & 0x80).ShouldNotBe(0);
}
[Fact]
public void Connect_packet_with_username_and_password_flags()
{
ReadOnlySpan<byte> bytes =
[
0x10, 0x13,
0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
0x04, 0xC2, 0x00, 0x3C,
0x00, 0x01, (byte)'c',
0x00, 0x01, (byte)'u',
0x00, 0x01, (byte)'p',
];
var packet = MqttPacketReader.Read(bytes);
var connectFlags = packet.Payload.Span[7];
(connectFlags & 0x80).ShouldNotBe(0); // username flag
(connectFlags & 0x40).ShouldNotBe(0); // password flag
}
// Go: TestMQTTParseConnect — "no user but password" server/mqtt_test.go:1678
[Fact]
public void Connect_flags_password_without_user_is_protocol_violation()
{
byte connectFlags = 0x40;
(connectFlags & 0x80).ShouldBe(0);
(connectFlags & 0x40).ShouldNotBe(0);
}
// Go: TestMQTTParseConnect — "reserved flag" server/mqtt_test.go:1674
[Fact]
public void Connect_flags_reserved_bit_must_be_zero()
{
byte connectFlags = 0x01;
(connectFlags & 0x01).ShouldNotBe(0);
}
// =========================================================================
// Go: TestMQTTConnectNotFirstPacket server/mqtt_test.go:1618
// =========================================================================
[Fact]
public async Task Non_connect_as_first_packet_is_handled()
{
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 MqttAuthWire.WriteLineAsync(stream, "PUB some.topic hello");
var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
if (response != null)
{
response.ShouldNotBe("CONNACK");
}
}
// Go: TestMQTTSecondConnect server/mqtt_test.go:1645
[Fact]
public async Task Second_connect_from_same_tcp_connection_is_handled()
{
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 MqttAuthWire.WriteLineAsync(stream, "CONNECT second-conn clean=true");
(await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
await MqttAuthWire.WriteLineAsync(stream, "CONNECT second-conn clean=true");
var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
_ = response; // Just verify no crash
}
}
internal static class MqttAuthWire
{
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]);
}
public static async Task<string?> 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__";
}
}
}