using System.Net; using System.Text; using System.Text.Json; using GerbilManagerWebAPI.Names; using GerbilManagerWebAPI.SaleAd; using Microsoft.Extensions.Options; namespace GerbilManager.Tests { /// /// FEAT-NAMEGEN / NAMEGEN-2-BE: NameSuggestionService — prompt assembly, usage-code mapping, /// JSON parse (incl. Markdown fence strip), 503-not-configured path, upstream-error path. /// public class NameSuggestionTests { // ── Prompt assembly ─────────────────────────────────────────────────── [Fact] public void SystemPrompt_verlangt_reines_JSON_ohne_Erklärungen() { var prompt = NameSuggestionService.BuildSystemPrompt(); Assert.Contains("reinen JSON-Array", prompt); Assert.Contains("KEINE Markdown-Code-Blöcke", prompt); Assert.Contains("name", prompt); Assert.Contains("meaning", prompt); Assert.Contains("origin", prompt); } [Fact] public void SystemPrompt_erklärt_thematische_Kategorien() { var prompt = NameSuggestionService.BuildSystemPrompt(); Assert.Contains("thematischen Kategorien", prompt); Assert.Contains("Erfinde KEINE Etymologie", prompt); } [Fact] public void UserPrompt_enthält_Anzahl_und_Anfangsbuchstaben() { var prompt = NameSuggestionService.BuildUserPrompt("A", "female", "norn,mythg", 6); Assert.Contains("6", prompt); Assert.Contains("\"A\"", prompt); Assert.Contains("weibliche", prompt); // codes are mapped to German descriptions Assert.Contains("Nordische/Altnordische Etymologie", prompt); Assert.Contains("Griechische Mythologie", prompt); Assert.Contains("Kulturkreisen", prompt); } [Fact] public void UserPrompt_ohne_optionale_Parameter_ist_gültig() { var prompt = NameSuggestionService.BuildUserPrompt(null, null, null, 5); Assert.Contains("5", prompt); Assert.DoesNotContain("Buchstaben", prompt); Assert.DoesNotContain("weibliche", prompt); Assert.DoesNotContain("männliche", prompt); } [Fact] public void UserPrompt_gender_any_wird_nicht_im_Prompt_erwähnt() { var prompt = NameSuggestionService.BuildUserPrompt(null, "any", null, 3); Assert.DoesNotContain("weibliche", prompt); Assert.DoesNotContain("männliche", prompt); } // ── NAMEGEN-2-BE: neue Themen-Kategorien ───────────────────────────── [Theory] [InlineData("disney", "Disney-Charaktere")] [InlineData("pokemon", "Pokémon-Namen")] [InlineData("encities", "Namen englischer Städte")] [InlineData("hrcities", "Namen kroatischer Städte")] [InlineData("usstates", "Namen von US-Bundesstaaten")] public void UsageMap_enthält_alle_fünf_neuen_Codes(string code, string expectedDescription) { Assert.True(NameSuggestionService.UsageMap.TryGetValue(code, out var entry)); Assert.Equal(expectedDescription, entry.Description); Assert.True(entry.Thematic); } [Theory] [InlineData("disney", "Disney-Charaktere")] [InlineData("pokemon", "Pokémon-Namen")] [InlineData("encities", "Namen englischer Städte")] [InlineData("hrcities", "Namen kroatischer Städte")] [InlineData("usstates", "Namen von US-Bundesstaaten")] public void UserPrompt_enthält_Themen_Kategorie_Beschreibung(string code, string expectedDescription) { var prompt = NameSuggestionService.BuildUserPrompt(null, null, code, 5); Assert.Contains(expectedDescription, prompt); Assert.Contains("Themen-Kategorien", prompt); Assert.DoesNotContain("Kulturkreisen", prompt); } [Fact] public void UserPrompt_trennt_etym_und_thematische_Kategorien() { var prompt = NameSuggestionService.BuildUserPrompt("D", null, "norn,disney", 4); Assert.Contains("Nordische/Altnordische Etymologie", prompt); Assert.Contains("Kulturkreisen", prompt); Assert.Contains("Disney-Charaktere", prompt); Assert.Contains("Themen-Kategorien", prompt); } [Fact] public void UserPrompt_thematisch_enthält_Geschlecht_Hinweis() { var prompt = NameSuggestionService.BuildUserPrompt(null, "female", "encities", 3); Assert.Contains("Geschlecht-Filter kann ignoriert werden", prompt); } [Fact] public async Task SuggestAsync_parst_thematische_Kategorie_Antwort() { var payload = """[{"name":"Dumbo","meaning":"Elefant mit großen Ohren aus dem Disney-Film Dumbo (1941)","origin":"Disney"}]"""; var handler = new StubHandler(_ => Canned(payload)); var service = CreateService("https://api.example.com/v1", "k", "m", handler); var result = await service.SuggestAsync("D", null, "disney", 1); Assert.Equal(NameSuggestionStatus.Ok, result.Status); Assert.NotNull(result.Suggestions); Assert.Equal("Dumbo", result.Suggestions![0].Name); Assert.Equal("Disney", result.Suggestions[0].Origin); } // ── JSON parsing ────────────────────────────────────────────────────── [Fact] public void ParseSuggestions_verarbeitet_reines_JSON() { var json = """[{"name":"Astrid","meaning":"göttliche Stärke","origin":"Altnordisch"}]"""; var result = NameSuggestionService.ParseSuggestions(json); Assert.NotNull(result); Assert.Single(result); Assert.Equal("Astrid", result[0].Name); Assert.Equal("göttliche Stärke", result[0].Meaning); Assert.Equal("Altnordisch", result[0].Origin); } [Fact] public void ParseSuggestions_strippt_json_Markdown_Fence() { var fenced = "```json\n[{\"name\":\"Aiko\",\"meaning\":\"kleine Geliebte\",\"origin\":\"Japanisch\"}]\n```"; var result = NameSuggestionService.ParseSuggestions(fenced); Assert.NotNull(result); Assert.Equal("Aiko", result![0].Name); } [Fact] public void ParseSuggestions_strippt_generische_Markdown_Fence() { var fenced = "```\n[{\"name\":\"Luna\",\"meaning\":\"Mond\",\"origin\":\"Lateinisch\"}]\n```"; var result = NameSuggestionService.ParseSuggestions(fenced); Assert.NotNull(result); Assert.Equal("Luna", result![0].Name); } [Fact] public void ParseSuggestions_toleriert_umgebenden_Text_vor_Array() { var messy = "Hier sind die Namen:\n[{\"name\":\"Sol\",\"meaning\":\"Sonne\",\"origin\":\"Nordisch\"}]\nHoffnungslos."; var result = NameSuggestionService.ParseSuggestions(messy); Assert.NotNull(result); Assert.Equal("Sol", result![0].Name); } [Fact] public void ParseSuggestions_gibt_null_zurück_bei_ungültigem_JSON() { Assert.Null(NameSuggestionService.ParseSuggestions("kein json")); Assert.Null(NameSuggestionService.ParseSuggestions("{\"name\":\"X\"}")); Assert.Null(NameSuggestionService.ParseSuggestions("")); } // ── 503: nicht konfiguriert ─────────────────────────────────────────── [Theory] [InlineData(null, "key", "model")] [InlineData("https://api.example.com/v1", null, "model")] [InlineData("https://api.example.com/v1", "key", null)] [InlineData(null, null, null)] public async Task SuggestAsync_gibt_NotConfigured_wenn_AI_Key_fehlt( string? baseUrl, string? apiKey, string? model) { var service = CreateService(baseUrl, apiKey, model, new StubHandler(_ => throw new InvalidOperationException("darf nicht aufgerufen werden"))); var result = await service.SuggestAsync(null, null, null, 5); Assert.Equal(NameSuggestionStatus.NotConfigured, result.Status); Assert.Null(result.Suggestions); } // ── Gemini-Antwort wird geparst ─────────────────────────────────────── [Fact] public async Task SuggestAsync_parst_valide_JSON_Antwort() { var payload = """[{"name":"Astrid","meaning":"göttliche Stärke","origin":"Altnordisch"},{"name":"Aiko","meaning":"kleine Geliebte","origin":"Japanisch"}]"""; var handler = new StubHandler(_ => Canned(payload)); var service = CreateService("https://generativelanguage.googleapis.com/v1beta/openai", "k", "gemini-2.0-flash", handler); var result = await service.SuggestAsync("A", "female", "norn,japa", 2); Assert.Equal(NameSuggestionStatus.Ok, result.Status); Assert.NotNull(result.Suggestions); Assert.Equal(2, result.Suggestions!.Count); Assert.Equal("Astrid", result.Suggestions[0].Name); } [Fact] public async Task SuggestAsync_parst_JSON_in_Markdown_Fence() { var fenced = "```json\n[{\"name\":\"Luna\",\"meaning\":\"Mond\",\"origin\":\"Lateinisch\"}]\n```"; var handler = new StubHandler(_ => Canned(fenced)); var service = CreateService("https://api.example.com/v1", "k", "m", handler); var result = await service.SuggestAsync(null, null, null, 1); Assert.Equal(NameSuggestionStatus.Ok, result.Status); Assert.Equal("Luna", result.Suggestions![0].Name); } [Fact] public async Task SuggestAsync_gibt_UpstreamError_bei_ungültigem_JSON() { var handler = new StubHandler(_ => Canned("das ist kein json")); var service = CreateService("https://api.example.com/v1", "k", "m", handler); var result = await service.SuggestAsync(null, null, null, 3); Assert.Equal(NameSuggestionStatus.UpstreamError, result.Status); Assert.Null(result.Suggestions); } [Fact] public async Task SuggestAsync_gibt_UpstreamError_bei_HTTP_Fehler() { var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.TooManyRequests)); var service = CreateService("https://api.example.com/v1", "k", "m", handler); var result = await service.SuggestAsync(null, null, null, 3); Assert.Equal(NameSuggestionStatus.UpstreamError, result.Status); } // ── Helfer ──────────────────────────────────────────────────────────── private static NameSuggestionService CreateService( string? baseUrl, string? apiKey, string? model, StubHandler handler) { var options = Options.Create(new AiOptions { BaseUrl = baseUrl, ApiKey = apiKey, Model = model }); return new NameSuggestionService(new HttpClient(handler), options); } private static HttpResponseMessage Canned(string content) { var completion = new { choices = new[] { new { message = new { role = "assistant", content } } }, }; return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(completion), Encoding.UTF8, "application/json"), }; } private sealed class StubHandler(Func respond) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(respond(request)); } } }