fix(import): tolerate numeric my_rotation_id; skip empty deck slots

A real /load/index dump emits my_rotation_id as a bare number (0) for
unset MyRotation slots, which 400'd against the string? DTO field
(AllowReadingFromString only covers string->number). FlexibleStringConverter
accepts either form. Also skip empty deck slots (no cards) on import — a
dump carries every slot, mostly empty placeholders the client manages
itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-29 21:03:10 -04:00
parent 06108e4b6f
commit f754ef1ad3
4 changed files with 91 additions and 1 deletions

View File

@@ -400,4 +400,54 @@ public class AdminControllerTests
Assert.That(stored.Decks.Any(d => d.Name == "Wire Deck" && d.Format == Format.Rotation), Is.True,
"decks snake_case keys must bind (deck_format/deck_no/class_id/card_id_array/...).");
}
[Test]
public async Task ImportViewer_tolerates_numeric_my_rotation_id_and_skips_empty_decks()
{
using var factory = new SVSimTestFactory();
const ulong steamId = 76_561_198_111_222_341UL;
long viewerId = await factory.SeedViewerAsync(steamId: steamId);
int classId, leaderSkinId; long sleeveId;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
classId = (await db.Classes.FirstAsync()).Id;
sleeveId = (await db.Sleeves.FirstAsync()).Id;
leaderSkinId = (await db.LeaderSkins.FirstAsync()).Id;
}
// Mirrors a real prod dump: empty MyRotation slots carry "my_rotation_id": 0 (a NUMBER,
// not a string), and dozens of empty slots accompany the few real decks.
string json = $$"""
{
"steam_id": {{steamId}},
"decks": [
{ "deck_format": 2, "deck_no": 1, "deck_name": "Real", "class_id": {{classId}},
"sleeve_id": {{sleeveId}}, "leader_skin_id": {{leaderSkinId}},
"is_random_leader_skin": 0, "card_id_array": [10001001, 10001002] },
{ "deck_format": 5, "deck_no": 1, "deck_name": "", "class_id": 0,
"sleeve_id": 3000011, "leader_skin_id": 0, "is_random_leader_skin": 0,
"my_rotation_id": 0, "card_id_array": [] },
{ "deck_format": 1, "deck_no": 3, "deck_name": "", "class_id": 1,
"sleeve_id": 3000011, "leader_skin_id": 0, "is_random_leader_skin": 0,
"card_id_array": [] }
]
}
""";
using var client = factory.CreateClient();
using var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("/admin/import_viewer", content);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK),
await response.Content.ReadAsStringAsync());
using var scope2 = factory.Services.CreateScope();
var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
var stored = await db2.Viewers.Include(v => v.Decks).FirstAsync(v => v.Id == viewerId);
Assert.That(stored.Decks.Count, Is.EqualTo(1),
"Empty deck slots must be skipped; only the real (non-empty) deck imports.");
Assert.That(stored.Decks.Single().Name, Is.EqualTo("Real"));
}
}