# 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 (4 tests) ✓ checkFeatureAccess (14 tests) ✓ updateUserPlan (4 tests) ✓ verifyStripeWebhook (4 tests) ✓ calculateProrata (6 tests) Test Files 6 passed (6) Tests 39 passed (39) 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é)