// Go: TestMonitorConnzWithRoutes server/monitor_test.go:1405
// Go: TestMonitorRoutezRace server/monitor_test.go:2210
// Go: TestMonitorRoutezRTT server/monitor_test.go:3919
// Go: TestMonitorRoutezPoolSize server/monitor_test.go:5705
// Go: TestMonitorClusterEmptyWhenNotDefined server/monitor_test.go:2456
using System.Net;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.Monitoring;
namespace NATS.Server.Tests.Monitoring;
///
/// Tests covering /routez endpoint behavior, ported from the Go server's monitor_test.go.
///
public class MonitorRoutezTests
{
///
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies that /routez returns valid JSON with routes and num_routes fields.
///
[Fact]
public async Task Routez_returns_routes_and_num_routes()
{
await using var fx = await RoutezFixture.StartAsync();
var body = await fx.GetStringAsync("/routez");
body.ShouldContain("routes");
body.ShouldContain("num_routes");
}
///
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez num_routes is 0 when no cluster routes are configured.
///
[Fact]
public async Task Routez_num_routes_is_zero_without_cluster()
{
await using var fx = await RoutezFixture.StartAsync();
var doc = await fx.GetJsonDocumentAsync("/routez");
doc.RootElement.GetProperty("num_routes").GetInt32().ShouldBe(0);
}
///
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /connz does not include route connections (they appear under /routez only).
///
[Fact]
public async Task Connz_does_not_include_route_connections()
{
await using var fx = await RoutezFixture.StartAsync();
var connz = await fx.GetFromJsonAsync("/connz");
connz.ShouldNotBeNull();
// Without any clients, connz should be empty
connz.NumConns.ShouldBe(0);
}
///
/// Go: TestMonitorRoutezRace (line 2210).
/// Verifies concurrent /routez requests do not cause errors or data corruption.
///
[Fact]
public async Task Routez_handles_concurrent_requests()
{
await using var fx = await RoutezFixture.StartAsync();
var tasks = Enumerable.Range(0, 10).Select(async _ =>
{
var response = await fx.GetAsync("/routez");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
});
await Task.WhenAll(tasks);
}
///
/// Go: TestMonitorClusterEmptyWhenNotDefined (line 2456).
/// Verifies /varz cluster section has empty name when no cluster is configured.
///
[Fact]
public async Task Varz_cluster_empty_when_not_defined()
{
await using var fx = await RoutezFixture.StartAsync();
var varz = await fx.GetFromJsonAsync("/varz");
varz.ShouldNotBeNull();
varz.Cluster.ShouldNotBeNull();
varz.Cluster.Name.ShouldBe("");
}
///
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez JSON field naming matches Go server format.
///
[Fact]
public async Task Routez_json_uses_expected_field_names()
{
await using var fx = await RoutezFixture.StartAsync();
var body = await fx.GetStringAsync("/routez");
body.ShouldContain("\"routes\"");
body.ShouldContain("\"num_routes\"");
}
///
/// Go: TestMonitorCluster (line 2724).
/// Verifies /varz includes cluster section even when cluster is enabled.
/// Note: The .NET server currently initializes the cluster section with defaults;
/// the Go server populates it with cluster config. This test verifies the section exists.
///
[Fact]
public async Task Varz_includes_cluster_section_when_cluster_enabled()
{
await using var fx = await RoutezFixture.StartWithClusterAsync();
var varz = await fx.GetFromJsonAsync("/varz");
varz.ShouldNotBeNull();
varz.Cluster.ShouldNotBeNull();
}
///
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez response includes routes field even when num_routes is 0.
///
[Fact]
public async Task Routez_includes_routes_field_even_when_empty()
{
await using var fx = await RoutezFixture.StartAsync();
var doc = await fx.GetJsonDocumentAsync("/routez");
doc.RootElement.TryGetProperty("routes", out _).ShouldBeTrue();
}
///
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez returns HTTP 200 OK.
///
[Fact]
public async Task Routez_returns_http_200()
{
await using var fx = await RoutezFixture.StartAsync();
var response = await fx.GetAsync("/routez");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
///
/// Go: TestMonitorCluster (line 2724).
/// Verifies /routez endpoint is accessible when cluster is configured.
///
[Fact]
public async Task Routez_accessible_with_cluster_config()
{
await using var fx = await RoutezFixture.StartWithClusterAsync();
var response = await fx.GetAsync("/routez");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var body = await response.Content.ReadAsStringAsync();
body.ShouldContain("routes");
}
}
internal sealed class RoutezFixture : IAsyncDisposable
{
private readonly NatsServer _server;
private readonly CancellationTokenSource _cts;
private readonly HttpClient _http;
private readonly int _monitorPort;
private RoutezFixture(NatsServer server, CancellationTokenSource cts, HttpClient http, int monitorPort)
{
_server = server;
_cts = cts;
_http = http;
_monitorPort = monitorPort;
}
public static async Task StartAsync()
{
var monitorPort = GetFreePort();
var options = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
MonitorPort = monitorPort,
};
return await CreateAndStartAsync(options, monitorPort);
}
public static async Task StartWithClusterAsync()
{
var monitorPort = GetFreePort();
var options = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
MonitorPort = monitorPort,
Cluster = new ClusterOptions
{
Host = "127.0.0.1",
Port = 0,
Name = "test-cluster",
},
};
return await CreateAndStartAsync(options, monitorPort);
}
private static async Task CreateAndStartAsync(NatsOptions options, int monitorPort)
{
var server = new NatsServer(options, NullLoggerFactory.Instance);
var cts = new CancellationTokenSource();
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
var http = new HttpClient();
for (var i = 0; i < 50; i++)
{
try
{
var response = await http.GetAsync($"http://127.0.0.1:{monitorPort}/healthz");
if (response.IsSuccessStatusCode) break;
}
catch { }
await Task.Delay(50);
}
return new RoutezFixture(server, cts, http, monitorPort);
}
public Task GetStringAsync(string path)
=> _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}{path}");
public Task GetAsync(string path)
=> _http.GetAsync($"http://127.0.0.1:{_monitorPort}{path}");
public Task GetFromJsonAsync(string path)
=> _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}{path}");
public async Task GetJsonDocumentAsync(string path)
{
var body = await GetStringAsync(path);
return JsonDocument.Parse(body);
}
public async ValueTask DisposeAsync()
{
_http.Dispose();
await _cts.CancelAsync();
_server.Dispose();
_cts.Dispose();
}
private static int GetFreePort()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
return ((IPEndPoint)socket.LocalEndPoint!).Port;
}
}