End-to-end exercises the v1.2 burst: each TurnEnd from the client now produces TurnStart + TurnEnd + Judge through the real WS pump.
177 lines
9.0 KiB
C#
177 lines
9.0 KiB
C#
using System.Text.Json;
|
||
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.Database;
|
||
using SVSim.Database.Models;
|
||
using SVSim.EmulatedEntrypoint;
|
||
using SVSim.UnitTests.Infrastructure;
|
||
|
||
namespace SVSim.UnitTests.BattleNode.Integration;
|
||
|
||
[TestFixture]
|
||
public class BattleNodeFlowTests
|
||
{
|
||
/// <summary>
|
||
/// End-to-end smoke for the v1.2 scripted lifecycle. Boots the EmulatedEntrypoint via
|
||
/// SVSimTestFactory, mints a battle through IMatchingBridge with a fixture MatchContext,
|
||
/// opens a raw Socket.IO v2 client against the in-process TestServer, and drives
|
||
/// InitNetwork → Loaded → Swap → TurnEnd × 2, asserting the right scripted frames come
|
||
/// back in order including the two-cycle three-frame opponent-turn loop (TurnStart +
|
||
/// TurnEnd + Judge per cycle).
|
||
/// </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, context: FixtureCtx());
|
||
|
||
var key = MakeKey();
|
||
var encryptedVid = NodeCrypto.EncryptForNode("906243102", key);
|
||
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);
|
||
|
||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
||
|
||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||
|
||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 3), key, ct);
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.BattleStart));
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal));
|
||
|
||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 4,
|
||
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));
|
||
|
||
// --- v1.2 opponent turn loop: drive two consecutive cycles ---
|
||
// Cycle 1: player ends turn -> server pushes opponent TurnStart + TurnEnd + Judge.
|
||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.TurnEnd, pubSeq: 5), key, ct);
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.TurnStart));
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.TurnEnd));
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Judge));
|
||
|
||
// Cycle 2: same burst again -- session phase reset to AfterReady, so the next TurnEnd matches.
|
||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.TurnEnd, pubSeq: 6), key, ct);
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.TurnStart));
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.TurnEnd));
|
||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Judge));
|
||
}
|
||
|
||
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
|
||
: uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching
|
||
: EmitCategory.Battle,
|
||
PubSeq: pubSeq, PlaySeq: null, Body: new RawBody(body ?? new Dictionary<string, object?>()));
|
||
|
||
private static string MakeKey()
|
||
{
|
||
var seq = 0;
|
||
return NodeCrypto.GenerateKey(() => (seq++ * 13) % 16);
|
||
}
|
||
|
||
internal static MatchContext FixtureCtx(IReadOnlyList<long>? deck = null) => new(
|
||
SelfDeckCardIds: deck ?? Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(),
|
||
ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",
|
||
CountryCode: "KOR", UserName: "Player", SleeveId: "3000011",
|
||
EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0,
|
||
BattleType: 11);
|
||
|
||
/// <summary>
|
||
/// End-to-end: a viewer with a real TK2 run sees their drafted card-ids in the Matched
|
||
/// frame's selfDeck. This is the "visible win" — proves the full plumbing chain works
|
||
/// against an actual seeded viewer.
|
||
/// </summary>
|
||
[Test]
|
||
[Timeout(30000)]
|
||
public async Task Matched_frame_contains_drafted_deck_cards()
|
||
{
|
||
await using var factory = new SVSimTestFactory();
|
||
var vid = await factory.SeedViewerAsync();
|
||
var draftedDeck = Enumerable.Range(1, 30).Select(i => 200_000_000L + i).ToList();
|
||
|
||
using (var seedScope = factory.Services.CreateScope())
|
||
{
|
||
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||
db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun
|
||
{
|
||
ViewerId = vid,
|
||
EntryId = 1,
|
||
ClassId = 1,
|
||
LeaderSkinId = 1,
|
||
SelectedCardIdsJson = JsonSerializer.Serialize(draftedDeck),
|
||
IsSelectCompleted = true,
|
||
MaxBattleCount = 5,
|
||
CandidateClassIdsJson = "[1,2,3]",
|
||
PendingPickSetsJson = "[]",
|
||
ResultListJson = "[]",
|
||
NextCandidateId = 1,
|
||
});
|
||
await db.SaveChangesAsync();
|
||
}
|
||
|
||
using var scope = factory.Services.CreateScope();
|
||
var builder = scope.ServiceProvider.GetRequiredService<SVSim.EmulatedEntrypoint.Services.IMatchContextBuilder>();
|
||
var ctx = await builder.BuildForTwoPickAsync(vid);
|
||
var bridge = factory.Services.GetRequiredService<IMatchingBridge>();
|
||
|
||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||
var ct = cts.Token;
|
||
var pending = bridge.RegisterPendingBattle(viewerId: vid, context: ctx);
|
||
|
||
var key = MakeKey();
|
||
var encryptedVid = NodeCrypto.EncryptForNode(vid.ToString(), key);
|
||
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);
|
||
|
||
// InitNetwork → ack
|
||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
|
||
await client.ReceiveSynchronizeAsync(ct);
|
||
|
||
// InitBattle → Matched (this is the frame we care about)
|
||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
|
||
var matched = await client.ReceiveSynchronizeAsync(ct);
|
||
Assert.That(matched.Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||
|
||
// MsgEnvelope.FromJson always inflates Body as a RawBody dictionary — selfDeck is a
|
||
// List<object?> of nested dicts with int "idx" + long "cardId" keys.
|
||
var body = ((RawBody)matched.Body).Entries;
|
||
var selfDeck = (List<object?>)body["selfDeck"]!;
|
||
Assert.That(selfDeck.Count, Is.EqualTo(30));
|
||
for (int i = 0; i < 30; i++)
|
||
{
|
||
var entry = (Dictionary<string, object?>)selfDeck[i]!;
|
||
Assert.That((long)entry["idx"]!, Is.EqualTo(i + 1L),
|
||
$"slot {i}: idx should be 1-based position");
|
||
Assert.That((long)entry["cardId"]!, Is.EqualTo(draftedDeck[i]),
|
||
$"slot {i}: cardId should match the drafted card");
|
||
}
|
||
}
|
||
|
||
private static MsgEnvelope MakeEnvelopeWith(long vid, NetworkBattleUri uri, long pubSeq) =>
|
||
new(uri, ViewerId: vid, Uuid: "udid-test", Bid: null, Try: 0,
|
||
Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General
|
||
: uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching
|
||
: EmitCategory.Battle,
|
||
PubSeq: pubSeq, PlaySeq: null, Body: new RawBody(new Dictionary<string, object?>()));
|
||
}
|