Files
natsdotnet/tests/NATS.Server.Monitoring.Tests/Monitoring/MonitorRoutezTests.cs
Joseph Doherty 0c086522a4 refactor: extract NATS.Server.Monitoring.Tests project
Move 39 monitoring, events, and system endpoint test files from
NATS.Server.Tests into a dedicated NATS.Server.Monitoring.Tests project.
Update namespaces, replace private GetFreePort/ReadUntilAsync with
TestUtilities shared helpers, add InternalsVisibleTo, and register
in the solution file. All 439 tests pass.
2026-03-12 15:44:12 -04:00

264 lines
8.3 KiB
C#

// 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;
using NATS.Server.TestUtilities;
namespace NATS.Server.Monitoring.Tests.Monitoring;
/// <summary>
/// Tests covering /routez endpoint behavior, ported from the Go server's monitor_test.go.
/// </summary>
public class MonitorRoutezTests
{
/// <summary>
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies that /routez returns valid JSON with routes and num_routes fields.
/// </summary>
[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");
}
/// <summary>
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez num_routes is 0 when no cluster routes are configured.
/// </summary>
[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);
}
/// <summary>
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /connz does not include route connections (they appear under /routez only).
/// </summary>
[Fact]
public async Task Connz_does_not_include_route_connections()
{
await using var fx = await RoutezFixture.StartAsync();
var connz = await fx.GetFromJsonAsync<Connz>("/connz");
connz.ShouldNotBeNull();
// Without any clients, connz should be empty
connz.NumConns.ShouldBe(0);
}
/// <summary>
/// Go: TestMonitorRoutezRace (line 2210).
/// Verifies concurrent /routez requests do not cause errors or data corruption.
/// </summary>
[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);
}
/// <summary>
/// Go: TestMonitorClusterEmptyWhenNotDefined (line 2456).
/// Verifies /varz cluster section has empty name when no cluster is configured.
/// </summary>
[Fact]
public async Task Varz_cluster_empty_when_not_defined()
{
await using var fx = await RoutezFixture.StartAsync();
var varz = await fx.GetFromJsonAsync<Varz>("/varz");
varz.ShouldNotBeNull();
varz.Cluster.ShouldNotBeNull();
varz.Cluster.Name.ShouldBe("");
}
/// <summary>
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez JSON field naming matches Go server format.
/// </summary>
[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\"");
}
/// <summary>
/// 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.
/// </summary>
[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");
varz.ShouldNotBeNull();
varz.Cluster.ShouldNotBeNull();
}
/// <summary>
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez response includes routes field even when num_routes is 0.
/// </summary>
[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();
}
/// <summary>
/// Go: TestMonitorConnzWithRoutes (line 1405).
/// Verifies /routez returns HTTP 200 OK.
/// </summary>
[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);
}
/// <summary>
/// Go: TestMonitorCluster (line 2724).
/// Verifies /routez endpoint is accessible when cluster is configured.
/// </summary>
[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<RoutezFixture> StartAsync()
{
var monitorPort = TestPortAllocator.GetFreePort();
var options = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
MonitorPort = monitorPort,
};
return await CreateAndStartAsync(options, monitorPort);
}
public static async Task<RoutezFixture> StartWithClusterAsync()
{
var monitorPort = TestPortAllocator.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<RoutezFixture> 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<string> GetStringAsync(string path)
=> _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}{path}");
public Task<HttpResponseMessage> GetAsync(string path)
=> _http.GetAsync($"http://127.0.0.1:{_monitorPort}{path}");
public Task<T?> GetFromJsonAsync<T>(string path)
=> _http.GetFromJsonAsync<T>($"http://127.0.0.1:{_monitorPort}{path}");
public async Task<JsonDocument> 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();
}
}