test(backend): end-to-end WebSocket smoke test via WebApplicationFactory
This commit is contained in:
95
Backend.Tests/EndToEndSmokeTests.cs
Normal file
95
Backend.Tests/EndToEndSmokeTests.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Backend;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Backend.Tests;
|
||||
|
||||
public class EndToEndSmokeTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public EndToEndSmokeTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(b =>
|
||||
{
|
||||
// Prevent MVC picking up assemblies; force env vars if needed.
|
||||
b.UseEnvironment("Development");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WebSocket_ReceivesInitAndStateFrames()
|
||||
{
|
||||
// Prerequisites: publish/GameCli/GameCli and models/ppo_lander.onnx must exist.
|
||||
var repoRoot = FindRepoRoot();
|
||||
Assert.True(File.Exists(Path.Combine(repoRoot, "publish", "GameCli", "GameCli")),
|
||||
"run `dotnet publish GameCli -c Release -o publish/GameCli` before this test");
|
||||
Assert.True(File.Exists(Path.Combine(repoRoot, "models", "ppo_lander.onnx")),
|
||||
"run `python Training/export_onnx.py ...` before this test");
|
||||
|
||||
var client = _factory.Server.CreateWebSocketClient();
|
||||
var baseUri = _factory.Server.BaseAddress;
|
||||
var wsUri = new UriBuilder(baseUri) { Scheme = "ws", Path = "/ws/game" }.Uri;
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
using var ws = await client.ConnectAsync(wsUri, cts.Token);
|
||||
|
||||
var initText = await ReceiveTextAsync(ws, cts.Token);
|
||||
using var initDoc = JsonDocument.Parse(initText);
|
||||
Assert.Equal("init", initDoc.RootElement.GetProperty("type").GetString());
|
||||
|
||||
// Send a cursor and receive at least one state frame.
|
||||
var cursorPayload = """{"type":"cursor","x":0.3,"y":0.7}""";
|
||||
await ws.SendAsync(Encoding.UTF8.GetBytes(cursorPayload),
|
||||
WebSocketMessageType.Text, true, cts.Token);
|
||||
|
||||
// Read up to N frames looking for a state frame; the ticker runs at 50 Hz.
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var text = await ReceiveTextAsync(ws, cts.Token);
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
if (doc.RootElement.GetProperty("type").GetString() == "state")
|
||||
{
|
||||
Assert.True(doc.RootElement.TryGetProperty("x", out _));
|
||||
Assert.True(doc.RootElement.TryGetProperty("y", out _));
|
||||
Assert.True(doc.RootElement.TryGetProperty("angle", out _));
|
||||
Assert.True(doc.RootElement.TryGetProperty("engine", out _));
|
||||
Assert.True(doc.RootElement.TryGetProperty("target", out _));
|
||||
Assert.True(doc.RootElement.TryGetProperty("step", out _));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Assert.Fail("did not receive a state frame in 100 messages");
|
||||
}
|
||||
|
||||
private static async Task<string> ReceiveTextAsync(WebSocket ws, CancellationToken ct)
|
||||
{
|
||||
var buffer = new byte[16 * 1024];
|
||||
var sb = new StringBuilder();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await ws.ReceiveAsync(buffer, ct);
|
||||
sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
} while (!result.EndOfMessage);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir, "GameCli.sln"))) return dir;
|
||||
var parent = Directory.GetParent(dir);
|
||||
if (parent is null) break;
|
||||
dir = parent.FullName;
|
||||
}
|
||||
throw new InvalidOperationException("repo root not found");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user