using System.Text.Json; using GerbilManagerWebAPI.Import; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Xunit.Abstractions; namespace GerbilManager.Tests { /// /// Golden regression fixture: ingests the REAL tools/import/output/resolved_import.json /// (gitignored, local only) and asserts that every verified key animal's Akte still matches its /// frozen golden snapshot in tools/import/verified-golden.json (committed fixture). /// /// Both files are optional: if EITHER is missing the test skips cleanly (no failure), so it is a /// no-op on CI / fresh clones but a real guard on the breeder's machine once she has verified /// animals and exported the fixture. /// public class VerifiedGoldenTests { private readonly ITestOutputHelper _out; public VerifiedGoldenTests(ITestOutputHelper output) => _out = output; private sealed record GoldenEntry(Guid GerbilId, string? Name, DateTimeOffset? VerifiedAt, GerbilSnapshot? Snapshot); private static readonly JsonSerializerOptions Web = new(JsonSerializerDefaults.Web); private static string? FindRepoRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null) { if (Directory.Exists(Path.Combine(dir.FullName, "tools", "import"))) return dir.FullName; dir = dir.Parent; } return null; } [Fact] public async Task Verified_animals_still_match_their_golden_snapshot() { var repoRoot = FindRepoRoot(); if (repoRoot is null) { _out.WriteLine("SKIP: repo root (tools/import) not found from the test host."); return; } var goldenPath = Path.Combine(repoRoot, "tools", "import", "verified-golden.json"); var outputDir = Path.Combine(repoRoot, "tools", "import", "output"); var resolvedPath = Path.Combine(outputDir, "resolved_import.json"); if (!File.Exists(goldenPath)) { _out.WriteLine($"SKIP: golden fixture not present ({goldenPath})."); return; } if (!File.Exists(resolvedPath)) { _out.WriteLine($"SKIP: resolved_import.json not present ({resolvedPath}) — run the import pipeline."); return; } List? golden; try { golden = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(goldenPath), Web); } catch (Exception ex) { _out.WriteLine($"SKIP: could not parse golden fixture: {ex.Message}"); return; } if (golden is null || golden.Count == 0) { _out.WriteLine("SKIP: golden fixture is empty."); return; } // Ingest the real resolved_import.json into an InMemory DB. var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase("verified-golden-" + Guid.NewGuid().ToString("N")) .Options; using var db = new ApplicationContext(opts); db.Database.EnsureCreated(); var photoDir = Path.Combine(Path.GetTempPath(), "verified-golden-photos-" + Guid.NewGuid().ToString("N")); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "Import:SourcePath", outputDir }, { "Photos:RootPath", photoDir }, }) .Build(); try { var result = await new IngestResolvedService(db, config, null!).RunAsync(); Assert.StartsWith("Ingestion successful!", result); } catch (Exception ex) { _out.WriteLine($"SKIP: ingest of the real resolved_import.json failed: {ex.Message}"); return; } finally { try { Directory.Delete(photoDir, recursive: true); } catch { } } var mismatches = new List(); int checkedCount = 0; foreach (var entry in golden) { if (entry.Snapshot is null) continue; checkedCount++; GerbilSnapshot? actual; try { actual = await GerbilSnapshotService.BuildSnapshotAsync(db, entry.GerbilId); } catch (Exception ex) { mismatches.Add($"• {entry.Name ?? entry.GerbilId.ToString()}: Akte konnte nicht gebaut werden — {ex.Message}"); continue; } if (actual is null) { mismatches.Add($"• {entry.Name ?? entry.GerbilId.ToString()}: fehlt im Import (verifiziertes Tier verschwunden)."); continue; } var diffs = GerbilSnapshotService.Diff(entry.Snapshot, actual); if (diffs.Count > 0) { var fields = string.Join("; ", diffs.Select(d => $"{d.Path}: golden='{d.GoldenValue}' ≠ import='{d.OtherValue}'")); mismatches.Add($"• {entry.Name ?? entry.GerbilId.ToString()}: {diffs.Count} Abweichung(en) — {fields}"); } } _out.WriteLine($"Golden-Fixture geprüft: {checkedCount} verifizierte Tiere, {mismatches.Count} abweichend."); if (mismatches.Count > 0) { var report = $"Golden-Regression fehlgeschlagen — {mismatches.Count} von {checkedCount} verifizierten Tieren weichen vom eingefrorenen Snapshot ab:\n" + string.Join("\n", mismatches); Assert.Fail(report); } } } }