Files
siop2/apps/api/test/auth.e2e-spec.ts
pr-daaif 8adb561b63 feat(r0.10): apps/api — auth fermée par défaut, matrice en base, démo-login ADR-002, seed
- packages/shared : rôles/catégories, schémas Zod, contrat d'API ;
  pnpm contract → docs/openapi.json committée (règle d'or ADR-001)
- apps/api : NestJS 11 + Prisma 6, migration r0_identity (Role/Permission/User) ;
  guard JWT global + @Public() ; PermissionsGuard (@RequirePermission,
  matrice relue en base, cache 60 s) ; FileStorage (seul import MinIO) ; /health
- démo-login ADR-002 : module conditionnel DEMO_MODE (404 sinon, testé e2e),
  double verrou production, refus des comptes isDemo=false
- seed idempotent : 7 rôles, matrice complète (70 lignes), 7 comptes démo
- 19 tests Jest (unit + e2e) ; smoke test sur build de prod

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:55:04 +01:00

125 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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('lAPI 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);
});
});