feat: implement NatsServer orchestrator with accept loop and message routing

This commit is contained in:
Joseph Doherty
2026-02-22 20:27:31 -05:00
parent 8db2de37cd
commit 1bc6870238
2 changed files with 256 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using NATS.Server;
namespace NATS.Server.Tests;
public class ServerTests : IAsyncDisposable
{
private readonly NatsServer _server;
private readonly int _port;
private readonly CancellationTokenSource _cts = new();
public ServerTests()
{
// Use random port
_port = GetFreePort();
_server = new NatsServer(new NatsOptions { Port = _port });
}
public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
_server.Dispose();
}
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 async Task<Socket> ConnectClientAsync()
{
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(IPAddress.Loopback, _port);
return sock;
}
private static async Task<string> ReadLineAsync(Socket sock, int bufSize = 4096)
{
var buf = new byte[bufSize];
var n = await sock.ReceiveAsync(buf, SocketFlags.None);
return Encoding.ASCII.GetString(buf, 0, n);
}
[Fact]
public async Task Server_accepts_connection_and_sends_INFO()
{
var serverTask = _server.StartAsync(_cts.Token);
await Task.Delay(100); // let server start
using var client = await ConnectClientAsync();
var response = await ReadLineAsync(client);
Assert.StartsWith("INFO ", response);
await _cts.CancelAsync();
}
[Fact]
public async Task Server_basic_pubsub()
{
var serverTask = _server.StartAsync(_cts.Token);
await Task.Delay(100);
using var pub = await ConnectClientAsync();
using var sub = await ConnectClientAsync();
// Read INFO from both
await ReadLineAsync(pub);
await ReadLineAsync(sub);
// CONNECT + SUB on subscriber
await sub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nSUB foo 1\r\n"));
await Task.Delay(50);
// CONNECT + PUB on publisher
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPUB foo 5\r\nHello\r\n"));
await Task.Delay(100);
// Read MSG from subscriber
var buf = new byte[4096];
var n = await sub.ReceiveAsync(buf, SocketFlags.None);
var msg = Encoding.ASCII.GetString(buf, 0, n);
Assert.Contains("MSG foo 1 5\r\nHello\r\n", msg);
await _cts.CancelAsync();
}
[Fact]
public async Task Server_wildcard_matching()
{
var serverTask = _server.StartAsync(_cts.Token);
await Task.Delay(100);
using var pub = await ConnectClientAsync();
using var sub = await ConnectClientAsync();
await ReadLineAsync(pub);
await ReadLineAsync(sub);
await sub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nSUB foo.* 1\r\n"));
await Task.Delay(50);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPUB foo.bar 5\r\nHello\r\n"));
await Task.Delay(100);
var buf = new byte[4096];
var n = await sub.ReceiveAsync(buf, SocketFlags.None);
var msg = Encoding.ASCII.GetString(buf, 0, n);
Assert.Contains("MSG foo.bar 1 5\r\n", msg);
await _cts.CancelAsync();
}
}