diff --git a/Lutra/.github/copilot-instructions.md b/Lutra/.github/copilot-instructions.md index 762b1f1..50e3da5 100644 --- a/Lutra/.github/copilot-instructions.md +++ b/Lutra/.github/copilot-instructions.md @@ -1,4 +1,4 @@ -# Copilot Instructions for Lutra +# Copilot Instructions for Lutra Use these standards when generating or modifying code in this repository. @@ -13,6 +13,8 @@ Use these standards when generating or modifying code in this repository. - `Lutra.API` - `Lutra.AppHost` - `Lutra.Infrastructure.Migrator` + - `Lutra.Application.UnitTests` — xUnit unit tests for Application handlers + - integration tests for API behavior (`Lutra.API.IntegrationTests`) - Database: PostgreSQL via EF Core - Orchestration: .NET Aspire - Messaging pattern: `Cortex.Mediator` for CQRS handling @@ -31,10 +33,11 @@ Use these standards when generating or modifying code in this repository. ## Clean Architecture guidance -Use Jason Taylor’s CleanArchitecture project as a reference for intent and structure, but adapt to this codebase’s actual implementation. +Use Jason Taylor’s CleanArchitecture project as a reference for intent and structure, but adapt to this codebase’s actual implementation. Where the reference template uses MediatR, this project uses `Cortex.Mediator`. -Add FluentValidation, AutoMapper, NUnit, Shouldly, Moq, or Respawn only if the repository already uses them or the change clearly requires them. +The established test stack is **xUnit** with **FluentAssertions** — both are already in use and should be used for all new tests. +Add FluentValidation, AutoMapper, Shouldly, Moq, or Respawn only if the repository already uses them or the change clearly requires them. ## Domain layer rules @@ -57,6 +60,7 @@ Add FluentValidation, AutoMapper, NUnit, Shouldly, Moq, or Respawn only if the r - Use `Cortex.Mediator` request and handler interfaces. - Keep handlers focused on orchestration and application logic. - Use `ILutraDbContext` rather than referencing the concrete DbContext directly when possible. +- Shared model types (DTOs, enums) that are not use-case-specific live in `Models//` (e.g., `Models/Verspakketten/`, `Models/Supermarkten/`). ## Infrastructure rules @@ -89,10 +93,12 @@ Add FluentValidation, AutoMapper, NUnit, Shouldly, Moq, or Respawn only if the r ## Testing guidance - If tests exist, update or add tests alongside behavior changes. -- Follow the reference template’s intent for test coverage: - - unit tests for business logic - - integration tests for infrastructure and data access - - functional tests for API behavior +- Follow the reference template's intent for test coverage: + - unit tests for business logic (`Lutra.Application.UnitTests`) + - integration tests for API behavior (`Lutra.API.IntegrationTests`) +- Test framework: **xUnit** (`xunit` v2 + `xunit.runner.visualstudio`). +- Assertions: **FluentAssertions**. +- Integration tests use **SQLite** (via `Microsoft.EntityFrameworkCore.Sqlite`) as the in-process test database -- not PostgreSQL. Use `LutraApiFactory` and `IntegrationTestBase` as the base infrastructure. - Keep tests readable and focused on behavior. ## When making changes diff --git a/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs b/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs index 830a513..eca8272 100644 --- a/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs +++ b/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs @@ -91,7 +91,7 @@ public class VerspakkettenControllerTests(LutraApiFactory factory) CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }); - var command = new CreateVerspakket.Command("Herfst Pakket", 1499, 3, supermarkt.Id); + var command = new CreateVerspakket.Command("Herfst Pakket", 1499, 3, supermarkt.Id, null); var response = await Client.PostAsJsonAsync("/api/verspakketten", command); response.StatusCode.Should().Be(HttpStatusCode.Created); @@ -100,10 +100,43 @@ public class VerspakkettenControllerTests(LutraApiFactory factory) response.Headers.Location.Should().NotBeNull(); } + [Fact] + public async Task Post_CreatesVerspakket_WithBeoordeling_AndReturns201() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Picnic", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var command = new CreateVerspakket.Command( + "Herfst Pakket", + 1499, + 3, + supermarkt.Id, + new Lutra.Application.Models.Verspakketten.Beoordeling + { + CijferSmaak = 9, + CijferBereiden = 8, + Aanbevolen = true, + Tekst = "Heel goed" + }); + + var response = await Client.PostAsJsonAsync("/api/verspakketten", command); + + response.StatusCode.Should().Be(HttpStatusCode.Created); + var body = await response.Content.ReadFromJsonAsync(); + body!.Id.Should().NotBeEmpty(); + + var created = await Client.GetFromJsonAsync($"/api/verspakketten/{body.Id}"); + created!.Verspakket.Beoordelingen.Should().ContainSingle(); + created.Verspakket.Beoordelingen!.Single().CijferSmaak.Should().Be(9); + } + [Fact] public async Task Post_ReturnsBadRequest_WhenSupermarktDoesNotExist() { - var command = new CreateVerspakket.Command("Winter Pakket", 999, 2, Guid.NewGuid()); + var command = new CreateVerspakket.Command("Winter Pakket", 999, 2, Guid.NewGuid(), null); var response = await Client.PostAsJsonAsync("/api/verspakketten", command); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/Lutra/Lutra.API/Controllers/SupermarktenController.cs b/Lutra/Lutra.API/Controllers/SupermarktenController.cs index d0ace7c..b15c83d 100644 --- a/Lutra/Lutra.API/Controllers/SupermarktenController.cs +++ b/Lutra/Lutra.API/Controllers/SupermarktenController.cs @@ -1,6 +1,5 @@ using Cortex.Mediator; using Lutra.Application.Supermarkten; -using Lutra.Application.Verspakketten; using Microsoft.AspNetCore.Mvc; namespace Lutra.API.Controllers; diff --git a/Lutra/Lutra.API/Program.cs b/Lutra/Lutra.API/Program.cs index 6f73940..dc3ba86 100644 --- a/Lutra/Lutra.API/Program.cs +++ b/Lutra/Lutra.API/Program.cs @@ -14,6 +14,22 @@ namespace Lutra.API { var builder = WebApplication.CreateBuilder(args); + builder.Services.AddCors(options => + { + options.AddPolicy("AllowLocalDevelopment", policy => + policy.SetIsOriginAllowed(origin => + { + if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) + { + return false; + } + + return uri.Host is "localhost" or "127.0.0.1" or "[::1]"; + }) + .AllowAnyHeader() + .AllowAnyMethod()); + }); + builder.Services.AddCortexMediator( handlerAssemblyMarkerTypes: [typeof(Program), typeof(GetVerspakketten)], options => options.AddDefaultBehaviors() @@ -37,6 +53,8 @@ namespace Lutra.API app.UseHttpsRedirection(); + app.UseCors("AllowLocalDevelopment"); + app.UseAuthorization(); diff --git a/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs b/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs index 08d77c0..8e24843 100644 --- a/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs +++ b/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Lutra.Application.Interfaces; +using Lutra.Application.Models.Verspakketten; using Lutra.Application.Verspakketten; using Moq; using Moq.EntityFrameworkCore; @@ -30,7 +31,7 @@ public class CreateVerspakketHandlerTests _contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List()); _contextMock.Setup(c => c.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); - var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, supermarktId); + var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, supermarktId, null); var result = await _handler.Handle(command, CancellationToken.None); @@ -43,7 +44,7 @@ public class CreateVerspakketHandlerTests { _contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(new List()); - var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, Guid.NewGuid()); + var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, Guid.NewGuid(), null); var act = () => _handler.Handle(command, CancellationToken.None); @@ -68,7 +69,7 @@ public class CreateVerspakketHandlerTests .Callback((v, _) => savedVerspakket = v); _contextMock.Setup(c => c.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); - var command = new CreateVerspakket.Command("Zomer Pakket", 999, 4, supermarktId); + var command = new CreateVerspakket.Command("Zomer Pakket", 999, 4, supermarktId, null); await _handler.Handle(command, CancellationToken.None); @@ -78,4 +79,45 @@ public class CreateVerspakketHandlerTests savedVerspakket.AantalPersonen.Should().Be(4); savedVerspakket.SupermarktId.Should().Be(supermarktId); } + + [Fact] + public async Task Handle_WithBeoordeling_CreatesVerspakketWithBeoordeling() + { + var supermarktId = Guid.NewGuid(); + var supermarkten = new List + { + new() { Id = supermarktId, Naam = "Jumbo", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow } + }; + + Domain.Entities.Verspakket? savedVerspakket = null; + _contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten); + _contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List()); + _contextMock + .Setup(c => c.Verspaketten.AddAsync(It.IsAny(), It.IsAny())) + .Callback((v, _) => savedVerspakket = v); + _contextMock.Setup(c => c.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); + + var command = new CreateVerspakket.Command( + "Zomer Pakket", + 999, + 4, + supermarktId, + new Beoordeling + { + CijferSmaak = 8, + CijferBereiden = 7, + Aanbevolen = true, + Tekst = "Lekker" + }); + + await _handler.Handle(command, CancellationToken.None); + + savedVerspakket.Should().NotBeNull(); + savedVerspakket!.Beoordelingen.Should().ContainSingle(); + var beoordeling = savedVerspakket.Beoordelingen.Single(); + beoordeling.CijferSmaak.Should().Be(8); + beoordeling.CijferBereiden.Should().Be(7); + beoordeling.Aanbevolen.Should().BeTrue(); + beoordeling.Tekst.Should().Be("Lekker"); + } } diff --git a/Lutra/Lutra.Application/Models/Verspakketten/Beoordeling.cs b/Lutra/Lutra.Application/Models/Verspakketten/Beoordeling.cs index 45fb971..84f8f1e 100644 --- a/Lutra/Lutra.Application/Models/Verspakketten/Beoordeling.cs +++ b/Lutra/Lutra.Application/Models/Verspakketten/Beoordeling.cs @@ -1,12 +1,17 @@ -namespace Lutra.Application.Models.Verspakketten; +using System.ComponentModel.DataAnnotations; + +namespace Lutra.Application.Models.Verspakketten; public class Beoordeling { + [Range(1, 10)] public required int CijferSmaak { get; init; } + [Range(1, 10)] public required int CijferBereiden { get; init; } public required bool Aanbevolen { get; init; } + [MaxLength(1024)] public string? Tekst { get; init; } } diff --git a/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Command.cs b/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Command.cs index 3c81545..ce31fc2 100644 --- a/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Command.cs +++ b/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Command.cs @@ -1,4 +1,5 @@ using Cortex.Mediator.Commands; +using Lutra.Application.Models.Verspakketten; using System.ComponentModel.DataAnnotations; namespace Lutra.Application.Verspakketten; @@ -9,5 +10,6 @@ public sealed partial class CreateVerspakket string Naam, int? PrijsInCenten, [Range(1, 10)] int AantalPersonen, - Guid SupermarktId) : ICommand; + Guid SupermarktId, + Beoordeling? Beoordeling) : ICommand; } diff --git a/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Handler.cs b/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Handler.cs index 271f3b1..c459515 100644 --- a/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Handler.cs +++ b/Lutra/Lutra.Application/Verspakketten/CreateVerspakket.Handler.cs @@ -1,5 +1,6 @@ using Cortex.Mediator.Commands; using Lutra.Application.Interfaces; +using Lutra.Application.Models.Verspakketten; using Microsoft.EntityFrameworkCore; namespace Lutra.Application.Verspakketten; @@ -31,6 +32,21 @@ public sealed partial class CreateVerspakket ModifiedAt = now }; + if (request.Beoordeling is not null) + { + verspakket.AddBeoordeling(new Domain.Entities.Beoordeling + { + Id = Guid.NewGuid(), + CijferSmaak = request.Beoordeling.CijferSmaak, + CijferBereiden = request.Beoordeling.CijferBereiden, + Aanbevolen = request.Beoordeling.Aanbevolen, + Tekst = request.Beoordeling.Tekst, + VerspakketId = verspakket.Id, + CreatedAt = now, + ModifiedAt = now + }); + } + await context.Verspaketten.AddAsync(verspakket, cancellationToken); await context.SaveChangesAsync(cancellationToken);