Implement Task 3 of MQTT orchestration: - Create MqttPacketHandlers.cs with ParseConnect(), ProcessConnect(), EnqueueConnAck(), HandleDisconnect() - Wire CONNECT and DISCONNECT dispatch in MqttParser.cs - Parse CONNECT: protocol name/level, flags, keep-alive, client ID, will, auth - Send CONNACK (4-byte fixed packet with return code) - DISCONNECT clears will message and closes connection cleanly - Auto-generate client ID for empty ID + clean session - Validate reserved bit, will flags, username/password consistency - Add Reader field to MqttHandler for per-connection parsing - 11 unit tests for CONNECT parsing and processing - 1 end-to-end integration test: TCP → CONNECT → CONNACK over the wire
278 lines
8.9 KiB
C#
278 lines
8.9 KiB
C#
// Copyright 2013-2026 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
using System.Net;
|
|
using System.Text;
|
|
using Shouldly;
|
|
using ZB.MOM.NatsNet.Server;
|
|
|
|
namespace ZB.MOM.NatsNet.Server.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// End-to-end server boot tests that validate the full
|
|
/// Start() → AcceptLoop → client connection lifecycle.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public sealed class ServerBootTests : IDisposable
|
|
{
|
|
private readonly string _storeDir;
|
|
|
|
public ServerBootTests()
|
|
{
|
|
_storeDir = Path.Combine(Path.GetTempPath(), $"natsnet-test-{Guid.NewGuid():N}");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_storeDir, true); } catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a server can boot, accept a TCP connection, and send
|
|
/// a NATS protocol INFO line. This proves the full Start() → AcceptLoop
|
|
/// → CreateClient pipeline works end-to-end.
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Validates that a server can boot, bind a port, and accept a TCP connection.
|
|
/// Note: GenerateClientInfoJSON is currently a stub (returns empty), so we only
|
|
/// verify the TCP handshake succeeds — not the INFO protocol line.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ServerBoot_AcceptsTcpConnection_ShouldSucceed()
|
|
{
|
|
// Arrange — create server with ephemeral port
|
|
var opts = new ServerOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0, // ephemeral
|
|
};
|
|
|
|
var (server, err) = NatsServer.NewServer(opts);
|
|
err.ShouldBeNull("NewServer should succeed");
|
|
server.ShouldNotBeNull();
|
|
|
|
try
|
|
{
|
|
// Act — start the server
|
|
server!.Start();
|
|
|
|
// Get the actual bound port
|
|
var addr = server.Addr() as IPEndPoint;
|
|
addr.ShouldNotBeNull("Server should have a listener address after Start()");
|
|
addr!.Port.ShouldBeGreaterThan(0);
|
|
|
|
// Connect a raw TCP client — proves AcceptLoop is working
|
|
using var tcp = new System.Net.Sockets.TcpClient();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
await tcp.ConnectAsync(addr.Address, addr.Port, cts.Token);
|
|
|
|
tcp.Connected.ShouldBeTrue();
|
|
|
|
// Verify the server registered the client
|
|
await Task.Delay(100); // Give CreateClient a moment to run
|
|
server.NumClients().ShouldBeGreaterThan(0);
|
|
}
|
|
finally
|
|
{
|
|
server!.Shutdown();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a server can boot with an MQTT listener on an ephemeral port,
|
|
/// accept a TCP connection on the MQTT port, and register it as a client.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task MqttBoot_AcceptsConnection_ShouldSucceed()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
Mqtt = { Port = -1, Host = "127.0.0.1" },
|
|
};
|
|
|
|
var (server, err) = NatsServer.NewServer(opts);
|
|
err.ShouldBeNull("NewServer should succeed");
|
|
server.ShouldNotBeNull();
|
|
|
|
try
|
|
{
|
|
server!.Start();
|
|
|
|
// Verify MQTT listener is up
|
|
var mqttAddr = server.MqttAddr();
|
|
mqttAddr.ShouldNotBeNull("MqttAddr should return the MQTT listener address");
|
|
mqttAddr!.Port.ShouldBeGreaterThan(0);
|
|
|
|
// ReadyForConnections should include MQTT
|
|
server.ReadyForConnections(TimeSpan.FromSeconds(5)).ShouldBeTrue();
|
|
|
|
// Connect a raw TCP client to the MQTT port
|
|
using var tcp = new System.Net.Sockets.TcpClient();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
await tcp.ConnectAsync(mqttAddr.Address, mqttAddr.Port, cts.Token);
|
|
tcp.Connected.ShouldBeTrue();
|
|
|
|
// Give CreateMqttClient a moment to register
|
|
await Task.Delay(100);
|
|
server.NumClients().ShouldBeGreaterThan(0);
|
|
}
|
|
finally
|
|
{
|
|
server!.Shutdown();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that an MQTT listener starts and shuts down cleanly.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MqttBoot_StartAndShutdown_ShouldSucceed()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
DontListen = true,
|
|
Mqtt = { Port = -1, Host = "127.0.0.1" },
|
|
};
|
|
|
|
var (server, err) = NatsServer.NewServer(opts);
|
|
err.ShouldBeNull();
|
|
server.ShouldNotBeNull();
|
|
|
|
server!.Start();
|
|
server.Running().ShouldBeTrue();
|
|
server.MqttAddr().ShouldNotBeNull();
|
|
|
|
server.Shutdown();
|
|
server.Running().ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// End-to-end: TCP connect → send MQTT CONNECT → receive CONNACK.
|
|
/// Validates the full MQTT handshake over the wire.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task MqttBoot_ConnectHandshake_ShouldReceiveConnAck()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
Mqtt = { Port = -1, Host = "127.0.0.1" },
|
|
};
|
|
|
|
var (server, err) = NatsServer.NewServer(opts);
|
|
err.ShouldBeNull();
|
|
server.ShouldNotBeNull();
|
|
|
|
try
|
|
{
|
|
server!.Start();
|
|
var mqttAddr = server.MqttAddr();
|
|
mqttAddr.ShouldNotBeNull();
|
|
|
|
using var tcp = new System.Net.Sockets.TcpClient();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
await tcp.ConnectAsync(mqttAddr!.Address, mqttAddr.Port, cts.Token);
|
|
|
|
var stream = tcp.GetStream();
|
|
|
|
// Build and send MQTT CONNECT packet.
|
|
var connectPacket = BuildMqttConnectPacket("integration-test");
|
|
await stream.WriteAsync(connectPacket, cts.Token);
|
|
await stream.FlushAsync(cts.Token);
|
|
|
|
// Read CONNACK response (4 bytes).
|
|
var response = new byte[4];
|
|
var totalRead = 0;
|
|
while (totalRead < 4)
|
|
{
|
|
var n = await stream.ReadAsync(response.AsMemory(totalRead, 4 - totalRead), cts.Token);
|
|
if (n == 0) break;
|
|
totalRead += n;
|
|
}
|
|
|
|
totalRead.ShouldBe(4, "Should receive 4-byte CONNACK");
|
|
response[0].ShouldBe((byte)0x20, "Packet type should be CONNACK");
|
|
response[1].ShouldBe((byte)0x02, "Remaining length should be 2");
|
|
response[3].ShouldBe((byte)0x00, "Return code should be Accepted (0)");
|
|
}
|
|
finally
|
|
{
|
|
server!.Shutdown();
|
|
}
|
|
}
|
|
|
|
/// <summary>Builds a minimal MQTT CONNECT packet.</summary>
|
|
private static byte[] BuildMqttConnectPacket(string clientId)
|
|
{
|
|
var payload = new List<byte>();
|
|
// Protocol name "MQTT"
|
|
payload.AddRange(new byte[] { 0x00, 0x04 });
|
|
payload.AddRange(System.Text.Encoding.UTF8.GetBytes("MQTT"));
|
|
// Protocol level 4
|
|
payload.Add(0x04);
|
|
// Flags: clean session
|
|
payload.Add(0x02);
|
|
// Keep alive: 60s
|
|
payload.AddRange(new byte[] { 0x00, 0x3C });
|
|
// Client ID
|
|
var cidBytes = System.Text.Encoding.UTF8.GetBytes(clientId);
|
|
payload.Add((byte)(cidBytes.Length >> 8));
|
|
payload.Add((byte)(cidBytes.Length & 0xFF));
|
|
payload.AddRange(cidBytes);
|
|
|
|
// Fixed header
|
|
var result = new List<byte>();
|
|
result.Add(0x10); // CONNECT type
|
|
var remLen = payload.Count;
|
|
do
|
|
{
|
|
var b = (byte)(remLen & 0x7F);
|
|
remLen >>= 7;
|
|
if (remLen > 0) b |= 0x80;
|
|
result.Add(b);
|
|
} while (remLen > 0);
|
|
result.AddRange(payload);
|
|
return result.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that Shutdown() after Start() completes cleanly.
|
|
/// Uses DontListen to skip TCP binding — tests lifecycle only.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ServerBoot_StartAndShutdown_ShouldSucceed()
|
|
{
|
|
var opts = new ServerOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 0,
|
|
DontListen = true,
|
|
};
|
|
|
|
var (server, err) = NatsServer.NewServer(opts);
|
|
err.ShouldBeNull();
|
|
server.ShouldNotBeNull();
|
|
|
|
server!.Start();
|
|
server.Running().ShouldBeTrue();
|
|
|
|
server.Shutdown();
|
|
server.Running().ShouldBeFalse();
|
|
}
|
|
}
|