Optionally add a beoordeling while adding a verspakket
This commit is contained in:
+13
-7
@@ -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/<FeatureArea>/` (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
|
||||
|
||||
@@ -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<CreateVerspakket.Response>();
|
||||
body!.Id.Should().NotBeEmpty();
|
||||
|
||||
var created = await Client.GetFromJsonAsync<GetVerspakket.Response>($"/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);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Cortex.Mediator;
|
||||
using Lutra.Application.Supermarkten;
|
||||
using Lutra.Application.Verspakketten;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Lutra.API.Controllers;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
|
||||
@@ -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<Domain.Entities.Verspakket>());
|
||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).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<Domain.Entities.Supermarkt>());
|
||||
|
||||
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<Domain.Entities.Verspakket, CancellationToken>((v, _) => savedVerspakket = v);
|
||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).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<Domain.Entities.Supermarkt>
|
||||
{
|
||||
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<Domain.Entities.Verspakket>());
|
||||
_contextMock
|
||||
.Setup(c => c.Verspaketten.AddAsync(It.IsAny<Domain.Entities.Verspakket>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => savedVerspakket = v);
|
||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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<Response>;
|
||||
Guid SupermarktId,
|
||||
Beoordeling? Beoordeling) : ICommand<Response>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user