feat(ruecknahmen): zurückgenommene Tiere erfassen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
208
GerbilManager.Tests/ReturnRecordEndpointTests.cs
Normal file
208
GerbilManager.Tests/ReturnRecordEndpointTests.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// RÜCKNAHMEN (getback_tb): zurückgekommene/zurückgenommene Tiere.
|
||||
/// - Voller CRUD-Round-Trip (POST/GET[?gerbilId=]/PUT/DELETE).
|
||||
/// - Validierung: ohne Tier (weder GerbilId noch GerbilName) → 400.
|
||||
/// - CRITICAL: Rücknahme-Zeilen überleben den Import-Re-Ingest-Wipe
|
||||
/// (lose, FK-freie GerbilId/FromContactId — wie Feedback).
|
||||
/// </summary>
|
||||
public class ReturnRecordEndpointTests : IClassFixture<ApiFactory>
|
||||
{
|
||||
private readonly ApiFactory _factory;
|
||||
public ReturnRecordEndpointTests(ApiFactory factory) => _factory = factory;
|
||||
|
||||
[Fact]
|
||||
public async Task Crud_round_trip_create_list_update_delete()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var gerbilId = Guid.NewGuid();
|
||||
var contactId = Guid.NewGuid();
|
||||
|
||||
// POST
|
||||
var resp = await client.PostAsJsonAsync("/returns", new
|
||||
{
|
||||
gerbilId,
|
||||
gerbilName = "Krümel",
|
||||
returnDate = "2026-05-01T00:00:00Z",
|
||||
returnPrice = 0m,
|
||||
originalPrice = 15m,
|
||||
originalSaleDate = "2025-09-01T00:00:00Z",
|
||||
fromContactId = contactId,
|
||||
fromContactName = "Familie Meier",
|
||||
note = "Allergie in der Familie",
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
|
||||
var created = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
|
||||
var id = created.GetProperty("id").GetString()!;
|
||||
Assert.Equal("Krümel", created.GetProperty("gerbilName").GetString());
|
||||
Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString());
|
||||
Assert.Equal("Familie Meier", created.GetProperty("fromContactName").GetString());
|
||||
|
||||
// GET (unfiltered) returns it
|
||||
var listed = JsonDocument.Parse(await client.GetStringAsync("/returns")).RootElement;
|
||||
Assert.Contains(listed.EnumerateArray(), r => r.GetProperty("id").GetString() == id);
|
||||
|
||||
// GET ?gerbilId= filters
|
||||
var filtered = JsonDocument.Parse(
|
||||
await client.GetStringAsync($"/returns?gerbilId={gerbilId}")).RootElement;
|
||||
Assert.All(filtered.EnumerateArray(),
|
||||
r => Assert.Equal(gerbilId.ToString(), r.GetProperty("gerbilId").GetString()));
|
||||
Assert.Contains(filtered.EnumerateArray(), r => r.GetProperty("id").GetString() == id);
|
||||
|
||||
var other = JsonDocument.Parse(
|
||||
await client.GetStringAsync($"/returns?gerbilId={Guid.NewGuid()}")).RootElement;
|
||||
Assert.Empty(other.EnumerateArray());
|
||||
|
||||
// PUT updates
|
||||
var putResp = await client.PutAsJsonAsync($"/returns/{id}", new
|
||||
{
|
||||
gerbilId,
|
||||
gerbilName = "Krümel",
|
||||
returnDate = "2026-05-02T00:00:00Z",
|
||||
returnPrice = 5m,
|
||||
note = "Korrigierter Grund",
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.OK, putResp.StatusCode);
|
||||
var updated = JsonDocument.Parse(await putResp.Content.ReadAsStringAsync()).RootElement;
|
||||
Assert.Equal("Korrigierter Grund", updated.GetProperty("note").GetString());
|
||||
Assert.Equal(5m, updated.GetProperty("returnPrice").GetDecimal());
|
||||
|
||||
// DELETE removes
|
||||
var delResp = await client.DeleteAsync($"/returns/{id}");
|
||||
Assert.Equal(HttpStatusCode.NoContent, delResp.StatusCode);
|
||||
var afterDelete = JsonDocument.Parse(await client.GetStringAsync("/returns")).RootElement;
|
||||
Assert.DoesNotContain(afterDelete.EnumerateArray(), r => r.GetProperty("id").GetString() == id);
|
||||
|
||||
// DELETE again → 404
|
||||
var delAgain = await client.DeleteAsync($"/returns/{id}");
|
||||
Assert.Equal(HttpStatusCode.NotFound, delAgain.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_rejects_record_without_animal()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
var resp = await client.PostAsJsonAsync("/returns", new { note = "irgendwas" });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_unknown_id_returns_404()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
var resp = await client.PutAsJsonAsync($"/returns/{Guid.NewGuid()}", new { gerbilName = "X" });
|
||||
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnRecord_survives_ingest_wipe()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "returns-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<object>(),
|
||||
};
|
||||
File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(data));
|
||||
|
||||
var opts = new DbContextOptionsBuilder<ApplicationContext>()
|
||||
.UseInMemoryDatabase("returns-ingest-" + Guid.NewGuid().ToString("N"))
|
||||
.Options;
|
||||
using var db = new ApplicationContext(opts);
|
||||
db.Database.EnsureCreated();
|
||||
|
||||
// A return record referencing the gerbil + contact that the wipe will delete.
|
||||
var recordId = Guid.NewGuid();
|
||||
db.ReturnRecords.Add(new ReturnRecord
|
||||
{
|
||||
Id = recordId,
|
||||
GerbilId = fatherId,
|
||||
GerbilName = "Papa",
|
||||
ReturnDate = new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
ReturnPrice = 0m,
|
||||
FromContactId = contactId,
|
||||
FromContactName = "Test Breeder",
|
||||
Note = "kam zurück",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?> { { "Import:SourcePath", dir } })
|
||||
.Build();
|
||||
|
||||
var result = await new IngestResolvedService(db, config, null!).RunAsync();
|
||||
Assert.Contains("Ingestion successful!", result);
|
||||
|
||||
// The gerbils/contacts were wiped & recreated, but the return record is untouched.
|
||||
var survivor = await db.ReturnRecords.SingleAsync(r => r.Id == recordId);
|
||||
Assert.Equal(fatherId, survivor.GerbilId); // loose id preserved even though the gerbil row was deleted/recreated
|
||||
Assert.Equal(contactId, survivor.FromContactId);
|
||||
Assert.Equal("Papa", survivor.GerbilName);
|
||||
Assert.Equal("Test Breeder", survivor.FromContactName);
|
||||
Assert.Equal(1, await db.ReturnRecords.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<string>(),
|
||||
CharacterNote = (string?)null,
|
||||
IsDeaf = false,
|
||||
IsResident = true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user