// Go: TestSubsz server/monitor_test.go:1538
// Go: TestMonitorSubszDetails server/monitor_test.go:1609
// Go: TestMonitorSubszWithOffsetAndLimit server/monitor_test.go:1642
// Go: TestMonitorSubszTestPubSubject server/monitor_test.go:1675
// Go: TestMonitorSubszMultiAccount server/monitor_test.go:1709
// Go: TestMonitorSubszMultiAccountWithOffsetAndLimit server/monitor_test.go:1777
using System.Net;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Monitoring;
using NATS.Server.TestUtilities;
namespace NATS.Server.Monitoring.Tests.Monitoring;
///
/// Tests covering /subz (subscriptionsz) endpoint behavior,
/// ported from the Go server's monitor_test.go.
///
public class MonitorSubszTests : IAsyncLifetime
{
private readonly NatsServer _server;
private readonly int _natsPort;
private readonly int _monitorPort;
private readonly CancellationTokenSource _cts = new();
private readonly HttpClient _http = new();
public MonitorSubszTests()
{
_natsPort = TestPortAllocator.GetFreePort();
_monitorPort = TestPortAllocator.GetFreePort();
_server = new NatsServer(
new NatsOptions { Port = _natsPort, MonitorPort = _monitorPort },
NullLoggerFactory.Instance);
}
public async Task InitializeAsync()
{
_ = _server.StartAsync(_cts.Token);
await _server.WaitForReadyAsync();
for (var i = 0; i < 50; i++)
{
try
{
var probe = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
if (probe.IsSuccessStatusCode) break;
}
catch (HttpRequestException) { }
await Task.Delay(50);
}
}
public async Task DisposeAsync()
{
_http.Dispose();
await _cts.CancelAsync();
_server.Dispose();
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subz returns valid JSON with server_id, num_subscriptions fields.
///
[Fact]
public async Task Subz_returns_valid_json_with_server_id()
{
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var subsz = await response.Content.ReadFromJsonAsync();
subsz.ShouldNotBeNull();
subsz.Id.ShouldNotBeNullOrEmpty();
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subz reports num_subscriptions after clients subscribe.
///
[Fact]
public async Task Subz_reports_subscription_count()
{
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
await Task.Delay(200);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz");
subsz.ShouldNotBeNull();
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
}
///
/// Go: TestMonitorSubszDetails (line 1609).
/// Verifies /subz?subs=1 returns subscription details with subject info.
///
[Fact]
public async Task Subz_with_subs_returns_subscription_details()
{
using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\nSUB foo.foo 3\r\n");
await Task.Delay(200);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
subsz.ShouldNotBeNull();
// Go: sl.NumSubs != 3, sl.Total != 3, len(sl.Subs) != 3
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(3u);
subsz.Total.ShouldBeGreaterThanOrEqualTo(3);
subsz.Subs.Length.ShouldBeGreaterThanOrEqualTo(3);
}
///
/// Go: TestMonitorSubszDetails (line 1609).
/// Verifies subscription detail entries contain the correct subject names.
///
[Fact]
public async Task Subz_detail_entries_contain_subject_names()
{
using var sock = await ConnectClientAsync("SUB foo.bar 1\r\nSUB foo.baz 2\r\n");
await Task.Delay(200);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
subsz.ShouldNotBeNull();
subsz.Subs.ShouldContain(s => s.Subject == "foo.bar");
subsz.Subs.ShouldContain(s => s.Subject == "foo.baz");
}
///
/// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
/// Verifies /subz pagination with offset and limit parameters.
///
[Fact]
public async Task Subz_pagination_with_offset_and_limit()
{
// Create many subscriptions
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
var buf = new byte[4096];
_ = await sock.ReceiveAsync(buf, SocketFlags.None);
await sock.SendAsync("CONNECT {}\r\n"u8.ToArray(), SocketFlags.None);
for (var i = 0; i < 200; i++)
await sock.SendAsync(System.Text.Encoding.ASCII.GetBytes($"SUB foo.{i} {i + 1}\r\n"), SocketFlags.None);
await Task.Delay(300);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1&offset=10&limit=100");
subsz.ShouldNotBeNull();
// Go: sl.NumSubs != 200, sl.Total != 200, sl.Offset != 10, sl.Limit != 100, len(sl.Subs) != 100
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(200u);
subsz.Total.ShouldBeGreaterThanOrEqualTo(200);
subsz.Offset.ShouldBe(10);
subsz.Limit.ShouldBe(100);
subsz.Subs.Length.ShouldBe(100);
}
///
/// Go: TestMonitorSubszTestPubSubject (line 1675).
/// Verifies /subz?test=foo.foo filters subscriptions matching a concrete subject.
///
[Fact]
public async Task Subz_test_subject_filters_matching_subscriptions()
{
using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\nSUB foo.foo 3\r\n");
await Task.Delay(200);
// foo.foo matches "foo.*" and "foo.foo" but not "foo.bar"
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo.foo");
subsz.ShouldNotBeNull();
// Go: sl.Total != 2, len(sl.Subs) != 2
subsz.Total.ShouldBe(2);
subsz.Subs.Length.ShouldBe(2);
}
///
/// Go: TestMonitorSubszTestPubSubject (line 1675).
/// Verifies /subz?test=foo returns no matches when no subscription matches exactly.
///
[Fact]
public async Task Subz_test_subject_no_match_returns_empty()
{
using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\n");
await Task.Delay(200);
// "foo" alone does not match "foo.*" or "foo.bar"
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo");
subsz.ShouldNotBeNull();
subsz.Subs.Length.ShouldBe(0);
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subz default has no subscription details (subs not requested).
///
[Fact]
public async Task Subz_default_does_not_include_details()
{
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
await Task.Delay(200);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz");
subsz.ShouldNotBeNull();
subsz.Subs.Length.ShouldBe(0);
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subscriptionsz works as an alias for /subz.
///
[Fact]
public async Task Subscriptionsz_is_alias_for_subz()
{
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
await Task.Delay(200);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subscriptionsz");
subsz.ShouldNotBeNull();
subsz.Id.ShouldNotBeNullOrEmpty();
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subz JSON uses correct Go-compatible field names.
///
[Fact]
public async Task Subz_json_uses_go_field_names()
{
var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/subz");
body.ShouldContain("\"server_id\"");
body.ShouldContain("\"num_subscriptions\"");
}
///
/// Go: TestMonitorSubszDetails (line 1609).
/// Verifies subscription details include sid and cid fields.
///
[Fact]
public async Task Subz_details_include_sid_and_cid()
{
using var sock = await ConnectClientAsync("SUB foo 99\r\n");
await Task.Delay(200);
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
subsz.ShouldNotBeNull();
subsz.Subs.Length.ShouldBeGreaterThanOrEqualTo(1);
var sub = subsz.Subs.First(s => s.Subject == "foo");
sub.Sid.ShouldBe("99");
sub.Cid.ShouldBeGreaterThan(0UL);
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subz returns HTTP 200 OK.
///
[Fact]
public async Task Subz_returns_http_200()
{
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
///
/// Go: TestSubsz (line 1538).
/// Verifies /subz num_cache reflects the cache state of the subscription trie.
///
[Fact]
public async Task Subz_includes_num_cache()
{
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz");
subsz.ShouldNotBeNull();
// num_cache should be >= 0
subsz.NumCache.ShouldBeGreaterThanOrEqualTo(0);
}
///
/// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
/// Verifies /subz with offset=0 and limit=0 uses defaults.
///
[Fact]
public async Task Subz_offset_zero_uses_default_limit()
{
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?offset=0");
subsz.ShouldNotBeNull();
subsz.Offset.ShouldBe(0);
subsz.Limit.ShouldBe(1024); // default limit
}
///
/// Go: TestMonitorConcurrentMonitoring (line 2148).
/// Verifies concurrent /subz requests do not cause errors.
///
[Fact]
public async Task Subz_handles_concurrent_requests()
{
using var sock = await ConnectClientAsync("SUB foo 1\r\n");
await Task.Delay(200);
var tasks = Enumerable.Range(0, 10).Select(async _ =>
{
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
});
await Task.WhenAll(tasks);
}
///
/// Go: TestMonitorSubszTestPubSubject (line 1675).
/// Verifies /subz?test with wildcard subject foo.* matches foo.bar and foo.baz.
///
[Fact]
public async Task Subz_test_wildcard_match()
{
using var sock = await ConnectClientAsync("SUB foo.bar 1\r\nSUB foo.baz 2\r\nSUB bar.x 3\r\n");
await Task.Delay(200);
// test=foo.bar should match foo.bar literal
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo.bar");
subsz.ShouldNotBeNull();
subsz.Total.ShouldBe(1);
subsz.Subs.Length.ShouldBe(1);
subsz.Subs[0].Subject.ShouldBe("foo.bar");
}
///
/// Go: TestMonitorSubszMultiAccount (line 1709).
/// Verifies /subz now timestamp is plausible.
///
[Fact]
public async Task Subz_now_is_plausible_timestamp()
{
var before = DateTime.UtcNow;
var subsz = await _http.GetFromJsonAsync($"http://127.0.0.1:{_monitorPort}/subz");
var after = DateTime.UtcNow;
subsz.ShouldNotBeNull();
subsz.Now.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(-1));
subsz.Now.ShouldBeLessThanOrEqualTo(after.AddSeconds(1));
}
private async Task ConnectClientAsync(string extraCommands)
{
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
var buf = new byte[4096];
_ = await sock.ReceiveAsync(buf, SocketFlags.None);
await sock.SendAsync(System.Text.Encoding.ASCII.GetBytes($"CONNECT {{}}\r\n{extraCommands}"), SocketFlags.None);
return sock;
}
}