@programisto/edrm-exams 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,7 +84,31 @@ class ExamsRouter extends EnduranceRouter {
84
84
  requireAuth: false,
85
85
  permissions: []
86
86
  };
87
- // Créer une catégorie
87
+ /**
88
+ * @swagger
89
+ * /exams/categories:
90
+ * post:
91
+ * summary: Créer une catégorie
92
+ * description: Crée une catégorie de test avec son nom.
93
+ * tags: [Examens]
94
+ * requestBody:
95
+ * required: true
96
+ * content:
97
+ * application/json:
98
+ * schema:
99
+ * type: object
100
+ * required: [name]
101
+ * properties:
102
+ * name:
103
+ * type: string
104
+ * responses:
105
+ * 201:
106
+ * description: Catégorie créée
107
+ * 400:
108
+ * description: Paramètres manquants
109
+ * 500:
110
+ * description: Erreur interne
111
+ */
88
112
  this.post('/categories', authenticatedOptions, async (req, res) => {
89
113
  const { name } = req.body;
90
114
  if (!name) {
@@ -100,7 +124,19 @@ class ExamsRouter extends EnduranceRouter {
100
124
  res.status(500).json({ message: 'Internal server error' });
101
125
  }
102
126
  });
103
- // Lister toutes les catégories
127
+ /**
128
+ * @swagger
129
+ * /exams/categories:
130
+ * get:
131
+ * summary: Lister les catégories
132
+ * description: Retourne toutes les catégories de test.
133
+ * tags: [Examens]
134
+ * responses:
135
+ * 200:
136
+ * description: Liste des catégories
137
+ * 500:
138
+ * description: Erreur interne
139
+ */
104
140
  this.get('/categories', authenticatedOptions, async (req, res) => {
105
141
  try {
106
142
  const categories = await TestCategory.find();
@@ -111,7 +147,27 @@ class ExamsRouter extends EnduranceRouter {
111
147
  res.status(500).json({ message: 'Internal server error' });
112
148
  }
113
149
  });
114
- // Obtenir une catégorie par son ID
150
+ /**
151
+ * @swagger
152
+ * /exams/categorie/{id}:
153
+ * get:
154
+ * summary: Détail d'une catégorie
155
+ * description: Récupère une catégorie par son identifiant.
156
+ * tags: [Examens]
157
+ * parameters:
158
+ * - in: path
159
+ * name: id
160
+ * required: true
161
+ * schema:
162
+ * type: string
163
+ * responses:
164
+ * 200:
165
+ * description: Catégorie trouvée
166
+ * 404:
167
+ * description: Catégorie non trouvée
168
+ * 500:
169
+ * description: Erreur interne
170
+ */
115
171
  this.get('/categorie/:id', authenticatedOptions, async (req, res) => {
116
172
  const { id } = req.params;
117
173
  try {
@@ -126,7 +182,31 @@ class ExamsRouter extends EnduranceRouter {
126
182
  res.status(500).json({ message: 'Internal server error' });
127
183
  }
128
184
  });
129
- // Créer un job type
185
+ /**
186
+ * @swagger
187
+ * /exams/jobs:
188
+ * post:
189
+ * summary: Créer un job cible
190
+ * description: Crée un job cible pour les tests.
191
+ * tags: [Examens]
192
+ * requestBody:
193
+ * required: true
194
+ * content:
195
+ * application/json:
196
+ * schema:
197
+ * type: object
198
+ * required: [name]
199
+ * properties:
200
+ * name:
201
+ * type: string
202
+ * responses:
203
+ * 201:
204
+ * description: Job créé
205
+ * 400:
206
+ * description: Paramètres manquants
207
+ * 500:
208
+ * description: Erreur interne
209
+ */
130
210
  this.post('/jobs', authenticatedOptions, async (req, res) => {
131
211
  const { name } = req.body;
132
212
  if (!name) {
@@ -142,7 +222,19 @@ class ExamsRouter extends EnduranceRouter {
142
222
  res.status(500).json({ message: 'Internal server error' });
143
223
  }
144
224
  });
145
- // Lister tous les jobs
225
+ /**
226
+ * @swagger
227
+ * /exams/jobs:
228
+ * get:
229
+ * summary: Lister les jobs cibles
230
+ * description: Retourne l'ensemble des jobs disponibles pour les tests.
231
+ * tags: [Examens]
232
+ * responses:
233
+ * 200:
234
+ * description: Liste des jobs
235
+ * 500:
236
+ * description: Erreur interne
237
+ */
146
238
  this.get('/jobs', authenticatedOptions, async (req, res) => {
147
239
  try {
148
240
  const jobs = await TestJob.find();
@@ -153,7 +245,27 @@ class ExamsRouter extends EnduranceRouter {
153
245
  res.status(500).json({ message: 'Internal server error' });
154
246
  }
155
247
  });
156
- // Obtenir un job par son ID
248
+ /**
249
+ * @swagger
250
+ * /exams/jobs/{id}:
251
+ * get:
252
+ * summary: Détail d'un job
253
+ * description: Récupère un job cible par ID.
254
+ * tags: [Examens]
255
+ * parameters:
256
+ * - in: path
257
+ * name: id
258
+ * required: true
259
+ * schema:
260
+ * type: string
261
+ * responses:
262
+ * 200:
263
+ * description: Job trouvé
264
+ * 404:
265
+ * description: Job non trouvé
266
+ * 500:
267
+ * description: Erreur interne
268
+ */
157
269
  this.get('/jobs/:id', authenticatedOptions, async (req, res) => {
158
270
  const { id } = req.params;
159
271
  try {
@@ -168,7 +280,19 @@ class ExamsRouter extends EnduranceRouter {
168
280
  res.status(500).json({ message: 'Internal server error' });
169
281
  }
170
282
  });
171
- // Migrer tous les tests avec l'ancien format targetJob
283
+ /**
284
+ * @swagger
285
+ * /exams/migrate-targetjobs:
286
+ * post:
287
+ * summary: Migrer les tests (targetJob)
288
+ * description: Convertit les tests utilisant l'ancien format de targetJob vers les références TestJob.
289
+ * tags: [Examens]
290
+ * responses:
291
+ * 200:
292
+ * description: Migration exécutée
293
+ * 500:
294
+ * description: Erreur interne
295
+ */
172
296
  this.post('/migrate-targetjobs', authenticatedOptions, async (req, res) => {
173
297
  try {
174
298
  const tests = await Test.find();
@@ -198,7 +322,48 @@ class ExamsRouter extends EnduranceRouter {
198
322
  res.status(500).json({ message: 'Erreur interne du serveur' });
199
323
  }
200
324
  });
201
- // Créer un test
325
+ /**
326
+ * @swagger
327
+ * /exams/test:
328
+ * post:
329
+ * summary: Créer un test
330
+ * description: Crée un test avec titre, description, job cible, séniorité et catégories.
331
+ * tags: [Examens]
332
+ * requestBody:
333
+ * required: true
334
+ * content:
335
+ * application/json:
336
+ * schema:
337
+ * type: object
338
+ * required: [title, targetJob, seniorityLevel]
339
+ * properties:
340
+ * title:
341
+ * type: string
342
+ * description:
343
+ * type: string
344
+ * targetJob:
345
+ * type: string
346
+ * seniorityLevel:
347
+ * type: string
348
+ * categories:
349
+ * type: array
350
+ * items:
351
+ * type: object
352
+ * properties:
353
+ * name:
354
+ * type: string
355
+ * expertiseLevel:
356
+ * type: string
357
+ * state:
358
+ * type: string
359
+ * responses:
360
+ * 201:
361
+ * description: Test créé
362
+ * 400:
363
+ * description: Paramètres manquants
364
+ * 500:
365
+ * description: Erreur interne
366
+ */
202
367
  this.post('/test', authenticatedOptions, async (req, res) => {
203
368
  const { title, description, targetJob, seniorityLevel, categories, state = 'draft' } = req.body;
204
369
  const user = req.user;
@@ -239,7 +404,8 @@ class ExamsRouter extends EnduranceRouter {
239
404
  targetJob: targetJobId,
240
405
  seniorityLevel,
241
406
  state,
242
- categories: processedCategories
407
+ categories: processedCategories,
408
+ ...(req.entity?._id && { entityId: req.entity._id })
243
409
  });
244
410
  await newTest.save();
245
411
  res.status(201).json({ message: 'test created with sucess', data: newTest });
@@ -249,7 +415,35 @@ class ExamsRouter extends EnduranceRouter {
249
415
  res.status(500).json({ message: 'Internal server error' });
250
416
  }
251
417
  });
252
- // Modifier un test
418
+ /**
419
+ * @swagger
420
+ * /exams/test/{id}:
421
+ * put:
422
+ * summary: Modifier un test
423
+ * description: Met à jour les métadonnées, catégories ou questions d'un test.
424
+ * tags: [Examens]
425
+ * parameters:
426
+ * - in: path
427
+ * name: id
428
+ * required: true
429
+ * schema:
430
+ * type: string
431
+ * requestBody:
432
+ * required: true
433
+ * content:
434
+ * application/json:
435
+ * schema:
436
+ * type: object
437
+ * responses:
438
+ * 200:
439
+ * description: Test modifié
440
+ * 404:
441
+ * description: Test non trouvé
442
+ * 400:
443
+ * description: Données invalides
444
+ * 500:
445
+ * description: Erreur interne
446
+ */
253
447
  this.put('/test/:id', authenticatedOptions, async (req, res) => {
254
448
  const { id } = req.params;
255
449
  const { title, description, targetJob, seniorityLevel, categories, state, questions } = req.body;
@@ -258,6 +452,9 @@ class ExamsRouter extends EnduranceRouter {
258
452
  if (!test) {
259
453
  return res.status(404).json({ message: 'Test non trouvé' });
260
454
  }
455
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
456
+ return res.status(404).json({ message: 'Test non trouvé' });
457
+ }
261
458
  if (title)
262
459
  test.title = title;
263
460
  if (description)
@@ -318,7 +515,27 @@ class ExamsRouter extends EnduranceRouter {
318
515
  res.status(500).json({ message: 'Erreur interne du serveur' });
319
516
  }
320
517
  });
321
- // Supprimer un test
518
+ /**
519
+ * @swagger
520
+ * /exams/test/{id}:
521
+ * delete:
522
+ * summary: Supprimer un test
523
+ * description: Supprime un test ainsi que ses questions et résultats associés.
524
+ * tags: [Examens]
525
+ * parameters:
526
+ * - in: path
527
+ * name: id
528
+ * required: true
529
+ * schema:
530
+ * type: string
531
+ * responses:
532
+ * 200:
533
+ * description: Test supprimé
534
+ * 404:
535
+ * description: Test non trouvé
536
+ * 500:
537
+ * description: Erreur interne
538
+ */
322
539
  this.delete('/test/:id', authenticatedOptions, async (req, res) => {
323
540
  const { id } = req.params;
324
541
  try {
@@ -326,6 +543,9 @@ class ExamsRouter extends EnduranceRouter {
326
543
  if (!test) {
327
544
  return res.status(404).json({ message: 'Test not found' });
328
545
  }
546
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
547
+ return res.status(404).json({ message: 'Test not found' });
548
+ }
329
549
  for (let i = 0; i < test.questions.length; i++) {
330
550
  await TestQuestion.findByIdAndDelete(test.questions[i].questionId);
331
551
  }
@@ -338,7 +558,27 @@ class ExamsRouter extends EnduranceRouter {
338
558
  res.status(500).json({ message: 'Internal server error' });
339
559
  }
340
560
  });
341
- // Obtenir un test par son ID
561
+ /**
562
+ * @swagger
563
+ * /exams/test/{id}:
564
+ * get:
565
+ * summary: Détail d'un test
566
+ * description: Retourne un test avec ses questions et nom du job cible.
567
+ * tags: [Examens]
568
+ * parameters:
569
+ * - in: path
570
+ * name: id
571
+ * required: true
572
+ * schema:
573
+ * type: string
574
+ * responses:
575
+ * 200:
576
+ * description: Test trouvé
577
+ * 404:
578
+ * description: Test non trouvé
579
+ * 500:
580
+ * description: Erreur interne
581
+ */
342
582
  this.get('/test/:id', authenticatedOptions, async (req, res) => {
343
583
  const { id } = req.params;
344
584
  try {
@@ -346,6 +586,10 @@ class ExamsRouter extends EnduranceRouter {
346
586
  if (!test) {
347
587
  return res.status(404).json({ message: 'no test founded with this id' });
348
588
  }
589
+ // Vérifier que le test appartient à l'entité courante (multi-entités)
590
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
591
+ return res.status(404).json({ message: 'no test founded with this id' });
592
+ }
349
593
  // Migration automatique si nécessaire
350
594
  await migrateTestIfNeeded(test);
351
595
  const questions = [];
@@ -367,7 +611,59 @@ class ExamsRouter extends EnduranceRouter {
367
611
  res.status(500).json({ message: 'Internal server error' });
368
612
  }
369
613
  });
370
- // Lister tous les tests
614
+ /**
615
+ * @swagger
616
+ * /exams:
617
+ * get:
618
+ * summary: Lister les tests
619
+ * description: Liste paginée des tests avec filtres, recherche et tri.
620
+ * tags: [Examens]
621
+ * parameters:
622
+ * - in: query
623
+ * name: page
624
+ * schema:
625
+ * type: integer
626
+ * default: 1
627
+ * - in: query
628
+ * name: limit
629
+ * schema:
630
+ * type: integer
631
+ * default: 10
632
+ * - in: query
633
+ * name: search
634
+ * schema:
635
+ * type: string
636
+ * - in: query
637
+ * name: targetJob
638
+ * schema:
639
+ * type: string
640
+ * default: all
641
+ * - in: query
642
+ * name: seniorityLevel
643
+ * schema:
644
+ * type: string
645
+ * default: all
646
+ * - in: query
647
+ * name: state
648
+ * schema:
649
+ * type: string
650
+ * default: all
651
+ * - in: query
652
+ * name: sortBy
653
+ * schema:
654
+ * type: string
655
+ * default: updatedAt
656
+ * - in: query
657
+ * name: sortOrder
658
+ * schema:
659
+ * type: string
660
+ * default: desc
661
+ * responses:
662
+ * 200:
663
+ * description: Tests paginés
664
+ * 500:
665
+ * description: Erreur interne
666
+ */
371
667
  this.get('/', authenticatedOptions, async (req, res) => {
372
668
  try {
373
669
  const page = parseInt(req.query.page) || 1;
@@ -381,6 +677,10 @@ class ExamsRouter extends EnduranceRouter {
381
677
  const sortOrder = req.query.sortOrder || 'desc';
382
678
  // Construction de la requête de recherche
383
679
  const query = {};
680
+ // Filtre par entité (contexte multi-entités)
681
+ if (req.entity?._id) {
682
+ query.entityId = req.entity._id;
683
+ }
384
684
  // Filtres
385
685
  if (targetJob !== 'all') {
386
686
  // Si on filtre par targetJob, on cherche d'abord le TestJob correspondant
@@ -462,7 +762,36 @@ class ExamsRouter extends EnduranceRouter {
462
762
  res.status(500).json({ message: 'Internal server error' });
463
763
  }
464
764
  });
465
- // Supprimer une catégorie d'un test
765
+ /**
766
+ * @swagger
767
+ * /exams/test/removeCategory/{testId}:
768
+ * delete:
769
+ * summary: Retirer une catégorie d'un test
770
+ * description: Supprime une catégorie d'un test par son nom.
771
+ * tags: [Examens]
772
+ * parameters:
773
+ * - in: path
774
+ * name: testId
775
+ * required: true
776
+ * schema:
777
+ * type: string
778
+ * requestBody:
779
+ * required: true
780
+ * content:
781
+ * application/json:
782
+ * schema:
783
+ * type: object
784
+ * properties:
785
+ * categoryName:
786
+ * type: string
787
+ * responses:
788
+ * 200:
789
+ * description: Catégorie supprimée
790
+ * 404:
791
+ * description: Test ou catégorie non trouvé
792
+ * 500:
793
+ * description: Erreur interne
794
+ */
466
795
  this.delete('/test/removeCategory/:testId', authenticatedOptions, async (req, res) => {
467
796
  const { testId } = req.params;
468
797
  const { categoryName } = req.body;
@@ -470,9 +799,13 @@ class ExamsRouter extends EnduranceRouter {
470
799
  const category = await TestCategory.findOne({ name: categoryName });
471
800
  if (!category)
472
801
  return res.status(404).json({ message: 'Category not found' });
473
- const test = await Test.findByIdAndUpdate(testId, { $pull: { categories: { categoryId: category._id } } }, { new: true });
802
+ let test = await Test.findById(testId);
474
803
  if (!test)
475
804
  return res.status(404).json({ message: 'Test not found' });
805
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
806
+ return res.status(404).json({ message: 'Test not found' });
807
+ }
808
+ test = await Test.findByIdAndUpdate(testId, { $pull: { categories: { categoryId: category._id } } }, { new: true });
476
809
  res.status(200).json({ message: 'Category removed', test });
477
810
  }
478
811
  catch (err) {
@@ -480,7 +813,38 @@ class ExamsRouter extends EnduranceRouter {
480
813
  res.status(500).json({ message: 'Internal server error' });
481
814
  }
482
815
  });
483
- // Ajouter une catégorie à un test
816
+ /**
817
+ * @swagger
818
+ * /exams/test/addCategory/{testId}:
819
+ * put:
820
+ * summary: Ajouter une catégorie à un test
821
+ * description: Ajoute une catégorie (créée si besoin) à un test avec niveau d'expertise.
822
+ * tags: [Examens]
823
+ * parameters:
824
+ * - in: path
825
+ * name: testId
826
+ * required: true
827
+ * schema:
828
+ * type: string
829
+ * requestBody:
830
+ * required: true
831
+ * content:
832
+ * application/json:
833
+ * schema:
834
+ * type: object
835
+ * properties:
836
+ * categoryName:
837
+ * type: string
838
+ * expertiseLevel:
839
+ * type: string
840
+ * responses:
841
+ * 200:
842
+ * description: Catégorie ajoutée
843
+ * 404:
844
+ * description: Test non trouvé
845
+ * 500:
846
+ * description: Erreur interne
847
+ */
484
848
  this.put('/test/addCategory/:testId', authenticatedOptions, async (req, res) => {
485
849
  const { testId } = req.params;
486
850
  const { categoryName, expertiseLevel } = req.body;
@@ -494,6 +858,9 @@ class ExamsRouter extends EnduranceRouter {
494
858
  if (!test) {
495
859
  return res.status(404).json({ message: 'Test not found' });
496
860
  }
861
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
862
+ return res.status(404).json({ message: 'Test not found' });
863
+ }
497
864
  const categoryExists = test.categories.some(cat => cat.categoryId.equals(category._id));
498
865
  if (categoryExists) {
499
866
  return res.status(200).json({ message: 'Category already exists in the test' });
@@ -507,7 +874,27 @@ class ExamsRouter extends EnduranceRouter {
507
874
  res.status(500).json({ message: 'Internal server error' });
508
875
  }
509
876
  });
510
- // Obtenir une question par son ID
877
+ /**
878
+ * @swagger
879
+ * /exams/test/question/{questionId}:
880
+ * get:
881
+ * summary: Détail d'une question
882
+ * description: Retourne une question de test par son identifiant.
883
+ * tags: [Examens]
884
+ * parameters:
885
+ * - in: path
886
+ * name: questionId
887
+ * required: true
888
+ * schema:
889
+ * type: string
890
+ * responses:
891
+ * 200:
892
+ * description: Question trouvée
893
+ * 404:
894
+ * description: Question non trouvée
895
+ * 500:
896
+ * description: Erreur interne
897
+ */
511
898
  this.get('/test/question/:questionId', authenticatedOptions, async (req, res) => {
512
899
  const { questionId } = req.params;
513
900
  const question = await TestQuestion.findById(questionId);
@@ -516,7 +903,27 @@ class ExamsRouter extends EnduranceRouter {
516
903
  }
517
904
  res.status(200).json({ data: question });
518
905
  });
519
- // Obtenir toutes les questions d'un test
906
+ /**
907
+ * @swagger
908
+ * /exams/test/questions/{testId}:
909
+ * get:
910
+ * summary: Lister les questions d'un test
911
+ * description: Retourne toutes les questions d'un test.
912
+ * tags: [Examens]
913
+ * parameters:
914
+ * - in: path
915
+ * name: testId
916
+ * required: true
917
+ * schema:
918
+ * type: string
919
+ * responses:
920
+ * 200:
921
+ * description: Questions retournées
922
+ * 404:
923
+ * description: Test non trouvé
924
+ * 500:
925
+ * description: Erreur interne
926
+ */
520
927
  this.get('/test/questions/:testId', authenticatedOptions, async (req, res) => {
521
928
  const { testId } = req.params;
522
929
  try {
@@ -524,6 +931,9 @@ class ExamsRouter extends EnduranceRouter {
524
931
  if (!test) {
525
932
  return res.status(404).json({ message: 'Test not found' });
526
933
  }
934
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
935
+ return res.status(404).json({ message: 'Test not found' });
936
+ }
527
937
  const questions = [];
528
938
  for (const questionId of test.questions) {
529
939
  const question = await TestQuestion.findById(questionId);
@@ -538,7 +948,32 @@ class ExamsRouter extends EnduranceRouter {
538
948
  res.status(500).json({ message: 'Internal server error' });
539
949
  }
540
950
  });
541
- // Supprimer une question d'un test
951
+ /**
952
+ * @swagger
953
+ * /exams/test/question/{testId}/{questionId}:
954
+ * delete:
955
+ * summary: Supprimer une question d'un test
956
+ * description: Supprime une question spécifique et réordonne les questions restantes.
957
+ * tags: [Examens]
958
+ * parameters:
959
+ * - in: path
960
+ * name: testId
961
+ * required: true
962
+ * schema:
963
+ * type: string
964
+ * - in: path
965
+ * name: questionId
966
+ * required: true
967
+ * schema:
968
+ * type: string
969
+ * responses:
970
+ * 200:
971
+ * description: Question supprimée
972
+ * 404:
973
+ * description: Test ou question non trouvés
974
+ * 500:
975
+ * description: Erreur interne
976
+ */
542
977
  this.delete('/test/question/:testId/:questionId', authenticatedOptions, async (req, res) => {
543
978
  const { testId, questionId } = req.params;
544
979
  const question = await TestQuestion.findByIdAndDelete(questionId);
@@ -549,6 +984,9 @@ class ExamsRouter extends EnduranceRouter {
549
984
  if (!test) {
550
985
  return res.status(404).json({ message: 'no test founded with this id' });
551
986
  }
987
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
988
+ return res.status(404).json({ message: 'no test founded with this id' });
989
+ }
552
990
  // Supprimer la question du tableau questions en filtrant par questionId
553
991
  test.questions = test.questions.filter(q => q.questionId.toString() !== questionId);
554
992
  // Recalculer les ordres pour que ça se suive
@@ -558,13 +996,36 @@ class ExamsRouter extends EnduranceRouter {
558
996
  await test.save();
559
997
  res.status(200).json({ message: 'question deleted with sucess' });
560
998
  });
561
- // Supprimer toutes les questions d'un test
999
+ /**
1000
+ * @swagger
1001
+ * /exams/test/questions/{testId}:
1002
+ * delete:
1003
+ * summary: Supprimer toutes les questions d'un test
1004
+ * description: Supprime toutes les questions associées à un test.
1005
+ * tags: [Examens]
1006
+ * parameters:
1007
+ * - in: path
1008
+ * name: testId
1009
+ * required: true
1010
+ * schema:
1011
+ * type: string
1012
+ * responses:
1013
+ * 200:
1014
+ * description: Questions supprimées
1015
+ * 404:
1016
+ * description: Test non trouvé
1017
+ * 500:
1018
+ * description: Erreur interne
1019
+ */
562
1020
  this.delete('/test/questions/:testId', authenticatedOptions, async (req, res) => {
563
1021
  const { testId } = req.params;
564
1022
  const test = await Test.findById(testId);
565
1023
  if (!test) {
566
1024
  return res.status(404).json({ message: 'no test founded with this id' });
567
1025
  }
1026
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
1027
+ return res.status(404).json({ message: 'no test founded with this id' });
1028
+ }
568
1029
  for (const questionId of test.questions) {
569
1030
  await TestQuestion.findByIdAndDelete(questionId);
570
1031
  }
@@ -572,7 +1033,33 @@ class ExamsRouter extends EnduranceRouter {
572
1033
  await test.save();
573
1034
  res.status(200).json({ message: 'questions deleted with sucess' });
574
1035
  });
575
- // Modifier une question
1036
+ /**
1037
+ * @swagger
1038
+ * /exams/test/modifyQuestion/{id}:
1039
+ * put:
1040
+ * summary: Modifier une question
1041
+ * description: Met à jour les champs d'une question (texte, score, temps, réponses possibles).
1042
+ * tags: [Examens]
1043
+ * parameters:
1044
+ * - in: path
1045
+ * name: id
1046
+ * required: true
1047
+ * schema:
1048
+ * type: string
1049
+ * requestBody:
1050
+ * required: true
1051
+ * content:
1052
+ * application/json:
1053
+ * schema:
1054
+ * type: object
1055
+ * responses:
1056
+ * 200:
1057
+ * description: Question modifiée
1058
+ * 404:
1059
+ * description: Question non trouvée
1060
+ * 500:
1061
+ * description: Erreur interne
1062
+ */
576
1063
  this.put('/test/modifyQuestion/:id', authenticatedOptions, async (req, res) => {
577
1064
  const { id } = req.params;
578
1065
  const { instruction, maxScore, time, possibleResponses, textType } = req.body;
@@ -604,7 +1091,42 @@ class ExamsRouter extends EnduranceRouter {
604
1091
  res.status(500).json({ message: 'Internal server error' });
605
1092
  }
606
1093
  });
607
- // Ajouter une question à un test
1094
+ /**
1095
+ * @swagger
1096
+ * /exams/test/addCustomQuestion/{id}:
1097
+ * put:
1098
+ * summary: Ajouter une question personnalisée
1099
+ * description: Ajoute une question manuelle à un test.
1100
+ * tags: [Examens]
1101
+ * parameters:
1102
+ * - in: path
1103
+ * name: id
1104
+ * required: true
1105
+ * schema:
1106
+ * type: string
1107
+ * requestBody:
1108
+ * required: true
1109
+ * content:
1110
+ * application/json:
1111
+ * schema:
1112
+ * type: object
1113
+ * properties:
1114
+ * questionType:
1115
+ * type: string
1116
+ * instruction:
1117
+ * type: string
1118
+ * maxScore:
1119
+ * type: number
1120
+ * time:
1121
+ * type: number
1122
+ * responses:
1123
+ * 200:
1124
+ * description: Question ajoutée
1125
+ * 404:
1126
+ * description: Test non trouvé
1127
+ * 500:
1128
+ * description: Erreur interne
1129
+ */
608
1130
  this.put('/test/addCustomQuestion/:id', authenticatedOptions, async (req, res) => {
609
1131
  const { id } = req.params;
610
1132
  const { questionType, instruction, maxScore, time } = req.body;
@@ -613,6 +1135,9 @@ class ExamsRouter extends EnduranceRouter {
613
1135
  if (!test) {
614
1136
  return res.status(404).json({ message: 'no test founded with this id' });
615
1137
  }
1138
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
1139
+ return res.status(404).json({ message: 'no test founded with this id' });
1140
+ }
616
1141
  const question = new TestQuestion({
617
1142
  questionType,
618
1143
  instruction,
@@ -629,7 +1154,40 @@ class ExamsRouter extends EnduranceRouter {
629
1154
  res.status(500).json({ message: 'Internal server error' });
630
1155
  }
631
1156
  });
632
- // Ajouter une question à un test
1157
+ /**
1158
+ * @swagger
1159
+ * /exams/test/addQuestion/{id}:
1160
+ * put:
1161
+ * summary: Générer et ajouter une question
1162
+ * description: Génère une question via assistant et l'ajoute au test.
1163
+ * tags: [Examens]
1164
+ * parameters:
1165
+ * - in: path
1166
+ * name: id
1167
+ * required: true
1168
+ * schema:
1169
+ * type: string
1170
+ * requestBody:
1171
+ * required: true
1172
+ * content:
1173
+ * application/json:
1174
+ * schema:
1175
+ * type: object
1176
+ * properties:
1177
+ * questionType:
1178
+ * type: string
1179
+ * category:
1180
+ * type: string
1181
+ * expertiseLevel:
1182
+ * type: string
1183
+ * responses:
1184
+ * 200:
1185
+ * description: Question générée et ajoutée
1186
+ * 404:
1187
+ * description: Test non trouvé
1188
+ * 500:
1189
+ * description: Erreur interne
1190
+ */
633
1191
  this.put('/test/addQuestion/:id', authenticatedOptions, async (req, res) => {
634
1192
  const { id } = req.params;
635
1193
  const { questionType, category, expertiseLevel } = req.body;
@@ -638,6 +1196,9 @@ class ExamsRouter extends EnduranceRouter {
638
1196
  if (!test) {
639
1197
  return res.status(404).json({ message: 'no test founded with this id' });
640
1198
  }
1199
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
1200
+ return res.status(404).json({ message: 'no test founded with this id' });
1201
+ }
641
1202
  const otherQuestionsIds = test.questions.map(question => question.questionId);
642
1203
  const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
643
1204
  const jobName = await getJobName(test.targetJob);
@@ -660,7 +1221,27 @@ class ExamsRouter extends EnduranceRouter {
660
1221
  res.status(500).json({ message: 'Internal server error' });
661
1222
  }
662
1223
  });
663
- // Mélanger les questions d'un test
1224
+ /**
1225
+ * @swagger
1226
+ * /exams/test/shuffle/{testId}:
1227
+ * get:
1228
+ * summary: Mélanger les questions d'un test
1229
+ * description: Mélange l'ordre des questions d'un test.
1230
+ * tags: [Examens]
1231
+ * parameters:
1232
+ * - in: path
1233
+ * name: testId
1234
+ * required: true
1235
+ * schema:
1236
+ * type: string
1237
+ * responses:
1238
+ * 200:
1239
+ * description: Questions mélangées
1240
+ * 404:
1241
+ * description: Test non trouvé
1242
+ * 500:
1243
+ * description: Erreur interne
1244
+ */
664
1245
  this.get('/test/shuffle/:testId', authenticatedOptions, async (req, res) => {
665
1246
  const { testId } = req.params;
666
1247
  try {
@@ -668,6 +1249,9 @@ class ExamsRouter extends EnduranceRouter {
668
1249
  if (!test) {
669
1250
  return res.status(404).json({ message: 'Test not found' });
670
1251
  }
1252
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
1253
+ return res.status(404).json({ message: 'Test not found' });
1254
+ }
671
1255
  for (let i = test.questions.length - 1; i > 0; i--) {
672
1256
  const j = Math.floor(Math.random() * (i + 1));
673
1257
  [test.questions[i], test.questions[j]] = [test.questions[j], test.questions[i]];
@@ -680,7 +1264,36 @@ class ExamsRouter extends EnduranceRouter {
680
1264
  res.status(500).json({ message: 'Internal server error' });
681
1265
  }
682
1266
  });
683
- // Ajouter un texte d'invitation à un test
1267
+ /**
1268
+ * @swagger
1269
+ * /exams/test/addInvitationText/{id}:
1270
+ * put:
1271
+ * summary: Ajouter un texte d'invitation
1272
+ * description: Ajoute ou met à jour le texte d'invitation utilisé pour un test.
1273
+ * tags: [Examens]
1274
+ * parameters:
1275
+ * - in: path
1276
+ * name: id
1277
+ * required: true
1278
+ * schema:
1279
+ * type: string
1280
+ * requestBody:
1281
+ * required: true
1282
+ * content:
1283
+ * application/json:
1284
+ * schema:
1285
+ * type: object
1286
+ * properties:
1287
+ * invitationText:
1288
+ * type: string
1289
+ * responses:
1290
+ * 200:
1291
+ * description: Texte mis à jour
1292
+ * 404:
1293
+ * description: Test non trouvé
1294
+ * 500:
1295
+ * description: Erreur interne
1296
+ */
684
1297
  this.put('/test/addInvitationText/:id', authenticatedOptions, async (req, res) => {
685
1298
  const { id } = req.params;
686
1299
  const { invitationText } = req.body;
@@ -689,6 +1302,9 @@ class ExamsRouter extends EnduranceRouter {
689
1302
  if (!test) {
690
1303
  return res.status(404).json({ message: 'no test founded with this id' });
691
1304
  }
1305
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
1306
+ return res.status(404).json({ message: 'no test founded with this id' });
1307
+ }
692
1308
  test.invitationText = invitationText;
693
1309
  await test.save();
694
1310
  res.status(200).json({
@@ -701,7 +1317,27 @@ class ExamsRouter extends EnduranceRouter {
701
1317
  res.status(500).json({ message: 'Internal server error' });
702
1318
  }
703
1319
  });
704
- // Obtenir un résultat par son ID
1320
+ /**
1321
+ * @swagger
1322
+ * /exams/result/{id}:
1323
+ * get:
1324
+ * summary: Détail d'un résultat
1325
+ * description: Retourne un TestResult par identifiant.
1326
+ * tags: [Résultats]
1327
+ * parameters:
1328
+ * - in: path
1329
+ * name: id
1330
+ * required: true
1331
+ * schema:
1332
+ * type: string
1333
+ * responses:
1334
+ * 200:
1335
+ * description: Résultat trouvé
1336
+ * 404:
1337
+ * description: Résultat non trouvé
1338
+ * 500:
1339
+ * description: Erreur interne
1340
+ */
705
1341
  this.get('/result/:id', authenticatedOptions, async (req, res) => {
706
1342
  const { id } = req.params;
707
1343
  try {
@@ -716,7 +1352,21 @@ class ExamsRouter extends EnduranceRouter {
716
1352
  res.status(500).json({ message: 'Internal server error' });
717
1353
  }
718
1354
  });
719
- // Lister tous les résultats
1355
+ /**
1356
+ * @swagger
1357
+ * /exams/results/:
1358
+ * get:
1359
+ * summary: Lister les résultats
1360
+ * description: Retourne tous les résultats existants.
1361
+ * tags: [Résultats]
1362
+ * responses:
1363
+ * 200:
1364
+ * description: Liste des résultats
1365
+ * 404:
1366
+ * description: Aucun résultat
1367
+ * 500:
1368
+ * description: Erreur interne
1369
+ */
720
1370
  this.get('/results/', authenticatedOptions, async (req, res) => {
721
1371
  try {
722
1372
  const results = await TestResult.find();
@@ -730,7 +1380,35 @@ class ExamsRouter extends EnduranceRouter {
730
1380
  res.status(500).json({ message: 'Internal server error' });
731
1381
  }
732
1382
  });
733
- // Créer un résultat
1383
+ /**
1384
+ * @swagger
1385
+ * /exams/invite:
1386
+ * post:
1387
+ * summary: Inviter un candidat à un test
1388
+ * description: Crée un TestResult et envoie un email d'invitation au candidat.
1389
+ * tags: [Résultats]
1390
+ * requestBody:
1391
+ * required: true
1392
+ * content:
1393
+ * application/json:
1394
+ * schema:
1395
+ * type: object
1396
+ * required: [candidateId, testId]
1397
+ * properties:
1398
+ * candidateId:
1399
+ * type: string
1400
+ * testId:
1401
+ * type: string
1402
+ * responses:
1403
+ * 201:
1404
+ * description: Invitation créée
1405
+ * 404:
1406
+ * description: Test ou candidat non trouvé
1407
+ * 400:
1408
+ * description: Paramètres manquants
1409
+ * 500:
1410
+ * description: Erreur interne
1411
+ */
734
1412
  this.post('/invite', authenticatedOptions, async (req, res) => {
735
1413
  const { candidateId, testId } = req.body;
736
1414
  if (!candidateId || !testId) {
@@ -786,7 +1464,32 @@ class ExamsRouter extends EnduranceRouter {
786
1464
  res.status(500).json({ message: 'Internal server error' });
787
1465
  }
788
1466
  });
789
- // Obtenir la question suivante
1467
+ /**
1468
+ * @swagger
1469
+ * /exams/result/getNextQuestion/{id}/{idCurrentQuestion}:
1470
+ * get:
1471
+ * summary: Obtenir la question suivante
1472
+ * description: Retourne la prochaine question pour un TestResult ou null s'il n'y en a plus.
1473
+ * tags: [Résultats]
1474
+ * parameters:
1475
+ * - in: path
1476
+ * name: id
1477
+ * required: true
1478
+ * schema:
1479
+ * type: string
1480
+ * - in: path
1481
+ * name: idCurrentQuestion
1482
+ * required: true
1483
+ * schema:
1484
+ * type: string
1485
+ * responses:
1486
+ * 200:
1487
+ * description: Prochaine question ou null
1488
+ * 404:
1489
+ * description: Résultat ou test non trouvé
1490
+ * 500:
1491
+ * description: Erreur interne
1492
+ */
790
1493
  this.get('/result/getNextQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
791
1494
  const { id, idCurrentQuestion } = req.params;
792
1495
  try {
@@ -812,7 +1515,32 @@ class ExamsRouter extends EnduranceRouter {
812
1515
  res.status(500).json({ message: 'Internal server error' });
813
1516
  }
814
1517
  });
815
- // Vérifier si c'est la dernière question
1518
+ /**
1519
+ * @swagger
1520
+ * /exams/result/isLastQuestion/{id}/{idCurrentQuestion}:
1521
+ * get:
1522
+ * summary: Vérifier la dernière question
1523
+ * description: Indique si la question courante est la dernière du test.
1524
+ * tags: [Résultats]
1525
+ * parameters:
1526
+ * - in: path
1527
+ * name: id
1528
+ * required: true
1529
+ * schema:
1530
+ * type: string
1531
+ * - in: path
1532
+ * name: idCurrentQuestion
1533
+ * required: true
1534
+ * schema:
1535
+ * type: string
1536
+ * responses:
1537
+ * 200:
1538
+ * description: Booléen retourné
1539
+ * 404:
1540
+ * description: Résultat ou test non trouvé
1541
+ * 500:
1542
+ * description: Erreur interne
1543
+ */
816
1544
  this.get('/result/isLastQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
817
1545
  const { id, idCurrentQuestion } = req.params;
818
1546
  try {
@@ -837,7 +1565,27 @@ class ExamsRouter extends EnduranceRouter {
837
1565
  res.status(500).json({ message: 'Internal server error' });
838
1566
  }
839
1567
  });
840
- // Obtenir une question
1568
+ /**
1569
+ * @swagger
1570
+ * /exams/result/question/{questionId}:
1571
+ * get:
1572
+ * summary: Obtenir une question (résultat)
1573
+ * description: Retourne une question via son identifiant pour un résultat.
1574
+ * tags: [Résultats]
1575
+ * parameters:
1576
+ * - in: path
1577
+ * name: questionId
1578
+ * required: true
1579
+ * schema:
1580
+ * type: string
1581
+ * responses:
1582
+ * 200:
1583
+ * description: Question trouvée
1584
+ * 404:
1585
+ * description: Question non trouvée
1586
+ * 500:
1587
+ * description: Erreur interne
1588
+ */
841
1589
  this.get('/result/question/:questionId', authenticatedOptions, async (req, res) => {
842
1590
  const { questionId } = req.params;
843
1591
  try {
@@ -852,7 +1600,41 @@ class ExamsRouter extends EnduranceRouter {
852
1600
  res.status(500).json({ message: 'Internal server error' });
853
1601
  }
854
1602
  });
855
- // Envoyer une réponse
1603
+ /**
1604
+ * @swagger
1605
+ * /exams/result/sendResponse/{id}/{idCurrentQuestion}:
1606
+ * put:
1607
+ * summary: Envoyer une réponse
1608
+ * description: Enregistre la réponse d'un candidat pour une question d'un test.
1609
+ * tags: [Résultats]
1610
+ * parameters:
1611
+ * - in: path
1612
+ * name: id
1613
+ * required: true
1614
+ * schema:
1615
+ * type: string
1616
+ * - in: path
1617
+ * name: idCurrentQuestion
1618
+ * required: true
1619
+ * schema:
1620
+ * type: string
1621
+ * requestBody:
1622
+ * required: true
1623
+ * content:
1624
+ * application/json:
1625
+ * schema:
1626
+ * type: object
1627
+ * properties:
1628
+ * candidateResponse:
1629
+ * type: string
1630
+ * responses:
1631
+ * 200:
1632
+ * description: Réponse enregistrée
1633
+ * 404:
1634
+ * description: Test ou résultat non trouvé
1635
+ * 500:
1636
+ * description: Erreur interne
1637
+ */
856
1638
  this.put('/result/sendResponse/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
857
1639
  const { id, idCurrentQuestion } = req.params;
858
1640
  const { candidateResponse } = req.body;
@@ -889,7 +1671,27 @@ class ExamsRouter extends EnduranceRouter {
889
1671
  res.status(500).json({ message: 'Internal server error' });
890
1672
  }
891
1673
  });
892
- // Corriger un test
1674
+ /**
1675
+ * @swagger
1676
+ * /exams/result/correct/{id}:
1677
+ * post:
1678
+ * summary: Lancer la correction d'un test
1679
+ * description: Déclenche la correction d'un TestResult.
1680
+ * tags: [Résultats]
1681
+ * parameters:
1682
+ * - in: path
1683
+ * name: id
1684
+ * required: true
1685
+ * schema:
1686
+ * type: string
1687
+ * responses:
1688
+ * 200:
1689
+ * description: Correction lancée
1690
+ * 404:
1691
+ * description: Résultat non trouvé
1692
+ * 500:
1693
+ * description: Erreur interne
1694
+ */
893
1695
  this.post('/result/correct/:id', authenticatedOptions, async (req, res) => {
894
1696
  const { id } = req.params;
895
1697
  try {
@@ -905,7 +1707,27 @@ class ExamsRouter extends EnduranceRouter {
905
1707
  res.status(500).json({ message: 'Internal server error' });
906
1708
  }
907
1709
  });
908
- // Calculer le score
1710
+ /**
1711
+ * @swagger
1712
+ * /exams/result/calculateScore/{id}:
1713
+ * put:
1714
+ * summary: Calculer le score
1715
+ * description: Calcule et enregistre le score final d'un TestResult.
1716
+ * tags: [Résultats]
1717
+ * parameters:
1718
+ * - in: path
1719
+ * name: id
1720
+ * required: true
1721
+ * schema:
1722
+ * type: string
1723
+ * responses:
1724
+ * 200:
1725
+ * description: Score calculé
1726
+ * 404:
1727
+ * description: Résultat non trouvé
1728
+ * 500:
1729
+ * description: Erreur interne
1730
+ */
909
1731
  this.put('/result/calculateScore/:id', authenticatedOptions, async (req, res) => {
910
1732
  const { id } = req.params;
911
1733
  try {
@@ -948,7 +1770,27 @@ class ExamsRouter extends EnduranceRouter {
948
1770
  res.status(500).json({ message: 'Internal server error' });
949
1771
  }
950
1772
  });
951
- // Obtenir le score maximum
1773
+ /**
1774
+ * @swagger
1775
+ * /exams/maxscore/{resultId}:
1776
+ * get:
1777
+ * summary: Obtenir le score maximum
1778
+ * description: Calcule le score maximal possible pour un résultat.
1779
+ * tags: [Résultats]
1780
+ * parameters:
1781
+ * - in: path
1782
+ * name: resultId
1783
+ * required: true
1784
+ * schema:
1785
+ * type: string
1786
+ * responses:
1787
+ * 200:
1788
+ * description: Score maximum retourné
1789
+ * 404:
1790
+ * description: Résultat ou test non trouvé
1791
+ * 500:
1792
+ * description: Erreur interne
1793
+ */
952
1794
  this.get('/maxscore/:resultId', authenticatedOptions, async (req, res) => {
953
1795
  const { resultId } = req.params;
954
1796
  try {
@@ -974,7 +1816,27 @@ class ExamsRouter extends EnduranceRouter {
974
1816
  res.status(500).json({ message: 'Internal server error' });
975
1817
  }
976
1818
  });
977
- // Obtenir le score d'un résultat
1819
+ /**
1820
+ * @swagger
1821
+ * /exams/result/score/{id}:
1822
+ * get:
1823
+ * summary: Obtenir le score d'un résultat
1824
+ * description: Retourne le score calculé d'un TestResult.
1825
+ * tags: [Résultats]
1826
+ * parameters:
1827
+ * - in: path
1828
+ * name: id
1829
+ * required: true
1830
+ * schema:
1831
+ * type: string
1832
+ * responses:
1833
+ * 200:
1834
+ * description: Score retourné
1835
+ * 404:
1836
+ * description: Résultat non trouvé
1837
+ * 500:
1838
+ * description: Erreur interne
1839
+ */
978
1840
  this.get('/result/score/:id', authenticatedOptions, async (req, res) => {
979
1841
  const { id } = req.params;
980
1842
  try {
@@ -989,7 +1851,43 @@ class ExamsRouter extends EnduranceRouter {
989
1851
  res.status(500).json({ message: 'Internal server error' });
990
1852
  }
991
1853
  });
992
- // Générer plusieurs questions pour un test
1854
+ /**
1855
+ * @swagger
1856
+ * /exams/test/generateQuestions/{id}:
1857
+ * put:
1858
+ * summary: Générer des questions pour un test
1859
+ * description: Génère plusieurs questions (IA) pour un test donné, éventuellement filtrées par catégorie et type.
1860
+ * tags: [Examens]
1861
+ * parameters:
1862
+ * - in: path
1863
+ * name: id
1864
+ * required: true
1865
+ * schema:
1866
+ * type: string
1867
+ * requestBody:
1868
+ * required: true
1869
+ * content:
1870
+ * application/json:
1871
+ * schema:
1872
+ * type: object
1873
+ * required: [numberOfQuestions]
1874
+ * properties:
1875
+ * numberOfQuestions:
1876
+ * type: integer
1877
+ * category:
1878
+ * type: string
1879
+ * questionType:
1880
+ * type: string
1881
+ * responses:
1882
+ * 200:
1883
+ * description: Questions générées
1884
+ * 400:
1885
+ * description: Paramètres invalides
1886
+ * 404:
1887
+ * description: Test non trouvé
1888
+ * 500:
1889
+ * description: Erreur interne
1890
+ */
993
1891
  this.put('/test/generateQuestions/:id', authenticatedOptions, async (req, res) => {
994
1892
  const { id } = req.params;
995
1893
  const { numberOfQuestions, category, questionType } = req.body;
@@ -1001,6 +1899,9 @@ class ExamsRouter extends EnduranceRouter {
1001
1899
  if (!test) {
1002
1900
  return res.status(404).json({ message: 'Test non trouvé' });
1003
1901
  }
1902
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
1903
+ return res.status(404).json({ message: 'Test non trouvé' });
1904
+ }
1004
1905
  let categoriesToUse = [];
1005
1906
  if (category && category !== 'ALL') {
1006
1907
  const categoryInfo = test.categories.find(cat => cat.categoryId.toString() === category);
@@ -1069,7 +1970,56 @@ class ExamsRouter extends EnduranceRouter {
1069
1970
  res.status(500).json({ message: 'Erreur interne du serveur' });
1070
1971
  }
1071
1972
  });
1072
- // Lister tous les candidats invités à un test
1973
+ /**
1974
+ * @swagger
1975
+ * /exams/test/{testId}/candidates:
1976
+ * get:
1977
+ * summary: Lister les candidats invités
1978
+ * description: Liste paginée des candidats invités à un test avec filtres et tri.
1979
+ * tags: [Résultats]
1980
+ * parameters:
1981
+ * - in: path
1982
+ * name: testId
1983
+ * required: true
1984
+ * schema:
1985
+ * type: string
1986
+ * - in: query
1987
+ * name: page
1988
+ * schema:
1989
+ * type: integer
1990
+ * default: 1
1991
+ * - in: query
1992
+ * name: limit
1993
+ * schema:
1994
+ * type: integer
1995
+ * default: 10
1996
+ * - in: query
1997
+ * name: search
1998
+ * schema:
1999
+ * type: string
2000
+ * - in: query
2001
+ * name: state
2002
+ * schema:
2003
+ * type: string
2004
+ * default: all
2005
+ * - in: query
2006
+ * name: sortBy
2007
+ * schema:
2008
+ * type: string
2009
+ * default: invitationDate
2010
+ * - in: query
2011
+ * name: sortOrder
2012
+ * schema:
2013
+ * type: string
2014
+ * default: desc
2015
+ * responses:
2016
+ * 200:
2017
+ * description: Candidats paginés
2018
+ * 404:
2019
+ * description: Test non trouvé
2020
+ * 500:
2021
+ * description: Erreur interne
2022
+ */
1073
2023
  this.get('/test/:testId/candidates', authenticatedOptions, async (req, res) => {
1074
2024
  const { testId } = req.params;
1075
2025
  const page = parseInt(req.query.page) || 1;
@@ -1084,6 +2034,9 @@ class ExamsRouter extends EnduranceRouter {
1084
2034
  if (!test) {
1085
2035
  return res.status(404).json({ message: 'Test non trouvé' });
1086
2036
  }
2037
+ if (req.entity?._id && test.entityId && !test.entityId.equals(req.entity._id)) {
2038
+ return res.status(404).json({ message: 'Test non trouvé' });
2039
+ }
1087
2040
  // Construction de la requête
1088
2041
  const query = { testId };
1089
2042
  if (state !== 'all') {
@@ -1231,7 +2184,27 @@ class ExamsRouter extends EnduranceRouter {
1231
2184
  res.status(500).json({ message: 'Erreur interne du serveur' });
1232
2185
  }
1233
2186
  });
1234
- // Renvoyer l'email d'invitation à un candidat
2187
+ /**
2188
+ * @swagger
2189
+ * /exams/reinvite/{resultId}:
2190
+ * post:
2191
+ * summary: Renvoyer une invitation
2192
+ * description: Récupère un TestResult et renvoie l'email d'invitation.
2193
+ * tags: [Résultats]
2194
+ * parameters:
2195
+ * - in: path
2196
+ * name: resultId
2197
+ * required: true
2198
+ * schema:
2199
+ * type: string
2200
+ * responses:
2201
+ * 200:
2202
+ * description: Invitation renvoyée
2203
+ * 404:
2204
+ * description: Ressource non trouvée
2205
+ * 500:
2206
+ * description: Erreur interne
2207
+ */
1235
2208
  this.post('/reinvite/:resultId', authenticatedOptions, async (req, res) => {
1236
2209
  const { resultId } = req.params;
1237
2210
  try {
@@ -1280,7 +2253,45 @@ class ExamsRouter extends EnduranceRouter {
1280
2253
  res.status(500).json({ message: 'Internal server error' });
1281
2254
  }
1282
2255
  });
1283
- // Correction manuelle d'une réponse à une question d'un testResult
2256
+ /**
2257
+ * @swagger
2258
+ * /exams/result/{testResultId}/response/{questionId}:
2259
+ * put:
2260
+ * summary: Correction manuelle d'une réponse
2261
+ * description: Met à jour le score/commentaire d'une réponse et recalcule le score global.
2262
+ * tags: [Résultats]
2263
+ * parameters:
2264
+ * - in: path
2265
+ * name: testResultId
2266
+ * required: true
2267
+ * schema:
2268
+ * type: string
2269
+ * - in: path
2270
+ * name: questionId
2271
+ * required: true
2272
+ * schema:
2273
+ * type: string
2274
+ * requestBody:
2275
+ * required: true
2276
+ * content:
2277
+ * application/json:
2278
+ * schema:
2279
+ * type: object
2280
+ * properties:
2281
+ * score:
2282
+ * type: number
2283
+ * comment:
2284
+ * type: string
2285
+ * responses:
2286
+ * 200:
2287
+ * description: Correction enregistrée
2288
+ * 404:
2289
+ * description: Ressource non trouvée
2290
+ * 400:
2291
+ * description: Score invalide
2292
+ * 500:
2293
+ * description: Erreur interne
2294
+ */
1284
2295
  this.put('/result/:testResultId/response/:questionId', authenticatedOptions, async (req, res) => {
1285
2296
  try {
1286
2297
  const { testResultId, questionId } = req.params;
@@ -1323,7 +2334,27 @@ class ExamsRouter extends EnduranceRouter {
1323
2334
  res.status(500).json({ message: 'Erreur interne du serveur' });
1324
2335
  }
1325
2336
  });
1326
- // Supprimer un test result
2337
+ /**
2338
+ * @swagger
2339
+ * /exams/result/{id}:
2340
+ * delete:
2341
+ * summary: Supprimer un résultat
2342
+ * description: Supprime un TestResult.
2343
+ * tags: [Résultats]
2344
+ * parameters:
2345
+ * - in: path
2346
+ * name: id
2347
+ * required: true
2348
+ * schema:
2349
+ * type: string
2350
+ * responses:
2351
+ * 200:
2352
+ * description: Résultat supprimé
2353
+ * 404:
2354
+ * description: Résultat non trouvé
2355
+ * 500:
2356
+ * description: Erreur interne
2357
+ */
1327
2358
  this.delete('/result/:id', authenticatedOptions, async (req, res) => {
1328
2359
  const { id } = req.params;
1329
2360
  try {