/** * E2E auth — nécessite l'infra locale (pnpm infra:up) : base seedée au beforeAll. * DEMO_MODE est activé AVANT la composition d'AppModule (voir demo-off.e2e-spec * pour l'état inverse : les routes démo doivent répondre 404). */ process.env.DEMO_MODE = 'true'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import request from 'supertest'; import { seed } from '../prisma/seed'; import { AppModule } from '../src/app.module'; describe('Auth (e2e, DEMO_MODE=true)', () => { let app: INestApplication; let http: () => request.Agent; const prisma = new PrismaClient(); beforeAll(async () => { await seed(prisma); const moduleRef = await Test.createTestingModule({ imports: [AppModule.forRoot()], }).compile(); app = moduleRef.createNestApplication(); await app.init(); http = () => request(app.getHttpServer()); }); afterAll(async () => { await app?.close(); await prisma.$disconnect(); }); it('GET /health est publique', async () => { const res = await http().get('/health').expect(200); expect(res.body.services.database).toBe('up'); }); it('l’API est fermée par défaut (401 sans jeton)', async () => { await http().get('/users/me').expect(401); }); it('GET /auth/demo-accounts liste les 7 comptes seedés, sans secret', async () => { const res = await http().get('/auth/demo-accounts').expect(200); expect(res.body.accounts).toHaveLength(7); for (const account of res.body.accounts) { expect(Object.keys(account).sort()).toEqual([ 'displayName', 'id', 'initials', 'roleName', ]); } }); it('demo-login émet un jeton utilisable sur /users/me (critère < 3 s)', async () => { const start = Date.now(); const { body: accounts } = await http().get('/auth/demo-accounts'); const admin = accounts.accounts.find( (a: { roleName: string }) => a.roleName === 'Administrateur', ); const login = await http() .post('/auth/demo-login') .send({ userId: admin.id }) .expect(200); const me = await http() .get('/users/me') .set('Authorization', `Bearer ${login.body.accessToken}`) .expect(200); expect(Date.now() - start).toBeLessThan(3000); expect(me.body.role.name).toBe('Administrateur'); expect(me.body.isDemo).toBe(true); // La matrice complète accompagne le profil (une ligne par catégorie) expect(me.body.permissions).toHaveLength(10); }); it('demo-login refuse un compte isDemo=false (403)', async () => { const role = await prisma.role.findUniqueOrThrow({ where: { name: 'Administrateur' }, }); const real = await prisma.user.upsert({ where: { email: 'real@spelev.ma' }, update: {}, create: { email: 'real@spelev.ma', displayName: 'Compte Réel', roleId: role.id, isDemo: false, }, }); try { await http().post('/auth/demo-login').send({ userId: real.id }).expect(403); } finally { await prisma.user.delete({ where: { id: real.id } }); } }); it('demo-login sur un id inconnu → 404', async () => { await http() .post('/auth/demo-login') .send({ userId: '00000000-0000-4000-8000-000000000000' }) .expect(404); }); it('la connexion classique fonctionne (seed) et rejette un mauvais mot de passe', async () => { const password = process.env.SEED_DEMO_PASSWORD ?? 'Demo!2026'; const ok = await http() .post('/auth/login') .send({ email: 'dispatcher@demo.siop.ma', password }) .expect(200); expect(ok.body.user.role.name).toBe('Dispatcher'); await http() .post('/auth/login') .send({ email: 'dispatcher@demo.siop.ma', password: 'mauvais' }) .expect(401); }); it('valide les corps de requête via le contrat Zod (400)', async () => { await http().post('/auth/demo-login').send({ userId: 'pas-un-uuid' }).expect(400); await http().post('/auth/login').send({ email: 'x' }).expect(400); }); });