Port 405 new test methods across 5 subsystems for Go parity: - Monitoring: 102 tests (varz, connz, routez, subsz, stacksz) - Leaf Nodes: 85 tests (connection, forwarding, loop detection, subject filter, JetStream) - MQTT Bridge: 86 tests (advanced, auth, retained messages, topic mapping, will messages) - Client Protocol: 73 tests (connection handling, protocol violations, limits) - Config Reload: 59 tests (hot reload, option changes, permission updates) Total: 1,678 tests passing, 0 failures, 3 skipped
360 lines
13 KiB
C#
360 lines
13 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Tests covering /subz (subscriptionsz) endpoint behavior,
|
|
/// ported from the Go server's monitor_test.go.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subz returns valid JSON with server_id, num_subscriptions fields.
|
|
/// </summary>
|
|
[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>();
|
|
subsz.ShouldNotBeNull();
|
|
subsz.Id.ShouldNotBeNullOrEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subz reports num_subscriptions after clients subscribe.
|
|
/// </summary>
|
|
[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<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
|
subsz.ShouldNotBeNull();
|
|
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszDetails (line 1609).
|
|
/// Verifies /subz?subs=1 returns subscription details with subject info.
|
|
/// </summary>
|
|
[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<Subsz>($"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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszDetails (line 1609).
|
|
/// Verifies subscription detail entries contain the correct subject names.
|
|
/// </summary>
|
|
[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<Subsz>($"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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
|
|
/// Verifies /subz pagination with offset and limit parameters.
|
|
/// </summary>
|
|
[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<Subsz>($"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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszTestPubSubject (line 1675).
|
|
/// Verifies /subz?test=foo.foo filters subscriptions matching a concrete subject.
|
|
/// </summary>
|
|
[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<Subsz>($"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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszTestPubSubject (line 1675).
|
|
/// Verifies /subz?test=foo returns no matches when no subscription matches exactly.
|
|
/// </summary>
|
|
[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<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo");
|
|
subsz.ShouldNotBeNull();
|
|
subsz.Subs.Length.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subz default has no subscription details (subs not requested).
|
|
/// </summary>
|
|
[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<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
|
subsz.ShouldNotBeNull();
|
|
subsz.Subs.Length.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subscriptionsz works as an alias for /subz.
|
|
/// </summary>
|
|
[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<Subsz>($"http://127.0.0.1:{_monitorPort}/subscriptionsz");
|
|
subsz.ShouldNotBeNull();
|
|
subsz.Id.ShouldNotBeNullOrEmpty();
|
|
subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subz JSON uses correct Go-compatible field names.
|
|
/// </summary>
|
|
[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\"");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszDetails (line 1609).
|
|
/// Verifies subscription details include sid and cid fields.
|
|
/// </summary>
|
|
[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<Subsz>($"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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subz returns HTTP 200 OK.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestSubsz (line 1538).
|
|
/// Verifies /subz num_cache reflects the cache state of the subscription trie.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Subz_includes_num_cache()
|
|
{
|
|
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
|
|
subsz.ShouldNotBeNull();
|
|
// num_cache should be >= 0
|
|
subsz.NumCache.ShouldBeGreaterThanOrEqualTo(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
|
|
/// Verifies /subz with offset=0 and limit=0 uses defaults.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Subz_offset_zero_uses_default_limit()
|
|
{
|
|
var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?offset=0");
|
|
subsz.ShouldNotBeNull();
|
|
subsz.Offset.ShouldBe(0);
|
|
subsz.Limit.ShouldBe(1024); // default limit
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorConcurrentMonitoring (line 2148).
|
|
/// Verifies concurrent /subz requests do not cause errors.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszTestPubSubject (line 1675).
|
|
/// Verifies /subz?test with wildcard subject foo.* matches foo.bar and foo.baz.
|
|
/// </summary>
|
|
[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<Subsz>($"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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Go: TestMonitorSubszMultiAccount (line 1709).
|
|
/// Verifies /subz now timestamp is plausible.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Subz_now_is_plausible_timestamp()
|
|
{
|
|
var before = DateTime.UtcNow;
|
|
var subsz = await _http.GetFromJsonAsync<Subsz>($"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<Socket> 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;
|
|
}
|
|
}
|