Tests
À quoi sert cette page
Section intitulée « À quoi sert cette page »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"]
Outillage
Section intitulée « Outillage »| Outil | Rôle | Où |
|---|---|---|
| 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/**'],}Seuils de couverture
Section intitulée « Seuils de couverture »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.
Organisation des tests
Section intitulée « Organisation des tests »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).
2. Tests HTTP (tests/http/*)
Section intitulée « 2. Tests HTTP (tests/http/*) »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éeChaque 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 |
Fixtures partagées (tests/fixtures/)
Section intitulée « Fixtures partagées (tests/fixtures/) »Le cœur de la testabilité. Deux familles :
- Builders —
build-app.ts(assemble les use cases avec dépendances mémoire pour les tests unitaires d’application) etbuild-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 externesfake-payment-provider.ts,fake-gift-card-provider.ts,fake-webhook-sender.ts; et les utilitaires déterministesfixed-clock.ts(horloge figée) etstatic-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.
Domaines fonctionnels couverts
Section intitulée « Domaines fonctionnels couverts »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.
Comment lancer les tests
Section intitulée « Comment lancer les tests »| 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. |
# Quotidien : unitaires + HTTP, rapidepnpm test
# Avant un commit important : couverture + seuilspnpm test:cov
# Valider le SQL réel (Firebird requis localement)pnpm test:integration
# Sans Firebird local : tout dans Dockerpnpm test:integration:dockerConventions de test
Section intitulée « Conventions de test »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()nirandomUUID()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.