Files
SVSimServer/SVSim.UnitTests/Services/MatchContextBuilderTests.cs
gamer147 7eaf13893e feat(matching): MatchContextBuilder.BuildForRankBattleAsync for rank battles
Sibling to BuildForTwoPickAsync. Routes through IDeckRepository.GetDeck
to pull the viewer's deck #1 for the requested format (avoiding the
viewer-graph nav-ref auto-load pitfall — DeckCard.Card silently ships
card_id=0 via the default include path). Throws if the viewer has no
deck for the format. Cosmetics fall back to DefaultLoadoutConfig
defaults when unequipped, same shape as TK2.

Used by RankBattleController in a later task to build self-context for
/ai_<fmt>_rank_battle/start and to pair-up under /<fmt>_rank_battle/do_matching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 01:13:19 -04:00

193 lines
7.9 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SVSim.BattleNode.Bridge;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.EmulatedEntrypoint.Services;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Services;
[TestFixture]
public class MatchContextBuilderTests
{
[Test]
public async Task BuildForTwoPick_returns_context_from_run_state()
{
await using var factory = new SVSimTestFactory();
var vid = await factory.SeedViewerAsync();
var deck = Enumerable.Range(1, 30).Select(i => 100_011_000L + i).ToList();
int emblemId, degreeId;
using (var seedScope = factory.Services.CreateScope())
{
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var emblem = await db.Emblems.FirstAsync();
var degree = await db.Degrees.FirstAsync();
emblemId = emblem.Id;
degreeId = degree.Id;
var viewer = await db.Viewers.FindAsync(vid);
viewer!.DisplayName = "Drafter";
viewer.Info.CountryCode = "KOR";
viewer.Info.IsOfficial = false;
viewer.Info.SelectedEmblem = emblem;
viewer.Info.SelectedDegree = degree;
db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun
{
ViewerId = vid,
EntryId = 1,
ClassId = 5,
LeaderSkinId = 5_000_001L,
SelectedCardIdsJson = JsonSerializer.Serialize(deck),
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<IMatchContextBuilder>();
var ctx = await builder.BuildForTwoPickAsync(vid);
Assert.That(ctx.SelfDeckCardIds, Is.EqualTo(deck));
Assert.That(ctx.ClassId, Is.EqualTo("5"));
Assert.That(ctx.CharaId, Is.EqualTo("5000001")); // LeaderSkinId set
Assert.That(ctx.CountryCode, Is.EqualTo("KOR"));
Assert.That(ctx.UserName, Is.EqualTo("Drafter"));
Assert.That(ctx.EmblemId, Is.EqualTo(emblemId.ToString()));
Assert.That(ctx.DegreeId, Is.EqualTo(degreeId.ToString()));
Assert.That(ctx.IsOfficial, Is.EqualTo(0));
Assert.That(ctx.BattleType, Is.EqualTo(11));
// Hardcoded v1 fixtures (see spec §Deferred plumbing)
Assert.That(ctx.CardMasterName, Is.EqualTo("card_master_node_10015"));
Assert.That(ctx.FieldId, Is.EqualTo(43));
// Sleeve comes from ChallengeConfig.TwoPickSleeveId — ShippedDefaults is 0.
Assert.That(ctx.SleeveId, Is.EqualTo("0"));
}
[Test]
public async Task BuildForTwoPick_throws_when_no_run()
{
await using var factory = new SVSimTestFactory();
var vid = await factory.SeedViewerAsync();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<IMatchContextBuilder>();
var ex = Assert.ThrowsAsync<ArenaTwoPickException>(() => builder.BuildForTwoPickAsync(vid));
Assert.That(ex!.ErrorCode, Is.EqualTo("arena_two_pick_no_active_run"));
}
[Test]
public async Task BuildForTwoPick_throws_when_draft_incomplete()
{
await using var factory = new SVSimTestFactory();
var vid = await factory.SeedViewerAsync();
using (var seedScope = factory.Services.CreateScope())
{
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun
{
ViewerId = vid,
EntryId = 1,
ClassId = 1,
SelectedCardIdsJson = JsonSerializer.Serialize(new long[] { 100_011_001L, 100_011_002L }),
IsSelectCompleted = false,
CandidateClassIdsJson = "[1,2,3]",
PendingPickSetsJson = "[]",
ResultListJson = "[]",
MaxBattleCount = 5,
NextCandidateId = 1,
});
await db.SaveChangesAsync();
}
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<IMatchContextBuilder>();
var ex = Assert.ThrowsAsync<ArenaTwoPickException>(() => builder.BuildForTwoPickAsync(vid));
Assert.That(ex!.ErrorCode, Is.EqualTo("arena_two_pick_draft_incomplete"));
}
[Test]
public async Task BuildForRankBattle_returns_MatchContext_with_format_specific_deck()
{
await using var factory = new SVSimTestFactory();
var viewerId = await factory.SeedViewerAsync(displayName: "Ranker");
await factory.SeedGlobalsAsync();
await factory.SeedDeckAsync(viewerId, Format.Rotation, 1, "Rank Rotation Deck");
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<IMatchContextBuilder>();
var ctx = await builder.BuildForRankBattleAsync(viewerId, Format.Rotation);
Assert.That(ctx.UserName, Is.EqualTo("Ranker"));
Assert.That(ctx.BattleType, Is.EqualTo(11), "BattleType=11 matches the prod rank-battle wire value (same as TK2).");
Assert.That(ctx.ClassId, Is.Not.Null.And.Not.Empty, "ClassId from the selected deck's class.");
Assert.That(ctx.CardMasterName, Is.EqualTo("card_master_node_10015"));
Assert.That(ctx.FieldId, Is.EqualTo(43));
}
[Test]
public async Task BuildForRankBattle_throws_when_no_deck_for_format()
{
await using var factory = new SVSimTestFactory();
var viewerId = await factory.SeedViewerAsync();
await factory.SeedGlobalsAsync();
// Intentionally no SeedDeckAsync for Rotation.
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<IMatchContextBuilder>();
Assert.That(async () => await builder.BuildForRankBattleAsync(viewerId, Format.Rotation),
Throws.Exception);
}
[Test]
public async Task BuildForTwoPick_falls_back_to_default_loadout_when_unequipped()
{
await using var factory = new SVSimTestFactory();
var vid = await factory.SeedViewerAsync();
var deck = Enumerable.Range(1, 30).Select(i => 100_011_000L + i).ToList();
using (var seedScope = factory.Services.CreateScope())
{
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
// No SelectedEmblem / SelectedDegree set → default (Id=0) nav rows.
db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun
{
ViewerId = vid,
EntryId = 1,
ClassId = 1,
LeaderSkinId = 0, // No skin chosen → CharaId == ClassId
SelectedCardIdsJson = JsonSerializer.Serialize(deck),
IsSelectCompleted = true,
CandidateClassIdsJson = "[1,2,3]",
PendingPickSetsJson = "[]",
ResultListJson = "[]",
MaxBattleCount = 5,
NextCandidateId = 1,
});
await db.SaveChangesAsync();
}
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<IMatchContextBuilder>();
var ctx = await builder.BuildForTwoPickAsync(vid);
// DefaultLoadoutConfig.ShippedDefaults: EmblemId=100000000, DegreeId=300003
Assert.That(ctx.EmblemId, Is.EqualTo("100000000"));
Assert.That(ctx.DegreeId, Is.EqualTo("300003"));
Assert.That(ctx.CharaId, Is.EqualTo("1")); // falls back to ClassId
}
}