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> { private readonly WebApplicationFactory _factory; public EndToEndSmokeTests(WebApplicationFactory 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 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"); } }