feat(battle-node): thread MatchContext through bridge to BattleSession
IMatchingBridge.RegisterPendingBattle now takes a MatchContext; PendingBattle carries it; BattleSession stores it. ArenaTwoPickBattleController builds ctx from IMatchContextBuilder. ScriptedLifecycle still uses ScriptedProfiles for the player half — Tasks 5/6 migrate the lifecycle. Existing tests updated: MatchingBridgeTests, BattleNodeFlowTests, InMemoryBattleSessionStoreTests, BattleSessionDispatchTests, BattleSession PumpTests, ArenaTwoPickBattleControllerTests (which now seeds a TK2 run + adds a no-active-run 400 case). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,10 @@ namespace SVSim.BattleNode.Bridge;
|
||||
public interface IMatchingBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// Mint a battle id, register a pending session for the given viewer, and return the
|
||||
/// URL the client should open a socket to.
|
||||
/// Mint a battle id, register a pending session for the given viewer with their per-battle
|
||||
/// MatchContext snapshot, and return the URL the client should open a socket to.
|
||||
/// </summary>
|
||||
PendingMatch RegisterPendingBattle(long viewerId);
|
||||
PendingMatch RegisterPendingBattle(long viewerId, MatchContext context);
|
||||
}
|
||||
|
||||
public sealed record PendingMatch(string BattleId, string NodeServerUrl);
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class MatchingBridge : IMatchingBridge
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public PendingMatch RegisterPendingBattle(long viewerId)
|
||||
public PendingMatch RegisterPendingBattle(long viewerId, MatchContext context)
|
||||
{
|
||||
// 12-digit decimal battle id mirrors the captures (e.g. "975695075012").
|
||||
// Two unbiased 6-digit draws concatenated — RandomNumberGenerator.GetInt32 uses
|
||||
@@ -31,7 +31,7 @@ public sealed class MatchingBridge : IMatchingBridge
|
||||
var hi = RandomNumberGenerator.GetInt32(0, 1_000_000);
|
||||
var lo = RandomNumberGenerator.GetInt32(0, 1_000_000);
|
||||
var battleId = $"{hi:D6}{lo:D6}";
|
||||
_store.RegisterPending(new PendingBattle(battleId, viewerId));
|
||||
_store.RegisterPending(new PendingBattle(battleId, viewerId, context));
|
||||
return new PendingMatch(battleId, _options.NodeServerUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public sealed class BattleNodeWebSocketHandler
|
||||
|
||||
var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||
_store.RemovePending(battleId);
|
||||
var session = new BattleSession(ws, battleId, viewerId, _loggerFactory.CreateLogger<BattleSession>());
|
||||
var session = new BattleSession(ws, battleId, viewerId, pending.Context, _loggerFactory.CreateLogger<BattleSession>());
|
||||
await session.RunAsync(ctx.RequestAborted);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Net.WebSockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Lifecycle;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Protocol.Bodies;
|
||||
@@ -37,12 +38,19 @@ public sealed class BattleSession
|
||||
public InboundTracker Inbound { get; } = new();
|
||||
public OutboundSequencer Outbound { get; } = new();
|
||||
|
||||
public BattleSession(WebSocket ws, string battleId, long viewerId, ILogger<BattleSession> log)
|
||||
/// <summary>
|
||||
/// Player-side snapshot captured at do_matching time. ScriptedLifecycle reads the player
|
||||
/// half of Matched/BattleStart frames from here; opponent half stays in ScriptedProfiles.
|
||||
/// </summary>
|
||||
internal MatchContext Context { get; }
|
||||
|
||||
public BattleSession(WebSocket ws, string battleId, long viewerId, MatchContext context, ILogger<BattleSession> log)
|
||||
{
|
||||
_ws = ws;
|
||||
_log = log;
|
||||
BattleId = battleId;
|
||||
ViewerId = viewerId;
|
||||
Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using SVSim.BattleNode.Bridge;
|
||||
|
||||
namespace SVSim.BattleNode.Sessions;
|
||||
|
||||
/// <summary>
|
||||
/// Sparse pre-connect record: enough to validate the incoming WS connect and resolve
|
||||
/// the viewer. Full BattleSession is created on connect.
|
||||
/// Sparse pre-connect record: viewer id + the per-battle MatchContext snapshot. Enough to
|
||||
/// validate the incoming WS connect, resolve the viewer, and seed the BattleSession with the
|
||||
/// player-half lifecycle data. Full BattleSession is created on connect.
|
||||
/// </summary>
|
||||
public sealed record PendingBattle(string BattleId, long ViewerId);
|
||||
public sealed record PendingBattle(string BattleId, long ViewerId, MatchContext Context);
|
||||
|
||||
@@ -11,24 +11,37 @@ public class ArenaTwoPickBattleController : SVSimController
|
||||
{
|
||||
private readonly IArenaTwoPickService _svc;
|
||||
private readonly IMatchingBridge _matching;
|
||||
private readonly IMatchContextBuilder _matchContextBuilder;
|
||||
|
||||
public ArenaTwoPickBattleController(IArenaTwoPickService svc, IMatchingBridge matching)
|
||||
public ArenaTwoPickBattleController(
|
||||
IArenaTwoPickService svc,
|
||||
IMatchingBridge matching,
|
||||
IMatchContextBuilder matchContextBuilder)
|
||||
{
|
||||
_svc = svc;
|
||||
_matching = matching;
|
||||
_matchContextBuilder = matchContextBuilder;
|
||||
}
|
||||
|
||||
[HttpPost("do_matching")]
|
||||
public IActionResult DoMatching([FromBody] DoMatchingRequest req)
|
||||
public async Task<IActionResult> DoMatching([FromBody] DoMatchingRequest req)
|
||||
{
|
||||
if (!TryGetViewerId(out var vid)) return Unauthorized();
|
||||
var match = _matching.RegisterPendingBattle(vid);
|
||||
return Ok(new DoMatchingResponseDto
|
||||
try
|
||||
{
|
||||
MatchingState = 3004,
|
||||
BattleId = match.BattleId,
|
||||
NodeServerUrl = match.NodeServerUrl,
|
||||
});
|
||||
var ctx = await _matchContextBuilder.BuildForTwoPickAsync(vid);
|
||||
var match = _matching.RegisterPendingBattle(vid, ctx);
|
||||
return Ok(new DoMatchingResponseDto
|
||||
{
|
||||
MatchingState = 3004,
|
||||
BattleId = match.BattleId,
|
||||
NodeServerUrl = match.NodeServerUrl,
|
||||
});
|
||||
}
|
||||
catch (ArenaTwoPickException ex)
|
||||
{
|
||||
return BadRequest(new { error_code = ex.ErrorCode });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("finish")]
|
||||
|
||||
@@ -12,14 +12,16 @@ public class MatchingBridgeTests
|
||||
{
|
||||
var store = new InMemoryBattleSessionStore();
|
||||
var bridge = new MatchingBridge(store, new BattleNodeOptions { NodeServerUrl = "localhost:5148/socket.io/" });
|
||||
var ctx = FixtureCtx();
|
||||
|
||||
var match = bridge.RegisterPendingBattle(viewerId: 906243102);
|
||||
var match = bridge.RegisterPendingBattle(viewerId: 906243102, context: ctx);
|
||||
|
||||
Assert.That(match.NodeServerUrl, Is.EqualTo("localhost:5148/socket.io/"));
|
||||
Assert.That(match.BattleId, Is.Not.Empty);
|
||||
var pending = store.TryGetPending(match.BattleId);
|
||||
Assert.That(pending, Is.Not.Null);
|
||||
Assert.That(pending!.ViewerId, Is.EqualTo(906243102));
|
||||
Assert.That(pending.Context, Is.SameAs(ctx));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -27,8 +29,8 @@ public class MatchingBridgeTests
|
||||
{
|
||||
var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions());
|
||||
|
||||
var a = bridge.RegisterPendingBattle(1);
|
||||
var b = bridge.RegisterPendingBattle(2);
|
||||
var a = bridge.RegisterPendingBattle(1, FixtureCtx());
|
||||
var b = bridge.RegisterPendingBattle(2, FixtureCtx());
|
||||
|
||||
Assert.That(a.BattleId, Is.Not.EqualTo(b.BattleId));
|
||||
}
|
||||
@@ -38,9 +40,16 @@ public class MatchingBridgeTests
|
||||
{
|
||||
var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions());
|
||||
|
||||
var match = bridge.RegisterPendingBattle(viewerId: 1);
|
||||
var match = bridge.RegisterPendingBattle(viewerId: 1, context: FixtureCtx());
|
||||
|
||||
Assert.That(match.BattleId, Has.Length.EqualTo(12));
|
||||
Assert.That(match.BattleId, Does.Match("^[0-9]{12}$"));
|
||||
}
|
||||
|
||||
private static MatchContext FixtureCtx() => new(
|
||||
SelfDeckCardIds: 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);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,9 @@ 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.
|
||||
/// 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, asserting the right scripted frames come back in order.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Timeout(30000)]
|
||||
@@ -28,11 +27,10 @@ public class BattleNodeFlowTests
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
var ct = cts.Token;
|
||||
var pending = bridge.RegisterPendingBattle(viewerId: 906243102);
|
||||
var pending = bridge.RegisterPendingBattle(viewerId: 906243102, context: FixtureCtx());
|
||||
|
||||
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();
|
||||
@@ -40,20 +38,16 @@ public class BattleNodeFlowTests
|
||||
await using var client = new RawSocketIoTestClient(ws);
|
||||
await client.ConsumeHandshakeAsync(ct);
|
||||
|
||||
// 1. InitNetwork → expect InitNetwork ack push only.
|
||||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
|
||||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
||||
|
||||
// 2. InitBattle → expect Matched (handler is now subscribed on the client side).
|
||||
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
|
||||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||||
|
||||
// 3. Loaded → expect BattleStart + Deal.
|
||||
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));
|
||||
|
||||
// 4. Swap with empty idxList → expect Swap response + Ready.
|
||||
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));
|
||||
@@ -62,7 +56,6 @@ public class BattleNodeFlowTests
|
||||
|
||||
private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri, long pubSeq, Dictionary<string, object?>? body = null) =>
|
||||
new(uri, ViewerId: 906243102, Uuid: "udid-test", Bid: null, Try: 0,
|
||||
// EmitMsgPack: InitNetwork → general(99); other matching URIs → matching(2); else battle(1).
|
||||
Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General
|
||||
: uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching
|
||||
: EmitCategory.Battle,
|
||||
@@ -73,4 +66,11 @@ public class BattleNodeFlowTests
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Protocol.Bodies;
|
||||
using SVSim.BattleNode.Sessions;
|
||||
@@ -13,9 +14,16 @@ public class BattleSessionDispatchTests
|
||||
private static BattleSession NewSession()
|
||||
{
|
||||
// ws is unused by ComputeResponses; pass null! and rely on the test never invoking the pump.
|
||||
return new BattleSession(ws: null!, battleId: "bid-1", viewerId: 1, log: NullLogger<BattleSession>.Instance);
|
||||
return new BattleSession(ws: null!, battleId: "bid-1", viewerId: 1, context: FixtureCtx(), log: NullLogger<BattleSession>.Instance);
|
||||
}
|
||||
|
||||
private static MatchContext FixtureCtx() => new(
|
||||
SelfDeckCardIds: 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);
|
||||
|
||||
private static MsgEnvelope NewEnvelope(NetworkBattleUri uri) =>
|
||||
new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0, Cat: EmitCategory.Battle,
|
||||
PubSeq: null, PlaySeq: null, Body: new RawBody(new Dictionary<string, object?>()));
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions;
|
||||
using SVSim.BattleNode.Wire;
|
||||
@@ -39,7 +40,7 @@ public class BattleSessionPumpTests
|
||||
{
|
||||
var ws = new TestWebSocket();
|
||||
var session = new BattleSession(
|
||||
ws: ws, battleId: "bid-pump", viewerId: 906243102,
|
||||
ws: ws, battleId: "bid-pump", viewerId: 906243102, context: FixtureCtx(),
|
||||
log: NullLogger<BattleSession>.Instance);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
@@ -84,7 +85,7 @@ public class BattleSessionPumpTests
|
||||
{
|
||||
var ws = new TestWebSocket();
|
||||
var session = new BattleSession(
|
||||
ws: ws, battleId: "bid-cancel", viewerId: 906243102,
|
||||
ws: ws, battleId: "bid-cancel", viewerId: 906243102, context: FixtureCtx(),
|
||||
log: NullLogger<BattleSession>.Instance);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
@@ -127,7 +128,7 @@ public class BattleSessionPumpTests
|
||||
var ws = new TestWebSocket();
|
||||
var logCapture = new CapturingLogger();
|
||||
var session = new BattleSession(
|
||||
ws: ws, battleId: "bid-clip", viewerId: 906243102, log: logCapture);
|
||||
ws: ws, battleId: "bid-clip", viewerId: 906243102, context: FixtureCtx(), log: logCapture);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
var runTask = session.RunAsync(cts.Token);
|
||||
@@ -208,6 +209,13 @@ public class BattleSessionPumpTests
|
||||
return NodeCrypto.GenerateKey(() => (seq++ * 7) % 16);
|
||||
}
|
||||
|
||||
private static MatchContext FixtureCtx() => new(
|
||||
SelfDeckCardIds: 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);
|
||||
|
||||
private sealed class CapturingLogger : ILogger<BattleSession>
|
||||
{
|
||||
public List<string> Warnings { get; } = new();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Sessions;
|
||||
|
||||
namespace SVSim.UnitTests.BattleNode.Sessions;
|
||||
@@ -13,7 +14,7 @@ public class InMemoryBattleSessionStoreTests
|
||||
[Test]
|
||||
public void RegisterThenGet_ReturnsRegisteredBattle()
|
||||
{
|
||||
var battle = new PendingBattle("bid-1", 906243102);
|
||||
var battle = new PendingBattle("bid-1", 906243102, FixtureCtx());
|
||||
_store.RegisterPending(battle);
|
||||
|
||||
Assert.That(_store.TryGetPending("bid-1"), Is.EqualTo(battle));
|
||||
@@ -28,7 +29,7 @@ public class InMemoryBattleSessionStoreTests
|
||||
[Test]
|
||||
public void Remove_ReturnsTrueWhenPresent_FalseWhenAbsent()
|
||||
{
|
||||
_store.RegisterPending(new PendingBattle("bid", 1));
|
||||
_store.RegisterPending(new PendingBattle("bid", 1, FixtureCtx()));
|
||||
Assert.That(_store.RemovePending("bid"), Is.True);
|
||||
Assert.That(_store.RemovePending("bid"), Is.False);
|
||||
}
|
||||
@@ -36,8 +37,15 @@ public class InMemoryBattleSessionStoreTests
|
||||
[Test]
|
||||
public void Register_DuplicateBattleId_OverwritesPrior()
|
||||
{
|
||||
_store.RegisterPending(new PendingBattle("bid", 1));
|
||||
_store.RegisterPending(new PendingBattle("bid", 2));
|
||||
_store.RegisterPending(new PendingBattle("bid", 1, FixtureCtx()));
|
||||
_store.RegisterPending(new PendingBattle("bid", 2, FixtureCtx()));
|
||||
Assert.That(_store.TryGetPending("bid")!.ViewerId, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private static MatchContext FixtureCtx() => new(
|
||||
SelfDeckCardIds: 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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Controllers;
|
||||
@@ -12,6 +15,8 @@ public class ArenaTwoPickBattleControllerTests
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await factory.SeedViewerAsync();
|
||||
await SeedCompleteTwoPickRunAsync(factory, viewerId);
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var req = new {
|
||||
deck_no = 1L, need_init = 1, log = 1, excluded_field_id_list = new long[] { }, use_stage_select = 1, is_default_skin = 0,
|
||||
@@ -28,12 +33,50 @@ public class ArenaTwoPickBattleControllerTests
|
||||
var battleId = root.GetProperty("battle_id").GetString();
|
||||
Assert.That(battleId, Is.Not.Null.And.Not.Empty);
|
||||
var nodeUrl = root.GetProperty("node_server_url").GetString();
|
||||
// Matches prod wire format: host:port/socket.io/, no scheme prefix.
|
||||
Assert.That(nodeUrl, Does.Contain("/socket.io/"));
|
||||
Assert.That(nodeUrl, Does.Not.StartWith("ws://"));
|
||||
Assert.That(nodeUrl, Does.Not.StartWith("http://"));
|
||||
// Required when matching_state ∈ {3004,3007,3011} per
|
||||
// DoMatchingBase.SettingCardMasterId; client throws KeyNotFoundException without it.
|
||||
Assert.That(root.GetProperty("card_master_id").GetInt32(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DoMatching_NoActiveRun_Returns400WithErrorCode()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await factory.SeedViewerAsync();
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var req = new {
|
||||
deck_no = 1L, need_init = 1, log = 1, excluded_field_id_list = new long[] { }, use_stage_select = 1, is_default_skin = 0,
|
||||
viewer_id = "0", steam_id = 0, steam_session_ticket = "",
|
||||
};
|
||||
var resp = await client.PostAsync("/arena_two_pick_battle/do_matching", JsonContent.Create(req));
|
||||
|
||||
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
Assert.That(doc.RootElement.GetProperty("error_code").GetString(),
|
||||
Is.EqualTo("arena_two_pick_no_active_run"));
|
||||
}
|
||||
|
||||
private static async Task SeedCompleteTwoPickRunAsync(SVSimTestFactory factory, long viewerId)
|
||||
{
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var deck = Enumerable.Range(1, 30).Select(i => 100_011_000L + i).ToList();
|
||||
db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun
|
||||
{
|
||||
ViewerId = viewerId,
|
||||
EntryId = 1,
|
||||
ClassId = 1,
|
||||
LeaderSkinId = 1,
|
||||
SelectedCardIdsJson = JsonSerializer.Serialize(deck),
|
||||
IsSelectCompleted = true,
|
||||
MaxBattleCount = 5,
|
||||
CandidateClassIdsJson = "[1,2,3]",
|
||||
PendingPickSetsJson = "[]",
|
||||
ResultListJson = "[]",
|
||||
NextCandidateId = 1,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user