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