// Go: TestSubsz server/monitor_test.go:1538 // Go: TestMonitorSubszDetails server/monitor_test.go:1609 // Go: TestMonitorSubszWithOffsetAndLimit server/monitor_test.go:1642 // Go: TestMonitorSubszTestPubSubject server/monitor_test.go:1675 // Go: TestMonitorSubszMultiAccount server/monitor_test.go:1709 // Go: TestMonitorSubszMultiAccountWithOffsetAndLimit server/monitor_test.go:1777 using System.Net; using System.Net.Http.Json; using System.Net.Sockets; using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Monitoring; namespace NATS.Server.Tests.Monitoring; /// /// 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 = 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(); } /// /// 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; } 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; } }