215 lines
9.7 KiB
C#
215 lines
9.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// ABGABE-STATUS (Reservierungs-/Abgabe-Status): the pre-handover pipeline
|
|
/// verfügbar → reserviert → abgegeben.
|
|
/// - Full CRUD round-trip via /reservations.
|
|
/// - GET ?gerbilId= filters to one animal.
|
|
/// - Invalid status falls back to "verfuegbar".
|
|
/// - CRITICAL: rows survive the import re-ingest wipe (loose, FK-free GerbilId/ReservedForContactId).
|
|
/// </summary>
|
|
public class SaleReservationEndpointTests : IClassFixture<ApiFactory>
|
|
{
|
|
private readonly ApiFactory _factory;
|
|
public SaleReservationEndpointTests(ApiFactory factory) => _factory = factory;
|
|
|
|
[Fact]
|
|
public async Task Reservation_crud_lifecycle_create_reserve_handover_delete()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
var gerbilId = Guid.NewGuid();
|
|
var contactId = Guid.NewGuid();
|
|
|
|
// Create -> defaults to verfuegbar.
|
|
var create = await client.PostAsJsonAsync("/reservations", new
|
|
{
|
|
gerbilId,
|
|
gerbilName = "Pippa",
|
|
});
|
|
Assert.Equal(HttpStatusCode.Created, create.StatusCode);
|
|
var created = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement;
|
|
var id = created.GetProperty("id").GetString();
|
|
Assert.False(string.IsNullOrEmpty(id));
|
|
Assert.Equal("verfuegbar", created.GetProperty("status").GetString());
|
|
Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString());
|
|
Assert.Equal("Pippa", created.GetProperty("gerbilName").GetString());
|
|
|
|
// List contains it.
|
|
var listed = JsonDocument.Parse(await client.GetStringAsync("/reservations")).RootElement;
|
|
Assert.Contains(listed.EnumerateArray(), r => r.GetProperty("id").GetString() == id);
|
|
|
|
// Filter by gerbilId returns exactly this row.
|
|
var byGerbil = JsonDocument.Parse(await client.GetStringAsync($"/reservations?gerbilId={gerbilId}")).RootElement;
|
|
Assert.All(byGerbil.EnumerateArray(), r => Assert.Equal(gerbilId.ToString(), r.GetProperty("gerbilId").GetString()));
|
|
Assert.Contains(byGerbil.EnumerateArray(), r => r.GetProperty("id").GetString() == id);
|
|
|
|
// PUT -> reserve for a contact with an appointment + price.
|
|
var reserve = await client.PutAsJsonAsync($"/reservations/{id}", new
|
|
{
|
|
status = "reserviert",
|
|
reservedForContactId = contactId,
|
|
contactName = "Familie Huber",
|
|
appointmentDate = "2026-07-01T10:00:00Z",
|
|
price = 25.50m,
|
|
note = "Käfig wird mitgebracht.",
|
|
});
|
|
Assert.Equal(HttpStatusCode.OK, reserve.StatusCode);
|
|
var reserved = JsonDocument.Parse(await reserve.Content.ReadAsStringAsync()).RootElement;
|
|
Assert.Equal("reserviert", reserved.GetProperty("status").GetString());
|
|
Assert.Equal(contactId.ToString(), reserved.GetProperty("reservedForContactId").GetString());
|
|
Assert.Equal("Familie Huber", reserved.GetProperty("contactName").GetString());
|
|
Assert.Equal(25.50m, reserved.GetProperty("price").GetDecimal());
|
|
|
|
// PUT -> hand over.
|
|
var handover = await client.PutAsJsonAsync($"/reservations/{id}", new
|
|
{
|
|
status = "abgegeben",
|
|
reservedForContactId = contactId,
|
|
handedOverDate = "2026-07-01T10:30:00Z",
|
|
});
|
|
Assert.Equal(HttpStatusCode.OK, handover.StatusCode);
|
|
var handed = JsonDocument.Parse(await handover.Content.ReadAsStringAsync()).RootElement;
|
|
Assert.Equal("abgegeben", handed.GetProperty("status").GetString());
|
|
Assert.NotEqual(JsonValueKind.Null, handed.GetProperty("handedOverDate").ValueKind);
|
|
|
|
// Delete -> 204, then 404.
|
|
Assert.Equal(HttpStatusCode.NoContent, (await client.DeleteAsync($"/reservations/{id}")).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await client.DeleteAsync($"/reservations/{id}")).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await client.PutAsJsonAsync($"/reservations/{id}", new { status = "verfuegbar" })).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Post_with_unknown_status_falls_back_to_verfuegbar()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
var create = await client.PostAsJsonAsync("/reservations", new { gerbilId = Guid.NewGuid(), status = "bananen" });
|
|
Assert.Equal(HttpStatusCode.Created, create.StatusCode);
|
|
var dto = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement;
|
|
Assert.Equal("verfuegbar", dto.GetProperty("status").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Post_without_gerbilId_is_rejected()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
var create = await client.PostAsJsonAsync("/reservations", new { gerbilId = Guid.Empty, status = "reserviert" });
|
|
Assert.Equal(HttpStatusCode.BadRequest, create.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reservation_survives_ingest_wipe()
|
|
{
|
|
// Fresh in-memory DB seeded with a resolved import file (mirrors IngestResolvedServiceTests).
|
|
var dir = Path.Combine(Path.GetTempPath(), "reservation-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, NameSuffix = (string?)null, Provenance = (string?)null }
|
|
},
|
|
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<object>(),
|
|
};
|
|
File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(data));
|
|
|
|
var opts = new DbContextOptionsBuilder<ApplicationContext>()
|
|
.UseInMemoryDatabase("reservation-ingest-" + Guid.NewGuid().ToString("N"))
|
|
.Options;
|
|
using var db = new ApplicationContext(opts);
|
|
db.Database.EnsureCreated();
|
|
|
|
// A reservation referencing the gerbil + contact that the wipe will delete.
|
|
var resId = Guid.NewGuid();
|
|
db.SaleReservations.Add(new SaleReservation
|
|
{
|
|
Id = resId,
|
|
GerbilId = fatherId,
|
|
GerbilName = "Papa",
|
|
Status = "reserviert",
|
|
ReservedForContactId = contactId,
|
|
ContactName = "Test Breeder",
|
|
AppointmentDate = new DateTime(2026, 7, 1, 10, 0, 0, DateTimeKind.Utc),
|
|
Price = 30m,
|
|
Note = "Abholung am Wochenende.",
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?> { { "Import:SourcePath", dir } })
|
|
.Build();
|
|
|
|
// Run the ingest wipe + reload.
|
|
var result = await new IngestResolvedService(db, config, null!).RunAsync();
|
|
Assert.Contains("Ingestion successful!", result);
|
|
|
|
// Gerbils/contacts were wiped & re-created, but the reservation is untouched.
|
|
var survivor = await db.SaleReservations.SingleAsync(r => r.Id == resId);
|
|
Assert.Equal(fatherId, survivor.GerbilId); // loose id preserved
|
|
Assert.Equal(contactId, survivor.ReservedForContactId);
|
|
Assert.Equal("Papa", survivor.GerbilName);
|
|
Assert.Equal("Test Breeder", survivor.ContactName);
|
|
Assert.Equal("reserviert", survivor.Status);
|
|
Assert.Equal(30m, survivor.Price);
|
|
Assert.Equal(1, await db.SaleReservations.CountAsync());
|
|
}
|
|
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<string>(),
|
|
CharacterNote = (string?)null,
|
|
IsDeaf = false,
|
|
IsResident = true,
|
|
};
|
|
}
|