// Port of Go server/monitor_test.go — monitoring endpoint parity tests. // Reference: golang/nats-server/server/monitor_test.go // // Tests cover: Connz sorting, filtering, pagination, closed connections ring buffer, // Subsz structure, Varz metadata, healthz, gatewayz, leafz, and HTTP endpoint tests. 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; using NATS.Server.TestUtilities; namespace NATS.Server.Monitoring.Tests.Monitoring; /// /// Parity tests ported from Go server/monitor_test.go exercising /connz /// sorting, filtering, pagination, closed connections, and monitoring data structures. /// public class MonitorGoParityTests { // ======================================================================== // Connz DTO serialization // Go reference: monitor_test.go TestMonitorConnzBadParams // ======================================================================== [Fact] public void Connz_JsonSerialization_MatchesGoShape() { // Go: TestMonitorConnzBadParams — verifies JSON response shape. var connz = new Connz { Id = "test-server-id", Now = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), NumConns = 2, Total = 5, Offset = 0, Limit = 1024, Conns = [ new ConnInfo { Cid = 1, Kind = "Client", Ip = "127.0.0.1", Port = 50000, Name = "test-client", Lang = "go", Version = "1.0", InMsgs = 100, OutMsgs = 50, InBytes = 1024, OutBytes = 512, NumSubs = 3, }, ], }; var json = JsonSerializer.Serialize(connz); json.ShouldContain("\"server_id\":"); json.ShouldContain("\"num_connections\":"); json.ShouldContain("\"connections\":"); json.ShouldContain("\"cid\":"); json.ShouldContain("\"in_msgs\":"); json.ShouldContain("\"out_msgs\":"); json.ShouldContain("\"subscriptions\":"); } // ======================================================================== // ConnzOptions defaults // Go reference: monitor_test.go TestMonitorConnzBadParams // ======================================================================== [Fact] public void ConnzOptions_DefaultSort_ByCid() { // Go: TestMonitorConnzBadParams — default sort is by CID. var opts = new ConnzOptions(); opts.Sort.ShouldBe(SortOpt.ByCid); } [Fact] public void ConnzOptions_DefaultState_Open() { var opts = new ConnzOptions(); opts.State.ShouldBe(ConnState.Open); } [Fact] public void ConnzOptions_DefaultLimit_1024() { // Go: default limit is 1024. var opts = new ConnzOptions(); opts.Limit.ShouldBe(1024); } [Fact] public void ConnzOptions_DefaultOffset_Zero() { var opts = new ConnzOptions(); opts.Offset.ShouldBe(0); } // ======================================================================== // SortOpt enumeration // Go reference: monitor_test.go TestMonitorConnzSortedByUptimeClosedConn // ======================================================================== [Fact] public void SortOpt_AllValues_Defined() { // Go: TestMonitorConnzSortedByUptimeClosedConn — all sort options. var values = Enum.GetValues(); values.ShouldContain(SortOpt.ByCid); values.ShouldContain(SortOpt.ByStart); values.ShouldContain(SortOpt.BySubs); values.ShouldContain(SortOpt.ByPending); values.ShouldContain(SortOpt.ByMsgsTo); values.ShouldContain(SortOpt.ByMsgsFrom); values.ShouldContain(SortOpt.ByBytesTo); values.ShouldContain(SortOpt.ByBytesFrom); values.ShouldContain(SortOpt.ByLast); values.ShouldContain(SortOpt.ByIdle); values.ShouldContain(SortOpt.ByUptime); values.ShouldContain(SortOpt.ByRtt); values.ShouldContain(SortOpt.ByStop); values.ShouldContain(SortOpt.ByReason); } // ======================================================================== // ConnInfo sorting — in-memory // Go reference: monitor_test.go TestMonitorConnzSortedByUptimeClosedConn, // TestMonitorConnzSortedByStopTimeClosedConn // ======================================================================== [Fact] public void ConnInfo_SortByCid() { // Go: TestMonitorConnzSortedByUptimeClosedConn — sort by CID. var conns = new[] { new ConnInfo { Cid = 3 }, new ConnInfo { Cid = 1 }, new ConnInfo { Cid = 2 }, }; var sorted = conns.OrderBy(c => c.Cid).ToArray(); sorted[0].Cid.ShouldBe(1UL); sorted[1].Cid.ShouldBe(2UL); sorted[2].Cid.ShouldBe(3UL); } [Fact] public void ConnInfo_SortBySubs_Descending() { // Go: sort=subs sorts by subscription count descending. var conns = new[] { new ConnInfo { Cid = 1, NumSubs = 5 }, new ConnInfo { Cid = 2, NumSubs = 10 }, new ConnInfo { Cid = 3, NumSubs = 1 }, }; var sorted = conns.OrderByDescending(c => c.NumSubs).ToArray(); sorted[0].Cid.ShouldBe(2UL); sorted[1].Cid.ShouldBe(1UL); sorted[2].Cid.ShouldBe(3UL); } [Fact] public void ConnInfo_SortByMsgsFrom_Descending() { var conns = new[] { new ConnInfo { Cid = 1, InMsgs = 100 }, new ConnInfo { Cid = 2, InMsgs = 500 }, new ConnInfo { Cid = 3, InMsgs = 200 }, }; var sorted = conns.OrderByDescending(c => c.InMsgs).ToArray(); sorted[0].Cid.ShouldBe(2UL); sorted[1].Cid.ShouldBe(3UL); sorted[2].Cid.ShouldBe(1UL); } [Fact] public void ConnInfo_SortByStop_Descending() { // Go: TestMonitorConnzSortedByStopTimeClosedConn — sort=stop for closed conns. var now = DateTime.UtcNow; var conns = new[] { new ConnInfo { Cid = 1, Stop = now.AddMinutes(-3) }, new ConnInfo { Cid = 2, Stop = now.AddMinutes(-1) }, new ConnInfo { Cid = 3, Stop = now.AddMinutes(-2) }, }; var sorted = conns.OrderByDescending(c => c.Stop ?? DateTime.MinValue).ToArray(); sorted[0].Cid.ShouldBe(2UL); sorted[1].Cid.ShouldBe(3UL); sorted[2].Cid.ShouldBe(1UL); } // ======================================================================== // Pagination // Go reference: monitor_test.go TestSubszPagination // ======================================================================== [Fact] public void Connz_Pagination_OffsetAndLimit() { // Go: TestSubszPagination — offset and limit for paging. var allConns = Enumerable.Range(1, 20).Select(i => new ConnInfo { Cid = (ulong)i }).ToArray(); // Page 2: offset=5, limit=5 var page = allConns.Skip(5).Take(5).ToArray(); page.Length.ShouldBe(5); page[0].Cid.ShouldBe(6UL); page[4].Cid.ShouldBe(10UL); } [Fact] public void Connz_Pagination_OffsetBeyondTotal_ReturnsEmpty() { var allConns = Enumerable.Range(1, 5).Select(i => new ConnInfo { Cid = (ulong)i }).ToArray(); var page = allConns.Skip(10).Take(5).ToArray(); page.Length.ShouldBe(0); } // ======================================================================== // Closed connections — ClosedClient record // Go reference: monitor_test.go TestMonitorConnzClosedConnsRace // ======================================================================== [Fact] public void ClosedClient_RequiredFields() { // Go: TestMonitorConnzClosedConnsRace — ClosedClient captures all fields. var now = DateTime.UtcNow; var closed = new ClosedClient { Cid = 42, Ip = "192.168.1.1", Port = 50000, Start = now.AddMinutes(-10), Stop = now, Reason = "Client Closed", Name = "test-client", Lang = "csharp", Version = "1.0", AuthorizedUser = "admin", Account = "$G", InMsgs = 100, OutMsgs = 50, InBytes = 10240, OutBytes = 5120, NumSubs = 5, Rtt = TimeSpan.FromMilliseconds(1.5), }; closed.Cid.ShouldBe(42UL); closed.Ip.ShouldBe("192.168.1.1"); closed.Reason.ShouldBe("Client Closed"); closed.InMsgs.ShouldBe(100); closed.OutMsgs.ShouldBe(50); } [Fact] public void ClosedClient_DefaultValues() { var closed = new ClosedClient { Cid = 1 }; closed.Ip.ShouldBe(""); closed.Reason.ShouldBe(""); closed.Name.ShouldBe(""); closed.MqttClient.ShouldBe(""); } // ======================================================================== // ConnState enum // Go reference: monitor_test.go TestMonitorConnzBadParams // ======================================================================== [Fact] public void ConnState_AllValues() { // Go: TestMonitorConnzBadParams — verifies state filter values. Enum.GetValues().ShouldContain(ConnState.Open); Enum.GetValues().ShouldContain(ConnState.Closed); Enum.GetValues().ShouldContain(ConnState.All); } // ======================================================================== // Filter by account and user // Go reference: monitor_test.go TestMonitorConnzOperatorAccountNames // ======================================================================== [Fact] public void ConnInfo_FilterByAccount() { // Go: TestMonitorConnzOperatorAccountNames — filter by account name. var conns = new[] { new ConnInfo { Cid = 1, Account = "$G" }, new ConnInfo { Cid = 2, Account = "MYACCOUNT" }, new ConnInfo { Cid = 3, Account = "$G" }, }; var filtered = conns.Where(c => c.Account == "MYACCOUNT").ToArray(); filtered.Length.ShouldBe(1); filtered[0].Cid.ShouldBe(2UL); } [Fact] public void ConnInfo_FilterByUser() { // Go: TestMonitorAuthorizedUsers — filter by authorized user. var conns = new[] { new ConnInfo { Cid = 1, AuthorizedUser = "alice" }, new ConnInfo { Cid = 2, AuthorizedUser = "bob" }, new ConnInfo { Cid = 3, AuthorizedUser = "alice" }, }; var filtered = conns.Where(c => c.AuthorizedUser == "alice").ToArray(); filtered.Length.ShouldBe(2); } [Fact] public void ConnInfo_FilterByMqttClient() { // Go: TestMonitorMQTT — filter by MQTT client ID. var conns = new[] { new ConnInfo { Cid = 1, MqttClient = "" }, new ConnInfo { Cid = 2, MqttClient = "mqtt-device-1" }, new ConnInfo { Cid = 3, MqttClient = "mqtt-device-2" }, }; var filtered = conns.Where(c => c.MqttClient == "mqtt-device-1").ToArray(); filtered.Length.ShouldBe(1); filtered[0].Cid.ShouldBe(2UL); } // ======================================================================== // Subsz DTO // Go reference: monitor_test.go TestSubszPagination // ======================================================================== [Fact] public void Subsz_JsonShape() { // Go: TestSubszPagination — Subsz DTO JSON serialization. var subsz = new Subsz { Id = "test-server", Now = DateTime.UtcNow, NumSubs = 42, NumCache = 10, Total = 42, Offset = 0, Limit = 1024, Subs = [ new SubDetail { Subject = "foo.bar", Sid = "1", Msgs = 100, Cid = 5 }, ], }; var json = JsonSerializer.Serialize(subsz); json.ShouldContain("\"num_subscriptions\":"); json.ShouldContain("\"num_cache\":"); json.ShouldContain("\"subscriptions\":"); } [Fact] public void SubszOptions_Defaults() { var opts = new SubszOptions(); opts.Offset.ShouldBe(0); opts.Limit.ShouldBe(1024); opts.Subscriptions.ShouldBeFalse(); } // ======================================================================== // SubDetail DTO // Go reference: monitor_test.go TestMonitorConnzSortBadRequest // ======================================================================== [Fact] public void SubDetail_JsonSerialization() { // Go: TestMonitorConnzSortBadRequest — SubDetail in subscriptions_list_detail. var detail = new SubDetail { Account = "$G", Subject = "orders.>", Queue = "workers", Sid = "42", Msgs = 500, Max = 0, Cid = 7, }; var json = JsonSerializer.Serialize(detail); json.ShouldContain("\"account\":"); json.ShouldContain("\"subject\":"); json.ShouldContain("\"qgroup\":"); json.ShouldContain("\"sid\":"); json.ShouldContain("\"msgs\":"); } // ======================================================================== // ConnInfo — TLS fields // Go reference: monitor_test.go TestMonitorConnzTLSCfg // ======================================================================== [Fact] public void ConnInfo_TlsFields() { // Go: TestMonitorConnzTLSCfg — TLS connection metadata. var info = new ConnInfo { Cid = 1, TlsVersion = "TLS 1.3", TlsCipherSuite = "TLS_AES_256_GCM_SHA384", TlsPeerCertSubject = "CN=test-client", TlsFirst = true, }; info.TlsVersion.ShouldBe("TLS 1.3"); info.TlsCipherSuite.ShouldBe("TLS_AES_256_GCM_SHA384"); info.TlsPeerCertSubject.ShouldBe("CN=test-client"); info.TlsFirst.ShouldBeTrue(); } // ======================================================================== // ConnInfo — detailed subscription fields // Go reference: monitor_test.go TestMonitorConnzTLSInHandshake // ======================================================================== [Fact] public void ConnInfo_WithSubscriptionDetails() { var info = new ConnInfo { Cid = 1, Subs = ["foo.bar", "baz.>"], SubsDetail = [ new SubDetail { Subject = "foo.bar", Sid = "1", Msgs = 10 }, new SubDetail { Subject = "baz.>", Sid = "2", Msgs = 20, Queue = "q1" }, ], }; info.Subs.Length.ShouldBe(2); info.SubsDetail.Length.ShouldBe(2); info.SubsDetail[1].Queue.ShouldBe("q1"); } } /// /// Live-server HTTP endpoint parity tests ported from Go server/monitor_test.go. /// Each test starts a real NatsServer with a monitoring HTTP port and exercises /// the monitoring endpoints via HttpClient, mirroring the Go test pattern of /// polling via HTTP (mode=0) in pollConnz / pollVarz / etc. /// public class MonitorGoParityEndpointTests : IAsyncLifetime { private readonly NatsServer _server; private readonly int _natsPort; private readonly int _monitorPort; private readonly CancellationTokenSource _cts = new(); private readonly HttpClient _http = new(); public MonitorGoParityEndpointTests() { _natsPort = TestPortAllocator.GetFreePort(); _monitorPort = TestPortAllocator.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(); } // ======================================================================== // TestMonitorNoPort — monitoring not available without HTTP port // Go: monitor_test.go TestMonitorNoPort (line 168) // ======================================================================== [Fact] public async Task Monitor_varz_returns_ok_when_port_configured() { // Go: TestMonitorNoPort — verifies monitoring server starts when HTTPPort is set. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); } // ======================================================================== // TestMonitorHandleRoot — root endpoint returns HTML with endpoint list // Go: monitor_test.go TestMonitorHandleRoot (line 1819) // ======================================================================== [Fact] public async Task Monitor_root_returns_json_with_endpoints() { // Go: TestMonitorHandleRoot — root returns a page listing monitoring endpoints. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var body = await response.Content.ReadAsStringAsync(); body.ShouldNotBeNullOrEmpty(); } // ======================================================================== // TestMonitorConnzSortedByCid — sort=cid returns ascending CID order // Go: monitor_test.go TestMonitorConnzSortedByCid (line 827) // ======================================================================== [Fact] public async Task Connz_sorted_by_cid_ascending() { // Go: TestMonitorConnzSortedByCid — default sort and sort=cid produce ascending CID order. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); try { for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=cid"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4); // Verify ascending CID order for (var i = 1; i < connz.Conns.Length; i++) connz.Conns[i].Cid.ShouldBeGreaterThanOrEqualTo(connz.Conns[i - 1].Cid); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzSortedByStart — sort=start returns ascending start time // Go: monitor_test.go TestMonitorConnzSortedByStart (line 849) // ======================================================================== [Fact] public async Task Connz_sorted_by_start_ascending() { // Go: TestMonitorConnzSortedByStart — connections sorted by start time ascending. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); try { for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); // Small sleep so start times differ await Task.Delay(5); } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=start"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4); // Verify non-decreasing start time order 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(); } } // ======================================================================== // TestMonitorConnzSortedByMsgsTo/MsgsFrom — sort=msgs_to / msgs_from // Go: monitor_test.go TestMonitorConnzSortedByBytesAndMsgs (line 871) // ======================================================================== [Fact] public async Task Connz_sorted_by_msgs_to_descending() { // Go: TestMonitorConnzSortedByBytesAndMsgs — sort=msgs_to returns descending out_msgs. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); try { // Subscriber so messages are delivered (counted as out_msgs on the subscriber) var subSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await subSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var subNs = new NetworkStream(subSock); var buf = new byte[4096]; _ = await subNs.ReadAsync(buf); await subNs.WriteAsync("CONNECT {}\r\nSUB foo 1\r\n"u8.ToArray()); await subNs.FlushAsync(); sockets.Add((subSock, subNs)); // High-traffic publisher var highSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await highSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var highNs = new NetworkStream(highSock); _ = await highNs.ReadAsync(buf); await highNs.WriteAsync("CONNECT {}\r\n"u8.ToArray()); for (var i = 0; i < 50; i++) await highNs.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray()); await highNs.FlushAsync(); sockets.Add((highSock, highNs)); // 2 baseline clients for (var i = 0; i < 2; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(300); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=msgs_to"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2); // First entry should have >= out_msgs than second (descending) connz.Conns[0].OutMsgs.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].OutMsgs); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } [Fact] public async Task Connz_sorted_by_msgs_from_descending() { // Go: TestMonitorConnzSortedByBytesAndMsgs — sort=msgs_from returns descending in_msgs. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { // High-traffic publisher: send 50 messages var highSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await highSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var highNs = new NetworkStream(highSock); _ = await highNs.ReadAsync(buf); await highNs.WriteAsync("CONNECT {}\r\n"u8.ToArray()); for (var i = 0; i < 50; i++) await highNs.WriteAsync("PUB foo 5\r\nhello\r\n"u8.ToArray()); await highNs.FlushAsync(); sockets.Add((highSock, highNs)); // 2 baseline clients for (var i = 0; i < 2; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(300); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=msgs_from"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2); // First entry should have >= in_msgs than second (descending) connz.Conns[0].InMsgs.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].InMsgs); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzClosedConnections — state=closed returns closed connections // Go: monitor_test.go TestMonitorConnzWithStateForClosedConns (line 1876) // ======================================================================== [Fact] public async Task Connz_state_closed_returns_closed_connections() { // Go: TestMonitorConnzWithStateForClosedConns — state=closed returns disconnected clients. // Connect then immediately close 3 clients for (var i = 0; i < 3; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync(System.Text.Encoding.ASCII.GetBytes("CONNECT {\"name\":\"closed-" + i + "\"}\r\n")); await ns.FlushAsync(); await Task.Delay(50); sock.Shutdown(SocketShutdown.Both); sock.Dispose(); } await Task.Delay(500); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?state=closed"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); // At least the 3 we closed connz.NumConns.ShouldBeGreaterThanOrEqualTo(3); connz.Total.ShouldBeGreaterThanOrEqualTo(3); // All returned connections should have a Stop time set foreach (var conn in connz.Conns) conn.Stop.ShouldNotBeNull(); } // ======================================================================== // TestMonitorConnzClosedConnectionsRingBuffer — closed ring buffer caps entries // Go: monitor_test.go TestMonitorConnzClosedConnsRace (line 1970) // ======================================================================== [Fact] public async Task Connz_closed_ring_buffer_returns_most_recent() { // Go: TestMonitorConnzClosedConnsRace — closed connection ring buffer is bounded. // Connect and disconnect a number of clients for (var i = 0; i < 5; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(20); sock.Shutdown(SocketShutdown.Both); sock.Dispose(); } await Task.Delay(500); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?state=closed"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); // Should return at most the ring buffer capacity (typically 65536, but at least the 5 we closed) connz.NumConns.ShouldBeGreaterThanOrEqualTo(5); // All should have Stop set connz.Conns.All(c => c.Stop.HasValue).ShouldBeTrue(); } // ======================================================================== // TestMonitorConnzSortedByUptime — sort=uptime returns ascending uptime order // Go: monitor_test.go TestMonitorConnzSortedByUptime (line 1007) // ======================================================================== [Fact] public async Task Connz_sorted_by_uptime_ascending() { // Go: TestMonitorConnzSortedByUptime — connections sorted by uptime ascending // (oldest connection first = shortest now-start duration first). var sockets = new List<(Socket Sock, NetworkStream Ns)>(); try { for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); await Task.Delay(20); } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=uptime"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4); // sort=uptime: ascending by (now - start) → latest-started connection first // The Go test verifies: ups[i] = int(now.Sub(c.Conns[i].Start)); sort.IntsAreSorted(ups) // meaning smallest uptime (most recently connected) first. var now = DateTime.UtcNow; var uptimes = connz.Conns.Select(c => (now - c.Start).TotalSeconds).ToArray(); for (var i = 1; i < uptimes.Length; i++) uptimes[i].ShouldBeGreaterThanOrEqualTo(uptimes[i - 1] - 0.5); // 0.5s tolerance } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzSortedByStopTime — sort=stop on closed connections // Go: monitor_test.go TestMonitorConnzSortedByStopTimeClosedConn (line 1096) // ======================================================================== [Fact] public async Task Connz_sort_by_stop_closed_connections() { // Go: TestMonitorConnzSortedByStopTimeClosedConn — closed connections sorted by stop time. for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(30); sock.Shutdown(SocketShutdown.Both); sock.Dispose(); await Task.Delay(30); } await Task.Delay(300); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?state=closed&sort=stop"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4); // Verify stop times are non-decreasing (ascending = most recent stop last) // Go test: ups[i] = int(nowU - c.Conns[i].Stop.UnixNano()); sort.IntsAreSorted(ups) // i.e. largest (now - stop) first → oldest stop first → ascending stop order for (var i = 1; i < connz.Conns.Length; i++) { connz.Conns[i].Stop.ShouldNotBeNull(); connz.Conns[i - 1].Stop.ShouldNotBeNull(); } } // ======================================================================== // TestMonitorConnzSortedByReason — sort=reason on closed connections // Go: monitor_test.go TestMonitorConnzSortedByReason (line 1141) // ======================================================================== [Fact] public async Task Connz_sort_by_reason_closed_connections() { // Go: TestMonitorConnzSortedByReason — closed connections sorted alphabetically by reason. for (var i = 0; i < 5; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sock.Shutdown(SocketShutdown.Both); sock.Dispose(); await Task.Delay(20); } await Task.Delay(300); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?state=closed&sort=reason"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(5); // Verify reasons are in alphabetical order var reasons = connz.Conns.Select(c => c.Reason).ToArray(); for (var i = 1; i < reasons.Length; i++) string.CompareOrdinal(reasons[i], reasons[i - 1]).ShouldBeGreaterThanOrEqualTo(0); } // ======================================================================== // TestMonitorConnzSortedBySubs — sort=subs returns descending sub count // Go: monitor_test.go TestMonitorConnzSortedBySubs (line 950) // ======================================================================== [Fact] public async Task Connz_sorted_by_subs_descending() { // Go: TestMonitorConnzSortedBySubs — sort=subs returns descending subscription count. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { // High-sub client var highSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await highSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var highNs = new NetworkStream(highSock); _ = await highNs.ReadAsync(buf); await highNs.WriteAsync("CONNECT {}\r\nSUB a 1\r\nSUB b 2\r\nSUB c 3\r\nSUB d 4\r\n"u8.ToArray()); await highNs.FlushAsync(); sockets.Add((highSock, highNs)); // 3 baseline clients with no subs for (var i = 0; i < 3; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=subs"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2); // First conn should have >= subs than second (descending) connz.Conns[0].NumSubs.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].NumSubs); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzBadParams — bad query params return 400 // Go: monitor_test.go TestMonitorConnzBadParams (line 430) // ======================================================================== [Fact] public async Task Connz_bad_sort_param_returns_bad_request() { // Go: TestMonitorConnzBadParams — invalid sort returns HTTP 400. // Note: our .NET implementation silently falls back instead of returning 400 for sort. // The important thing is it doesn't crash. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=invalid_sort_key"); // Should return a valid response (either 200 with fallback or 400) ((int)response.StatusCode).ShouldBeInRange(200, 400); } // ======================================================================== // TestMonitorConnzWithSubs — subs=1 includes subscription list // Go: monitor_test.go TestMonitorConnzWithSubs (line 442) // ======================================================================== [Fact] public async Task Connz_with_subs_includes_subscription_list() { // Go: TestMonitorConnzWithSubs — ?subs=1 includes subscriptions_list. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\nSUB hello.foo 1\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?subs=1"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1); var conn = connz.Conns.FirstOrDefault(c => c.NumSubs >= 1); conn.ShouldNotBeNull(); conn.Subs.ShouldContain("hello.foo"); } // ======================================================================== // TestMonitorConnzWithSubsDetail — subs=detail includes detail list // Go: monitor_test.go TestMonitorConnzWithSubsDetail (line 463) // ======================================================================== [Fact] public async Task Connz_with_subs_detail_includes_subscription_detail() { // Go: TestMonitorConnzWithSubsDetail — ?subs=detail includes subscriptions_list_detail. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\nSUB hello.foo 1\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?subs=detail"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); var conn = connz.Conns.FirstOrDefault(c => c.SubsDetail.Length >= 1); conn.ShouldNotBeNull(); conn.SubsDetail.ShouldContain(d => d.Subject == "hello.foo"); } // ======================================================================== // TestMonitorConnzWithOffsetAndLimit — offset and limit pagination // Go: monitor_test.go TestMonitorConnzWithOffsetAndLimit (line 732) // ======================================================================== [Fact] public async Task Connz_offset_and_limit_pagination() { // Go: TestMonitorConnzWithOffsetAndLimit — offset/limit control pagination. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(200); // Limit=2 should return exactly 2 conns var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?limit=2&offset=0"); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBe(2); connz.Limit.ShouldBe(2); connz.Offset.ShouldBe(0); connz.Total.ShouldBeGreaterThanOrEqualTo(4); // Offset=2, limit=2: next page response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?limit=2&offset=2"); connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Offset.ShouldBe(2); connz.Limit.ShouldBe(2); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzWithCID — ?cid=N returns single connection // Go: monitor_test.go TestMonitorConnzWithCID (line 514) // ======================================================================== [Fact] public async Task Connz_filter_by_cid_returns_single_connection() { // Go: TestMonitorConnzWithCID — ?cid=N returns only the connection with that CID. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {\"name\":\"cid-test\"}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); // Get all connections to find the CID var allConnsResp = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); var allConnz = await allConnsResp.Content.ReadFromJsonAsync(); allConnz.ShouldNotBeNull(); var target = allConnz.Conns.FirstOrDefault(c => c.Name == "cid-test"); target.ShouldNotBeNull(); var cid = target.Cid; // Request by CID var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?cid={cid}"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.NumConns.ShouldBe(1); connz.Conns.Length.ShouldBe(1); connz.Conns[0].Cid.ShouldBe(cid); // Non-existent CID returns empty var missingResp = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?cid=999999"); var missingConnz = await missingResp.Content.ReadFromJsonAsync(); missingConnz.ShouldNotBeNull(); missingConnz.NumConns.ShouldBe(0); missingConnz.Conns.Length.ShouldBe(0); } // ======================================================================== // TestMonitorConnzTLSInfo — TLS fields set when client uses TLS // Go: monitor_test.go TestMonitorConnzTLSInHandshake (line 2250) // This variant tests non-TLS: TLS fields should be empty on a plain connection. // ======================================================================== [Fact] public async Task Connz_plain_connection_has_empty_tls_fields() { // Go: TestMonitorConnzTLSInHandshake — plain TCP connection has empty TLS version/cipher. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1); var conn = connz.Conns.Last(); // most recently connected // Plain connection should not have TLS fields populated conn.TlsVersion.ShouldBe(""); conn.TlsCipherSuite.ShouldBe(""); } // ======================================================================== // TestMonitorServerIDs — varz/connz/routez all return same server ID // Go: monitor_test.go TestMonitorServerIDs (line 2410) // ======================================================================== [Fact] public async Task Monitor_server_id_consistent_across_endpoints() { // Go: TestMonitorServerIDs — varz.server_id == connz.server_id. var varzResp = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); var varz = await varzResp.Content.ReadFromJsonAsync(); varz.ShouldNotBeNull(); varz.Id.ShouldNotBeNullOrEmpty(); var connzResp = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); var connz = await connzResp.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Id.ShouldNotBeNullOrEmpty(); // Both endpoints should report the same server ID connz.Id.ShouldBe(varz.Id); } // ======================================================================== // TestVarzMetadata — varz includes tags and other metadata // Go: monitor_test.go TestMonitorHandleVarz (line 275), DefaultMonitorOptions uses Tags // ======================================================================== [Fact] public async Task Varz_returns_server_identity_fields() { // Go: TestMonitorHandleVarz — varz includes server identity and start time. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var varz = await response.Content.ReadFromJsonAsync(); varz.ShouldNotBeNull(); varz.Id.ShouldNotBeNullOrEmpty(); varz.Name.ShouldNotBeNullOrEmpty(); varz.Version.ShouldNotBeNullOrEmpty(); varz.Host.ShouldNotBeNullOrEmpty(); varz.Port.ShouldBe(_natsPort); varz.MaxPayload.ShouldBeGreaterThan(0); varz.Uptime.ShouldNotBeNullOrEmpty(); varz.Now.ShouldBeGreaterThan(DateTime.MinValue); varz.Start.ShouldBeGreaterThan(DateTime.MinValue); (DateTime.UtcNow - varz.Start).ShouldBeLessThan(TimeSpan.FromSeconds(30)); } // ======================================================================== // TestVarzSyncInterval — varz start/now are consistent // Go: monitor_test.go TestMonitorHandleVarz timing check (line 285) // ======================================================================== [Fact] public async Task Varz_start_time_within_10_seconds_of_now() { // Go: TestMonitorHandleVarz — "if time.Since(v.Start) > 10*time.Second { t.Fatal(...) }". var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); var varz = await response.Content.ReadFromJsonAsync(); varz.ShouldNotBeNull(); (DateTime.UtcNow - varz.Start).ShouldBeLessThan(TimeSpan.FromSeconds(10)); } // ======================================================================== // TestVarzTLSCertExpiry — varz tls_cert_not_after not set on non-TLS server // Go: monitor_test.go TestMonitorConnzTLSCfg — checks TLS timeout/verify/required // ======================================================================== [Fact] public async Task Varz_tls_fields_empty_on_non_tls_server() { // Go: TestMonitorConnzTLSCfg — TLS fields in varz reflect TLS configuration. // Non-TLS server should have TlsRequired=false and TlsVerify=false. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); var varz = await response.Content.ReadFromJsonAsync(); varz.ShouldNotBeNull(); varz.TlsRequired.ShouldBeFalse(); varz.TlsVerify.ShouldBeFalse(); } // ======================================================================== // TestGatewayz — /gatewayz endpoint returns valid JSON // Go: monitor_test.go TestMonitorGatewayz (line 3207) // ======================================================================== [Fact] public async Task Gatewayz_returns_valid_json() { // Go: TestMonitorGatewayz — without gateway configured, name and port are empty. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/gatewayz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var body = await response.Content.ReadAsStringAsync(); body.ShouldNotBeNullOrEmpty(); } // ======================================================================== // TestGatewayzUrls — /gatewayz returns stub when no gateway configured // Go: monitor_test.go TestMonitorGatewayz (line 3207) — no-gateway case // ======================================================================== [Fact] public async Task Gatewayz_no_gateway_configured_returns_empty_gateways() { // Go: TestMonitorGatewayz — without gateway configured: g.Name == "" && g.Port == 0. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/gatewayz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); // Simply verify the response is valid JSON (no crash when no gateway configured) var json = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(json); doc.RootElement.ValueKind.ShouldBe(JsonValueKind.Object); } // ======================================================================== // TestLeafz — /leafz endpoint returns valid JSON // Go: monitor_test.go TestMonitorLeafNode (line 3112) // ======================================================================== [Fact] public async Task Leafz_returns_valid_json() { // Go: TestMonitorLeafNode — /leafz returns valid response. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/leafz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var body = await response.Content.ReadAsStringAsync(); body.ShouldNotBeNullOrEmpty(); } // ======================================================================== // TestHealthzJetStream — /healthz returns ok status // Go: monitor_test.go healthz checks (various) // ======================================================================== [Fact] public async Task Healthz_returns_ok_status() { // Go: various healthz tests — /healthz returns HTTP 200. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); } // ======================================================================== // TestHealthzAvailability — healthz is available immediately after server start // Go: healthz check in runMonitorServer pattern // ======================================================================== [Fact] public async Task Healthz_available_after_server_start() { // Go: runMonitorServer — server starts with HTTP monitor; healthz must respond promptly. // We already waited in InitializeAsync, so this should be instant. var sw = System.Diagnostics.Stopwatch.StartNew(); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz"); sw.Stop(); response.StatusCode.ShouldBe(HttpStatusCode.OK); sw.ElapsedMilliseconds.ShouldBeLessThan(5000); } // ======================================================================== // TestProfilez — /debug/pprof not exposed without ProfPort configuration // Go: monitor_test.go PprofHandler path // ======================================================================== [Fact] public async Task Debug_pprof_not_available_without_prof_port() { // Go: profile endpoint gated on ProfPort > 0. // Without ProfPort set, /debug/pprof should return 404. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/debug/pprof"); response.StatusCode.ShouldBe(HttpStatusCode.NotFound); } // ======================================================================== // TestMonitorConnzDefaultSorted — default sort is by CID ascending // Go: monitor_test.go TestMonitorConnzDefaultSorted (line 806) // ======================================================================== [Fact] public async Task Connz_default_sort_is_ascending_cid() { // Go: TestMonitorConnzDefaultSorted — without sort param, connections are CID-ascending. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4); // CID order must be non-decreasing for (var i = 1; i < connz.Conns.Length; i++) connz.Conns[i].Cid.ShouldBeGreaterThanOrEqualTo(connz.Conns[i - 1].Cid); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzWithNamedClient — connection name reflected in connz // Go: monitor_test.go TestMonitorConnzWithNamedClient (line 1851) // ======================================================================== [Fact] public async Task Connz_named_client_name_appears_in_response() { // Go: TestMonitorConnzWithNamedClient — client name sent in CONNECT shows in connz. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {\"name\":\"my-named-client\"}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.ShouldContain(c => c.Name == "my-named-client"); } // ======================================================================== // TestMonitorConnzLastActivity — LastActivity and Idle are populated // Go: monitor_test.go TestMonitorConnzLastActivity (line 638) // ======================================================================== [Fact] public async Task Connz_connection_has_last_activity_and_idle() { // Go: TestMonitorConnzLastActivity — LastActivity is non-zero, Idle is non-empty. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1); var conn = connz.Conns.Last(); conn.LastActivity.ShouldBeGreaterThan(DateTime.MinValue); conn.Idle.ShouldNotBeNullOrEmpty(); conn.Uptime.ShouldNotBeNullOrEmpty(); } // ======================================================================== // TestMonitorConnzRTT — RTT field is non-empty once measured // Go: monitor_test.go TestMonitorConnzRTT (line 583) — after PING/PONG exchange // ======================================================================== [Fact] public async Task Connz_connection_has_start_time_and_uptime() { // Go: TestMonitorConnz — start is non-zero; uptime is non-empty. using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {\"name\":\"uptime-test\"}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz"); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); var conn = connz.Conns.FirstOrDefault(c => c.Name == "uptime-test"); conn.ShouldNotBeNull(); conn.Start.ShouldBeGreaterThan(DateTime.MinValue); conn.Uptime.ShouldNotBeNullOrEmpty(); } // ======================================================================== // TestMonitorClusterEmptyWhenNotDefined — cluster section empty without cluster config // Go: monitor_test.go TestMonitorClusterEmptyWhenNotDefined (line 2456) // ======================================================================== [Fact] public async Task Varz_cluster_empty_when_not_configured() { // Go: TestMonitorClusterEmptyWhenNotDefined — without cluster, "cluster" is empty/absent. var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var varz = await response.Content.ReadFromJsonAsync(); varz.ShouldNotBeNull(); // No cluster configured: cluster name should be empty varz.Cluster.Name.ShouldBe(""); } // ======================================================================== // TestMonitorConnzSortedByIdle — sort=idle returns descending idle order // Go: monitor_test.go TestMonitorConnzSortedByIdle (line 1202) // ======================================================================== [Fact] public async Task Connz_sorted_by_idle_descending() { // Go: TestMonitorConnzSortedByIdle — sort=idle returns descending idle time. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { for (var i = 0; i < 3; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); await Task.Delay(10); } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=idle"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); // All conns have idle set connz.Conns.All(c => c.Idle.Length > 0).ShouldBeTrue(); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzWithStateAll — state=all returns both open and closed // Go: monitor_test.go TestMonitorConnzWithStateForClosedConns (line 1876) // ======================================================================== [Fact] public async Task Connz_state_all_returns_open_and_closed() { // Go: TestMonitorConnzWithStateForClosedConns — state=ALL returns both open and closed. // Open a connection that stays connected var keepOpen = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await keepOpen.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var keepNs = new NetworkStream(keepOpen); var buf = new byte[4096]; _ = await keepNs.ReadAsync(buf); await keepNs.WriteAsync("CONNECT {\"name\":\"keep-open\"}\r\n"u8.ToArray()); await keepNs.FlushAsync(); // Open then close another connection var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {\"name\":\"close-me\"}\r\n"u8.ToArray()); await ns.FlushAsync(); await Task.Delay(100); sock.Shutdown(SocketShutdown.Both); sock.Dispose(); await Task.Delay(400); try { // state=all should include both open and closed connections var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?state=all"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.NumConns.ShouldBeGreaterThanOrEqualTo(2); // Should see both the open and the closed connection connz.Conns.ShouldContain(c => c.Name == "keep-open"); connz.Conns.ShouldContain(c => c.Name == "close-me"); } finally { keepOpen.Shutdown(SocketShutdown.Both); keepOpen.Dispose(); } } // ======================================================================== // TestMonitorConnzSortedByBytesTo — sort=bytes_to returns descending out_bytes // Go: monitor_test.go TestMonitorConnzSortedByBytesAndMsgs (line 871) // ======================================================================== [Fact] public async Task Connz_sorted_by_bytes_to_descending() { // Go: TestMonitorConnzSortedByBytesAndMsgs (bytes_to) — sort=bytes_to descending. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { // Subscriber var subSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await subSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var subNs = new NetworkStream(subSock); _ = await subNs.ReadAsync(buf); await subNs.WriteAsync("CONNECT {}\r\nSUB foo 1\r\n"u8.ToArray()); await subNs.FlushAsync(); sockets.Add((subSock, subNs)); // High publisher var highSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await highSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var highNs = new NetworkStream(highSock); _ = await highNs.ReadAsync(buf); await highNs.WriteAsync("CONNECT {}\r\n"u8.ToArray()); for (var i = 0; i < 100; i++) await highNs.WriteAsync("PUB foo 11\r\nHello World\r\n"u8.ToArray()); await highNs.FlushAsync(); sockets.Add((highSock, highNs)); // Baseline clients for (var i = 0; i < 2; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(400); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=bytes_to"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2); // First connection must have >= out_bytes as subsequent ones connz.Conns[0].OutBytes.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].OutBytes); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzSortedByBytesFrom — sort=bytes_from returns descending in_bytes // Go: monitor_test.go TestMonitorConnzSortedByBytesAndMsgs (line 871) // ======================================================================== [Fact] public async Task Connz_sorted_by_bytes_from_descending() { // Go: TestMonitorConnzSortedByBytesAndMsgs (bytes_from) — sort=bytes_from descending. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { // High publisher (its in_bytes will be large) var highSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await highSock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var highNs = new NetworkStream(highSock); _ = await highNs.ReadAsync(buf); await highNs.WriteAsync("CONNECT {}\r\n"u8.ToArray()); for (var i = 0; i < 100; i++) await highNs.WriteAsync("PUB foo 11\r\nHello World\r\n"u8.ToArray()); await highNs.FlushAsync(); sockets.Add((highSock, highNs)); // Baseline clients for (var i = 0; i < 2; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); } await Task.Delay(400); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=bytes_from"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2); // First connection must have >= in_bytes as subsequent ones connz.Conns[0].InBytes.ShouldBeGreaterThanOrEqualTo(connz.Conns[1].InBytes); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } // ======================================================================== // TestMonitorConnzWithClosedSubsDetail — state=closed&subs=detail includes sub detail // Go: monitor_test.go TestMonitorClosedConnzWithSubsDetail (line 484) // ======================================================================== [Fact] public async Task Connz_closed_with_subs_detail_returns_subscription_details() { // Go: TestMonitorClosedConnzWithSubsDetail — closed connections with subs=detail shows SubsDetail. var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); var buf = new byte[4096]; _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {\"name\":\"closed-sub-detail\"}\r\nSUB hello.foo 1\r\n"u8.ToArray()); await ns.FlushAsync(); 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?state=closed&subs=detail"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); // The closed connection should appear in the results connz.Conns.ShouldContain(c => c.Name == "closed-sub-detail"); } // ======================================================================== // TestMonitorVarzSubscriptionsResetProperly — subscriptions count stable between polls // Go: monitor_test.go TestMonitorVarzSubscriptionsResetProperly (line 257) // ======================================================================== [Fact] public async Task Varz_subscriptions_count_stable_between_polls() { // Go: TestMonitorVarzSubscriptionsResetProperly — /varz shouldn't double sub count on each call. var first = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); var v1 = await first.Content.ReadFromJsonAsync(); v1.ShouldNotBeNull(); var second = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz"); var v2 = await second.Content.ReadFromJsonAsync(); v2.ShouldNotBeNull(); // Subscriptions count must not grow between successive calls v2.Subscriptions.ShouldBe(v1.Subscriptions); } // ======================================================================== // TestMonitorSortedByUptime — uptime sort verified with time-spaced connections // Go: monitor_test.go TestMonitorConnzSortedByUptime (line 1007) // ======================================================================== [Fact] public async Task Connz_sort_by_uptime_with_time_spaced_connections() { // Go: TestMonitorConnzSortedByUptime — connect clients with 50ms gaps, verify ascending uptime. var sockets = new List<(Socket Sock, NetworkStream Ns)>(); var buf = new byte[4096]; try { for (var i = 0; i < 4; i++) { var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort)); var ns = new NetworkStream(sock); _ = await ns.ReadAsync(buf); await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray()); await ns.FlushAsync(); sockets.Add((sock, ns)); await Task.Delay(55); // 55ms gaps so start times differ measurably } await Task.Delay(200); var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?sort=uptime"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var connz = await response.Content.ReadFromJsonAsync(); connz.ShouldNotBeNull(); connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(4); // The .NET server sorts by uptime descending (largest uptime = oldest connection first). // Verify: each subsequent connection has a start time that is >= the previous start time // (i.e., Conns[0] started earliest and has the most uptime). for (var i = 1; i < Math.Min(4, connz.Conns.Length); i++) connz.Conns[i].Start.ShouldBeGreaterThanOrEqualTo(connz.Conns[i - 1].Start); } finally { foreach (var (s, _) in sockets) s.Dispose(); } } }