Im Tier-Formular gibt es jetzt den Block „Abstammung" mit Wurf-Auswahl und
Vater-/Mutter-Picker — die Eltern müssen nicht mehr über die Wurf-Seite
gesucht und dort editiert werden. Das Datenmodell bleibt unverändert: Eltern
hängen weiterhin am Geburtswurf.
- Backend: PUT /gerbils/{id}/parents schreibt Litter.FatherId/MotherId des
Geburtswurfs. Ohne Wurf wird ein bestehender mit gleichem Elternpaar +
gleichem Datum verknüpft, sonst ein Träger-Wurf angelegt
("Wurf von X + Y", ShowInChronicle=false, IsManual=true).
Geschlechts-Regel wiederverwendet LitterEndpoints.ValidateParents,
Selbstbezug (Tier als eigener Elternteil) wird abgewiesen.
- UI: Vorbelegung aus dem gewählten Wurf, Warnung mit Anzahl der Geschwister
(Eltern gehören dem Wurf → Änderung gilt für alle), Hinweis wenn ein
Wurf-Eintrag angelegt wird. Texte in de.ts.
- Nebenbei: Speichern nutzt im Edit-Modus die Route-Id (PUT /gerbils/{id}
antwortet 204 ohne Body) und der vorher schon rote Spec-Locator
„Würfe als Elternteil" ist auf den Abschnitt eingegrenzt.
- Tests: GerbilParentsTests (9), e2e tiere.spec (2 neu) + Mock-Route.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
189 lines
8.0 KiB
C#
189 lines
8.0 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace GerbilManager.Tests;
|
|
|
|
/// <summary>
|
|
/// QOL-ELTERN: PUT /gerbils/{id}/parents — Eltern direkt an der Tier-Akte pflegen.
|
|
/// Das Datenmodell bleibt unverändert (Eltern hängen am Geburtswurf), der Endpoint
|
|
/// schreibt nur dorthin bzw. legt den Träger-Wurf an, wenn es noch keinen gibt.
|
|
/// </summary>
|
|
public class GerbilParentsTests : IClassFixture<ApiFactory>
|
|
{
|
|
private readonly HttpClient _client;
|
|
public GerbilParentsTests(ApiFactory factory) => _client = factory.CreateClient();
|
|
|
|
private async Task<string> CreateGerbil(string name, string gender, string? dob = null, string? litterId = null)
|
|
{
|
|
var resp = await _client.PostAsJsonAsync("/gerbils", new
|
|
{
|
|
name,
|
|
gender,
|
|
dateOfBirth = dob,
|
|
litterId,
|
|
});
|
|
resp.EnsureSuccessStatusCode();
|
|
return JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement
|
|
.GetProperty("id").GetString()!;
|
|
}
|
|
|
|
private async Task<string> CreateLitter(string name, string date, string? fatherId = null, string? motherId = null)
|
|
{
|
|
var resp = await _client.PostAsJsonAsync("/litters", new { name, date, fatherId, motherId });
|
|
resp.EnsureSuccessStatusCode();
|
|
return JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement
|
|
.GetProperty("id").GetString()!;
|
|
}
|
|
|
|
private async Task<JsonElement> SetParents(string gerbilId, string? fatherId, string? motherId)
|
|
{
|
|
var resp = await _client.PutAsJsonAsync($"/gerbils/{gerbilId}/parents", new { fatherId, motherId });
|
|
resp.EnsureSuccessStatusCode();
|
|
return JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
|
|
}
|
|
|
|
private async Task<JsonElement> GetLitter(string litterId) =>
|
|
JsonDocument.Parse(await _client.GetStringAsync($"/litters/{litterId}")).RootElement;
|
|
|
|
private static string? Str(JsonElement el, string prop) =>
|
|
el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
|
|
|
[Fact]
|
|
public async Task Parents_of_existing_litter_are_written_to_that_litter()
|
|
{
|
|
var father = await CreateGerbil("Vater A", "male", "2024-01-01");
|
|
var mother = await CreateGerbil("Mutter A", "female", "2024-01-01");
|
|
var litterId = await CreateLitter("A-Wurf", "2025-03-01");
|
|
var pup = await CreateGerbil("Jungtier A", "female", "2025-03-01", litterId);
|
|
|
|
var result = await SetParents(pup, father, mother);
|
|
|
|
Assert.Equal(litterId, Str(result, "litterId"));
|
|
Assert.False(result.GetProperty("litterCreated").GetBoolean());
|
|
Assert.False(result.GetProperty("litterAttached").GetBoolean());
|
|
var litter = await GetLitter(litterId);
|
|
Assert.Equal(father, Str(litter, "fatherId"));
|
|
Assert.Equal(mother, Str(litter, "motherId"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SiblingCount_reports_the_other_pups_the_change_applies_to()
|
|
{
|
|
var litterId = await CreateLitter("B-Wurf", "2025-04-01");
|
|
var pup = await CreateGerbil("Jungtier B1", "female", "2025-04-01", litterId);
|
|
await CreateGerbil("Jungtier B2", "male", "2025-04-01", litterId);
|
|
await CreateGerbil("Jungtier B3", "male", "2025-04-01", litterId);
|
|
var mother = await CreateGerbil("Mutter B", "female", "2024-02-01");
|
|
|
|
var result = await SetParents(pup, null, mother);
|
|
|
|
Assert.Equal(2, result.GetProperty("siblingCount").GetInt32());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Gerbil_without_litter_gets_a_hidden_carrier_litter()
|
|
{
|
|
var father = await CreateGerbil("Vater C", "male", "2024-03-01");
|
|
var mother = await CreateGerbil("Mutter C", "female", "2024-03-01");
|
|
var orphan = await CreateGerbil("Waise C", "female", "2025-05-01");
|
|
|
|
var result = await SetParents(orphan, father, mother);
|
|
|
|
Assert.True(result.GetProperty("litterCreated").GetBoolean());
|
|
var litterId = Str(result, "litterId")!;
|
|
Assert.Equal("Wurf von Vater C + Mutter C", Str(result, "litterName"));
|
|
|
|
var litter = await GetLitter(litterId);
|
|
Assert.Equal(father, Str(litter, "fatherId"));
|
|
Assert.Equal(mother, Str(litter, "motherId"));
|
|
// Nur zur Abstammung — der Hilfs-Wurf darf die Wurfchronik nicht zumüllen.
|
|
Assert.False(litter.GetProperty("showInChronicle").GetBoolean());
|
|
// Wurfdatum = Geburtsdatum des Tiers.
|
|
Assert.Equal("2025-05-01", Str(litter, "date"));
|
|
|
|
// Das Tier hängt jetzt an diesem Wurf.
|
|
var gerbil = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{orphan}")).RootElement;
|
|
Assert.Equal(litterId, Str(gerbil, "litterId"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Same_parents_and_same_dob_attach_to_the_existing_litter_instead_of_duplicating()
|
|
{
|
|
var father = await CreateGerbil("Vater D", "male", "2024-04-01");
|
|
var mother = await CreateGerbil("Mutter D", "female", "2024-04-01");
|
|
var first = await CreateGerbil("Waise D1", "female", "2025-06-01");
|
|
var second = await CreateGerbil("Waise D2", "male", "2025-06-01");
|
|
|
|
var created = await SetParents(first, father, mother);
|
|
Assert.True(created.GetProperty("litterCreated").GetBoolean());
|
|
|
|
var attached = await SetParents(second, father, mother);
|
|
Assert.False(attached.GetProperty("litterCreated").GetBoolean());
|
|
Assert.True(attached.GetProperty("litterAttached").GetBoolean());
|
|
// Beide sind jetzt Geschwister im selben Wurf.
|
|
Assert.Equal(Str(created, "litterId"), Str(attached, "litterId"));
|
|
Assert.Equal(1, attached.GetProperty("siblingCount").GetInt32());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Clearing_parents_nulls_them_on_the_litter()
|
|
{
|
|
var father = await CreateGerbil("Vater E", "male", "2024-05-01");
|
|
var litterId = await CreateLitter("E-Wurf", "2025-07-01", father);
|
|
var pup = await CreateGerbil("Jungtier E", "female", "2025-07-01", litterId);
|
|
|
|
await SetParents(pup, null, null);
|
|
|
|
var litter = await GetLitter(litterId);
|
|
Assert.Equal(JsonValueKind.Null, litter.GetProperty("fatherId").ValueKind);
|
|
Assert.Equal(JsonValueKind.Null, litter.GetProperty("motherId").ValueKind);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task No_litter_and_no_parents_is_a_no_op()
|
|
{
|
|
var orphan = await CreateGerbil("Waise F", "female", "2025-08-01");
|
|
|
|
var result = await SetParents(orphan, null, null);
|
|
|
|
Assert.Equal(JsonValueKind.Null, result.GetProperty("litterId").ValueKind);
|
|
Assert.False(result.GetProperty("litterCreated").GetBoolean());
|
|
var gerbil = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{orphan}")).RootElement;
|
|
Assert.Equal(JsonValueKind.Null, gerbil.GetProperty("litterId").ValueKind);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wrong_gender_parent_returns_400_InvalidParentGender()
|
|
{
|
|
var female = await CreateGerbil("Weibchen G", "female", "2024-06-01");
|
|
var pup = await CreateGerbil("Jungtier G", "male", "2025-09-01");
|
|
|
|
var resp = await _client.PutAsJsonAsync($"/gerbils/{pup}/parents",
|
|
new { fatherId = female, motherId = (string?)null });
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
|
Assert.Contains("InvalidParentGender", await resp.Content.ReadAsStringAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Self_as_parent_returns_400()
|
|
{
|
|
var g = await CreateGerbil("Selbstbezug H", "male", "2025-10-01");
|
|
|
|
var resp = await _client.PutAsJsonAsync($"/gerbils/{g}/parents",
|
|
new { fatherId = g, motherId = (string?)null });
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unknown_gerbil_returns_404()
|
|
{
|
|
var resp = await _client.PutAsJsonAsync($"/gerbils/{Guid.NewGuid()}/parents",
|
|
new { fatherId = (string?)null, motherId = (string?)null });
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
|
}
|
|
}
|