/** * E2E R3.2 — bibliothèque (upload MinIO via FileStorage, download streamé, * refus typés, permission sur la cible) + analytics (dérivé du réel). * Nécessite MinIO (infra locale / service CI). */ 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'; const PNG_1PX = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64', ); describe('Bibliothèque & analytics (e2e)', () => { let app: INestApplication; let nadia: string; let karim: string; // Demandeur : aucune permission d'édition const prisma = new PrismaClient(); const http = () => request(app.getHttpServer()); const auth = (t: string) => ({ Authorization: `Bearer ${t}` }); const suffix = `doc-${Date.now().toString(36)}`; 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); return (await http().post('/auth/demo-login').send({ userId: compte.id })).body .accessToken as string; }; nadia = await login('Gestionnaire'); karim = await login('Demandeur'); }); afterAll(async () => { await prisma.document.deleteMany({ where: { fileName: { contains: suffix } } }); await app?.close(); await prisma.$disconnect(); }); it('upload → liste filtrée → download identique → suppression', async () => { const { body: assets } = await http().get('/assets').set(auth(nadia)); const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1'); const envoye = await http() .post('/documents') .set(auth(nadia)) .field('kind', 'CERTIFICATE') .field('assetId', a1.id) .attach('file', PNG_1PX, { filename: `certificat-${suffix}.png`, contentType: 'image/png' }) .expect(201); expect(envoye.body.assetReference).toBe('A1'); expect(envoye.body.size).toBe(PNG_1PX.length); const liste = await http() .get(`/documents?assetId=${a1.id}&kind=CERTIFICATE`) .set(auth(nadia)) .expect(200); expect( liste.body.documents.some((d: { fileName: string }) => d.fileName.includes(suffix)), ).toBe(true); const telecharge = await http() .get(`/documents/${envoye.body.id}/download`) .set(auth(nadia)) .buffer(true) .parse((res, cb) => { const chunks: Buffer[] = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => cb(null, Buffer.concat(chunks))); }) .expect(200); expect(Buffer.compare(telecharge.body as Buffer, PNG_1PX)).toBe(0); // octets identiques await http().delete(`/documents/${envoye.body.id}`).set(auth(nadia)).expect(204); await http().get(`/documents/${envoye.body.id}/download`).set(auth(nadia)).expect(404); }); it('refus typés : format, rattachement manquant, cible inconnue, permission', async () => { const { body: assets } = await http().get('/assets').set(auth(nadia)); const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1'); await http() .post('/documents') .set(auth(nadia)) .field('kind', 'OTHER') .field('assetId', a1.id) .attach('file', Buffer.from('binaire'), { filename: `x-${suffix}.exe`, contentType: 'application/octet-stream' }) .expect(400); await http() .post('/documents') .set(auth(nadia)) .field('kind', 'PHOTO') .attach('file', PNG_1PX, { filename: `orphelin-${suffix}.png`, contentType: 'image/png' }) .expect(400); // aucun rattachement await http() .post('/documents') .set(auth(nadia)) .field('kind', 'PHOTO') .field('assetId', '00000000-0000-4000-8000-000000000000') .attach('file', PNG_1PX, { filename: `fantome-${suffix}.png`, contentType: 'image/png' }) .expect(400); // Karim (Demandeur) : lecture de la bibliothèque OK, édition refusée await http().get('/documents').set(auth(karim)).expect(200); await http() .post('/documents') .set(auth(karim)) .field('kind', 'PHOTO') .field('assetId', a1.id) .attach('file', PNG_1PX, { filename: `interdit-${suffix}.png`, contentType: 'image/png' }) .expect(403); }); it('analytics : le tableau de la direction est dérivé du réel (bilans, coûts figés)', async () => { const res = await http().get('/analytics/summary').set(auth(nadia)).expect(200); const s = res.body; expect(s.costsByMonth).toHaveLength(12); // période par défaut // Le seed a consommé 325 MAD de pièces + 180 de MO sur OT-0341 ce mois-ci expect(s.monthCost).toBeGreaterThanOrEqual(505); // Pannes par organe : OT-0332 (seed) a un bilan « Guides » expect( s.failuresByComponent.some((f: { label: string }) => f.label === 'Guides'), ).toBe(true); // Top équipements : A1 porte les coûts du seed expect(s.topAssets.some((t: { reference: string }) => t.reference === 'A1')).toBe(true); expect(s.closed.total).toBeGreaterThanOrEqual(1); // Karim (Demandeur, sans ANALYTICS) → 403 await http().get('/analytics/summary').set(auth(karim)).expect(403); }); });