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.
This commit is contained in:
@@ -16,7 +16,7 @@ public sealed class MonitorServerFixture : IAsyncLifetime
|
|||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
_server = new NatsServerProcess(enableMonitoring: true);
|
_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}") };
|
MonitorClient = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{MonitorPort}") };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ public sealed class NatsServerProcess : IAsyncDisposable
|
|||||||
_process.BeginErrorReadLine();
|
_process.BeginErrorReadLine();
|
||||||
|
|
||||||
await WaitForTcpReadyAsync();
|
await WaitForTcpReadyAsync();
|
||||||
|
|
||||||
|
if (_enableMonitoring && MonitorPort.HasValue)
|
||||||
|
await WaitForMonitorPortReadyAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
@@ -115,9 +118,11 @@ public sealed class NatsServerProcess : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
await _process.WaitForExitAsync(cts.Token);
|
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()
|
private async Task WaitForTcpReadyAsync()
|
||||||
{
|
{
|
||||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -145,14 +152,38 @@ public sealed class NatsServerProcess : IAsyncDisposable
|
|||||||
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, Port), timeout.Token);
|
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, Port), timeout.Token);
|
||||||
return; // Connected — server is ready
|
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(
|
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()
|
private static string ResolveHostDll()
|
||||||
|
|||||||
52
tests/NATS.E2E.Tests/MonitoringTests.cs
Normal file
52
tests/NATS.E2E.Tests/MonitoringTests.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user