/** * E2E R2.2 — préventif : génération IDEMPOTENTE (l'unicité est en base), * premier contrôle (tout), périodicités ancrées sur la mise en service ; * compteurs strictement croissants. * Les générations de test utilisent des mois lointains (2031) puis sont purgées. */ 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('Préventif & compteurs (e2e)', () => { let app: INestApplication; let admin: string; let ahmed: string; const prisma = new PrismaClient(); const http = () => request(app.getHttpServer()); const auth = (t: string) => ({ Authorization: `Bearer ${t}` }); beforeAll(async () => { await seed(prisma); await prisma.workOrder.deleteMany({ where: { periodKey: { startsWith: '2031-' } } }); 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; }; admin = await login('Administrateur'); ahmed = await login('Technicien'); }); afterAll(async () => { await prisma.workOrder.deleteMany({ where: { periodKey: { startsWith: '2031-' } } }); await prisma.taskTemplate.deleteMany({ where: { label: { contains: 'TEST-' } } }); await prisma.meterReading.deleteMany({ where: { value: { gte: 90000000 } } }); await app?.close(); await prisma.$disconnect(); }); // Les assertions portent sur les 8 appareils du SEED : d'autres specs Jest // créent/détruisent des appareils en parallèle sur la même base. const SEED_REFS = ['A1', 'A2', 'B1', 'B2', 'C1', 'D1', 'E2', 'M1']; const grillesSeed = (period: string) => prisma.workOrder.count({ where: { periodKey: period, asset: { reference: { in: SEED_REFS } } }, }); it('génère une grille par appareil sous contrat — et regénérer ne double RIEN', async () => { const premiere = await http() .post('/preventive/generate') .set(auth(admin)) .send({ month: '2031-01' }) .expect(200); expect(premiere.body.created).toBeGreaterThanOrEqual(SEED_REFS.length); expect(premiere.body.references.length).toBe(premiere.body.created); expect(await grillesSeed('2031-01')).toBe(SEED_REFS.length); const seconde = await http() .post('/preventive/generate') .set(auth(admin)) .send({ month: '2031-01' }) .expect(200); expect(seconde.body.skipped).toBeGreaterThanOrEqual(SEED_REFS.length); expect(await grillesSeed('2031-01')).toBe(SEED_REFS.length); // toujours 8, pas 16 }); it('premier contrôle (toutes les tâches) pour un appareil sans historique ; grille normale sinon', async () => { const templatesActifs = await prisma.taskTemplate.count({ where: { isActive: true } }); const mensuelles = await prisma.taskTemplate.count({ where: { isActive: true, periodMonths: 1 }, }); // Un appareil VIERGE créé par ce test (aucune génération n'a pu le toucher) const categorie = await prisma.category.findFirstOrThrow({ where: { kind: 'EQUIPMENT' } }); const site = await prisma.location.findFirstOrThrow({ where: { parentId: null } }); const vierge = await prisma.asset.create({ data: { reference: `PVT-${Date.now().toString(36)}`, brand: 'Test', categoryId: categorie.id, locationId: site.id, }, }); try { await http() .post('/preventive/generate') .set(auth(admin)) .send({ month: '2031-09' }) .expect(200); const grille = await prisma.workOrder.findUniqueOrThrow({ where: { assetId_periodKey: { assetId: vierge.id, periodKey: '2031-09' } }, include: { _count: { select: { checklist: true } } }, }); expect(grille.title).toContain('Premier contrôle'); expect(grille._count.checklist).toBe(templatesActifs); } finally { await prisma.workOrder.deleteMany({ where: { assetId: vierge.id } }); await prisma.asset.delete({ where: { id: vierge.id } }); } // A1 a toujours un historique préventif (seed) → grille normale. // Mise en service mars 2020 : janvier 2031 (écart 130 mois) n'est le mois // ni du 3, ni du 6, ni du 12 → seules les tâches MENSUELLES sont dues. const a1 = await prisma.asset.findUniqueOrThrow({ where: { reference: 'A1' } }); const grilleA1 = await prisma.workOrder.findUniqueOrThrow({ where: { assetId_periodKey: { assetId: a1.id, periodKey: '2031-01' } }, include: { _count: { select: { checklist: true } } }, }); expect(grilleA1.title).toContain('Grille du mois'); expect(grilleA1._count.checklist).toBe(mensuelles); }); it('mois anniversaire de mise en service : les périodicités longues tombent (mars 2031 pour A1)', async () => { await http() .post('/preventive/generate') .set(auth(admin)) .send({ month: '2031-03' }) .expect(200); // écart mars 2020 → mars 2031 = 132 mois : divisible par 3, 6 et 12 const templatesActifs = await prisma.taskTemplate.count({ where: { isActive: true } }); const a1 = await prisma.asset.findUniqueOrThrow({ where: { reference: 'A1' } }); const grille = await prisma.workOrder.findUniqueOrThrow({ where: { assetId_periodKey: { assetId: a1.id, periodKey: '2031-03' } }, include: { checklist: true }, }); expect(grille.checklist.length).toBe(templatesActifs); expect(grille.checklist.some((c) => c.label === 'Essai du parachute')).toBe(true); }); it('gabarits : ajout, doublon 409, désactivation (exclu des générations suivantes), 404', async () => { const cree = await http() .post('/preventive/templates') .set(auth(admin)) .send({ label: 'TEST-Contrôle éclairage gaine', periodMonths: 1 }) .expect(201); await http() .post('/preventive/templates') .set(auth(admin)) .send({ label: 'TEST-Contrôle éclairage gaine', periodMonths: 1 }) .expect(409); await http() .patch(`/preventive/templates/${cree.body.id}`) .set(auth(admin)) .send({ isActive: false }) .expect(200); await http() .patch('/preventive/templates/00000000-0000-4000-8000-000000000000') .set(auth(admin)) .send({ label: 'X' }) .expect(404); // désactivé → absent de la génération d'un nouveau mois await http() .post('/preventive/generate') .set(auth(admin)) .send({ month: '2031-05' }) .expect(200); const labels = await prisma.checklistItem.findMany({ where: { workOrder: { periodKey: '2031-05' } }, select: { label: true }, }); expect(labels.some((l) => l.label.startsWith('TEST-'))).toBe(false); // le statut du mois courant répond (structure) const status = await http().get('/preventive/status').set(auth(ahmed)).expect(200); expect(status.body).toHaveProperty('generated'); expect(status.body).toHaveProperty('late'); }); it('compteurs : relevé croissant accepté, régression refusée, appareil inconnu 404', async () => { const { body: assets } = await http().get('/assets').set(auth(ahmed)); const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1'); const liste = await http().get(`/assets/${a1.id}/meters`).set(auth(ahmed)).expect(200); expect(liste.body.meters).toHaveLength(2); // les 2 compteurs, même vides const heures = liste.body.meters.find((m: { kind: string }) => m.kind === 'RUNNING_HOURS'); const dernier = heures.readings[0]?.value ?? 0; const apres = await http() .post(`/assets/${a1.id}/meter-readings`) .set(auth(ahmed)) .send({ kind: 'RUNNING_HOURS', value: 90000000 + dernier }) .expect(201); const majHeures = apres.body.meters.find( (m: { kind: string }) => m.kind === 'RUNNING_HOURS', ); expect(majHeures.readings[0].value).toBe(90000000 + dernier); expect(majHeures.readings[0].readBy.displayName).toBe('Ahmed Benali'); await http() .post(`/assets/${a1.id}/meter-readings`) .set(auth(ahmed)) .send({ kind: 'RUNNING_HOURS', value: dernier }) .expect(400); await http() .get('/assets/00000000-0000-4000-8000-000000000000/meters') .set(auth(ahmed)) .expect(404); }); });