using System.IO.Compression; using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace GerbilManager.Tests; /// /// FEAT-13 phase B: Endpoint-Round-Trips für /contracts + /settings/breeder-profile /// gegen den In-Memory-Host (ApiFactory). Deckt den Abgabe-Abschluss ab: /// Vertrag erzeugen -> Tiere stehen auf Abgegeben -> .docx ist herunterladbar. /// public class ContractEndpointTests : IClassFixture { private static readonly JsonSerializerOptions Json = CreateJsonOptions(); private static JsonSerializerOptions CreateJsonOptions() { var o = new JsonSerializerOptions(JsonSerializerDefaults.Web); o.Converters.Add(new JsonStringEnumConverter()); return o; } private readonly ApiFactory _factory; public ContractEndpointTests(ApiFactory factory) => _factory = factory; private sealed record IdDto(Guid Id); private sealed record GerbilView(Guid Id, string Status, Guid? ReceiverContactId, DateOnly? GoHomeDate); private sealed record ContractView( Guid Id, Guid ContactId, decimal Price, DateOnly HandoverDate, DateOnly ContractDate, string FileName, List GerbilIds, string Url); private sealed record Paged(List Items, int TotalCount, int Page, int PageSize); private async Task CreateContactAsync(HttpClient client, string name = "Herr Max Beispiel") { var response = await client.PostAsJsonAsync("/contacts", new { name, email = "max@example.com", phone = "0987 654321", address = "Beispielallee 7, 54321 Beispielstadt", }, Json); Assert.Equal(HttpStatusCode.Created, response.StatusCode); return (await response.Content.ReadFromJsonAsync(Json))!.Id; } private async Task CreateGerbilAsync(HttpClient client, string name) { var response = await client.PostAsJsonAsync("/gerbils", new { name, gender = "female", dateOfBirth = "2025-03-09", }, Json); Assert.Equal(HttpStatusCode.Created, response.StatusCode); return (await response.Content.ReadFromJsonAsync(Json))!.Id; } [Fact] public async Task ZuchtprofilRoundTrip_PutDannGet() { var client = _factory.CreateClient(); var put = await client.PutAsJsonAsync("/settings/breeder-profile", new { zuchtName = "Zucht Testhausen", name = "Frau Erika Muster", address = "Musterweg 1, 12345 Testhausen", phone = "0123 456789", email = "zucht@example.org", homepage = "https://zucht.example.org/", city = "Testhausen", }, Json); Assert.Equal(HttpStatusCode.NoContent, put.StatusCode); var profile = await client.GetFromJsonAsync>("/settings/breeder-profile", Json); Assert.Equal("Zucht Testhausen", profile!["zuchtName"]); Assert.Equal("Testhausen", profile["city"]); } [Fact] public async Task VertragErzeugen_SchließtDieAbgabeAb() { var client = _factory.CreateClient(); // Zuchtprofil füllen, damit der Verkäufer-Block im Dokument landet. await client.PutAsJsonAsync("/settings/breeder-profile", new { zuchtName = "Zucht Testhausen", name = "Frau Erika Muster", address = "Musterweg 1, 12345 Testhausen", phone = "0123 456789", email = "zucht@example.org", homepage = "https://zucht.example.org/", city = "Testhausen", }, Json); var contactId = await CreateContactAsync(client); var krümel = await CreateGerbilAsync(client, "Krümel"); var luna = await CreateGerbilAsync(client, "Luna"); // POST /contracts — der Abschluss. var post = await client.PostAsJsonAsync("/contracts", new { contactId, gerbilIds = new[] { krümel, luna }, price = 72.0m, handoverDate = "2026-06-05", }, Json); Assert.Equal(HttpStatusCode.Created, post.StatusCode); var contract = (await post.Content.ReadFromJsonAsync(Json))!; Assert.Equal(contactId, contract.ContactId); Assert.Equal(2, contract.GerbilIds.Count); Assert.Equal(new DateOnly(2026, 6, 5), contract.ContractDate); // Standard = Übergabedatum Assert.Equal($"/contracts/{contract.Id}/file", contract.Url); // Tiere stehen jetzt auf Abgegeben — mit Abnehmer und Abgabedatum. foreach (var id in new[] { krümel, luna }) { var gerbil = await client.GetFromJsonAsync($"/gerbils/{id}", Json); Assert.Equal("GivenAway", gerbil!.Status); Assert.Equal(contactId, gerbil.ReceiverContactId); Assert.Equal(new DateOnly(2026, 6, 5), gerbil.GoHomeDate); } // Download: echtes .docx mit beiden Tieren, Käufer, de-DE-Preis. var file = await client.GetAsync(contract.Url); Assert.Equal(HttpStatusCode.OK, file.StatusCode); Assert.Equal( "application/vnd.openxmlformats-officedocument.wordprocessingml.document", file.Content.Headers.ContentType!.MediaType); Assert.Contains("Abgabevertrag_2026-06-05_Herr-Max-Beispiel.docx", file.Content.Headers.ContentDisposition!.FileNameStar ?? file.Content.Headers.ContentDisposition.FileName); var text = DocumentText(await file.Content.ReadAsByteArrayAsync()); Assert.Contains("Krümel", text); Assert.Contains("Luna", text); Assert.Contains("Herr Max Beispiel", text); Assert.Contains("Kaufpreis von 72,00 €", text); Assert.Contains("Testhausen, den 05.06.2026", text); Assert.DoesNotContain("{{", text); // Liste: nach Abnehmer filterbar (Gridify). var list = await client.GetFromJsonAsync>( $"/contracts?filter=contactId=={contactId}", Json); Assert.Equal(1, list!.TotalCount); Assert.Equal(contract.Id, list.Items[0].Id); // DELETE räumt Zeile und Datei ab; Tier-Status bleibt unangetastet. var delete = await client.DeleteAsync($"/contracts/{contract.Id}"); Assert.Equal(HttpStatusCode.NoContent, delete.StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync(contract.Url)).StatusCode); var still = await client.GetFromJsonAsync($"/gerbils/{krümel}", Json); Assert.Equal("GivenAway", still!.Status); } [Fact] public async Task VertragOhneTiereOderMitUnbekanntemTier_Validierungsfehler() { var client = _factory.CreateClient(); var contactId = await CreateContactAsync(client, "Frau Lisa Test"); var empty = await client.PostAsJsonAsync("/contracts", new { contactId, gerbilIds = Array.Empty(), price = 10m, handoverDate = "2026-06-05", }, Json); Assert.Equal(HttpStatusCode.BadRequest, empty.StatusCode); var unknown = await client.PostAsJsonAsync("/contracts", new { contactId, gerbilIds = new[] { Guid.NewGuid() }, price = 10m, handoverDate = "2026-06-05", }, Json); Assert.Equal(HttpStatusCode.BadRequest, unknown.StatusCode); } /// Sichtbarer Text aus word/document.xml (Tags entfernt). private static string DocumentText(byte[] docx) { using var zip = new ZipArchive(new MemoryStream(docx), ZipArchiveMode.Read); var entry = Assert.Single(zip.Entries, e => e.FullName == "word/document.xml"); using var reader = new StreamReader(entry.Open(), Encoding.UTF8); return Regex.Replace(reader.ReadToEnd(), "<[^>]+>", ""); } }