Added tests, need to go through them manually to check if they make sense and are correct later
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user