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:
@@ -50,4 +50,11 @@ public sealed class BattlePassRepository : IBattlePassRepository
|
|||||||
}
|
}
|
||||||
finally { _curveCacheLock.Release(); }
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ public class LoadController : SVSimController
|
|||||||
private readonly IGlobalsRepository _globalsRepository;
|
private readonly IGlobalsRepository _globalsRepository;
|
||||||
private readonly ICardAcquisitionService _acquisition;
|
private readonly ICardAcquisitionService _acquisition;
|
||||||
private readonly IGameConfigService _config;
|
private readonly IGameConfigService _config;
|
||||||
|
private readonly IBattlePassService _battlePass;
|
||||||
|
|
||||||
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
||||||
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository,
|
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository,
|
||||||
ICardAcquisitionService acquisition, IGameConfigService config)
|
ICardAcquisitionService acquisition, IGameConfigService config,
|
||||||
|
IBattlePassService battlePass)
|
||||||
{
|
{
|
||||||
_viewerRepository = viewerRepository;
|
_viewerRepository = viewerRepository;
|
||||||
_cardRepository = cardRepository;
|
_cardRepository = cardRepository;
|
||||||
@@ -57,6 +59,7 @@ public class LoadController : SVSimController
|
|||||||
_globalsRepository = globalsRepository;
|
_globalsRepository = globalsRepository;
|
||||||
_acquisition = acquisition;
|
_acquisition = acquisition;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_battlePass = battlePass;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("index")]
|
[HttpPost("index")]
|
||||||
@@ -194,9 +197,8 @@ public class LoadController : SVSimController
|
|||||||
LootBoxRegulations = new LootBoxRegulations(),
|
LootBoxRegulations = new LootBoxRegulations(),
|
||||||
GatheringInfo = new GatheringInfo(),
|
GatheringInfo = new GatheringInfo(),
|
||||||
IsBattlePassPeriod = rotation.IsBattlePassPeriod,
|
IsBattlePassPeriod = rotation.IsBattlePassPeriod,
|
||||||
// Optional per spec (load-index.md:228). We have BattlePassLevelEntry rows seeded, but
|
BattlePassLevelInfo = (await _battlePass.GetLevelCurveAsync(CancellationToken.None))
|
||||||
// no per-viewer Battle Pass progression yet — emit null until that subsystem lands.
|
as Dictionary<string, BattlePassLevel>,
|
||||||
BattlePassLevelInfo = null,
|
|
||||||
SpecialCrystalInfos = new List<SpecialCrystalInfo>(),
|
SpecialCrystalInfos = new List<SpecialCrystalInfo>(),
|
||||||
AvatarRotationInfo = await BuildAvatarInfoAsync(),
|
AvatarRotationInfo = await BuildAvatarInfoAsync(),
|
||||||
MyRotationInfo = await BuildMyRotationInfoAsync(),
|
MyRotationInfo = await BuildMyRotationInfoAsync(),
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public class LoadControllerTests
|
|||||||
private const string IndexRequestJson =
|
private const string IndexRequestJson =
|
||||||
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam","card_master_hash":""}""";
|
"""{"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>
|
/// <summary>
|
||||||
/// Wire keys (from <c>[Key("...")]</c> / mirrored <c>[JsonPropertyName]</c>) for fields the
|
/// 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>
|
/// 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
|
// Optional/absent fields stay absent when nothing meaningful to surface
|
||||||
Assert.That(root.TryGetProperty("daily_login_bonus", out _), Is.False,
|
Assert.That(root.TryGetProperty("daily_login_bonus", out _), Is.False,
|
||||||
"daily_login_bonus optional per spec; emit null when no active campaign");
|
"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]
|
[Test]
|
||||||
@@ -373,4 +379,32 @@ public class LoadControllerTests
|
|||||||
"skin 407 should have been backfilled by /load/index");
|
"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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using SVSim.Bootstrap.Importers;
|
|||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Repositories.BattlePass;
|
||||||
using SVSim.Database.Repositories.Deck;
|
using SVSim.Database.Repositories.Deck;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
using SVSim.EmulatedEntrypoint;
|
using SVSim.EmulatedEntrypoint;
|
||||||
@@ -196,6 +197,8 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
|
|||||||
await new AvatarAbilityImporter().ImportAsync(ctx, seedDir);
|
await new AvatarAbilityImporter().ImportAsync(ctx, seedDir);
|
||||||
await new ArenaSeasonImporter().ImportAsync(ctx, seedDir);
|
await new ArenaSeasonImporter().ImportAsync(ctx, seedDir);
|
||||||
await new BattlePassImporter().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 BattlePassSeasonImporter().ImportAsync(ctx, seedDir);
|
||||||
await new BattlePassRewardImporter().ImportAsync(ctx, seedDir);
|
await new BattlePassRewardImporter().ImportAsync(ctx, seedDir);
|
||||||
await new DailyLoginBonusImporter().ImportAsync(ctx, seedDir);
|
await new DailyLoginBonusImporter().ImportAsync(ctx, seedDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user