From 3a1071dadc17ac28171cda0ed7d835a34545e036 Mon Sep 17 00:00:00 2001 From: moarten Date: Sun, 19 Apr 2026 20:55:26 +0200 Subject: [PATCH] Added tests, need to go through them manually to check if they make sense and are correct later --- .../SupermarktenControllerTests.cs | 157 ++++++++++ .../VerspakkettenControllerTests.cs | 270 ++++++++++++++++++ .../Infrastructure/IntegrationTestBase.cs | 51 ++++ .../Infrastructure/LutraApiFactory.cs | 69 +++++ .../Lutra.API.IntegrationTests.csproj | 32 +++ .../Controllers/VerspakkettenController.cs | 22 +- .../Lutra.Application.UnitTests.csproj | 29 ++ .../AddBeoordelingHandlerTests.cs | 72 +++++ .../CreateVerspakketHandlerTests.cs | 81 ++++++ Lutra/Lutra.sln | 82 ++++++ 10 files changed, 862 insertions(+), 3 deletions(-) create mode 100644 Lutra/Lutra.API.IntegrationTests/Controllers/SupermarktenControllerTests.cs create mode 100644 Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs create mode 100644 Lutra/Lutra.API.IntegrationTests/Infrastructure/IntegrationTestBase.cs create mode 100644 Lutra/Lutra.API.IntegrationTests/Infrastructure/LutraApiFactory.cs create mode 100644 Lutra/Lutra.API.IntegrationTests/Lutra.API.IntegrationTests.csproj create mode 100644 Lutra/Lutra.Application.UnitTests/Lutra.Application.UnitTests.csproj create mode 100644 Lutra/Lutra.Application.UnitTests/Verspakketten/AddBeoordelingHandlerTests.cs create mode 100644 Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs diff --git a/Lutra/Lutra.API.IntegrationTests/Controllers/SupermarktenControllerTests.cs b/Lutra/Lutra.API.IntegrationTests/Controllers/SupermarktenControllerTests.cs new file mode 100644 index 0000000..a6a81ae --- /dev/null +++ b/Lutra/Lutra.API.IntegrationTests/Controllers/SupermarktenControllerTests.cs @@ -0,0 +1,157 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using Lutra.API.IntegrationTests.Infrastructure; +using Lutra.Application.Supermarkten; +using Lutra.Domain.Entities; + +namespace Lutra.API.IntegrationTests.Controllers; + +public class SupermarktenControllerTests(LutraApiFactory factory) + : IntegrationTestBase(factory) +{ + // ── GET /api/supermarkten ───────────────────────────────────────────────── + + [Fact] + public async Task Get_ReturnsOk_WithEmptyList_WhenNoDataExists() + { + var response = await Client.GetAsync("/api/supermarkten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body.Should().NotBeNull(); + body!.Supermarkten.Should().BeEmpty(); + } + + [Fact] + public async Task Get_ReturnsSeededSupermarkt() + { + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Albert Heijn", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/supermarkten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Supermarkten.Should().HaveCount(1); + body.Supermarkten.First().Naam.Should().Be("Albert Heijn"); + } + + [Fact] + public async Task Get_ReturnsAllSeededSupermarkten() + { + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Albert Heijn", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Jumbo", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Picnic", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/supermarkten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Supermarkten.Should().HaveCount(3); + } + + // ── GET /api/supermarkten — pagination ──────────────────────────────────── + + [Fact] + public async Task Get_Pagination_ReturnsCorrectPage() + { + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Albert Heijn", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Jumbo", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Picnic", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/supermarkten?skip=1&take=1"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Supermarkten.Should().HaveCount(1); + // Handler orders by Naam ascending, so skip=1 skips "Albert Heijn" and returns "Jumbo". + body.Supermarkten.First().Naam.Should().Be("Jumbo"); + } + + [Fact] + public async Task Get_Pagination_ReturnsEmptyList_WhenSkipExceedsTotalCount() + { + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Albert Heijn", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/supermarkten?skip=10&take=50"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Supermarkten.Should().BeEmpty(); + } + + // ── GET /api/supermarkten — sorting ─────────────────────────────────────── + + [Fact] + public async Task Get_ReturnsSupermarkten_OrderedByNaamAscending() + { + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Picnic", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Albert Heijn", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/supermarkten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + var namen = body!.Supermarkten.Select(s => s.Naam).ToList(); + namen.Should().BeInAscendingOrder(); + } + + // ── Soft-delete behaviour ───────────────────────────────────────────────── + + [Fact] + public async Task Get_DoesNotReturn_SoftDeletedSupermarkt() + { + await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Verwijderd", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow, + DeletedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/supermarkten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Supermarkten.Should().BeEmpty(); + } +} diff --git a/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs b/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs new file mode 100644 index 0000000..830a513 --- /dev/null +++ b/Lutra/Lutra.API.IntegrationTests/Controllers/VerspakkettenControllerTests.cs @@ -0,0 +1,270 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using Lutra.API.IntegrationTests.Infrastructure; +using Lutra.API.Requests; +using Lutra.Application.Verspakketten; +using Lutra.Domain.Entities; + +namespace Lutra.API.IntegrationTests.Controllers; + +public class VerspakkettenControllerTests(LutraApiFactory factory) + : IntegrationTestBase(factory) +{ + // ── GET /api/verspakketten ──────────────────────────────────────────────── + + [Fact] + public async Task Get_ReturnsOk_WithEmptyList_WhenNoDataExists() + { + var response = await Client.GetAsync("/api/verspakketten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body.Should().NotBeNull(); + body!.Verspakketten.Should().BeEmpty(); + } + + [Fact] + public async Task Get_ReturnsSeededVerspakket() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "AH", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Lente Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/verspakketten"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Verspakketten.Should().HaveCount(1); + body.Verspakketten.First().Naam.Should().Be("Lente Pakket"); + } + + // ── GET /api/verspakketten/{id} ─────────────────────────────────────────── + + [Fact] + public async Task GetById_ReturnsNotFound_WhenVerspakketDoesNotExist() + { + var response = await Client.GetAsync($"/api/verspakketten/{Guid.NewGuid()}"); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task GetById_ReturnsVerspakket_WhenItExists() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Jumbo", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + var verspakket = await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Zomer Pakket", AantalPersonen = 4, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync($"/api/verspakketten/{verspakket.Id}"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Verspakket.Should().NotBeNull(); + body.Verspakket!.Naam.Should().Be("Zomer Pakket"); + } + + // ── POST /api/verspakketten ─────────────────────────────────────────────── + + [Fact] + public async Task Post_CreatesVerspakket_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); + var response = await Client.PostAsJsonAsync("/api/verspakketten", command); + + response.StatusCode.Should().Be(HttpStatusCode.Created); + var body = await response.Content.ReadFromJsonAsync(); + body!.Id.Should().NotBeEmpty(); + response.Headers.Location.Should().NotBeNull(); + } + + [Fact] + public async Task Post_ReturnsBadRequest_WhenSupermarktDoesNotExist() + { + var command = new CreateVerspakket.Command("Winter Pakket", 999, 2, Guid.NewGuid()); + var response = await Client.PostAsJsonAsync("/api/verspakketten", command); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + // ── PUT /api/verspakketten/{id} ─────────────────────────────────────────── + + [Fact] + public async Task Update_ReturnsNoContent_WhenVerspakketExists() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "AH", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + var verspakket = await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Oud Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var request = new UpdateVerspakketRequest("Nieuw Pakket", 1999, 3, supermarkt.Id); + var response = await Client.PutAsJsonAsync($"/api/verspakketten/{verspakket.Id}", request); + + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Fact] + public async Task Update_ReturnsNotFound_WhenVerspakketDoesNotExist() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "AH", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var request = new UpdateVerspakketRequest("Pakket", 999, 2, supermarkt.Id); + var response = await Client.PutAsJsonAsync($"/api/verspakketten/{Guid.NewGuid()}", request); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task Update_ReturnsBadRequest_WhenSupermarktDoesNotExist() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "AH", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + var verspakket = await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var request = new UpdateVerspakketRequest("Pakket", 999, 2, Guid.NewGuid()); + var response = await Client.PutAsJsonAsync($"/api/verspakketten/{verspakket.Id}", request); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + // ── GET /api/verspakketten — pagination & sorting ───────────────────────── + + [Fact] + public async Task Get_Pagination_ReturnsCorrectPage() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "AH", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Aardappel Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Broccoli Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Courgette Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/verspakketten?skip=1&take=1"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Verspakketten.Should().HaveCount(1); + body.Verspakketten.First().Naam.Should().Be("Broccoli Pakket"); + } + + [Fact] + public async Task Get_SortDescending_ReturnsItemsInReverseOrder() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "AH", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Aardappel Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Zomerpakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var response = await Client.GetAsync("/api/verspakketten?sortDirection=Descending"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body!.Verspakketten.First().Naam.Should().Be("Zomerpakket"); + body.Verspakketten.Last().Naam.Should().Be("Aardappel Pakket"); + } + + // ── POST /api/verspakketten/{id}/beoordelingen ──────────────────────────── + + [Fact] + public async Task AddBeoordeling_ReturnsBadRequest_WhenVerspakketDoesNotExist() + { + var command = new AddBeoordeling.Command(Guid.NewGuid(), 8, 7, true, "Heerlijk!"); + var response = await Client.PostAsJsonAsync($"/api/verspakketten/{command.VerspakketId}/beoordelingen", command); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task AddBeoordeling_Returns201_WhenVerspakketExists() + { + var supermarkt = await SeedAsync(new Supermarkt + { + Id = Guid.NewGuid(), Naam = "Lidl", + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + var verspakket = await SeedAsync(new Verspakket + { + Id = Guid.NewGuid(), Naam = "Basis Pakket", AantalPersonen = 2, + SupermarktId = supermarkt.Id, + CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow + }); + + var command = new AddBeoordeling.Command(verspakket.Id, 8, 7, true, "Heerlijk!"); + var response = await Client.PostAsJsonAsync($"/api/verspakketten/{verspakket.Id}/beoordelingen", command); + + response.StatusCode.Should().Be(HttpStatusCode.Created); + var body = await response.Content.ReadFromJsonAsync(); + body!.Id.Should().NotBeEmpty(); + } +} diff --git a/Lutra/Lutra.API.IntegrationTests/Infrastructure/IntegrationTestBase.cs b/Lutra/Lutra.API.IntegrationTests/Infrastructure/IntegrationTestBase.cs new file mode 100644 index 0000000..779ac84 --- /dev/null +++ b/Lutra/Lutra.API.IntegrationTests/Infrastructure/IntegrationTestBase.cs @@ -0,0 +1,51 @@ +using Lutra.Application.Interfaces; +using Lutra.Infrastructure.Sql; +using Microsoft.Extensions.DependencyInjection; + +namespace Lutra.API.IntegrationTests.Infrastructure; + +/// +/// Base class for integration tests. Provides a shared factory and helper methods +/// to seed and reset the database between tests. +/// +public abstract class IntegrationTestBase : IClassFixture, IAsyncLifetime +{ + protected readonly LutraApiFactory Factory; + protected readonly HttpClient Client; + + protected IntegrationTestBase(LutraApiFactory factory) + { + Factory = factory; + Client = factory.CreateClient(); + } + + /// Seed data or perform setup before each test. + public virtual Task InitializeAsync() + { + Factory.EnsureSchemaCreated(); + return Task.CompletedTask; + } + + /// Reset database state after each test. + public async Task DisposeAsync() + { + using var scope = Factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService() as LutraDbContext; + if (db is not null) + { + db.Beoordelingen.RemoveRange(db.Beoordelingen); + db.Verspaketten.RemoveRange(db.Verspaketten); + db.Supermarkten.RemoveRange(db.Supermarkten); + await db.SaveChangesAsync(CancellationToken.None); + } + } + + protected async Task SeedAsync(T entity) where T : class + { + using var scope = Factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService() as LutraDbContext; + db!.Set().Add(entity); + await db.SaveChangesAsync(CancellationToken.None); + return entity; + } +} diff --git a/Lutra/Lutra.API.IntegrationTests/Infrastructure/LutraApiFactory.cs b/Lutra/Lutra.API.IntegrationTests/Infrastructure/LutraApiFactory.cs new file mode 100644 index 0000000..de5444d --- /dev/null +++ b/Lutra/Lutra.API.IntegrationTests/Infrastructure/LutraApiFactory.cs @@ -0,0 +1,69 @@ +using Lutra.Application.Interfaces; +using Lutra.Infrastructure.Sql; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Lutra.API.IntegrationTests.Infrastructure; + +/// +/// Custom WebApplicationFactory that replaces the PostgreSQL database with SQLite in-memory +/// so that integration tests can run without a live database server. +/// A single SqliteConnection is kept open for the lifetime of the factory so that +/// all DI scopes share the same in-memory database. +/// +public class LutraApiFactory : WebApplicationFactory +{ + // Opened immediately so it is ready when ConfigureWebHost runs. + private readonly SqliteConnection _connection = new("Data Source=:memory:"); + private bool _schemaCreated; + + public LutraApiFactory() + { + _connection.Open(); + } + + /// Ensures the SQLite schema is created. Call once before the first test. + public void EnsureSchemaCreated() + { + if (_schemaCreated) return; + + using var scope = Services.CreateScope(); + var db = (LutraDbContext)scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + _schemaCreated = true; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + // EF Core 10 stores provider configuration in IDbContextOptionsConfiguration + // descriptors (one per AddDbContext call). All four registration types must be + // removed so neither Npgsql options nor its provider services survive into the + // SQLite registration. + services.RemoveAll(); + services.RemoveAll(); + services.RemoveAll>(); + services.RemoveAll(); + services.RemoveAll(typeof(IDbContextOptionsConfiguration)); + + // Register SQLite using the shared open connection. + services.AddDbContext(options => + options.UseSqlite(_connection)); + }); + + builder.UseEnvironment("Testing"); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + _connection.Dispose(); + } +} diff --git a/Lutra/Lutra.API.IntegrationTests/Lutra.API.IntegrationTests.csproj b/Lutra/Lutra.API.IntegrationTests/Lutra.API.IntegrationTests.csproj new file mode 100644 index 0000000..139006b --- /dev/null +++ b/Lutra/Lutra.API.IntegrationTests/Lutra.API.IntegrationTests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + false + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lutra/Lutra.API/Controllers/VerspakkettenController.cs b/Lutra/Lutra.API/Controllers/VerspakkettenController.cs index 7c5db45..8608aff 100644 --- a/Lutra/Lutra.API/Controllers/VerspakkettenController.cs +++ b/Lutra/Lutra.API/Controllers/VerspakkettenController.cs @@ -85,12 +85,28 @@ namespace Lutra.API.Controllers /// [HttpPut("{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Update(Guid id, [FromBody] UpdateVerspakketRequest request) { - var command = new UpdateVerspakket.Command(id, request.Naam, request.PrijsInCenten, request.AantalPersonen, request.SupermarktId); - await mediator.SendCommandAsync(command); - return NoContent(); + try + { + var command = new UpdateVerspakket.Command(id, request.Naam, request.PrijsInCenten, request.AantalPersonen, request.SupermarktId); + await mediator.SendCommandAsync(command); + return NoContent(); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + catch (InvalidOperationException ex) when (ex.Message.StartsWith($"Verspakket with id '{id}'")) + { + return NotFound(); + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } } /// diff --git a/Lutra/Lutra.Application.UnitTests/Lutra.Application.UnitTests.csproj b/Lutra/Lutra.Application.UnitTests/Lutra.Application.UnitTests.csproj new file mode 100644 index 0000000..e8d1d29 --- /dev/null +++ b/Lutra/Lutra.Application.UnitTests/Lutra.Application.UnitTests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lutra/Lutra.Application.UnitTests/Verspakketten/AddBeoordelingHandlerTests.cs b/Lutra/Lutra.Application.UnitTests/Verspakketten/AddBeoordelingHandlerTests.cs new file mode 100644 index 0000000..4393a2b --- /dev/null +++ b/Lutra/Lutra.Application.UnitTests/Verspakketten/AddBeoordelingHandlerTests.cs @@ -0,0 +1,72 @@ +using FluentAssertions; +using Lutra.Application.Interfaces; +using Lutra.Application.Verspakketten; +using Microsoft.EntityFrameworkCore; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Lutra.Application.UnitTests.Verspakketten; + +public class AddBeoordelingHandlerTests +{ + private readonly Mock _contextMock; + private readonly AddBeoordeling.Handler _handler; + + public AddBeoordelingHandlerTests() + { + _contextMock = new Mock(); + _handler = new AddBeoordeling.Handler(_contextMock.Object); + } + + [Fact] + public async Task Handle_VerspakketExists_AddsBeoordeling() + { + var verspakketId = Guid.NewGuid(); + var verspakketten = new List + { + new() { Id = verspakketId, Naam = "Test", AantalPersonen = 2, SupermarktId = Guid.NewGuid(), CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow } + }; + + _contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(verspakketten); + _contextMock.Setup(c => c.Beoordelingen).ReturnsDbSet(new List()); + _contextMock.Setup(c => c.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); + + var command = new AddBeoordeling.Command(verspakketId, 8, 7, true, "Lekker!"); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Id.Should().NotBeEmpty(); + _contextMock.Verify(c => c.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_VerspakketNotFound_ThrowsInvalidOperationException() + { + _contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(new List()); + + var command = new AddBeoordeling.Command(Guid.NewGuid(), 8, 7, true, null); + + var act = () => _handler.Handle(command, CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("*was not found*"); + } + + [Fact] + public async Task Handle_VerspakketDeleted_ThrowsInvalidOperationException() + { + var verspakketId = Guid.NewGuid(); + var verspakketten = new List + { + new() { Id = verspakketId, Naam = "Deleted", AantalPersonen = 2, SupermarktId = Guid.NewGuid(), CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow, DeletedAt = DateTime.UtcNow } + }; + + _contextMock.Setup(c => c.Verspaketten).ReturnsDbSet(verspakketten); + + var command = new AddBeoordeling.Command(verspakketId, 8, 7, true, null); + + var act = () => _handler.Handle(command, CancellationToken.None); + + await act.Should().ThrowAsync(); + } +} diff --git a/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs b/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs new file mode 100644 index 0000000..08d77c0 --- /dev/null +++ b/Lutra/Lutra.Application.UnitTests/Verspakketten/CreateVerspakketHandlerTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using Lutra.Application.Interfaces; +using Lutra.Application.Verspakketten; +using Moq; +using Moq.EntityFrameworkCore; + +namespace Lutra.Application.UnitTests.Verspakketten; + +public class CreateVerspakketHandlerTests +{ + private readonly Mock _contextMock; + private readonly CreateVerspakket.Handler _handler; + + public CreateVerspakketHandlerTests() + { + _contextMock = new Mock(); + _handler = new CreateVerspakket.Handler(_contextMock.Object); + } + + [Fact] + public async Task Handle_SupermarktExists_CreatesVerspakket() + { + var supermarktId = Guid.NewGuid(); + var supermarkten = new List + { + new() { Id = supermarktId, Naam = "AH", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow } + }; + + _contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(supermarkten); + _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 result = await _handler.Handle(command, CancellationToken.None); + + result.Id.Should().NotBeEmpty(); + _contextMock.Verify(c => c.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_SupermarktNotFound_ThrowsInvalidOperationException() + { + _contextMock.Setup(c => c.Supermarkten).ReturnsDbSet(new List()); + + var command = new CreateVerspakket.Command("Lente Pakket", 1299, 2, Guid.NewGuid()); + + var act = () => _handler.Handle(command, CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("*was not found*"); + } + + [Fact] + public async Task Handle_CreatesVerspakketWithCorrectProperties() + { + 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); + + await _handler.Handle(command, CancellationToken.None); + + savedVerspakket.Should().NotBeNull(); + savedVerspakket!.Naam.Should().Be("Zomer Pakket"); + savedVerspakket.PrijsInCenten.Should().Be(999); + savedVerspakket.AantalPersonen.Should().Be(4); + savedVerspakket.SupermarktId.Should().Be(supermarktId); + } +} diff --git a/Lutra/Lutra.sln b/Lutra/Lutra.sln index 04a0753..422f997 100644 --- a/Lutra/Lutra.sln +++ b/Lutra/Lutra.sln @@ -15,36 +15,118 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.AppHost", "Lutra.AppH EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.Infrastructure.Migrator", "Lutra.Infrastructure.Migrator\Lutra.Infrastructure.Migrator.csproj", "{6582486C-CEB7-78BF-D043-E0C3C0D6A923}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.Application.UnitTests", "Lutra.Application.UnitTests\Lutra.Application.UnitTests.csproj", "{923A138F-A7A1-4254-A95F-447977B7AAC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lutra.API.IntegrationTests", "Lutra.API.IntegrationTests\Lutra.API.IntegrationTests.csproj", "{E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lutra.Infrastructure", "Lutra.Infrastructure", "{DF712D4B-6D10-CA62-6EF1-190F54D65E10}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Debug|x64.ActiveCfg = Debug|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Debug|x64.Build.0 = Debug|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Debug|x86.ActiveCfg = Debug|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Debug|x86.Build.0 = Debug|Any CPU {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Release|Any CPU.Build.0 = Release|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Release|x64.ActiveCfg = Release|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Release|x64.Build.0 = Release|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Release|x86.ActiveCfg = Release|Any CPU + {22B74CFE-E9D0-42F3-96E3-818D014E1A2E}.Release|x86.Build.0 = Release|Any CPU {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Debug|x64.Build.0 = Debug|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Debug|x86.Build.0 = Debug|Any CPU {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Release|Any CPU.Build.0 = Release|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Release|x64.ActiveCfg = Release|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Release|x64.Build.0 = Release|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Release|x86.ActiveCfg = Release|Any CPU + {CD66D294-FB25-4CA1-8DCA-7EF3DDDFE0C5}.Release|x86.Build.0 = Release|Any CPU {DACBC33C-A76A-483A-8535-AA62D33D4977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DACBC33C-A76A-483A-8535-AA62D33D4977}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Debug|x64.ActiveCfg = Debug|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Debug|x64.Build.0 = Debug|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Debug|x86.ActiveCfg = Debug|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Debug|x86.Build.0 = Debug|Any CPU {DACBC33C-A76A-483A-8535-AA62D33D4977}.Release|Any CPU.ActiveCfg = Release|Any CPU {DACBC33C-A76A-483A-8535-AA62D33D4977}.Release|Any CPU.Build.0 = Release|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Release|x64.ActiveCfg = Release|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Release|x64.Build.0 = Release|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Release|x86.ActiveCfg = Release|Any CPU + {DACBC33C-A76A-483A-8535-AA62D33D4977}.Release|x86.Build.0 = Release|Any CPU {0D9E0DD9-E914-483F-8046-8A57B510E984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0D9E0DD9-E914-483F-8046-8A57B510E984}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Debug|x64.ActiveCfg = Debug|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Debug|x64.Build.0 = Debug|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Debug|x86.Build.0 = Debug|Any CPU {0D9E0DD9-E914-483F-8046-8A57B510E984}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D9E0DD9-E914-483F-8046-8A57B510E984}.Release|Any CPU.Build.0 = Release|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Release|x64.ActiveCfg = Release|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Release|x64.Build.0 = Release|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Release|x86.ActiveCfg = Release|Any CPU + {0D9E0DD9-E914-483F-8046-8A57B510E984}.Release|x86.Build.0 = Release|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|x64.ActiveCfg = Debug|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|x64.Build.0 = Debug|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|x86.ActiveCfg = Debug|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Debug|x86.Build.0 = Debug|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|Any CPU.Build.0 = Release|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|x64.ActiveCfg = Release|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|x64.Build.0 = Release|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|x86.ActiveCfg = Release|Any CPU + {B9B41350-EB18-4564-81AB-C16F3E5A0CBB}.Release|x86.Build.0 = Release|Any CPU {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|x64.ActiveCfg = Debug|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|x64.Build.0 = Debug|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|x86.ActiveCfg = Debug|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Debug|x86.Build.0 = Debug|Any CPU {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|Any CPU.ActiveCfg = Release|Any CPU {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|Any CPU.Build.0 = Release|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|x64.ActiveCfg = Release|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|x64.Build.0 = Release|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|x86.ActiveCfg = Release|Any CPU + {6582486C-CEB7-78BF-D043-E0C3C0D6A923}.Release|x86.Build.0 = Release|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Debug|x64.Build.0 = Debug|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Debug|x86.Build.0 = Debug|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Release|Any CPU.Build.0 = Release|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Release|x64.ActiveCfg = Release|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Release|x64.Build.0 = Release|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Release|x86.ActiveCfg = Release|Any CPU + {923A138F-A7A1-4254-A95F-447977B7AAC0}.Release|x86.Build.0 = Release|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Debug|x64.Build.0 = Debug|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Debug|x86.Build.0 = Debug|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Release|Any CPU.Build.0 = Release|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Release|x64.ActiveCfg = Release|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Release|x64.Build.0 = Release|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Release|x86.ActiveCfg = Release|Any CPU + {E1B105D6-DCC2-42F5-9CB9-3A295804DCF6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE