feat(items): catalog import with Type + ThumbnailPath columns
ItemEntry gains Type (client item_type enum, 1=challenge, 2=card-pack ticket, 3=premium orb, 4=colosseum, 5=orb piece, 6=skin/event ticket, 7=other) and ThumbnailPath. ItemImporter mirrors PaymentItemImporter shape: find-or-create per item_id, save once, idempotent. Wired into Bootstrap.Program and SVSimTestFactory.SeedGlobalsAsync. Unblocks /item_purchase/info (filters card-pack tickets by Type==2) and any reward grant of UserGoodsType.Item, which previously threw because the catalog was empty. 466 tests pass (was 461). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
362
SVSim.Bootstrap/Data/seeds/items.json
Normal file
362
SVSim.Bootstrap/Data/seeds/items.json
Normal file
@@ -0,0 +1,362 @@
|
||||
[
|
||||
{
|
||||
"item_id": 1,
|
||||
"name": "Challenge Ticket",
|
||||
"type": 1,
|
||||
"thumbnail_path": "ticket_1"
|
||||
},
|
||||
{
|
||||
"item_id": 2,
|
||||
"name": "Grand Prix Ticket",
|
||||
"type": 4,
|
||||
"thumbnail_path": "ticket_colosseum"
|
||||
},
|
||||
{
|
||||
"item_id": 1000,
|
||||
"name": "Seer's Globe",
|
||||
"type": 3,
|
||||
"thumbnail_path": "thumbnail_orb"
|
||||
},
|
||||
{
|
||||
"item_id": 1001,
|
||||
"name": "Seer's Globe Shards",
|
||||
"type": 5,
|
||||
"thumbnail_path": "thumbnail_orb_piece"
|
||||
},
|
||||
{
|
||||
"item_id": 2001,
|
||||
"name": "Umamusume Bingo Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_2001"
|
||||
},
|
||||
{
|
||||
"item_id": 2002,
|
||||
"name": "Chiikawa Bingo Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_2002"
|
||||
},
|
||||
{
|
||||
"item_id": 2003,
|
||||
"name": "7th Anniversary Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_2003"
|
||||
},
|
||||
{
|
||||
"item_id": 2004,
|
||||
"name": "Fennie Bingo Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_2004"
|
||||
},
|
||||
{
|
||||
"item_id": 10001,
|
||||
"name": "Classic Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10001"
|
||||
},
|
||||
{
|
||||
"item_id": 10002,
|
||||
"name": "Darkness Evolved Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10002"
|
||||
},
|
||||
{
|
||||
"item_id": 10003,
|
||||
"name": "Rise of Bahamut Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10003"
|
||||
},
|
||||
{
|
||||
"item_id": 10004,
|
||||
"name": "Tempest of the Gods Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10004"
|
||||
},
|
||||
{
|
||||
"item_id": 10005,
|
||||
"name": "Wonderland Dreams Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10005"
|
||||
},
|
||||
{
|
||||
"item_id": 10006,
|
||||
"name": "Starforged Legends Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10006"
|
||||
},
|
||||
{
|
||||
"item_id": 10007,
|
||||
"name": "Chronogenesis Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10007"
|
||||
},
|
||||
{
|
||||
"item_id": 10008,
|
||||
"name": "Dawnbreak, Nightedge Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10008"
|
||||
},
|
||||
{
|
||||
"item_id": 10009,
|
||||
"name": "Brigade of the Sky Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10009"
|
||||
},
|
||||
{
|
||||
"item_id": 10010,
|
||||
"name": "Omen of the Ten Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10010"
|
||||
},
|
||||
{
|
||||
"item_id": 10011,
|
||||
"name": "Altersphere Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10011"
|
||||
},
|
||||
{
|
||||
"item_id": 10012,
|
||||
"name": "Steel Rebellion Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10012"
|
||||
},
|
||||
{
|
||||
"item_id": 10013,
|
||||
"name": "Rebirth of Glory Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10013"
|
||||
},
|
||||
{
|
||||
"item_id": 10014,
|
||||
"name": "Verdant Conflict Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10014"
|
||||
},
|
||||
{
|
||||
"item_id": 10015,
|
||||
"name": "Ultimate Colosseum Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10015"
|
||||
},
|
||||
{
|
||||
"item_id": 10016,
|
||||
"name": "World Uprooted Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10016"
|
||||
},
|
||||
{
|
||||
"item_id": 10017,
|
||||
"name": "Fortune's Hand Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10017"
|
||||
},
|
||||
{
|
||||
"item_id": 10018,
|
||||
"name": "Storm Over Rivayle Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10018"
|
||||
},
|
||||
{
|
||||
"item_id": 10019,
|
||||
"name": "Eternal Awakening Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10019"
|
||||
},
|
||||
{
|
||||
"item_id": 10020,
|
||||
"name": "Darkness Over Vellsar Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10020"
|
||||
},
|
||||
{
|
||||
"item_id": 10021,
|
||||
"name": "Renascent Chronicles Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10021"
|
||||
},
|
||||
{
|
||||
"item_id": 10022,
|
||||
"name": "Dawn of Calamity Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10022"
|
||||
},
|
||||
{
|
||||
"item_id": 10023,
|
||||
"name": "Omen of Storms Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10023"
|
||||
},
|
||||
{
|
||||
"item_id": 10024,
|
||||
"name": "Edge of Paradise Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10024"
|
||||
},
|
||||
{
|
||||
"item_id": 10025,
|
||||
"name": "Roar of the Godwyrm Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10025"
|
||||
},
|
||||
{
|
||||
"item_id": 10026,
|
||||
"name": "Celestial Dragonblade Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10026"
|
||||
},
|
||||
{
|
||||
"item_id": 10027,
|
||||
"name": "Eightfold Abyss: Azvaldt Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10027"
|
||||
},
|
||||
{
|
||||
"item_id": 10028,
|
||||
"name": "Academy of Ages Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10028"
|
||||
},
|
||||
{
|
||||
"item_id": 10029,
|
||||
"name": "Heroes of Rivenbrandt Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10029"
|
||||
},
|
||||
{
|
||||
"item_id": 10030,
|
||||
"name": "Order Shift Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10030"
|
||||
},
|
||||
{
|
||||
"item_id": 10031,
|
||||
"name": "Resurgent Legends Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10031"
|
||||
},
|
||||
{
|
||||
"item_id": 10032,
|
||||
"name": "Heroes of Shadowverse Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_10032"
|
||||
},
|
||||
{
|
||||
"item_id": 60001,
|
||||
"name": "4th Birthday Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60001"
|
||||
},
|
||||
{
|
||||
"item_id": 60019,
|
||||
"name": "Eternal Awakening Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60019"
|
||||
},
|
||||
{
|
||||
"item_id": 60020,
|
||||
"name": "Darkness Over Vellsar Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60020"
|
||||
},
|
||||
{
|
||||
"item_id": 60021,
|
||||
"name": "Renascent Chronicles Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60021"
|
||||
},
|
||||
{
|
||||
"item_id": 60022,
|
||||
"name": "Dawn of Calamity Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60022"
|
||||
},
|
||||
{
|
||||
"item_id": 60023,
|
||||
"name": "Omen of Storms Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60023"
|
||||
},
|
||||
{
|
||||
"item_id": 60024,
|
||||
"name": "Edge of Paradise Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60024"
|
||||
},
|
||||
{
|
||||
"item_id": 60025,
|
||||
"name": "Roar of the Godwyrm Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60025"
|
||||
},
|
||||
{
|
||||
"item_id": 60026,
|
||||
"name": "Celestial Dragonblade Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60026"
|
||||
},
|
||||
{
|
||||
"item_id": 60027,
|
||||
"name": "Eightfold Abyss: Azvaldt Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60027"
|
||||
},
|
||||
{
|
||||
"item_id": 60028,
|
||||
"name": "Academy of Ages Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60028"
|
||||
},
|
||||
{
|
||||
"item_id": 60029,
|
||||
"name": "Heroes of Rivenbrandt Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60029"
|
||||
},
|
||||
{
|
||||
"item_id": 60030,
|
||||
"name": "Order Shift Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60030"
|
||||
},
|
||||
{
|
||||
"item_id": 60031,
|
||||
"name": "Resurgent Legends Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60031"
|
||||
},
|
||||
{
|
||||
"item_id": 60032,
|
||||
"name": "Heroes of Shadowverse Temporary Deck Ticket",
|
||||
"type": 6,
|
||||
"thumbnail_path": "ticket_60032"
|
||||
},
|
||||
{
|
||||
"item_id": 70001,
|
||||
"name": "4th Birthday Leader Ticket",
|
||||
"type": 7,
|
||||
"thumbnail_path": "ticket_70001"
|
||||
},
|
||||
{
|
||||
"item_id": 70002,
|
||||
"name": "Champion's Battle Leader Ticket",
|
||||
"type": 7,
|
||||
"thumbnail_path": "ticket_70002"
|
||||
},
|
||||
{
|
||||
"item_id": 80001,
|
||||
"name": "Throwback Rotation Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_80001"
|
||||
},
|
||||
{
|
||||
"item_id": 90001,
|
||||
"name": "Legendary Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_90001"
|
||||
},
|
||||
{
|
||||
"item_id": 92001,
|
||||
"name": "4th Birthday Card Pack Ticket",
|
||||
"type": 2,
|
||||
"thumbnail_path": "ticket_92001"
|
||||
}
|
||||
]
|
||||
52
SVSim.Bootstrap/Importers/ItemImporter.cs
Normal file
52
SVSim.Bootstrap/Importers/ItemImporter.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Bootstrap.Models.Seed;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.Bootstrap.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent upsert of the item catalog from <c>seeds/items.json</c>. Source is the client's
|
||||
/// <c>item_master.csv</c> + <c>itemtext.json</c> (extracted via
|
||||
/// <c>data_dumps/extract/extract-items.py</c>). Rows missing from the seed are LEFT INTACT.
|
||||
/// </summary>
|
||||
public class ItemImporter
|
||||
{
|
||||
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
string path = Path.Combine(seedDir, "items.json");
|
||||
var seed = SeedLoader.LoadList<ItemSeed>(path);
|
||||
if (seed.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[ItemImporter] No seed rows; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existing = await context.Items.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0, updated = 0;
|
||||
|
||||
foreach (var s in seed)
|
||||
{
|
||||
if (s.ItemId == 0) continue;
|
||||
|
||||
var entry = existing.TryGetValue(s.ItemId, out var ex)
|
||||
? ex : new ItemEntry { Id = s.ItemId };
|
||||
|
||||
entry.Name = s.Name;
|
||||
entry.Type = s.Type;
|
||||
entry.ThumbnailPath = s.ThumbnailPath;
|
||||
|
||||
if (ex is null)
|
||||
{
|
||||
context.Items.Add(entry);
|
||||
existing[s.ItemId] = entry;
|
||||
created++;
|
||||
}
|
||||
else updated++;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[ItemImporter] +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
}
|
||||
11
SVSim.Bootstrap/Models/Seed/ItemSeed.cs
Normal file
11
SVSim.Bootstrap/Models/Seed/ItemSeed.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.Bootstrap.Models.Seed;
|
||||
|
||||
public sealed class ItemSeed
|
||||
{
|
||||
[JsonPropertyName("item_id")] public int ItemId { get; set; }
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
[JsonPropertyName("type")] public int Type { get; set; }
|
||||
[JsonPropertyName("thumbnail_path")] public string ThumbnailPath { get; set; } = "";
|
||||
}
|
||||
@@ -97,6 +97,7 @@ public static class Program
|
||||
|
||||
await new PracticeOpponentImporter().ImportAsync(context, opts.SeedDir);
|
||||
await new PaymentItemImporter().ImportAsync(context, opts.SeedDir);
|
||||
await new ItemImporter().ImportAsync(context, opts.SeedDir);
|
||||
var puzzleImporter = new PuzzleImporter();
|
||||
await puzzleImporter.ImportGroupsAsync(context, opts.SeedDir);
|
||||
await puzzleImporter.ImportPuzzlesAsync(context, opts.SeedDir);
|
||||
|
||||
3278
SVSim.Database/Migrations/20260528013825_AddItemTypeAndThumbnail.Designer.cs
generated
Normal file
3278
SVSim.Database/Migrations/20260528013825_AddItemTypeAndThumbnail.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddItemTypeAndThumbnail : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
table: "Items",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Type",
|
||||
table: "Items",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ThumbnailPath",
|
||||
table: "Items");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Type",
|
||||
table: "Items");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1095,6 +1095,13 @@ namespace SVSim.Database.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Items");
|
||||
|
||||
@@ -2,7 +2,21 @@ using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Item master row. Mirrors the client's <c>item_master.csv</c> + <c>itemtext.json</c>
|
||||
/// (under <c>data_dumps/client_master_csv/</c>): <see cref="Type"/> matches the client-side
|
||||
/// item_type enum (1 = challenge ticket, 2 = card-pack ticket, 3 = premium orb,
|
||||
/// 4 = colosseum ticket, 5 = orb piece, 6 = skin/event ticket, 7 = other);
|
||||
/// <see cref="ThumbnailPath"/> is the client-resolved sprite key.
|
||||
/// </summary>
|
||||
public class ItemEntry : BaseEntity<int>
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Client-side item_type enum (1-7). Drives shop categorisation, e.g.
|
||||
/// <c>user_card_pack_ticket_list</c> in /item_purchase/info filters on Type == 2.</summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>Sprite key, e.g. <c>"ticket_10032"</c>. Empty when unknown.</summary>
|
||||
public string ThumbnailPath { get; set; } = string.Empty;
|
||||
}
|
||||
92
SVSim.UnitTests/Importers/ItemImporterTests.cs
Normal file
92
SVSim.UnitTests/Importers/ItemImporterTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Importers;
|
||||
|
||||
public class ItemImporterTests
|
||||
{
|
||||
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
|
||||
|
||||
[Test]
|
||||
public async Task Imports_items_from_seed_file()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
await new ItemImporter().ImportAsync(db, SeedDir);
|
||||
|
||||
var items = await db.Items.OrderBy(i => i.Id).ToListAsync();
|
||||
Assert.That(items.Count, Is.GreaterThan(0), "seed file must contain items");
|
||||
// Spot-check the card-pack-ticket cluster: Type==2, thumbnail follows ticket_<id> convention.
|
||||
var pack = items.FirstOrDefault(i => i.Id == 10032);
|
||||
Assert.That(pack, Is.Not.Null, "card-pack ticket 10032 (latest expansion) should be seeded");
|
||||
Assert.That(pack!.Type, Is.EqualTo(2));
|
||||
Assert.That(pack.ThumbnailPath, Is.EqualTo("ticket_10032"));
|
||||
Assert.That(pack.Name, Is.Not.Empty, "name should resolve via itemtext");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Is_idempotent_on_rerun()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
await new ItemImporter().ImportAsync(db, SeedDir);
|
||||
int before = await db.Items.CountAsync();
|
||||
await new ItemImporter().ImportAsync(db, SeedDir);
|
||||
int after = await db.Items.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<SVSimDbContext>();
|
||||
|
||||
const int legacyId = 99999;
|
||||
db.Items.Add(new SVSim.Database.Models.ItemEntry
|
||||
{
|
||||
Id = legacyId,
|
||||
Name = "legacy",
|
||||
Type = 0,
|
||||
ThumbnailPath = "",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await new ItemImporter().ImportAsync(db, SeedDir);
|
||||
|
||||
var legacy = await db.Items.FindAsync(legacyId);
|
||||
Assert.That(legacy, Is.Not.Null, "seed-missing row must be left intact");
|
||||
Assert.That(legacy!.Name, Is.EqualTo("legacy"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Skips_rows_with_zero_item_id()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
string tmp = Path.Combine(Path.GetTempPath(), $"seed-{Guid.NewGuid()}");
|
||||
Directory.CreateDirectory(tmp);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(tmp, "items.json"),
|
||||
"[{\"item_id\":0,\"name\":\"junk\",\"type\":1,\"thumbnail_path\":\"\"}]");
|
||||
|
||||
await new ItemImporter().ImportAsync(db, tmp);
|
||||
|
||||
int count = await db.Items.CountAsync();
|
||||
Assert.That(count, Is.EqualTo(0), "rows with item_id=0 must not be inserted");
|
||||
}
|
||||
finally { Directory.Delete(tmp, true); }
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,7 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
|
||||
|
||||
await new PracticeOpponentImporter().ImportAsync(ctx, seedDir);
|
||||
await new PaymentItemImporter().ImportAsync(ctx, seedDir);
|
||||
await new ItemImporter().ImportAsync(ctx, seedDir);
|
||||
var puzzleImporter = new PuzzleImporter();
|
||||
await puzzleImporter.ImportGroupsAsync(ctx, seedDir);
|
||||
await puzzleImporter.ImportPuzzlesAsync(ctx, seedDir);
|
||||
|
||||
Reference in New Issue
Block a user