Files
SVSimServer/SVSim.UnitTests/Importers/CardListsImporterTests.cs
gamer147 d14a0be2c8 refactor(bootstrap): finalize load-index migration; GlobalsImporter is now a stub
Stage 9C of the bootstrap-seed-refactor:

- Add 6 seed DTOs for the card-id-keyed load-index tables (SpotCard,
  ReprintedCard, UnlimitedRestriction, LoadingExclusionCard, MaintenanceCard,
  FeatureMaintenance).
- Add CardListsImporter: idempotent upsert of the 6 tables, sharing one
  Cards FK set for orphan-warning. FeatureMaintenances clear-and-rewrites
  (synthetic ordinal Id; no natural key).
- Add RotationFlagUpdater: reads RotationConfig.RotationCardSetIds from the
  GameConfigs section (populated by RotationConfigImporter) and flips
  CardSet.IsInRotation to match.
- Add RotationConfig.RotationCardSetIds list property + wire it through
  RotationConfigImporter. No migration needed (sections are JSON blobs).
- RotationConfigImporter: use legacy local-kind DateTime parse for schedule
  windows so the JSON round-trip stays byte-equivalent to GlobalsImporter.
- Strip GlobalsImporter down to a no-op stub (Task 10 will delete it).
- Wire all 9 new importers into Program.cs and SVSimTestFactory.SeedGlobalsAsync,
  in the order RotationConfigImporter -> ... -> CardListsImporter -> RotationFlagUpdater.
- Delete prod-captures/load-index-2026-05-23.json.
- Add CardListsImporterTests covering each sub-table, idempotency,
  empty-seed handling, orphan-warning, and the clear-and-rewrite path.

Tests: 391 passing (382 baseline + 9 new).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:46:36 -04:00

181 lines
7.3 KiB
C#

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;
/// <summary>
/// Coverage for CardListsImporter (Stage 9C): one happy-path test per card-list sub-table plus
/// idempotency and orphan-warning behavior. Production seeds reference cards that don't exist in
/// the minimal 3-card test set, so the importer must complete without failing on FK orphans.
/// </summary>
public class CardListsImporterTests
{
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
[Test]
public async Task ImportAsync_writes_spot_cards_from_seed()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
await new CardListsImporter().ImportAsync(db, SeedDir);
var rows = await db.SpotCards.ToListAsync();
Assert.That(rows.Count, Is.GreaterThan(0), "spot-cards.json must produce rows");
Assert.That(rows.All(r => r.Cost >= 0), Is.True, "Cost must be >= 0");
}
[Test]
public async Task ImportAsync_writes_reprinted_cards_from_seed()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
await new CardListsImporter().ImportAsync(db, SeedDir);
Assert.That(await db.ReprintedCards.CountAsync(), Is.GreaterThan(0),
"reprinted-cards.json must produce rows");
}
[Test]
public async Task ImportAsync_writes_unlimited_restrictions_with_values()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
await new CardListsImporter().ImportAsync(db, SeedDir);
var rows = await db.UnlimitedRestrictions.ToListAsync();
Assert.That(rows.Count, Is.GreaterThan(0), "unlimited-restrictions.json must produce rows");
// RestrictionValue field must survive the import (e.g. 0 or 1).
Assert.That(rows.All(r => r.RestrictionValue >= 0), Is.True);
}
[Test]
public async Task ImportAsync_writes_loading_exclusion_cards_from_seed()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
await new CardListsImporter().ImportAsync(db, SeedDir);
Assert.That(await db.LoadingExclusionCards.CountAsync(), Is.GreaterThan(0),
"loading-exclusion-cards.json must produce rows");
}
[Test]
public async Task ImportAsync_handles_empty_maintenance_card_seed()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
// The shipped maintenance-cards.json is `[]` — confirm no rows created and no crash.
await new CardListsImporter().ImportAsync(db, SeedDir);
Assert.That(await db.MaintenanceCards.CountAsync(), Is.EqualTo(0),
"Empty maintenance seed should leave the table empty");
}
[Test]
public async Task ImportAsync_handles_empty_feature_maintenance_seed()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
await new CardListsImporter().ImportAsync(db, SeedDir);
Assert.That(await db.FeatureMaintenances.CountAsync(), Is.EqualTo(0),
"Empty feature-maintenances seed should leave the table empty");
}
[Test]
public async Task ImportAsync_is_idempotent()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
await new CardListsImporter().ImportAsync(db, SeedDir);
int spots1 = await db.SpotCards.CountAsync();
int reprinted1 = await db.ReprintedCards.CountAsync();
int unlimited1 = await db.UnlimitedRestrictions.CountAsync();
int excl1 = await db.LoadingExclusionCards.CountAsync();
await new CardListsImporter().ImportAsync(db, SeedDir);
int spots2 = await db.SpotCards.CountAsync();
int reprinted2 = await db.ReprintedCards.CountAsync();
int unlimited2 = await db.UnlimitedRestrictions.CountAsync();
int excl2 = await db.LoadingExclusionCards.CountAsync();
Assert.Multiple(() =>
{
Assert.That(spots2, Is.EqualTo(spots1));
Assert.That(reprinted2, Is.EqualTo(reprinted1));
Assert.That(unlimited2, Is.EqualTo(unlimited1));
Assert.That(excl2, Is.EqualTo(excl1));
});
}
[Test]
public async Task ImportAsync_completes_when_seed_card_ids_are_orphans()
{
// The shipped seeds reference card_ids that DON'T exist in SVSimTestFactory's minimal
// 3-card set — the orphan-warning path should log to stderr without throwing.
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
Assert.That(await db.Cards.CountAsync(), Is.EqualTo(3),
"Test factory should seed exactly 3 cards (orphan-warning precondition)");
Assert.DoesNotThrowAsync(async () =>
{
await new CardListsImporter().ImportAsync(db, SeedDir);
});
// Importer still wrote rows despite orphans.
Assert.That(await db.SpotCards.CountAsync(), Is.GreaterThan(0));
}
[Test]
public async Task ImportAsync_writes_feature_maintenances_from_tiny_fixture()
{
using var factory = new SVSimTestFactory();
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
// Build a temp seed dir with just feature-maintenances.json populated so we can exercise
// the FeatureMaintenances clear-and-rewrite path without polluting the shipped seeds.
string tmp = Path.Combine(Path.GetTempPath(), $"seed-{Guid.NewGuid()}");
Directory.CreateDirectory(tmp);
try
{
File.WriteAllText(Path.Combine(tmp, "feature-maintenances.json"),
"[{\"id\":1,\"feature_key\":\"test_feature\",\"data\":{\"foo\":\"bar\"}}]");
await new CardListsImporter().ImportAsync(db, tmp);
var rows = await db.FeatureMaintenances.ToListAsync();
Assert.That(rows.Count, Is.EqualTo(1));
Assert.That(rows[0].FeatureKey, Is.EqualTo("test_feature"));
Assert.That(rows[0].Data, Does.Contain("foo"));
// Rerun: clear-and-rewrite should keep the table at 1 row (same data).
await new CardListsImporter().ImportAsync(db, tmp);
Assert.That(await db.FeatureMaintenances.CountAsync(), Is.EqualTo(1));
}
finally { Directory.Delete(tmp, true); }
}
}