using System.Net; using System.Net.Http.Json; using System.Text.Json; using GerbilManagerWebAPI.Import; using GerbilManagerWebAPI.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace GerbilManager.Tests; /// /// EXHIBITION: show/exhibition results & awards per animal (/exhibitions). /// - POST creates a row; GET (optionally filtered by gerbilId) returns it, newest first. /// - PUT edits, DELETE removes; unknown ids return 404; an empty EventName is rejected (400). /// - CRITICAL: exhibition rows survive the import re-ingest wipe (loose, FK-free GerbilId). /// public class ExhibitionEndpointTests : IClassFixture { private readonly ApiFactory _factory; public ExhibitionEndpointTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Post_creates_and_get_filters_by_gerbilId() { var client = _factory.CreateClient(); var gerbilId = Guid.NewGuid(); var resp = await client.PostAsJsonAsync("/exhibitions", new { gerbilId, entityName = "Krümel", eventName = "Nationale Rennmausschau 2026", date = "2026-03-15T00:00:00Z", placement = "1. Platz", award = "Best in Show", note = "Sehr ausgeglichenes Tier.", }); Assert.Equal(HttpStatusCode.Created, resp.StatusCode); var created = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement; var id = created.GetProperty("id").GetString(); Assert.False(string.IsNullOrEmpty(id)); Assert.Equal("Nationale Rennmausschau 2026", created.GetProperty("eventName").GetString()); Assert.Equal("1. Platz", created.GetProperty("placement").GetString()); Assert.Equal("Best in Show", created.GetProperty("award").GetString()); Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString()); // GET filtered to this gerbil returns the row. var listed = JsonDocument.Parse(await client.GetStringAsync($"/exhibitions?gerbilId={gerbilId}")).RootElement; Assert.Contains(listed.EnumerateArray(), x => x.GetProperty("id").GetString() == id && x.GetProperty("eventName").GetString() == "Nationale Rennmausschau 2026"); // GET filtered to a DIFFERENT gerbil does not. var other = JsonDocument.Parse(await client.GetStringAsync($"/exhibitions?gerbilId={Guid.NewGuid()}")).RootElement; Assert.DoesNotContain(other.EnumerateArray(), x => x.GetProperty("id").GetString() == id); } [Fact] public async Task Crud_lifecycle_edit_and_delete() { var client = _factory.CreateClient(); var create = await client.PostAsJsonAsync("/exhibitions", new { eventName = "Lokalschau", entityName = "Balu", }); Assert.Equal(HttpStatusCode.Created, create.StatusCode); var id = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement.GetProperty("id").GetString(); // Edit: change placement + add a date. var edit = await client.PutAsJsonAsync($"/exhibitions/{id}", new { placement = "2. Platz", date = "2025-11-01T00:00:00Z", }); Assert.Equal(HttpStatusCode.OK, edit.StatusCode); var edited = JsonDocument.Parse(await edit.Content.ReadAsStringAsync()).RootElement; Assert.Equal("2. Platz", edited.GetProperty("placement").GetString()); Assert.Equal("Lokalschau", edited.GetProperty("eventName").GetString()); // Delete -> 204, then 404 on subsequent edit/delete. var del = await client.DeleteAsync($"/exhibitions/{id}"); Assert.Equal(HttpStatusCode.NoContent, del.StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await client.DeleteAsync($"/exhibitions/{id}")).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await client.PutAsJsonAsync($"/exhibitions/{id}", new { eventName = "x" })).StatusCode); } [Fact] public async Task Post_rejects_empty_event_name() { var client = _factory.CreateClient(); var resp = await client.PostAsJsonAsync("/exhibitions", new { eventName = " " }); Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); } [Fact] public async Task Put_and_delete_unknown_id_return_404() { var client = _factory.CreateClient(); var missing = Guid.NewGuid(); Assert.Equal(HttpStatusCode.NotFound, (await client.PutAsJsonAsync($"/exhibitions/{missing}", new { eventName = "x" })).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await client.DeleteAsync($"/exhibitions/{missing}")).StatusCode); } [Fact] public async Task Exhibition_survives_ingest_wipe() { // Fresh in-memory DB seeded with a resolved import file (mirrors IngestResolvedServiceTests). var dir = Path.Combine(Path.GetTempPath(), "exhibition-ingest-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); try { var contactId = Guid.NewGuid(); var fatherId = Guid.NewGuid(); var motherId = Guid.NewGuid(); var litterId = Guid.NewGuid(); var data = new { Contacts = new[] { new { Id = contactId, Name = "Test Breeder", Email = "t@e.de", Phone = "", Address = "", Notes = (string?)null, IsBreeder = true, IsReceiver = false, NameSuffix = (string?)null, Provenance = (string?)null } }, Litters = new[] { new { Id = litterId, Name = "Wurf A", Date = "2026-01-01", TotalBorn = 5, DeathsWithin8Weeks = 0, FatherId = fatherId, MotherId = motherId, ExpectedGoHomeDate = (string?)null, Notes = "", PairingCode = "PC01", ExternalRef = "ext-litter-1", LitterLetter = "A" } }, Gerbils = new[] { Animal(fatherId, "Papa", "male", contactId), Animal(motherId, "Mama", "female", contactId), }, GerbilPhotos = Array.Empty(), }; File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(data)); var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase("exhibition-ingest-" + Guid.NewGuid().ToString("N")) .Options; using var db = new ApplicationContext(opts); db.Database.EnsureCreated(); // An exhibition result referencing the gerbil that the wipe will delete/recreate. var exhId = Guid.NewGuid(); db.ExhibitionResults.Add(new ExhibitionResult { Id = exhId, GerbilId = fatherId, EntityName = "Papa", EventName = "Nationale Rennmausschau 2026", Date = new DateTime(2026, 3, 15, 0, 0, 0, DateTimeKind.Utc), Placement = "1. Platz", Award = "Best in Show", Note = "Tolles Tier.", CreatedAt = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "Import:SourcePath", dir } }) .Build(); // Run the ingest wipe + reload. var result = await new IngestResolvedService(db, config, null!).RunAsync(); Assert.Contains("Ingestion successful!", result); // Gerbils/litters/contacts were wiped & re-created, but the exhibition row is untouched. var survivor = await db.ExhibitionResults.SingleAsync(x => x.Id == exhId); Assert.Equal(fatherId, survivor.GerbilId); // loose id preserved even though the gerbil row was deleted/recreated Assert.Equal("Papa", survivor.EntityName); Assert.Equal("Nationale Rennmausschau 2026", survivor.EventName); Assert.Equal("1. Platz", survivor.Placement); Assert.Equal("Best in Show", survivor.Award); Assert.Equal(1, await db.ExhibitionResults.CountAsync()); } finally { try { Directory.Delete(dir, recursive: true); } catch { /* best effort */ } } } private static object Animal(Guid id, string name, string gender, Guid contactId) => new { Id = id, Name = name, Gender = gender, Status = "Breeding", LitterId = (Guid?)null, OriginContactId = contactId, ReceiverContactId = (Guid?)null, EnclosureId = (Guid?)null, ColorVarietyId = new Guid("00000000-0000-0000-0000-000000000006"), 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-" + name, RawImportData = "{}", OriginBreeder = "Test Zucht", NameSearch = name.ToLowerInvariant(), CharacterTraits = Array.Empty(), CharacterNote = (string?)null, IsDeaf = false, IsResident = true, }; }