refactor(bootstrap): migrate basic puzzles to seed files
Replaces GlobalsImporter's ImportPuzzleGroups/Puzzles/Missions methods (plus the DeriveTargetPuzzleGroupId regex helper) with a dedicated PuzzleImporter that reads three flat seed JSONs (puzzle-groups, puzzles, puzzle-missions) produced by the Python extractor. Groups run before puzzles to satisfy the FK; missions upsert by sequential id. Wired into Program.cs and SVSimTestFactory after PaymentItemImporter so existing GlobalsImporterPuzzleTests continue to pass unchanged via SeedGlobalsAsync. The original prod-capture JSONs are deleted now that the seeds are authoritative. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
154
SVSim.UnitTests/Importers/PuzzleImporterTests.cs
Normal file
154
SVSim.UnitTests/Importers/PuzzleImporterTests.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
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 PuzzleImporterTests
|
||||
{
|
||||
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
|
||||
|
||||
[Test]
|
||||
public async Task ImportsGroups_PuzzlesAndMissions_from_seed_files()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
var importer = new PuzzleImporter();
|
||||
await importer.ImportGroupsAsync(db, SeedDir);
|
||||
await importer.ImportPuzzlesAsync(db, SeedDir);
|
||||
await importer.ImportMissionsAsync(db, SeedDir);
|
||||
|
||||
int groupCount = await db.PuzzleGroups.CountAsync();
|
||||
int puzzleCount = await db.Puzzles.CountAsync();
|
||||
int missionCount = await db.PuzzleMissions.CountAsync();
|
||||
|
||||
Assert.That(groupCount, Is.GreaterThan(0), "seed must contain groups");
|
||||
Assert.That(puzzleCount, Is.GreaterThan(0), "seed must contain puzzles");
|
||||
Assert.That(missionCount, Is.GreaterThan(0), "seed must contain missions");
|
||||
|
||||
// Every puzzle's GroupId must reference an existing group (FK satisfied).
|
||||
var groupIds = await db.PuzzleGroups.Select(g => g.Id).ToListAsync();
|
||||
var groupIdSet = new HashSet<int>(groupIds);
|
||||
var puzzleGroupIds = await db.Puzzles.Select(p => p.GroupId).Distinct().ToListAsync();
|
||||
foreach (var gid in puzzleGroupIds)
|
||||
{
|
||||
Assert.That(groupIdSet, Does.Contain(gid),
|
||||
$"puzzle references unknown group_id={gid}");
|
||||
}
|
||||
}
|
||||
|
||||
[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>();
|
||||
|
||||
var importer = new PuzzleImporter();
|
||||
await importer.ImportGroupsAsync(db, SeedDir);
|
||||
await importer.ImportPuzzlesAsync(db, SeedDir);
|
||||
await importer.ImportMissionsAsync(db, SeedDir);
|
||||
|
||||
int g1 = await db.PuzzleGroups.CountAsync();
|
||||
int p1 = await db.Puzzles.CountAsync();
|
||||
int m1 = await db.PuzzleMissions.CountAsync();
|
||||
|
||||
await importer.ImportGroupsAsync(db, SeedDir);
|
||||
await importer.ImportPuzzlesAsync(db, SeedDir);
|
||||
await importer.ImportMissionsAsync(db, SeedDir);
|
||||
|
||||
Assert.That(await db.PuzzleGroups.CountAsync(), Is.EqualTo(g1));
|
||||
Assert.That(await db.Puzzles.CountAsync(), Is.EqualTo(p1));
|
||||
Assert.That(await db.PuzzleMissions.CountAsync(), Is.EqualTo(m1));
|
||||
}
|
||||
|
||||
[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 legacyGroupId = 99999;
|
||||
const int legacyPuzzleId = 99998;
|
||||
const int legacyMissionId = 99997;
|
||||
|
||||
db.PuzzleGroups.Add(new PuzzleGroupEntry
|
||||
{
|
||||
Id = legacyGroupId,
|
||||
BasicTitleTextId = "legacy_group",
|
||||
DifficultyNameListJson = "{\"legacy\":\"1\"}",
|
||||
});
|
||||
db.Puzzles.Add(new PuzzleEntry
|
||||
{
|
||||
Id = legacyPuzzleId,
|
||||
GroupId = legacyGroupId,
|
||||
PuzzleDifficulty = 5,
|
||||
ReleaseConditionTextId = "legacy_puzzle",
|
||||
});
|
||||
db.PuzzleMissions.Add(new PuzzleMissionEntry
|
||||
{
|
||||
Id = legacyMissionId,
|
||||
MissionName = "legacy_mission",
|
||||
AchievedMessage = "legacy_achieved",
|
||||
RequireNumber = 42,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var importer = new PuzzleImporter();
|
||||
await importer.ImportGroupsAsync(db, SeedDir);
|
||||
await importer.ImportPuzzlesAsync(db, SeedDir);
|
||||
await importer.ImportMissionsAsync(db, SeedDir);
|
||||
|
||||
var g = await db.PuzzleGroups.FindAsync(legacyGroupId);
|
||||
Assert.That(g, Is.Not.Null);
|
||||
Assert.That(g!.BasicTitleTextId, Is.EqualTo("legacy_group"));
|
||||
|
||||
var p = await db.Puzzles.FindAsync(legacyPuzzleId);
|
||||
Assert.That(p, Is.Not.Null);
|
||||
Assert.That(p!.PuzzleDifficulty, Is.EqualTo(5));
|
||||
|
||||
var m = await db.PuzzleMissions.FindAsync(legacyMissionId);
|
||||
Assert.That(m, Is.Not.Null);
|
||||
Assert.That(m!.MissionName, Is.EqualTo("legacy_mission"));
|
||||
Assert.That(m.RequireNumber, Is.EqualTo(42));
|
||||
}
|
||||
|
||||
[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<SVSimDbContext>();
|
||||
|
||||
string tmp = Path.Combine(Path.GetTempPath(), $"seed-{Guid.NewGuid()}");
|
||||
Directory.CreateDirectory(tmp);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(tmp, "puzzle-groups.json"),
|
||||
"[{\"id\":0,\"basic_title_text_id\":\"junk\"}]");
|
||||
File.WriteAllText(Path.Combine(tmp, "puzzles.json"),
|
||||
"[{\"id\":0,\"group_id\":1,\"puzzle_difficulty\":1}]");
|
||||
File.WriteAllText(Path.Combine(tmp, "puzzle-missions.json"),
|
||||
"[{\"id\":0,\"mission_name\":\"junk\"}]");
|
||||
|
||||
var importer = new PuzzleImporter();
|
||||
await importer.ImportGroupsAsync(db, tmp);
|
||||
await importer.ImportPuzzlesAsync(db, tmp);
|
||||
await importer.ImportMissionsAsync(db, tmp);
|
||||
|
||||
Assert.That(await db.PuzzleGroups.CountAsync(), Is.EqualTo(0),
|
||||
"rows with id=0 must not be inserted into groups");
|
||||
Assert.That(await db.Puzzles.CountAsync(), Is.EqualTo(0),
|
||||
"rows with id=0 must not be inserted into puzzles");
|
||||
Assert.That(await db.PuzzleMissions.CountAsync(), Is.EqualTo(0),
|
||||
"rows with id=0 must not be inserted into missions");
|
||||
}
|
||||
finally { Directory.Delete(tmp, true); }
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,10 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
|
||||
// practice-opponent rows after the corresponding block was lifted out of GlobalsImporter.
|
||||
await new PracticeOpponentImporter().ImportAsync(ctx, seedDir);
|
||||
await new PaymentItemImporter().ImportAsync(ctx, seedDir);
|
||||
var puzzleImporter = new PuzzleImporter();
|
||||
await puzzleImporter.ImportGroupsAsync(ctx, seedDir);
|
||||
await puzzleImporter.ImportPuzzlesAsync(ctx, seedDir);
|
||||
await puzzleImporter.ImportMissionsAsync(ctx, seedDir);
|
||||
}
|
||||
|
||||
/// <summary>Convenience: bake the X-Test-Viewer-Id header into a fresh client.</summary>
|
||||
|
||||
Reference in New Issue
Block a user