From c2d4584e60883fcc714fce42d4a68cd8e0a733fb Mon Sep 17 00:00:00 2001 From: Edwin Yangwhich <112264917+Lightsul@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:02:39 -0400 Subject: [PATCH 1/6] =?UTF-8?q?feat(api):=20configurer=20le=20DbContext=20?= =?UTF-8?q?et=20la=20connexion=20=C3=A0=20PostgreSQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Retire le template WeatherForecast (contrôleur, modèle, requête .http) - Ajoute MoissonDbContext dans Data/ et l'enregistre avec Npgsql - Lit ConnectionStrings:Moisson depuis la configuration (user-secrets en local, variables d'environnement sinon) ; l'API refuse de démarrer si la chaîne est absente - Fixe Microsoft.EntityFrameworkCore.Relational en 10.0.13 pour régler le conflit de versions d'EF Core dans Tests --- .../Controllers/WeatherForecastController.cs | 25 ------------------- .../Data/MoissonDbContext.cs | 8 ++++++ .../MoissonMontreal.Api.csproj | 4 ++- .../MoissonMontreal.Api.http | 4 +-- src/MoissonMontreal.Api/Program.cs | 11 ++++++++ src/MoissonMontreal.Api/WeatherForecast.cs | 12 --------- 6 files changed, 23 insertions(+), 41 deletions(-) delete mode 100644 src/MoissonMontreal.Api/Controllers/WeatherForecastController.cs create mode 100644 src/MoissonMontreal.Api/Data/MoissonDbContext.cs delete mode 100644 src/MoissonMontreal.Api/WeatherForecast.cs diff --git a/src/MoissonMontreal.Api/Controllers/WeatherForecastController.cs b/src/MoissonMontreal.Api/Controllers/WeatherForecastController.cs deleted file mode 100644 index bf425b8..0000000 --- a/src/MoissonMontreal.Api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace MoissonMontreal.Api.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = - [ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - ]; - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/src/MoissonMontreal.Api/Data/MoissonDbContext.cs b/src/MoissonMontreal.Api/Data/MoissonDbContext.cs new file mode 100644 index 0000000..bc3c387 --- /dev/null +++ b/src/MoissonMontreal.Api/Data/MoissonDbContext.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +namespace MoissonMontreal.Api.Data; + +public class MoissonDbContext(DbContextOptions options) : DbContext(options) +{ + // Les tables (DbSet, DbSet, ...) seront ajoutées avec l'issue du schéma. +} diff --git a/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj b/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj index a3c98d8..bc7ae47 100644 --- a/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj +++ b/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj @@ -1,9 +1,10 @@ - + net10.0 enable enable + 61115b38-e487-4411-983f-5d85d34daf9f @@ -12,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/MoissonMontreal.Api/MoissonMontreal.Api.http b/src/MoissonMontreal.Api/MoissonMontreal.Api.http index 01669a9..ed734ad 100644 --- a/src/MoissonMontreal.Api/MoissonMontreal.Api.http +++ b/src/MoissonMontreal.Api/MoissonMontreal.Api.http @@ -1,6 +1,4 @@ @MoissonMontreal.Api_HostAddress = http://localhost:5297 -GET {{MoissonMontreal.Api_HostAddress}}/weatherforecast/ -Accept: application/json - +GET {{MoissonMontreal.Api_HostAddress}}/health ### diff --git a/src/MoissonMontreal.Api/Program.cs b/src/MoissonMontreal.Api/Program.cs index 666a9c5..a52ecb7 100644 --- a/src/MoissonMontreal.Api/Program.cs +++ b/src/MoissonMontreal.Api/Program.cs @@ -1,5 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using MoissonMontreal.Api.Data; + + var builder = WebApplication.CreateBuilder(args); +var connectionString = builder.Configuration.GetConnectionString("Moisson") + ?? throw new InvalidOperationException( + "La chaîne de connexion 'Moisson' est absente. Définissez ConnectionStrings__Moisson " + + "ou utilisez 'dotnet user-secrets' (voir README)."); + +builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); + // Add services to the container. builder.Services.AddControllers(); diff --git a/src/MoissonMontreal.Api/WeatherForecast.cs b/src/MoissonMontreal.Api/WeatherForecast.cs deleted file mode 100644 index be41ad1..0000000 --- a/src/MoissonMontreal.Api/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MoissonMontreal.Api; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} From 4be3b27574ab78af7c85eb05132db7f4e44dbd5c Mon Sep 17 00:00:00 2001 From: Edwin Yangwhich <112264917+Lightsul@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:19:11 -0400 Subject: [PATCH 2/6] =?UTF-8?q?feat(api):=20ajouter=20l'endpoint=20/health?= =?UTF-8?q?=20avec=20v=C3=A9rification=20de=20la=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajoute le package HealthChecks.EntityFrameworkCore 10.0.13 - Expose /health : Healthy (200) si PostgreSQL répond, Unhealthy (503) sinon - Rend le message d'erreur de connexion autonome (commande user-secrets incluse) --- src/MoissonMontreal.Api/MoissonMontreal.Api.csproj | 1 + src/MoissonMontreal.Api/Program.cs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj b/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj index bc7ae47..16e4907 100644 --- a/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj +++ b/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj @@ -14,6 +14,7 @@ all + diff --git a/src/MoissonMontreal.Api/Program.cs b/src/MoissonMontreal.Api/Program.cs index a52ecb7..6bf0a9e 100644 --- a/src/MoissonMontreal.Api/Program.cs +++ b/src/MoissonMontreal.Api/Program.cs @@ -9,10 +9,14 @@ "La chaîne de connexion 'Moisson' est absente. Définissez ConnectionStrings__Moisson " + "ou utilisez 'dotnet user-secrets' (voir README)."); -builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); // Add services to the container. +builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); + +builder.Services.AddHealthChecks() + .AddDbContextCheck("database"); + builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); @@ -31,4 +35,6 @@ app.MapControllers(); +app.MapHealthChecks("/health"); + app.Run(); From 0059579284f7181000c4fba1ad47dbc306951d1e Mon Sep 17 00:00:00 2001 From: Edwin Yangwhich <112264917+Lightsul@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:28:01 -0400 Subject: [PATCH 3/6] =?UTF-8?q?test(api):=20ajouter=20les=20tests=20d'int?= =?UTF-8?q?=C3=A9gration=20de=20l'endpoint=20/health?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WebApplicationFactory pour démarrer l'API en mémoire - Testcontainers pour une base PostgreSQL 17 jetable - Vérifie Healthy (200) quand la base répond et Unhealthy (503) quand elle est injoignable --- .../Api/HealthEndpointTests.cs | 45 +++++++++++++++++++ .../MoissonMontreal.Tests.csproj | 2 + 2 files changed, 47 insertions(+) create mode 100644 tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs diff --git a/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs b/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs new file mode 100644 index 0000000..ec63456 --- /dev/null +++ b/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs @@ -0,0 +1,45 @@ +using System.Net; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Testcontainers.PostgreSql; + +namespace MoissonMontreal.Tests.Api; + +public class HealthEndpointTests : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder("postgres:17").Build(); + + public Task InitializeAsync() => _postgres.StartAsync(); + + public Task DisposeAsync() => _postgres.DisposeAsync().AsTask(); + + [Fact] + public async Task Health_RetourneHealthy_QuandLaBaseRepond() + { + await using var factory = CreerFactory(_postgres.GetConnectionString()); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Health_RetourneUnhealthy_QuandLaBaseEstInjoignable() + { + // Rien n'écoute sur le port 1 : la connexion échoue immédiatement. + await using var factory = CreerFactory("Host=localhost;Port=1;Database=inexistante;Username=test;Timeout=2"); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/health"); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); + } + + private static WebApplicationFactory CreerFactory(string connectionString) => + new WebApplicationFactory().WithWebHostBuilder(builder => builder + .UseEnvironment("Testing") + .UseSetting("ConnectionStrings:Moisson", connectionString)); +} diff --git a/tests/MoissonMontreal.Tests/MoissonMontreal.Tests.csproj b/tests/MoissonMontreal.Tests/MoissonMontreal.Tests.csproj index bcb3521..4aae91e 100644 --- a/tests/MoissonMontreal.Tests/MoissonMontreal.Tests.csproj +++ b/tests/MoissonMontreal.Tests/MoissonMontreal.Tests.csproj @@ -9,7 +9,9 @@ + + From b8e802bb37c11a2ddfb5b66ef1cc1053bb764d25 Mon Sep 17 00:00:00 2001 From: Edwin Yangwhich <112264917+Lightsul@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:31:09 -0400 Subject: [PATCH 4/6] fix(api): retirer le renvoi au README du message d'erreur de connexion --- src/MoissonMontreal.Api/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/MoissonMontreal.Api/Program.cs b/src/MoissonMontreal.Api/Program.cs index 6bf0a9e..39ca713 100644 --- a/src/MoissonMontreal.Api/Program.cs +++ b/src/MoissonMontreal.Api/Program.cs @@ -7,11 +7,10 @@ var connectionString = builder.Configuration.GetConnectionString("Moisson") ?? throw new InvalidOperationException( "La chaîne de connexion 'Moisson' est absente. Définissez ConnectionStrings__Moisson " + - "ou utilisez 'dotnet user-secrets' (voir README)."); + "ou utilisez 'dotnet user-secrets'. "); // Add services to the container. - builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); builder.Services.AddHealthChecks() From d606f29bc357e57e41566b82a1f939e8e65bbd39 Mon Sep 17 00:00:00 2001 From: Edwin Yangwhich <112264917+Lightsul@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:17:11 -0400 Subject: [PATCH 5/6] =?UTF-8?q?chore(api):=20retirer=20la=20r=C3=A9f=C3=A9?= =?UTF-8?q?rence=20redondante=20=C3=A0=20EF=20Core=20Relational?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Déjà fournie en 10.0.13 via HealthChecks.EntityFrameworkCore - Retire un espace en trop dans le message d'erreur de connexion --- src/MoissonMontreal.Api/MoissonMontreal.Api.csproj | 1 - src/MoissonMontreal.Api/Program.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj b/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj index 16e4907..b7495b4 100644 --- a/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj +++ b/src/MoissonMontreal.Api/MoissonMontreal.Api.csproj @@ -13,7 +13,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/src/MoissonMontreal.Api/Program.cs b/src/MoissonMontreal.Api/Program.cs index 39ca713..7731696 100644 --- a/src/MoissonMontreal.Api/Program.cs +++ b/src/MoissonMontreal.Api/Program.cs @@ -7,7 +7,7 @@ var connectionString = builder.Configuration.GetConnectionString("Moisson") ?? throw new InvalidOperationException( "La chaîne de connexion 'Moisson' est absente. Définissez ConnectionStrings__Moisson " + - "ou utilisez 'dotnet user-secrets'. "); + "ou utilisez 'dotnet user-secrets'."); // Add services to the container. From 5f11bd01825ac8ff91b2299afd9809683629389e Mon Sep 17 00:00:00 2001 From: Edwin Yangwhich Date: Thu, 29 Oct 2026 23:18:51 -0400 Subject: [PATCH 6/6] fix: address review feedback on API setup and health tests - Add local dev connection string to appsettings.Development.json - Exclude /health from HTTPS redirection so HTTP probes get the real status - Share a single PostgreSQL container across health tests via IClassFixture - Tag health tests as Integration and document the Docker requirement in the README - Use 127.0.0.1 instead of localhost for the unreachable database test --- README.md | 10 ++++++++ src/MoissonMontreal.Api/Program.cs | 5 +++- .../appsettings.Development.json | 3 +++ .../Api/HealthEndpointTests.cs | 23 ++++++++----------- .../Fixtures/PostgresFixture.cs | 17 ++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 tests/MoissonMontreal.Tests/Fixtures/PostgresFixture.cs diff --git a/README.md b/README.md index 17f82c1..2e7ed91 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # moisson-project Windows desktop application for accurate donation valuation and tax receipt generation for Moisson Montréal (Montréal, QC). +## Running tests + +```bash +dotnet test # all tests (Docker must be running) +dotnet test --filter "Category!=Integration" # unit tests only, no Docker needed +``` + +Integration tests use Testcontainers to start a disposable PostgreSQL 17 container, so Docker Desktop (or Docker Engine) must be running. + + diff --git a/src/MoissonMontreal.Api/Program.cs b/src/MoissonMontreal.Api/Program.cs index 7731696..fe32925 100644 --- a/src/MoissonMontreal.Api/Program.cs +++ b/src/MoissonMontreal.Api/Program.cs @@ -28,7 +28,10 @@ app.MapOpenApi(); } -app.UseHttpsRedirection(); +// Health probes call /health over plain HTTP, so it must not be redirected. +app.UseWhen( + context => !context.Request.Path.StartsWithSegments("/health"), + branch => branch.UseHttpsRedirection()); app.UseAuthorization(); diff --git a/src/MoissonMontreal.Api/appsettings.Development.json b/src/MoissonMontreal.Api/appsettings.Development.json index 0c208ae..1e60eb8 100644 --- a/src/MoissonMontreal.Api/appsettings.Development.json +++ b/src/MoissonMontreal.Api/appsettings.Development.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Moisson": "Host=localhost;Port=5432;Database=moisson_dev;Username=moisson;Password=moisson" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs b/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs index ec63456..3c9d7ca 100644 --- a/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs +++ b/tests/MoissonMontreal.Tests/Api/HealthEndpointTests.cs @@ -1,22 +1,17 @@ using System.Net; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Testcontainers.PostgreSql; +using MoissonMontreal.Tests.Fixtures; namespace MoissonMontreal.Tests.Api; -public class HealthEndpointTests : IAsyncLifetime +[Trait("Category", "Integration")] +public class HealthEndpointTests(PostgresFixture postgres) : IClassFixture { - private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder("postgres:17").Build(); - - public Task InitializeAsync() => _postgres.StartAsync(); - - public Task DisposeAsync() => _postgres.DisposeAsync().AsTask(); - [Fact] - public async Task Health_RetourneHealthy_QuandLaBaseRepond() + public async Task Health_ReturnsHealthy_WhenDatabaseIsReachable() { - await using var factory = CreerFactory(_postgres.GetConnectionString()); + await using var factory = CreateFactory(postgres.ConnectionString); using var client = factory.CreateClient(); var response = await client.GetAsync("/health"); @@ -26,10 +21,10 @@ public async Task Health_RetourneHealthy_QuandLaBaseRepond() } [Fact] - public async Task Health_RetourneUnhealthy_QuandLaBaseEstInjoignable() + public async Task Health_ReturnsUnhealthy_WhenDatabaseIsUnreachable() { - // Rien n'écoute sur le port 1 : la connexion échoue immédiatement. - await using var factory = CreerFactory("Host=localhost;Port=1;Database=inexistante;Username=test;Timeout=2"); + // Nothing listens on port 1, so the connection is refused immediately. + await using var factory = CreateFactory("Host=127.0.0.1;Port=1;Database=unreachable;Username=test;Timeout=2"); using var client = factory.CreateClient(); var response = await client.GetAsync("/health"); @@ -38,7 +33,7 @@ public async Task Health_RetourneUnhealthy_QuandLaBaseEstInjoignable() Assert.Equal("Unhealthy", await response.Content.ReadAsStringAsync()); } - private static WebApplicationFactory CreerFactory(string connectionString) => + private static WebApplicationFactory CreateFactory(string connectionString) => new WebApplicationFactory().WithWebHostBuilder(builder => builder .UseEnvironment("Testing") .UseSetting("ConnectionStrings:Moisson", connectionString)); diff --git a/tests/MoissonMontreal.Tests/Fixtures/PostgresFixture.cs b/tests/MoissonMontreal.Tests/Fixtures/PostgresFixture.cs new file mode 100644 index 0000000..145445b --- /dev/null +++ b/tests/MoissonMontreal.Tests/Fixtures/PostgresFixture.cs @@ -0,0 +1,17 @@ +using Testcontainers.PostgreSql; + +namespace MoissonMontreal.Tests.Fixtures; + +/// +/// Starts a single PostgreSQL container shared by every test of a class. +/// +public class PostgresFixture : IAsyncLifetime +{ + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder("postgres:17").Build(); + + public string ConnectionString => _container.GetConnectionString(); + + public Task InitializeAsync() => _container.StartAsync(); + + public Task DisposeAsync() => _container.DisposeAsync().AsTask(); +} \ No newline at end of file