Files
SVSimServer/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs
gamer147 905fdc780a test(battle-node): end-to-end flow test through Ready via WebApplicationFactory
Boots SVSimTestFactory (in-memory SQLite + reference-data CSV import),
mints a battle via IMatchingBridge, opens a raw Socket.IO v2 client
against the in-process TestServer, drives InitNetwork → Loaded → Swap,
and asserts the right scripted frames come back in order.

Verifies the full transport stack end-to-end: EIO3+SIO2 framing,
encryptForNode codec, MsgPayloadCodec roundtrip, InboundTracker
pubSeq dedup + ack echo, OutboundSequencer playSeq assignment, and
ScriptedLifecycle's Path-A frame builders.

Note: RawSocketIoTestClient.DisposeAsync skips the graceful CloseAsync
handshake — TestServer's in-process WebSocket implementation can hang
on it. Abrupt Dispose is fine: the server's ReceiveAsync throws
WebSocketException, BattleSession.RunAsync returns, and the handler
completes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 23:37:31 -04:00

74 lines
3.5 KiB
C#

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using SVSim.BattleNode.Bridge;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Wire;
using SVSim.EmulatedEntrypoint;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.BattleNode.Integration;
[TestFixture]
public class BattleNodeFlowTests
{
/// <summary>
/// End-to-end smoke for the v1 scripted lifecycle. Boots the EmulatedEntrypoint via
/// SVSimTestFactory (in-memory SQLite + reference-data CSV import), mints a battle
/// through IMatchingBridge, opens a raw Socket.IO v2 client against the in-process
/// TestServer, and drives InitNetwork → Loaded → Swap, asserting the right scripted
/// frames come back in order.
/// </summary>
[Test]
[Timeout(30000)]
public async Task ClientWalksHandshakeToReady_ReceivesAllScriptedFrames()
{
await using var factory = new SVSimTestFactory();
var bridge = factory.Services.GetRequiredService<IMatchingBridge>();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var ct = cts.Token;
var pending = bridge.RegisterPendingBattle(viewerId: 906243102);
var key = MakeKey();
var encryptedVid = NodeCrypto.EncryptForNode("906243102", key);
// TestServer ignores the host portion of the URI — only the path + query route.
var wsUri = new Uri($"ws://localhost/socket.io/?BattleId={pending.BattleId}&viewerId={Uri.EscapeDataString(encryptedVid)}&EIO=3&transport=websocket");
var wsClient = factory.Server.CreateWebSocketClient();
var ws = await wsClient.ConnectAsync(wsUri, ct);
await using var client = new RawSocketIoTestClient(ws);
await client.ConsumeHandshakeAsync(ct);
// 1. InitNetwork → expect InitNetwork ack push, then Matched, then BattleStart.
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
var f1 = await client.ReceiveSynchronizeAsync(ct);
var f2 = await client.ReceiveSynchronizeAsync(ct);
var f3 = await client.ReceiveSynchronizeAsync(ct);
Assert.That(f1.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
Assert.That(f2.Uri, Is.EqualTo(NetworkBattleUri.Matched));
Assert.That(f3.Uri, Is.EqualTo(NetworkBattleUri.BattleStart));
// 2. Loaded → expect Deal.
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 2), key, ct);
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal));
// 3. Swap with empty idxList → expect Swap response + Ready.
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 3,
body: new Dictionary<string, object?> { ["idxList"] = new List<object?>() }), key, ct);
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Swap));
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Ready));
}
private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri, long pubSeq, Dictionary<string, object?>? body = null) =>
new(uri, ViewerId: 906243102, Uuid: "udid-test", Bid: null, Try: 0,
Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General : EmitCategory.Battle,
PubSeq: pubSeq, PlaySeq: null, Body: body ?? new Dictionary<string, object?>());
private static string MakeKey()
{
var seq = 0;
return NodeCrypto.GenerateKey(() => (seq++ * 13) % 16);
}
}