BattlePassRepository._curveCache and MissionCatalogRepository._maxLevelCache
were private-static fields populated lazily on first read from whatever
DbContext happened to be in scope. In production "one DbContext lineage
per process" makes that fine. Under parallel test execution each
SVSimTestFactory owns its own SQLite :memory: DB, so the first reader's
DB (often empty, in tests that don't seed BP) poisoned the cache for
concurrent readers from a seeded DB — assertions like "BP level info
must be present after seeding" failed because the process-static cache
returned an empty list populated by the other test's empty DB.
The first patch attempted a `BypassCacheForTests` static flag, which is
exactly the kind of test-only seam that rots the production code: future
caches get the same flag, repos accumulate hidden knobs, and the
underlying invariant ("a cache populated from arbitrary scope serves
arbitrary scope") goes unaddressed.
Instead, move both caches into the DI-registered IMemoryCache.
AddMemoryCache() registers it as singleton-per-service-provider:
production has one provider → one IMemoryCache → identical caching
semantics to before. Each WebApplicationFactory builds its own
provider → its own IMemoryCache → cache is naturally scoped per fixture,
no cross-test bleed possible.
The ResetLevelCurveCache() method and its three call sites
(SVSimTestFactory.SeedGlobalsAsync, BattlePassServiceTests,
LoadControllerTests) are deleted — a fresh factory owns a fresh empty
cache, no manual invalidation needed.
With this and the previous StoryService fixture-instance fix in place,
ParallelScope.All works: 776/776 in 57s wall clock (down from 59s on
Fixtures, 2m13s pre-parallelism).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
408 lines
21 KiB
C#
408 lines
21 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using SVSim.Database;
|
|
using SVSim.Database.Enums;
|
|
using SVSim.Database.Models;
|
|
using SVSim.UnitTests.Infrastructure;
|
|
|
|
namespace SVSim.UnitTests.Controllers;
|
|
|
|
/// <summary>
|
|
/// Coverage for <c>/load/index</c>. The endpoint hits the heaviest <c>.Include</c> chain in the
|
|
/// app (<c>ViewerRepository.GetViewerByShortUdid</c>) and serializes the wide
|
|
/// <c>IndexResponse</c> shape — first end-to-end exercise of either against a real EF provider.
|
|
/// Shape assertions are split per test so a single regression pinpoints one named expectation.
|
|
/// </summary>
|
|
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>
|
|
/// or <c>TryGetValue</c> guard). Missing any of these crashes the client.
|
|
///
|
|
/// Fields that ARE guarded by the client get a separate, dedicated assertion (or no assertion)
|
|
/// — they're allowed to be omitted. Examples: <c>arena_info</c>, <c>daily_login_bonus</c>,
|
|
/// <c>battle_pass_level_info</c>, <c>pre_release_info</c>, <c>my_rotation_info</c>,
|
|
/// <c>avatar_info</c>, <c>item_expire_date</c> are all optional per
|
|
/// <c>docs/api-spec/endpoints/post-login/load-index.md</c>.
|
|
/// </summary>
|
|
private static readonly string[] RequiredIndexKeys =
|
|
{
|
|
"user_tutorial", "user_info", "user_crystal_count", "user_item_list",
|
|
"user_deck_rotation", "user_deck_unlimited", "user_deck_my_rotation",
|
|
"user_card_list", "user_class_list", "user_sleeve_list", "user_emblem_list",
|
|
"user_degree_list", "user_leader_skin_list", "user_mypage_list",
|
|
"user_rank", "user_rank_match_list", "challenge_config",
|
|
"red_ether_overwrite_list", "maintenance_card_list", "rank_info",
|
|
"class_exp", "loading_exclusion_card_list", "default_setting",
|
|
"unlimited_restricted_base_card_id_list", "rotation_card_set_id_list",
|
|
"reprinted_base_card_ids", "spot_cards", "feature_maintenance_list",
|
|
"special_crystal_info", "open_battle_field_id_list", "loot_box_regulation",
|
|
"gathering_info", "user_config", "deck_format", "card_set_id_for_resource_dl_view"
|
|
};
|
|
|
|
private static async Task<JsonElement> PostIndexAndReadBody(SVSimTestFactory factory, long viewerId)
|
|
{
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
var response = await client.PostAsync("/load/index",
|
|
new StringContent(IndexRequestJson, Encoding.UTF8, "application/json"));
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
|
|
|
var doc = JsonDocument.Parse(body);
|
|
return doc.RootElement.Clone();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_with_minimal_viewer_returns_200()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
|
|
var response = await client.PostAsync("/load/index",
|
|
new StringContent(IndexRequestJson, Encoding.UTF8, "application/json"));
|
|
|
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK),
|
|
await response.Content.ReadAsStringAsync());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_with_no_auth_header_returns_401()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
using var client = factory.CreateClient();
|
|
|
|
var response = await client.PostAsync("/load/index",
|
|
new StringContent(IndexRequestJson, Encoding.UTF8, "application/json"));
|
|
|
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_returns_all_required_keys()
|
|
{
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
var missing = RequiredIndexKeys.Where(k => !root.TryGetProperty(k, out _)).ToList();
|
|
Assert.That(missing, Is.Empty,
|
|
$"Required IndexResponse keys missing: {string.Join(", ", missing)}");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_rank_info_is_array_not_dict()
|
|
{
|
|
// Guards the dict-vs-array regression that ate a previous release. Client iterates
|
|
// user_rank by index; a dict would silently deserialize as zero entries.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
Assert.That(root.GetProperty("user_rank").ValueKind, Is.EqualTo(JsonValueKind.Array));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_user_rank_has_five_entries()
|
|
{
|
|
// Hard-coded format list in LoadController.RankFormats — five entries, one per
|
|
// deck_format discriminator. Client indexes by format value; mismatched count
|
|
// would point the wrong format at the wrong rank slot.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
Assert.That(root.GetProperty("user_rank").GetArrayLength(), Is.EqualTo(5));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_user_rank_deck_formats_are_wire_codes_not_internal_enum()
|
|
{
|
|
// Regression for the /load/index KeyNotFoundException crash (2026-05-23):
|
|
// server was emitting (int)Format directly, so deck_format 0 (Format.Rotation
|
|
// internal) reached the client, ParseApiFormat mapped wire-0 to Format.Max, and
|
|
// LoadDetail._userRank[2] threw. Wire codes per Data.FormatConvertApi:
|
|
// Rotation→1, Unlimited→2, Crossover→4, MyRotation→5, Avatar→39.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
var deckFormats = root.GetProperty("user_rank").EnumerateArray()
|
|
.Select(e => e.GetProperty("deck_format").GetInt32())
|
|
.ToList();
|
|
Assert.That(deckFormats, Is.EquivalentTo(new[] { 1, 2, 5, 39, 4 }),
|
|
"user_rank entries must carry wire deck_format codes, not internal Format ints.");
|
|
|
|
// The top-level deck_format default is also a wire code (Rotation = wire 1).
|
|
Assert.That(root.GetProperty("deck_format").GetInt32(), Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_rotation_card_set_id_list_has_at_least_two_entries()
|
|
{
|
|
// LoadDetail.cs:184 unconditionally indexes [1] and [Count-1] — fewer than two
|
|
// entries crashes the client at the home screen.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
Assert.That(root.GetProperty("rotation_card_set_id_list").GetArrayLength(),
|
|
Is.GreaterThanOrEqualTo(2));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_omits_arena_info_when_empty()
|
|
{
|
|
// ArenaData(JsonData) ctor reads data[0] inside the Keys.Contains("arena_info")
|
|
// branch (LoadDetail.cs:261 → ArenaData.cs:48) — an empty array crashes the client
|
|
// with ArgumentOutOfRangeException. Field must be absent when there's no arena.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
Assert.That(root.TryGetProperty("arena_info", out _), Is.False,
|
|
"arena_info must be omitted when empty; the client crashes on []. " +
|
|
"If you re-add it, populate at least one entry with a valid format_info.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_user_card_list_excludes_zero_count_entries()
|
|
{
|
|
// Documents the divergence from prod (see load-index.md §user_card_list policy).
|
|
// Our server emits only owned cards (Count > 0) plus basics; prod returns a
|
|
// larger curated set that includes some 0-count "ever-touched" rows we don't
|
|
// model. The client falls back to 0 for absent ids (DataMgr.cs:1182), so this
|
|
// is semantically safe — but if anything ever starts emitting Count=0 rows again
|
|
// (e.g. someone re-introduces a left-join against the full card catalog), this
|
|
// test pins the policy.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
var userCards = root.GetProperty("user_card_list");
|
|
Assert.That(userCards.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
|
var zeroCount = userCards.EnumerateArray()
|
|
.Where(c => c.GetProperty("number").GetInt32() == 0)
|
|
.ToList();
|
|
Assert.That(zeroCount, Is.Empty,
|
|
"user_card_list must not contain Count=0 entries; we ship only the owned-only " +
|
|
"subset (plus basics with count=3). See load-index.md §user_card_list policy.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_when_viewer_has_no_decks_returns_empty_format_lists()
|
|
{
|
|
// A freshly-registered viewer has no decks of any format. The three per-format deck
|
|
// containers must still be present and empty so the client's iteration is well-formed.
|
|
using var factory = new SVSimTestFactory();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
foreach (var key in new[] { "user_deck_rotation", "user_deck_unlimited", "user_deck_my_rotation" })
|
|
{
|
|
var container = root.GetProperty(key);
|
|
Assert.That(container.ValueKind, Is.EqualTo(JsonValueKind.Object),
|
|
$"{key} should be the UserFormatDeckInfo object wrapper, not a raw array.");
|
|
var inner = container.GetProperty("user_deck_list");
|
|
Assert.That(inner.ValueKind, Is.EqualTo(JsonValueKind.Array));
|
|
Assert.That(inner.GetArrayLength(), Is.EqualTo(0),
|
|
$"{key}.user_deck_list must be an empty array for a deckless viewer, not null.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task Index_surfaces_seeded_globals_after_bootstrap()
|
|
{
|
|
// Verifies the end-to-end seed → repo → controller wiring for the load-index globals.
|
|
// Counts and spot-checked values come from the 2026-05-23 capture; if a recapture lands
|
|
// with different cardinalities, update the assertions alongside.
|
|
using var factory = new SVSimTestFactory();
|
|
await factory.SeedGlobalsAsync();
|
|
long viewerId = await factory.SeedViewerAsync();
|
|
|
|
var root = await PostIndexAndReadBody(factory, viewerId);
|
|
|
|
// SpotCards: dict[card_id_str] → cost, 239 entries
|
|
var spotCards = root.GetProperty("spot_cards");
|
|
Assert.That(spotCards.ValueKind, Is.EqualTo(JsonValueKind.Object));
|
|
Assert.That(spotCards.EnumerateObject().Count(), Is.EqualTo(239), "spot_cards entry count");
|
|
|
|
// ReprintedCards: flat number[], 54 entries
|
|
var reprinted = root.GetProperty("reprinted_base_card_ids");
|
|
Assert.That(reprinted.GetArrayLength(), Is.EqualTo(54), "reprinted_base_card_ids length");
|
|
|
|
// UnlimitedBanList: dict[card_id_str] → restriction value, 3 entries; 107813030 = hard ban
|
|
var bans = root.GetProperty("unlimited_restricted_base_card_id_list");
|
|
Assert.That(bans.EnumerateObject().Count(), Is.EqualTo(3));
|
|
Assert.That(bans.GetProperty("107813030").GetInt32(), Is.EqualTo(1));
|
|
|
|
// LoadingExclusion: 176 ids
|
|
Assert.That(root.GetProperty("loading_exclusion_card_list").GetArrayLength(), Is.EqualTo(176));
|
|
|
|
// GameConfiguration-sourced scalars
|
|
Assert.That(root.GetProperty("is_battle_pass_period").GetBoolean(), Is.True,
|
|
"is_battle_pass_period is bool on the wire (matches prod 2026-05-23)");
|
|
Assert.That(root.GetProperty("card_set_id_for_resource_dl_view").GetInt32(), Is.EqualTo(1));
|
|
|
|
// challenge_config sourced from GameConfiguration cols
|
|
var challenge = root.GetProperty("challenge_config");
|
|
Assert.That(challenge.GetProperty("use_challenge_two_pick_premium_card").GetInt32(), Is.EqualTo(0));
|
|
Assert.That(challenge.GetProperty("challenge_two_pick_sleeve_id").GetInt32(), Is.EqualTo(3000011));
|
|
|
|
// arena_info: single element with format_info populated
|
|
Assert.That(root.TryGetProperty("arena_info", out var arenaInfo), Is.True,
|
|
"arena_info present once an ArenaSeasonConfig row is seeded");
|
|
Assert.That(arenaInfo.GetArrayLength(), Is.EqualTo(1));
|
|
var fi = arenaInfo[0].GetProperty("format_info");
|
|
Assert.That(fi.GetProperty("card_pool_name").GetString(), Does.Contain("Take Two"));
|
|
|
|
// my_rotation_info: setting dict has 27 entries
|
|
var mri = root.GetProperty("my_rotation_info");
|
|
Assert.That(mri.GetProperty("setting").EnumerateObject().Count(), Is.EqualTo(27));
|
|
Assert.That(mri.GetProperty("abilities").EnumerateObject().Count(), Is.EqualTo(6));
|
|
|
|
// my_rotation_info.schedules drives the client's "Custom Rotation" button visibility
|
|
// (Wizard/MyRotationAllInfo.cs:45 — IsMyRotationEnable). RotationConfigImporter sources the
|
|
// window from the prod capture; default-initialised DateTime.MinValue values would hide
|
|
// the button. Assert the captured 2024→2030 free_battle window round-trips through the
|
|
// MyRotationScheduleConfig section.
|
|
var fb = mri.GetProperty("schedules").GetProperty("free_battle");
|
|
Assert.That(DateTime.Parse(fb.GetProperty("begin_time").GetString()!),
|
|
Is.EqualTo(new DateTime(2024, 5, 1, 20, 0, 0, DateTimeKind.Utc)));
|
|
Assert.That(DateTime.Parse(fb.GetProperty("end_time").GetString()!),
|
|
Is.EqualTo(new DateTime(2030, 6, 26, 19, 59, 59, DateTimeKind.Utc)));
|
|
|
|
// avatar_info: abilities dict has 24 entries; schedules is empty list
|
|
var ai = root.GetProperty("avatar_info");
|
|
Assert.That(ai.GetProperty("abilities").EnumerateObject().Count(), Is.EqualTo(24));
|
|
Assert.That(ai.GetProperty("schedules").ValueKind, Is.EqualTo(JsonValueKind.Array));
|
|
Assert.That(ai.GetProperty("schedules").GetArrayLength(), Is.EqualTo(0));
|
|
|
|
// pre_release_info: present (singleton seeded, even with stale dates per audit)
|
|
Assert.That(root.TryGetProperty("pre_release_info", out var pri), Is.True);
|
|
Assert.That(pri.GetProperty("id").GetString(), Is.EqualTo("1"));
|
|
|
|
// rotation_card_set_id_list: now comes from the real CardSets table — six entries after
|
|
// RotationFlagUpdater flags IsInRotation on the rotation_card_set_ids seeded list. But
|
|
// CardImport isn't run in tests, so the table is empty and we fall back to StubRotationSets
|
|
// (3 entries). That's still ≥ 2 so the client won't crash.
|
|
Assert.That(root.GetProperty("rotation_card_set_id_list").GetArrayLength(),
|
|
Is.GreaterThanOrEqualTo(2));
|
|
|
|
// 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");
|
|
|
|
// 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]
|
|
public async Task LoadIndex_GrantsMissingCosmeticsForOwnedCards()
|
|
{
|
|
// Verifies the C.2 wiring: /load/index invokes ICardAcquisitionService in backfill mode,
|
|
// so a viewer who already owns a leader card but lacks its associated cosmetics gets
|
|
// them granted on the next load. Mirrors the in-flight migration story for existing
|
|
// accounts that pre-date the cosmetic-grant feature.
|
|
using var factory = new SVSimTestFactory();
|
|
var viewerId = await factory.SeedViewerAsync();
|
|
|
|
// Seed viewer with leader card 704741010 (count=1), seed mapping → skin 407, seed
|
|
// master skin row. Viewer does NOT yet own the skin.
|
|
using (var scope = factory.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
var viewer = await db.Viewers.Include(v => v.Cards).FirstAsync(v => v.Id == viewerId);
|
|
var card = await db.Cards.FindAsync(704741010L);
|
|
if (card is null)
|
|
{
|
|
card = new ShadowverseCardEntry { Id = 704741010L, Name = "TestLeader", Rarity = Rarity.Legendary, IsFoil = false };
|
|
db.Cards.Add(card);
|
|
}
|
|
viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = 1, IsProtected = false });
|
|
db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
|
|
if (await db.LeaderSkins.FindAsync(407) is null)
|
|
db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
// Call /load/index — backfill should fire as part of the action.
|
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
|
var response = await client.PostAsync("/load/index",
|
|
new StringContent(IndexRequestJson, Encoding.UTF8, "application/json"));
|
|
|
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK),
|
|
await response.Content.ReadAsStringAsync());
|
|
|
|
// Verify the response payload includes the backfilled cosmetic (not just the DB state).
|
|
// This guards against a regression where the controller serves a stale viewer snapshot
|
|
// (GetViewerByShortUdid uses .AsNoTracking() so the in-memory `viewer` reference does
|
|
// not see writes the service makes on its own tracked instance — without a post-grant
|
|
// re-fetch the first /load/index would report the skin as un-owned even though the DB
|
|
// had been updated). user_leader_skin_list always carries all master skins; the per-entry
|
|
// is_owned flag is the actual ownership signal.
|
|
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
|
var skin407 = doc.RootElement.GetProperty("user_leader_skin_list").EnumerateArray()
|
|
.FirstOrDefault(e => e.GetProperty("leader_skin_id").GetInt32() == 407);
|
|
Assert.That(skin407.ValueKind, Is.Not.EqualTo(JsonValueKind.Undefined),
|
|
"response payload should include leader skin 407 entry");
|
|
Assert.That(skin407.GetProperty("is_owned").GetBoolean(), Is.True,
|
|
"response payload should mark backfilled skin 407 as owned, not just DB state");
|
|
|
|
// Verify skin 407 was actually granted by re-reading viewer state.
|
|
using (var scope = factory.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
|
var viewer = await db.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
|
|
Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True,
|
|
"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);
|
|
}
|
|
|
|
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"));
|
|
}
|
|
}
|