tests: add integration test for wrapper web socket class

This commit is contained in:
Stevan Freeborn
2025-05-09 01:54:06 -05:00
parent 7f6c9d5624
commit 4bfe705fb6
10 changed files with 220 additions and 2 deletions
+2
View File
@@ -3,3 +3,5 @@ dotnet_diagnostic.CA1515.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA2201.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1303.severity = none
dotnet_diagnostic.CA1031.severity = none
@@ -0,0 +1,112 @@
namespace StevesBot.Worker.Tests.Integration.Infrastructure;
# pragma warning disable CA1001
public sealed class TestWebSocketServer : IAsyncLifetime
{
private readonly IWebHost _host;
private readonly CancellationTokenSource _echoCts = new();
private Uri? _webSocketUri;
public Uri Uri
{
get
{
_webSocketUri ??= GetWebSocketUri();
return _webSocketUri;
}
}
public TestWebSocketServer()
{
_host = new WebHostBuilder()
.Configure(app =>
{
app.UseWebSockets();
app.Run(async context =>
{
if (context.Request.Path == "/ws" && context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(webSocket, _echoCts.Token);
}
else
{
context.Response.StatusCode = 400;
}
});
})
.UseKestrel()
.UseUrls("http://[::1]:0")
.Build();
}
public async Task InitializeAsync()
{
await _host.StartAsync();
}
public async Task DisposeAsync()
{
await _echoCts.CancelAsync();
await _host.StopAsync();
_echoCts.Dispose();
_host.Dispose();
}
private Uri GetWebSocketUri()
{
var address = _host.ServerFeatures.GetRequiredFeature<IServerAddressesFeature>().Addresses.First();
var webSocketAddress = address.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
return new Uri(webSocketAddress + "/ws");
}
private static async Task Echo(WebSocket webSocket, CancellationToken cancellationToken)
{
var buffer = new byte[1024 * 4];
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
while (result.CloseStatus.HasValue is false && cancellationToken.IsCancellationRequested is false)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
}
if (result.CloseStatus.HasValue && cancellationToken.IsCancellationRequested is false)
{
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
Console.WriteLine($"{nameof(TestWebSocketServer)}: Client connection closed prematurely.");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{nameof(TestWebSocketServer)}: Echo operation cancelled (server shutting down).");
if (webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived)
{
await webSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server shutting down", CancellationToken.None);
}
}
catch (Exception ex)
{
Console.WriteLine($"{nameof(TestWebSocketServer)}: Error during echo: {ex.Message}");
if (webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived)
{
await webSocket.CloseAsync(WebSocketCloseStatus.InternalServerError, "Server error", CancellationToken.None);
}
}
finally
{
webSocket.Dispose();
}
}
}
@@ -0,0 +1,43 @@
using System.Text;
using WebSocket = StevesBot.Worker.WebSockets.WebSocket;
namespace StevesBot.Worker.Tests.Integration;
public sealed class WebSocketTests : IClassFixture<TestWebSocketServer>, IDisposable
{
private readonly WebSocket _client = new();
private readonly TestWebSocketServer _server;
public WebSocketTests(TestWebSocketServer server)
{
_server = server;
}
[Fact]
public async Task WebSocket_WhenUsed_ItShouldProperlyConnectSendMsgReceiveMsgAndClose()
{
await _client.ConnectAsync(_server.Uri, CancellationToken.None);
var message = "Hello, WebSocket!";
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message));
await _client.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
var receivedBuffer = new ArraySegment<byte>(new byte[buffer.Count]);
var receivedMessage = await _client.ReceiveAsync(receivedBuffer, CancellationToken.None);
receivedMessage.MessageType.Should().Be(WebSocketMessageType.Text);
receivedMessage.EndOfMessage.Should().BeTrue();
receivedMessage.Count.Should().Be(buffer.Count);
var receivedString = Encoding.UTF8.GetString([.. receivedBuffer], 0, receivedMessage.Count);
receivedString.Should().Be(message);
await _client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", CancellationToken.None);
}
public void Dispose()
{
_client.Dispose();
}
}
@@ -14,6 +14,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="7.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="moq" Version="4.20.72" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
+11 -1
View File
@@ -1,3 +1,13 @@
global using System.Net.WebSockets;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Hosting.Server.Features;
global using Microsoft.AspNetCore.Http.Features;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using StevesBot.Worker.Discord;
global using StevesBot.Worker.Discord.Events;
global using StevesBot.Worker.Threading;
global using StevesBot.Worker.Tests.Integration.Infrastructure;
global using StevesBot.Worker.Threading;
+2 -1
View File
@@ -1,2 +1,3 @@
global using System.Net.WebSockets;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Text.Json.Serialization;
@@ -0,0 +1,11 @@
namespace StevesBot.Worker.WebSockets;
internal interface IWebSocket : IDisposable
{
WebSocketState State { get; }
Task ConnectAsync(Uri uri, CancellationToken cancellationToken);
Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken);
Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken);
Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);
}
@@ -0,0 +1,38 @@
namespace StevesBot.Worker.WebSockets;
internal class WebSocket : IWebSocket
{
private readonly ClientWebSocket _clientWebSocket;
public WebSocketState State => _clientWebSocket.State;
public WebSocket(ClientWebSocket? clientWebSocket = null)
{
_clientWebSocket = clientWebSocket ?? new ClientWebSocket();
}
public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
{
await _clientWebSocket.ConnectAsync(uri, cancellationToken);
}
public async Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
{
await _clientWebSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);
}
public async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
{
return await _clientWebSocket.ReceiveAsync(buffer, cancellationToken);
}
public async Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
{
await _clientWebSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken);
}
public void Dispose()
{
_clientWebSocket.Dispose();
}
}