From 350f3b898adffb0f4c11519fb965555cd059bda6 Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:20:43 +0100 Subject: [PATCH 1/3] tech(api): migrate particpant result repository to its bounded-context --- .../participant-result-repository.js | 30 +++++++++---------- .../participant-result-repository_test.js | 8 ++--- 2 files changed, 19 insertions(+), 19 deletions(-) rename api/{lib => src/prescription/campaign-participation}/infrastructure/repositories/participant-result-repository.js (84%) rename api/tests/{ => prescription/campaign-participation}/integration/infrastructure/repositories/participant-result-repository_test.js (99%) diff --git a/api/lib/infrastructure/repositories/participant-result-repository.js b/api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js similarity index 84% rename from api/lib/infrastructure/repositories/participant-result-repository.js rename to api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js index 8b39d15c4c1..ca488fb99dd 100644 --- a/api/lib/infrastructure/repositories/participant-result-repository.js +++ b/api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js @@ -1,20 +1,20 @@ import _ from 'lodash'; -import { knex } from '../../../db/knex-database-connection.js'; -import * as flash from '../../../src/certification/flash-certification/domain/services/algorithm-methods/flash.js'; -import * as dataFetcher from '../../../src/evaluation/domain/services/algorithm-methods/data-fetcher.js'; -import { convertLevelStagesIntoThresholds } from '../../../src/evaluation/domain/services/stages/convert-level-stages-into-thresholds-service.js'; -import { NotFoundError } from '../../../src/shared/domain/errors.js'; -import { Assessment } from '../../../src/shared/domain/models/index.js'; -import { AssessmentResult } from '../../../src/shared/domain/read-models/participant-results/AssessmentResult.js'; -import * as answerRepository from '../../../src/shared/infrastructure/repositories/answer-repository.js'; -import * as areaRepository from '../../../src/shared/infrastructure/repositories/area-repository.js'; -import * as challengeRepository from '../../../src/shared/infrastructure/repositories/challenge-repository.js'; -import * as competenceRepository from '../../../src/shared/infrastructure/repositories/competence-repository.js'; -import * as skillRepository from '../../../src/shared/infrastructure/repositories/skill-repository.js'; -import * as campaignRepository from './campaign-repository.js'; -import * as flashAssessmentResultRepository from './flash-assessment-result-repository.js'; -import * as knowledgeElementRepository from './knowledge-element-repository.js'; +import { knex } from '../../../../../db/knex-database-connection.js'; +import * as flash from '../../../../certification/flash-certification/domain/services/algorithm-methods/flash.js'; +import * as dataFetcher from '../../../../evaluation/domain/services/algorithm-methods/data-fetcher.js'; +import { convertLevelStagesIntoThresholds } from '../../../../evaluation/domain/services/stages/convert-level-stages-into-thresholds-service.js'; +import { NotFoundError } from '../../../../shared/domain/errors.js'; +import { Assessment } from '../../../../shared/domain/models/index.js'; +import { AssessmentResult } from '../../../../shared/domain/read-models/participant-results/AssessmentResult.js'; +import * as answerRepository from '../../../../shared/infrastructure/repositories/answer-repository.js'; +import * as areaRepository from '../../../../shared/infrastructure/repositories/area-repository.js'; +import * as challengeRepository from '../../../../shared/infrastructure/repositories/challenge-repository.js'; +import * as competenceRepository from '../../../../shared/infrastructure/repositories/competence-repository.js'; +import * as skillRepository from '../../../../shared/infrastructure/repositories/skill-repository.js'; +import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js'; +import * as flashAssessmentResultRepository from '../../../../../lib/infrastructure/repositories/flash-assessment-result-repository.js'; +import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js'; /** * diff --git a/api/tests/integration/infrastructure/repositories/participant-result-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js similarity index 99% rename from api/tests/integration/infrastructure/repositories/participant-result-repository_test.js rename to api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js index a31bb810937..343a80093cc 100644 --- a/api/tests/integration/infrastructure/repositories/participant-result-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js @@ -1,11 +1,11 @@ -import * as participantResultRepository from '../../../../lib/infrastructure/repositories/participant-result-repository.js'; -import { NotFoundError } from '../../../../src/shared/domain/errors.js'; +import * as participantResultRepository from '../../../../../../src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js'; +import { NotFoundError } from '../../../../../../src/shared/domain/errors.js'; import { Assessment, CampaignParticipationStatuses, KnowledgeElement, -} from '../../../../src/shared/domain/models/index.js'; -import { catchErr, databaseBuilder, domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; +} from '../../../../../../src/shared/domain/models/index.js'; +import { catchErr, databaseBuilder, domainBuilder, expect, mockLearningContent } from '../../../../../test-helper.js'; const { STARTED } = CampaignParticipationStatuses; From b986fd53886b3d96c5bdb724065f128f3737caa4 Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:25:37 +0100 Subject: [PATCH 2/3] tech(api): migrate particpant result usecase to its bounded-context --- api/lib/domain/usecases/index.js | 2 - .../get-user-campaign-assessment-result.js | 18 +++---- .../domain/usecases/index.js | 50 +++++++++++++++++-- ...et-user-campaign-assessment-result_test.js | 11 ++-- 4 files changed, 59 insertions(+), 22 deletions(-) rename api/{lib => src/prescription/campaign-participation}/domain/usecases/get-user-campaign-assessment-result.js (73%) rename api/tests/{ => prescription/campaign-participation}/unit/domain/usecases/get-user-campaign-assessment-result_test.js (92%) diff --git a/api/lib/domain/usecases/index.js b/api/lib/domain/usecases/index.js index af0c6fd437d..b7ce7b4f65f 100644 --- a/api/lib/domain/usecases/index.js +++ b/api/lib/domain/usecases/index.js @@ -144,7 +144,6 @@ import * as membershipRepository from '../../infrastructure/repositories/members import * as organizationLearnerRepository from '../../infrastructure/repositories/organization-learner-repository.js'; import * as organizationMemberIdentityRepository from '../../infrastructure/repositories/organization-member-identity-repository.js'; import * as organizationTagRepository from '../../infrastructure/repositories/organization-tag-repository.js'; -import * as participantResultRepository from '../../infrastructure/repositories/participant-result-repository.js'; import { participantResultsSharedRepository } from '../../infrastructure/repositories/participant-results-shared-repository.js'; import * as studentRepository from '../../infrastructure/repositories/student-repository.js'; import * as targetProfileForUpdateRepository from '../../infrastructure/repositories/target-profile-for-update-repository.js'; @@ -291,7 +290,6 @@ const dependencies = { organizationRepository, organizationTagRepository, organizationValidator, - participantResultRepository, participantResultsSharedRepository, passwordGenerator, passwordValidator, diff --git a/api/lib/domain/usecases/get-user-campaign-assessment-result.js b/api/src/prescription/campaign-participation/domain/usecases/get-user-campaign-assessment-result.js similarity index 73% rename from api/lib/domain/usecases/get-user-campaign-assessment-result.js rename to api/src/prescription/campaign-participation/domain/usecases/get-user-campaign-assessment-result.js index c7dec1359c0..901b7db6f30 100644 --- a/api/lib/domain/usecases/get-user-campaign-assessment-result.js +++ b/api/src/prescription/campaign-participation/domain/usecases/get-user-campaign-assessment-result.js @@ -1,11 +1,5 @@ -// eslint-disable-next-line @eslint-community/eslint-comments/disable-enable-pair -/* eslint-disable import/no-restricted-paths */ -import * as defaultCompareStageAndAcquiredStagesService from '../../../src/evaluation/domain/services/stages/stage-and-stage-acquisition-comparison-service.js'; -import * as defaultStageAcquisitionRepository from '../../../src/evaluation/infrastructure/repositories/stage-acquisition-repository.js'; -import * as defaultStageRepository from '../../../src/evaluation/infrastructure/repositories/stage-repository.js'; -import { NoCampaignParticipationForUserAndCampaign, NotFoundError } from '../../../src/shared/domain/errors.js'; -import { CampaignParticipationStatuses } from '../../../src/shared/domain/models/index.js'; -import * as defaultParticipantResultRepository from '../../infrastructure/repositories/participant-result-repository.js'; +import { NoCampaignParticipationForUserAndCampaign, NotFoundError } from '../../../../shared/domain/errors.js'; +import { CampaignParticipationStatuses } from '../../../../shared/domain/models/index.js'; const getUserCampaignAssessmentResult = async function ({ userId, @@ -14,10 +8,10 @@ const getUserCampaignAssessmentResult = async function ({ badgeRepository, knowledgeElementRepository, badgeForCalculationRepository, - stageRepository = defaultStageRepository, - stageAcquisitionRepository = defaultStageAcquisitionRepository, - participantResultRepository = defaultParticipantResultRepository, - compareStagesAndAcquiredStages = defaultCompareStageAndAcquiredStagesService, + participantResultRepository, + stageRepository, + stageAcquisitionRepository, + compareStagesAndAcquiredStages, }) { const { SHARED, TO_SHARE } = CampaignParticipationStatuses; const campaignParticipationStatus = await participantResultRepository.getCampaignParticipationStatus({ diff --git a/api/src/prescription/campaign-participation/domain/usecases/index.js b/api/src/prescription/campaign-participation/domain/usecases/index.js index 2274e64adc4..9fbb259c2f9 100644 --- a/api/src/prescription/campaign-participation/domain/usecases/index.js +++ b/api/src/prescription/campaign-participation/domain/usecases/index.js @@ -2,14 +2,18 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as badgeAcquisitionRepository from '../../../../../lib/infrastructure/repositories/badge-acquisition-repository.js'; +import * as badgeForCalculationRepository from '../../../../../lib/infrastructure/repositories/badge-for-calculation-repository.js'; import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js'; import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js'; import * as learningContentRepository from '../../../../../lib/infrastructure/repositories/learning-content-repository.js'; -// TODO : use an API for this import import * as organizationLearnerRepository from '../../../../../lib/infrastructure/repositories/organization-learner-repository.js'; import * as stageCollectionRepository from '../../../../../lib/infrastructure/repositories/user-campaign-results/stage-collection-repository.js'; +import * as badgeRepository from '../../../../../src/evaluation/infrastructure/repositories/badge-repository.js'; import * as tutorialRepository from '../../../../devcomp/infrastructure/repositories/tutorial-repository.js'; +import * as compareStagesAndAcquiredStages from '../../../../evaluation/domain/services/stages/stage-and-stage-acquisition-comparison-service.js'; import * as competenceEvaluationRepository from '../../../../evaluation/infrastructure/repositories/competence-evaluation-repository.js'; +import * as stageAcquisitionRepository from '../../../../evaluation/infrastructure/repositories/stage-acquisition-repository.js'; +import * as stageRepository from '../../../../evaluation/infrastructure/repositories/stage-repository.js'; import * as areaRepository from '../../../../shared/infrastructure/repositories/area-repository.js'; import * as assessmentRepository from '../../../../shared/infrastructure/repositories/assessment-repository.js'; import * as competenceRepository from '../../../../shared/infrastructure/repositories/competence-repository.js'; @@ -20,19 +24,52 @@ import * as campaignAssessmentParticipationRepository from '../../infrastructure import * as campaignAssessmentParticipationResultRepository from '../../infrastructure/repositories/campaign-assessment-participation-result-repository.js'; import * as campaignParticipationRepository from '../../infrastructure/repositories/campaign-participation-repository.js'; import * as campaignProfileRepository from '../../infrastructure/repositories/campaign-profile-repository.js'; -import { repositories as campaignRepositories } from '../../infrastructure/repositories/index.js'; +import { repositories as campaignRepositories } from '../../infrastructure/repositories/index.js'; // needed to includes organizationFeatureAPI from another BC import { participationResultCalculationJobRepository } from '../../infrastructure/repositories/jobs/participation-result-calculation-job-repository.js'; import { participationSharedJobRepository } from '../../infrastructure/repositories/jobs/participation-shared-job-repository.js'; import { participationStartedJobRepository } from '../../infrastructure/repositories/jobs/participation-started-job-repository.js'; +import * as participantResultRepository from '../../infrastructure/repositories/participant-result-repository.js'; import * as participationsForCampaignManagementRepository from '../../infrastructure/repositories/participations-for-campaign-management-repository.js'; import * as participationsForUserManagementRepository from '../../infrastructure/repositories/participations-for-user-management-repository.js'; import * as poleEmploiSendingRepository from '../../infrastructure/repositories/pole-emploi-sending-repository.js'; +/** + * @typedef { import ('../../../../shared/infrastructure/repositories/area-repository.js')} AreaRepository + * @typedef { import ('../../../../shared/infrastructure/repositories/assessment-repository.js')} AssessmentRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/badge-acquisition-repository.js')} BadgeAcquisitionRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/badge-for-calculation-repository.js')} BadgeForCalculationRepository + * @typedef { import ('../../../../../src/evaluation/infrastructure/repositories/badge-repository.js')} BadgeRepository + * @typedef { import ('../../infrastructure/repositories/campaign-analysis-repository.js')} CampaignAnalysisRepository + * @typedef { import ('../../infrastructure/repositories/campaign-assessment-participation-repository.js')} CampaignAssessmentParticipationRepository + * @typedef { import ('../../infrastructure/repositories/campaign-assessment-participation-result-repository.js')} CampaignAssessmentParticipationResultRepository + * @typedef { import ('../../infrastructure/repositories/index.js')} CampaignParticipantRepository + * @typedef { import ('../../infrastructure/repositories/campaign-participation-repository.js')} CampaignParticipationRepository + * @typedef { import ('../../infrastructure/repositories/campaign-profile-repository.js')} CampaignProfileRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/campaign-repository.js')} CampaignRepository + * @typedef { import ('../../../../evaluation/domain/services/stages/stage-and-stage-acquisition-comparison-service.js')} CompareStagesAndAcquiredStages + * @typedef { import ('../../../../evaluation/infrastructure/repositories/competence-evaluation-repository.js')} CompetenceEvaluationRepository + * @typedef { import ('../../../../shared/infrastructure/repositories/competence-repository.js')} CompetenceRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/knowledge-element-repository.js')} KnowledgeElementRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/learning-content-repository.js')} LearningContentRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/organization-learner-repository.js')} OrganizationLearnerRepository + * @typedef { import ('../../infrastructure/repositories/participant-result-repository.js')} ParticipantResultRepository + * @typedef { import ('../../infrastructure/repositories/jobs/participation-result-calculation-job-repository.js')} ParticipationResultCalculationJobRepository + * @typedef { import ('../../infrastructure/repositories/participations-for-campaign-management-repository.js')} ParticipationsForCampaignManagementRepository + * @typedef { import ('../../infrastructure/repositories/participations-for-user-management-repository.js')} ParticipationsForUserManagementRepository + * @typedef { import ('../../infrastructure/repositories/jobs/participation-shared-job-repository.js')} ParticipationSharedJobRepository + * @typedef { import ('../../infrastructure/repositories/jobs/participation-started-job-repository.js')} ParticipationStartedJobRepository + * @typedef { import ('../../infrastructure/repositories/pole-emploi-sending-repository.js')} PoleEmploiSendingRepository + * @typedef { import ('../../../../evaluation/infrastructure/repositories/stage-acquisition-repository.js')} StageAcquisitionRepository + * @typedef { import ('../../../../../lib/infrastructure/repositories/user-campaign-results/stage-collection-repository.js')} StageCollectionRepository + * @typedef { import ('../../../../evaluation/infrastructure/repositories/stage-repository.js')} StageRepository + * @typedef { import ('../../../../devcomp/infrastructure/repositories/tutorial-repository.js')} TutorialRepository + */ const dependencies = { areaRepository, - competenceRepository, assessmentRepository, badgeAcquisitionRepository, + badgeForCalculationRepository, + badgeRepository, campaignAnalysisRepository, campaignAssessmentParticipationRepository, campaignAssessmentParticipationResultRepository, @@ -40,18 +77,23 @@ const dependencies = { campaignParticipationRepository, campaignProfileRepository, campaignRepository, + compareStagesAndAcquiredStages, competenceEvaluationRepository, + competenceRepository, knowledgeElementRepository, learningContentRepository, + organizationLearnerRepository, + participantResultRepository, participationResultCalculationJobRepository, participationsForCampaignManagementRepository, participationsForUserManagementRepository, participationSharedJobRepository, participationStartedJobRepository, poleEmploiSendingRepository, + stageAcquisitionRepository, stageCollectionRepository, + stageRepository, tutorialRepository, - organizationLearnerRepository, }; const path = dirname(fileURLToPath(import.meta.url)); diff --git a/api/tests/unit/domain/usecases/get-user-campaign-assessment-result_test.js b/api/tests/prescription/campaign-participation/unit/domain/usecases/get-user-campaign-assessment-result_test.js similarity index 92% rename from api/tests/unit/domain/usecases/get-user-campaign-assessment-result_test.js rename to api/tests/prescription/campaign-participation/unit/domain/usecases/get-user-campaign-assessment-result_test.js index 409f02eb444..f67a15b8ea8 100644 --- a/api/tests/unit/domain/usecases/get-user-campaign-assessment-result_test.js +++ b/api/tests/prescription/campaign-participation/unit/domain/usecases/get-user-campaign-assessment-result_test.js @@ -1,7 +1,10 @@ -import { getUserCampaignAssessmentResult } from '../../../../lib/domain/usecases/get-user-campaign-assessment-result.js'; -import { NoCampaignParticipationForUserAndCampaign, NotFoundError } from '../../../../src/shared/domain/errors.js'; -import { CampaignParticipationStatuses } from '../../../../src/shared/domain/models/index.js'; -import { catchErr, domainBuilder, expect, sinon } from '../../../test-helper.js'; +import { getUserCampaignAssessmentResult } from '../../../../../../src/prescription/campaign-participation/domain/usecases/get-user-campaign-assessment-result.js'; +import { + NoCampaignParticipationForUserAndCampaign, + NotFoundError, +} from '../../../../../../src/shared/domain/errors.js'; +import { CampaignParticipationStatuses } from '../../../../../../src/shared/domain/models/index.js'; +import { catchErr, domainBuilder, expect, sinon } from '../../../../../test-helper.js'; describe('Unit | UseCase | get-user-campaign-assessment-result', function () { const locale = 'locale', From c0a8f973a64e8d2c545494e8071a4d840673ad7e Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:05:29 +0100 Subject: [PATCH 3/3] tech(api): migrate particpant result controller/route to its bounded-context --- api/lib/application/users/index.js | 25 -- api/lib/application/users/user-controller.js | 23 -- .../campaign-participation-controller.js | 23 ++ .../campaign-participation-route.js | 25 ++ .../participant-result-repository.js | 6 +- .../jsonapi/participant-result-serializer.js | 0 ...et-user-campaign-assessment-result_test.js | 382 ------------------ .../application/users/user-controller_test.js | 68 ---- ...s => campaign-participation-route_test.js} | 366 +++++++++++++++++ .../campaign-participation-route_test.js | 43 +- .../campaign-participation-controller_test.js | 38 ++ .../campaign-participation-route_test.js | 52 +++ .../participant-result-serializer_test.js | 8 +- .../unit/application/users/index_test.js | 52 --- 14 files changed, 553 insertions(+), 558 deletions(-) rename api/{lib => src/prescription/campaign-participation}/infrastructure/serializers/jsonapi/participant-result-serializer.js (100%) delete mode 100644 api/tests/acceptance/application/users/get-user-campaign-assessment-result_test.js rename api/tests/prescription/campaign-participation/acceptance/application/{campaign-participation-controller_test.js => campaign-participation-route_test.js} (56%) rename api/tests/{ => prescription/campaign-participation}/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js (95%) diff --git a/api/lib/application/users/index.js b/api/lib/application/users/index.js index db92a1fc247..43f7137cdc1 100644 --- a/api/lib/application/users/index.js +++ b/api/lib/application/users/index.js @@ -274,31 +274,6 @@ const register = async function (server) { tags: ['api'], }, }, - { - method: 'GET', - path: '/api/users/{userId}/campaigns/{campaignId}/assessment-result', - config: { - validate: { - params: Joi.object({ - userId: identifiersType.userId, - campaignId: identifiersType.campaignId, - }), - }, - pre: [ - { - method: securityPreHandlers.checkRequestedUserIsAuthenticatedUser, - assign: 'requestedUserIsAuthenticatedUser', - }, - ], - handler: userController.getUserCampaignAssessmentResult, - notes: [ - '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + - '- Récupération des résultats d’un parcours pour un utilisateur (**userId**) et pour la campagne d’évaluation donnée (**campaignId**)\n' + - '- L’id demandé doit correspondre à celui de l’utilisateur authentifié', - ], - tags: ['api', 'user', 'campaign'], - }, - }, { method: 'GET', path: '/api/users/{userId}/campaigns/{campaignId}/campaign-participations', diff --git a/api/lib/application/users/user-controller.js b/api/lib/application/users/user-controller.js index f4adb3ff3cd..ab9639f04c6 100644 --- a/api/lib/application/users/user-controller.js +++ b/api/lib/application/users/user-controller.js @@ -10,7 +10,6 @@ import { usecases } from '../../domain/usecases/index.js'; import { DomainTransaction } from '../../infrastructure/DomainTransaction.js'; import * as campaignParticipationOverviewSerializer from '../../infrastructure/serializers/jsonapi/campaign-participation-overview-serializer.js'; import * as certificationCenterMembershipSerializer from '../../infrastructure/serializers/jsonapi/certification-center-membership-serializer.js'; -import * as participantResultSerializer from '../../infrastructure/serializers/jsonapi/participant-result-serializer.js'; import * as userAnonymizedDetailsForAdminSerializer from '../../infrastructure/serializers/jsonapi/user-anonymized-details-for-admin-serializer.js'; import * as userOrganizationForAdminSerializer from '../../infrastructure/serializers/jsonapi/user-organization-for-admin-serializer.js'; @@ -110,27 +109,6 @@ const getUserCampaignParticipationToCampaign = function ( .then((campaignParticipation) => dependencies.campaignParticipationSerializer.serialize(campaignParticipation)); }; -const getUserCampaignAssessmentResult = async function ( - request, - h, - dependencies = { - participantResultSerializer, - requestResponseUtils, - }, -) { - const authenticatedUserId = request.auth.credentials.userId; - const campaignId = request.params.campaignId; - const locale = dependencies.requestResponseUtils.extractLocaleFromRequest(request); - - const campaignAssessmentResult = await usecases.getUserCampaignAssessmentResult({ - userId: authenticatedUserId, - campaignId, - locale, - }); - - return dependencies.participantResultSerializer.serialize(campaignAssessmentResult); -}; - const anonymizeUser = async function (request, h, dependencies = { userAnonymizedDetailsForAdminSerializer }) { const userToAnonymizeId = request.params.id; const adminMemberId = request.auth.credentials.userId; @@ -217,7 +195,6 @@ const userController = { findUserOrganizationsForAdmin, getCampaignParticipationOverviews, getCampaignParticipations, - getUserCampaignAssessmentResult, getUserCampaignParticipationToCampaign, getUserDetailsForAdmin, reassignAuthenticationMethods, diff --git a/api/src/prescription/campaign-participation/application/campaign-participation-controller.js b/api/src/prescription/campaign-participation/application/campaign-participation-controller.js index c03672b84c6..a93cafc334c 100644 --- a/api/src/prescription/campaign-participation/application/campaign-participation-controller.js +++ b/api/src/prescription/campaign-participation/application/campaign-participation-controller.js @@ -6,6 +6,7 @@ import * as campaignAnalysisSerializer from '../infrastructure/serializers/jsona import * as campaignAssessmentParticipationResultSerializer from '../infrastructure/serializers/jsonapi/campaign-assessment-participation-result-serializer.js'; import * as campaignAssessmentParticipationSerializer from '../infrastructure/serializers/jsonapi/campaign-assessment-participation-serializer.js'; import * as campaignProfileSerializer from '../infrastructure/serializers/jsonapi/campaign-profile-serializer.js'; +import * as participantResultSerializer from '../infrastructure/serializers/jsonapi/participant-result-serializer.js'; import * as participationForCampaignManagementSerializer from '../infrastructure/serializers/jsonapi/participation-for-campaign-management-serializer.js'; const findPaginatedParticipationsForCampaignManagement = async function (request) { @@ -116,8 +117,30 @@ const getCampaignParticipationsForOrganizationLearner = async function ( return dependencies.availableCampaignParticipationsSerializer.serialize(availableCampaignParticipations); }; +const getUserCampaignAssessmentResult = async function ( + request, + _, + dependencies = { + participantResultSerializer, + extractLocaleFromRequest, + }, +) { + const authenticatedUserId = request.auth.credentials.userId; + const campaignId = request.params.campaignId; + const locale = dependencies.extractLocaleFromRequest(request); + + const campaignAssessmentResult = await usecases.getUserCampaignAssessmentResult({ + userId: authenticatedUserId, + campaignId, + locale, + }); + + return dependencies.participantResultSerializer.serialize(campaignAssessmentResult); +}; + const campaignParticipationController = { findPaginatedParticipationsForCampaignManagement, + getUserCampaignAssessmentResult, getAnalysis, getCampaignProfile, getCampaignAssessmentParticipation, diff --git a/api/src/prescription/campaign-participation/application/campaign-participation-route.js b/api/src/prescription/campaign-participation/application/campaign-participation-route.js index e260757100a..ded0516c093 100644 --- a/api/src/prescription/campaign-participation/application/campaign-participation-route.js +++ b/api/src/prescription/campaign-participation/application/campaign-participation-route.js @@ -206,6 +206,31 @@ const register = async function (server) { tags: ['api', 'campaign-participations'], }, }, + { + method: 'GET', + path: '/api/users/{userId}/campaigns/{campaignId}/assessment-result', + config: { + validate: { + params: Joi.object({ + userId: identifiersType.userId, + campaignId: identifiersType.campaignId, + }), + }, + pre: [ + { + method: securityPreHandlers.checkRequestedUserIsAuthenticatedUser, + assign: 'requestedUserIsAuthenticatedUser', + }, + ], + handler: campaignParticipationController.getUserCampaignAssessmentResult, + notes: [ + '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + + '- Récupération des résultats d’un parcours pour un utilisateur (**userId**) et pour la campagne d’évaluation donnée (**campaignId**)\n' + + '- L’id demandé doit correspondre à celui de l’utilisateur authentifié', + ], + tags: ['api', 'user', 'campaign'], + }, + }, ]); }; diff --git a/api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js b/api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js index ca488fb99dd..aaecfbb8d32 100644 --- a/api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js +++ b/api/src/prescription/campaign-participation/infrastructure/repositories/participant-result-repository.js @@ -1,6 +1,9 @@ import _ from 'lodash'; import { knex } from '../../../../../db/knex-database-connection.js'; +import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js'; +import * as flashAssessmentResultRepository from '../../../../../lib/infrastructure/repositories/flash-assessment-result-repository.js'; +import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js'; import * as flash from '../../../../certification/flash-certification/domain/services/algorithm-methods/flash.js'; import * as dataFetcher from '../../../../evaluation/domain/services/algorithm-methods/data-fetcher.js'; import { convertLevelStagesIntoThresholds } from '../../../../evaluation/domain/services/stages/convert-level-stages-into-thresholds-service.js'; @@ -12,9 +15,6 @@ import * as areaRepository from '../../../../shared/infrastructure/repositories/ import * as challengeRepository from '../../../../shared/infrastructure/repositories/challenge-repository.js'; import * as competenceRepository from '../../../../shared/infrastructure/repositories/competence-repository.js'; import * as skillRepository from '../../../../shared/infrastructure/repositories/skill-repository.js'; -import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js'; -import * as flashAssessmentResultRepository from '../../../../../lib/infrastructure/repositories/flash-assessment-result-repository.js'; -import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js'; /** * diff --git a/api/lib/infrastructure/serializers/jsonapi/participant-result-serializer.js b/api/src/prescription/campaign-participation/infrastructure/serializers/jsonapi/participant-result-serializer.js similarity index 100% rename from api/lib/infrastructure/serializers/jsonapi/participant-result-serializer.js rename to api/src/prescription/campaign-participation/infrastructure/serializers/jsonapi/participant-result-serializer.js diff --git a/api/tests/acceptance/application/users/get-user-campaign-assessment-result_test.js b/api/tests/acceptance/application/users/get-user-campaign-assessment-result_test.js deleted file mode 100644 index 1c8b330c4f3..00000000000 --- a/api/tests/acceptance/application/users/get-user-campaign-assessment-result_test.js +++ /dev/null @@ -1,382 +0,0 @@ -import _ from 'lodash'; - -import { SCOPES } from '../../../../src/shared/domain/models/BadgeDetails.js'; -import { - createServer, - databaseBuilder, - expect, - generateValidRequestAuthorizationHeader, - learningContentBuilder, - mockLearningContent, -} from '../../../test-helper.js'; - -describe('Acceptance | API | Campaign Assessment Result', function () { - const JAFFA_COLOR = 'jaffa'; - const EMERALD_COLOR = 'emerald'; - const WILD_STRAWBERRY_COLOR = 'wild-strawberry'; - - let user, campaign, assessment, campaignParticipation, targetProfile, campaignSkills; - - let server, badge1, badge2, stage; - - beforeEach(async function () { - server = await createServer(); - - const oldDate = new Date('2018-02-03'); - const recentDate = new Date('2018-05-06'); - const futureDate = new Date('2018-07-10'); - const skillIds = [ - 'recSkill1', - 'recSkill2', - 'recSkill3', - 'recSkill4', - 'recSkill5', - 'recSkill6', - 'recSkill7', - 'recSkill8', - ]; - - user = databaseBuilder.factory.buildUser(); - targetProfile = databaseBuilder.factory.buildTargetProfile(); - campaign = databaseBuilder.factory.buildCampaign({ - targetProfileId: targetProfile.id, - }); - campaignSkills = _.times(8, (index) => { - return databaseBuilder.factory.buildCampaignSkill({ - campaignId: campaign.id, - skillId: skillIds[index], - }); - }); - campaignParticipation = databaseBuilder.factory.buildCampaignParticipation({ - campaignId: campaign.id, - userId: user.id, - sharedAt: recentDate, - masteryRate: 0.38, - }); - - assessment = databaseBuilder.factory.buildAssessment({ - campaignParticipationId: campaignParticipation.id, - userId: user.id, - type: 'CAMPAIGN', - state: 'completed', - }); - - badge1 = databaseBuilder.factory.buildBadge({ - id: 1, - altMessage: 'Low threshold badge', - imageUrl: '/img/banana.svg', - message: 'You won a badge that had a criterion threshold set at 0', - title: 'Badge 1', - key: 'PIX_BADGE_1', - targetProfileId: targetProfile.id, - isAlwaysVisible: true, - }); - - badge2 = databaseBuilder.factory.buildBadge({ - id: 2, - altMessage: 'High threshold badge', - imageUrl: '/img/banana.svg', - message: 'You won a badge that had a criterion threshold set at 90', - title: 'Badge 2', - key: 'PIX_BADGE_2', - targetProfileId: targetProfile.id, - isAlwaysVisible: true, - }); - - databaseBuilder.factory.buildBadgeCriterion({ - badgeId: 1, - scope: SCOPES.CAMPAIGN_PARTICIPATION, - threshold: 0, - }); - - databaseBuilder.factory.buildBadgeCriterion({ - badgeId: 2, - scope: SCOPES.CAMPAIGN_PARTICIPATION, - threshold: 90, - }); - - databaseBuilder.factory.buildBadgeAcquisition({ - userId: user.id, - campaignParticipationId: campaignParticipation.id, - badgeId: badge1.id, - }); - - stage = databaseBuilder.factory.buildStage({ - id: 1, - message: 'Tu as le palier 1', - title: 'palier 1', - threshold: 20, - targetProfileId: targetProfile.id, - }); - - databaseBuilder.factory.buildStage({ - id: 2, - message: 'Tu as le palier 2', - title: 'palier 2', - threshold: 50, - targetProfileId: targetProfile.id, - }); - - databaseBuilder.factory.buildStageAcquisition({ - stageId: stage.id, - userId: user.id, - campaignParticipationId: campaignParticipation.id, - }); - - campaignSkills.slice(2).forEach((campaignSkill, index) => { - databaseBuilder.factory.buildKnowledgeElement({ - userId: user.id, - assessmentId: assessment.id, - skillId: campaignSkill.skillId, - status: index < 3 ? 'validated' : 'invalidated', - createdAt: index < 5 ? oldDate : futureDate, - }); - }); - - databaseBuilder.factory.buildKnowledgeElement({ - userId: user.id, - assessmentId: assessment.id, - skillId: 'otherSkillId', - createdAt: oldDate, - }); - - const learningContent = [ - { - id: 'recArea1', - title_i18n: { fr: 'DomaineNom1' }, - color: JAFFA_COLOR, - competences: [ - { - id: 1, - name_i18n: { fr: 'Agir collectivement' }, - description_i18n: { fr: 'Sauver le monde' }, - index: '1.2', - tubes: [{ id: 'recTube1', skills: [{ id: 'recSkill1' }] }], - }, - ], - }, - { - id: 'recArea2', - title_i18n: { fr: 'DomaineNom2' }, - color: EMERALD_COLOR, - competences: [ - { - id: 2, - name_i18n: { fr: 'Nécessité de la pensée radicale' }, - description_i18n: { fr: 'Sauver le monde' }, - index: '2.1', - tubes: [ - { - id: 'recTube2', - skills: [{ id: 'recSkill2' }, { id: 'recSkill3' }, { id: 'recSkill4' }], - }, - ], - }, - { - id: 3, - name_i18n: { fr: 'Changer efficacement le monde' }, - description_i18n: { fr: 'Sauver le monde' }, - index: '2.2', - tubes: [ - { - id: 'recTube3', - skills: [{ id: 'recSkill5' }, { id: 'recSkill6' }, { id: 'recSkill7' }, { id: 'recSkill8' }], - }, - ], - }, - ], - }, - { - id: 'recArea3', - title_i18n: { fr: 'DomaineNom3' }, - color: WILD_STRAWBERRY_COLOR, - competences: [ - { - id: 4, - name_i18n: { fr: 'Oser la paresse' }, - description_i18n: { fr: 'Sauver le monde' }, - index: '4.3', - tubes: [{ id: 'recTube0', skills: [{ id: 'notIncludedSkillId' }] }], - }, - ], - }, - ]; - const learningContentObjects = learningContentBuilder.fromAreas(learningContent); - mockLearningContent(learningContentObjects); - await databaseBuilder.commit(); - }); - - describe('GET /api/users/{userId}/campaigns/{campaignId}/assessment-result', function () { - let options; - - beforeEach(async function () { - options = { - method: 'GET', - url: `/api/users/${user.id}/campaigns/${campaign.id}/assessment-result`, - headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, - }; - }); - - it('should return the campaign assessment result', async function () { - // given - const expectedResponse = { - data: { - type: 'campaign-participation-results', - id: campaignParticipation.id.toString(), - attributes: { - 'mastery-rate': 0.38, - 'total-skills-count': 8, - 'tested-skills-count': 5, - 'validated-skills-count': 3, - 'is-completed': true, - 'is-shared': true, - 'can-retry': false, - 'can-reset': false, - 'can-improve': false, - 'is-disabled': false, - 'participant-external-id': 'participantExternalId', - }, - relationships: { - 'campaign-participation-badges': { - data: [ - { - id: `${badge1.id}`, - type: 'campaignParticipationBadges', - }, - { - id: `${badge2.id}`, - type: 'campaignParticipationBadges', - }, - ], - }, - 'competence-results': { - data: [ - { - id: '1', - type: 'competenceResults', - }, - { - id: '2', - type: 'competenceResults', - }, - { - id: '3', - type: 'competenceResults', - }, - ], - }, - 'reached-stage': { - data: { - id: `${stage.id}`, - type: 'reached-stages', - }, - }, - }, - }, - included: [ - { - attributes: { - 'acquisition-percentage': 100, - 'alt-message': 'Low threshold badge', - 'image-url': '/img/banana.svg', - 'is-acquired': true, - 'is-always-visible': true, - 'is-certifiable': false, - 'is-valid': true, - key: 'PIX_BADGE_1', - title: 'Badge 1', - message: 'You won a badge that had a criterion threshold set at 0', - }, - id: '1', - type: 'campaignParticipationBadges', - }, - { - attributes: { - 'acquisition-percentage': 42, - 'alt-message': 'High threshold badge', - 'image-url': '/img/banana.svg', - 'is-acquired': false, - 'is-always-visible': true, - 'is-certifiable': false, - 'is-valid': false, - key: 'PIX_BADGE_2', - title: 'Badge 2', - message: 'You won a badge that had a criterion threshold set at 90', - }, - id: '2', - type: 'campaignParticipationBadges', - }, - { - type: 'competenceResults', - id: '1', - attributes: { - name: 'Agir collectivement', - index: '1.2', - description: 'Sauver le monde', - 'mastery-percentage': 0, - 'total-skills-count': 1, - 'tested-skills-count': 0, - 'validated-skills-count': 0, - 'area-color': JAFFA_COLOR, - 'area-title': 'DomaineNom1', - 'reached-stage': 0, - 'flash-pix-score': undefined, - }, - }, - { - type: 'competenceResults', - id: '2', - attributes: { - name: 'Nécessité de la pensée radicale', - index: '2.1', - description: 'Sauver le monde', - 'mastery-percentage': 67, - 'total-skills-count': 3, - 'tested-skills-count': 2, - 'validated-skills-count': 2, - 'area-color': EMERALD_COLOR, - 'area-title': 'DomaineNom2', - 'reached-stage': 2, - 'flash-pix-score': undefined, - }, - }, - { - type: 'competenceResults', - id: '3', - attributes: { - name: 'Changer efficacement le monde', - index: '2.2', - description: 'Sauver le monde', - 'mastery-percentage': 25, - 'total-skills-count': 4, - 'tested-skills-count': 3, - 'validated-skills-count': 1, - 'area-color': EMERALD_COLOR, - 'area-title': 'DomaineNom2', - 'reached-stage': 1, - 'flash-pix-score': undefined, - }, - }, - { - attributes: { - message: 'Tu as le palier 1', - title: 'palier 1', - threshold: 20, - 'reached-stage': 1, - 'total-stage': 2, - }, - id: stage.id.toString(), - type: 'reached-stages', - }, - ], - }; - - // when - const response = await server.inject(options); - - // then - expect(response.result).to.deep.equal(expectedResponse); - expect(response.statusCode).to.equal(200); - }); - }); -}); diff --git a/api/tests/integration/application/users/user-controller_test.js b/api/tests/integration/application/users/user-controller_test.js index c7066a00a26..5d601173fbc 100644 --- a/api/tests/integration/application/users/user-controller_test.js +++ b/api/tests/integration/application/users/user-controller_test.js @@ -2,7 +2,6 @@ import * as moduleUnderTest from '../../../../lib/application/users/index.js'; import { usecases } from '../../../../lib/domain/usecases/index.js'; import { securityPreHandlers } from '../../../../src/shared/application/security-pre-handlers.js'; import { UserNotAuthorizedToRemoveAuthenticationMethod } from '../../../../src/shared/domain/errors.js'; -import { AssessmentResult } from '../../../../src/shared/domain/read-models/participant-results/AssessmentResult.js'; import { domainBuilder, expect, HttpTestServer, sinon } from '../../../test-helper.js'; describe('Integration | Application | Users | user-controller', function () { @@ -15,7 +14,6 @@ describe('Integration | Application | Users | user-controller', function () { sandbox.stub(securityPreHandlers, 'hasAtLeastOneAccessOf'); sandbox.stub(usecases, 'getUserCampaignParticipationToCampaign'); - sandbox.stub(usecases, 'getUserCampaignAssessmentResult'); sandbox.stub(usecases, 'removeAuthenticationMethod'); httpTestServer = new HttpTestServer(); @@ -72,72 +70,6 @@ describe('Integration | Application | Users | user-controller', function () { }); }); - describe('#getUserCampaignAssessmentResult', function () { - const auth = { credentials: {}, strategy: {} }; - - context('Success cases', function () { - let campaignAssessmentResult; - - beforeEach(function () { - securityPreHandlers.checkRequestedUserIsAuthenticatedUser.returns(true); - auth.credentials.userId = '1234'; - campaignAssessmentResult = new AssessmentResult({ - participationResults: { knowledgeElements: [] }, - competences: [], - badgeResultsDTO: [], - stageCollection: domainBuilder.buildStageCollectionForUserCampaignResults({ campaignId: 5678, stages: [] }), - isCampaignMultipleSendings: false, - }); - }); - - it('should return an HTTP response with status code 200', async function () { - // given - usecases.getUserCampaignAssessmentResult - .withArgs({ userId: '1234', campaignId: 5678, locale: 'fr-fr' }) - .resolves(campaignAssessmentResult); - - // when - const response = await httpTestServer.request( - 'GET', - '/api/users/1234/campaigns/5678/assessment-result', - null, - auth, - ); - - // then - expect(response.statusCode).to.equal(200); - }); - }); - - context('Error cases', function () { - it('should return a 403 HTTP response', async function () { - // given - securityPreHandlers.checkRequestedUserIsAuthenticatedUser.callsFake((request, h) => { - return Promise.resolve(h.response().code(403).takeover()); - }); - - // when - const response = await httpTestServer.request('GET', '/api/users/1234/campaigns/5678/assessment-result'); - - // then - expect(response.statusCode).to.equal(403); - }); - - it('should return a 401 HTTP response', async function () { - // given - securityPreHandlers.checkRequestedUserIsAuthenticatedUser.callsFake((request, h) => { - return Promise.resolve(h.response().code(401).takeover()); - }); - - // when - const response = await httpTestServer.request('GET', '/api/users/1234/campaigns/5678/assessment-result'); - - // then - expect(response.statusCode).to.equal(401); - }); - }); - }); - describe('#removeAuthenticationMethod', function () { const method = 'POST'; const url = '/api/admin/users/1/remove-authentication'; diff --git a/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-controller_test.js b/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js similarity index 56% rename from api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-controller_test.js rename to api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js index 088a499ac19..e53f7574f00 100644 --- a/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-controller_test.js +++ b/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js @@ -1,3 +1,6 @@ +import times from 'lodash/times.js'; + +import { SCOPES } from '../../../../../src/shared/domain/models/BadgeDetails.js'; import { Membership } from '../../../../../src/shared/domain/models/index.js'; import { createServer, @@ -412,4 +415,367 @@ describe('Acceptance | API | Campaign Participations', function () { expect(response.result.data[0].id).to.equal(campaignParticipation.id.toString()); }); }); + + describe('GET /api/users/{userId}/campaigns/{campaignId}/assessment-result', function () { + const JAFFA_COLOR = 'jaffa'; + const EMERALD_COLOR = 'emerald'; + const WILD_STRAWBERRY_COLOR = 'wild-strawberry'; + + let user, campaign, assessment, campaignParticipation, targetProfile, campaignSkills; + + let server, badge1, badge2, stage; + + beforeEach(async function () { + server = await createServer(); + + const oldDate = new Date('2018-02-03'); + const recentDate = new Date('2018-05-06'); + const futureDate = new Date('2018-07-10'); + const skillIds = [ + 'recSkill1', + 'recSkill2', + 'recSkill3', + 'recSkill4', + 'recSkill5', + 'recSkill6', + 'recSkill7', + 'recSkill8', + ]; + + user = databaseBuilder.factory.buildUser(); + targetProfile = databaseBuilder.factory.buildTargetProfile(); + campaign = databaseBuilder.factory.buildCampaign({ + targetProfileId: targetProfile.id, + }); + campaignSkills = times(8, (index) => { + return databaseBuilder.factory.buildCampaignSkill({ + campaignId: campaign.id, + skillId: skillIds[index], + }); + }); + campaignParticipation = databaseBuilder.factory.buildCampaignParticipation({ + campaignId: campaign.id, + userId: user.id, + sharedAt: recentDate, + masteryRate: 0.38, + }); + + assessment = databaseBuilder.factory.buildAssessment({ + campaignParticipationId: campaignParticipation.id, + userId: user.id, + type: 'CAMPAIGN', + state: 'completed', + }); + + badge1 = databaseBuilder.factory.buildBadge({ + id: 1, + altMessage: 'Low threshold badge', + imageUrl: '/img/banana.svg', + message: 'You won a badge that had a criterion threshold set at 0', + title: 'Badge 1', + key: 'PIX_BADGE_1', + targetProfileId: targetProfile.id, + isAlwaysVisible: true, + }); + + badge2 = databaseBuilder.factory.buildBadge({ + id: 2, + altMessage: 'High threshold badge', + imageUrl: '/img/banana.svg', + message: 'You won a badge that had a criterion threshold set at 90', + title: 'Badge 2', + key: 'PIX_BADGE_2', + targetProfileId: targetProfile.id, + isAlwaysVisible: true, + }); + + databaseBuilder.factory.buildBadgeCriterion({ + badgeId: 1, + scope: SCOPES.CAMPAIGN_PARTICIPATION, + threshold: 0, + }); + + databaseBuilder.factory.buildBadgeCriterion({ + badgeId: 2, + scope: SCOPES.CAMPAIGN_PARTICIPATION, + threshold: 90, + }); + + databaseBuilder.factory.buildBadgeAcquisition({ + userId: user.id, + campaignParticipationId: campaignParticipation.id, + badgeId: badge1.id, + }); + + stage = databaseBuilder.factory.buildStage({ + id: 1, + message: 'Tu as le palier 1', + title: 'palier 1', + threshold: 20, + targetProfileId: targetProfile.id, + }); + + databaseBuilder.factory.buildStage({ + id: 2, + message: 'Tu as le palier 2', + title: 'palier 2', + threshold: 50, + targetProfileId: targetProfile.id, + }); + + databaseBuilder.factory.buildStageAcquisition({ + stageId: stage.id, + userId: user.id, + campaignParticipationId: campaignParticipation.id, + }); + + campaignSkills.slice(2).forEach((campaignSkill, index) => { + databaseBuilder.factory.buildKnowledgeElement({ + userId: user.id, + assessmentId: assessment.id, + skillId: campaignSkill.skillId, + status: index < 3 ? 'validated' : 'invalidated', + createdAt: index < 5 ? oldDate : futureDate, + }); + }); + + databaseBuilder.factory.buildKnowledgeElement({ + userId: user.id, + assessmentId: assessment.id, + skillId: 'otherSkillId', + createdAt: oldDate, + }); + + const learningContent = [ + { + id: 'recArea1', + title_i18n: { fr: 'DomaineNom1' }, + color: JAFFA_COLOR, + competences: [ + { + id: 1, + name_i18n: { fr: 'Agir collectivement' }, + description_i18n: { fr: 'Sauver le monde' }, + index: '1.2', + tubes: [{ id: 'recTube1', skills: [{ id: 'recSkill1' }] }], + }, + ], + }, + { + id: 'recArea2', + title_i18n: { fr: 'DomaineNom2' }, + color: EMERALD_COLOR, + competences: [ + { + id: 2, + name_i18n: { fr: 'Nécessité de la pensée radicale' }, + description_i18n: { fr: 'Sauver le monde' }, + index: '2.1', + tubes: [ + { + id: 'recTube2', + skills: [{ id: 'recSkill2' }, { id: 'recSkill3' }, { id: 'recSkill4' }], + }, + ], + }, + { + id: 3, + name_i18n: { fr: 'Changer efficacement le monde' }, + description_i18n: { fr: 'Sauver le monde' }, + index: '2.2', + tubes: [ + { + id: 'recTube3', + skills: [{ id: 'recSkill5' }, { id: 'recSkill6' }, { id: 'recSkill7' }, { id: 'recSkill8' }], + }, + ], + }, + ], + }, + { + id: 'recArea3', + title_i18n: { fr: 'DomaineNom3' }, + color: WILD_STRAWBERRY_COLOR, + competences: [ + { + id: 4, + name_i18n: { fr: 'Oser la paresse' }, + description_i18n: { fr: 'Sauver le monde' }, + index: '4.3', + tubes: [{ id: 'recTube0', skills: [{ id: 'notIncludedSkillId' }] }], + }, + ], + }, + ]; + const learningContentObjects = learningContentBuilder.fromAreas(learningContent); + mockLearningContent(learningContentObjects); + await databaseBuilder.commit(); + }); + + it('should return the campaign assessment result', async function () { + // given + const expectedResponse = { + data: { + type: 'campaign-participation-results', + id: campaignParticipation.id.toString(), + attributes: { + 'mastery-rate': 0.38, + 'total-skills-count': 8, + 'tested-skills-count': 5, + 'validated-skills-count': 3, + 'is-completed': true, + 'is-shared': true, + 'can-retry': false, + 'can-reset': false, + 'can-improve': false, + 'is-disabled': false, + 'participant-external-id': 'participantExternalId', + }, + relationships: { + 'campaign-participation-badges': { + data: [ + { + id: `${badge1.id}`, + type: 'campaignParticipationBadges', + }, + { + id: `${badge2.id}`, + type: 'campaignParticipationBadges', + }, + ], + }, + 'competence-results': { + data: [ + { + id: '1', + type: 'competenceResults', + }, + { + id: '2', + type: 'competenceResults', + }, + { + id: '3', + type: 'competenceResults', + }, + ], + }, + 'reached-stage': { + data: { + id: `${stage.id}`, + type: 'reached-stages', + }, + }, + }, + }, + included: [ + { + attributes: { + 'acquisition-percentage': 100, + 'alt-message': 'Low threshold badge', + 'image-url': '/img/banana.svg', + 'is-acquired': true, + 'is-always-visible': true, + 'is-certifiable': false, + 'is-valid': true, + key: 'PIX_BADGE_1', + title: 'Badge 1', + message: 'You won a badge that had a criterion threshold set at 0', + }, + id: '1', + type: 'campaignParticipationBadges', + }, + { + attributes: { + 'acquisition-percentage': 42, + 'alt-message': 'High threshold badge', + 'image-url': '/img/banana.svg', + 'is-acquired': false, + 'is-always-visible': true, + 'is-certifiable': false, + 'is-valid': false, + key: 'PIX_BADGE_2', + title: 'Badge 2', + message: 'You won a badge that had a criterion threshold set at 90', + }, + id: '2', + type: 'campaignParticipationBadges', + }, + { + type: 'competenceResults', + id: '1', + attributes: { + name: 'Agir collectivement', + index: '1.2', + description: 'Sauver le monde', + 'mastery-percentage': 0, + 'total-skills-count': 1, + 'tested-skills-count': 0, + 'validated-skills-count': 0, + 'area-color': JAFFA_COLOR, + 'area-title': 'DomaineNom1', + 'reached-stage': 0, + 'flash-pix-score': undefined, + }, + }, + { + type: 'competenceResults', + id: '2', + attributes: { + name: 'Nécessité de la pensée radicale', + index: '2.1', + description: 'Sauver le monde', + 'mastery-percentage': 67, + 'total-skills-count': 3, + 'tested-skills-count': 2, + 'validated-skills-count': 2, + 'area-color': EMERALD_COLOR, + 'area-title': 'DomaineNom2', + 'reached-stage': 2, + 'flash-pix-score': undefined, + }, + }, + { + type: 'competenceResults', + id: '3', + attributes: { + name: 'Changer efficacement le monde', + index: '2.2', + description: 'Sauver le monde', + 'mastery-percentage': 25, + 'total-skills-count': 4, + 'tested-skills-count': 3, + 'validated-skills-count': 1, + 'area-color': EMERALD_COLOR, + 'area-title': 'DomaineNom2', + 'reached-stage': 1, + 'flash-pix-score': undefined, + }, + }, + { + attributes: { + message: 'Tu as le palier 1', + title: 'palier 1', + threshold: 20, + 'reached-stage': 1, + 'total-stage': 2, + }, + id: stage.id.toString(), + type: 'reached-stages', + }, + ], + }; + + // when + const response = await server.inject({ + method: 'GET', + url: `/api/users/${user.id}/campaigns/${campaign.id}/assessment-result`, + headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, + }); + + // then + expect(response.result).to.deep.equal(expectedResponse); + expect(response.statusCode).to.equal(200); + }); + }); }); diff --git a/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js b/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js index 974507834e4..9c561b709cc 100644 --- a/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js +++ b/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js @@ -1,11 +1,15 @@ import { campaignParticipationController } from '../../../../../src/prescription/campaign-participation/application/campaign-participation-controller.js'; import * as moduleUnderTest from '../../../../../src/prescription/campaign-participation/application/campaign-participation-route.js'; +import { securityPreHandlers } from '../../../../../src/shared/application/security-pre-handlers.js'; import { expect, HttpTestServer, sinon } from '../../../../test-helper.js'; describe('Integration | Application | Route | campaignParticipationRouter', function () { - let httpTestServer; + let httpTestServer, sandbox; beforeEach(async function () { + sandbox = sinon.createSandbox(); + sandbox.stub(securityPreHandlers, 'checkRequestedUserIsAuthenticatedUser'); + sinon.stub(campaignParticipationController, 'getAnalysis').callsFake((request, h) => h.response('ok').code(200)); sinon .stub(campaignParticipationController, 'getCampaignAssessmentParticipation') @@ -13,11 +17,48 @@ describe('Integration | Application | Route | campaignParticipationRouter', func sinon .stub(campaignParticipationController, 'getCampaignAssessmentParticipationResult') .callsFake((request, h) => h.response('ok').code(200)); + sinon + .stub(campaignParticipationController, 'getUserCampaignAssessmentResult') + .callsFake((request, h) => h.response('ok').code(200)); httpTestServer = new HttpTestServer(); await httpTestServer.register(moduleUnderTest); }); + afterEach(function () { + sandbox.restore(); + }); + + describe('#getUserCampaignAssessmentResult', function () { + context('Error cases', function () { + it('should not called controller when user not authenticated', async function () { + // given + securityPreHandlers.checkRequestedUserIsAuthenticatedUser.callsFake((request, h) => { + return Promise.resolve(h.response().code(403).takeover()); + }); + + // when + httpTestServer.request('GET', '/api/users/1234/campaigns/5678/assessment-result'); + + // then + expect(campaignParticipationController.getUserCampaignAssessmentResult.notCalled).to.be.true; + }); + + it('should return a 401 HTTP response', async function () { + // given + securityPreHandlers.checkRequestedUserIsAuthenticatedUser.callsFake((request, h) => { + return Promise.resolve(h.response().code(401).takeover()); + }); + + // when + const response = await httpTestServer.request('GET', '/api/users/1234/campaigns/5678/assessment-result'); + + // then + expect(response.statusCode).to.equal(401); + }); + }); + }); + describe('GET /api/campaign-participations/{id}/analyses', function () { const method = 'GET'; diff --git a/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js b/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js index a9ccb56ea95..5beecb25259 100644 --- a/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js +++ b/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js @@ -209,6 +209,44 @@ describe('Unit | Application | Controller | Campaign-Participation', function () }); }); + describe('#getUserCampaignAssessmentResult', function () { + beforeEach(function () { + sinon.stub(usecases, 'getUserCampaignAssessmentResult'); + }); + + it('should call usecase and serializer with expected parameters', async function () { + //given + const locale = Symbol('locale'); + const userId = Symbol('userId'); + const campaignId = Symbol('campaignId'); + + const expectedResult = Symbol('expectedResult'); + const serializedResult = Symbol('serializedResult'); + + const request = { + auth: { credentials: { userId } }, + params: { campaignId }, + }; + const dependencies = { + extractLocaleFromRequest: sinon.stub(), + participantResultSerializer: { serialize: sinon.stub() }, + }; + usecases.getUserCampaignAssessmentResult.withArgs({ locale, userId, campaignId }).returns(expectedResult); + dependencies.extractLocaleFromRequest.withArgs(request).returns(locale); + dependencies.participantResultSerializer.serialize.withArgs(expectedResult).returns(serializedResult); + + // when + const response = await campaignParticipationController.getUserCampaignAssessmentResult( + request, + hFake, + dependencies, + ); + + // then + expect(response).to.be.equal(serializedResult); + }); + }); + describe('#deleteParticipation', function () { it('should call the usecase to delete the campaignParticipation', async function () { // given diff --git a/api/tests/prescription/campaign-participation/unit/application/campaign-participation-route_test.js b/api/tests/prescription/campaign-participation/unit/application/campaign-participation-route_test.js index fa397f4655f..b24ad486272 100644 --- a/api/tests/prescription/campaign-participation/unit/application/campaign-participation-route_test.js +++ b/api/tests/prescription/campaign-participation/unit/application/campaign-participation-route_test.js @@ -4,6 +4,58 @@ import { securityPreHandlers } from '../../../../../src/shared/application/secur import { expect, HttpTestServer, sinon } from '../../../../test-helper.js'; describe('Unit | Application | Router | campaign-participation-router ', function () { + describe('GET /api/users/{userId}/campaigns/{campaignId}/assessment-result', function () { + const method = 'GET'; + + it('returns 200', async function () { + // given + sinon.stub(campaignParticipationController, 'getUserCampaignAssessmentResult').returns('ok'); + sinon + .stub(securityPreHandlers, 'checkRequestedUserIsAuthenticatedUser') + .callsFake((request, h) => h.response(true)); + const httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + + const url = '/api/users/12/campaigns/34/assessment-result'; + + // when + const result = await httpTestServer.request(method, url); + + // then + expect(result.statusCode).to.equal(200); + }); + + it('returns 400 when userId is not a number', async function () { + // given + const httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + + const userId = 'wrongId'; + const url = `/api/users/${userId}/campaigns/34/assessment-result`; + + // when + const result = await httpTestServer.request(method, url); + + // then + expect(result.statusCode).to.equal(400); + }); + + it('returns 400 when campaignId is not a number', async function () { + // given + const httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + + const campaignId = 'wrongId'; + const url = `/api/users/12/campaigns/${campaignId}/assessment-result`; + + // when + const result = await httpTestServer.request(method, url); + + // then + expect(result.statusCode).to.equal(400); + }); + }); + describe('PATCH /api/admin/campaign-participations/{id}', function () { it('returns 200 when admin member has rights', async function () { // given diff --git a/api/tests/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js b/api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js similarity index 95% rename from api/tests/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js rename to api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js index 105a6429dce..1f503793240 100644 --- a/api/tests/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js +++ b/api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/participant-result-serializer_test.js @@ -1,7 +1,7 @@ -import * as serializer from '../../../../../lib/infrastructure/serializers/jsonapi/participant-result-serializer.js'; -import { KnowledgeElement } from '../../../../../src/shared/domain/models/index.js'; -import { AssessmentResult } from '../../../../../src/shared/domain/read-models/participant-results/AssessmentResult.js'; -import { domainBuilder, expect } from '../../../../test-helper.js'; +import * as serializer from '../../../../../../../src/prescription/campaign-participation/infrastructure/serializers/jsonapi/participant-result-serializer.js'; +import { KnowledgeElement } from '../../../../../../../src/shared/domain/models/index.js'; +import { AssessmentResult } from '../../../../../../../src/shared/domain/read-models/participant-results/AssessmentResult.js'; +import { domainBuilder, expect } from '../../../../../../test-helper.js'; describe('Unit | Serializer | JSON API | participant-result-serializer', function () { context('#serialize', function () { diff --git a/api/tests/unit/application/users/index_test.js b/api/tests/unit/application/users/index_test.js index 892410c3229..ac4f1cf2994 100644 --- a/api/tests/unit/application/users/index_test.js +++ b/api/tests/unit/application/users/index_test.js @@ -10,58 +10,6 @@ const CODE_IDENTITY_PROVIDER_POLE_EMPLOI = OidcIdentityProviders.POLE_EMPLOI.cod const oidcProviderCode = 'genericOidcProviderCode'; describe('Unit | Router | user-router', function () { - describe('GET /api/users/{userId}/campaigns/{campaignId}/assessment-result', function () { - const method = 'GET'; - - it('returns 200', async function () { - // given - sinon.stub(userController, 'getUserCampaignAssessmentResult').returns('ok'); - sinon - .stub(securityPreHandlers, 'checkRequestedUserIsAuthenticatedUser') - .callsFake((request, h) => h.response(true)); - const httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - - const url = '/api/users/12/campaigns/34/assessment-result'; - - // when - const result = await httpTestServer.request(method, url); - - // then - expect(result.statusCode).to.equal(200); - }); - - it('returns 400 when userId is not a number', async function () { - // given - const httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - - const userId = 'wrongId'; - const url = `/api/users/${userId}/campaigns/34/assessment-result`; - - // when - const result = await httpTestServer.request(method, url); - - // then - expect(result.statusCode).to.equal(400); - }); - - it('returns 400 when campaignId is not a number', async function () { - // given - const httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - - const campaignId = 'wrongId'; - const url = `/api/users/12/campaigns/${campaignId}/assessment-result`; - - // when - const result = await httpTestServer.request(method, url); - - // then - expect(result.statusCode).to.equal(400); - }); - }); - describe('GET /api/users/{userId}/campaigns/{campaignId}/campaign-participations', function () { const method = 'GET';