using SystemWebSocket = System.Net.WebSockets.WebSocket;
using System.Net.WebSockets;
using NSubstitute;
using NATS.Server.LeafNodes;
namespace NATS.Server.LeafNodes.Tests.LeafNodes;
///
/// Unit tests for (Gap 12.5).
/// Verifies stream capability flags, read/write delegation to WebSocket,
/// telemetry counters, and IsConnected state reflection.
/// Go reference: leafnode.go wsCreateLeafConnection, client.go wsRead/wsWrite.
///
public class LeafWebSocketTests
{
// -------------------------------------------------------------------------
// Helper
// -------------------------------------------------------------------------
private static SystemWebSocket CreateMockWebSocket(byte[]? readData = null)
{
var ws = Substitute.For();
ws.State.Returns(WebSocketState.Open);
if (readData != null)
{
ws.ReceiveAsync(Arg.Any>(), Arg.Any())
.Returns(callInfo =>
{
var mem = callInfo.ArgAt>(0);
var toCopy = Math.Min(readData.Length, mem.Length);
readData.AsSpan(0, toCopy).CopyTo(mem.Span);
return new ValueTask(
new ValueWebSocketReceiveResult(toCopy, WebSocketMessageType.Binary, true));
});
}
return ws;
}
// -------------------------------------------------------------------------
// Tests 1-3: Stream capability flags
// -------------------------------------------------------------------------
// Go reference: client.go wsRead — reads are supported
[Fact]
public void CanRead_ReturnsTrue()
{
var ws = CreateMockWebSocket();
var adapter = new WebSocketStreamAdapter(ws);
adapter.CanRead.ShouldBeTrue();
}
// Go reference: client.go wsWrite — writes are supported
[Fact]
public void CanWrite_ReturnsTrue()
{
var ws = CreateMockWebSocket();
var adapter = new WebSocketStreamAdapter(ws);
adapter.CanWrite.ShouldBeTrue();
}
// Go reference: leafnode.go wsCreateLeafConnection — WebSocket is not seekable
[Fact]
public void CanSeek_ReturnsFalse()
{
var ws = CreateMockWebSocket();
var adapter = new WebSocketStreamAdapter(ws);
adapter.CanSeek.ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Test 4: ReadAsync delegates to WebSocket
// -------------------------------------------------------------------------
// Go reference: client.go wsRead — receive next message from WebSocket
[Fact]
public async Task ReadAsync_ReceivesFromWebSocket()
{
var expected = "hello"u8.ToArray();
var ws = CreateMockWebSocket(readData: expected);
var adapter = new WebSocketStreamAdapter(ws);
var buffer = new byte[16];
var read = await adapter.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None);
read.ShouldBe(expected.Length);
buffer[..read].ShouldBe(expected);
}
// -------------------------------------------------------------------------
// Test 5: WriteAsync delegates to WebSocket
// -------------------------------------------------------------------------
// Go reference: client.go wsWrite — send data as a single binary frame
[Fact]
public async Task WriteAsync_SendsToWebSocket()
{
var ws = Substitute.For();
ws.State.Returns(WebSocketState.Open);
var capturedData = new List();
ws.SendAsync(
Arg.Any>(),
WebSocketMessageType.Binary,
true,
Arg.Any())
.Returns(callInfo =>
{
var mem = callInfo.ArgAt>(0);
capturedData.AddRange(mem.ToArray());
return ValueTask.CompletedTask;
});
var adapter = new WebSocketStreamAdapter(ws);
var payload = "world"u8.ToArray();
await adapter.WriteAsync(payload, 0, payload.Length, CancellationToken.None);
await ws.Received(1).SendAsync(
Arg.Any>(),
WebSocketMessageType.Binary,
true,
Arg.Any());
capturedData.ShouldBe(payload);
}
// -------------------------------------------------------------------------
// Test 6: BytesRead tracking
// -------------------------------------------------------------------------
// Go reference: client.go wsRead — track inbound byte count
[Fact]
public async Task BytesRead_TracksTotal()
{
var payload = new byte[] { 1, 2, 3, 4, 5 };
var ws = CreateMockWebSocket(readData: payload);
var adapter = new WebSocketStreamAdapter(ws);
var buffer = new byte[16];
var bytesRead = await adapter.ReadAsync(buffer.AsMemory(), CancellationToken.None);
bytesRead.ShouldBeGreaterThan(0);
adapter.BytesRead.ShouldBe(payload.Length);
}
// -------------------------------------------------------------------------
// Test 7: BytesWritten tracking
// -------------------------------------------------------------------------
// Go reference: client.go wsWrite — track outbound byte count
[Fact]
public async Task BytesWritten_TracksTotal()
{
var ws = Substitute.For();
ws.State.Returns(WebSocketState.Open);
ws.SendAsync(Arg.Any>(), Arg.Any(), Arg.Any(), Arg.Any())
.Returns(ValueTask.CompletedTask);
var adapter = new WebSocketStreamAdapter(ws);
var payload = new byte[] { 10, 20, 30 };
await adapter.WriteAsync(payload, 0, payload.Length, CancellationToken.None);
adapter.BytesWritten.ShouldBe(payload.Length);
}
// -------------------------------------------------------------------------
// Test 8: MessagesRead counter
// -------------------------------------------------------------------------
// Go reference: client.go wsRead — each completed WebSocket message increments counter
[Fact]
public async Task MessagesRead_Incremented()
{
var payload = new byte[] { 0xAA, 0xBB };
var ws = CreateMockWebSocket(readData: payload);
var adapter = new WebSocketStreamAdapter(ws);
var buffer = new byte[16];
_ = await adapter.ReadAsync(buffer.AsMemory(), CancellationToken.None);
adapter.MessagesRead.ShouldBe(1);
}
// -------------------------------------------------------------------------
// Test 9: MessagesWritten counter
// -------------------------------------------------------------------------
// Go reference: client.go wsWrite — each SendAsync call is one message
[Fact]
public async Task MessagesWritten_Incremented()
{
var ws = Substitute.For();
ws.State.Returns(WebSocketState.Open);
ws.SendAsync(Arg.Any>(), Arg.Any(), Arg.Any(), Arg.Any())
.Returns(ValueTask.CompletedTask);
var adapter = new WebSocketStreamAdapter(ws);
await adapter.WriteAsync(ReadOnlyMemory.Empty, CancellationToken.None);
await adapter.WriteAsync(ReadOnlyMemory.Empty, CancellationToken.None);
adapter.MessagesWritten.ShouldBe(2);
}
// -------------------------------------------------------------------------
// Test 10: IsConnected reflects WebSocket.State
// -------------------------------------------------------------------------
// Go reference: leafnode.go wsCreateLeafConnection — connection liveness check
[Fact]
public void IsConnected_ReflectsWebSocketState()
{
var openWs = Substitute.For();
openWs.State.Returns(WebSocketState.Open);
var closedWs = Substitute.For();
closedWs.State.Returns(WebSocketState.Closed);
var openAdapter = new WebSocketStreamAdapter(openWs);
var closedAdapter = new WebSocketStreamAdapter(closedWs);
openAdapter.IsConnected.ShouldBeTrue();
closedAdapter.IsConnected.ShouldBeFalse();
}
}