From 8ad2172e3cc9c2febd8672eb89636136cd9cca67 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 12 Mar 2026 19:10:33 -0400 Subject: [PATCH] test: add E2E monitoring endpoint tests (varz, connz, healthz) Replace Task.Delay polling with PeriodicTimer in NatsServerProcess readiness checks and extend StartAsync to also TCP-poll the monitor port when enabled, so MonitorServerFixture is guaranteed ready before tests run. --- .../Infrastructure/MonitorServerFixture.cs | 2 +- .../Infrastructure/NatsServerProcess.cs | 43 ++++++++++++--- tests/NATS.E2E.Tests/MonitoringTests.cs | 52 +++++++++++++++++++ 3 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 tests/NATS.E2E.Tests/MonitoringTests.cs diff --git a/tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs b/tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs index 13d30be..d6ee6a5 100644 --- a/tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs +++ b/tests/NATS.E2E.Tests/Infrastructure/MonitorServerFixture.cs @@ -16,7 +16,7 @@ public sealed class MonitorServerFixture : IAsyncLifetime public async Task InitializeAsync() { _server = new NatsServerProcess(enableMonitoring: true); - await _server.StartAsync(); + await _server.StartAsync(); // StartAsync polls both the NATS port and the monitor TCP port before returning MonitorClient = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{MonitorPort}") }; } diff --git a/tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs b/tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs index 0deb879..35f3fa4 100644 --- a/tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs +++ b/tests/NATS.E2E.Tests/Infrastructure/NatsServerProcess.cs @@ -100,6 +100,9 @@ public sealed class NatsServerProcess : IAsyncDisposable _process.BeginErrorReadLine(); await WaitForTcpReadyAsync(); + + if (_enableMonitoring && MonitorPort.HasValue) + await WaitForMonitorPortReadyAsync(); } public async ValueTask DisposeAsync() @@ -115,9 +118,11 @@ public sealed class NatsServerProcess : IAsyncDisposable { await _process.WaitForExitAsync(cts.Token); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) when (!_process.HasExited) { - // Already killed the tree above; nothing more to do + // Kill timed out and process is still running — force-terminate and surface the error + throw new InvalidOperationException( + $"NATS server process did not exit within 5s after kill.", ex); } } @@ -136,8 +141,10 @@ public sealed class NatsServerProcess : IAsyncDisposable private async Task WaitForTcpReadyAsync() { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100)); + SocketException? lastError = null; - while (!timeout.Token.IsCancellationRequested) + while (await timer.WaitForNextTickAsync(timeout.Token).ConfigureAwait(false)) { try { @@ -145,14 +152,38 @@ public sealed class NatsServerProcess : IAsyncDisposable await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, Port), timeout.Token); return; // Connected — server is ready } - catch (SocketException) + catch (SocketException ex) { - await Task.Delay(100, timeout.Token); + lastError = ex; // Server not yet accepting connections — retry on next tick } } throw new TimeoutException( - $"NATS server did not become ready on port {Port} within 10s.\n\nServer output:\n{Output}"); + $"NATS server did not become ready on port {Port} within 10s. Last error: {lastError?.Message}\n\nServer output:\n{Output}"); + } + + private async Task WaitForMonitorPortReadyAsync() + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100)); + SocketException? lastError = null; + + while (await timer.WaitForNextTickAsync(timeout.Token).ConfigureAwait(false)) + { + try + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, MonitorPort!.Value), timeout.Token); + return; // Monitor HTTP port is accepting connections + } + catch (SocketException ex) + { + lastError = ex; // Monitor not yet accepting connections — retry on next tick + } + } + + throw new TimeoutException( + $"NATS monitor port {MonitorPort} did not become ready within 10s. Last error: {lastError?.Message}\n\nServer output:\n{Output}"); } private static string ResolveHostDll() diff --git a/tests/NATS.E2E.Tests/MonitoringTests.cs b/tests/NATS.E2E.Tests/MonitoringTests.cs new file mode 100644 index 0000000..641bd92 --- /dev/null +++ b/tests/NATS.E2E.Tests/MonitoringTests.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using NATS.Client.Core; +using NATS.E2E.Tests.Infrastructure; + +namespace NATS.E2E.Tests; + +[Collection("E2E-Monitor")] +public class MonitoringTests(MonitorServerFixture fixture) +{ + [Fact] + public async Task Varz_ReturnsServerInfo() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var response = await fixture.MonitorClient.GetAsync("/varz", cts.Token); + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + + var json = await response.Content.ReadAsStringAsync(cts.Token); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + root.TryGetProperty("server_name", out _).ShouldBeTrue(); + root.TryGetProperty("version", out _).ShouldBeTrue(); + root.TryGetProperty("max_payload", out _).ShouldBeTrue(); + } + + [Fact] + public async Task Connz_ReflectsConnectedClients() + { + await using var client = fixture.CreateClient(); + await client.ConnectAsync(); + await client.PingAsync(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var response = await fixture.MonitorClient.GetAsync("/connz", cts.Token); + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + + var json = await response.Content.ReadAsStringAsync(cts.Token); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + root.TryGetProperty("num_connections", out var numConns).ShouldBeTrue(); + numConns.GetInt32().ShouldBeGreaterThanOrEqualTo(1); + } + + [Fact] + public async Task Healthz_ReturnsOk() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var response = await fixture.MonitorClient.GetAsync("/healthz", cts.Token); + response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); + } +}