using System.IO.Compression; using System.Text; using System.Text.RegularExpressions; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Validation; using GerbilManagerWebAPI.Contracts; namespace GerbilManager.Tests; /// /// FEAT-13: Tests des Abgabevertrag-Generators. Die erzeugte .docx wird als /// Zip geöffnet und auf word/document.xml geprüft — kein Word nötig. /// public class ContractGeneratorTests { private static BreederProfile Seller() => 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", }; private static ContractAnimal Krümel() => new( Name: "Krümel", Geschlecht: "Weiblich", Geburtsdatum: new DateOnly(2025, 3, 9), Farbschlag: "Agouti"); private static ContractData OneAnimal() => new( Seller: Seller(), Buyer: new ContractBuyer( Name: "Herr Max Beispiel", Address: "Beispielallee 7, 54321 Beispielstadt", Phone: "0987 654321", Email: "max@example.com"), Animals: [Krümel()], Price: 72m, HandoverDate: new DateOnly(2026, 6, 5)); /// /// Sichtbarer Text aus word/document.xml der erzeugten Datei. Tags werden /// entfernt, damit Aussagen über Run-Grenzen hinweg möglich sind (Werte /// und Fließtext liegen in getrennten w:t-Runs). /// 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(), "<[^>]+>", ""); } [Fact] public void EinTier_EnthaeltAlleVertragswerte() { var text = DocumentText(ContractGenerator.Generate(OneAnimal())); // Verkäufer-Block (aus dem Zuchtprofil) Assert.Contains("Zucht Testhausen", text); Assert.Contains("Frau Erika Muster", text); Assert.Contains("Musterweg 1, 12345 Testhausen", text); Assert.Contains("0123 456789", text); Assert.Contains("zucht@example.org", text); Assert.Contains("https://zucht.example.org/", text); // Käufer-Block Assert.Contains("Herr Max Beispiel", text); Assert.Contains("Beispielallee 7, 54321 Beispielstadt", text); Assert.Contains("0987 654321", text); Assert.Contains("max@example.com", text); // Tier Assert.Contains("Krümel", text); Assert.Contains("Weiblich", text); Assert.Contains("09.03.2025", text); Assert.Contains("Agouti", text); // Kaufpreis (de-DE) + Übergabedatum (TT.MM.JJJJ) Assert.Contains("Kaufpreis von 72,00 €", text); Assert.Contains("am 05.06.2026 dem Käufer", text); // Unterschriftszeile: Ort des Züchters + Vertragsdatum (= Übergabedatum) Assert.Contains("Testhausen, den 05.06.2026", text); } [Fact] public void KeineUnersetztenTokens() { var text = DocumentText(ContractGenerator.Generate(OneAnimal())); Assert.DoesNotContain("{{", text); Assert.DoesNotContain("}}", text); } [Fact] public void DreiTiere_KlontDieTierTabelleProTier() { var data = OneAnimal() with { Animals = [ Krümel(), new ContractAnimal("Fridolin", "Männlich", new DateOnly(2023, 5, 1), "Schwarz"), new ContractAnimal("Luna", "Weiblich", new DateOnly(2023, 8, 15), "Gold"), ], }; var text = DocumentText(ContractGenerator.Generate(data)); Assert.Contains("Krümel", text); Assert.Contains("Fridolin", text); Assert.Contains("Luna", text); Assert.Contains("01.05.2023", text); Assert.Contains("15.08.2023", text); // Pro Tier eine Tabelle: „Tierart:“ ist das fixe Label jeder Tier-Tabelle. Assert.Equal(3, Regex.Matches(text, "Tierart:").Count); Assert.DoesNotContain("{{", text); } [Fact] public void PreisUndDatum_DeutscheFormate() { var data = OneAnimal() with { Price = 1234.5m, HandoverDate = new DateOnly(2026, 1, 3) }; var text = DocumentText(ContractGenerator.Generate(data)); Assert.Contains("Kaufpreis von 1.234,50 €", text); Assert.Contains("am 03.01.2026 dem Käufer", text); } [Fact] public void VertragsDatum_UeberschreibtUebergabedatumInDerUnterschriftszeile() { var data = OneAnimal() with { ContractDate = new DateOnly(2026, 6, 7) }; var text = DocumentText(ContractGenerator.Generate(data)); Assert.Contains("Testhausen, den 07.06.2026", text); Assert.Contains("am 05.06.2026 dem Käufer", text); // Übergabedatum bleibt eigenständig } [Fact] public void OptionaleFelder_LeerStattPlatzhalter() { var data = OneAnimal() with { Buyer = new ContractBuyer("Frau Lisa Test", "Testgasse 2, 11111 Teststadt"), Animals = [new ContractAnimal("Momo", "Unbekannt")], }; var text = DocumentText(ContractGenerator.Generate(data)); Assert.Contains("Frau Lisa Test", text); Assert.Contains("Momo", text); Assert.DoesNotContain("{{", text); Assert.DoesNotContain("null", text, StringComparison.OrdinalIgnoreCase); } [Fact] public void OhneTiere_WirftArgumentException() { var data = OneAnimal() with { Animals = [] }; var ex = Assert.Throws(() => ContractGenerator.Generate(data)); Assert.Contains("mindestens ein Tier", ex.Message); } [Fact] public void ErzeugteDatei_IstEinGueltigesDocxPaket() { var docx = ContractGenerator.Generate(OneAnimal()); using var zip = new ZipArchive(new MemoryStream(docx), ZipArchiveMode.Read); Assert.Contains(zip.Entries, e => e.FullName == "[Content_Types].xml"); Assert.Contains(zip.Entries, e => e.FullName == "word/document.xml"); // Tierfotos wurden beim Vorlagenbau entfernt, das Zucht-Logo bleibt. Assert.Contains(zip.Entries, e => e.FullName == "word/media/image1.jpeg"); Assert.DoesNotContain(zip.Entries, e => e.FullName == "word/media/image2.jpeg"); } [Fact] public void ErzeugtesDokument_ValidiertGegenOpenXmlSchema() { // Office-2013-Schema: die Vorlage nutzt w:tblLook-Attribute der // Word-2010-Form, die der 2007-Default des Validators nicht kennt. var validator = new OpenXmlValidator(DocumentFormat.OpenXml.FileFormatVersions.Office2013); var data = OneAnimal() with { Animals = [Krümel(), new ContractAnimal("Fridolin", "Männlich"), new ContractAnimal("Luna", "Weiblich")], }; using var generatedDoc = WordprocessingDocument.Open( new MemoryStream(ContractGenerator.Generate(data)), isEditable: false); var errors = validator.Validate(generatedDoc) .Select(e => $"{e.ErrorType}: {e.Description} [{e.Path?.XPath}]") .ToList(); Assert.True(errors.Count == 0, "OpenXml-Validierungsfehler:\n" + string.Join("\n", errors)); } [Fact] public void VorlageSelbst_EnthaeltKeineEchtdaten() { // Die Vorlage wird hier über den Generator-Pfad geladen: ein Vertrag // mit leeren Werten darf keinerlei Daten des Mustervertrags enthalten. var data = new ContractData( new BreederProfile(), new ContractBuyer("", ""), [new ContractAnimal("", "")], 0m, new DateOnly(2026, 1, 1)); var text = DocumentText(ContractGenerator.Generate(data)); // Stichproben der Original-PII (Name/Ort/Mail des Mustervertrags). Assert.DoesNotContain("Nießner", text); Assert.DoesNotContain("Hartengrund", text); Assert.DoesNotContain("Ronneburg", text); Assert.DoesNotContain("Schädtler", text); Assert.DoesNotContain("gmx.de", text); Assert.DoesNotContain("jimdofree", text); } }