Aller au contenu

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 dans src/ (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) ou describe('<task>CronTask') ; 2e niveau describe('Quand <précondition>') ; it('Alors <observable>') — pas de should, 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'expect que 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). JwtAuthGuard valide 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). Utiliser NestFactory.create(AppModule, …) (déjà fait dans getTestApp()).
  • DO NOT réintroduire un boot par fichier (beforeAll(() => createTestApp())) — l'app est déjà partagée via getTestApp().
  • DO NOT introduire un parallélisme inter-tests dans cette phase → singleFork: true garantit l'isolation séquentielle du reset beforeEach
  • 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) → le global-setup lance pnpm 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_centimes 1_000_00, seeds et assertions compris (cf. generic-monorepo-rules §DO)