feat: phase A foundation test parity — 64 new tests across 11 subsystems
Port Go NATS server test behaviors to .NET: - Client pub/sub (5 tests): simple, no-echo, reply, queue distribution, empty body - Client UNSUB (4 tests): unsub, auto-unsub max, unsub after auto, disconnect cleanup - Client headers (3 tests): HPUB/HMSG, server info headers, no-responders 503 - Client lifecycle (3 tests): connect proto, max subscriptions, auth timeout - Client slow consumer (1 test): pending limit detection and disconnect - Parser edge cases (3 tests + 2 bug fixes): PUB arg variations, malformed protocol, max control line - SubList concurrency (13 tests): race on remove/insert/match, large lists, invalid subjects, wildcards - Server config (4 tests): ephemeral port, server name, name defaults, lame duck - Route config (3 tests): cluster formation, cross-cluster messaging, reconnect - Gateway basic (2 tests): cross-cluster forwarding, no echo to origin - Leaf node basic (2 tests): hub-to-spoke and spoke-to-hub forwarding - Account import/export (2 tests): stream export/import delivery, isolation Also fixes NatsParser.ParseSub/ParseUnsub to throw ProtocolViolationException for short command lines instead of ArgumentOutOfRangeException. Full suite: 933 passed, 0 failed (up from 869).
This commit is contained in:
187
tests/NATS.Server.Tests/ClientLifecycleTests.cs
Normal file
187
tests/NATS.Server.Tests/ClientLifecycleTests.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
// Port of Go client_test.go: TestClientConnect, TestClientConnectProto, TestAuthorizationTimeout
|
||||
// Reference: golang/nats-server/server/client_test.go lines 475, 537, 1260
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for client lifecycle: connection handshake, CONNECT proto parsing,
|
||||
/// subscription limits, and auth timeout enforcement.
|
||||
/// Reference: Go TestClientConnect, TestClientConnectProto, TestAuthorizationTimeout
|
||||
/// </summary>
|
||||
public class ClientLifecycleTests
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadUntilAsync(Socket sock, string expected, int timeoutMs = 5000)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(timeoutMs);
|
||||
var sb = new StringBuilder();
|
||||
var buf = new byte[4096];
|
||||
while (!sb.ToString().Contains(expected))
|
||||
{
|
||||
var n = await sock.ReceiveAsync(buf, SocketFlags.None, cts.Token);
|
||||
if (n == 0) break;
|
||||
sb.Append(Encoding.ASCII.GetString(buf, 0, n));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TestClientConnectProto: Sends CONNECT with verbose:false, pedantic:false, name:"test-client"
|
||||
/// and verifies the server responds with PONG, confirming the connection is accepted.
|
||||
/// Reference: Go client_test.go TestClientConnectProto (line 537)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Connect_proto_accepted()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var server = new NatsServer(new NatsOptions { Port = port }, NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
|
||||
// Read INFO
|
||||
var buf = new byte[4096];
|
||||
var n = await client.ReceiveAsync(buf, SocketFlags.None);
|
||||
var info = Encoding.ASCII.GetString(buf, 0, n);
|
||||
info.ShouldStartWith("INFO ");
|
||||
|
||||
// Send CONNECT with client name, then PING to flush
|
||||
var connectMsg = """CONNECT {"verbose":false,"pedantic":false,"name":"test-client"}""" + "\r\nPING\r\n";
|
||||
await client.SendAsync(Encoding.ASCII.GetBytes(connectMsg));
|
||||
|
||||
// Should receive PONG confirming connection is accepted
|
||||
var response = await ReadUntilAsync(client, "PONG");
|
||||
response.ShouldContain("PONG\r\n");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Max_subscriptions_enforced: Creates a server with MaxSubs=10, subscribes 10 times,
|
||||
/// then verifies that the 11th SUB triggers a -ERR 'Maximum Subscriptions Exceeded'
|
||||
/// and the connection is closed.
|
||||
/// Reference: Go client_test.go — MaxSubs enforcement in NatsClient.cs line 527
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Max_subscriptions_enforced()
|
||||
{
|
||||
const int maxSubs = 10;
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var server = new NatsServer(
|
||||
new NatsOptions { Port = port, MaxSubs = maxSubs },
|
||||
NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
|
||||
// Read INFO
|
||||
var buf = new byte[4096];
|
||||
await client.ReceiveAsync(buf, SocketFlags.None);
|
||||
|
||||
// Send CONNECT
|
||||
await client.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\n"));
|
||||
|
||||
// Subscribe up to the limit
|
||||
var subsBuilder = new StringBuilder();
|
||||
for (int i = 1; i <= maxSubs; i++)
|
||||
{
|
||||
subsBuilder.Append($"SUB foo.{i} {i}\r\n");
|
||||
}
|
||||
// Send the 11th subscription (one over the limit)
|
||||
subsBuilder.Append($"SUB foo.overflow {maxSubs + 1}\r\n");
|
||||
|
||||
await client.SendAsync(Encoding.ASCII.GetBytes(subsBuilder.ToString()));
|
||||
|
||||
// Server should send -ERR 'Maximum Subscriptions Exceeded' and close
|
||||
var response = await ReadUntilAsync(client, "-ERR", timeoutMs: 5000);
|
||||
response.ShouldContain("-ERR 'Maximum Subscriptions Exceeded'");
|
||||
|
||||
// Connection should be closed after the error
|
||||
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
var n = await client.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||
n.ShouldBe(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auth_timeout_closes_connection_if_no_connect: Creates a server with auth
|
||||
/// (token-based) and a short AuthTimeout of 500ms. Connects a raw socket,
|
||||
/// reads INFO, but does NOT send CONNECT. Verifies the server closes the
|
||||
/// connection with -ERR 'Authentication Timeout' after the timeout expires.
|
||||
/// Reference: Go client_test.go TestAuthorizationTimeout (line 1260)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Auth_timeout_closes_connection_if_no_connect()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var server = new NatsServer(
|
||||
new NatsOptions
|
||||
{
|
||||
Port = port,
|
||||
Authorization = "my_secret_token",
|
||||
AuthTimeout = TimeSpan.FromMilliseconds(500),
|
||||
},
|
||||
NullLoggerFactory.Instance);
|
||||
_ = server.StartAsync(cts.Token);
|
||||
await server.WaitForReadyAsync();
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
|
||||
// Read INFO — server requires auth so INFO will have auth_required:true
|
||||
var buf = new byte[4096];
|
||||
var n = await client.ReceiveAsync(buf, SocketFlags.None);
|
||||
var info = Encoding.ASCII.GetString(buf, 0, n);
|
||||
info.ShouldStartWith("INFO ");
|
||||
|
||||
// Do NOT send CONNECT — wait for auth timeout to fire
|
||||
// AuthTimeout is 500ms; wait up to 3x that for the error
|
||||
var response = await ReadUntilAsync(client, "Authentication Timeout", timeoutMs: 3000);
|
||||
response.ShouldContain("-ERR 'Authentication Timeout'");
|
||||
|
||||
// Connection should be closed after the auth timeout error
|
||||
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
n = await client.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||
n.ShouldBe(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
server.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user