ImportDocxService gains a three-way match-or-create loop:
Path 1 (ExternalRef 'docx-…'): previously-created docx animals → enrich fill-NULL-only (idempotency)
Path 2 (name+dob): existing main-import animals → enrich fill-NULL-only (as before)
Path 3 (no match): CREATE new Gerbil — abgegebene Jungtiere not in pedigree charts
Status=GivenAway (or Deceased), IsResident=false,
OriginBreeder='Zucht der Kleinen Chaoten', ImportSource='docx',
ExternalRef='docx-{ws}-{normname}-{litterDob}' (stable idempotent key),
Gender/ColorVariety/LitterId/ReceiverContact/GoHomeDate/DateOfDeath all set.
ImportDocxReport gains Created field; contact-creation eagerly saved before gerbil batch.
5 SQLite-backed tests: dry-run counts, create+FK, idempotency, enrich-not-duplicate, Deceased status.
Gate: 163/163 tests, has-pending=No, no schema change.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
231 lines
9.3 KiB
C#
231 lines
9.3 KiB
C#
using GerbilManagerWebAPI.Import;
|
|
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace GerbilManager.Tests
|
|
{
|
|
/// <summary>
|
|
/// FEAT-8d-CREATE: docx importer CREATE path tests.
|
|
/// Uses SQLite (not InMemory) so FK constraints are enforced.
|
|
/// </summary>
|
|
public class ImportDocxServiceTests : IDisposable
|
|
{
|
|
private readonly string _dir;
|
|
|
|
public ImportDocxServiceTests()
|
|
{
|
|
_dir = Path.Combine(Path.GetTempPath(), "feat8d-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_dir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, recursive: true); } catch { }
|
|
}
|
|
|
|
// One open connection keeps the :memory: SQLite DB alive across calls.
|
|
private (ApplicationContext db, SqliteConnection conn) NewSqliteDb()
|
|
{
|
|
var conn = new SqliteConnection("DataSource=:memory:");
|
|
conn.Open();
|
|
var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(conn).Options;
|
|
var db = new ApplicationContext(opts);
|
|
db.Database.EnsureCreated();
|
|
return (db, conn);
|
|
}
|
|
|
|
private void WriteLitters(object litters) =>
|
|
File.WriteAllText(Path.Combine(_dir, "docx_litters.json"),
|
|
System.Text.Json.JsonSerializer.Serialize(litters));
|
|
|
|
private void WriteAnimals(object animals) =>
|
|
File.WriteAllText(Path.Combine(_dir, "docx_animals.json"),
|
|
System.Text.Json.JsonSerializer.Serialize(animals));
|
|
|
|
// ── Test 1: dry-run shows correct counts without writing ─────────────────
|
|
|
|
[Fact]
|
|
public async Task DryRun_counts_new_animal_without_writing()
|
|
{
|
|
var (db, conn) = NewSqliteDb();
|
|
await using (conn)
|
|
await using (db)
|
|
{
|
|
// Litter in DB with PairingCode matching the docx WS-code
|
|
var litter = new Litter
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Testwurf WS1",
|
|
Date = new DateOnly(2023, 5, 1),
|
|
PairingCode = "WS1"
|
|
};
|
|
db.Litters.Add(litter);
|
|
await db.SaveChangesAsync();
|
|
|
|
WriteLitters(Array.Empty<object>());
|
|
WriteAnimals(new[]
|
|
{
|
|
new { wsCode = "WS1", litterDob = "01.05.2023", name = "Pepper", gender = "female",
|
|
owner = "Max Mustermann", abgabeDate = "01.07.2023",
|
|
deathDate = "", deathCause = "", farbschlag = "" }
|
|
});
|
|
|
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: false);
|
|
|
|
Assert.False(report.Executed);
|
|
Assert.Equal(1, report.Created);
|
|
Assert.Equal(1, report.LitterLinked);
|
|
Assert.Equal(1, report.GoHomeFilled); // new animal will have GoHomeDate set
|
|
Assert.Equal(0, await db.Gerbils.CountAsync()); // nothing written
|
|
Assert.Equal(0, await db.Contacts.CountAsync()); // nothing written
|
|
}
|
|
}
|
|
|
|
// ── Test 2: execute creates animal with all fields + FK constraints ───────
|
|
|
|
[Fact]
|
|
public async Task Execute_creates_animal_with_litter_contact_and_goHomeDate()
|
|
{
|
|
var (db, conn) = NewSqliteDb();
|
|
await using (conn)
|
|
await using (db)
|
|
{
|
|
var litter = new Litter
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Testwurf WS1",
|
|
Date = new DateOnly(2023, 5, 1),
|
|
PairingCode = "WS1"
|
|
};
|
|
db.Litters.Add(litter);
|
|
await db.SaveChangesAsync();
|
|
|
|
WriteLitters(Array.Empty<object>());
|
|
WriteAnimals(new[]
|
|
{
|
|
new { wsCode = "WS1", litterDob = "01.05.2023", name = "Pepper", gender = "female",
|
|
owner = "Max Mustermann", abgabeDate = "01.07.2023",
|
|
deathDate = "", deathCause = "", farbschlag = "" }
|
|
});
|
|
|
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
|
|
|
Assert.True(report.Executed);
|
|
Assert.Equal(1, report.Created);
|
|
Assert.Equal(1, report.ContactsCreated);
|
|
|
|
var gerbil = await db.Gerbils.SingleAsync();
|
|
Assert.Equal("Pepper", gerbil.Name);
|
|
Assert.Equal(new DateOnly(2023, 5, 1), gerbil.DateOfBirth);
|
|
Assert.Equal(Gender.female, gerbil.Gender);
|
|
Assert.Equal(GerbilStatus.GivenAway, gerbil.Status);
|
|
Assert.Equal(litter.Id, gerbil.LitterId);
|
|
Assert.Equal(new DateOnly(2023, 7, 1), gerbil.GoHomeDate);
|
|
Assert.Equal("Zucht der Kleinen Chaoten", gerbil.OriginBreeder);
|
|
Assert.False(gerbil.IsResident);
|
|
Assert.Equal("docx", gerbil.ImportSource);
|
|
Assert.StartsWith("docx-", gerbil.ExternalRef);
|
|
|
|
var contact = await db.Contacts.SingleAsync();
|
|
Assert.Equal("Max Mustermann", contact.Name);
|
|
Assert.Equal(contact.Id, gerbil.ReceiverContactId);
|
|
}
|
|
}
|
|
|
|
// ── Test 3: idempotency — second run creates zero ────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Execute_is_idempotent_second_run_creates_zero()
|
|
{
|
|
var (db, conn) = NewSqliteDb();
|
|
await using (conn)
|
|
await using (db)
|
|
{
|
|
WriteLitters(Array.Empty<object>());
|
|
WriteAnimals(new[]
|
|
{
|
|
new { wsCode = "WS2", litterDob = "15.03.2023", name = "Flash", gender = "male",
|
|
owner = "", abgabeDate = "01.05.2023",
|
|
deathDate = "", deathCause = "", farbschlag = "" }
|
|
});
|
|
|
|
var first = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
|
Assert.Equal(1, first.Created);
|
|
|
|
var second = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
|
Assert.Equal(0, second.Created); // ExternalRef path, no dupe
|
|
Assert.Equal(1, await db.Gerbils.CountAsync()); // exactly one row
|
|
}
|
|
}
|
|
|
|
// ── Test 4: existing main-import animal → enrich only, no duplicate ──────
|
|
|
|
[Fact]
|
|
public async Task Execute_enriches_existing_animal_does_not_duplicate()
|
|
{
|
|
var (db, conn) = NewSqliteDb();
|
|
await using (conn)
|
|
await using (db)
|
|
{
|
|
// Animal already in DB (from main import, ExternalRef ≠ "docx-…")
|
|
var existing = new Gerbil
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Eddie",
|
|
DateOfBirth = new DateOnly(2022, 8, 10),
|
|
Gender = Gender.male,
|
|
ExternalRef = "main-eddie-xyz", // NOT a "docx-" ref
|
|
IsResident = false,
|
|
};
|
|
db.Gerbils.Add(existing);
|
|
await db.SaveChangesAsync();
|
|
|
|
WriteLitters(Array.Empty<object>());
|
|
WriteAnimals(new[]
|
|
{
|
|
// Same name+dob → enrich, not create
|
|
new { wsCode = "", litterDob = "10.08.2022", name = "Eddie", gender = "male",
|
|
owner = "Anna Beispiel", abgabeDate = "01.10.2022",
|
|
deathDate = "", deathCause = "", farbschlag = "" }
|
|
});
|
|
|
|
var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
|
|
|
Assert.Equal(0, report.Created);
|
|
Assert.Equal(1, await db.Gerbils.CountAsync()); // still exactly one
|
|
|
|
// GoHomeDate was enriched
|
|
var updated = await db.Gerbils.SingleAsync();
|
|
Assert.Equal(new DateOnly(2022, 10, 1), updated.GoHomeDate);
|
|
}
|
|
}
|
|
|
|
// ── Test 5: Deceased status when deathDate set ───────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Execute_sets_Deceased_status_when_deathDate_provided()
|
|
{
|
|
var (db, conn) = NewSqliteDb();
|
|
await using (conn)
|
|
await using (db)
|
|
{
|
|
WriteLitters(Array.Empty<object>());
|
|
WriteAnimals(new[]
|
|
{
|
|
new { wsCode = "WS3", litterDob = "01.01.2022", name = "Ghost", gender = "male",
|
|
owner = "", abgabeDate = "",
|
|
deathDate = "15.06.2022", deathCause = "Tumor", farbschlag = "" }
|
|
});
|
|
|
|
await new ImportDocxService(db, _dir).RunAsync(execute: true);
|
|
|
|
var gerbil = await db.Gerbils.SingleAsync();
|
|
Assert.Equal(GerbilStatus.Deceased, gerbil.Status);
|
|
Assert.Equal(new DateOnly(2022, 6, 15), gerbil.DateOfDeath);
|
|
Assert.Equal("Tumor", gerbil.CauseOfDeath);
|
|
}
|
|
}
|
|
}
|
|
}
|