Files
GerbilManager/GerbilManager.Tests/VerifiedGoldenTests.cs
Gulum 45b8533f18
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m11s
CI / Frontend Tests (Node/Vite) (push) Failing after 4m59s
CI / Docker Build & Push (push) Has been skipped
CI / Deploy auf TrueNAS (Custom App) (push) Has been skipped
feat(deploy): TrueNAS Custom-App + Auto-Deploy, plus aufgelaufene Arbeit
Deployment:
- custom-app.compose.yaml: self-contained Compose fuer TrueNAS "Custom App"
  (absolute Host-Bind-Pfade, postgres:18, pull_policy always, Port 8090)
- scripts/truenas-deploy.sh: Host-Skript create/redeploy via midclt (App
  bleibt unter Apps sichtbar) inkl. Image-Pull + Health-Check
- ci.yml Deploy-Job: laeuft auf ubuntu-latest-Runner, kopiert Deploy-Dateien
  per SSH auf den NAS-Host und triggert truenas-deploy.sh (statt runs-on goldeye)
- compose.yaml/.env.example: postgres:18 (Locale-Match zur Quell-DB), Port 8090
- .gitignore: .agents/, tools/rag/, deploy/truenas/.env (Secrets/Scratch)

Aufgelaufene Feature-Arbeit (verified/Freeze, Migrationen, Import-Triage):
- GerbilOverride/VerifiedGerbil-Endpoints + GerbilSnapshotService + Tests
- EF-Migrationen (ShowInChronicle, Stillborn, BirthOrder, ManualFlag, DSGVO)
- Frontend VerifizierteTierePage + verified-API + e2e-Spec
- diverse Import-/Triage-Skripte und -Tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 09:19:11 +02:00

152 lines
6.0 KiB
C#

using System.Text.Json;
using GerbilManagerWebAPI.Import;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Xunit.Abstractions;
namespace GerbilManager.Tests
{
/// <summary>
/// Golden regression fixture: ingests the REAL <c>tools/import/output/resolved_import.json</c>
/// (gitignored, local only) and asserts that every verified key animal's Akte still matches its
/// frozen golden snapshot in <c>tools/import/verified-golden.json</c> (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.
/// </summary>
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<GoldenEntry>? golden;
try
{
golden = JsonSerializer.Deserialize<List<GoldenEntry>>(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<ApplicationContext>()
.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<string, string?>
{
{ "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<string>();
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);
}
}
}
}