- Add Microsoft.Extensions.Logging + Serilog to NatsServer and NatsClient - Convert all test assertions from xUnit Assert to Shouldly - Add NSubstitute package for future mocking needs - Introduce Central Package Management via Directory.Packages.props - Add documentation_rules.md with style guide, generation/update rules, component map - Generate 10 documentation files across 5 component folders (GettingStarted, Protocol, Subscriptions, Server, Configuration/Operations) - Update CLAUDE.md with logging, testing, porting, agent model, CPM, and documentation guidance
118 lines
3.5 KiB
C#
118 lines
3.5 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
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 }, NullLoggerFactory.Instance);
|
|
}
|
|
|
|
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);
|
|
|
|
response.ShouldStartWith("INFO ");
|
|
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);
|
|
|
|
msg.ShouldContain("MSG foo 1 5\r\nHello\r\n");
|
|
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);
|
|
|
|
msg.ShouldContain("MSG foo.bar 1 5\r\n");
|
|
await _cts.CancelAsync();
|
|
}
|
|
}
|