Aller au contenu

Tests

Le backend Titreo E-Pay orchestre de l’argent réel (cartes cadeaux + PSP). Une régression silencieuse = un paiement perdu ou doublé. La suite de tests est donc le filet de sécurité de la passation : elle documente le comportement attendu de chaque brique et permet de refactorer sans casser le métier.

L’architecture hexagonale du projet rend les tests faciles parce que tout est injecté par constructeur : on remplace la base de données, l’horloge, le PSP, le générateur d’ID par des fausses implémentations en mémoire (fixtures), et on teste la logique en isolation, sans réseau ni Firebird.

Trois niveaux coexistent, du plus rapide au plus lourd :

flowchart LR
    A["Unitaires (src/**/*.test.ts)"] -->|"domaine + use cases, fixtures memoire"| D["pnpm test"]
    B["HTTP (tests/http/*)"] -->|"app Fastify injectee, sans reseau"| D
    C["Integration (tests/integration/*)"] -->|"vrai Firebird requis"| E["pnpm test:integration"]
Outil Rôle
Vitest Lanceur de tests (rapide, ESM natif, compatible TS) api/vitest.config.ts
Coverage v8 Couverture via le moteur V8 (provider: 'v8'), rapports text + html + lcov bloc coverage du même fichier
app.inject() Injection de requêtes HTTP dans Fastify sans ouvrir de socket tests tests/http/*
Firebird natif Vraie base pour les tests d’intégration (driver node-firebird-driver-native) tests tests/integration/*

La configuration vit dans api/vitest.config.ts. Points clés :

test: {
globals: false, // describe/it/expect importés explicitement
environment: 'node',
include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
exclude: process.env.INTEGRATION_TESTS === '1' ? [] : ['tests/integration/**'],
}

Le bloc coverage impose des planchers qui font échouer test:cov s’ils ne sont pas atteints :

Métrique Seuil
lines 80 %
functions 80 %
branches 75 %
statements 80 %

Sont exclus du calcul (code d’infra non testable unitairement ou trivial) : src/**/*.test.ts, src/http/server.ts, src/config/**, src/application/ports/**, src/infrastructure/db/**, src/infrastructure/clock.ts, src/infrastructure/id-generator.ts, src/infrastructure/psp/stripe-adapter.ts.

Des alias de chemin (@domain, @application, @infrastructure, @http, @workers, @config) sont déclarés dans resolve.alias pour aligner Vitest sur le tsconfig.

Trois emplacements, trois rôles distincts.

1. Tests unitaires — à côté du code (src/**/*.test.ts)

Section intitulée « 1. Tests unitaires — à côté du code (src/**/*.test.ts) »

Chaque module testé a son *.test.ts voisin. Ils couvrent le domaine pur (entités, machines à états, value objects) et les use cases applicatifs, en branchant des fixtures mémoire. Aucun réseau, aucune base.

Exemple représentatif — src/application/sessions/create-session.test.ts. Style AAA (Arrange / Act / Assert), use case construit via la fixture buildApp qui injecte les dépendances mémoire :

import { describe, expect, it } from 'vitest'
import { buildApp } from '../../../tests/fixtures/build-app.js'
describe('CreateSession', () => {
it('crée session avec expiresAt = now + ttl', async () => {
const app = buildApp({ merchantId, sessionTtlMinutes: 30 }) // Arrange
const session = await app.useCases.createSession.execute({ // Act
merchantId, reference: 'ORDER-1', totalAmount: 5000, currency: 'EUR',
})
expect(session.expiresAt.getTime() - session.createdAt.getTime()) // Assert
.toBe(30 * 60_000)
expect(app.sessions.size()).toBe(1)
})
})

On retrouve dans ce répertoire les tests des entités (session.test.ts, leg.test.ts), des machines à états (session-status.test.ts, leg-status.test.ts), des value objects (money.test.ts, currency.test.ts, id.test.ts), de la crypto (aead.test.ts, scrypt-password-hasher.test.ts, ed25519-signer.test.ts, api-key.test.ts), des adapters PSP/Titreo (adyen-adapter.test.ts, titreo-gift-card-adapter.test.ts) et des workers (process-outbox-entry.test.ts, cleanup-expired-sessions.test.ts, outbox-poller.test.ts).

Ils montent une vraie application Fastify complète (plugins d’auth, CORS, rate-limit, idempotence, error handler) mais l’interrogent via app.inject() — pas de socket TCP ouverte. C’est le niveau qui valide la couche HTTP bout en bout : sérialisation, codes de statut, en-têtes, garde de scopes.

Tout passe par la fixture buildHttpFixture (tests/fixtures/build-http-app.ts), qui assemble le container complet avec dépendances mémoire, crée un marchand de test, une clé API valide (apiToken) et renvoie aussi des handles sur les repos pour inspecter l’état après coup.

Extrait — tests/http/http-idempotency.test.ts. Noter app.inject(), l’en-tête idempotency-key, et l’inspection de l’état mémoire (fixture.idempotency.all()) :

const a = await fixture.app.inject({
method: 'POST',
url: '/v1/sessions',
headers: { authorization: auth, 'idempotency-key': 'idem-abc-1' },
payload: validBody,
})
expect(a.statusCode).toBe(201)
const b = await fixture.app.inject({ /* même clé, même body */ })
expect(b.headers['idempotency-replayed']).toBe('true') // rejoué, pas recréé
expect(fixture.idempotency.all().length).toBe(1) // une seule entrée stockée

Chaque test afterEach(() => fixture.app.close()) pour libérer l’instance Fastify.

3. Tests d’intégration (tests/integration/*) — Firebird requis

Section intitulée « 3. Tests d’intégration (tests/integration/*) — Firebird requis »

Ils branchent les vrais adapters Firebird (FirebirdDb, FirebirdSessionRepo, FirebirdUnitOfWork, FirebirdMerchantRepo…) sur une base réelle, appliquent les migrations (runMigrations) et vérifient les aller-retours de persistance, l’idempotence au niveau base, le chiffrement AEAD au repos, la pagination des listes et les logs d’audit. C’est le seul niveau qui valide le SQL réel — voir Base de données.

Connexion paramétrée par variables d’environnement, avec valeurs par défaut :

Variable Défaut Rôle
INTEGRATION_TESTS (absent) doit valoir 1 pour activer ces tests
FB_HOST 127.0.0.1 hôte Firebird
FB_PORT 3050 port Firebird
FB_DATABASE /firebird/data/orchpay-test.fdb chemin du fichier .fdb de test
FB_USER SYSDBA utilisateur
FB_PASSWORD masterkey mot de passe

Le cœur de la testabilité. Deux familles :

  • Buildersbuild-app.ts (assemble les use cases avec dépendances mémoire pour les tests unitaires d’application) et build-http-app.ts (monte l’app Fastify pour les tests HTTP).
  • Fakes / repos mémoire — implémentations des ports remplaçant l’infra réelle : in-memory-session-repo.ts, in-memory-outbox-repo.ts, in-memory-merchant-repo.ts, in-memory-api-key-repo.ts, in-memory-merchant-psp-config-repo.ts, in-memory-idempotency-store.ts, in-memory-audit-log-repo.ts, in-memory-uow.ts, in-memory-webhook-events.ts, etc. ; plus les fakes de services externes fake-payment-provider.ts, fake-gift-card-provider.ts, fake-webhook-sender.ts ; et les utilitaires déterministes fixed-clock.ts (horloge figée) et static-id-generator.ts (SequenceIdGenerator, IDs prévisibles).

Deux fixtures portent elles-mêmes des tests (in-memory-outbox-repo.test.ts, in-memory-webhook-events.test.ts) : on teste la fixture pour garantir qu’elle se comporte comme l’adapter réel qu’elle imite.

Tableau des grands axes et de leurs fichiers de test (HTTP + intégration ; les unitaires correspondants sont à côté du code source).

Domaine Ce qui est vérifié Fichiers représentatifs
Sessions (création, legs, submit) flux gift + psp + submit, expiration TTL, idempotence par référence tests/http/http-sessions.test.ts, src/application/sessions/*.test.ts
Liste & pagination des sessions filtres, tri, curseur tests/http/http-sessions-list.test.ts, tests/integration/firebird-sessions-list.test.ts
Refund remboursement total/partiel d’une session tests/http/http-refund.test.ts, src/application/sessions/refund-session.test.ts
Auth marchand & admin login, signup, vérification, jeton de session tests/http/http-auth.test.ts, src/domain/auth/scope.test.ts
Lockout login verrouillage après 5 échecs tests/http/http-login-lockout.test.ts
Lien de vérification signup exposition conditionnelle du lien tests/http/http-signup-verification-link.test.ts
Clés API création, listing, révocation, scopes, garde de scope tests/http/http-api-keys.test.ts, src/application/api-keys/api-keys.test.ts
Client token jeton client + allowlist d’origines tests/http/http-client-token.test.ts
Configs PSP upsert/lecture/suppression chiffrées (AEAD) tests/http/http-psp-configs.test.ts, tests/integration/firebird-psp-configs.test.ts, src/application/psp-configs/psp-configs.test.ts
Idempotence rejeu, conflit IDEMPOTENCY_KEY_CONFLICT (422), isolation par clé API tests/http/http-idempotency.test.ts, tests/integration/firebird-idempotency.test.ts
Rate limit plafonnement des requêtes tests/http/http-rate-limit.test.ts
CORS allowlist d’origines tests/http/http-cors.test.ts
Audit écriture et listing des logs d’audit tests/http/http-audit.test.ts, tests/integration/firebird-audit-logs.test.ts
Gift cards solde, ajout de leg cadeau tests/http/http-gift-cards.test.ts, src/application/gift/gift-ops.test.ts
Marchand (me / admin) profil, mise à jour, branding tests/http/http-merchants-me.test.ts, tests/http/http-admin-merchants.test.ts
Webhooks traitement webhook PSP, sortie outbox src/application/webhooks/handle-psp-webhook.test.ts
Outbox & workers traitement d’entrée outbox, cleanup sessions expirées src/application/workers/*.test.ts, tests/integration/firebird-repo.test.ts

Voir aussi Workers et Outbox & orchestration pour le contexte métier de ces flux.

Commande Effet
pnpm test Lance tout sauf l’intégration (unitaires + HTTP). C’est la commande du quotidien.
pnpm test:watch Mode watch interactif (vitest sans run).
pnpm test:cov Comme pnpm test + rapport de couverture v8 et vérification des seuils (échoue sous 80/75 %).
pnpm test:integration INTEGRATION_TESTS=1 vitest run tests/integration --no-file-parallelism. Nécessite un Firebird joignable (voir variables FB_*).
pnpm test:integration:docker Lance l’intégration dans l’image Docker dédiée (docker-compose.test.yml), Firebird inclus. À utiliser si l’addon natif ne compile pas localement.
Fenêtre de terminal
# Quotidien : unitaires + HTTP, rapide
pnpm test
# Avant un commit important : couverture + seuils
pnpm test:cov
# Valider le SQL réel (Firebird requis localement)
pnpm test:integration
# Sans Firebird local : tout dans Docker
pnpm test:integration:docker

Les règles transverses (style AAA, injection par constructeur, fakes mémoire, nommage *.test.ts, assertions explicites) sont décrites dans les conventions. En résumé pour la passation :

  • Un test = un comportement. Titre it(...) descriptif en français, souvent avec le résultat attendu ('… → 422 conflict').
  • Injection par constructeur : on ne mocke jamais un import, on passe une fausse implémentation via la fixture.
  • Déterminisme : horloge figée + IDs séquentiels, jamais Date.now() ni randomUUID() réel dans une assertion (sauf intégration, qui utilise de vrais UUID pour éviter les collisions de référence).
  • Domaine 100 % pur : aucun test de domaine n’importe Fastify, Firebird ni process.env.

Voir Architecture hexagonale pour comprendre pourquoi cette testabilité découle directement du design ports/adapters, et Parcours d’appel pour le chemin complet qu’un test HTTP exerce.