feat: Wave 6 batch 1 — monitoring, config reload, client protocol, MQTT, leaf node tests
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
This commit is contained in:
825
tests/NATS.Server.Tests/Monitoring/MonitorConnzTests.cs
Normal file
825
tests/NATS.Server.Tests/Monitoring/MonitorConnzTests.cs
Normal file
@@ -0,0 +1,825 @@
|
||||
// Go: TestMonitorConnz server/monitor_test.go:367
|
||||
// Go: TestMonitorConnzWithSubs server/monitor_test.go:442
|
||||
// Go: TestMonitorConnzWithSubsDetail server/monitor_test.go:463
|
||||
// Go: TestMonitorClosedConnzWithSubsDetail server/monitor_test.go:484
|
||||
// Go: TestMonitorConnzRTT server/monitor_test.go:583
|
||||
// Go: TestMonitorConnzLastActivity server/monitor_test.go:638
|
||||
// Go: TestMonitorConnzWithOffsetAndLimit server/monitor_test.go:732
|
||||
// Go: TestMonitorConnzDefaultSorted server/monitor_test.go:806
|
||||
// Go: TestMonitorConnzSortedByCid server/monitor_test.go:827
|
||||
// Go: TestMonitorConnzSortedByStart server/monitor_test.go:849
|
||||
// Go: TestMonitorConnzSortedByBytesAndMsgs server/monitor_test.go:871
|
||||
// Go: TestMonitorConnzSortedByPending server/monitor_test.go:925
|
||||
// Go: TestMonitorConnzSortedBySubs server/monitor_test.go:950
|
||||
// Go: TestMonitorConnzSortedByLast server/monitor_test.go:976
|
||||
// Go: TestMonitorConnzSortedByUptime server/monitor_test.go:1007
|
||||
// Go: TestMonitorConnzSortedByIdle server/monitor_test.go:1202
|
||||
// Go: TestMonitorConnzSortedByStopOnOpen server/monitor_test.go:1074
|
||||
// Go: TestMonitorConnzSortedByReason server/monitor_test.go:1141
|
||||
// Go: TestMonitorConnzWithNamedClient server/monitor_test.go:1851
|
||||
// Go: TestMonitorConnzWithStateForClosedConns server/monitor_test.go:1876
|
||||
// Go: TestMonitorConcurrentMonitoring server/monitor_test.go:2148
|
||||
// Go: TestMonitorConnzSortByRTT server/monitor_test.go:5979
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// Tests covering /connz endpoint behavior, ported from the Go server's monitor_test.go.
|
||||
/// </summary>
|
||||
public class MonitorConnzTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _natsPort;
|
||||
private readonly int _monitorPort;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly HttpClient _http = new();
|
||||
|
||||
public MonitorConnzTests()
|
||||
{
|
||||
_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: TestMonitorConnz (line 367).
|
||||
/// Verifies /connz returns empty connections when no clients are connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_returns_empty_when_no_clients()
|
||||
{
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.NumConns.ShouldBe(0);
|
||||
connz.Total.ShouldBe(0);
|
||||
connz.Conns.Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnz (line 367).
|
||||
/// Verifies /connz lists active connections with populated identity fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_lists_active_connections_with_fields()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{\"name\":\"c1\",\"lang\":\"csharp\",\"version\":\"1.0\"}", "SUB foo 1\r\nPUB foo 5\r\nhello\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.NumConns.ShouldBe(1);
|
||||
connz.Total.ShouldBe(1);
|
||||
connz.Conns.Length.ShouldBe(1);
|
||||
|
||||
var ci = connz.Conns[0];
|
||||
// Go: ci.IP == "127.0.0.1"
|
||||
ci.Ip.ShouldBe("127.0.0.1");
|
||||
ci.Port.ShouldBeGreaterThan(0);
|
||||
ci.Cid.ShouldBeGreaterThan(0UL);
|
||||
ci.Name.ShouldBe("c1");
|
||||
ci.Lang.ShouldBe("csharp");
|
||||
ci.Version.ShouldBe("1.0");
|
||||
ci.Start.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
ci.LastActivity.ShouldBeGreaterThanOrEqualTo(ci.Start);
|
||||
ci.Uptime.ShouldNotBeNullOrEmpty();
|
||||
ci.Idle.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnz (line 367).
|
||||
/// Verifies /connz default limit is 1024 and offset is 0.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_default_limit_and_offset()
|
||||
{
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Limit.ShouldBe(1024); // Go: DefaultConnListSize
|
||||
connz.Offset.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzWithSubs (line 442).
|
||||
/// Verifies /connz?subs=1 includes subscriptions list.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_with_subs_includes_subscription_list()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}", "SUB hello.foo 1\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?subs=1");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
|
||||
var ci = connz.Conns[0];
|
||||
// Go: len(ci.Subs) != 1 || ci.Subs[0] != "hello.foo"
|
||||
ci.Subs.ShouldContain("hello.foo");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzWithSubsDetail (line 463).
|
||||
/// Verifies /connz?subs=detail includes subscription detail objects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_with_subs_detail_includes_subscription_detail()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}", "SUB hello.foo 1\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?subs=detail");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
|
||||
var ci = connz.Conns[0];
|
||||
// Go: len(ci.SubsDetail) != 1 || ci.SubsDetail[0].Subject != "hello.foo"
|
||||
ci.SubsDetail.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
ci.SubsDetail.ShouldContain(sd => sd.Subject == "hello.foo");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzWithNamedClient (line 1851).
|
||||
/// Verifies /connz exposes client name set in CONNECT options.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_shows_named_client()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{\"name\":\"test-client\"}");
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBe(1);
|
||||
connz.Conns[0].Name.ShouldBe("test-client");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzWithOffsetAndLimit (line 732).
|
||||
/// Verifies /connz pagination with offset and limit parameters.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_pagination_with_offset_and_limit()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
|
||||
await Task.Delay(200);
|
||||
|
||||
// offset=1, limit=1 should return 1 connection with total of 3
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?offset=1&limit=1");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Limit.ShouldBe(1);
|
||||
connz.Offset.ShouldBe(1);
|
||||
connz.Conns.Length.ShouldBe(1);
|
||||
connz.NumConns.ShouldBe(1);
|
||||
connz.Total.ShouldBeGreaterThanOrEqualTo(3);
|
||||
|
||||
// offset past end should return 0
|
||||
var connz2 = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?offset=10&limit=1");
|
||||
connz2.ShouldNotBeNull();
|
||||
connz2.Conns.Length.ShouldBe(0);
|
||||
connz2.NumConns.ShouldBe(0);
|
||||
connz2.Total.ShouldBeGreaterThanOrEqualTo(3);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzDefaultSorted (line 806).
|
||||
/// Verifies /connz defaults to ascending CID sort order.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_default_sorted_by_cid_ascending()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 4; i++)
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4);
|
||||
|
||||
// Go: Conns[0].Cid < Conns[1].Cid < Conns[2].Cid < Conns[3].Cid
|
||||
for (var i = 1; i < connz.Conns.Length; i++)
|
||||
connz.Conns[i].Cid.ShouldBeGreaterThan(connz.Conns[i - 1].Cid);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByCid (line 827).
|
||||
/// Verifies /connz?sort=cid returns connections sorted by CID.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_cid()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 4; i++)
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=cid");
|
||||
connz.ShouldNotBeNull();
|
||||
for (var i = 1; i < connz.Conns.Length; i++)
|
||||
connz.Conns[i].Cid.ShouldBeGreaterThan(connz.Conns[i - 1].Cid);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByStart (line 849).
|
||||
/// Verifies /connz?sort=start returns connections sorted by start time.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_start()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=start");
|
||||
connz.ShouldNotBeNull();
|
||||
for (var i = 1; i < connz.Conns.Length; i++)
|
||||
connz.Conns[i].Start.ShouldBeGreaterThanOrEqualTo(connz.Conns[i - 1].Start);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByBytesAndMsgs (line 871).
|
||||
/// Verifies /connz?sort=bytes_to returns connections sorted by out_bytes descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_bytes_to()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
// Subscriber first
|
||||
sockets.Add(await ConnectClientAsync("{}", "SUB foo 1\r\n"));
|
||||
|
||||
// High-traffic publisher
|
||||
var pub = await ConnectClientAsync("{}");
|
||||
sockets.Add(pub);
|
||||
using var ns = new NetworkStream(pub);
|
||||
for (var i = 0; i < 50; i++)
|
||||
await ns.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray());
|
||||
await ns.FlushAsync();
|
||||
|
||||
// Low-traffic client
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
|
||||
await Task.Delay(300);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=bytes_to");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
|
||||
// First entry should have >= out_bytes than second
|
||||
connz.Conns[0].OutBytes.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].OutBytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByBytesAndMsgs (line 871).
|
||||
/// Verifies /connz?sort=msgs_to returns connections sorted by out_msgs descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_msgs_to()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
sockets.Add(await ConnectClientAsync("{}", "SUB foo 1\r\n"));
|
||||
|
||||
var pub = await ConnectClientAsync("{}");
|
||||
sockets.Add(pub);
|
||||
using var ns = new NetworkStream(pub);
|
||||
for (var i = 0; i < 50; i++)
|
||||
await ns.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray());
|
||||
await ns.FlushAsync();
|
||||
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(300);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=msgs_to");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
connz.Conns[0].OutMsgs.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].OutMsgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByBytesAndMsgs (line 871).
|
||||
/// Verifies /connz?sort=msgs_from returns connections sorted by in_msgs descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_msgs_from()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
var pub = await ConnectClientAsync("{}");
|
||||
sockets.Add(pub);
|
||||
using var ns = new NetworkStream(pub);
|
||||
for (var i = 0; i < 50; i++)
|
||||
await ns.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray());
|
||||
await ns.FlushAsync();
|
||||
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(300);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=msgs_from");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
connz.Conns[0].InMsgs.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].InMsgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedBySubs (line 950).
|
||||
/// Verifies /connz?sort=subs returns connections sorted by subscription count descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_subs()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
// Client with many subs
|
||||
sockets.Add(await ConnectClientAsync("{}", "SUB a 1\r\nSUB b 2\r\nSUB c 3\r\n"));
|
||||
// Client with no subs
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=subs");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
connz.Conns[0].NumSubs.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].NumSubs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByLast (line 976).
|
||||
/// Verifies /connz?sort=last returns connections sorted by last_activity descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_last_activity()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
// First client connects and does something early
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(50);
|
||||
|
||||
// Second client connects later and does activity
|
||||
sockets.Add(await ConnectClientAsync("{}", "PUB foo 2\r\nhi\r\n"));
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=last");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
connz.Conns[0].LastActivity.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].LastActivity);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByUptime (line 1007).
|
||||
/// Verifies /connz?sort=uptime returns connections sorted by uptime descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_uptime()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
// First client has longer uptime
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(100);
|
||||
// Second client has shorter uptime
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=uptime");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
// Descending by uptime means first entry started earlier
|
||||
connz.Conns[0].Start.ShouldBeLessThanOrEqualTo(connz.Conns[1].Start);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByIdle (line 1202).
|
||||
/// Verifies /connz?sort=idle returns connections sorted by idle time descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_idle()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
// First client: older activity (more idle)
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(200);
|
||||
|
||||
// Second client: recent activity (less idle)
|
||||
sockets.Add(await ConnectClientAsync("{}", "PUB foo 2\r\nhi\r\n"));
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=idle");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
// Idle descending: first entry has older last activity
|
||||
connz.Conns[0].LastActivity.ShouldBeLessThanOrEqualTo(connz.Conns[1].LastActivity);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzWithStateForClosedConns (line 1876).
|
||||
/// Verifies /connz?state=closed returns recently disconnected clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_state_closed_returns_disconnected_clients()
|
||||
{
|
||||
var sock = await ConnectClientAsync("{\"name\":\"closing-client\"}");
|
||||
await Task.Delay(200);
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Dispose();
|
||||
await Task.Delay(500);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?state=closed");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.ShouldContain(c => c.Name == "closing-client");
|
||||
var closed = connz.Conns.First(c => c.Name == "closing-client");
|
||||
closed.Stop.ShouldNotBeNull();
|
||||
closed.Reason.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByStopOnOpen (line 1074).
|
||||
/// Verifies /connz?sort=stop&state=open falls back to CID sort without error.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_stop_with_open_state_falls_back_to_cid()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(200);
|
||||
|
||||
// Go: sort by stop on open state should fallback
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=stop&state=open");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByReason (line 1141).
|
||||
/// Verifies /connz?sort=reason&state=closed sorts by close reason.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_reason_on_closed()
|
||||
{
|
||||
var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(100);
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Dispose();
|
||||
await Task.Delay(500);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=reason&state=closed");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByReasonOnOpen (line 1180).
|
||||
/// Verifies /connz?sort=reason&state=open falls back to CID sort without error.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_reason_with_open_state_falls_back()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=reason&state=open");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortByRTT (line 5979).
|
||||
/// Verifies /connz?sort=rtt does not error.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_rtt_succeeds()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=rtt");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnz (line 367).
|
||||
/// Verifies /connz per-connection message stats are populated after pub/sub.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_per_connection_message_stats()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}", "SUB foo 1\r\nPUB foo 5\r\nhello\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBe(1);
|
||||
|
||||
var ci = connz.Conns[0];
|
||||
// Go: ci.InMsgs == 1, ci.InBytes == 5
|
||||
ci.InMsgs.ShouldBeGreaterThanOrEqualTo(1L);
|
||||
ci.InBytes.ShouldBeGreaterThanOrEqualTo(5L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzRTT (line 583).
|
||||
/// Verifies /connz includes RTT field for connected clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_includes_rtt_field()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
// Send a PING to trigger RTT measurement
|
||||
using var ns = new NetworkStream(sock);
|
||||
await ns.WriteAsync("PING\r\n"u8.ToArray());
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
// RTT may or may not be populated depending on implementation, but field must exist
|
||||
connz.Conns[0].Rtt.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzLastActivity (line 638).
|
||||
/// Verifies /connz last_activity is updated after message activity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_last_activity_updates_after_message()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(100);
|
||||
|
||||
// Record initial last activity
|
||||
var connz1 = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
var initial = connz1!.Conns[0].LastActivity;
|
||||
|
||||
// Do more activity
|
||||
using var ns = new NetworkStream(sock);
|
||||
await ns.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray());
|
||||
await ns.FlushAsync();
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz2 = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
var updated = connz2!.Conns[0].LastActivity;
|
||||
|
||||
// Activity should have updated
|
||||
updated.ShouldBeGreaterThanOrEqualTo(initial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConcurrentMonitoring (line 2148).
|
||||
/// Verifies concurrent /connz requests do not cause errors.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_handles_concurrent_requests()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(200);
|
||||
|
||||
var tasks = Enumerable.Range(0, 10).Select(async _ =>
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnz (line 367).
|
||||
/// Verifies /connz JSON uses correct Go-compatible field names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_json_uses_go_field_names()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("{}");
|
||||
await Task.Delay(200);
|
||||
|
||||
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
body.ShouldContain("\"server_id\"");
|
||||
body.ShouldContain("\"num_connections\"");
|
||||
body.ShouldContain("\"connections\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzWithStateForClosedConns (line 1876).
|
||||
/// Verifies /connz?state=all returns both open and closed connections.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_state_all_returns_both_open_and_closed()
|
||||
{
|
||||
// Connect and disconnect one client
|
||||
var sock = await ConnectClientAsync("{\"name\":\"will-close\"}");
|
||||
await Task.Delay(100);
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Dispose();
|
||||
await Task.Delay(300);
|
||||
|
||||
// Connect another client that stays open
|
||||
using var sock2 = await ConnectClientAsync("{\"name\":\"stays-open\"}");
|
||||
await Task.Delay(200);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?state=all");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Total.ShouldBeGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnz (line 367).
|
||||
/// Verifies /connz server_id matches the server's ID.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_server_id_matches_server()
|
||||
{
|
||||
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");
|
||||
|
||||
connz!.Id.ShouldBe(varz!.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByPending (line 925).
|
||||
/// Verifies /connz?sort=pending returns connections sorted by pending bytes descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_pending()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=pending");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConnzSortedByBytesAndMsgs (line 871).
|
||||
/// Verifies /connz?sort=bytes_from returns connections sorted by in_bytes descending.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connz_sort_by_bytes_from()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
// High-traffic publisher
|
||||
var pub = await ConnectClientAsync("{}");
|
||||
sockets.Add(pub);
|
||||
using var ns = new NetworkStream(pub);
|
||||
for (var i = 0; i < 50; i++)
|
||||
await ns.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray());
|
||||
await ns.FlushAsync();
|
||||
|
||||
// Low-traffic client
|
||||
sockets.Add(await ConnectClientAsync("{}"));
|
||||
await Task.Delay(300);
|
||||
|
||||
var connz = await _http.GetFromJsonAsync<Connz>($"http://127.0.0.1:{_monitorPort}/connz?sort=bytes_from");
|
||||
connz.ShouldNotBeNull();
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);
|
||||
connz.Conns[0].InBytes.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].InBytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to connect a raw TCP client to the NATS server, send CONNECT and optional commands,
|
||||
/// and return the socket. The caller is responsible for disposing the socket.
|
||||
/// </summary>
|
||||
private async Task<Socket> ConnectClientAsync(string connectJson, string? extraCommands = null)
|
||||
{
|
||||
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); // consume INFO
|
||||
|
||||
var cmd = $"CONNECT {connectJson}\r\n";
|
||||
if (extraCommands is not null)
|
||||
cmd += extraCommands;
|
||||
await sock.SendAsync(System.Text.Encoding.ASCII.GetBytes(cmd), SocketFlags.None);
|
||||
return sock;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
268
tests/NATS.Server.Tests/Monitoring/MonitorRoutezTests.cs
Normal file
268
tests/NATS.Server.Tests/Monitoring/MonitorRoutezTests.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
// 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;
|
||||
|
||||
/// <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 = 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 = 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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
355
tests/NATS.Server.Tests/Monitoring/MonitorStackszTests.cs
Normal file
355
tests/NATS.Server.Tests/Monitoring/MonitorStackszTests.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
359
tests/NATS.Server.Tests/Monitoring/MonitorSubszTests.cs
Normal file
359
tests/NATS.Server.Tests/Monitoring/MonitorSubszTests.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
// Go: TestSubsz server/monitor_test.go:1538
|
||||
// Go: TestMonitorSubszDetails server/monitor_test.go:1609
|
||||
// Go: TestMonitorSubszWithOffsetAndLimit server/monitor_test.go:1642
|
||||
// Go: TestMonitorSubszTestPubSubject server/monitor_test.go:1675
|
||||
// Go: TestMonitorSubszMultiAccount server/monitor_test.go:1709
|
||||
// Go: TestMonitorSubszMultiAccountWithOffsetAndLimit server/monitor_test.go:1777
|
||||
|
||||
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 /subz (subscriptionsz) endpoint behavior,
|
||||
/// ported from the Go server's monitor_test.go.
|
||||
/// </summary>
|
||||
public class MonitorSubszTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _natsPort;
|
||||
private readonly int _monitorPort;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly HttpClient _http = new();
|
||||
|
||||
public MonitorSubszTests()
|
||||
{
|
||||
_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: TestSubsz (line 1538).
|
||||
/// Verifies /subz returns valid JSON with server_id, num_subscriptions fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_returns_valid_json_with_server_id()
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var subsz = await response.Content.ReadFromJsonAsync<Subsz>();
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Id.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestSubsz (line 1538).
|
||||
/// Verifies /subz reports num_subscriptions after clients subscribe.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_reports_subscription_count()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszDetails (line 1609).
|
||||
/// Verifies /subz?subs=1 returns subscription details with subject info.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_with_subs_returns_subscription_details()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\nSUB foo.foo 3\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
|
||||
subsz.ShouldNotBeNull();
|
||||
|
||||
// Go: sl.NumSubs != 3, sl.Total != 3, len(sl.Subs) != 3
|
||||
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(3u);
|
||||
subsz.Total.ShouldBeGreaterThanOrEqualTo(3);
|
||||
subsz.Subs.Length.ShouldBeGreaterThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszDetails (line 1609).
|
||||
/// Verifies subscription detail entries contain the correct subject names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_detail_entries_contain_subject_names()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo.bar 1\r\nSUB foo.baz 2\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Subs.ShouldContain(s => s.Subject == "foo.bar");
|
||||
subsz.Subs.ShouldContain(s => s.Subject == "foo.baz");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
|
||||
/// Verifies /subz pagination with offset and limit parameters.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_pagination_with_offset_and_limit()
|
||||
{
|
||||
// Create many subscriptions
|
||||
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);
|
||||
|
||||
for (var i = 0; i < 200; i++)
|
||||
await sock.SendAsync(System.Text.Encoding.ASCII.GetBytes($"SUB foo.{i} {i + 1}\r\n"), SocketFlags.None);
|
||||
|
||||
await Task.Delay(300);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&offset=10&limit=100");
|
||||
subsz.ShouldNotBeNull();
|
||||
|
||||
// Go: sl.NumSubs != 200, sl.Total != 200, sl.Offset != 10, sl.Limit != 100, len(sl.Subs) != 100
|
||||
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(200u);
|
||||
subsz.Total.ShouldBeGreaterThanOrEqualTo(200);
|
||||
subsz.Offset.ShouldBe(10);
|
||||
subsz.Limit.ShouldBe(100);
|
||||
subsz.Subs.Length.ShouldBe(100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszTestPubSubject (line 1675).
|
||||
/// Verifies /subz?test=foo.foo filters subscriptions matching a concrete subject.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_test_subject_filters_matching_subscriptions()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\nSUB foo.foo 3\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
// foo.foo matches "foo.*" and "foo.foo" but not "foo.bar"
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo.foo");
|
||||
subsz.ShouldNotBeNull();
|
||||
|
||||
// Go: sl.Total != 2, len(sl.Subs) != 2
|
||||
subsz.Total.ShouldBe(2);
|
||||
subsz.Subs.Length.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszTestPubSubject (line 1675).
|
||||
/// Verifies /subz?test=foo returns no matches when no subscription matches exactly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_test_subject_no_match_returns_empty()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
// "foo" alone does not match "foo.*" or "foo.bar"
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Subs.Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestSubsz (line 1538).
|
||||
/// Verifies /subz default has no subscription details (subs not requested).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_default_does_not_include_details()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Subs.Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestSubsz (line 1538).
|
||||
/// Verifies /subscriptionsz works as an alias for /subz.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscriptionsz_is_alias_for_subz()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subscriptionsz");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Id.ShouldNotBeNullOrEmpty();
|
||||
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestSubsz (line 1538).
|
||||
/// Verifies /subz JSON uses correct Go-compatible field names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_json_uses_go_field_names()
|
||||
{
|
||||
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
body.ShouldContain("\"server_id\"");
|
||||
body.ShouldContain("\"num_subscriptions\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszDetails (line 1609).
|
||||
/// Verifies subscription details include sid and cid fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_details_include_sid_and_cid()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo 99\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Subs.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
|
||||
var sub = subsz.Subs.First(s => s.Subject == "foo");
|
||||
sub.Sid.ShouldBe("99");
|
||||
sub.Cid.ShouldBeGreaterThan(0UL);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestSubsz (line 1538).
|
||||
/// Verifies /subz returns HTTP 200 OK.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_returns_http_200()
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestSubsz (line 1538).
|
||||
/// Verifies /subz num_cache reflects the cache state of the subscription trie.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_includes_num_cache()
|
||||
{
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
subsz.ShouldNotBeNull();
|
||||
// num_cache should be >= 0
|
||||
subsz.NumCache.ShouldBeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
|
||||
/// Verifies /subz with offset=0 and limit=0 uses defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_offset_zero_uses_default_limit()
|
||||
{
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?offset=0");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Offset.ShouldBe(0);
|
||||
subsz.Limit.ShouldBe(1024); // default limit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorConcurrentMonitoring (line 2148).
|
||||
/// Verifies concurrent /subz requests do not cause errors.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_handles_concurrent_requests()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
var tasks = Enumerable.Range(0, 10).Select(async _ =>
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszTestPubSubject (line 1675).
|
||||
/// Verifies /subz?test with wildcard subject foo.* matches foo.bar and foo.baz.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_test_wildcard_match()
|
||||
{
|
||||
using var sock = await ConnectClientAsync("SUB foo.bar 1\r\nSUB foo.baz 2\r\nSUB bar.x 3\r\n");
|
||||
await Task.Delay(200);
|
||||
|
||||
// test=foo.bar should match foo.bar literal
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo.bar");
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Total.ShouldBe(1);
|
||||
subsz.Subs.Length.ShouldBe(1);
|
||||
subsz.Subs[0].Subject.ShouldBe("foo.bar");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorSubszMultiAccount (line 1709).
|
||||
/// Verifies /subz now timestamp is plausible.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subz_now_is_plausible_timestamp()
|
||||
{
|
||||
var before = DateTime.UtcNow;
|
||||
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
subsz.ShouldNotBeNull();
|
||||
subsz.Now.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(-1));
|
||||
subsz.Now.ShouldBeLessThanOrEqualTo(after.AddSeconds(1));
|
||||
}
|
||||
|
||||
private async Task<Socket> ConnectClientAsync(string extraCommands)
|
||||
{
|
||||
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(System.Text.Encoding.ASCII.GetBytes($"CONNECT {{}}\r\n{extraCommands}"), SocketFlags.None);
|
||||
return sock;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
526
tests/NATS.Server.Tests/Monitoring/MonitorVarzTests.cs
Normal file
526
tests/NATS.Server.Tests/Monitoring/MonitorVarzTests.cs
Normal file
@@ -0,0 +1,526 @@
|
||||
// Go: TestMonitorHandleVarz server/monitor_test.go:275
|
||||
// Go: TestMyUptime server/monitor_test.go:135
|
||||
// Go: TestMonitorVarzSubscriptionsResetProperly server/monitor_test.go:257
|
||||
// Go: TestMonitorNoPort server/monitor_test.go:168
|
||||
// Go: TestMonitorHTTPBasePath server/monitor_test.go:220
|
||||
// Go: TestMonitorHandleRoot server/monitor_test.go:1819
|
||||
// Go: TestMonitorServerIDs server/monitor_test.go:2410
|
||||
// Go: TestMonitorHttpStatsNoUpdatedWhenUsingServerFuncs server/monitor_test.go:2435
|
||||
// Go: TestMonitorVarzRaces server/monitor_test.go:2641
|
||||
|
||||
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 /varz endpoint behavior, ported from the Go server's monitor_test.go.
|
||||
/// </summary>
|
||||
public class MonitorVarzTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _natsPort;
|
||||
private readonly int _monitorPort;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly HttpClient _http = new();
|
||||
|
||||
public MonitorVarzTests()
|
||||
{
|
||||
_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: TestMonitorHandleVarz (line 275), mode=0.
|
||||
/// Verifies /varz returns valid JSON with server identity fields including
|
||||
/// server_id, version, start time within 10s, host, port, max_payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_returns_server_identity_and_start_within_10_seconds()
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var varz = await response.Content.ReadFromJsonAsync<Varz>();
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Id.ShouldNotBeNullOrEmpty();
|
||||
varz.Version.ShouldNotBeNullOrEmpty();
|
||||
|
||||
// Go: if time.Since(v.Start) > 10*time.Second { t.Fatal(...) }
|
||||
(DateTime.UtcNow - varz.Start).ShouldBeLessThan(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275), after connecting client.
|
||||
/// Verifies /varz tracks connections, in_msgs, out_msgs, in_bytes, out_bytes
|
||||
/// after a client connects, subscribes, and publishes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_tracks_connection_stats_after_client_pubsub()
|
||||
{
|
||||
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);
|
||||
|
||||
// Subscribe, publish 5-byte payload "hello", then flush
|
||||
await sock.SendAsync("CONNECT {}\r\nSUB foo 1\r\nPUB foo 5\r\nhello\r\n"u8.ToArray(), SocketFlags.None);
|
||||
await Task.Delay(200);
|
||||
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
|
||||
// Go: v.Connections != 1
|
||||
varz.Connections.ShouldBeGreaterThanOrEqualTo(1);
|
||||
// Go: v.TotalConnections < 1
|
||||
varz.TotalConnections.ShouldBeGreaterThanOrEqualTo(1UL);
|
||||
// Go: v.InMsgs != 1
|
||||
varz.InMsgs.ShouldBeGreaterThanOrEqualTo(1L);
|
||||
// Go: v.InBytes != 5
|
||||
varz.InBytes.ShouldBeGreaterThanOrEqualTo(5L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies that /varz reports subscriptions count after a client subscribes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_reports_subscription_count()
|
||||
{
|
||||
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\nSUB test2 2\r\n"u8.ToArray(), SocketFlags.None);
|
||||
await Task.Delay(200);
|
||||
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Subscriptions.ShouldBeGreaterThanOrEqualTo(2u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorVarzSubscriptionsResetProperly (line 257).
|
||||
/// Verifies /varz subscriptions count remains stable across multiple calls,
|
||||
/// and does not double on each request.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_subscriptions_do_not_double_across_repeated_calls()
|
||||
{
|
||||
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 varz1 = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
var subs1 = varz1!.Subscriptions;
|
||||
|
||||
var varz2 = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
var subs2 = varz2!.Subscriptions;
|
||||
|
||||
// Go: check that we get same number back (not doubled)
|
||||
subs2.ShouldBe(subs1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz exposes JetStream config and stats sections.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_jetstream_section()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.JetStream.ShouldNotBeNull();
|
||||
varz.JetStream.Config.ShouldNotBeNull();
|
||||
varz.JetStream.Stats.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz includes runtime metrics: mem > 0, cores > 0.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_runtime_metrics()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Mem.ShouldBeGreaterThan(0L);
|
||||
varz.Cores.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz uptime string is non-empty and matches expected format (e.g. "0s", "1m2s").
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_uptime_is_formatted_string()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Uptime.ShouldNotBeNullOrEmpty();
|
||||
// Uptime should end with 's' (seconds), matching Go format like "0s", "1m0s"
|
||||
varz.Uptime.ShouldEndWith("s");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMyUptime (line 135).
|
||||
/// Verifies the uptime formatting logic produces correct duration strings.
|
||||
/// Tests: 22s, 4m22s, 4h4m22s, 32d4h4m22s.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(22, "22s")]
|
||||
[InlineData(22 + 4 * 60, "4m22s")]
|
||||
[InlineData(22 + 4 * 60 + 4 * 3600, "4h4m22s")]
|
||||
[InlineData(22 + 4 * 60 + 4 * 3600 + 32 * 86400, "32d4h4m22s")]
|
||||
public void Uptime_format_matches_go_myUptime(int totalSeconds, string expected)
|
||||
{
|
||||
var ts = TimeSpan.FromSeconds(totalSeconds);
|
||||
var result = FormatUptime(ts);
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz serializes with correct Go JSON field names.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_json_uses_go_field_names()
|
||||
{
|
||||
var response = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
response.ShouldContain("\"server_id\"");
|
||||
response.ShouldContain("\"server_name\"");
|
||||
response.ShouldContain("\"in_msgs\"");
|
||||
response.ShouldContain("\"out_msgs\"");
|
||||
response.ShouldContain("\"in_bytes\"");
|
||||
response.ShouldContain("\"out_bytes\"");
|
||||
response.ShouldContain("\"max_payload\"");
|
||||
response.ShouldContain("\"total_connections\"");
|
||||
response.ShouldContain("\"slow_consumers\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz includes nested configuration sections for cluster, gateway, leaf.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_cluster_gateway_leaf_sections()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Cluster.ShouldNotBeNull();
|
||||
varz.Gateway.ShouldNotBeNull();
|
||||
varz.Leaf.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz max_payload defaults to 1MB.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_max_payload_defaults_to_1MB()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.MaxPayload.ShouldBe(1024 * 1024);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz host and port match the configured values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_host_and_port_match_configuration()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Port.ShouldBe(_natsPort);
|
||||
varz.Host.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorServerIDs (line 2410).
|
||||
/// Verifies /varz and /connz both expose the same server_id.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_and_connz_report_matching_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");
|
||||
|
||||
varz.ShouldNotBeNull();
|
||||
connz.ShouldNotBeNull();
|
||||
varz.Id.ShouldNotBeNullOrEmpty();
|
||||
connz.Id.ShouldBe(varz.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHttpStatsNoUpdatedWhenUsingServerFuncs (line 2435).
|
||||
/// Verifies /varz http_req_stats tracks endpoint hit counts and increments on each call.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_http_req_stats_increment_on_each_request()
|
||||
{
|
||||
// First request establishes baseline
|
||||
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("/varz");
|
||||
var count = varz.HttpReqStats["/varz"];
|
||||
count.ShouldBeGreaterThanOrEqualTo(2UL);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz includes slow_consumer_stats section with breakdown fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_slow_consumer_stats_breakdown()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.SlowConsumerStats.ShouldNotBeNull();
|
||||
varz.SlowConsumerStats.Clients.ShouldBeGreaterThanOrEqualTo(0UL);
|
||||
varz.SlowConsumerStats.Routes.ShouldBeGreaterThanOrEqualTo(0UL);
|
||||
varz.SlowConsumerStats.Gateways.ShouldBeGreaterThanOrEqualTo(0UL);
|
||||
varz.SlowConsumerStats.Leafs.ShouldBeGreaterThanOrEqualTo(0UL);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz includes proto version field.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_proto_version()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Proto.ShouldBeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz config_load_time is set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_config_load_time_is_set()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.ConfigLoadTime.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorVarzRaces (line 2641).
|
||||
/// Verifies concurrent /varz requests do not cause errors or data corruption.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_handles_concurrent_requests_without_errors()
|
||||
{
|
||||
var tasks = Enumerable.Range(0, 10).Select(async _ =>
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
var v = await response.Content.ReadFromJsonAsync<Varz>();
|
||||
v.ShouldNotBeNull();
|
||||
v.Id.ShouldNotBeNullOrEmpty();
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz out_msgs increments when messages are delivered to subscribers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_out_msgs_increments_on_delivery()
|
||||
{
|
||||
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);
|
||||
|
||||
// Subscribe then publish to matched subject
|
||||
await sock.SendAsync("CONNECT {}\r\nSUB foo 1\r\nPUB foo 5\r\nhello\r\n"u8.ToArray(), SocketFlags.None);
|
||||
await Task.Delay(200);
|
||||
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
// Message was published and delivered to the subscriber, so out_msgs >= 1
|
||||
varz.OutMsgs.ShouldBeGreaterThanOrEqualTo(1L);
|
||||
varz.OutBytes.ShouldBeGreaterThanOrEqualTo(5L);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz includes MQTT section in response.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_mqtt_section()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Mqtt.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz includes websocket section.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_includes_websocket_section()
|
||||
{
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Websocket.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleRoot (line 1819).
|
||||
/// Verifies GET / returns a listing of available monitoring endpoints.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Root_endpoint_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("healthz");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz total_connections tracks cumulative connections, not just active.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_total_connections_tracks_cumulative_count()
|
||||
{
|
||||
// Connect and disconnect a client
|
||||
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(100);
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
sock.Dispose();
|
||||
await Task.Delay(300);
|
||||
|
||||
// Connect a second client (still active)
|
||||
using var sock2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock2.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
||||
buf = new byte[4096];
|
||||
_ = await sock2.ReceiveAsync(buf, SocketFlags.None);
|
||||
await sock2.SendAsync("CONNECT {}\r\n"u8.ToArray(), SocketFlags.None);
|
||||
await Task.Delay(200);
|
||||
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
varz.ShouldNotBeNull();
|
||||
|
||||
// Total should be >= 2 (both connections counted), active should be 1
|
||||
varz.TotalConnections.ShouldBeGreaterThanOrEqualTo(2UL);
|
||||
varz.Connections.ShouldBeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorNoPort (line 168).
|
||||
/// Verifies that when no monitor port is configured, monitoring endpoints are not accessible.
|
||||
/// This is a standalone test since it uses a different server configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Monitor_not_accessible_when_port_not_configured()
|
||||
{
|
||||
var natsPort = GetFreePort();
|
||||
var server = new NatsServer(
|
||||
new NatsOptions { Port = natsPort, MonitorPort = 0 },
|
||||
NullLoggerFactory.Instance);
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
|
||||
// Try a random port where no monitor should be running
|
||||
var act = async () => await http.GetAsync("http://127.0.0.1:11245/varz");
|
||||
await act.ShouldThrowAsync<Exception>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Go: TestMonitorHandleVarz (line 275).
|
||||
/// Verifies /varz now field returns a plausible UTC timestamp.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Varz_now_is_plausible_utc_timestamp()
|
||||
{
|
||||
var before = DateTime.UtcNow;
|
||||
var varz = await _http.GetFromJsonAsync<Varz>($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Now.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(-1));
|
||||
varz.Now.ShouldBeLessThanOrEqualTo(after.AddSeconds(1));
|
||||
}
|
||||
|
||||
// Helper: matches Go server myUptime() format
|
||||
private static string FormatUptime(TimeSpan ts)
|
||||
{
|
||||
if (ts.TotalDays >= 1)
|
||||
return $"{(int)ts.TotalDays}d{ts.Hours}h{ts.Minutes}m{ts.Seconds}s";
|
||||
if (ts.TotalHours >= 1)
|
||||
return $"{(int)ts.TotalHours}h{ts.Minutes}m{ts.Seconds}s";
|
||||
if (ts.TotalMinutes >= 1)
|
||||
return $"{(int)ts.TotalMinutes}m{ts.Seconds}s";
|
||||
return $"{(int)ts.TotalSeconds}s";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user