From 9bec1df52f6424a0e8d5916e45d5477da2e9740a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 26 May 2026 22:56:41 -0400 Subject: [PATCH] feat(load-index): populate battle_pass_level_info from IBattlePassService Wire IBattlePassService.GetLevelCurveAsync into LoadController so /load/index emits the 100-entry battle_pass_level_info dict when levels are seeded. Also adds BattlePassRepository.ResetLevelCurveCache() to bust the process-level static cache in tests that seed levels after earlier HTTP calls have primed it with an empty list, and updates SVSimTestFactory.SeedGlobalsAsync + the stale Index_surfaces_seeded_globals_after_bootstrap assertion accordingly. Co-Authored-By: Claude Sonnet 4.6 --- .../BattlePass/BattlePassRepository.cs | 7 ++++ .../Controllers/LoadController.cs | 10 +++-- .../Controllers/LoadControllerTests.cs | 38 ++++++++++++++++++- .../Infrastructure/SVSimTestFactory.cs | 3 ++ 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/SVSim.Database/Repositories/BattlePass/BattlePassRepository.cs b/SVSim.Database/Repositories/BattlePass/BattlePassRepository.cs index 0d60cf5..d2f1899 100644 --- a/SVSim.Database/Repositories/BattlePass/BattlePassRepository.cs +++ b/SVSim.Database/Repositories/BattlePass/BattlePassRepository.cs @@ -50,4 +50,11 @@ public sealed class BattlePassRepository : IBattlePassRepository } finally { _curveCacheLock.Release(); } } + + /// + /// Drops the process-level level-curve cache. Tests that seed BattlePassLevels after the + /// cache has already been populated (by an earlier test's HTTP call) must call this before + /// re-seeding so the next read fetches fresh rows. + /// + public static void ResetLevelCurveCache() => _curveCache = null; } diff --git a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs index 122e5e3..fbc94ba 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs @@ -46,10 +46,12 @@ public class LoadController : SVSimController private readonly IGlobalsRepository _globalsRepository; private readonly ICardAcquisitionService _acquisition; private readonly IGameConfigService _config; + private readonly IBattlePassService _battlePass; public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository, ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository, - ICardAcquisitionService acquisition, IGameConfigService config) + ICardAcquisitionService acquisition, IGameConfigService config, + IBattlePassService battlePass) { _viewerRepository = viewerRepository; _cardRepository = cardRepository; @@ -57,6 +59,7 @@ public class LoadController : SVSimController _globalsRepository = globalsRepository; _acquisition = acquisition; _config = config; + _battlePass = battlePass; } [HttpPost("index")] @@ -194,9 +197,8 @@ public class LoadController : SVSimController LootBoxRegulations = new LootBoxRegulations(), GatheringInfo = new GatheringInfo(), IsBattlePassPeriod = rotation.IsBattlePassPeriod, - // Optional per spec (load-index.md:228). We have BattlePassLevelEntry rows seeded, but - // no per-viewer Battle Pass progression yet — emit null until that subsystem lands. - BattlePassLevelInfo = null, + BattlePassLevelInfo = (await _battlePass.GetLevelCurveAsync(CancellationToken.None)) + as Dictionary, SpecialCrystalInfos = new List(), AvatarRotationInfo = await BuildAvatarInfoAsync(), MyRotationInfo = await BuildMyRotationInfoAsync(), diff --git a/SVSim.UnitTests/Controllers/LoadControllerTests.cs b/SVSim.UnitTests/Controllers/LoadControllerTests.cs index 25ed0bf..ee9f4ac 100644 --- a/SVSim.UnitTests/Controllers/LoadControllerTests.cs +++ b/SVSim.UnitTests/Controllers/LoadControllerTests.cs @@ -21,6 +21,8 @@ public class LoadControllerTests private const string IndexRequestJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam","card_master_hash":""}"""; + private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds"); + /// /// Wire keys (from [Key("...")] / mirrored [JsonPropertyName]) for fields the /// client reads UNCONDITIONALLY in LoadDetail.ConvertJsonData (no Keys.Contains @@ -308,8 +310,12 @@ public class LoadControllerTests // Optional/absent fields stay absent when nothing meaningful to surface Assert.That(root.TryGetProperty("daily_login_bonus", out _), Is.False, "daily_login_bonus optional per spec; emit null when no active campaign"); - Assert.That(root.TryGetProperty("battle_pass_level_info", out _), Is.False, - "battle_pass_level_info optional per spec; emit null until viewer pass state is wired"); + + // battle_pass_level_info is present when levels are seeded — 100-entry dict keyed by level string. + Assert.That(root.TryGetProperty("battle_pass_level_info", out var bpli), Is.True, + "battle_pass_level_info must be present once BattlePassLevels rows are seeded"); + Assert.That(bpli.ValueKind, Is.EqualTo(JsonValueKind.Object)); + Assert.That(bpli.EnumerateObject().Count(), Is.EqualTo(100)); } [Test] @@ -373,4 +379,32 @@ public class LoadControllerTests "skin 407 should have been backfilled by /load/index"); } } + + [Test] + public async Task LoadIndex_emits_battle_pass_level_info_with_100_entries_when_period_active() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + await new SVSim.Bootstrap.Importers.BattlePassImporter().ImportAsync(db, SeedDir); + } + // Bust the process-level level-curve cache so the next /load/index call reads the + // freshly-seeded rows rather than a stale empty list from an earlier test's HTTP call. + SVSim.Database.Repositories.BattlePass.BattlePassRepository.ResetLevelCurveCache(); + + using var client = factory.CreateAuthenticatedClient(viewerId); + var response = await client.PostAsync("/load/index", + new StringContent(IndexRequestJson, System.Text.Encoding.UTF8, "application/json")); + var body = await response.Content.ReadAsStringAsync(); + Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.OK), body); + + using var doc = System.Text.Json.JsonDocument.Parse(body); + var levels = doc.RootElement.GetProperty("battle_pass_level_info"); + Assert.That(levels.ValueKind, Is.EqualTo(System.Text.Json.JsonValueKind.Object)); + Assert.That(levels.GetProperty("1").GetProperty("level").GetString(), Is.EqualTo("1")); + Assert.That(levels.GetProperty("1").GetProperty("required_point").GetString(), Is.EqualTo("0")); + Assert.That(levels.GetProperty("100").GetProperty("required_point").GetString(), Is.EqualTo("49500")); + } } diff --git a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs index c66ea8e..c1b1bc2 100644 --- a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs +++ b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs @@ -10,6 +10,7 @@ using SVSim.Bootstrap.Importers; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; +using SVSim.Database.Repositories.BattlePass; using SVSim.Database.Repositories.Deck; using SVSim.Database.Repositories.Viewer; using SVSim.EmulatedEntrypoint; @@ -196,6 +197,8 @@ internal sealed class SVSimTestFactory : WebApplicationFactory await new AvatarAbilityImporter().ImportAsync(ctx, seedDir); await new ArenaSeasonImporter().ImportAsync(ctx, seedDir); await new BattlePassImporter().ImportAsync(ctx, seedDir); + // Reset the process-level level-curve cache so the next HTTP call reads freshly-seeded rows. + BattlePassRepository.ResetLevelCurveCache(); await new BattlePassSeasonImporter().ImportAsync(ctx, seedDir); await new BattlePassRewardImporter().ImportAsync(ctx, seedDir); await new DailyLoginBonusImporter().ImportAsync(ctx, seedDir);