From 3716dcb1f3883c5593f4bc6f8c75a7118a2a2269 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 09:33:40 +0200 Subject: [PATCH] =?UTF-8?q?test:=20SearchVariationsTests=20=E2=80=94=209?= =?UTF-8?q?=20search=20variations=20(contains,=20case-insensitive,=20separ?= =?UTF-8?q?ator-insensitive=20nameSearch,=20umlauts,=20OriginBreeder=20fil?= =?UTF-8?q?ter,=20/breeders=20distinct,=20paging,=20combined).=20Fills=20t?= =?UTF-8?q?he=20gap=20where=20SEARCH-1's=20tests=20never=20reached=20main.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GerbilManager.Tests/SearchVariationsTests.cs | 150 +++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 GerbilManager.Tests/SearchVariationsTests.cs diff --git a/GerbilManager.Tests/SearchVariationsTests.cs b/GerbilManager.Tests/SearchVariationsTests.cs new file mode 100644 index 0000000..2619c99 --- /dev/null +++ b/GerbilManager.Tests/SearchVariationsTests.cs @@ -0,0 +1,150 @@ +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); + } +}