Added tests, need to go through them manually to check if they make sense and are correct later

This commit is contained in:
moarten
2026-04-19 20:55:26 +02:00
parent 11bff0de63
commit 3a1071dadc
10 changed files with 862 additions and 3 deletions

View File

@@ -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<GetSupermarkten.Response>();
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<GetSupermarkten.Response>();
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<GetSupermarkten.Response>();
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<GetSupermarkten.Response>();
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<GetSupermarkten.Response>();
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<GetSupermarkten.Response>();
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<GetSupermarkten.Response>();
body!.Supermarkten.Should().BeEmpty();
}
}

View File

@@ -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<GetVerspakketten.Response>();
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<GetVerspakketten.Response>();
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<GetVerspakket.Response>();
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<CreateVerspakket.Response>();
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<GetVerspakketten.Response>();
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<GetVerspakketten.Response>();
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<AddBeoordeling.Response>();
body!.Id.Should().NotBeEmpty();
}
}

View File

@@ -0,0 +1,51 @@
using Lutra.Application.Interfaces;
using Lutra.Infrastructure.Sql;
using Microsoft.Extensions.DependencyInjection;
namespace Lutra.API.IntegrationTests.Infrastructure;
/// <summary>
/// Base class for integration tests. Provides a shared factory and helper methods
/// to seed and reset the database between tests.
/// </summary>
public abstract class IntegrationTestBase : IClassFixture<LutraApiFactory>, IAsyncLifetime
{
protected readonly LutraApiFactory Factory;
protected readonly HttpClient Client;
protected IntegrationTestBase(LutraApiFactory factory)
{
Factory = factory;
Client = factory.CreateClient();
}
/// <summary>Seed data or perform setup before each test.</summary>
public virtual Task InitializeAsync()
{
Factory.EnsureSchemaCreated();
return Task.CompletedTask;
}
/// <summary>Reset database state after each test.</summary>
public async Task DisposeAsync()
{
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ILutraDbContext>() 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<T> SeedAsync<T>(T entity) where T : class
{
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ILutraDbContext>() as LutraDbContext;
db!.Set<T>().Add(entity);
await db.SaveChangesAsync(CancellationToken.None);
return entity;
}
}

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public class LutraApiFactory : WebApplicationFactory<Program>
{
// Opened immediately so it is ready when ConfigureWebHost runs.
private readonly SqliteConnection _connection = new("Data Source=:memory:");
private bool _schemaCreated;
public LutraApiFactory()
{
_connection.Open();
}
/// <summary>Ensures the SQLite schema is created. Call once before the first test.</summary>
public void EnsureSchemaCreated()
{
if (_schemaCreated) return;
using var scope = Services.CreateScope();
var db = (LutraDbContext)scope.ServiceProvider.GetRequiredService<ILutraDbContext>();
db.Database.EnsureCreated();
_schemaCreated = true;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// EF Core 10 stores provider configuration in IDbContextOptionsConfiguration<T>
// 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<ILutraDbContext>();
services.RemoveAll<LutraDbContext>();
services.RemoveAll<DbContextOptions<LutraDbContext>>();
services.RemoveAll<DbContextOptions>();
services.RemoveAll(typeof(IDbContextOptionsConfiguration<LutraDbContext>));
// Register SQLite using the shared open connection.
services.AddDbContext<ILutraDbContext, LutraDbContext>(options =>
options.UseSqlite(_connection));
});
builder.UseEnvironment("Testing");
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_connection.Dispose();
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="8.9.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lutra.API\Lutra.API.csproj" />
<ProjectReference Include="..\Lutra.Infrastructure\Lutra.Infrastructure.Sql.csproj" />
</ItemGroup>
</Project>