tests: add integration test for wrapper web socket class
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user