Files
GerbilManager/GerbilManager.Tests/ImportDocxServiceTests.cs
Gulum 8c43a8dc03 FEAT-8d-CREATE: docx importer CREATE path for ~194 abgegebene Jungtiere
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>
2026-06-06 23:13:21 +02:00

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);
}
}
}
}