diff --git a/src/routes/__tests__/sujets.test.ts b/src/routes/__tests__/sujets.test.ts new file mode 100644 index 0000000..badb147 --- /dev/null +++ b/src/routes/__tests__/sujets.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Hono } from 'hono' + +// ─── Mocks ─────────────────────────────────────────────────────────────────── + +vi.mock('../../lib/supabase', () => ({ + supabase: { + from: vi.fn(), + }, +})) + +vi.mock('../../middleware/auth', () => ({ + authMiddleware: async (c: any, next: any) => { + const authHeader = c.req.header('Authorization') + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return c.json({ error: true, code: 'AUTH_REQUIRED' }, 401) + } + c.set('user', { id: 'test-user-id', email: 'u@test.com' }) + c.set('profile', { + id: 'test-user-id', + email: 'u@test.com', + plan: 'free', + simulations_used: 0, + stripe_customer_id: null, + stripe_subscription_id: null, + plan_expires_at: null, + created_at: '2026-01-01', + updated_at: '2026-01-01', + }) + await next() + }, +})) + +import { supabase } from '../../lib/supabase' +import sujetsRoutes from '../sujets' + +function buildApp() { + const app = new Hono() + app.route('/sujets', sujetsRoutes) + return app +} + +/** Mock from('sujets').select().eq('mode').eq('tache').eq('actif').order() */ +function mockSujetsQuery(rows: unknown[] | null, error: unknown = null) { + vi.mocked(supabase.from).mockReturnValueOnce({ + select: vi.fn(() => ({ + eq: vi.fn(() => ({ + eq: vi.fn(() => ({ + eq: vi.fn(() => ({ + order: vi.fn(() => ({ data: rows, error })), + })), + })), + })), + })), + } as any) +} + +const AUTH_HEADERS = { Authorization: 'Bearer valid-token' } + +describe('GET /sujets', () => { + beforeEach(() => { + vi.mocked(supabase.from).mockReset() + }) + + it('retourne 401 sans authentification', async () => { + const app = buildApp() + const res = await app.request('/sujets?mode=EE&tache=1') + + expect(res.status).toBe(401) + const body = await res.json() + expect(body.code).toBe('AUTH_REQUIRED') + }) + + it('retourne 400 VALIDATION_ERROR si mode invalide', async () => { + const app = buildApp() + const res = await app.request('/sujets?mode=XX&tache=1', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(400) + const body = await res.json() + expect(body.code).toBe('VALIDATION_ERROR') + }) + + it('retourne 400 VALIDATION_ERROR si mode manquant', async () => { + const app = buildApp() + const res = await app.request('/sujets?tache=1', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(400) + const body = await res.json() + expect(body.code).toBe('VALIDATION_ERROR') + }) + + it('retourne 400 VALIDATION_ERROR si tache invalide', async () => { + const app = buildApp() + const res = await app.request('/sujets?mode=EE&tache=9', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(400) + const body = await res.json() + expect(body.code).toBe('VALIDATION_ERROR') + }) + + it('retourne 400 VALIDATION_ERROR si tache non numérique', async () => { + const app = buildApp() + const res = await app.request('/sujets?mode=EE&tache=abc', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(400) + const body = await res.json() + expect(body.code).toBe('VALIDATION_ERROR') + }) + + it('retourne la liste des sujets actifs pour (mode, tache) valides', async () => { + const rows = [ + { + id: 'sujet-1', + consigne: 'Écrivez un texte.', + role: null, + contexte: null, + doc1_titre: null, + doc1_texte: null, + doc2_titre: null, + doc2_texte: null, + }, + { + id: 'sujet-2', + consigne: 'Autre consigne.', + role: 'journaliste', + contexte: 'magazine', + doc1_titre: null, + doc1_texte: null, + doc2_titre: null, + doc2_texte: null, + }, + ] + mockSujetsQuery(rows) + + const app = buildApp() + const res = await app.request('/sujets?mode=EE&tache=1', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(200) + const body = await res.json() + expect(body).toEqual({ sujets: rows }) + expect(vi.mocked(supabase.from).mock.calls[0][0]).toBe('sujets') + }) + + it('retourne un tableau vide si aucun sujet actif', async () => { + mockSujetsQuery([]) + + const app = buildApp() + const res = await app.request('/sujets?mode=EO&tache=3', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(200) + const body = await res.json() + expect(body).toEqual({ sujets: [] }) + }) + + it('retourne 500 INTERNAL_ERROR si Supabase échoue', async () => { + mockSujetsQuery(null, { message: 'connection refused' }) + + const app = buildApp() + const res = await app.request('/sujets?mode=EE&tache=2', { headers: AUTH_HEADERS }) + + expect(res.status).toBe(500) + const body = await res.json() + expect(body.code).toBe('INTERNAL_ERROR') + }) +})