docs: ajout documentation de référence
This commit is contained in:
parent
9e57400b02
commit
f221daf542
8 changed files with 2893 additions and 0 deletions
528
docs/TESTS_AUTOMATISES.md
Normal file
528
docs/TESTS_AUTOMATISES.md
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
# TESTS_AUTOMATISES.md — Expria / Coach TCF Canada
|
||||
|
||||
> **Document de référence — Version 1.0**
|
||||
> Ce document contient les tests Vitest automatisés à implémenter dans le backend.
|
||||
> Ces tests s'exécutent en quelques secondes et détectent les régressions invisibles
|
||||
> que le Golden Dataset ne peut pas attraper par des tests manuels.
|
||||
|
||||
---
|
||||
|
||||
## 1. Principe
|
||||
|
||||
**Ces 6 fonctions sont critiques.** Si l'une d'elles casse, toute l'application tombe :
|
||||
|
||||
| Fonction | Rôle | Conséquence si cassée |
|
||||
|---|---|---|
|
||||
| `canUserSimulate` | Vérifie quota + plan avant simulation | N'importe qui peut simuler sans limite |
|
||||
| `getPlanPermissions` | Retourne les permissions d'un plan | Mauvaises features affichées / refusées |
|
||||
| `checkFeatureAccess` | Vérifie l'accès à une feature spécifique | Features Premium accessibles en Free |
|
||||
| `updateUserPlan` | Met à jour le plan dans Supabase | Paiement reçu mais accès non débloqué |
|
||||
| `verifyStripeWebhook` | Valide la signature Stripe | N'importe qui peut déclencher un webhook |
|
||||
| `calculateProrata` | Calcule le montant d'upgrade | Mauvais montant affiché à l'utilisateur |
|
||||
|
||||
---
|
||||
|
||||
## 2. Installation
|
||||
|
||||
Dans le dépôt `expria-backend`, installer Vitest :
|
||||
|
||||
```bash
|
||||
npm install --save-dev vitest @vitest/coverage-v8
|
||||
```
|
||||
|
||||
Ajouter dans `package.json` :
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Créer le fichier `vitest.config.ts` :
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
coverage: {
|
||||
reporter: ['text', 'html'],
|
||||
include: ['src/lib/**', 'src/controllers/**'],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Tests — canUserSimulate
|
||||
|
||||
**Fichier :** `src/lib/__tests__/canUserSimulate.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { canUserSimulate } from '../access'
|
||||
|
||||
describe('canUserSimulate', () => {
|
||||
|
||||
// Plan FREE — dans les limites
|
||||
it('autorise un utilisateur free avec 0 simulation utilisée', () => {
|
||||
const result = canUserSimulate({ plan: 'free', simulations_used: 0 })
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('autorise un utilisateur free avec 4 simulations utilisées', () => {
|
||||
const result = canUserSimulate({ plan: 'free', simulations_used: 4 })
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
// Plan FREE — quota atteint
|
||||
it('bloque un utilisateur free avec 5 simulations utilisées', () => {
|
||||
const result = canUserSimulate({ plan: 'free', simulations_used: 5 })
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.reason).toBe('quota_reached')
|
||||
})
|
||||
|
||||
it('bloque un utilisateur free avec plus de 5 simulations (sécurité)', () => {
|
||||
const result = canUserSimulate({ plan: 'free', simulations_used: 99 })
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.reason).toBe('quota_reached')
|
||||
})
|
||||
|
||||
// Plan STANDARD — illimité
|
||||
it('autorise toujours un utilisateur standard', () => {
|
||||
const result = canUserSimulate({ plan: 'standard', simulations_used: 999 })
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
// Plan PREMIUM — illimité
|
||||
it('autorise toujours un utilisateur premium', () => {
|
||||
const result = canUserSimulate({ plan: 'premium', simulations_used: 999 })
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
// Plan inconnu — sécurité défensive
|
||||
it('bloque un plan inconnu', () => {
|
||||
const result = canUserSimulate({ plan: 'unknown' as any, simulations_used: 0 })
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.reason).toBe('invalid_plan')
|
||||
})
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tests — getPlanPermissions
|
||||
|
||||
**Fichier :** `src/lib/__tests__/getPlanPermissions.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getPlanPermissions } from '../access'
|
||||
|
||||
describe('getPlanPermissions', () => {
|
||||
|
||||
describe('Plan FREE', () => {
|
||||
it('retourne les bonnes permissions pour free', () => {
|
||||
const perms = getPlanPermissions('free')
|
||||
expect(perms.simulations_lifetime).toBe(5)
|
||||
expect(perms.oral_t2_live).toBe(false)
|
||||
expect(perms.detailed_report).toBe(false)
|
||||
expect(perms.tips).toBe(false)
|
||||
expect(perms.dashboard).toBe(false)
|
||||
expect(perms.exam_mode).toBe(false)
|
||||
expect(perms.pattern_analysis).toBe(false)
|
||||
expect(perms.preparation_index).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Plan STANDARD', () => {
|
||||
it('retourne les bonnes permissions pour standard', () => {
|
||||
const perms = getPlanPermissions('standard')
|
||||
expect(perms.simulations_lifetime).toBeNull()
|
||||
expect(perms.oral_t2_live).toBe(false)
|
||||
expect(perms.detailed_report).toBe(true)
|
||||
expect(perms.tips).toBe(true)
|
||||
expect(perms.dashboard).toBe(true)
|
||||
expect(perms.exam_mode).toBe(false) // Standard n'a PAS le mode examen
|
||||
expect(perms.pattern_analysis).toBe(false)
|
||||
expect(perms.preparation_index).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Plan PREMIUM', () => {
|
||||
it('retourne les bonnes permissions pour premium', () => {
|
||||
const perms = getPlanPermissions('premium')
|
||||
expect(perms.simulations_lifetime).toBeNull()
|
||||
expect(perms.oral_t2_live).toBe(true)
|
||||
expect(perms.detailed_report).toBe(true)
|
||||
expect(perms.tips).toBe(true)
|
||||
expect(perms.dashboard).toBe(true)
|
||||
expect(perms.exam_mode).toBe(true)
|
||||
expect(perms.pattern_analysis).toBe(true)
|
||||
expect(perms.preparation_index).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('lève une erreur pour un plan inconnu', () => {
|
||||
expect(() => getPlanPermissions('unknown' as any)).toThrow()
|
||||
})
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Tests — checkFeatureAccess
|
||||
|
||||
**Fichier :** `src/lib/__tests__/checkFeatureAccess.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { checkFeatureAccess } from '../access'
|
||||
|
||||
describe('checkFeatureAccess', () => {
|
||||
|
||||
// Features accessibles en FREE
|
||||
it('free peut accéder au rapport basique', () => {
|
||||
expect(checkFeatureAccess('free', 'basic_report')).toBe(true)
|
||||
})
|
||||
|
||||
// Features BLOQUÉES en FREE
|
||||
it('free ne peut pas accéder au rapport détaillé', () => {
|
||||
expect(checkFeatureAccess('free', 'detailed_report')).toBe(false)
|
||||
})
|
||||
|
||||
it('free ne peut pas accéder au mode examen', () => {
|
||||
expect(checkFeatureAccess('free', 'exam_mode')).toBe(false)
|
||||
})
|
||||
|
||||
it('free ne peut pas accéder à la T2 live', () => {
|
||||
expect(checkFeatureAccess('free', 'oral_t2_live')).toBe(false)
|
||||
})
|
||||
|
||||
it('free ne peut pas accéder au dashboard', () => {
|
||||
expect(checkFeatureAccess('free', 'dashboard')).toBe(false)
|
||||
})
|
||||
|
||||
// Features STANDARD
|
||||
it('standard peut accéder au rapport détaillé', () => {
|
||||
expect(checkFeatureAccess('standard', 'detailed_report')).toBe(true)
|
||||
})
|
||||
|
||||
it('standard peut accéder au dashboard', () => {
|
||||
expect(checkFeatureAccess('standard', 'dashboard')).toBe(true)
|
||||
})
|
||||
|
||||
it('standard ne peut PAS accéder au mode examen', () => {
|
||||
expect(checkFeatureAccess('standard', 'exam_mode')).toBe(false)
|
||||
})
|
||||
|
||||
it('standard ne peut PAS accéder à la T2 live', () => {
|
||||
expect(checkFeatureAccess('standard', 'oral_t2_live')).toBe(false)
|
||||
})
|
||||
|
||||
it('standard ne peut PAS accéder à l\'analyse des patterns', () => {
|
||||
expect(checkFeatureAccess('standard', 'pattern_analysis')).toBe(false)
|
||||
})
|
||||
|
||||
// Features PREMIUM
|
||||
it('premium peut accéder au mode examen', () => {
|
||||
expect(checkFeatureAccess('premium', 'exam_mode')).toBe(true)
|
||||
})
|
||||
|
||||
it('premium peut accéder à la T2 live', () => {
|
||||
expect(checkFeatureAccess('premium', 'oral_t2_live')).toBe(true)
|
||||
})
|
||||
|
||||
it('premium peut accéder à l\'analyse des patterns', () => {
|
||||
expect(checkFeatureAccess('premium', 'pattern_analysis')).toBe(true)
|
||||
})
|
||||
|
||||
it('premium peut accéder à l\'indice de préparation', () => {
|
||||
expect(checkFeatureAccess('premium', 'preparation_index')).toBe(true)
|
||||
})
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Tests — updateUserPlan
|
||||
|
||||
**Fichier :** `src/lib/__tests__/updateUserPlan.test.ts`
|
||||
|
||||
> Ces tests utilisent un mock de Supabase — pas de vraie base de données.
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { updateUserPlan } from '../planController'
|
||||
|
||||
// Mock du client Supabase
|
||||
vi.mock('../supabase', () => ({
|
||||
supabase: {
|
||||
from: vi.fn(() => ({
|
||||
update: vi.fn(() => ({
|
||||
eq: vi.fn(() => ({
|
||||
select: vi.fn(() => ({
|
||||
single: vi.fn(() => ({
|
||||
data: { id: 'test-user-id', plan: 'standard' },
|
||||
error: null,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('updateUserPlan', () => {
|
||||
|
||||
it('met à jour le plan vers standard', async () => {
|
||||
const result = await updateUserPlan('test-user-id', 'standard')
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.plan).toBe('standard')
|
||||
})
|
||||
|
||||
it('met à jour le plan vers premium', async () => {
|
||||
const result = await updateUserPlan('test-user-id', 'premium')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('refuse une valeur de plan invalide', async () => {
|
||||
await expect(
|
||||
updateUserPlan('test-user-id', 'super_premium' as any)
|
||||
).rejects.toThrow('Plan invalide')
|
||||
})
|
||||
|
||||
it('refuse un userId vide', async () => {
|
||||
await expect(
|
||||
updateUserPlan('', 'standard')
|
||||
).rejects.toThrow('userId requis')
|
||||
})
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Tests — verifyStripeWebhook
|
||||
|
||||
**Fichier :** `src/lib/__tests__/verifyStripeWebhook.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { verifyStripeWebhook } from '../stripe'
|
||||
|
||||
// Mock de la librairie Stripe
|
||||
vi.mock('stripe', () => ({
|
||||
default: vi.fn(() => ({
|
||||
webhooks: {
|
||||
constructEvent: vi.fn((payload, signature, secret) => {
|
||||
if (signature === 'valid_signature') {
|
||||
return {
|
||||
type: 'checkout.session.completed',
|
||||
data: { object: { client_reference_id: 'user-123' } },
|
||||
}
|
||||
}
|
||||
throw new Error('No signatures found matching the expected signature')
|
||||
}),
|
||||
},
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('verifyStripeWebhook', () => {
|
||||
|
||||
it('valide un webhook avec une signature correcte', () => {
|
||||
const result = verifyStripeWebhook(
|
||||
Buffer.from('payload'),
|
||||
'valid_signature',
|
||||
'whsec_test_secret'
|
||||
)
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.event?.type).toBe('checkout.session.completed')
|
||||
})
|
||||
|
||||
it('rejette un webhook avec une signature incorrecte', () => {
|
||||
const result = verifyStripeWebhook(
|
||||
Buffer.from('payload'),
|
||||
'invalid_signature',
|
||||
'whsec_test_secret'
|
||||
)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
})
|
||||
|
||||
it('rejette un payload vide', () => {
|
||||
const result = verifyStripeWebhook(
|
||||
Buffer.from(''),
|
||||
'valid_signature',
|
||||
'whsec_test_secret'
|
||||
)
|
||||
expect(result.valid).toBe(false)
|
||||
})
|
||||
|
||||
it('rejette une signature vide', () => {
|
||||
const result = verifyStripeWebhook(
|
||||
Buffer.from('payload'),
|
||||
'',
|
||||
'whsec_test_secret'
|
||||
)
|
||||
expect(result.valid).toBe(false)
|
||||
})
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Tests — calculateProrata
|
||||
|
||||
**Fichier :** `src/lib/__tests__/calculateProrata.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { calculateProrata } from '../stripe'
|
||||
|
||||
describe('calculateProrata', () => {
|
||||
|
||||
it('calcule le prorata correct pour un upgrade à mi-période', () => {
|
||||
// Standard = 19.90€ / 28 jours, 14 jours restants
|
||||
// Premium = 39.90€ / 28 jours, 14 jours restants
|
||||
// Crédit Standard = 19.90 * (14/28) = 9.95€
|
||||
// Coût Premium = 39.90 * (14/28) = 19.95€
|
||||
// À payer = 19.95 - 9.95 = 10.00€
|
||||
const result = calculateProrata({
|
||||
currentPlanPrice: 19.90,
|
||||
newPlanPrice: 39.90,
|
||||
totalDays: 28,
|
||||
daysRemaining: 14,
|
||||
})
|
||||
expect(result.amount).toBeCloseTo(10.00, 1)
|
||||
})
|
||||
|
||||
it('retourne 0 si les plans ont le même prix', () => {
|
||||
const result = calculateProrata({
|
||||
currentPlanPrice: 19.90,
|
||||
newPlanPrice: 19.90,
|
||||
totalDays: 28,
|
||||
daysRemaining: 14,
|
||||
})
|
||||
expect(result.amount).toBeCloseTo(0, 1)
|
||||
})
|
||||
|
||||
it('retourne le plein tarif si aucun jour n\'a été consommé', () => {
|
||||
const result = calculateProrata({
|
||||
currentPlanPrice: 19.90,
|
||||
newPlanPrice: 39.90,
|
||||
totalDays: 28,
|
||||
daysRemaining: 28,
|
||||
})
|
||||
expect(result.amount).toBeCloseTo(20.00, 1)
|
||||
})
|
||||
|
||||
it('retourne un montant minimal si 1 seul jour reste', () => {
|
||||
const result = calculateProrata({
|
||||
currentPlanPrice: 19.90,
|
||||
newPlanPrice: 39.90,
|
||||
totalDays: 28,
|
||||
daysRemaining: 1,
|
||||
})
|
||||
expect(result.amount).toBeGreaterThan(0)
|
||||
expect(result.amount).toBeLessThan(5)
|
||||
})
|
||||
|
||||
it('refuse des valeurs négatives', () => {
|
||||
expect(() => calculateProrata({
|
||||
currentPlanPrice: -1,
|
||||
newPlanPrice: 39.90,
|
||||
totalDays: 28,
|
||||
daysRemaining: 14,
|
||||
})).toThrow()
|
||||
})
|
||||
|
||||
it('refuse daysRemaining > totalDays', () => {
|
||||
expect(() => calculateProrata({
|
||||
currentPlanPrice: 19.90,
|
||||
newPlanPrice: 39.90,
|
||||
totalDays: 28,
|
||||
daysRemaining: 30,
|
||||
})).toThrow()
|
||||
})
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Lancer les tests
|
||||
|
||||
```bash
|
||||
# Dans expria-backend/
|
||||
|
||||
# Lancer tous les tests une fois
|
||||
npm run test
|
||||
|
||||
# Lancer en mode watch (relance automatiquement à chaque modification)
|
||||
npm run test:watch
|
||||
|
||||
# Générer un rapport de couverture
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
**Résultat attendu (tous les tests au vert) :**
|
||||
```
|
||||
✓ canUserSimulate (7 tests)
|
||||
✓ getPlanPermissions (7 tests)
|
||||
✓ checkFeatureAccess (13 tests)
|
||||
✓ updateUserPlan (4 tests)
|
||||
✓ verifyStripeWebhook (4 tests)
|
||||
✓ calculateProrata (6 tests)
|
||||
|
||||
Test Files 6 passed (6)
|
||||
Tests 41 passed (41)
|
||||
Duration ~1.2s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Règle d'utilisation avec Claude Code
|
||||
|
||||
**Avant chaque session Claude Code qui touche au backend :**
|
||||
```bash
|
||||
npm run test
|
||||
# Tous les tests doivent être verts avant de commencer
|
||||
```
|
||||
|
||||
**Après chaque session Claude Code qui touche au backend :**
|
||||
```bash
|
||||
npm run test
|
||||
# Si un test passe au rouge → régression détectée → ne pas continuer
|
||||
```
|
||||
|
||||
**Dans le prompt à Claude Code, toujours inclure :**
|
||||
> "Après chaque modification, lance `npm run test`.
|
||||
> Si un test échoue, corrige la régression avant de passer à l'étape suivante.
|
||||
> Ne me montre le résultat final que quand tous les tests sont verts."
|
||||
|
||||
---
|
||||
|
||||
## 11. Quand ajouter de nouveaux tests
|
||||
|
||||
Ajouter un test automatisé chaque fois que Claude Code crée une nouvelle fonction qui :
|
||||
- Vérifie un droit d'accès
|
||||
- Modifie des données en base
|
||||
- Calcule un montant ou un score
|
||||
- Appelle une API externe
|
||||
|
||||
**Ne pas tester :**
|
||||
- Les composants d'affichage (trop fragiles, trop coûteux à maintenir)
|
||||
- Les routes HTTP directement (c'est le rôle du Golden Dataset)
|
||||
- La logique Supabase interne (c'est leur responsabilité)
|
||||
Loading…
Add table
Add a link
Reference in a new issue