API Integration Test Conventions¶
Le contrat, court. Le how-to complet (decision tree, exemples descriptors, fixtures avancées, auth, MSW, cron/worker, harness, boucle de dev, snapshot) vit dans le skill upsert-integration-tests — création/complétion guidée. Review du diff : skill review-integration-tests. Cadre Vitest commun + AI-slop checklist : test-conventions.mdc. Cadrage initial : BRI-324.
Cibles¶
Un test d'intégration porte sur un controller HTTP, une cron task, un worker ou un webhook — uniquement, pour l'instant. Logique pure (business, validators, helpers) → unit test (skill upsert-unit-tests). Jamais de tests spontanés pendant une implem : poser la question en fin d'implem (cf. api-conventions.mdc §Tests).
Architecture — trois couches¶
Tests qui parlent SQL connaissent le schéma : un rename de colonne touche N tests. Les reads passent par le repo prod ; les inserts par une fixture seed dédiée.
Layer 0 — endpoint descriptor @bricks-common/api-communication[-bricksoffice]
└→ Layer 1 — *.integration-test.ts describe(route) > describe(Quand) > it(Alors)
├→ reads : <module>.repository (PROD repo, méthodes utiles aux tests vivent ici)
└→ writes : <entity>.seed.ts (DB seed test-only)
└→ <entity>.build.ts (builder pur, no await, no DB)
Dans *.integration-test.ts : interdit db.{selectFrom,insertInto,updateTable,deleteFrom}(...) inline. Vérifié par pnpm --filter @bricks/api lint (script scripts/lint-conventions/lint-integration-test-rules.ts).
Reads — dans le repo prod, jamais dupliqués¶
Tout read utile à un test est ajouté au repo prod (<module>.repository.ts) puis importé directement — la grande majorité (findByEmail, countAll, existsBy*) a aussi du sens en prod ; un test-side facade = drift garanti. Exception rare : un read sans aucun sens prod va dans le .seed.ts du fixture qui en a besoin.
Writes — fixtures build + seed¶
build<Entity>(overrides) pur et plat (défauts = cas majoritaire, no await, no DB) ; seed<Entity>(overrides) écrit via l'API publique de la lib, le repo prod ou TypeORM — jamais de raw SQL qui bypasse une validation/audit prod (exception cadrée : table/colonne sans entité ni repo — rien à bypasser — justifiée inline, cf. investor-investment-plan.seed.ts). Le discriminant de variante (type, kind…) reste requis dans les overrides (pas de défaut). Les seeds possèdent leur graphe de FK. Pas de lib de factory ni de traits chaînés. Détails (couche d'écriture, grosses entités, fragments d'override, createdAt) : skill upsert.
Seeder de quoi discriminer¶
Dès qu'un it asserte un filtre, un tri, une exclusion ou une agrégation, seeder ≥ 2 rows dont au moins une qui ne doit PAS sortir (ou sort à une autre place) — resetAll() vide la DB avant chaque test : avec une seule row positive, un endpoint cassé renvoie la même row et le test ne prouve rien. Exception : lookup par id unique (identité, soldes, 404) → une seule row suffit.
Routes via descriptors¶
Path + body + response viennent du descriptor api-communication[-bricksoffice] — jamais d'URL hardcodée pour une route Bricks, pas de const single-use (inline au call site avec satisfies). Deux exceptions cadrées : routes better-auth (/api/auth/*, montées par la lib) et webhooks providers (appelés par le provider — payload via builder typé par le validator prod, vrai x-api-key lu dans l'env, jamais bypassé). Exemples + patterns : skill upsert.
Placement¶
- Test :
__tests__/à côté de l'entry point testé — controller (src/<module>/.../__tests__/), cron task (cron-task/<domaine>/__tests__/), worker (worker/__tests__/) — il suit l'entry point, pas le module de logique - Fixtures :
tests/integration/fixtures/<module au pluriel>/<entity>.{build,seed}.ts, jamais danssrc/(lint : importables uniquement par tests/fixtures) ; branded types purs →fixtures/primitives/
Cron / workers¶
Invocation, dry-run, délais métier, hooks test-only (FundingCounterReconciliationTestHooks), spies, et un worker single-batch : skill upsert §Cron / §Workers.
Naming¶
- Top-level
describe('METHOD /route')(technique, reste en anglais) oudescribe('<task>CronTask'); 2e niveaudescribe('Quand <précondition>');it('Alors <observable>')— pas deshould, pas de noms génériques (it('works')) - Langue : français, sauf les termes domaine/techniques (webhook, cron, payout, better-auth…) ; les tests existants en anglais se traduisent au fil des PRs, pas de big-bang
- Max 2 niveaux de nesting. 3 niveaux signe une route qui fait trop de choses
- Un seul Arrange-Act-Assert par
it, autant d'expectque nécessaire pour décrire l'outcome
DO NOT¶
- DO NOT utiliser de JWT pour l'auth →
TestAuthHelper.createInvestor()/.createAdmin()(cookie better-auth via sign-up HTTP).JwtAuthGuardvalide la session Better Auth cookie. - DO NOT mock la DB → la vraie Postgres + snapshot fournissent la fidélité prod (FKs, contraintes, triggers)
- DO NOT mock Redis → on teste aussi le comportement de cache
- DO NOT mock le service que le controller appelle → c'est un test unitaire déguisé
- DO NOT utiliser
Test.createTestingModule({ imports: [AppModule] }).compile()(nestjs-pino + SWC deadlocke). UtiliserNestFactory.create(AppModule, …)(déjà fait dansgetTestApp()). - DO NOT réintroduire un boot par fichier (
beforeAll(() => createTestApp())) — l'app est déjà partagée viagetTestApp(). - DO NOT introduire un parallélisme inter-tests dans cette phase →
singleFork: truegarantit l'isolation séquentielle du resetbeforeEach - DO NOT committer un snapshot > 1 MB → schéma +
__flyway_schema_history__only, pas les données métier - DO NOT ajouter en Flyway une colonne Better Auth (plugins /
additionalFields) → leglobal-setuplancepnpm better-auth:check-and-migrate --apply(même source que le deploy CI) - DO NOT écrire les montants cents en grouping milliers (
total: 100_000) → convention euros_centimes1_000_00, seeds et assertions compris (cf. generic-monorepo-rules §DO)