Neuer „Datenherkunft"-Button in der Rennmausakte öffnet einen Dialog, der zeigt, aus welchen Quellen der Eintrag erzeugt wurde: Quelldateien (Stammbäume + Wurfchronik), Anzahl zusammengeführter Datensätze, Eltern-Herleitung (Methode/Konfidenz) und Hinweise (z. B. „per manueller Entscheidung zugeordnet", „Konflikt gelöst", „aus N Datensätzen zusammengeführt"). Import: build_provenance() in merge_and_resolve.py sammelt die Herkunft über alle deduplizierten Datensätze und schreibt sie als Provenance-JSON je Tier in resolved_import.json. Backend: Gerbil.Provenance (nullable text) + Migration AddGerbilProvenance, gemappt im Ingest und im GerbilDto zurückgegeben. Frontend: ProvenanceDialog + Typen + Strings. Tests: Ingest-Round-trip (vorhanden/abwesend), e2e provenance.spec.ts. dotnet(212)/vitest(129)/playwright/tsc/eslint grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
190 lines
7.5 KiB
C#
190 lines
7.5 KiB
C#
using GerbilManagerWebAPI.Import;
|
|
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using System.Text.Json;
|
|
|
|
namespace GerbilManager.Tests
|
|
{
|
|
public class IngestResolvedServiceTests : IDisposable
|
|
{
|
|
private readonly string _dir;
|
|
private readonly string _resolvedJsonPath;
|
|
|
|
public IngestResolvedServiceTests()
|
|
{
|
|
_dir = Path.Combine(Path.GetTempPath(), "ingest-resolved-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_dir);
|
|
_resolvedJsonPath = Path.Combine(_dir, "resolved_import.json");
|
|
|
|
var contactId = Guid.NewGuid();
|
|
var fatherId = Guid.NewGuid();
|
|
var motherId = Guid.NewGuid();
|
|
var litterId = Guid.NewGuid();
|
|
var photoId = Guid.NewGuid();
|
|
|
|
var data = new
|
|
{
|
|
Contacts = new[]
|
|
{
|
|
new
|
|
{
|
|
Id = contactId,
|
|
Name = "Test Breeder",
|
|
ZuchtName = "Test Zucht",
|
|
City = "Test City",
|
|
Email = "test@example.com",
|
|
Homepage = "",
|
|
Phone = "",
|
|
Address = ""
|
|
}
|
|
},
|
|
Litters = new[]
|
|
{
|
|
new
|
|
{
|
|
Id = litterId,
|
|
Name = "Wurf A",
|
|
Date = "2026-01-01",
|
|
TotalBorn = 5,
|
|
DeathsWithin8Weeks = 0,
|
|
FatherId = fatherId,
|
|
MotherId = motherId,
|
|
ExpectedGoHomeDate = (string?)null,
|
|
Notes = "Test litter notes",
|
|
PairingCode = "PC01",
|
|
ExternalRef = "ext-litter-1",
|
|
LitterLetter = "A"
|
|
}
|
|
},
|
|
Gerbils = new[]
|
|
{
|
|
new
|
|
{
|
|
Id = fatherId,
|
|
Name = "Papa",
|
|
Gender = "male",
|
|
Status = "Breeding",
|
|
LitterId = (Guid?)null,
|
|
OriginContactId = contactId,
|
|
ReceiverContactId = (Guid?)null,
|
|
EnclosureId = (Guid?)null,
|
|
ColorVarietyId = new Guid("00000000-0000-0000-0000-000000000006"), // Schwarz
|
|
DateOfBirth = "2025-01-01",
|
|
DateOfDeath = (string?)null,
|
|
CauseOfDeath = (string?)null,
|
|
GoHomeDate = (string?)null,
|
|
Genotype = "aa CC DD EE GG PP spsp rere",
|
|
Notes = "",
|
|
ImportSource = "docx-export",
|
|
ExternalRef = "ext-papa",
|
|
RawImportData = "{}",
|
|
OriginBreeder = "Test Zucht",
|
|
NameSearch = "papa",
|
|
CharacterTraits = new string[] {},
|
|
CharacterNote = (string?)null,
|
|
IsDeaf = false,
|
|
IsResident = true,
|
|
Provenance = (string?)"{\"sourceFiles\":[\"Stammbaum von Papa.xlsx\",\"Wurfchronik-Detail.docx\"],\"mergedRecordCount\":2,\"fromWurfchronik\":true,\"notes\":[\"aus 2 Datensätzen zusammengeführt\"]}"
|
|
},
|
|
new
|
|
{
|
|
Id = motherId,
|
|
Name = "Mama",
|
|
Gender = "female",
|
|
Status = "Breeding",
|
|
LitterId = (Guid?)null,
|
|
OriginContactId = contactId,
|
|
ReceiverContactId = (Guid?)null,
|
|
EnclosureId = (Guid?)null,
|
|
ColorVarietyId = new Guid("00000000-0000-0000-0000-000000000006"), // Schwarz
|
|
DateOfBirth = "2025-01-01",
|
|
DateOfDeath = (string?)null,
|
|
CauseOfDeath = (string?)null,
|
|
GoHomeDate = (string?)null,
|
|
Genotype = "aa CC DD EE GG PP spsp rere",
|
|
Notes = "",
|
|
ImportSource = "docx-export",
|
|
ExternalRef = "ext-mama",
|
|
RawImportData = "{}",
|
|
OriginBreeder = "Test Zucht",
|
|
NameSearch = "mama",
|
|
CharacterTraits = new string[] {},
|
|
CharacterNote = (string?)null,
|
|
IsDeaf = false,
|
|
IsResident = true,
|
|
Provenance = (string?)null
|
|
}
|
|
},
|
|
GerbilPhotos = new[]
|
|
{
|
|
new
|
|
{
|
|
Id = photoId,
|
|
GerbilId = fatherId,
|
|
FileName = "photo1.jpg",
|
|
SortOrder = 0
|
|
}
|
|
}
|
|
};
|
|
|
|
File.WriteAllText(_resolvedJsonPath, JsonSerializer.Serialize(data));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, recursive: true); } catch { }
|
|
}
|
|
|
|
private ApplicationContext NewDb()
|
|
{
|
|
var opts = new DbContextOptionsBuilder<ApplicationContext>()
|
|
.UseInMemoryDatabase("ingest-resolved-" + Guid.NewGuid().ToString("N"))
|
|
.Options;
|
|
var db = new ApplicationContext(opts);
|
|
db.Database.EnsureCreated();
|
|
return db;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IngestResolved_loads_data_and_resolves_all_relations()
|
|
{
|
|
using var db = NewDb();
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
{ "Import:SourcePath", _dir }
|
|
})
|
|
.Build();
|
|
|
|
var service = new IngestResolvedService(db, config, null!);
|
|
var result = await service.RunAsync();
|
|
|
|
Assert.Contains("Ingestion successful!", result);
|
|
|
|
// Verify counts
|
|
Assert.Equal(1, await db.Contacts.CountAsync());
|
|
Assert.Equal(1, await db.Litters.CountAsync());
|
|
Assert.Equal(2, await db.Gerbils.CountAsync());
|
|
Assert.Equal(1, await db.GerbilPhotos.CountAsync());
|
|
|
|
// Verify relations
|
|
var litter = await db.Litters.SingleAsync();
|
|
var father = await db.Gerbils.SingleAsync(g => g.Name == "Papa");
|
|
var mother = await db.Gerbils.SingleAsync(g => g.Name == "Mama");
|
|
var photo = await db.GerbilPhotos.SingleAsync();
|
|
|
|
Assert.Equal(father.Id, litter.FatherId);
|
|
Assert.Equal(mother.Id, litter.MotherId);
|
|
Assert.Equal(father.Id, photo.GerbilId);
|
|
Assert.Equal(father.OriginContactId, mother.OriginContactId);
|
|
|
|
// Provenance round-trips verbatim through ingest (and stays null when absent).
|
|
Assert.NotNull(father.Provenance);
|
|
Assert.Contains("Stammbaum von Papa.xlsx", father.Provenance);
|
|
Assert.Contains("mergedRecordCount", father.Provenance);
|
|
Assert.Null(mother.Provenance);
|
|
}
|
|
}
|
|
}
|