From 2aefe5cfbeecb91b9ae5d86d57f8fd6e04296f9f Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Mon, 15 Jun 2026 16:53:20 +0200 Subject: [PATCH] feat(db): add test fixtures script --- eme-backend/package.json | 3 +- eme-backend/prisma/fixtures.ts | 479 +++++++++++++++++++++++++++++++++ review.md | 15 +- 3 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 eme-backend/prisma/fixtures.ts diff --git a/eme-backend/package.json b/eme-backend/package.json index 14f7129..c50a1a6 100644 --- a/eme-backend/package.json +++ b/eme-backend/package.json @@ -10,7 +10,8 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", - "seed": "ts-node prisma/seed.ts" + "seed": "ts-node prisma/seed.ts", + "fixtures": "ts-node prisma/fixtures.ts" }, "keywords": [], "author": "", diff --git a/eme-backend/prisma/fixtures.ts b/eme-backend/prisma/fixtures.ts new file mode 100644 index 0000000..efaddf5 --- /dev/null +++ b/eme-backend/prisma/fixtures.ts @@ -0,0 +1,479 @@ +import 'dotenv/config'; +import { PrismaClient } from '@prisma/client'; +import { PrismaMssql } from '@prisma/adapter-mssql'; + +// Fixtures de DEV/TEST. À lancer après `npm run seed` (dépend des données de référence). +// Non destructif : si déjà chargées (email témoin présent), le script s'arrête. + +function getEnv(name: string): string { + const value = process.env[name]; + + if (!value) { + throw new Error(`Variable d'environnement manquante: ${name}`); + } + + return value; +} + +function required(value: T | null, message: string): T { + if (value === null) { + throw new Error(message); + } + + return value; +} + +const adapter = new PrismaMssql({ + server: getEnv('DB_SERVER'), + port: Number(process.env.DB_PORT ?? 1433), + database: getEnv('DB_NAME'), + user: getEnv('DB_USER'), + password: getEnv('DB_PASSWORD'), + options: { + encrypt: true, + trustServerCertificate: true, + }, +}); + +const prisma = new PrismaClient({ adapter }); + +const WITNESS_EMAIL = 'marie.dupont@ensitech.eu'; + +const now = new Date(); +function daysAgo(n: number): Date { + const d = new Date(now); + d.setDate(d.getDate() - n); + return d; +} +function daysFromNow(n: number): Date { + const d = new Date(now); + d.setDate(d.getDate() + n); + return d; +} + +async function main(): Promise { + // Idempotence : on ne recharge pas si déjà présent. + const witness = await prisma.utilisateur.findUnique({ where: { email: WITNESS_EMAIL } }); + if (witness) { + console.log('[fixtures] deja chargees — rien a faire'); + return; + } + + // Références (doivent exister via le seed). + const roleEtudiant = required( + await prisma.role.findUnique({ where: { code: 'ETUDIANT' } }), + 'Role ETUDIANT manquant — lance d\'abord `npm run seed`', + ); + const roleResponsable = required( + await prisma.role.findUnique({ where: { code: 'RESPONSABLE' } }), + 'Role RESPONSABLE manquant — lance d\'abord `npm run seed`', + ); + const campus = required( + await prisma.campus.findFirst({ where: { nom: 'Campus Saint-Christophe' } }), + 'Campus manquant — lance d\'abord `npm run seed`', + ); + const salle = required( + await prisma.sallePret.findFirst({ where: { campusId: campus.id } }), + 'Salle de pret manquante — lance d\'abord `npm run seed`', + ); + const poste = required( + await prisma.posteEmprunt.findFirst({ where: { sallePretId: salle.id } }), + 'Poste manquant — lance d\'abord `npm run seed`', + ); + const catOrdi = required( + await prisma.categorieMateriel.findFirst({ where: { nom: 'Ordinateur portable' } }), + 'Categorie Ordinateur portable manquante', + ); + const catVideo = required( + await prisma.categorieMateriel.findFirst({ where: { nom: 'Vidéoprojecteur' } }), + 'Categorie Vidéoprojecteur manquante', + ); + const catTablette = required( + await prisma.categorieMateriel.findFirst({ where: { nom: 'Tablette' } }), + 'Categorie Tablette manquante', + ); + const catCamera = required( + await prisma.categorieMateriel.findFirst({ where: { nom: 'Caméra' } }), + 'Categorie Caméra manquante', + ); + const catAudio = required( + await prisma.categorieMateriel.findFirst({ where: { nom: 'Périphérique audio' } }), + 'Categorie Périphérique audio manquante', + ); + + // Utilisateurs : 3 étudiants (2 info @ensitech.eu, 1 autre @ensup.eu) + 1 responsable. + const marie = await prisma.utilisateur.create({ + data: { + microsoftId: 'ms-marie-dupont', + nom: 'Dupont', + prenom: 'Marie', + email: WITNESS_EMAIL, + classe: 'B3 Informatique', + roleId: roleEtudiant.id, + campusId: campus.id, + }, + }); + const lucas = await prisma.utilisateur.create({ + data: { + microsoftId: 'ms-lucas-martin', + nom: 'Martin', + prenom: 'Lucas', + email: 'lucas.martin@ensitech.eu', + classe: 'B2 Informatique', + roleId: roleEtudiant.id, + campusId: campus.id, + }, + }); + const sofia = await prisma.utilisateur.create({ + data: { + microsoftId: 'ms-sofia-bernard', + nom: 'Bernard', + prenom: 'Sofia', + email: 'sofia.bernard@ensup.eu', + classe: 'B1 Marketing', + roleId: roleEtudiant.id, + campusId: campus.id, + }, + }); + const responsable = await prisma.utilisateur.create({ + data: { + microsoftId: 'ms-karim-benali', + nom: 'Benali', + prenom: 'Karim', + email: 'karim.benali@ensup.eu', + classe: null, // un responsable n'a pas de classe + roleId: roleResponsable.id, + campusId: campus.id, + }, + }); + console.log('[fixtures] 4 utilisateurs crees (3 etudiants + 1 responsable)'); + + // Cartes étudiantes (qrCode unique par carte). + await prisma.carteEtudiante.createMany({ + data: [ + { + utilisateurId: marie.id, + qrCode: 'QR-ETU-0001', + dateActivation: daysAgo(300), + dateExpiration: daysFromNow(400), + }, + { + utilisateurId: lucas.id, + qrCode: 'QR-ETU-0002', + dateActivation: daysAgo(300), + dateExpiration: daysFromNow(400), + }, + { + utilisateurId: sofia.id, + qrCode: 'QR-ETU-0003', + dateActivation: daysAgo(300), + dateExpiration: daysFromNow(400), + }, + ], + }); + console.log('[fixtures] 3 cartes etudiantes creees'); + + // Accessoires. + const chargeur = await prisma.accessoire.create({ + data: { nom: 'Chargeur', description: 'Chargeur secteur.' }, + }); + const souris = await prisma.accessoire.create({ + data: { nom: 'Souris', description: 'Souris filaire.' }, + }); + const housse = await prisma.accessoire.create({ + data: { nom: 'Housse', description: 'Housse de protection.' }, + }); + + // Matériels (statut explicite — pas de défaut sur ce champ). + const pcDell = await prisma.materiel.create({ + data: { + nom: 'PC portable Dell Latitude', + marque: 'Dell', + modele: 'Latitude 5440', + reference: 'REF-PC-001', + numeroInventaire: 'INV-PC-001', + numeroSerie: 'SN-DELL-001', + statut: 'EMPRUNTE', + etatGeneral: 'Bon etat', + categorieId: catOrdi.id, + campusId: campus.id, + }, + }); + const projecteur = await prisma.materiel.create({ + data: { + nom: 'Vidéoprojecteur Epson', + marque: 'Epson', + modele: 'EB-W06', + reference: 'REF-VP-001', + numeroInventaire: 'INV-VP-001', + numeroSerie: 'SN-EPSON-001', + statut: 'EMPRUNTE', + etatGeneral: 'Bon etat', + categorieId: catVideo.id, + campusId: campus.id, + }, + }); + const ipad = await prisma.materiel.create({ + data: { + nom: 'Tablette iPad', + marque: 'Apple', + modele: 'iPad 10', + reference: 'REF-TAB-001', + numeroInventaire: 'INV-TAB-001', + numeroSerie: 'SN-IPAD-001', + statut: 'DISPONIBLE', + etatGeneral: 'Bon etat', + categorieId: catTablette.id, + campusId: campus.id, + }, + }); + const camera = await prisma.materiel.create({ + data: { + nom: 'Caméra Sony', + marque: 'Sony', + modele: 'Alpha 6400', + reference: 'REF-CAM-001', + numeroInventaire: 'INV-CAM-001', + numeroSerie: 'SN-SONY-001', + statut: 'DETERIORE', + etatGeneral: 'Housse manquante au retour', + categorieId: catCamera.id, + campusId: campus.id, + }, + }); + await prisma.materiel.create({ + data: { + nom: 'Micro Rode', + marque: 'Rode', + modele: 'NT-USB', + reference: 'REF-MIC-001', + numeroInventaire: 'INV-MIC-001', + numeroSerie: 'SN-RODE-001', + statut: 'DISPONIBLE', + etatGeneral: 'Bon etat', + categorieId: catAudio.id, + campusId: campus.id, + }, + }); + await prisma.materiel.create({ + data: { + nom: 'PC portable HP', + marque: 'HP', + modele: 'ProBook 450', + reference: 'REF-PC-002', + numeroInventaire: 'INV-PC-002', + numeroSerie: 'SN-HP-002', + statut: 'MAINTENANCE', + etatGeneral: 'En revision', + categorieId: catOrdi.id, + campusId: campus.id, + }, + }); + console.log('[fixtures] 6 materiels crees'); + + // Accessoires attendus (obligatoire explicite — pas de défaut). + await prisma.materielAccessoire.createMany({ + data: [ + { materielId: pcDell.id, accessoireId: chargeur.id, quantiteAttendue: 1, obligatoire: true }, + { materielId: pcDell.id, accessoireId: souris.id, quantiteAttendue: 1, obligatoire: false }, + { materielId: camera.id, accessoireId: housse.id, quantiteAttendue: 1, obligatoire: true }, + { materielId: camera.id, accessoireId: chargeur.id, quantiteAttendue: 1, obligatoire: true }, + ], + }); + + // Emprunt 1 — EN_COURS (Marie / PC Dell) + checklist de départ. + const empruntEnCours = await prisma.emprunt.create({ + data: { + utilisateurId: marie.id, + materielId: pcDell.id, + campusId: campus.id, + sallePretId: salle.id, + posteEmpruntId: poste.id, + dateEmprunt: daysAgo(2), + dateRetourPrevue: daysFromNow(5), + statut: 'EN_COURS', + modeIdentificationEmprunt: 'QR_CODE', + }, + }); + const clDepart1 = await prisma.checklist.create({ + data: { + empruntId: empruntEnCours.id, + utilisateurId: marie.id, + type: 'DEPART', + dateVerification: daysAgo(2), + }, + }); + await prisma.checklistElement.createMany({ + data: [ + { checklistId: clDepart1.id, accessoireId: chargeur.id, nomElement: 'Chargeur', etat: 'PRESENT', quantiteConstatee: 1 }, + { checklistId: clDepart1.id, accessoireId: souris.id, nomElement: 'Souris', etat: 'PRESENT', quantiteConstatee: 1 }, + ], + }); + + // Emprunt 2 — EN_RETARD (Lucas / Vidéoprojecteur), date de retour dépassée. + const empruntEnRetard = await prisma.emprunt.create({ + data: { + utilisateurId: lucas.id, + materielId: projecteur.id, + campusId: campus.id, + sallePretId: salle.id, + posteEmpruntId: poste.id, + dateEmprunt: daysAgo(20), + dateRetourPrevue: daysAgo(6), + statut: 'EN_RETARD', + modeIdentificationEmprunt: 'MAIL_ENSUP', + }, + }); + await prisma.checklist.create({ + data: { + empruntId: empruntEnRetard.id, + utilisateurId: lucas.id, + type: 'DEPART', + dateVerification: daysAgo(20), + }, + }); + + // Emprunt 3 — CLOTURE (Marie / iPad), retour conforme. + const empruntCloture = await prisma.emprunt.create({ + data: { + utilisateurId: marie.id, + materielId: ipad.id, + campusId: campus.id, + sallePretId: salle.id, + posteEmpruntId: poste.id, + dateEmprunt: daysAgo(10), + dateRetourPrevue: daysAgo(3), + dateRetourReelle: daysAgo(3), + statut: 'CLOTURE', + modeIdentificationEmprunt: 'QR_CODE', + modeIdentificationRetour: 'QR_CODE', + }, + }); + await prisma.checklist.create({ + data: { + empruntId: empruntCloture.id, + utilisateurId: marie.id, + type: 'DEPART', + dateVerification: daysAgo(10), + }, + }); + await prisma.checklist.create({ + data: { + empruntId: empruntCloture.id, + utilisateurId: marie.id, + type: 'RETOUR', + dateVerification: daysAgo(3), + }, + }); + + // Emprunt 4 — RETOUR_NON_CONFORME (Sofia / Caméra) : housse absente au retour. + const empruntNonConforme = await prisma.emprunt.create({ + data: { + utilisateurId: sofia.id, + materielId: camera.id, + campusId: campus.id, + sallePretId: salle.id, + posteEmpruntId: poste.id, + dateEmprunt: daysAgo(5), + dateRetourPrevue: daysAgo(1), + dateRetourReelle: daysAgo(1), + statut: 'RETOUR_NON_CONFORME', + modeIdentificationEmprunt: 'MAIL_ENSUP', + modeIdentificationRetour: 'MAIL_ENSUP', + commentaireRetour: 'Housse non rapportee.', + }, + }); + const clDepart4 = await prisma.checklist.create({ + data: { + empruntId: empruntNonConforme.id, + utilisateurId: sofia.id, + type: 'DEPART', + dateVerification: daysAgo(5), + }, + }); + await prisma.checklistElement.createMany({ + data: [ + { checklistId: clDepart4.id, accessoireId: housse.id, nomElement: 'Housse', etat: 'PRESENT', quantiteConstatee: 1 }, + { checklistId: clDepart4.id, accessoireId: chargeur.id, nomElement: 'Chargeur', etat: 'PRESENT', quantiteConstatee: 1 }, + ], + }); + const clRetour4 = await prisma.checklist.create({ + data: { + empruntId: empruntNonConforme.id, + utilisateurId: sofia.id, + type: 'RETOUR', + dateVerification: daysAgo(1), + }, + }); + await prisma.checklistElement.createMany({ + data: [ + { checklistId: clRetour4.id, accessoireId: housse.id, nomElement: 'Housse', etat: 'ABSENT', quantiteConstatee: 0, commentaire: 'Manquante au retour' }, + { checklistId: clRetour4.id, accessoireId: chargeur.id, nomElement: 'Chargeur', etat: 'PRESENT', quantiteConstatee: 1 }, + ], + }); + console.log('[fixtures] 4 emprunts + checklists crees'); + + // Anomalie sur l'emprunt non conforme + notification au responsable. + const anomalie = await prisma.anomalie.create({ + data: { + empruntId: empruntNonConforme.id, + materielId: camera.id, + utilisateurId: sofia.id, + type: 'ACCESSOIRE_MANQUANT', + description: 'Housse declaree presente au depart, absente au retour.', + statut: 'DETECTEE', + detecteeAutomatiquement: true, + dateDetection: daysAgo(1), + }, + }); + await prisma.notification.create({ + data: { + utilisateurId: responsable.id, + anomalieId: anomalie.id, + titre: 'Nouvelle anomalie detectee', + message: 'Retour non conforme sur la Caméra Sony (housse manquante).', + type: 'ANOMALIE', + }, + }); + console.log('[fixtures] 1 anomalie + 1 notification creees'); + + // Historique : quelques traces. + await prisma.historique.createMany({ + data: [ + { + utilisateurId: marie.id, + empruntId: empruntEnCours.id, + materielId: pcDell.id, + campusId: campus.id, + sallePretId: salle.id, + posteEmpruntId: poste.id, + action: 'CREATION_EMPRUNT', + description: 'Emprunt du PC portable Dell.', + dateAction: daysAgo(2), + }, + { + utilisateurId: sofia.id, + empruntId: empruntNonConforme.id, + materielId: camera.id, + campusId: campus.id, + sallePretId: salle.id, + posteEmpruntId: poste.id, + action: 'CREATION_ANOMALIE', + description: 'Anomalie detectee au retour de la Caméra Sony.', + dateAction: daysAgo(1), + }, + ], + }); + console.log('[fixtures] 2 entrees d\'historique creees'); +} + +main() + .then(async () => { + await prisma.$disconnect(); + console.log('[fixtures] termine'); + }) + .catch(async (error: unknown) => { + console.error('[fixtures] echec', error); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/review.md b/review.md index 5c2bab2..a0200d3 100644 --- a/review.md +++ b/review.md @@ -85,6 +85,19 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Idempotence : `upsert` sur `Role.code` ; `findFirst` avant `create` pour les entités sans clé unique. Relance vérifiée (aucun doublon). - Incident : le client Prisma était obsolète (ne connaissait que les 4 modèles de référence). Régénéré avec `npx prisma generate` avant le seed. Leçon : après des migrations, régénérer/vérifier le client avant d'exécuter du code qui l'utilise. +### Étape 10 — Fixtures de test +- Fichier `prisma/fixtures.ts` + script `npm run fixtures` (séparé du seed : données de dev/test, non installées en production). +- Non destructif : détection via un email témoin (`marie.dupont@ensitech.eu`) ; si présent, le script s'arrête (cohérent avec le principe "pas de suppression automatique"). +- Contenu : 4 utilisateurs (3 étudiants + 1 responsable), 3 cartes, 6 matériels (statuts variés), 3 accessoires + liaisons, 4 emprunts couvrant `EN_COURS` / `EN_RETARD` / `CLOTURE` / `RETOUR_NON_CONFORME`, 6 checklists, 1 anomalie + 1 notification, 2 entrées d'historique. +- Idempotence vérifiée (2e exécution = skip). + +#### Contexte métier clarifié — structure ENSUP (impacte la modélisation) +- ENSUP est un groupe **multi-campus** (Saint-Christophe/Cergy, Saint-Quentin, Nantes, Marseille...). +- Chaque campus héberge **deux écoles** : ENSUP (filières généralistes) et Ensitech (informatique). Bâtiments et salles **partagés**. +- La distinction d'école se fait par le **domaine email** : `ensitech.eu` (info) et `ensup.eu` (reste). La validation de domaine (RG, à venir) doit accepter les **deux**. +- Le modèle n'a pas d'entité "École" : seule `Campus` existe. Un campus = un lieu physique partagé par les deux écoles (et non une école). +- MVP : on démarre avec le **seul campus Saint-Christophe (Cergy)** ; les autres campus viendront ensuite. + --- ## Dette technique en attente @@ -99,4 +112,4 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y --- -*Dernière mise à jour : 2026-06-15 — Seeds de référence en place.* +*Dernière mise à jour : 2026-06-15 — Fixtures de test en place, Block 2 terminé.*