218 lines
9.3 KiB
C#
218 lines
9.3 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using GerbilManagerWebAPI.Inbox;
|
|
using GerbilManagerWebAPI.Models;
|
|
using GerbilManagerWebAPI.SaleAd;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace GerbilManager.Tests
|
|
{
|
|
/// <summary>
|
|
/// INBOX-2: KI-Antwortentwurf — Prompt-Assembly (inkl. Datenminimierung +
|
|
/// Zitat-/Signatur-Stripping), 503-unkonfiguriert, und der Endpoint-Round-Trip
|
|
/// gegen einen Stub-Anbieter (Entwurf wird gespeichert UND zurückgegeben).
|
|
/// </summary>
|
|
public class InboxDraftTests
|
|
{
|
|
private static Request SampleRequest() => new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
GmailMessageId = $"<{Guid.NewGuid():N}@mail.example>",
|
|
FromAddress = "anna.musterfrau@example.de",
|
|
FromName = "Anna Musterfrau",
|
|
Subject = "Anfrage: zwei Weibchen?",
|
|
BodyText = "Hallo! Habt ihr aktuell zwei junge Weibchen zur Abgabe?\nViele Grüße, Anna",
|
|
ReceivedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
|
|
// ── Zitat-/Signatur-Stripping ──────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void StripQuotedText_entfernt_Zitate_Signatur_und_Outlook_Header()
|
|
{
|
|
var body = string.Join('\n',
|
|
"Hallo, habt ihr Tiere abzugeben?",
|
|
"> alte zitierte Zeile",
|
|
"Von: jemand@example.de",
|
|
"Danke!",
|
|
"-- ",
|
|
"Anna Musterfrau",
|
|
"Musterweg 1");
|
|
var stripped = DraftReplyService.StripQuotedText(body);
|
|
|
|
Assert.Contains("Hallo, habt ihr Tiere abzugeben?", stripped);
|
|
Assert.Contains("Danke!", stripped);
|
|
Assert.DoesNotContain("alte zitierte Zeile", stripped);
|
|
Assert.DoesNotContain("jemand@example.de", stripped);
|
|
Assert.DoesNotContain("Musterweg 1", stripped); // Signatur weg
|
|
}
|
|
|
|
[Fact]
|
|
public void StripQuotedText_schneidet_beim_Gmail_Zitat_Intro_ab()
|
|
{
|
|
var body = "Meine Frage steht oben.\nAm 05.06.2026 um 10:00 schrieb Zucht der kleinen Chaoten:\n> früherer Verlauf";
|
|
var stripped = DraftReplyService.StripQuotedText(body);
|
|
|
|
Assert.Equal("Meine Frage steht oben.", stripped);
|
|
}
|
|
|
|
// ── Prompt-Assembly + Datenminimierung ─────────────────────────────
|
|
|
|
[Fact]
|
|
public void UserPrompt_enthält_Anfrage_und_Abgabeliste_aber_keine_Mailadresse()
|
|
{
|
|
var prompt = DraftReplyService.BuildUserPrompt(SampleRequest(),
|
|
[new DraftReplyService.ForSaleAnimal("Frieda", "Gold"),
|
|
new DraftReplyService.ForSaleAnimal("Fine", null)]);
|
|
|
|
Assert.Contains("Von: Anna Musterfrau", prompt);
|
|
Assert.Contains("Betreff: Anfrage: zwei Weibchen?", prompt);
|
|
Assert.Contains("zwei junge Weibchen zur Abgabe", prompt);
|
|
Assert.Contains("- Frieda (Gold)", prompt);
|
|
Assert.Contains("- Fine", prompt);
|
|
// Datenminimierung: die E-Mail-Adresse der Absenderin geht NICHT zum Anbieter
|
|
Assert.DoesNotContain("anna.musterfrau@example.de", prompt);
|
|
}
|
|
|
|
[Fact]
|
|
public void UserPrompt_ohne_Abgabetiere_enthält_keine_Liste()
|
|
{
|
|
var prompt = DraftReplyService.BuildUserPrompt(SampleRequest(), []);
|
|
Assert.DoesNotContain("abzugebende Tiere", prompt);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPrompt_verlangt_Entwurf_ohne_Preise_und_ohne_erfundene_Fakten()
|
|
{
|
|
var prompt = DraftReplyService.BuildSystemPrompt();
|
|
Assert.Contains("ENTWERFEN", prompt);
|
|
Assert.Contains("KEINE Fakten erfinden", prompt);
|
|
Assert.Contains("Keine Preise", prompt);
|
|
Assert.Contains("keine Adressen", prompt);
|
|
}
|
|
|
|
// ── 503: unkonfiguriert (Service-Ebene) ────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task DraftAsync_meldet_NotConfigured_ohne_AI_Konfiguration()
|
|
{
|
|
var service = new DraftReplyService(
|
|
new HttpClient(new StubHandler(_ => throw new InvalidOperationException("darf nicht aufgerufen werden"))),
|
|
Options.Create(new AiOptions()));
|
|
|
|
var result = await service.DraftAsync(SampleRequest(), []);
|
|
|
|
Assert.Equal(GerbilManagerWebAPI.Ai.AiCallStatus.NotConfigured, result.Status);
|
|
}
|
|
|
|
// ── Endpoint-Round-Trip ────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Draft_Endpoint_meldet_503_AiKeyMissing_wenn_unkonfiguriert()
|
|
{
|
|
using var factory = new ApiFactory();
|
|
var id = await SeedRequestAsync(factory);
|
|
|
|
var response = await factory.CreateClient().PostAsync($"/api/requests/{id}/draft", null);
|
|
|
|
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
|
Assert.Contains("AiKeyMissing", await response.Content.ReadAsStringAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Draft_Endpoint_speichert_und_liefert_den_Entwurf_des_Stubs()
|
|
{
|
|
const string canned = "Hallo Anna,\n\nschön, dass du fragst — aktuell suchen Frieda und Fine ein Zuhause.\n\nHerzliche Grüße";
|
|
var stub = new StubHandler(_ => Canned(canned));
|
|
using var factory = new ApiFactory
|
|
{
|
|
ConfigureTestServices = services =>
|
|
{
|
|
services.PostConfigure<AiOptions>(o =>
|
|
{
|
|
o.BaseUrl = "https://api.example.com/v1";
|
|
o.ApiKey = "test";
|
|
o.Model = "test-model";
|
|
});
|
|
services.AddHttpClient<DraftReplyService>()
|
|
.ConfigurePrimaryHttpMessageHandler(() => stub);
|
|
},
|
|
};
|
|
|
|
var id = await SeedRequestAsync(factory, alsoForSaleGerbil: true);
|
|
var client = factory.CreateClient();
|
|
|
|
var response = await client.PostAsync($"/api/requests/{id}/draft", null);
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
|
|
using var dto = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
|
Assert.Equal(canned, dto.RootElement.GetProperty("draftReply").GetString());
|
|
|
|
// Persistiert: erneutes GET liefert den Entwurf
|
|
var again = await client.GetFromJsonAsync<JsonElement>($"/api/requests/{id}");
|
|
Assert.Equal(canned, again.GetProperty("draftReply").GetString());
|
|
|
|
// Wire-Privacy: Anfragetext + ForSale-Tier gingen zum Anbieter, die
|
|
// Mailadresse der Absenderin NICHT.
|
|
Assert.NotNull(stub.LastRequestBody);
|
|
Assert.Contains("zwei junge Weibchen", stub.LastRequestBody);
|
|
Assert.Contains("Aki", stub.LastRequestBody);
|
|
Assert.DoesNotContain("anna.musterfrau@example.de", stub.LastRequestBody);
|
|
}
|
|
|
|
// ── Helfer ─────────────────────────────────────────────────────────
|
|
|
|
private static async Task<Guid> SeedRequestAsync(ApiFactory factory, bool alsoForSaleGerbil = false)
|
|
{
|
|
using var scope = factory.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
|
|
var request = SampleRequest();
|
|
db.Add(request);
|
|
if (alsoForSaleGerbil)
|
|
{
|
|
db.Add(new Gerbil
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Aki",
|
|
Gender = Gender.female,
|
|
Status = GerbilStatus.ForSale,
|
|
});
|
|
}
|
|
await db.SaveChangesAsync();
|
|
return request.Id;
|
|
}
|
|
|
|
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"),
|
|
};
|
|
}
|
|
|
|
/// <summary>HttpMessageHandler-Stub (Variante von SaleAdTests, hier mit Body-Capture).</summary>
|
|
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> respond)
|
|
: HttpMessageHandler
|
|
{
|
|
public string? LastRequestBody { get; private set; }
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
LastRequestBody = request.Content is null
|
|
? null
|
|
: await request.Content.ReadAsStringAsync(cancellationToken);
|
|
return respond(request);
|
|
}
|
|
}
|
|
}
|
|
}
|