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 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-26 22:56:41 -04:00
parent 9043e20646
commit 9bec1df52f
4 changed files with 52 additions and 6 deletions

View File

@@ -50,4 +50,11 @@ public sealed class BattlePassRepository : IBattlePassRepository
}
finally { _curveCacheLock.Release(); }
}
/// <summary>
/// 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.
/// </summary>
public static void ResetLevelCurveCache() => _curveCache = null;
}

View File

@@ -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<string, BattlePassLevel>,
SpecialCrystalInfos = new List<SpecialCrystalInfo>(),
AvatarRotationInfo = await BuildAvatarInfoAsync(),
MyRotationInfo = await BuildMyRotationInfoAsync(),

View File

@@ -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");
/// <summary>
/// Wire keys (from <c>[Key("...")]</c> / mirrored <c>[JsonPropertyName]</c>) for fields the
/// client reads UNCONDITIONALLY in <c>LoadDetail.ConvertJsonData</c> (no <c>Keys.Contains</c>
@@ -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<SVSimDbContext>();
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"));
}
}

View File

@@ -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<Program>
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);