171 lines
7.5 KiB
C#
171 lines
7.5 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
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":""}""";
|
|
|
|
/// <summary>
|
|
/// Wire keys (from <c>[Key("...")]</c> / mirrored <c>[JsonPropertyName]</c>) for fields the
|
|
/// client reads unconditionally in <c>LoadDetail.ConvertJsonData</c>. These are the names
|
|
/// the decompiled client actually looks up — NOT <c>SnakeCaseLower(C# property name)</c>.
|
|
/// Missing any of these crashes the client with <c>KeyNotFoundException</c> on /load/index.
|
|
/// </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", "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<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_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.");
|
|
}
|
|
}
|
|
}
|