/** * E2E R1 — référentiel : parcours de recette (site → zone → appareil → organes) * + invariants (profondeur 2, types d'organes, matrice de permissions vivante). */ 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('Référentiel (e2e)', () => { let app: INestApplication; let admin: string; // jetons let technicien: string; const prisma = new PrismaClient(); const http = () => request(app.getHttpServer()); const suffix = Date.now().toString(36); // données de test uniques et repérables 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 login = async (roleName: string) => { const compte = body.accounts.find((a: { roleName: string }) => a.roleName === roleName); const res = await http().post('/auth/demo-login').send({ userId: compte.id }); return res.body.accessToken as string; }; admin = await login('Administrateur'); technicien = await login('Technicien'); }); afterAll(async () => { await prisma.asset.deleteMany({ where: { reference: { contains: suffix } } }); await prisma.location.deleteMany({ where: { name: { contains: suffix } } }); await app?.close(); await prisma.$disconnect(); }); it('parcours de recette : site → zone → appareil avec organes', async () => { const site = await http() .post('/locations') .set('Authorization', `Bearer ${admin}`) .send({ name: `Site Recette ${suffix}`, city: 'Casablanca', latitude: 33.59, longitude: -7.61, }) .expect(201); expect(site.body.parentId).toBeNull(); const zone = await http() .post('/locations') .set('Authorization', `Bearer ${admin}`) .send({ name: `Hall Recette ${suffix}`, parentId: site.body.id }) .expect(201); const { body: cats } = await http() .get('/categories') .set('Authorization', `Bearer ${admin}`); const equipement = cats.categories.find( (c: { kind: string }) => c.kind === 'EQUIPMENT', ); const typeOrgane = cats.categories.find( (c: { kind: string; isActive: boolean }) => c.kind === 'COMPONENT_TYPE' && c.isActive, ); const asset = await http() .post('/assets') .set('Authorization', `Bearer ${admin}`) .send({ reference: `T-${suffix}`, brand: 'Otis', model: 'Gen2', categoryId: equipement.id, locationId: zone.body.id, components: [{ typeId: typeOrgane.id, designation: 'Organe de test' }], }) .expect(201); expect(asset.body.components).toHaveLength(1); expect(asset.body.siteName).toBe(`Site Recette ${suffix}`); // Ajout puis retrait d'un organe const organe = await http() .post(`/assets/${asset.body.id}/components`) .set('Authorization', `Bearer ${admin}`) .send({ typeId: typeOrgane.id }) .expect(201); await http() .delete(`/assets/${asset.body.id}/components/${organe.body.id}`) .set('Authorization', `Bearer ${admin}`) .expect(204); }); it('refuse une hiérarchie de profondeur 3 (site → zone → ?)', async () => { const site = await http() .post('/locations') .set('Authorization', `Bearer ${admin}`) .send({ name: `Site Profond ${suffix}` }) .expect(201); const zone = await http() .post('/locations') .set('Authorization', `Bearer ${admin}`) .send({ name: `Zone Profonde ${suffix}`, parentId: site.body.id }) .expect(201); await http() .post('/locations') .set('Authorization', `Bearer ${admin}`) .send({ name: `Sous-zone ${suffix}`, parentId: zone.body.id }) .expect(400); }); it('refuse un appareil dont la catégorie n’est pas un équipement', async () => { const { body: cats } = await http() .get('/categories') .set('Authorization', `Bearer ${admin}`); const typeOrgane = cats.categories.find((c: { kind: string }) => c.kind === 'COMPONENT_TYPE'); const { body: locs } = await http() .get('/locations') .set('Authorization', `Bearer ${admin}`); await http() .post('/assets') .set('Authorization', `Bearer ${admin}`) .send({ reference: `KO-${suffix}`, brand: 'Test', categoryId: typeOrgane.id, // kind COMPONENT_TYPE → refus locationId: locs.locations[0].id, }) .expect(400); }); it('la matrice vit : un Technicien lit le parc mais ne crée rien', async () => { await http().get('/assets').set('Authorization', `Bearer ${technicien}`).expect(200); await http().get('/locations').set('Authorization', `Bearer ${technicien}`).expect(200); await http() .post('/locations') .set('Authorization', `Bearer ${technicien}`) .send({ name: `Interdit ${suffix}` }) .expect(403); await http().get('/users').set('Authorization', `Bearer ${technicien}`).expect(403); }); it('le seed de la maquette est en place (A1 et ses organes, statuts)', async () => { const { body } = await http() .get('/assets') .set('Authorization', `Bearer ${admin}`) .expect(200); const a1 = body.assets.find((a: { reference: string }) => a.reference === 'A1'); expect(a1.siteName).toBe('Résidence Al Manar'); expect(a1.componentCount).toBe(4); const b2 = body.assets.find((a: { reference: string }) => a.reference === 'B2'); expect(b2.status).toBe('OUT_OF_SERVICE'); }); it('catégorie utilisée : désactivable, jamais supprimable (la route n’existe pas)', async () => { const { body: cats } = await http() .get('/categories') .set('Authorization', `Bearer ${admin}`); const used = cats.categories.find((c: { usageCount: number }) => c.usageCount > 0); await http() .delete(`/categories/${used.id}`) .set('Authorization', `Bearer ${admin}`) .expect(404); // pas de DELETE dans le contrat const off = await http() .patch(`/categories/${used.id}`) .set('Authorization', `Bearer ${admin}`) .send({ isActive: false }) .expect(200); expect(off.body.isActive).toBe(false); await http() .patch(`/categories/${used.id}`) .set('Authorization', `Bearer ${admin}`) .send({ isActive: true }) .expect(200); }); });