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.
|
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.API`
|
||||||
- `Lutra.AppHost`
|
- `Lutra.AppHost`
|
||||||
- `Lutra.Infrastructure.Migrator`
|
- `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
|
- Database: PostgreSQL via EF Core
|
||||||
- Orchestration: .NET Aspire
|
- Orchestration: .NET Aspire
|
||||||
- Messaging pattern: `Cortex.Mediator` for CQRS handling
|
- 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
|
## 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`.
|
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
|
## 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.
|
- Use `Cortex.Mediator` request and handler interfaces.
|
||||||
- Keep handlers focused on orchestration and application logic.
|
- Keep handlers focused on orchestration and application logic.
|
||||||
- Use `ILutraDbContext` rather than referencing the concrete DbContext directly when possible.
|
- 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
|
## Infrastructure rules
|
||||||
|
|
||||||
@@ -89,10 +93,12 @@ Add FluentValidation, AutoMapper, NUnit, Shouldly, Moq, or Respawn only if the r
|
|||||||
## Testing guidance
|
## Testing guidance
|
||||||
|
|
||||||
- If tests exist, update or add tests alongside behavior changes.
|
- If tests exist, update or add tests alongside behavior changes.
|
||||||
- Follow the reference template’s intent for test coverage:
|
- Follow the reference template's intent for test coverage:
|
||||||
- unit tests for business logic
|
- unit tests for business logic (`Lutra.Application.UnitTests`)
|
||||||
- integration tests for infrastructure and data access
|
- integration tests for API behavior (`Lutra.API.IntegrationTests`)
|
||||||
- functional tests for API behavior
|
- 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.
|
- Keep tests readable and focused on behavior.
|
||||||
|
|
||||||
## When making changes
|
## When making changes
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public class VerspakkettenControllerTests(LutraApiFactory factory)
|
|||||||
CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow
|
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);
|
var response = await Client.PostAsJsonAsync("/api/verspakketten", command);
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||||
@@ -100,10 +100,43 @@ public class VerspakkettenControllerTests(LutraApiFactory factory)
|
|||||||
response.Headers.Location.Should().NotBeNull();
|
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]
|
[Fact]
|
||||||
public async Task Post_ReturnsBadRequest_WhenSupermarktDoesNotExist()
|
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);
|
var response = await Client.PostAsJsonAsync("/api/verspakketten", command);
|
||||||
|
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Cortex.Mediator;
|
using Cortex.Mediator;
|
||||||
using Lutra.Application.Supermarkten;
|
using Lutra.Application.Supermarkten;
|
||||||
using Lutra.Application.Verspakketten;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Lutra.API.Controllers;
|
namespace Lutra.API.Controllers;
|
||||||
|
|||||||
@@ -14,6 +14,22 @@ namespace Lutra.API
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
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(
|
builder.Services.AddCortexMediator(
|
||||||
handlerAssemblyMarkerTypes: [typeof(Program), typeof(GetVerspakketten)],
|
handlerAssemblyMarkerTypes: [typeof(Program), typeof(GetVerspakketten)],
|
||||||
options => options.AddDefaultBehaviors()
|
options => options.AddDefaultBehaviors()
|
||||||
@@ -37,6 +53,8 @@ namespace Lutra.API
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseCors("AllowLocalDevelopment");
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Lutra.Application.Interfaces;
|
using Lutra.Application.Interfaces;
|
||||||
|
using Lutra.Application.Models.Verspakketten;
|
||||||
using Lutra.Application.Verspakketten;
|
using Lutra.Application.Verspakketten;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Moq.EntityFrameworkCore;
|
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.Verspaketten).ReturnsDbSet(new List<Domain.Entities.Verspakket>());
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
_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);
|
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>());
|
_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);
|
var act = () => _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ public class CreateVerspakketHandlerTests
|
|||||||
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => savedVerspakket = v);
|
.Callback<Domain.Entities.Verspakket, CancellationToken>((v, _) => savedVerspakket = v);
|
||||||
_contextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
_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);
|
await _handler.Handle(command, CancellationToken.None);
|
||||||
|
|
||||||
@@ -78,4 +79,45 @@ public class CreateVerspakketHandlerTests
|
|||||||
savedVerspakket.AantalPersonen.Should().Be(4);
|
savedVerspakket.AantalPersonen.Should().Be(4);
|
||||||
savedVerspakket.SupermarktId.Should().Be(supermarktId);
|
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
|
public class Beoordeling
|
||||||
{
|
{
|
||||||
|
[Range(1, 10)]
|
||||||
public required int CijferSmaak { get; init; }
|
public required int CijferSmaak { get; init; }
|
||||||
|
|
||||||
|
[Range(1, 10)]
|
||||||
public required int CijferBereiden { get; init; }
|
public required int CijferBereiden { get; init; }
|
||||||
|
|
||||||
public required bool Aanbevolen { get; init; }
|
public required bool Aanbevolen { get; init; }
|
||||||
|
|
||||||
|
[MaxLength(1024)]
|
||||||
public string? Tekst { get; init; }
|
public string? Tekst { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Cortex.Mediator.Commands;
|
using Cortex.Mediator.Commands;
|
||||||
|
using Lutra.Application.Models.Verspakketten;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
namespace Lutra.Application.Verspakketten;
|
||||||
@@ -9,5 +10,6 @@ public sealed partial class CreateVerspakket
|
|||||||
string Naam,
|
string Naam,
|
||||||
int? PrijsInCenten,
|
int? PrijsInCenten,
|
||||||
[Range(1, 10)] int AantalPersonen,
|
[Range(1, 10)] int AantalPersonen,
|
||||||
Guid SupermarktId) : ICommand<Response>;
|
Guid SupermarktId,
|
||||||
|
Beoordeling? Beoordeling) : ICommand<Response>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Cortex.Mediator.Commands;
|
using Cortex.Mediator.Commands;
|
||||||
using Lutra.Application.Interfaces;
|
using Lutra.Application.Interfaces;
|
||||||
|
using Lutra.Application.Models.Verspakketten;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Lutra.Application.Verspakketten;
|
namespace Lutra.Application.Verspakketten;
|
||||||
@@ -31,6 +32,21 @@ public sealed partial class CreateVerspakket
|
|||||||
ModifiedAt = now
|
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.Verspaketten.AddAsync(verspakket, cancellationToken);
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user