/** * E2E R1 — invitation & activation : lien 7 jours, usage unique, * aucun compte actif avant activation (décision maquettes R1). */ 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('Invitation → activation (e2e)', () => { let app: INestApplication; let admin: string; const prisma = new PrismaClient(); const http = () => request(app.getHttpServer()); const email = `invite-${Date.now().toString(36)}@spelev.ma`; beforeAll(async () => { await seed(prisma); const moduleRef = await Test.createTestingModule({ imports: [AppModule.forRoot()], }).compile(); app = moduleRef.createNestApplication(); await app.init(); const { body } = await http().get('/auth/demo-accounts'); const compte = body.accounts.find( (a: { roleName: string }) => a.roleName === 'Administrateur', ); admin = (await http().post('/auth/demo-login').send({ userId: compte.id })).body .accessToken; }); afterAll(async () => { await prisma.user.deleteMany({ where: { email } }); await app?.close(); await prisma.$disconnect(); }); it('invite, empêche la connexion avant activation, active, connecte', async () => { const { body: roles } = await http() .get('/roles') .set('Authorization', `Bearer ${admin}`) .expect(200); const technicien = roles.roles.find( (r: { name: string }) => r.name === 'Technicien', ); // 1. Invitation → compte « invited », lien émis const invitation = await http() .post('/users/invitations') .set('Authorization', `Bearer ${admin}`) .send({ email, displayName: 'Recrue Test', roleId: technicien.id }) .expect(201); expect(invitation.body.activationToken).toBeTruthy(); const { body: users } = await http() .get('/users') .set('Authorization', `Bearer ${admin}`); const invited = users.users.find((u: { email: string }) => u.email === email); expect(invited.status).toBe('invited'); // 2. Pas de connexion possible avant activation await http() .post('/auth/login') .send({ email, password: 'MotDePasse!123' }) .expect(401); // 3. Renvoi du lien : l'ancien devient invalide (usage unique) const renvoi = await http() .post(`/users/${invitation.body.userId}/invitation`) .set('Authorization', `Bearer ${admin}`) .expect(201); await http() .post('/auth/activate') .send({ token: invitation.body.activationToken, password: 'MotDePasse!123' }) .expect(400); // 4. Activation → connecté directement, statut « active » const activation = await http() .post('/auth/activate') .send({ token: renvoi.body.activationToken, password: 'MotDePasse!123' }) .expect(200); expect(activation.body.user.role.name).toBe('Technicien'); await http() .get('/users/me') .set('Authorization', `Bearer ${activation.body.accessToken}`) .expect(200); // 5. Le lien est consommé ; la connexion classique fonctionne désormais await http() .post('/auth/activate') .send({ token: renvoi.body.activationToken, password: 'Autre!12345' }) .expect(400); await http() .post('/auth/login') .send({ email, password: 'MotDePasse!123' }) .expect(200); // 6. Renvoyer un lien sur un compte activé → refus await http() .post(`/users/${invitation.body.userId}/invitation`) .set('Authorization', `Bearer ${admin}`) .expect(409); }); it('refuse un email déjà connu (409) et un lien fantaisiste (400)', async () => { const { body: roles } = await http() .get('/roles') .set('Authorization', `Bearer ${admin}`); await http() .post('/users/invitations') .set('Authorization', `Bearer ${admin}`) .send({ email: 'dispatcher@demo.siop.ma', displayName: 'Doublon', roleId: roles.roles[0].id, }) .expect(409); await http() .post('/auth/activate') .send({ token: 'jeton-inexistant-123', password: 'MotDePasse!123' }) .expect(400); }); });