refactor(bootstrap): migrate /pack/info to seed file
This commit is contained in:
@@ -1,37 +1,43 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Drives the importer + controller against the real prod capture (35 packs). Guards against
|
||||
/// regressions in either layer caused by future capture refreshes.
|
||||
/// Drives the importer + controller against the full production pack seed (35 packs). Guards
|
||||
/// against regressions in either layer caused by future seed refreshes.
|
||||
/// </summary>
|
||||
public class PackControllerProdCaptureTests
|
||||
{
|
||||
[Test]
|
||||
public async Task Info_returns_full_35_pack_catalog_from_prod_capture()
|
||||
{
|
||||
// The default captures dir has both pack-info-fixture.json (3 packs) and
|
||||
// pack-info-2026-05-23.json (35 packs). LoadCapture sorts by name descending and
|
||||
// "pack-info-fixture.json" > "pack-info-2026-05-23.json" lexicographically, so the
|
||||
// fixture would win. Copy captures to a temp dir, drop the fixture, then seed from there.
|
||||
var sourceDir = Path.Combine(AppContext.BaseDirectory, "Data", "prod-captures");
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "svsim-pack-prod-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
// The production seed (packs.json) is overlaid by a 3-pack test fixture in the default test
|
||||
// output dir (see SVSim.UnitTests.csproj). For this test we need the FULL 35-pack catalog,
|
||||
// so we point PackImporter at a temp seed dir holding only the upstream production seed
|
||||
// (copied from the Bootstrap project's source-tree Data/seeds/).
|
||||
var prodSeed = LocateProdSeed("packs.json");
|
||||
var tempSeedDir = Path.Combine(Path.GetTempPath(), "svsim-pack-prod-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempSeedDir);
|
||||
try
|
||||
{
|
||||
foreach (var src in Directory.EnumerateFiles(sourceDir))
|
||||
{
|
||||
if (Path.GetFileName(src).Equals("pack-info-fixture.json", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
File.Copy(src, Path.Combine(tempDir, Path.GetFileName(src)));
|
||||
}
|
||||
File.Copy(prodSeed, Path.Combine(tempSeedDir, "packs.json"));
|
||||
|
||||
using var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync(tempDir); // imports the 35-pack pack-info-2026-05-23.json
|
||||
// Run the default seed pipeline first so GlobalsImporter populates surrounding tables,
|
||||
// then re-run PackImporter against the prod seed to overwrite the fixture-loaded packs.
|
||||
await factory.SeedGlobalsAsync();
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
await new PackImporter().ImportAsync(ctx, tempSeedDir);
|
||||
}
|
||||
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
@@ -46,7 +52,7 @@ public class PackControllerProdCaptureTests
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var list = doc.RootElement.GetProperty("pack_config_list");
|
||||
Assert.That(list.GetArrayLength(), Is.EqualTo(35),
|
||||
"Full prod capture should yield 35 active packs as of 2026-05-23.");
|
||||
"Full prod seed should yield 35 active packs as of 2026-05-23.");
|
||||
|
||||
// Spot-check pack 99047 (LegendCardPack throwback, pack_category=1)
|
||||
bool sawSpecial = false;
|
||||
@@ -65,7 +71,25 @@ public class PackControllerProdCaptureTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(tempDir, recursive: true); } catch { /* best-effort cleanup */ }
|
||||
try { Directory.Delete(tempSeedDir, recursive: true); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The test output dir's <c>Data/seeds/packs.json</c> is the fixture overlay (3 packs). The
|
||||
/// upstream production seed lives in the Bootstrap project's source tree. Walk up from the
|
||||
/// test binary dir to the repo root and locate it there.
|
||||
/// </summary>
|
||||
private static string LocateProdSeed(string fileName)
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null)
|
||||
{
|
||||
var candidate = Path.Combine(dir.FullName, "SVSim.Bootstrap", "Data", "seeds", fileName);
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
throw new FileNotFoundException(
|
||||
$"Could not locate SVSim.Bootstrap/Data/seeds/{fileName} above {AppContext.BaseDirectory}.");
|
||||
}
|
||||
}
|
||||
|
||||
112
SVSim.UnitTests/Importers/PackImporterTests.cs
Normal file
112
SVSim.UnitTests/Importers/PackImporterTests.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Importers;
|
||||
|
||||
public class PackImporterTests
|
||||
{
|
||||
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
|
||||
|
||||
[Test]
|
||||
public async Task Imports_packs_from_seed_file()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
await new PackImporter().ImportAsync(db, SeedDir);
|
||||
|
||||
var packs = await db.Packs.OrderBy(p => p.Id).ToListAsync();
|
||||
Assert.That(packs.Count, Is.GreaterThan(0), "seed file must contain packs");
|
||||
Assert.That(packs.All(p => p.Id > 0), 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<SVSimDbContext>();
|
||||
|
||||
await new PackImporter().ImportAsync(db, SeedDir);
|
||||
int before = await db.Packs.CountAsync();
|
||||
await new PackImporter().ImportAsync(db, SeedDir);
|
||||
int after = await db.Packs.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.Packs.Add(new PackConfigEntry
|
||||
{
|
||||
Id = legacyId,
|
||||
BasePackId = legacyId,
|
||||
GachaType = 1,
|
||||
PackCategory = PackCategory.None,
|
||||
GachaDetail = "legacy",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await new PackImporter().ImportAsync(db, SeedDir);
|
||||
|
||||
var legacy = await db.Packs.FindAsync(legacyId);
|
||||
Assert.That(legacy, Is.Not.Null, "seed-missing row must be left intact");
|
||||
Assert.That(legacy!.GachaDetail, Is.EqualTo("legacy"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Skips_rows_with_zero_parent_gacha_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, "packs.json"),
|
||||
"[{\"parent_gacha_id\":0,\"base_pack_id\":1,\"gacha_type\":1,\"pack_category\":0,\"child_gachas\":[],\"banners\":[]}]");
|
||||
|
||||
await new PackImporter().ImportAsync(db, tmp);
|
||||
|
||||
int count = await db.Packs.CountAsync();
|
||||
Assert.That(count, Is.EqualTo(0), "rows with parent_gacha_id=0 must not be inserted");
|
||||
}
|
||||
finally { Directory.Delete(tmp, true); }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Owned_collections_are_replaced_wholesale_on_rerun()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
await new PackImporter().ImportAsync(db, SeedDir);
|
||||
var pack1 = await db.Packs.AsNoTracking().FirstAsync(p => p.Id == 10001);
|
||||
int childCountBefore = pack1.ChildGachas.Count;
|
||||
int bannerCountBefore = pack1.Banners.Count;
|
||||
|
||||
// Re-run: owned collections must NOT stack. Same fixture content -> same counts.
|
||||
await new PackImporter().ImportAsync(db, SeedDir);
|
||||
|
||||
var pack2 = await db.Packs.AsNoTracking().FirstAsync(p => p.Id == 10001);
|
||||
Assert.That(pack2.ChildGachas.Count, Is.EqualTo(childCountBefore),
|
||||
"child_gachas must be replaced wholesale on rerun, not stacked");
|
||||
Assert.That(pack2.Banners.Count, Is.EqualTo(bannerCountBefore),
|
||||
"banners must be replaced wholesale on rerun, not stacked");
|
||||
}
|
||||
}
|
||||
@@ -207,6 +207,7 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
|
||||
await mypage.ImportSpecialDeckFormatsAsync(ctx, seedDir);
|
||||
|
||||
await new DefaultDeckImporter().ImportAsync(ctx, seedDir);
|
||||
await new PackImporter().ImportAsync(ctx, seedDir);
|
||||
}
|
||||
|
||||
/// <summary>Convenience: bake the X-Test-Viewer-Id header into a fresh client.</summary>
|
||||
|
||||
@@ -47,6 +47,13 @@
|
||||
<Content Include="..\SVSim.Bootstrap\Data\seeds\**\*.json" Link="Data\seeds\%(RecursiveDir)%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- Test-only fixture seeds overlay the production seeds. Placed AFTER the production glob
|
||||
so MSBuild's "last Link wins" rule routes the fixture file into the same test output
|
||||
path as the production seed of the same name. Use for per-table fixtures (e.g. packs)
|
||||
where the production seed is too large to drive focused integration tests. -->
|
||||
<Content Include="..\SVSim.Bootstrap\Data\test-fixtures\seeds\*.json" Link="Data\seeds\%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- Test-only fixtures live outside prod-captures so the production bootstrap glob doesn't
|
||||
pick them up (a fixture-named file would win the importer's reverse-alphabetical sort
|
||||
against a dated capture). Linked into the same test output dir so SeedGlobalsAsync sees
|
||||
|
||||
Reference in New Issue
Block a user