using GerbilManagerWebAPI.Import; using GerbilManagerWebAPI.Models; using Microsoft.EntityFrameworkCore; namespace GerbilManager.Tests { /// /// FEAT-8c loader tests against a small JSON fixture + an in-memory DB (never Julian's /// instance). Covers categorisation, the quarantine policy, genotype composition, /// Farbschlag matching, high-confidence linking, and idempotency. /// public class ImportServiceTests : IDisposable { private readonly string _dir; public ImportServiceTests() { _dir = Path.Combine(Path.GetTempPath(), "feat8c-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_dir); File.WriteAllText(Path.Combine(_dir, "litters.json"), LittersJson); File.WriteAllText(Path.Combine(_dir, "animals.json"), AnimalsJson); } public void Dispose() { try { Directory.Delete(_dir, recursive: true); } catch { } } private ApplicationContext NewDb() { var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase("feat8c-" + Guid.NewGuid().ToString("N")) .Options; var db = new ApplicationContext(opts); db.Database.EnsureCreated(); // applies the 73-variety HasData seed return db; } [Fact] public async Task DryRun_categorises_without_writing() { using var db = NewDb(); var report = await new ImportService(db, _dir, _dir).RunAsync(execute: false); Assert.False(report.Executed); Assert.Equal(2, report.Litters.InSource); Assert.Equal(2, report.Litters.Created); // 4 animals: a1 (hoch) + a4 (date-only) loadable; a2 conflict, a3 stub quarantined Assert.Equal(4, report.Animals.InSource); Assert.Equal(2, report.Animals.Created); Assert.Equal(1, report.Animals.LinkedToLitter); // only a1 (hoch) Assert.Equal(1, report.Animals.FarbschlagMatched); // a1 -> Agouti Assert.Equal(1, report.Animals.Quarantined.Conflicts); Assert.Equal(1, report.Animals.Quarantined.Stubs); Assert.Equal(1, report.Animals.Quarantined.DateOnlyLinks); // nothing written in dry-run Assert.Equal(0, await db.Gerbils.CountAsync()); Assert.Equal(0, await db.Litters.CountAsync()); } [Fact] public async Task Execute_loads_conflict_free_and_links_high_confidence() { using var db = NewDb(); var report = await new ImportService(db, _dir, _dir).RunAsync(execute: true); Assert.True(report.Executed); Assert.Equal(2, await db.Litters.CountAsync()); Assert.Equal(2, await db.Gerbils.CountAsync()); // a1, a4 only // conflict + stub never loaded Assert.False(await db.Gerbils.AnyAsync(g => g.ExternalRef == "a2")); Assert.False(await db.Gerbils.AnyAsync(g => g.ExternalRef == "a3")); var a1 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a1"); Assert.NotNull(a1.LitterId); // high-confidence link Assert.Equal("FEAT-8 Stammbaum/Wurfchronik", a1.ImportSource); Assert.NotNull(a1.ColorVarietyId); // Agouti matched Assert.Contains("aa CC", a1.Genotype); // composed from mapped8locus Assert.Contains("??", a1.Genotype!); // missing loci -> ?? Assert.Contains("eef", a1.Genotype!); // e^f caret stripped Assert.Contains("RawGenotype", a1.RawImportData!); // raw verbatim preserved var a4 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a4"); Assert.Null(a4.LitterId); // date-only link quarantined // PairingCode carried through Assert.True(await db.Litters.AnyAsync(l => l.PairingCode == "G01/ZdkC")); } [Fact] public async Task Execute_is_idempotent() { using var db = NewDb(); await new ImportService(db, _dir, _dir).RunAsync(execute: true); var second = await new ImportService(db, _dir, _dir).RunAsync(execute: true); Assert.Equal(0, second.Animals.Created); Assert.Equal(0, second.Litters.Created); Assert.Equal(2, await db.Gerbils.CountAsync()); Assert.Equal(2, await db.Litters.CountAsync()); } [Fact] public void ComposeGenotype_strips_carets_and_fills_missing_loci() { var g = new SourceGenotype { Mapped8locus = new() { ["A"] = new() { "a", "a" }, ["C"] = new() { "C", "c^chm" }, ["E"] = new() { "e", "e^f" }, }, }; // order A C D E G P Sp Re ; missing -> ?? Assert.Equal("aa Ccchm ?? eef ?? ?? ?? ??", ImportService.ComposeGenotype(g)); } [Theory] [InlineData("01.02.2020", 2020, 2, 1)] [InlineData("5.3.21", 2021, 3, 5)] public void ParseDate_handles_german_dates(string s, int y, int m, int d) { var date = ImportService.ParseDate(s); Assert.Equal(new DateOnly(y, m, d), date); } // ---- fixtures ---- private const string LittersJson = """ [ {"id":"L1","litterId":"A","date":"01.02.2020","damName":"Mama [X]","sireName":"Papa of Y","totalBorn":4,"zuchtnummer":"G01/ZdkC","note":"erster Wurf"}, {"id":"L2","litterId":"B","date":"05.03.2021","damName":"Oma [Z]","sireName":"Opa of W","totalBorn":2,"zuchtnummer":"G02/ZdkC","note":""} ] """; private const string AnimalsJson = """ [ {"id":"a1","name":"Kind Eins","dob":"01.02.2020","death":"","gender":null, "farbschlag":"Agouti","farbschlagVariants":["Agouti"], "genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"]},"rawGenotype":"aa CC D- ee[f]","unmappedTokens":[]}, "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false, "litterRef":{"litterId":"L1","method":"geburtsdatum+eltern","confidence":"hoch"}}, {"id":"a2","name":"Streit","dob":"01.01.2019","death":"","gender":null, "farbschlag":"Schwarz","farbschlagVariants":["Schwarz"], "genotype":{"mapped8locus":{"A":["a","a"]},"rawGenotype":"aa","unmappedTokens":[]}, "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1","f2"],"conflict":true}, {"id":"a3","name":"Namenlos","dob":"","death":"","gender":null, "farbschlag":"","farbschlagVariants":[], "genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]}, "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false}, {"id":"a4","name":"Unsicher","dob":"05.03.2021","death":"","gender":null, "farbschlag":"","farbschlagVariants":[], "genotype":{"mapped8locus":{"A":["A","a"]},"rawGenotype":"Aa","unmappedTokens":[]}, "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false, "litterRef":{"litterId":"L2","method":"geburtsdatum","confidence":"niedrig"}} ] """; } }