From 83298a2d47ddf3c37d530147e551f2e48ab95dc3 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 26 May 2026 14:44:21 -0400 Subject: [PATCH] refactor(bootstrap): migrate default decks to seed file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts /deck/info's default_deck_list into seeds/default-decks.json via the new extract-default-decks.ps1 PowerShell script and imports through DefaultDeckImporter. The importer carries the same orphan- card-id warning the old GlobalsImporter path emitted; production cards yield 0 orphans. WarnOrphans stays inside GlobalsImporter for now — SpotCards/ReprintedCards/UnlimitedRestrictions/LoadingExclusionCards still use it until Task 9. Part of the bootstrap seed refactor (Task 6). --- .../prod-captures/deck-info-2026-05-23.json | 484 ------------------ SVSim.Bootstrap/Data/seeds/default-decks.json | 394 ++++++++++++++ .../Importers/DefaultDeckImporter.cs | 60 +++ SVSim.Bootstrap/Importers/GlobalsImporter.cs | 48 +- .../Models/Seed/DefaultDeckSeed.cs | 13 + SVSim.Bootstrap/Program.cs | 2 + .../Importers/DefaultDeckImporterTests.cs | 120 +++++ .../Infrastructure/SVSimTestFactory.cs | 2 + 8 files changed, 593 insertions(+), 530 deletions(-) delete mode 100644 SVSim.Bootstrap/Data/prod-captures/deck-info-2026-05-23.json create mode 100644 SVSim.Bootstrap/Data/seeds/default-decks.json create mode 100644 SVSim.Bootstrap/Importers/DefaultDeckImporter.cs create mode 100644 SVSim.Bootstrap/Models/Seed/DefaultDeckSeed.cs create mode 100644 SVSim.UnitTests/Importers/DefaultDeckImporterTests.cs diff --git a/SVSim.Bootstrap/Data/prod-captures/deck-info-2026-05-23.json b/SVSim.Bootstrap/Data/prod-captures/deck-info-2026-05-23.json deleted file mode 100644 index d22023c..0000000 --- a/SVSim.Bootstrap/Data/prod-captures/deck-info-2026-05-23.json +++ /dev/null @@ -1,484 +0,0 @@ -{ - "data_headers": { - "sid": "ac631c29b5f5d07ed5fb6712ad8623c31779553958", - "short_udid": 411054851, - "viewer_id": 906243102, - "servertime": 1779553958, - "result_code": 1 - }, - "data": { - "user_deck_rotation": [], - "user_deck_unlimited": [], - "maintenance_card_list": [], - "user_deck_my_rotation": [], - "trial_deck_list": [], - "default_deck_list": { - "91": { - "null": 1, - "deck_no": 91, - "class_id": 1, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100111010, - 100111010, - 100111010, - 100011020, - 100011020, - 100011020, - 100012010, - 100111020, - 100111020, - 100111020, - 100111040, - 100111040, - 100111040, - 100114010, - 100114010, - 100114010, - 100011030, - 100011030, - 100011030, - 100111060, - 100111060, - 100111060, - 100011040, - 100011040, - 100011040, - 100111030, - 100111030, - 100111030, - 100111050, - 100111050, - 100111050, - 100011050, - 100011050, - 100011050, - 100111070, - 100111070, - 100111070, - 100121010, - 100121010, - 100121010 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "92": { - "null": 1, - "deck_no": 92, - "class_id": 2, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100211010, - 100211010, - 100211010, - 100011020, - 100011020, - 100011020, - 100012010, - 100211020, - 100211020, - 100211020, - 100211060, - 100211060, - 100211060, - 100214010, - 100214010, - 100214010, - 100011030, - 100011030, - 100011030, - 100211030, - 100211030, - 100211030, - 100214020, - 100214020, - 100214020, - 100011040, - 100011040, - 100011040, - 100211040, - 100211040, - 100211040, - 100011050, - 100011050, - 100011050, - 100211050, - 100211050, - 100211050, - 100221020, - 100221020, - 100221020 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "93": { - "null": 1, - "deck_no": 93, - "class_id": 3, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100314010, - 100314010, - 100314010, - 100011020, - 100011020, - 100011020, - 100012010, - 100311010, - 100311010, - 100311010, - 100314030, - 100314030, - 100314030, - 100314020, - 100314020, - 100314020, - 100314040, - 100314040, - 100314040, - 100011030, - 100011030, - 100011030, - 100314050, - 100314050, - 100314050, - 100011040, - 100011040, - 100011040, - 100314060, - 100314060, - 100314060, - 100011050, - 100011050, - 100011050, - 100314070, - 100314070, - 100314070, - 100321010, - 100321010, - 100321010 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "94": { - "null": 1, - "deck_no": 94, - "class_id": 4, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100414020, - 100414020, - 100414020, - 100011020, - 100011020, - 100011020, - 100012010, - 100411010, - 100411010, - 100411010, - 100414010, - 100414010, - 100414010, - 100011030, - 100011030, - 100011030, - 100411050, - 100411050, - 100411050, - 100011040, - 100011040, - 100011040, - 100411030, - 100411030, - 100411030, - 100414030, - 100414030, - 100414030, - 100011050, - 100011050, - 100011050, - 100411020, - 100411020, - 100411020, - 100411040, - 100411040, - 100411040, - 100421020, - 100421020, - 100421020 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "95": { - "null": 1, - "deck_no": 95, - "class_id": 5, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100011020, - 100011020, - 100011020, - 100012010, - 100511010, - 100511010, - 100511010, - 100511020, - 100511020, - 100511020, - 100514010, - 100514010, - 100514010, - 100011030, - 100011030, - 100011030, - 100511030, - 100511030, - 100511030, - 100011040, - 100011040, - 100011040, - 100511040, - 100511040, - 100511040, - 100011050, - 100011050, - 100011050, - 100511050, - 100511050, - 100511050, - 100514020, - 100514020, - 100514020, - 100511060, - 100511060, - 100511060, - 100521030, - 100521030, - 100521030 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "96": { - "null": 1, - "deck_no": 96, - "class_id": 6, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100011020, - 100011020, - 100011020, - 100012010, - 100611010, - 100611010, - 100611010, - 100611020, - 100611020, - 100611020, - 100614010, - 100614010, - 100614010, - 100614020, - 100614020, - 100614020, - 100011030, - 100011030, - 100011030, - 100611030, - 100611030, - 100611030, - 100011040, - 100011040, - 100011040, - 100611050, - 100611050, - 100611050, - 100614030, - 100614030, - 100614030, - 100011050, - 100011050, - 100011050, - 100611040, - 100611040, - 100611040, - 100621010, - 100621010, - 100621010 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "97": { - "null": 1, - "deck_no": 97, - "class_id": 7, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100713020, - 100713020, - 100713020, - 100011020, - 100011020, - 100011020, - 100012010, - 100713010, - 100713010, - 100713010, - 100711010, - 100711010, - 100711010, - 100714010, - 100714010, - 100714010, - 100714020, - 100714020, - 100714020, - 100011030, - 100011030, - 100011030, - 100713030, - 100713030, - 100713030, - 100011040, - 100011040, - 100011040, - 100011050, - 100011050, - 100011050, - 100723010, - 100723010, - 100723010, - 100714030, - 100714030, - 100714030, - 100711020, - 100711020, - 100711020 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - }, - "98": { - "null": 1, - "deck_no": 98, - "class_id": 8, - "sleeve_id": 3000011, - "leader_skin_id": 0, - "deck_name": "Default", - "card_id_array": [ - 100011020, - 100011020, - 100011020, - 100012010, - 100811020, - 100811020, - 100811020, - 100811060, - 100811060, - 100811060, - 100811070, - 100811070, - 100811070, - 100814010, - 100814010, - 100814010, - 100011030, - 100011030, - 100011030, - 100811010, - 100811010, - 100811010, - 100811030, - 100811030, - 100811030, - 100011040, - 100011040, - 100011040, - 100811040, - 100811040, - 100811040, - 100824010, - 100824010, - 100824010, - 100011050, - 100011050, - 100011050, - 100811050, - 100811050, - 100811050 - ], - "is_complete_deck": 1, - "is_available_deck": 1, - "maintenance_card_ids": [] - } - }, - "user_leader_skin_setting_list": { - "1": { - "class_id": 1, - "is_random_leader_skin": 0, - "leader_skin_id": 1 - }, - "2": { - "class_id": 2, - "is_random_leader_skin": 0, - "leader_skin_id": 2 - }, - "3": { - "class_id": 3, - "is_random_leader_skin": 0, - "leader_skin_id": 3 - }, - "4": { - "class_id": 4, - "is_random_leader_skin": 0, - "leader_skin_id": 4 - }, - "5": { - "class_id": 5, - "is_random_leader_skin": 0, - "leader_skin_id": 5 - }, - "6": { - "class_id": 6, - "is_random_leader_skin": 0, - "leader_skin_id": 6 - }, - "7": { - "class_id": 7, - "is_random_leader_skin": 0, - "leader_skin_id": 7 - }, - "8": { - "class_id": 8, - "is_random_leader_skin": 0, - "leader_skin_id": 8 - } - } - } -} \ No newline at end of file diff --git a/SVSim.Bootstrap/Data/seeds/default-decks.json b/SVSim.Bootstrap/Data/seeds/default-decks.json new file mode 100644 index 0000000..0aafcb2 --- /dev/null +++ b/SVSim.Bootstrap/Data/seeds/default-decks.json @@ -0,0 +1,394 @@ +[ + { + "id": 91, + "class_id": 1, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100111010, + 100111010, + 100111010, + 100011020, + 100011020, + 100011020, + 100012010, + 100111020, + 100111020, + 100111020, + 100111040, + 100111040, + 100111040, + 100114010, + 100114010, + 100114010, + 100011030, + 100011030, + 100011030, + 100111060, + 100111060, + 100111060, + 100011040, + 100011040, + 100011040, + 100111030, + 100111030, + 100111030, + 100111050, + 100111050, + 100111050, + 100011050, + 100011050, + 100011050, + 100111070, + 100111070, + 100111070, + 100121010, + 100121010, + 100121010 + ] + }, + { + "id": 92, + "class_id": 2, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100211010, + 100211010, + 100211010, + 100011020, + 100011020, + 100011020, + 100012010, + 100211020, + 100211020, + 100211020, + 100211060, + 100211060, + 100211060, + 100214010, + 100214010, + 100214010, + 100011030, + 100011030, + 100011030, + 100211030, + 100211030, + 100211030, + 100214020, + 100214020, + 100214020, + 100011040, + 100011040, + 100011040, + 100211040, + 100211040, + 100211040, + 100011050, + 100011050, + 100011050, + 100211050, + 100211050, + 100211050, + 100221020, + 100221020, + 100221020 + ] + }, + { + "id": 93, + "class_id": 3, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100314010, + 100314010, + 100314010, + 100011020, + 100011020, + 100011020, + 100012010, + 100311010, + 100311010, + 100311010, + 100314030, + 100314030, + 100314030, + 100314020, + 100314020, + 100314020, + 100314040, + 100314040, + 100314040, + 100011030, + 100011030, + 100011030, + 100314050, + 100314050, + 100314050, + 100011040, + 100011040, + 100011040, + 100314060, + 100314060, + 100314060, + 100011050, + 100011050, + 100011050, + 100314070, + 100314070, + 100314070, + 100321010, + 100321010, + 100321010 + ] + }, + { + "id": 94, + "class_id": 4, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100414020, + 100414020, + 100414020, + 100011020, + 100011020, + 100011020, + 100012010, + 100411010, + 100411010, + 100411010, + 100414010, + 100414010, + 100414010, + 100011030, + 100011030, + 100011030, + 100411050, + 100411050, + 100411050, + 100011040, + 100011040, + 100011040, + 100411030, + 100411030, + 100411030, + 100414030, + 100414030, + 100414030, + 100011050, + 100011050, + 100011050, + 100411020, + 100411020, + 100411020, + 100411040, + 100411040, + 100411040, + 100421020, + 100421020, + 100421020 + ] + }, + { + "id": 95, + "class_id": 5, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100011020, + 100011020, + 100011020, + 100012010, + 100511010, + 100511010, + 100511010, + 100511020, + 100511020, + 100511020, + 100514010, + 100514010, + 100514010, + 100011030, + 100011030, + 100011030, + 100511030, + 100511030, + 100511030, + 100011040, + 100011040, + 100011040, + 100511040, + 100511040, + 100511040, + 100011050, + 100011050, + 100011050, + 100511050, + 100511050, + 100511050, + 100514020, + 100514020, + 100514020, + 100511060, + 100511060, + 100511060, + 100521030, + 100521030, + 100521030 + ] + }, + { + "id": 96, + "class_id": 6, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100011020, + 100011020, + 100011020, + 100012010, + 100611010, + 100611010, + 100611010, + 100611020, + 100611020, + 100611020, + 100614010, + 100614010, + 100614010, + 100614020, + 100614020, + 100614020, + 100011030, + 100011030, + 100011030, + 100611030, + 100611030, + 100611030, + 100011040, + 100011040, + 100011040, + 100611050, + 100611050, + 100611050, + 100614030, + 100614030, + 100614030, + 100011050, + 100011050, + 100011050, + 100611040, + 100611040, + 100611040, + 100621010, + 100621010, + 100621010 + ] + }, + { + "id": 97, + "class_id": 7, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100713020, + 100713020, + 100713020, + 100011020, + 100011020, + 100011020, + 100012010, + 100713010, + 100713010, + 100713010, + 100711010, + 100711010, + 100711010, + 100714010, + 100714010, + 100714010, + 100714020, + 100714020, + 100714020, + 100011030, + 100011030, + 100011030, + 100713030, + 100713030, + 100713030, + 100011040, + 100011040, + 100011040, + 100011050, + 100011050, + 100011050, + 100723010, + 100723010, + 100723010, + 100714030, + 100714030, + 100714030, + 100711020, + 100711020, + 100711020 + ] + }, + { + "id": 98, + "class_id": 8, + "sleeve_id": 3000011, + "leader_skin_id": 0, + "deck_name": "Default", + "card_id_array": [ + 100011020, + 100011020, + 100011020, + 100012010, + 100811020, + 100811020, + 100811020, + 100811060, + 100811060, + 100811060, + 100811070, + 100811070, + 100811070, + 100814010, + 100814010, + 100814010, + 100011030, + 100011030, + 100011030, + 100811010, + 100811010, + 100811010, + 100811030, + 100811030, + 100811030, + 100011040, + 100011040, + 100011040, + 100811040, + 100811040, + 100811040, + 100824010, + 100824010, + 100824010, + 100011050, + 100011050, + 100011050, + 100811050, + 100811050, + 100811050 + ] + } +] diff --git a/SVSim.Bootstrap/Importers/DefaultDeckImporter.cs b/SVSim.Bootstrap/Importers/DefaultDeckImporter.cs new file mode 100644 index 0000000..2d9cf67 --- /dev/null +++ b/SVSim.Bootstrap/Importers/DefaultDeckImporter.cs @@ -0,0 +1,60 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using SVSim.Bootstrap.Models.Seed; +using SVSim.Database; +using SVSim.Database.Models; + +namespace SVSim.Bootstrap.Importers; + +/// +/// Idempotent upsert of default decks from seeds/default-decks.json. Warns on orphan card +/// references (card_id not in Cards table) but never fails — CardImporter must run first for a +/// clean warning-free run. Rows missing from the seed are LEFT INTACT. +/// +public class DefaultDeckImporter +{ + public async Task ImportAsync(SVSimDbContext context, string seedDir) + { + var seed = SeedLoader.LoadList(Path.Combine(seedDir, "default-decks.json")); + if (seed.Count == 0) + { + Console.WriteLine("[DefaultDeckImporter] No seed rows; skipping."); + return 0; + } + + var existing = await context.DefaultDecks.ToDictionaryAsync(e => e.Id); + var knownCards = new HashSet(await context.Cards.Select(c => c.Id).ToListAsync()); + int created = 0, updated = 0, orphans = 0; + + foreach (var s in seed) + { + if (s.Id == 0) continue; + var entry = existing.TryGetValue(s.Id, out var ex) ? ex : new DefaultDeckEntry { Id = s.Id }; + entry.ClassId = s.ClassId; + entry.SleeveId = s.SleeveId; + entry.LeaderSkinId = s.LeaderSkinId; + entry.DeckName = s.DeckName; + entry.CardIdArray = JsonSerializer.Serialize(s.CardIdArray); + + // Orphan count against card master — informational, never throws. + foreach (var cardId in s.CardIdArray) + { + if (!knownCards.Contains(cardId)) orphans++; + } + + if (ex is null) { context.DefaultDecks.Add(entry); existing[s.Id] = entry; created++; } + else updated++; + } + + await context.SaveChangesAsync(); + WarnOrphans("DefaultDecks.card_id_array", orphans); + Console.WriteLine($"[DefaultDeckImporter] +{created}/~{updated}"); + return created + updated; + } + + private static void WarnOrphans(string label, int count) + { + if (count > 0) + Console.Error.WriteLine($"[DefaultDeckImporter] Warning: {label} has {count} orphan card_id(s) — run CardImporter first for clean references."); + } +} diff --git a/SVSim.Bootstrap/Importers/GlobalsImporter.cs b/SVSim.Bootstrap/Importers/GlobalsImporter.cs index c8e74d6..f34320e 100644 --- a/SVSim.Bootstrap/Importers/GlobalsImporter.cs +++ b/SVSim.Bootstrap/Importers/GlobalsImporter.cs @@ -10,7 +10,8 @@ namespace SVSim.Bootstrap.Importers; /// /// Imports prod-captured globals from {capturesDir}/{endpoint}-*.json snapshots into the -/// DB via idempotent upserts. Source endpoints: load-index, mypage-index, deck-info. +/// DB via idempotent upserts. Source endpoints: load-index, pack-info. Per-endpoint +/// seed-file importers (DefaultDeckImporter, MyPageGlobalsImporter, etc.) cover the rest. /// /// Topological order: GameConfiguration extensions → standalone tables → card-referencing tables → /// rotation CardSet flag update. Card-referencing importers warn on orphans (missing card rows) @@ -27,7 +28,6 @@ public class GlobalsImporter Console.WriteLine($"[GlobalsImporter] Loading captures from {capturesDir}..."); JsonElement? loadIndex = LoadCapture(capturesDir, "load-index"); - JsonElement? deckInfo = LoadCapture(capturesDir, "deck-info"); JsonElement? packInfo = LoadCapture(capturesDir, "pack-info"); int total = 0; @@ -50,11 +50,6 @@ public class GlobalsImporter total += await UpdateRotationCardSetFlags(context, loadIndex.Value); } - if (deckInfo.HasValue) - { - total += await ImportDefaultDecks(context, deckInfo.Value); - } - if (packInfo.HasValue) { total += await ImportPacks(context, packInfo.Value); @@ -511,45 +506,6 @@ public class GlobalsImporter return updated; } - // ---------- Deck/info: Default Decks ---------- - - private async Task ImportDefaultDecks(SVSimDbContext context, JsonElement deckInfo) - { - if (!deckInfo.TryGetProperty("default_deck_list", out var info) || info.ValueKind != JsonValueKind.Object) return 0; - - var existing = await context.DefaultDecks.ToDictionaryAsync(e => e.Id); - var knownSet = new HashSet(await context.Cards.Select(c => c.Id).ToListAsync()); - int created = 0, updated = 0, orphans = 0; - - foreach (var kv in info.EnumerateObject()) - { - if (!int.TryParse(kv.Name, out int deckNo)) continue; - var v = kv.Value; - var entry = existing.TryGetValue(deckNo, out var ex) ? ex : new DefaultDeckEntry { Id = deckNo }; - entry.ClassId = GetInt(v, "class_id"); - entry.SleeveId = GetLong(v, "sleeve_id"); - entry.LeaderSkinId = GetInt(v, "leader_skin_id"); - entry.DeckName = GetString(v, "deck_name"); - entry.CardIdArray = v.TryGetProperty("card_id_array", out var arr) ? Serialize(arr) : "[]"; - - // Count orphans against card master - if (arr.ValueKind == JsonValueKind.Array) - { - foreach (var c in arr.EnumerateArray()) - { - if (c.ValueKind != JsonValueKind.Number) continue; - if (!knownSet.Contains(c.GetInt64())) orphans++; - } - } - - if (ex is null) { context.DefaultDecks.Add(entry); created++; } - else updated++; - } - WarnOrphans("DefaultDecks.card_id_array", orphans); - Console.WriteLine($"[GlobalsImporter] DefaultDecks: +{created}/~{updated}"); - return created + updated; - } - // ---------- Pack catalog ---------- /// diff --git a/SVSim.Bootstrap/Models/Seed/DefaultDeckSeed.cs b/SVSim.Bootstrap/Models/Seed/DefaultDeckSeed.cs new file mode 100644 index 0000000..3022dda --- /dev/null +++ b/SVSim.Bootstrap/Models/Seed/DefaultDeckSeed.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SVSim.Bootstrap.Models.Seed; + +public sealed class DefaultDeckSeed +{ + [JsonPropertyName("id")] public int Id { get; set; } + [JsonPropertyName("class_id")] public int ClassId { get; set; } + [JsonPropertyName("sleeve_id")] public long SleeveId { get; set; } + [JsonPropertyName("leader_skin_id")] public int LeaderSkinId { get; set; } + [JsonPropertyName("deck_name")] public string DeckName { get; set; } = ""; + [JsonPropertyName("card_id_array")] public List CardIdArray { get; set; } = new(); +} diff --git a/SVSim.Bootstrap/Program.cs b/SVSim.Bootstrap/Program.cs index a307ef8..6c43607 100644 --- a/SVSim.Bootstrap/Program.cs +++ b/SVSim.Bootstrap/Program.cs @@ -90,6 +90,8 @@ public static class Program await mypage.ImportMasterPointRankingPeriodAsync(context, opts.SeedDir); await mypage.ImportSpecialDeckFormatsAsync(context, opts.SeedDir); + await new DefaultDeckImporter().ImportAsync(context, opts.SeedDir); + // BuildDeck pipeline: series CSV → catalog JSON → package CSV. Catalog must run after // series CSV (FK on products → series) and before package CSV (so the catalog-side // enriched rows take precedence over stub creation). diff --git a/SVSim.UnitTests/Importers/DefaultDeckImporterTests.cs b/SVSim.UnitTests/Importers/DefaultDeckImporterTests.cs new file mode 100644 index 0000000..554f117 --- /dev/null +++ b/SVSim.UnitTests/Importers/DefaultDeckImporterTests.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Bootstrap.Importers; +using SVSim.Database; +using SVSim.Database.Models; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Importers; + +public class DefaultDeckImporterTests +{ + private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds"); + + [Test] + public async Task Imports_default_decks_from_seed_file() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new DefaultDeckImporter().ImportAsync(db, SeedDir); + + var decks = await db.DefaultDecks.OrderBy(d => d.Id).ToListAsync(); + Assert.That(decks.Count, Is.GreaterThan(0), "seed file must contain default decks"); + Assert.That(decks.All(d => d.ClassId > 0), Is.True); + Assert.That(decks.All(d => !string.IsNullOrEmpty(d.DeckName)), Is.True); + // CardIdArray is a JSON array column; every row must serialize as such. + Assert.That(decks.All(d => d.CardIdArray.StartsWith("[")), Is.True); + } + + [Test] + public async Task Is_idempotent_on_rerun() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new DefaultDeckImporter().ImportAsync(db, SeedDir); + int before = await db.DefaultDecks.CountAsync(); + await new DefaultDeckImporter().ImportAsync(db, SeedDir); + int after = await db.DefaultDecks.CountAsync(); + + Assert.That(after, Is.EqualTo(before)); + } + + [Test] + public async Task Leaves_existing_rows_untouched_when_missing_from_seed() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + const int legacyId = 99999; + db.DefaultDecks.Add(new DefaultDeckEntry + { + Id = legacyId, + ClassId = 1, + SleeveId = 0, + LeaderSkinId = 0, + DeckName = "legacy", + CardIdArray = "[]", + }); + await db.SaveChangesAsync(); + + await new DefaultDeckImporter().ImportAsync(db, SeedDir); + + var legacy = await db.DefaultDecks.FindAsync(legacyId); + Assert.That(legacy, Is.Not.Null, "seed-missing row must be left intact"); + Assert.That(legacy!.DeckName, Is.EqualTo("legacy")); + } + + [Test] + public async Task Skips_rows_with_zero_id() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + string tmp = Path.Combine(Path.GetTempPath(), $"seed-{Guid.NewGuid()}"); + Directory.CreateDirectory(tmp); + try + { + File.WriteAllText(Path.Combine(tmp, "default-decks.json"), + "[{\"id\":0,\"class_id\":1,\"sleeve_id\":0,\"leader_skin_id\":0,\"deck_name\":\"junk\",\"card_id_array\":[1,2,3]}]"); + + await new DefaultDeckImporter().ImportAsync(db, tmp); + + int count = await db.DefaultDecks.CountAsync(); + Assert.That(count, Is.EqualTo(0), "rows with id=0 must not be inserted"); + } + finally { Directory.Delete(tmp, true); } + } + + [Test] + public async Task Warns_on_orphan_card_ids_but_does_not_fail() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // The test factory's minimal seed contains only cards 10001001/10001002/10001003. + // Reference a card id well outside that set so the orphan-count branch fires. + string tmp = Path.Combine(Path.GetTempPath(), $"seed-{Guid.NewGuid()}"); + Directory.CreateDirectory(tmp); + try + { + File.WriteAllText(Path.Combine(tmp, "default-decks.json"), + "[{\"id\":1234,\"class_id\":1,\"sleeve_id\":0,\"leader_skin_id\":0,\"deck_name\":\"orphans\",\"card_id_array\":[999999999,888888888]}]"); + + Assert.DoesNotThrowAsync(async () => + await new DefaultDeckImporter().ImportAsync(db, tmp), + "orphan card_ids must warn, never throw"); + + var row = await db.DefaultDecks.FindAsync(1234); + Assert.That(row, Is.Not.Null, "deck must be inserted even with orphan card refs"); + Assert.That(row!.DeckName, Is.EqualTo("orphans")); + } + finally { Directory.Delete(tmp, true); } + } +} diff --git a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs index 18cc2e2..e4ac5ca 100644 --- a/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs +++ b/SVSim.UnitTests/Infrastructure/SVSimTestFactory.cs @@ -205,6 +205,8 @@ internal sealed class SVSimTestFactory : WebApplicationFactory await mypage.ImportSealedAsync(ctx, seedDir); await mypage.ImportMasterPointRankingPeriodAsync(ctx, seedDir); await mypage.ImportSpecialDeckFormatsAsync(ctx, seedDir); + + await new DefaultDeckImporter().ImportAsync(ctx, seedDir); } /// Convenience: bake the X-Test-Viewer-Id header into a fresh client.