Skip to content

test(api): cover Production fail-fast on missing health-check connection string#519

Merged
ilyanfraimbault merged 4 commits into
developfrom
claude/210-healthcheck-regression-test
Jun 16, 2026
Merged

test(api): cover Production fail-fast on missing health-check connection string#519
ilyanfraimbault merged 4 commits into
developfrom
claude/210-healthcheck-regression-test

Conversation

@ilyanfraimbault

Copy link
Copy Markdown
Owner

Contexte

Suite à #210 (déjà corrigé sur develop par 6418a25 + bc76c44). Le code du fail-fast est en place, mais aucun test ne le couvrait : si quelqu'un casse/supprime ce garde-fou plus tard, rien ne le signalerait.

Changement

  • backend/tests/TrueMain.IntegrationTests/HealthCheckBootstrapIntegrationTests.cs (test uniquement, aucun changement de code applicatif) : démarre le host en environnement Production sans ConnectionStrings:TrueMain et vérifie qu'une InvalidOperationException est levée au démarrage (au lieu de laisser /readyz vert alors que Postgres est injoignable).

Le test cible uniquement Production, le seul environnement où le fail-fast s'applique (tous les déploiements tournent en Production — cf. compose*.yaml). Pas de fixture Postgres requise : le host lève l'exception pendant l'enregistrement des services, bien avant de toucher la base — la classe reste donc hors de la collection d'intégration sérialisée.

Notes

  • dotnet n'étant pas disponible dans l'environnement d'exécution, le build/les tests n'ont pas pu être lancés localement — la CI les validera.

https://claude.ai/code/session_01B1tjxyzK9zDzMWcD8aitxX


Generated by Claude Code

…ion string

Adds a regression test for the #210 fail-fast in Api/Program.cs: booting the
host in Production without ConnectionStrings:TrueMain must throw at startup
rather than silently dropping the Npgsql "ready" check (which would leave
/readyz green while Postgres is unreachable). No Postgres fixture is needed —
the host throws during service registration — so the class stays out of the
serialised integration collection.
@ilyanfraimbault ilyanfraimbault marked this pull request as ready for review June 16, 2026 12:18
Comment on lines +26 to +31
// configuration up to the health-check branch. The discard keeps this an
// Action: a bare property access can't be a statement-bodied lambda, so
// dropping it would infer Func<IServiceProvider> and bind a different
// assertion overload that doesn't observe the startup throw.
var act = () => _ = factory.Services;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NON BLOQUANT — factory.CreateClient() déclencherait EnsureServer() de la même façon, retourne HttpClient (type concret ≠ IServiceProvider), donc la lambda () => factory.CreateClient() est inférée Action sans avoir besoin du discard ni du commentaire explicatif. C'est le pattern utilisé dans CorsStartupIntegrationTests et serait plus lisible ici :

Suggested change
// configuration up to the health-check branch. The discard keeps this an
// Action: a bare property access can't be a statement-bodied lambda, so
// dropping it would infer Func<IServiceProvider> and bind a different
// assertion overload that doesn't observe the startup throw.
var act = () => _ = factory.Services;
var act = () => factory.CreateClient();

La logique est identique : les deux chemins remontent l'exception levée à la ligne 52 de Program.cs pendant la phase de construction du host, bien avant app.Run().

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Le PR est propre. Un seul fichier, aucun changement de code applicatif — uniquement un nouveau test qui couvre le fail-fast deja en place.

Ce qui est verifie :

  • Le throw a la ligne 52 de Program.cs se produit bien avant builder.Build() (pendant l enregistrement des services), donc il precede les guards CORS (ValidateOnStart), AddDbContext, AddMongoLogging, etc. — la classe n a pas besoin d injecter Cors:Origins ni de Postgres.
  • L exclusion de [Collection(IntegrationCollection.Name)] est intentionnelle et correcte : pas de fixture Postgres requise, pas de risque de contention sur le reset de la base.
  • Le pattern WithMessage avec la sous-chaine correspond bien au message reel du throw.
  • La gestion using var factory garantit le Dispose() meme lorsque l acces a Services leve l exception ; WebApplicationFactory.Dispose() gere proprement le cas ou _host est null.
  • La reference a TrueMainWebApplicationFactory.DefaultOpsApiKey pour satisfaire [MinLength(32)] est correcte (meme pattern que CorsStartupIntegrationTests).

Suggestions (non bloquantes) :

  • Voir le commentaire inline sur les lignes 26-31 : factory.CreateClient() est un declencheur plus simple et coherent avec CorsStartupIntegrationTests — il evite le discard _ = et le long commentaire explicatif.

Adopts review feedback: factory.CreateClient() triggers host build the same way
and infers a clean Action lambda, matching CorsStartupIntegrationTests — drops
the discard and the explanatory comment.
public void Build_ShouldThrow_WhenConnectionStringMissingInProduction()
{
using var factory = new MissingConnectionStringFactory();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NON BLOQUANTCorsStartupIntegrationTests utilise le pattern async Task + await using var factory pour laisser WebApplicationFactory<T> effectuer son nettoyage asynchrone. Ici la factory ne démarre jamais (elle lève pendant l'enregistrement des services), donc la version synchrone fonctionne en pratique — mais pour rester cohérent avec le reste de la suite, on pourrait écrire :

Suggested change
public async Task Build_ShouldThrow_WhenConnectionStringMissingInProduction()

…et remplacer using var factory par await using var factory à la ligne suivante.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Le PR ajoute un unique fichier de test couvrant le garde-fou fail-fast introduit par #210. La logique est correcte et bien documentée.

Ce qui a été vérifié :

  • Le wildcard "*readiness health check can be registered in Production*" correspond exactement au message levé dans Api/Program.cs (lignes 52-54). ✓
  • L'environnement "Production" déclenche bien la branche else if — l'ordre dans Program.cs garantit que ce throw intervient avant toute validation CORS. ✓
  • Mettre ConnectionStrings:TrueMain = null dans AddInMemoryCollection (source la plus prioritaire) fait retourner null à GetConnectionString, satisfaisant string.IsNullOrWhiteSpace. ✓
  • Ops:ApiKey injecté pour éviter que la validation [MinLength(32)] d'OpsOptions ne pollue l'exception attendue. ✓
  • Absence intentionnelle de [Collection(IntegrationCollection.Name)] : aucun accès à Postgres n'est requis puisque la levée survient pendant l'enregistrement des services. ✓

Suggestions (non bloquantes) :

  • Voir commentaire inline ligne 24 : rendre le test async Task + await using var factory pour rester cohérent avec le pattern de CorsStartupIntegrationTests.

Adopts review feedback: matches CorsStartupIntegrationTests' async cleanup
pattern (async Task + await using var factory).
// Null out the connection string in the highest-precedence source
// so any value inherited from earlier providers is overridden, and
// satisfy OpsOptions' [MinLength(32)] so the missing-connection
// branch is the only thing left to fail the boot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NON BLOQUANT — commentaire légèrement trompeur

Le commentaire affirme que fournir Ops:ApiKey est nécessaire pour « laisser le missing-connection branch être le seul échec au boot ». Ce n'est pas tout à fait exact : le throw au fail-fast (Program.cs ligne 52) se produit pendant l'enregistrement des services, soit avant l'appel à builder.Build(). Or, ValidateOnStart() sur OpsOptions ne s'exécute qu'après Build(), lors du démarrage du host. En environnement Production sans connection string, le throw de la garde santé se déclenche donc systématiquement avant que la validation des options soit atteinte — Ops:ApiKey est de la défense en profondeur, pas une contrainte fonctionnelle pour isoler la bonne branche.

Suggestion (non obligatoire) :

Suggested change
// branch is the only thing left to fail the boot.
// Null out the connection string in the highest-precedence source
// so any value inherited from earlier providers is overridden.
// Ops:ApiKey is provided defensively (satisfies [MinLength(32)]) even
// though the health-check throw fires during service registration — before
// builder.Build() and therefore before ValidateOnStart() runs for any option.

Addresses review feedback: the health-check throw fires during service
registration, before Build() and thus before any ValidateOnStart — so Ops:ApiKey
is defense-in-depth, not functionally required to isolate the branch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Le test couvre exactement le guard-fail qu'il vise : démarrage en Production sans ConnectionStrings:TrueMainInvalidOperationException. La logique est saine, le pattern correspond à celui de CorsStartupIntegrationTests, et les choix de conception sont correctement documentés (isolation hors collection, Ops:ApiKey défensif, scope Production uniquement).

Points vérifiés

  • Message pattern : "*readiness health check can be registered in Production*" correspond exactement à la chaîne lancée ligne 52-54 de Api/Program.cs. ✓
  • Isolation : absence de [Collection(IntegrationCollection.Name)] intentionnelle — le host lève l'exception pendant l'enregistrement des services, avant tout accès Postgres. ✓
  • Précédence de config : AddInMemoryCollection ajouté en dernier → priorité maximale, écrase toute valeur héritée. ✓
  • async Task + assertion synchrone : await using justifie le async; CreateClient() est synchrone donc .Should().Throw<>() est correct — même pattern que CorsStartupIntegrationTests. ✓
  • Primary constructor vide : syntaxe C# 12 valide, cohérente avec la base de code. ✓

Aucun point bloquant.

@ilyanfraimbault ilyanfraimbault merged commit 70ac0cc into develop Jun 16, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants