// 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; /// /// End-to-end server boot tests that validate the full /// Start() → AcceptLoop → client connection lifecycle. /// [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 { } } /// /// 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. /// /// /// 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. /// [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(); } } /// /// 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. /// [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(); } } /// /// Validates that an MQTT listener starts and shuts down cleanly. /// [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(); } /// /// End-to-end: TCP connect → send MQTT CONNECT → receive CONNACK. /// Validates the full MQTT handshake over the wire. /// [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(); } } /// Builds a minimal MQTT CONNECT packet. private static byte[] BuildMqttConnectPacket(string clientId) { var payload = new List(); // 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(); 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(); } /// /// End-to-end: CONNECT → SUBSCRIBE → verify SUBACK over the wire. /// [Fact] public async Task MqttBoot_SubscribeHandshake_ShouldReceiveSubAck() { 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(); // 1. CONNECT → CONNACK var connectPacket = BuildMqttConnectPacket("sub-test"); await stream.WriteAsync(connectPacket, cts.Token); await stream.FlushAsync(cts.Token); var connack = new byte[4]; await ReadExactAsync(stream, connack, cts.Token); connack[0].ShouldBe((byte)0x20); // CONNACK connack[3].ShouldBe((byte)0x00); // Accepted // 2. SUBSCRIBE to "test/sub" QoS 0 → SUBACK var subscribePacket = BuildMqttSubscribePacket(packetId: 1, topic: "test/sub", qos: 0); await stream.WriteAsync(subscribePacket, cts.Token); await stream.FlushAsync(cts.Token); // SUBACK: [0x90] [0x03] [PI high] [PI low] [granted QoS] var suback = new byte[5]; await ReadExactAsync(stream, suback, cts.Token); suback[0].ShouldBe((byte)0x90); // SUBACK suback[1].ShouldBe((byte)0x03); // remaining length = 3 suback[2].ShouldBe((byte)0x00); // PI high suback[3].ShouldBe((byte)0x01); // PI low = 1 suback[4].ShouldBe((byte)0x00); // granted QoS 0 } finally { server!.Shutdown(); } } /// Builds a minimal MQTT SUBSCRIBE packet. private static byte[] BuildMqttSubscribePacket(ushort packetId, string topic, byte qos) { var topicBytes = System.Text.Encoding.UTF8.GetBytes(topic); var payload = new List(); payload.Add((byte)(packetId >> 8)); payload.Add((byte)(packetId & 0xFF)); payload.Add((byte)(topicBytes.Length >> 8)); payload.Add((byte)(topicBytes.Length & 0xFF)); payload.AddRange(topicBytes); payload.Add(qos); var result = new List(); result.Add(0x82); // SUBSCRIBE + flags 0x02 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(); } /// Reads exactly .Length bytes from the stream. private static async Task ReadExactAsync(System.Net.Sockets.NetworkStream stream, byte[] buffer, CancellationToken ct) { var totalRead = 0; while (totalRead < buffer.Length) { var n = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead), ct); if (n == 0) break; totalRead += n; } totalRead.ShouldBe(buffer.Length); } /// /// Validates that Shutdown() after Start() completes cleanly. /// Uses DontListen to skip TCP binding — tests lifecycle only. /// [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(); } }