using System.Net; using System.Net.Http.Json; using System.Text.Json; using GerbilManagerWebAPI.Import; using GerbilManagerWebAPI.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace GerbilManager.Tests; /// /// FEEDBACK: the "Fehler melden" report sink. /// - POST /feedback persists a row (with captured debug context) and GET /feedback returns it. /// - Validation: an empty message is rejected with 400. /// - CRITICAL: feedback rows survive the import re-ingest wipe (loose, FK-free GerbilId/LitterId). /// public class FeedbackEndpointTests : IClassFixture { private readonly ApiFactory _factory; public FeedbackEndpointTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Post_feedback_persists_row_with_debug_context() { var client = _factory.CreateClient(); var gerbilId = Guid.NewGuid(); var resp = await client.PostAsJsonAsync("/feedback", new { message = "Der Stammbaum zeigt den falschen Vater.", context = "stammbaum", gerbilId, litterId = (Guid?)null, entityName = "Krümel", url = "http://localhost:5173/rennmaeuse/kruemel/stammbaum", clientTimestamp = "2026-06-22T12:00:00Z", }); Assert.Equal(HttpStatusCode.Created, resp.StatusCode); var created = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement; Assert.False(string.IsNullOrEmpty(created.GetProperty("id").GetString())); Assert.Equal("stammbaum", created.GetProperty("context").GetString()); Assert.Equal("Krümel", created.GetProperty("entityName").GetString()); Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString()); // GET returns it (newest first) var listed = JsonDocument.Parse(await client.GetStringAsync("/feedback")).RootElement; Assert.Contains(listed.EnumerateArray(), f => f.GetProperty("entityName").GetString() == "Krümel" && f.GetProperty("message").GetString() == "Der Stammbaum zeigt den falschen Vater."); } [Fact] public async Task Post_feedback_rejects_empty_message() { var client = _factory.CreateClient(); var resp = await client.PostAsJsonAsync("/feedback", new { message = " ", context = "gerbil-detail" }); Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); } [Fact] public async Task Feedback_survives_ingest_wipe() { // Fresh in-memory DB seeded with a resolved import file (mirrors IngestResolvedServiceTests). var dir = Path.Combine(Path.GetTempPath(), "feedback-ingest-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); try { var contactId = Guid.NewGuid(); var fatherId = Guid.NewGuid(); var motherId = Guid.NewGuid(); var litterId = Guid.NewGuid(); var data = new { Contacts = new[] { new { Id = contactId, Name = "Test Breeder", Email = "t@e.de", Phone = "", Address = "", Notes = (string?)null, IsBreeder = true, IsReceiver = false } }, Litters = new[] { new { Id = litterId, Name = "Wurf A", Date = "2026-01-01", TotalBorn = 5, DeathsWithin8Weeks = 0, FatherId = fatherId, MotherId = motherId, ExpectedGoHomeDate = (string?)null, Notes = "", PairingCode = "PC01", ExternalRef = "ext-litter-1", LitterLetter = "A" } }, Gerbils = new[] { Animal(fatherId, "Papa", "male", contactId), Animal(motherId, "Mama", "female", contactId), }, GerbilPhotos = Array.Empty(), }; File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(data)); var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase("feedback-ingest-" + Guid.NewGuid().ToString("N")) .Options; using var db = new ApplicationContext(opts); db.Database.EnsureCreated(); // A feedback report referencing the gerbil + litter that the wipe will delete. var feedbackId = Guid.NewGuid(); db.Feedback.Add(new Feedback { Id = feedbackId, Message = "Bitte prüfen.", Context = "gerbil-detail", GerbilId = fatherId, LitterId = litterId, EntityName = "Papa", Url = "http://localhost/rennmaeuse/papa", CreatedAt = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "Import:SourcePath", dir } }) .Build(); // Run the ingest wipe + reload. var result = await new IngestResolvedService(db, config, null!).RunAsync(); Assert.Contains("Ingestion successful!", result); // Gerbils/litters were wiped & re-created, but feedback is untouched. var survivor = await db.Feedback.SingleAsync(); Assert.Equal(feedbackId, survivor.Id); Assert.Equal(fatherId, survivor.GerbilId); // loose id preserved even though the gerbil row was deleted/recreated Assert.Equal(litterId, survivor.LitterId); Assert.Equal("Papa", survivor.EntityName); } finally { try { Directory.Delete(dir, recursive: true); } catch { /* best effort */ } } } private static object Animal(Guid id, string name, string gender, Guid contactId) => new { Id = id, Name = name, Gender = gender, Status = "Breeding", LitterId = (Guid?)null, OriginContactId = contactId, ReceiverContactId = (Guid?)null, EnclosureId = (Guid?)null, ColorVarietyId = new Guid("00000000-0000-0000-0000-000000000006"), DateOfBirth = "2025-01-01", DateOfDeath = (string?)null, CauseOfDeath = (string?)null, GoHomeDate = (string?)null, Genotype = "aa CC DD EE GG PP spsp rere", Notes = "", ImportSource = "docx-export", ExternalRef = "ext-" + name, RawImportData = "{}", OriginBreeder = "Test Zucht", NameSearch = name.ToLowerInvariant(), CharacterTraits = Array.Empty(), CharacterNote = (string?)null, IsDeaf = false, IsResident = true, }; }