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: NameSuggestionService — prompt assembly, 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 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); Assert.Contains("norn,mythg", 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); } // ── 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)); } } }