mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- liste OT (filtres, strie rouge, « immédiat ») ; fiche OT : transitions pilotées par allowedTransitions, clôture grisée avec la garde expliquée, bilan codé (6 selects sur référentiels), checklist Fait→N-A→à faire, activité + commentaires, assignation, annulation motivée - nouvel OT : interrupteur « personne bloquée » qui force la priorité - demandes : table + panneau d'approbation (priorité, assignation, approuver → fiche OT), rejet en modale à motif obligatoire, signalement interne ; statut « Résolue » dérivé de l'OT lié - préventif : tuiles réelles, générer + résumé (« regénérer ne double rien »), gabarits administrables ; compteurs : saisie + historique - tableau de bord réel : bandeau urgence cliquable, KPIs, OT par statut, interventions récentes ; accueil dédié aux rôles sans exploitation - urgence traversante : chip topbar pulsante (60 s), badges de nav - GET /assets/options (auth seule) : le Demandeur peut désigner l'appareil qu'il signale — trou débusqué par l'e2e (49 opérations au contrat) - génération préventive durcie : collision de référence RETENTÉE (plus de saut silencieux), P2003 toléré ; 3 runs Jest complets consécutifs verts - 9 tests Playwright (recette R2 officielle rejouée intégralement), 50 tests API (94,6 % / 78,9 %) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
209 lines
8.6 KiB
TypeScript
209 lines
8.6 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|