Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/ServerBootTests.cs
Joseph Doherty 6fb7f43335 feat(mqtt): add MQTT listener, client creation, and shutdown wiring
Wire up the MQTT server-side orchestration layer (Task 1 of 7):
- Create NatsServer.Mqtt.cs with StartMqttListener(), CreateMqttClient(), MqttAddr()
- Forward MqttHandler.StartMqtt() to server.StartMqttListener()
- Add _mqttListener to Shutdown() doneExpected counting
- Fix ReadyForConnections to recognize active MQTT listener
- Handle RandomPort (-1) as ephemeral for MQTT listener
- Remove duplicate Mqtt field from ClientConnection.cs (already in ClientConnection.Mqtt.cs)
- Add 2 MQTT boot integration tests (accept + shutdown lifecycle)
2026-03-01 15:35:41 -05:00

188 lines
5.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>
/// 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();
}
}