CR-9 (major): gidByNameDob TryGetValue + ExternalRef-Fallback Verhindert KeyNotFoundException wenn Name/DOB zwischen zwei Laeufen driftet (z.B. correctDob-Remap oder UI-Umbenennung). Fallback: ExternalRef-Dict-Lookup; bei Miss: sauberes Ueberspringen + Note statt 500. +Test CR9_NameDOB_drift. CR-11 (major): Farbschlag aus Genotyp ableiten (fill-NULL-only) Deep-Band-Tiere (gen>=2, kein Farbschlag-Feld) landen nicht laenger mit null ColorVariety. GenotypePotentiallyMatches() vergleicht locus-pair-weise (??=wildcard, case-insensitive). Nur vollstaendige Genotypen (8 Loci, kein ??) loesen Ableitung aus. Plan-Loop: fuellt colorVarietyId bei null + vollstaendigem Genotyp. Post-Sweep: bestehende DB-Tiere mit null ColorVarietyId werden nachgefuellt. AnimalSummary.FarbschlagDerivedFromGenotype = Zaehler. +Test CR11_ColorVariety_from_geno. CR-10 (major, Python): malformed Override-Genotyp wird nicht angewendet apply_conflict_decisions validiert mapped8locus nach gt.parse(). Leeres Ergebnis = Genotyp unveraendert + decisionWarning statt stillem Blanken. Konflikt wird trotzdem aufgeloest (Entscheidung gilt, nur Genotyp-Override ausgelassen). +5 Python-Tests (CR-10-Block in test_extract.py). DB-1 (high): filtered unique index auf Gerbil.ExternalRef WHERE ExternalRef IS NOT NULL — verhindert doppelten Import bei Race-Conditions oder Lauf-Ueberschneidungen. Migration UniqueExternalRef. SQLite-Testhost: HasFilter() wird via EnsureCreated appliziert (SQLite unterstuetzt Partial-Indexes). PartialUpdateTests externalRef-Assertion auf NotNull geaendert (name-hash unique). GATE: 139/139 C# + Python ALL PASS, ef has-pending=No.
146 lines
6.7 KiB
C#
146 lines
6.7 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace GerbilManager.Tests;
|
|
|
|
/// <summary>CR-2 regression: PUT /gerbils/{id} must not silently destroy fields that
|
|
/// the caller omits (PATCH semantics). The ForSale-toggle and Charakterbogen-save
|
|
/// send partial bodies — all other fields must be preserved unchanged.</summary>
|
|
public class PartialUpdateTests : IClassFixture<ApiFactory>
|
|
{
|
|
private readonly HttpClient _client;
|
|
|
|
public PartialUpdateTests(ApiFactory factory) => _client = factory.CreateClient();
|
|
|
|
// ------------------------------------------------------------------ helpers
|
|
|
|
private static Guid ExtractId(string json) =>
|
|
Guid.Parse(Regex.Match(json, "\"id\":\"([^\"]+)\"").Groups[1].Value);
|
|
|
|
private static string? GetStr(JsonElement el, string prop) =>
|
|
el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String
|
|
? v.GetString() : null;
|
|
|
|
private static string? GetNested(JsonElement el, string prop) =>
|
|
el.TryGetProperty(prop, out var v) ? v.ToString() : null;
|
|
|
|
/// <summary>Create a fully-populated gerbil and return its id.</summary>
|
|
private async Task<Guid> CreateFullGerbil(string name = "TestTier")
|
|
{
|
|
// Use name-derived unique externalRef so the DB-1 unique constraint doesn't fire
|
|
// when multiple tests in the same fixture share the SQLite connection.
|
|
var resp = await _client.PostAsync("/gerbils", JsonContent.Create(new
|
|
{
|
|
name,
|
|
gender = "female",
|
|
genotype = "aa CC DD ee GG PP spsp rere",
|
|
originBreeder = "Zucht der Kleinen Chaoten",
|
|
isDeaf = false,
|
|
characterTraits = new[] { "neugierig", "zutraulich" },
|
|
characterNote = "Liebling der Familie",
|
|
notes = "Eine Notiz",
|
|
externalRef = $"ext-{name.GetHashCode():X8}",
|
|
}));
|
|
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
|
|
return ExtractId(await resp.Content.ReadAsStringAsync());
|
|
}
|
|
|
|
// ------------------------------------------------------------------ tests
|
|
|
|
[Fact]
|
|
public async Task ForSale_toggle_does_not_wipe_other_fields()
|
|
{
|
|
// Arrange: create a fully-populated gerbil
|
|
var id = await CreateFullGerbil("ForSale-Tier");
|
|
|
|
// Act: send only { status: 'ForSale' } — exactly what the detail page does
|
|
var put = await _client.PutAsync($"/gerbils/{id}",
|
|
JsonContent.Create(new { status = "ForSale" }));
|
|
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
|
|
|
|
// Assert: re-read and check all fields survive
|
|
var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement;
|
|
|
|
Assert.Equal("ForSale", GetStr(json, "status"));
|
|
Assert.Equal("ForSale-Tier", GetStr(json, "name"));
|
|
Assert.Equal("female", GetStr(json, "gender"));
|
|
Assert.Equal("aa CC DD ee GG PP spsp rere", GetStr(json, "genotype"));
|
|
Assert.Equal("Zucht der Kleinen Chaoten", GetStr(json, "originBreeder"));
|
|
Assert.Equal("Eine Notiz", GetStr(json, "notes"));
|
|
Assert.NotNull(GetStr(json, "externalRef")); // externalRef set during create, not cleared by partial PUT
|
|
Assert.Contains("neugierig", GetNested(json, "characterTraits") ?? "");
|
|
Assert.Equal("Liebling der Familie", GetStr(json, "characterNote"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CharacterSave_does_not_wipe_other_fields()
|
|
{
|
|
// Arrange
|
|
var id = await CreateFullGerbil("Charakter-Tier");
|
|
|
|
// Act: send only { characterTraits, characterNote } — exactly what the Charakterbogen save does
|
|
var put = await _client.PutAsync($"/gerbils/{id}", JsonContent.Create(new
|
|
{
|
|
characterTraits = new[] { "handzahm" },
|
|
characterNote = "Neue Notiz",
|
|
}));
|
|
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
|
|
|
|
// Assert: name / genotype / originBreeder / status / externalRef all intact
|
|
var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement;
|
|
|
|
Assert.Equal("Active", GetStr(json, "status"));
|
|
Assert.Equal("Charakter-Tier", GetStr(json, "name"));
|
|
Assert.Equal("female", GetStr(json, "gender"));
|
|
Assert.Equal("aa CC DD ee GG PP spsp rere", GetStr(json, "genotype"));
|
|
Assert.Equal("Zucht der Kleinen Chaoten", GetStr(json, "originBreeder"));
|
|
Assert.Equal("Eine Notiz", GetStr(json, "notes"));
|
|
Assert.NotNull(GetStr(json, "externalRef")); // externalRef set during create, not cleared by partial PUT
|
|
Assert.Contains("handzahm", GetNested(json, "characterTraits") ?? "");
|
|
Assert.Equal("Neue Notiz", GetStr(json, "characterNote"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditForm_omitting_import_fields_keeps_them()
|
|
{
|
|
// Arrange: gerbil with import-set fields
|
|
var id = await CreateFullGerbil("EditForm-Tier");
|
|
|
|
// Act: send a typical edit-form body that omits originBreeder/isDeaf/externalRef/characterTraits/characterNote
|
|
var put = await _client.PutAsync($"/gerbils/{id}", JsonContent.Create(new
|
|
{
|
|
name = "EditForm-Tier (umbenannt)",
|
|
gender = "female",
|
|
genotype = "aa CC DD ee GG PP spsp rere",
|
|
notes = "Aktualisierte Notiz",
|
|
}));
|
|
Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
|
|
|
|
// Assert: name updated, import fields survive
|
|
var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement;
|
|
|
|
Assert.Equal("EditForm-Tier (umbenannt)", GetStr(json, "name"));
|
|
Assert.Equal("Aktualisierte Notiz", GetStr(json, "notes"));
|
|
Assert.Equal("Zucht der Kleinen Chaoten", GetStr(json, "originBreeder"));
|
|
Assert.NotNull(GetStr(json, "externalRef")); // externalRef set during create, not cleared by partial PUT
|
|
Assert.Contains("neugierig", GetNested(json, "characterTraits") ?? "");
|
|
Assert.Equal("Liebling der Familie", GetStr(json, "characterNote"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_with_minimal_body_uses_safe_defaults()
|
|
{
|
|
// A minimal POST (only name) must still produce a valid gerbil
|
|
var resp = await _client.PostAsync("/gerbils", JsonContent.Create(new { name = "Minimal" }));
|
|
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
|
|
|
|
var json = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
|
|
Assert.Equal("Minimal", GetStr(json, "name"));
|
|
Assert.Equal("Active", GetStr(json, "status"));
|
|
// characterTraits defaults to [] on create when not provided
|
|
Assert.Contains("[]", GetNested(json, "characterTraits") ?? "");
|
|
}
|
|
}
|