using System.Net.Http.Json; using System.Text.Json; namespace GerbilManager.Tests; /// /// SEARCH verification — every search variation Julian asked about, against the /// REAL endpoints (SQLite host). Covers: substring/contains, case-insensitivity, /// the no-trailing-* contains fix, separator-insensitive nameSearch (spaces == /// hyphens), umlauts, the OriginBreeder (Herkunft) filter, the /gerbils/breeders /// distinct dropdown source, paged envelope, and a combined status+name filter. /// Each test uses a unique token so the shared class DB doesn't cross-contaminate. /// public class SearchVariationsTests : IClassFixture { private readonly HttpClient _client; public SearchVariationsTests(ApiFactory factory) => _client = factory.CreateClient(); private async Task Create(string name, string? originBreeder = null, string status = "Active") { var resp = await _client.PostAsJsonAsync("/gerbils", new { name, gender = "female", status, originBreeder }); resp.EnsureSuccessStatusCode(); } /// GET /gerbils?filter=... → the list of returned names. private async Task> Names(string filter) { var url = $"/gerbils?filter={Uri.EscapeDataString(filter)}&page=1&pageSize=200"; var doc = JsonDocument.Parse(await _client.GetStringAsync(url)); return doc.RootElement.GetProperty("items").EnumerateArray() .Select(x => x.GetProperty("name").GetString()!).ToList(); } [Fact] public async Task Contains_matches_substring_case_insensitively() { await Create("SuchSchokoEins"); await Create("SuchSchokoladeZwei"); await Create("SuchKeksDrei"); // lowercase query, capitalised names → exercises /i contains var hits = await Names("name=*suchschoko/i"); Assert.Contains("SuchSchokoEins", hits); Assert.Contains("SuchSchokoladeZwei", hits); Assert.DoesNotContain("SuchKeksDrei", hits); } [Fact] public async Task Contains_single_letter_returns_matches_not_the_trailing_star_bug() { // Regression guard for the contains hotfix (0ee2b53): the frontend used to // build name=*VALUE* which Gridify read as literal "VALUE*" → 0 rows. // Correct form name=*VALUE must return every name containing VALUE. await Create("BugGuardAlpha"); await Create("BugGuardBeta"); var hits = await Names("name=*bugguard/i"); Assert.Equal(2, hits.Count(n => n.StartsWith("BugGuard"))); } [Fact] public async Task NameSearch_is_separator_insensitive_spaces_equal_hyphens() { // The headline ask: "clan kleine chaoten" must match "clan-kleine-chaoten". await Create("SepClan-Kleine-Chaoten"); await Create("SepClan Kleine Chaoten"); await Create("SepClanKleineChaoten"); await Create("SepUnrelatedMaus"); // Frontend normalizes the typed term the same way the backend stores it: // lowercase + strip whitespace/.-_ → "sepclankleinechaoten". var hits = await Names("nameSearch=*sepclankleinechaoten"); Assert.Contains("SepClan-Kleine-Chaoten", hits); // hyphens Assert.Contains("SepClan Kleine Chaoten", hits); // spaces Assert.Contains("SepClanKleineChaoten", hits); // none Assert.DoesNotContain("SepUnrelatedMaus", hits); } [Fact] public async Task NameSearch_dotted_and_underscored_separators_also_match() { await Create("SepDot.Von.Den.Chaoten"); await Create("SepDot_Von_Den_Chaoten"); var hits = await Names("nameSearch=*sepdotvondenchaoten"); Assert.Equal(2, hits.Count(n => n.StartsWith("SepDot"))); } [Fact] public async Task NameSearch_handles_umlauts() { await Create("SuchKrümel von den Chaoten"); // nameSearch is stored pre-lowercased; the frontend sends a pre-lowercased // normalized term, so this works without provider unicode case-folding. var hits = await Names("nameSearch=*suchkrümelvondenchaoten"); Assert.Contains("SuchKrümel von den Chaoten", hits); } [Fact] public async Task OriginBreeder_filter_returns_only_that_breeder() { await Create("HerkunftA1", originBreeder: "Zucht HerkunftTest Eins"); await Create("HerkunftA2", originBreeder: "Zucht HerkunftTest Eins"); await Create("HerkunftB1", originBreeder: "Zucht HerkunftTest Zwei"); var hits = await Names("originBreeder==Zucht HerkunftTest Eins"); Assert.Contains("HerkunftA1", hits); Assert.Contains("HerkunftA2", hits); Assert.DoesNotContain("HerkunftB1", hits); } [Fact] public async Task Breeders_endpoint_returns_distinct_nonempty_sorted_values() { await Create("DistinctX1", originBreeder: "ZZZ Distinct Eins"); await Create("DistinctX2", originBreeder: "ZZZ Distinct Eins"); // duplicate breeder await Create("DistinctX3", originBreeder: "ZZZ Distinct Zwei"); await Create("DistinctNoBreeder", originBreeder: null); var list = JsonDocument.Parse(await _client.GetStringAsync("/gerbils/breeders")) .RootElement.EnumerateArray().Select(x => x.GetString()!).ToList(); Assert.Contains("ZZZ Distinct Eins", list); Assert.Contains("ZZZ Distinct Zwei", list); Assert.Equal(1, list.Count(v => v == "ZZZ Distinct Eins")); // de-duplicated Assert.DoesNotContain(list, string.IsNullOrWhiteSpace); // no empty/null Assert.Equal(list.OrderBy(v => v, StringComparer.Ordinal).ToList(), list); // sorted } [Fact] public async Task List_returns_paged_envelope() { var doc = JsonDocument.Parse(await _client.GetStringAsync("/gerbils?page=1&pageSize=5")); var root = doc.RootElement; Assert.True(root.TryGetProperty("items", out _)); Assert.True(root.TryGetProperty("totalCount", out _)); Assert.Equal(1, root.GetProperty("page").GetInt32()); Assert.Equal(5, root.GetProperty("pageSize").GetInt32()); } [Fact] public async Task Combined_status_and_name_filter() { await Create("KombiVerkauf", status: "ForSale"); await Create("KombiAktiv", status: "Active"); var hits = await Names("status==ForSale,nameSearch=*kombi"); Assert.Contains("KombiVerkauf", hits); Assert.DoesNotContain("KombiAktiv", hits); } }