Port 405 new test methods across 5 subsystems for Go parity: - Monitoring: 102 tests (varz, connz, routez, subsz, stacksz) - Leaf Nodes: 85 tests (connection, forwarding, loop detection, subject filter, JetStream) - MQTT Bridge: 86 tests (advanced, auth, retained messages, topic mapping, will messages) - Client Protocol: 73 tests (connection handling, protocol violations, limits) - Config Reload: 59 tests (hot reload, option changes, permission updates) Total: 1,678 tests passing, 0 failures, 3 skipped
356 lines
13 KiB
C#
356 lines
13 KiB
C#
// Go: TestMonitorStacksz server/monitor_test.go:2135
|
|
// Go: TestMonitorConcurrentMonitoring server/monitor_test.go:2148
|
|
// Go: TestMonitorHandleRoot server/monitor_test.go:1819
|
|
// Go: TestMonitorHTTPBasePath server/monitor_test.go:220
|
|
// Go: TestMonitorAccountz server/monitor_test.go:4300
|
|
// Go: TestMonitorAccountStatz server/monitor_test.go:4330
|
|
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Net.Sockets;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Server.Monitoring;
|
|
|
|
namespace NATS.Server.Tests.Monitoring;
|
|
|
|
/// <summary>
|
|
/// Tests covering miscellaneous monitoring endpoints: root, accountz, accstatz,
|
|
/// gatewayz, leafz, and concurrent monitoring safety.
|
|
/// Ported from the Go server's monitor_test.go.
|
|
/// </summary>
|
|
public class MonitorStackszTests : IAsyncLifetime
|
|
{
|
|
private readonly NatsServer _server;
|
|
private readonly int _natsPort;
|
|
private readonly int _monitorPort;
|
|
private readonly CancellationTokenSource _cts = new();
|
|
private readonly HttpClient _http = new();
|
|
|
|
public MonitorStackszTests()
|
|
{
|
|
_natsPort = GetFreePort();
|
|
_monitorPort = GetFreePort();
|
|
_server = new NatsServer(
|
|
new NatsOptions { Port = _natsPort, MonitorPort = _monitorPort },
|
|
NullLoggerFactory.Instance);
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_ = _server.StartAsync(_cts.Token);
|
|
await _server.WaitForReadyAsync();
|
|
for (var i = 0; i < 50; i++)
|
|
{
|
|
try
|
|
{
|
|
var probe = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
|
|
if (probe.IsSuccessStatusCode) break;
|
|
}
|
|
catch (HttpRequestException) { }
|
|
await Task.Delay(50);
|
|
}
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
_http.Dispose();
|
|
await _cts.CancelAsync();
|
|
_server.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorHandleRoot (line 1819).
|
|
/// Verifies GET / returns HTTP 200 with endpoint listing.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Root_returns_endpoint_listing()
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
body.ShouldContain("varz");
|
|
body.ShouldContain("connz");
|
|
body.ShouldContain("routez");
|
|
body.ShouldContain("healthz");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorHandleRoot (line 1819).
|
|
/// Verifies GET / response includes subsz endpoint.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Root_includes_subz_endpoint()
|
|
{
|
|
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/");
|
|
body.ShouldContain("subz");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountz (line 4300).
|
|
/// Verifies /accountz returns valid JSON with accounts list.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accountz_returns_accounts_list()
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/accountz");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
body.ShouldContain("accounts");
|
|
body.ShouldContain("num_accounts");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountz (line 4300).
|
|
/// Verifies /accountz num_accounts is at least 1 (global account).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accountz_num_accounts_at_least_one()
|
|
{
|
|
var doc = JsonDocument.Parse(await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/accountz"));
|
|
doc.RootElement.GetProperty("num_accounts").GetInt32().ShouldBeGreaterThanOrEqualTo(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountStatz (line 4330).
|
|
/// Verifies /accstatz returns aggregate account statistics.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accstatz_returns_aggregate_stats()
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/accstatz");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
body.ShouldContain("total_accounts");
|
|
body.ShouldContain("total_connections");
|
|
body.ShouldContain("total_subscriptions");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountStatz (line 4330).
|
|
/// Verifies /accstatz total_accounts is at least 1.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accstatz_total_accounts_at_least_one()
|
|
{
|
|
var doc = JsonDocument.Parse(await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/accstatz"));
|
|
doc.RootElement.GetProperty("total_accounts").GetInt32().ShouldBeGreaterThanOrEqualTo(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorGateway (line 2880).
|
|
/// Verifies /gatewayz returns valid JSON.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Gatewayz_returns_valid_json()
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/gatewayz");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
body.ShouldContain("gateways");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorLeafNode (line 3112).
|
|
/// Verifies /leafz returns valid JSON.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Leafz_returns_valid_json()
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/leafz");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
body.ShouldContain("leafs");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorConcurrentMonitoring (line 2148).
|
|
/// Verifies concurrent requests across multiple endpoint types do not fail.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Concurrent_requests_across_endpoints_succeed()
|
|
{
|
|
var endpoints = new[] { "varz", "varz", "connz", "connz", "subz", "subz", "routez", "routez" };
|
|
var tasks = endpoints.Select(async endpoint =>
|
|
{
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/{endpoint}");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
}
|
|
});
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorConcurrentMonitoring (line 2148).
|
|
/// Verifies concurrent /healthz requests do not fail.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Concurrent_healthz_requests_succeed()
|
|
{
|
|
var tasks = Enumerable.Range(0, 20).Select(async _ =>
|
|
{
|
|
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
|
|
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
|
});
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorHttpStatsNoUpdatedWhenUsingServerFuncs (line 2435).
|
|
/// Verifies /varz http_req_stats keys include all endpoints that were accessed.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Http_req_stats_tracks_accessed_endpoints()
|
|
{
|
|
// Access multiple endpoints
|
|
await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz");
|
|
await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
|
|
await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/routez");
|
|
await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
|
|
|
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
|
varz.ShouldNotBeNull();
|
|
varz.HttpReqStats.ShouldContainKey("/connz");
|
|
varz.HttpReqStats.ShouldContainKey("/subz");
|
|
varz.HttpReqStats.ShouldContainKey("/routez");
|
|
varz.HttpReqStats.ShouldContainKey("/varz");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorHandleRoot (line 1819).
|
|
/// Verifies GET / includes jsz endpoint in listing.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Root_includes_jsz_endpoint()
|
|
{
|
|
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/");
|
|
body.ShouldContain("jsz");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorHandleRoot (line 1819).
|
|
/// Verifies GET / includes accountz endpoint in listing.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Root_includes_accountz_endpoint()
|
|
{
|
|
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/");
|
|
body.ShouldContain("accountz");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorServerIDs (line 2410).
|
|
/// Verifies multiple monitoring endpoints return the same server_id.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task All_endpoints_return_consistent_server_id()
|
|
{
|
|
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
|
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
|
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
|
|
|
varz.ShouldNotBeNull();
|
|
connz.ShouldNotBeNull();
|
|
subsz.ShouldNotBeNull();
|
|
|
|
var serverId = varz.Id;
|
|
serverId.ShouldNotBeNullOrEmpty();
|
|
connz.Id.ShouldBe(serverId);
|
|
subsz.Id.ShouldBe(serverId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountStatz (line 4330).
|
|
/// Verifies /accstatz total_connections updates after a client connects.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accstatz_total_connections_updates_after_connect()
|
|
{
|
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
|
var buf = new byte[4096];
|
|
_ = await sock.ReceiveAsync(buf, SocketFlags.None);
|
|
await sock.SendAsync("CONNECT {}\r\n"u8.ToArray(), SocketFlags.None);
|
|
await Task.Delay(200);
|
|
|
|
var doc = JsonDocument.Parse(await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/accstatz"));
|
|
doc.RootElement.GetProperty("total_connections").GetInt32().ShouldBeGreaterThanOrEqualTo(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountStatz (line 4330).
|
|
/// Verifies /accstatz total_subscriptions updates after a client subscribes.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accstatz_total_subscriptions_updates_after_subscribe()
|
|
{
|
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
|
var buf = new byte[4096];
|
|
_ = await sock.ReceiveAsync(buf, SocketFlags.None);
|
|
await sock.SendAsync("CONNECT {}\r\nSUB test 1\r\n"u8.ToArray(), SocketFlags.None);
|
|
await Task.Delay(200);
|
|
|
|
var doc = JsonDocument.Parse(await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/accstatz"));
|
|
doc.RootElement.GetProperty("total_subscriptions").GetInt32().ShouldBeGreaterThanOrEqualTo(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorAccountz (line 4300).
|
|
/// Verifies /accountz includes per-account fields: name, connections, subscriptions.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Accountz_includes_per_account_fields()
|
|
{
|
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
|
var buf = new byte[4096];
|
|
_ = await sock.ReceiveAsync(buf, SocketFlags.None);
|
|
await sock.SendAsync("CONNECT {}\r\nSUB test 1\r\n"u8.ToArray(), SocketFlags.None);
|
|
await Task.Delay(200);
|
|
|
|
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/accountz");
|
|
body.ShouldContain("\"name\"");
|
|
body.ShouldContain("\"connections\"");
|
|
body.ShouldContain("\"subscriptions\"");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorGateway (line 2880).
|
|
/// Verifies /gatewayz includes num_gateways field.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Gatewayz_includes_num_gateways()
|
|
{
|
|
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/gatewayz");
|
|
body.ShouldContain("gateways");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorLeafNode (line 3112).
|
|
/// Verifies /leafz includes num_leafs field.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Leafz_includes_num_leafs()
|
|
{
|
|
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/leafz");
|
|
body.ShouldContain("leafs");
|
|
}
|
|
|
|
private static int GetFreePort()
|
|
{
|
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
|
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
|
}
|
|
}
|