using System.Net; using System.Text; using System.Text.Json; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Controllers; /// /// Coverage for /load/index. The endpoint hits the heaviest .Include chain in the /// app (ViewerRepository.GetViewerByShortUdid) and serializes the wide /// IndexResponse 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. /// public class LoadControllerTests { private const string IndexRequestJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam","card_master_hash":""}"""; /// /// Wire keys (from [Key("...")] / mirrored [JsonPropertyName]) for fields the /// client reads unconditionally in LoadDetail.ConvertJsonData. These are the names /// the decompiled client actually looks up — NOT SnakeCaseLower(C# property name). /// Missing any of these crashes the client with KeyNotFoundException on /load/index. /// 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", "daily_login_bonus", "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 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_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_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."); } } }