// 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;
namespace NATS.Server.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 = 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();
}
// ========================================================================
// 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();
}
}
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;
}
}