T23: 27 tests — TLS reload, cluster auth, route pool, compression, limits T24: 23 tests — connz sort/closed, varz metadata, healthz, gatewayz, leafz Go refs: reload_test.go, monitor_test.go
1818 lines
75 KiB
C#
1818 lines
75 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Parity tests ported from Go server/monitor_test.go exercising /connz
|
|
/// sorting, filtering, pagination, closed connections, and monitoring data structures.
|
|
/// </summary>
|
|
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<SortOpt>();
|
|
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<ConnState>().ShouldContain(ConnState.Open);
|
|
Enum.GetValues<ConnState>().ShouldContain(ConnState.Closed);
|
|
Enum.GetValues<ConnState>().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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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<Connz>();
|
|
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>();
|
|
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<Connz>();
|
|
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>();
|
|
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>();
|
|
varz.ShouldNotBeNull();
|
|
varz.Id.ShouldNotBeNullOrEmpty();
|
|
|
|
var connzResp = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz");
|
|
var connz = await connzResp.Content.ReadFromJsonAsync<Connz>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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<Varz>();
|
|
v1.ShouldNotBeNull();
|
|
|
|
var second = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
|
var v2 = await second.Content.ReadFromJsonAsync<Varz>();
|
|
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>();
|
|
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;
|
|
}
|
|
}
|