refactor(bootstrap): migrate /pack/info to seed file

This commit is contained in:
gamer147
2026-05-26 15:02:49 -04:00
parent 83298a2d47
commit a71bf6c62b
12 changed files with 3287 additions and 192 deletions

View File

@@ -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}.");
}
}

View 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");
}
}

View File

@@ -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>

View File

@@ -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