From 103543b362be865fdc04be7c5c4cbc46d0213771 Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:47:49 +0100 Subject: [PATCH 1/4] tech(api): migrate repository to its bounded context --- api/lib/domain/usecases/index.js | 2 +- .../organization-learner-repository.js | 60 ------------------ .../organization-learner-repository.js | 61 ++++++++++++++++++- .../repositories/student-repository.js | 4 +- .../repositories/student-repository_test.js | 4 +- 5 files changed, 65 insertions(+), 66 deletions(-) rename api/{lib => src/prescription/learner-management}/infrastructure/repositories/student-repository.js (92%) rename api/tests/{ => prescription/learner-management}/integration/infrastructure/repositories/student-repository_test.js (94%) diff --git a/api/lib/domain/usecases/index.js b/api/lib/domain/usecases/index.js index ab16d204ff9..8783e779f13 100644 --- a/api/lib/domain/usecases/index.js +++ b/api/lib/domain/usecases/index.js @@ -76,6 +76,7 @@ import * as campaignProfileRepository from '../../../src/prescription/campaign-p import { participationCompletedJobRepository } from '../../../src/prescription/campaign-participation/infrastructure/repositories/jobs/participation-completed-job-repository.js'; import * as poleEmploiSendingRepository from '../../../src/prescription/campaign-participation/infrastructure/repositories/pole-emploi-sending-repository.js'; import * as prescriptionOrganizationLearnerRepository from '../../../src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js'; +import * as studentRepository from '../../../src/prescription/learner-management/infrastructure/repositories/student-repository.js'; import * as organizationLearnerActivityRepository from '../../../src/prescription/organization-learner/infrastructure/repositories/organization-learner-activity-repository.js'; import * as registrationOrganizationLearnerRepository from '../../../src/prescription/organization-learner/infrastructure/repositories/registration-organization-learner-repository.js'; import * as targetProfileSummaryForAdminRepository from '../../../src/prescription/target-profile/infrastructure/repositories/target-profile-summary-for-admin-repository.js'; @@ -144,7 +145,6 @@ import * as organizationLearnerRepository from '../../infrastructure/repositorie import * as organizationMemberIdentityRepository from '../../infrastructure/repositories/organization-member-identity-repository.js'; import * as organizationTagRepository from '../../infrastructure/repositories/organization-tag-repository.js'; import { participantResultsSharedRepository } from '../../infrastructure/repositories/participant-results-shared-repository.js'; -import * as studentRepository from '../../infrastructure/repositories/student-repository.js'; import * as targetProfileRepository from '../../infrastructure/repositories/target-profile-repository.js'; import * as targetProfileShareRepository from '../../infrastructure/repositories/target-profile-share-repository.js'; import * as targetProfileTrainingRepository from '../../infrastructure/repositories/target-profile-training-repository.js'; diff --git a/api/lib/infrastructure/repositories/organization-learner-repository.js b/api/lib/infrastructure/repositories/organization-learner-repository.js index d5e8d7f4318..46fb49a9d5d 100644 --- a/api/lib/infrastructure/repositories/organization-learner-repository.js +++ b/api/lib/infrastructure/repositories/organization-learner-repository.js @@ -10,31 +10,6 @@ import { OrganizationLearner } from '../../../src/shared/domain/models/Organizat import { ParticipantRepartition } from '../../../src/shared/domain/models/ParticipantRepartition.js'; import { fetchPage } from '../../../src/shared/infrastructure/utils/knex-utils.js'; import { DomainTransaction } from '../DomainTransaction.js'; -import * as studentRepository from './student-repository.js'; - -function _shouldStudentToImportBeReconciled( - allOrganizationLearnersInSameOrganization, - organizationLearner, - studentToImport, -) { - const organizationLearnerWithSameUserId = allOrganizationLearnersInSameOrganization.find( - (organizationLearnerInSameOrganization) => { - return organizationLearnerInSameOrganization.userId === organizationLearner.account.userId; - }, - ); - const isOrganizationLearnerReconciled = organizationLearnerWithSameUserId != null; - const organizationLearnerHasSameUserIdAndNationalStudentId = - organizationLearnerWithSameUserId?.nationalStudentId === organizationLearner.nationalStudentId; - - if (isOrganizationLearnerReconciled && !organizationLearnerHasSameUserIdAndNationalStudentId) { - return false; - } - - const isFromSameOrganization = studentToImport.organizationId === organizationLearner.account.organizationId; - const isFromDifferentOrganizationWithSameBirthday = - !isFromSameOrganization && studentToImport.birthdate === organizationLearner.account.birthdate; - return isFromSameOrganization || isFromDifferentOrganizationWithSameBirthday; -} const findByIds = async function ({ ids }) { const rawOrganizationLearners = await knex @@ -89,40 +64,6 @@ const findByUserId = async function ({ userId }) { return rawOrganizationLearners.map((rawOrganizationLearner) => new OrganizationLearner(rawOrganizationLearner)); }; -const _reconcileOrganizationLearners = async function (studentsToImport, allOrganizationLearnersInSameOrganization) { - const nationalStudentIdsFromFile = studentsToImport - .map((organizationLearnerData) => organizationLearnerData.nationalStudentId) - .filter(Boolean); - const organizationLearnersWithSameNationalStudentIdsAsImported = - await studentRepository.findReconciledStudentsByNationalStudentId(nationalStudentIdsFromFile); - - organizationLearnersWithSameNationalStudentIdsAsImported.forEach((organizationLearner) => { - const alreadyReconciledStudentToImport = studentsToImport.find( - (studentToImport) => studentToImport.userId === organizationLearner.account.userId, - ); - - if (alreadyReconciledStudentToImport) { - alreadyReconciledStudentToImport.userId = null; - return; - } - - const studentToImport = studentsToImport.find( - (studentToImport) => studentToImport.nationalStudentId === organizationLearner.nationalStudentId, - ); - - if ( - _shouldStudentToImportBeReconciled( - allOrganizationLearnersInSameOrganization, - organizationLearner, - studentToImport, - ) - ) { - studentToImport.userId = organizationLearner.account.userId; - } - }); - return studentsToImport; -}; - const findByOrganizationIdAndBirthdate = async function ({ organizationId, birthdate }) { const rawOrganizationLearners = await knex .select('*') @@ -347,7 +288,6 @@ const findAllLearnerWithAtLeastOneParticipationByOrganizationIds = async functio }; export { - _reconcileOrganizationLearners, countByOrganizationsWhichNeedToComputeCertificability, dissociateAllStudentsByUserId, findAllLearnerWithAtLeastOneParticipationByOrganizationId, diff --git a/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js b/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js index c5da1bc0b34..e7adafee960 100644 --- a/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js +++ b/api/src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js @@ -11,6 +11,7 @@ import { OrganizationLearner } from '../../../../shared/domain/models/index.js'; import { ApplicationTransaction } from '../../../shared/infrastructure/ApplicationTransaction.js'; import { CommonOrganizationLearner } from '../../domain/models/CommonOrganizationLearner.js'; import { OrganizationLearnerForAdmin } from '../../domain/read-models/OrganizationLearnerForAdmin.js'; +import * as studentRepository from './student-repository.js'; const dissociateUserFromOrganizationLearner = async function (organizationLearnerId) { const knexConn = DomainTransaction.getConnection(); @@ -82,7 +83,7 @@ const addOrUpdateOrganizationOfOrganizationLearners = async function (organizati ); const existingOrganizationLearners = await organizationLearnerRepository.findByOrganizationId({ organizationId }); - const reconciledOrganizationLearnersToImport = await organizationLearnerRepository._reconcileOrganizationLearners( + const reconciledOrganizationLearnersToImport = await _reconcileOrganizationLearners( organizationLearnersFromFile, existingOrganizationLearners, ); @@ -103,6 +104,64 @@ const addOrUpdateOrganizationOfOrganizationLearners = async function (organizati } }; +const _reconcileOrganizationLearners = async function (studentsToImport, allOrganizationLearnersInSameOrganization) { + const nationalStudentIdsFromFile = studentsToImport + .map((organizationLearnerData) => organizationLearnerData.nationalStudentId) + .filter(Boolean); + const organizationLearnersWithSameNationalStudentIdsAsImported = + await studentRepository.findReconciledStudentsByNationalStudentId(nationalStudentIdsFromFile); + + organizationLearnersWithSameNationalStudentIdsAsImported.forEach((organizationLearner) => { + const alreadyReconciledStudentToImport = studentsToImport.find( + (studentToImport) => studentToImport.userId === organizationLearner.account.userId, + ); + + if (alreadyReconciledStudentToImport) { + alreadyReconciledStudentToImport.userId = null; + return; + } + + const studentToImport = studentsToImport.find( + (studentToImport) => studentToImport.nationalStudentId === organizationLearner.nationalStudentId, + ); + + if ( + _shouldStudentToImportBeReconciled( + allOrganizationLearnersInSameOrganization, + organizationLearner, + studentToImport, + ) + ) { + studentToImport.userId = organizationLearner.account.userId; + } + }); + return studentsToImport; +}; + +function _shouldStudentToImportBeReconciled( + allOrganizationLearnersInSameOrganization, + organizationLearner, + studentToImport, +) { + const organizationLearnerWithSameUserId = allOrganizationLearnersInSameOrganization.find( + (organizationLearnerInSameOrganization) => { + return organizationLearnerInSameOrganization.userId === organizationLearner.account.userId; + }, + ); + const isOrganizationLearnerReconciled = organizationLearnerWithSameUserId != null; + const organizationLearnerHasSameUserIdAndNationalStudentId = + organizationLearnerWithSameUserId?.nationalStudentId === organizationLearner.nationalStudentId; + + if (isOrganizationLearnerReconciled && !organizationLearnerHasSameUserIdAndNationalStudentId) { + return false; + } + + const isFromSameOrganization = studentToImport.organizationId === organizationLearner.account.organizationId; + const isFromDifferentOrganizationWithSameBirthday = + !isFromSameOrganization && studentToImport.birthdate === organizationLearner.account.birthdate; + return isFromSameOrganization || isFromDifferentOrganizationWithSameBirthday; +} + const saveCommonOrganizationLearners = function (learners) { const knex = ApplicationTransaction.getConnection(); diff --git a/api/lib/infrastructure/repositories/student-repository.js b/api/src/prescription/learner-management/infrastructure/repositories/student-repository.js similarity index 92% rename from api/lib/infrastructure/repositories/student-repository.js rename to api/src/prescription/learner-management/infrastructure/repositories/student-repository.js index b98d03101a4..5c0830f0cae 100644 --- a/api/lib/infrastructure/repositories/student-repository.js +++ b/api/src/prescription/learner-management/infrastructure/repositories/student-repository.js @@ -1,7 +1,7 @@ import _ from 'lodash'; -import { Student } from '../../../src/shared/domain/models/Student.js'; -import { DomainTransaction } from '../DomainTransaction.js'; +import { DomainTransaction } from '../../../../../lib/infrastructure/DomainTransaction.js'; +import { Student } from '../../../../shared/domain/models/Student.js'; const _toStudents = function (results) { const students = []; diff --git a/api/tests/integration/infrastructure/repositories/student-repository_test.js b/api/tests/prescription/learner-management/integration/infrastructure/repositories/student-repository_test.js similarity index 94% rename from api/tests/integration/infrastructure/repositories/student-repository_test.js rename to api/tests/prescription/learner-management/integration/infrastructure/repositories/student-repository_test.js index da71c9f2ca4..74a9383df22 100644 --- a/api/tests/integration/infrastructure/repositories/student-repository_test.js +++ b/api/tests/prescription/learner-management/integration/infrastructure/repositories/student-repository_test.js @@ -1,5 +1,5 @@ -import * as studentRepository from '../../../../lib/infrastructure/repositories/student-repository.js'; -import { databaseBuilder, expect } from '../../../test-helper.js'; +import * as studentRepository from '../../../../../../src/prescription/learner-management/infrastructure/repositories/student-repository.js'; +import { databaseBuilder, expect } from '../../../../../test-helper.js'; describe('Integration | Infrastructure | Repository | student-repository', function () { describe('#findReconciledStudentsByNationalStudentId', function () { From c51af8a6013f5d5639aa4f4d21fefc5105f6a984 Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:09:54 +0100 Subject: [PATCH 2/4] tech(api): migrate reconcile manually usecase to prescription context --- .../domain/usecases/index.js | 17 +++- ...ncile-sco-organization-learner-manually.js | 16 ++-- ...-sco-organization-learner-manually_test.js | 91 +++++++++---------- 3 files changed, 66 insertions(+), 58 deletions(-) rename api/{lib => src/prescription/learner-management}/domain/usecases/reconcile-sco-organization-learner-manually.js (88%) rename api/tests/{ => prescription/learner-management}/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js (88%) diff --git a/api/src/prescription/learner-management/domain/usecases/index.js b/api/src/prescription/learner-management/domain/usecases/index.js index 78089168d5b..0c188ad2457 100644 --- a/api/src/prescription/learner-management/domain/usecases/index.js +++ b/api/src/prescription/learner-management/domain/usecases/index.js @@ -1,8 +1,11 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import * as obfuscationService from '../../../../../lib/domain/services/obfuscation-service.js'; import * as userReconciliationService from '../../../../../lib/domain/services/user-reconciliation-service.js'; import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js'; +import * as libOrganizationLearnerRepository from '../../../../../lib/infrastructure/repositories/organization-learner-repository.js'; +import * as userRepository from '../../../../identity-access-management/infrastructure/repositories/user.repository.js'; import * as organizationFeatureApi from '../../../../organizational-entities/application/api/organization-features-api.js'; import { logErrorWithCorrelationIds } from '../../../../shared/infrastructure/monitoring-tools.js'; import * as organizationRepository from '../../../../shared/infrastructure/repositories/organization-repository.js'; @@ -10,6 +13,7 @@ import { injectDependencies } from '../../../../shared/infrastructure/utils/depe import { importNamedExportsFromDirectory } from '../../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; import { logger } from '../../../../shared/infrastructure/utils/logger.js'; import * as membershipRepository from '../../../../team/infrastructure/repositories/membership.repository.js'; +import * as registrationOrganizationLearnerRepository from '../../../organization-learner/infrastructure/repositories/registration-organization-learner-repository.js'; import * as campaignParticipationRepository from '../../infrastructure/repositories/campaign-participation-repository.js'; import { repositories } from '../../infrastructure/repositories/index.js'; import { importOrganizationLearnersJobRepository } from '../../infrastructure/repositories/jobs/import-organization-learners-job-repository.js'; @@ -18,6 +22,7 @@ import { validateOrganizationImportFileJobRepository } from '../../infrastructur import * as organizationImportRepository from '../../infrastructure/repositories/organization-import-repository.js'; import * as organizationLearnerImportFormatRepository from '../../infrastructure/repositories/organization-learner-import-format-repository.js'; import * as organizationLearnerRepository from '../../infrastructure/repositories/organization-learner-repository.js'; +import * as studentRepository from '../../infrastructure/repositories/student-repository.js'; import * as supOrganizationLearnerRepository from '../../infrastructure/repositories/sup-organization-learner-repository.js'; import { importStorage } from '../../infrastructure/storage/import-storage.js'; @@ -27,17 +32,22 @@ import { importStorage } from '../../infrastructure/storage/import-storage.js'; * @typedef {import ('../../infrastructure/repositories/jobs/import-organization-learners-job-repository.js')} ImportOrganizationLearnersJobRepository * @typedef {import ('../../infrastructure/storage/import-storage.js')} ImportStorage * @typedef {import ('../../infrastructure/repositories/jobs/import-sup-organization-learners-job-repository.js')} ImportSupOrganizationLearnersJobRepository + * @typedef {import ('../../../../../lib/infrastructure/repositories/organization-learner-repository.js')} libOrganizationLearnerRepository * @typedef {import ('../../../../shared/infrastructure/monitoring-tools.js')} LogErrorWithCorrelationIds - * @typedef {import ('../../../../shared/infrastructure/utils/logger.js')} Loggger + * @typedef {import ('../../../../shared/infrastructure/utils/logger.js')} loggger * @typedef {import ('../../../../team/infrastructure/repositories/membership-repository.js')} MembershipRepository + * @typedef {import ('../../../../../lib/domain/services/obfuscation-service.js')} obfuscationService * @typedef {import ('../../../../organizational-entities/application/api/organization-features-api.js')} OrganizationFeatureApi * @typedef {import ('../../infrastructure/repositories/organization-feature-repository.js')} OrganizationFeatureRepository * @typedef {import ('../../infrastructure/repositories/organization-import-repository.js')} OrganizationImportRepository * @typedef {import ('../../infrastructure/repositories/organization-learner-import-format-repository.js')} OrganizationLearnerImportFormatRepository * @typedef {import ('../../infrastructure/repositories/organization-learner-repository.js')} OrganizationLearnerRepository * @typedef {import ('../../../../shared/infrastructure/repositories/organization-repository.js')} OrganizationRepository + * @typedef {import('../../../organization-learner/infrastructure/repositories/registration-organization-learner-repository.js')} registrationOrganizationLearnerRepository + * @typedef {import ('../../infrastructure/repositories/student-repository.js')} studentRepository * @typedef {import ('../../infrastructure/repositories/sup-organization-learner-repository.js')} SupOrganizationLearnerRepository * @typedef {import ('../../../../../lib/domain/services/user-reconciliation-service.js')} UserReconciliationService + * @typedef {import('../../../../identity-access-management/infrastructure/repositories/user.repository.js')} userRepository * @typedef {import ('../../infrastructure/repositories/jobs/validate-organization-learners-import-file-job-repository.js')} ValidateOrganizationImportFileJobRepository */ const dependencies = { @@ -46,17 +56,22 @@ const dependencies = { importOrganizationLearnersJobRepository, importStorage, importSupOrganizationLearnersJobRepository, + libOrganizationLearnerRepository, logErrorWithCorrelationIds, logger, membershipRepository, + obfuscationService, organizationFeatureApi, organizationFeatureRepository: repositories.organizationFeatureRepository, organizationImportRepository, organizationLearnerImportFormatRepository, organizationLearnerRepository, organizationRepository, + registrationOrganizationLearnerRepository, + studentRepository, supOrganizationLearnerRepository, userReconciliationService, + userRepository, validateOrganizationImportFileJobRepository, }; diff --git a/api/lib/domain/usecases/reconcile-sco-organization-learner-manually.js b/api/src/prescription/learner-management/domain/usecases/reconcile-sco-organization-learner-manually.js similarity index 88% rename from api/lib/domain/usecases/reconcile-sco-organization-learner-manually.js rename to api/src/prescription/learner-management/domain/usecases/reconcile-sco-organization-learner-manually.js index 382b99f3d5a..309800d0649 100644 --- a/api/lib/domain/usecases/reconcile-sco-organization-learner-manually.js +++ b/api/src/prescription/learner-management/domain/usecases/reconcile-sco-organization-learner-manually.js @@ -1,11 +1,11 @@ import lodash from 'lodash'; -import { STUDENT_RECONCILIATION_ERRORS } from '../../../src/shared/domain/constants.js'; +import { STUDENT_RECONCILIATION_ERRORS } from '../../../../shared/domain/constants.js'; import { CampaignCodeError, OrganizationLearnerAlreadyLinkedToUserError, UserShouldNotBeReconciledOnAnotherAccountError, -} from '../../../src/shared/domain/errors.js'; +} from '../../../../shared/domain/errors.js'; const { isEmpty } = lodash; @@ -14,8 +14,8 @@ const reconcileScoOrganizationLearnerManually = async function ({ reconciliationInfo, withReconciliation, campaignRepository, + libOrganizationLearnerRepository, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, studentRepository, userRepository, @@ -31,7 +31,7 @@ const reconcileScoOrganizationLearnerManually = async function ({ await userReconciliationService.findMatchingOrganizationLearnerForGivenOrganizationIdAndReconciliationInfo({ organizationId: campaign.organizationId, reconciliationInfo, - organizationLearnerRepository, + organizationLearnerRepository: libOrganizationLearnerRepository, }); await userReconciliationService.assertStudentHasAnAlreadyReconciledAccount( @@ -50,11 +50,11 @@ const reconcileScoOrganizationLearnerManually = async function ({ await _checkIfUserIsConnectedOnAnotherAccount({ organizationLearnerOfUserAccessingCampaign, authenticatedUserId: reconciliationInfo.id, - organizationLearnerRepository, + libOrganizationLearnerRepository, }); if (withReconciliation) { - return prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner({ + return organizationLearnerRepository.reconcileUserToOrganizationLearner({ userId: reconciliationInfo.id, organizationLearnerId: organizationLearnerOfUserAccessingCampaign.id, }); @@ -86,9 +86,9 @@ async function _checkIfAnotherStudentIsAlreadyReconciledWithTheSameOrganizationA async function _checkIfUserIsConnectedOnAnotherAccount({ organizationLearnerOfUserAccessingCampaign, authenticatedUserId, - organizationLearnerRepository, + libOrganizationLearnerRepository, }) { - const loggedAccountReconciledOrganizationLearners = await organizationLearnerRepository.findByUserId({ + const loggedAccountReconciledOrganizationLearners = await libOrganizationLearnerRepository.findByUserId({ userId: authenticatedUserId, }); diff --git a/api/tests/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js b/api/tests/prescription/learner-management/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js similarity index 88% rename from api/tests/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js rename to api/tests/prescription/learner-management/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js index 5cba799a59e..4fb9a306f7d 100644 --- a/api/tests/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js +++ b/api/tests/prescription/learner-management/unit/domain/usecases/reconcile-sco-organization-learner-manually_test.js @@ -1,22 +1,21 @@ -import { usecases } from '../../../../lib/domain/usecases/index.js'; +import { usecases } from '../../../../../../src/prescription/learner-management/domain/usecases/index.js'; import { CampaignCodeError, NotFoundError, OrganizationLearnerAlreadyLinkedToUserError, UserShouldNotBeReconciledOnAnotherAccountError, -} from '../../../../src/shared/domain/errors.js'; -import { OrganizationLearner } from '../../../../src/shared/domain/models/OrganizationLearner.js'; -import { catchErr, domainBuilder, expect, sinon } from '../../../test-helper.js'; +} from '../../../../../../src/shared/domain/errors.js'; +import { OrganizationLearner } from '../../../../../../src/shared/domain/models/OrganizationLearner.js'; +import { catchErr, domainBuilder, expect, sinon } from '../../../../../test-helper.js'; describe('Unit | UseCase | reconcile-sco-organization-learner-manually', function () { let campaignCode; let campaignRepository; let organizationLearnerRepository; - let prescriptionOrganizationLearnerRepository; let registrationOrganizationLearnerRepository; let userReconciliationService; - + let libOrganizationLearnerRepository; let organizationLearner; let user; const organizationId = 1; @@ -36,11 +35,11 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio getByCode: sinon.stub(), }; organizationLearnerRepository = { - findByUserId: sinon.stub(), - }; - prescriptionOrganizationLearnerRepository = { reconcileUserToOrganizationLearner: sinon.stub(), }; + libOrganizationLearnerRepository = { + findByUserId: sinon.stub(), + }; registrationOrganizationLearnerRepository = { findOneByUserIdAndOrganizationId: sinon.stub(), }; @@ -57,6 +56,7 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio // when const result = await catchErr(usecases.reconcileScoOrganizationLearnerManually)({ + libOrganizationLearnerRepository, reconciliationInfo: user, campaignCode, campaignRepository, @@ -79,6 +79,7 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio // when const result = await catchErr(usecases.reconcileScoOrganizationLearnerManually)({ + libOrganizationLearnerRepository, reconciliationInfo: user, campaignCode, campaignRepository, @@ -106,6 +107,7 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio // when const result = await catchErr(usecases.reconcileScoOrganizationLearnerManually)({ + libOrganizationLearnerRepository, reconciliationInfo: user, campaignCode, campaignRepository, @@ -147,6 +149,7 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio // when const result = await catchErr(usecases.reconcileScoOrganizationLearnerManually)({ + libOrganizationLearnerRepository, reconciliationInfo: user, campaignCode, campaignRepository, @@ -196,27 +199,23 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); registrationOrganizationLearnerRepository.findOneByUserIdAndOrganizationId.resolves(); - organizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner.resolves( - currentOrganizationLearner, - ); + libOrganizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); + organizationLearnerRepository.reconcileUserToOrganizationLearner.resolves(currentOrganizationLearner); // when await usecases.reconcileScoOrganizationLearnerManually({ + libOrganizationLearnerRepository, reconciliationInfo, withReconciliation: true, campaignCode, campaignRepository, userReconciliationService, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, }); // then - expect( - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner, - ).to.have.been.calledOnceWithExactly({ + expect(organizationLearnerRepository.reconcileUserToOrganizationLearner).to.have.been.calledOnceWithExactly({ userId: reconciliationInfo.id, organizationLearnerId: currentOrganizationLearner.id, }); @@ -256,10 +255,11 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); registrationOrganizationLearnerRepository.findOneByUserIdAndOrganizationId.resolves(); - organizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); + libOrganizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); // when const error = await catchErr(usecases.reconcileScoOrganizationLearnerManually)({ + libOrganizationLearnerRepository, reconciliationInfo, withReconciliation: true, campaignCode, @@ -311,27 +311,23 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); registrationOrganizationLearnerRepository.findOneByUserIdAndOrganizationId.resolves(); - organizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner.resolves( - currentOrganizationLearner, - ); + libOrganizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); + organizationLearnerRepository.reconcileUserToOrganizationLearner.resolves(currentOrganizationLearner); // when await usecases.reconcileScoOrganizationLearnerManually({ + libOrganizationLearnerRepository, reconciliationInfo, withReconciliation: true, campaignCode, campaignRepository, userReconciliationService, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, }); // then - expect( - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner, - ).to.have.been.calledOnceWithExactly({ + expect(organizationLearnerRepository.reconcileUserToOrganizationLearner).to.have.been.calledOnceWithExactly({ userId: reconciliationInfo.id, organizationLearnerId: currentOrganizationLearner.id, }); @@ -371,27 +367,23 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); registrationOrganizationLearnerRepository.findOneByUserIdAndOrganizationId.resolves(); - organizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner.resolves( - currentOrganizationLearner, - ); + libOrganizationLearnerRepository.findByUserId.withArgs({ userId: 1 }).resolves([previousOrganizationLearner]); + organizationLearnerRepository.reconcileUserToOrganizationLearner.resolves(currentOrganizationLearner); // when await usecases.reconcileScoOrganizationLearnerManually({ + libOrganizationLearnerRepository, reconciliationInfo, withReconciliation: true, campaignCode, campaignRepository, userReconciliationService, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, }); // then - expect( - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner, - ).to.have.been.calledOnceWithExactly({ + expect(organizationLearnerRepository.reconcileUserToOrganizationLearner).to.have.been.calledOnceWithExactly({ userId: reconciliationInfo.id, organizationLearnerId: currentOrganizationLearner.id, }); @@ -441,12 +433,13 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); registrationOrganizationLearnerRepository.findOneByUserIdAndOrganizationId.resolves(); - organizationLearnerRepository.findByUserId + libOrganizationLearnerRepository.findByUserId .withArgs({ userId: 1 }) .resolves([previousOrganizationLearner, otherOrganizationLearner]); // when const error = await catchErr(usecases.reconcileScoOrganizationLearnerManually)({ + libOrganizationLearnerRepository, reconciliationInfo, withReconciliation: true, campaignCode, @@ -501,29 +494,29 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); registrationOrganizationLearnerRepository.findOneByUserIdAndOrganizationId.resolves(); - organizationLearnerRepository.findByUserId + libOrganizationLearnerRepository.findByUserId .withArgs({ userId: 1 }) .resolves([previousOrganizationLearner, otherOrganizationLearner]); // when await usecases.reconcileScoOrganizationLearnerManually({ + libOrganizationLearnerRepository, reconciliationInfo, withReconciliation: true, campaignCode, campaignRepository, userReconciliationService, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, }); // then - expect( - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner, - ).to.have.been.calledOnceWithExactly({ - userId: reconciliationInfo.id, - organizationLearnerId: currentOrganizationLearner.id, - }); + expect(organizationLearnerRepository.reconcileUserToOrganizationLearner).to.have.been.calledOnceWithExactly( + { + userId: reconciliationInfo.id, + organizationLearnerId: currentOrganizationLearner.id, + }, + ); }); }); }, @@ -544,23 +537,23 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio organizationLearner, ); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); - prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner + organizationLearnerRepository.reconcileUserToOrganizationLearner .withArgs({ userId: user.id, organizationLearnerId: organizationLearnerId, }) .resolves(organizationLearner); - organizationLearnerRepository.findByUserId.resolves([organizationLearner]); + libOrganizationLearnerRepository.findByUserId.resolves([organizationLearner]); // when const result = await usecases.reconcileScoOrganizationLearnerManually({ + libOrganizationLearnerRepository, reconciliationInfo: user, withReconciliation, campaignCode, campaignRepository, userReconciliationService, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, }); @@ -583,24 +576,24 @@ describe('Unit | UseCase | reconcile-sco-organization-learner-manually', functio userReconciliationService.findMatchingOrganizationLearnerForGivenOrganizationIdAndReconciliationInfo.resolves( organizationLearner, ); - organizationLearnerRepository.findByUserId.resolves([organizationLearner]); + libOrganizationLearnerRepository.findByUserId.resolves([organizationLearner]); userReconciliationService.assertStudentHasAnAlreadyReconciledAccount.resolves(); // when const result = await usecases.reconcileScoOrganizationLearnerManually({ + libOrganizationLearnerRepository, reconciliationInfo: user, withReconciliation, campaignCode, campaignRepository, userReconciliationService, organizationLearnerRepository, - prescriptionOrganizationLearnerRepository, registrationOrganizationLearnerRepository, }); // then expect(result).to.be.undefined; - expect(prescriptionOrganizationLearnerRepository.reconcileUserToOrganizationLearner).to.not.have.been.called; + expect(organizationLearnerRepository.reconcileUserToOrganizationLearner).to.not.have.been.called; }); }); }); From 82305b24011dc9eeb336d16b570e191e0321604d Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:17:37 +0100 Subject: [PATCH 3/4] tech(api): migrate sco serializer --- .../jsonapi/sco-organization-learner-serializer.js | 0 .../jsonapi/sco-organization-learner-serializer_test.js | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename api/{lib => src/prescription/learner-management}/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js (100%) rename api/tests/{ => prescription/learner-management}/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js (91%) diff --git a/api/lib/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js b/api/src/prescription/learner-management/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js similarity index 100% rename from api/lib/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js rename to api/src/prescription/learner-management/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js diff --git a/api/tests/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js b/api/tests/prescription/learner-management/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js similarity index 91% rename from api/tests/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js rename to api/tests/prescription/learner-management/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js index e502912d97e..23f1af97297 100644 --- a/api/tests/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js +++ b/api/tests/prescription/learner-management/unit/infrastructure/serializers/jsonapi/sco-organization-learner-serializer_test.js @@ -1,6 +1,6 @@ -import * as serializer from '../../../../../lib/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; -import { OrganizationLearner } from '../../../../../src/shared/domain/models/index.js'; -import { expect } from '../../../../test-helper.js'; +import * as serializer from '../../../../../../../src/prescription/learner-management/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; +import { OrganizationLearner } from '../../../../../../../src/shared/domain/models/index.js'; +import { expect } from '../../../../../../test-helper.js'; describe('Unit | Serializer | JSONAPI | sco-organization-learner-serializer', function () { describe('#serializeIdentity', function () { From e03541d02a9b7713b6d7fbc79f91fd251eaeea29 Mon Sep 17 00:00:00 2001 From: Xavier Carron <33637571+xav-car@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:24:23 +0100 Subject: [PATCH 4/4] tech(api): migrate route / controller --- .../sco-organization-learners/index.js | 31 --- .../sco-organization-learner-controller.js | 36 +--- .../organization-learners-controller.js | 2 +- .../sco-organization-management-controller.js | 35 +++ .../sco-organization-management-route.js | 33 ++- ...co-organization-learner-controller_test.js | 97 --------- .../sco-organization-learners/index_test.js | 188 ---------------- .../sco-organization-management-route_test.js | 112 ++++++++++ .../sco-organization-management-route_test.js | 200 ++++++++++++++++++ 9 files changed, 381 insertions(+), 353 deletions(-) create mode 100644 api/tests/prescription/learner-management/acceptance/application/sco-organization-management-route_test.js create mode 100644 api/tests/prescription/learner-management/integration/application/sco-organization-management-route_test.js diff --git a/api/lib/application/sco-organization-learners/index.js b/api/lib/application/sco-organization-learners/index.js index c9eab833d79..c08011ca5bc 100644 --- a/api/lib/application/sco-organization-learners/index.js +++ b/api/lib/application/sco-organization-learners/index.js @@ -21,37 +21,6 @@ const inaPattern = new RegExp('^[0-9]{10}[a-zA-Z]{1}$'); const register = async function (server) { server.route([ - { - method: 'POST', - path: '/api/sco-organization-learners/association', - config: { - handler: scoOrganizationLearnerController.reconcileScoOrganizationLearnerManually, - validate: { - options: { - allowUnknown: false, - }, - payload: Joi.object({ - data: { - attributes: { - 'first-name': Joi.string().empty(Joi.string().regex(/^\s*$/)).required(), - 'last-name': Joi.string().empty(Joi.string().regex(/^\s*$/)).required(), - birthdate: Joi.date().format('YYYY-MM-DD').required(), - 'campaign-code': Joi.string().empty(Joi.string().regex(/^\s*$/)).required(), - }, - type: 'sco-organization-learners', - }, - }), - failAction: (request, h) => { - return sendJsonApiError(new UnprocessableEntityError('Un des champs saisis n’est pas valide.'), h); - }, - }, - notes: [ - '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + - '- Elle associe des données saisies par l’utilisateur à l’inscription de l’élève dans cette organisation', - ], - tags: ['api', 'sco-organization-learners'], - }, - }, { method: 'PUT', path: '/api/sco-organization-learners/possibilities', diff --git a/api/lib/application/sco-organization-learners/sco-organization-learner-controller.js b/api/lib/application/sco-organization-learners/sco-organization-learner-controller.js index a311e3bf6da..28f1ec48b3f 100644 --- a/api/lib/application/sco-organization-learners/sco-organization-learner-controller.js +++ b/api/lib/application/sco-organization-learners/sco-organization-learner-controller.js @@ -1,44 +1,11 @@ import dayjs from 'dayjs'; +import * as scoOrganizationLearnerSerializer from '../../../src/prescription/learner-management/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; import * as requestResponseUtils from '../../../src/shared/infrastructure/utils/request-response-utils.js'; import { usecases } from '../../domain/usecases/index.js'; import { DomainTransaction } from '../../infrastructure/DomainTransaction.js'; -import * as scoOrganizationLearnerSerializer from '../../infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; import * as studentInformationForAccountRecoverySerializer from '../../infrastructure/serializers/jsonapi/student-information-for-account-recovery-serializer.js'; -const reconcileScoOrganizationLearnerManually = async function ( - request, - h, - dependencies = { scoOrganizationLearnerSerializer }, -) { - const authenticatedUserId = request.auth.credentials.userId; - const payload = request.payload.data.attributes; - const campaignCode = payload['campaign-code']; - const withReconciliation = request.query.withReconciliation === 'true'; - - const reconciliationInfo = { - id: authenticatedUserId, - firstName: payload['first-name'], - lastName: payload['last-name'], - birthdate: payload['birthdate'], - }; - - const organizationLearner = await usecases.reconcileScoOrganizationLearnerManually({ - campaignCode, - reconciliationInfo, - withReconciliation, - }); - - let response; - if (withReconciliation) { - const serializedData = dependencies.scoOrganizationLearnerSerializer.serializeIdentity(organizationLearner); - response = h.response(serializedData).code(200); - } else { - response = h.response().code(204); - } - return response; -}; - const generateUsername = async function (request, h, dependencies = { scoOrganizationLearnerSerializer }) { const payload = request.payload.data.attributes; const { 'campaign-code': campaignCode } = payload; @@ -191,7 +158,6 @@ const batchGenerateOrganizationLearnersUsernameWithTemporaryPassword = async fun }; const scoOrganizationLearnerController = { - reconcileScoOrganizationLearnerManually, generateUsername, createAndReconcileUserToOrganizationLearner, createUserAndReconcileToOrganizationLearnerFromExternalUser, diff --git a/api/src/prescription/learner-management/application/organization-learners-controller.js b/api/src/prescription/learner-management/application/organization-learners-controller.js index 2943263207d..098dd9bfdad 100644 --- a/api/src/prescription/learner-management/application/organization-learners-controller.js +++ b/api/src/prescription/learner-management/application/organization-learners-controller.js @@ -1,7 +1,7 @@ -import * as scoOrganizationLearnerSerializer from '../../../../lib/infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; import { ApplicationTransaction } from '../../shared/infrastructure/ApplicationTransaction.js'; import { usecases } from '../domain/usecases/index.js'; +import * as scoOrganizationLearnerSerializer from '../infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; const deleteOrganizationLearners = async function (request, h) { const authenticatedUserId = request.auth.credentials.userId; diff --git a/api/src/prescription/learner-management/application/sco-organization-management-controller.js b/api/src/prescription/learner-management/application/sco-organization-management-controller.js index 6171ce6b264..cac2ab9a541 100644 --- a/api/src/prescription/learner-management/application/sco-organization-management-controller.js +++ b/api/src/prescription/learner-management/application/sco-organization-management-controller.js @@ -7,6 +7,7 @@ import { } from '../../../../src/shared/infrastructure/monitoring-tools.js'; import { usecases } from '../domain/usecases/index.js'; import { OrganizationLearnerParser } from '../infrastructure/serializers/csv/organization-learner-parser.js'; +import * as scoOrganizationLearnerSerializer from '../infrastructure/serializers/jsonapi/sco-organization-learner-serializer.js'; const INVALID_FILE_EXTENSION_ERROR = 'INVALID_FILE_EXTENSION'; @@ -66,8 +67,42 @@ const importOrganizationLearnersFromSIECLE = async function ( return h.response(null).code(204); }; +const reconcileScoOrganizationLearnerManually = async function ( + request, + h, + dependencies = { scoOrganizationLearnerSerializer }, +) { + const authenticatedUserId = request.auth.credentials.userId; + const payload = request.payload.data.attributes; + const campaignCode = payload['campaign-code']; + const withReconciliation = request.query.withReconciliation === 'true'; + + const reconciliationInfo = { + id: authenticatedUserId, + firstName: payload['first-name'], + lastName: payload['last-name'], + birthdate: payload['birthdate'], + }; + + const organizationLearner = await usecases.reconcileScoOrganizationLearnerManually({ + campaignCode, + reconciliationInfo, + withReconciliation, + }); + + let response; + if (withReconciliation) { + const serializedData = dependencies.scoOrganizationLearnerSerializer.serializeIdentity(organizationLearner); + response = h.response(serializedData).code(200); + } else { + response = h.response().code(204); + } + return response; +}; + const scoOrganizationManagementController = { importOrganizationLearnersFromSIECLE, + reconcileScoOrganizationLearnerManually, }; export { scoOrganizationManagementController }; diff --git a/api/src/prescription/learner-management/application/sco-organization-management-route.js b/api/src/prescription/learner-management/application/sco-organization-management-route.js index ff159ef1657..f4fab92d21e 100644 --- a/api/src/prescription/learner-management/application/sco-organization-management-route.js +++ b/api/src/prescription/learner-management/application/sco-organization-management-route.js @@ -2,7 +2,7 @@ import JoiDate from '@joi/date'; import BaseJoi from 'joi'; const Joi = BaseJoi.extend(JoiDate); -import { sendJsonApiError } from '../../../shared/application/http-errors.js'; +import { sendJsonApiError, UnprocessableEntityError } from '../../../shared/application/http-errors.js'; import { securityPreHandlers } from '../../../shared/application/security-pre-handlers.js'; import { identifiersType } from '../../../shared/domain/types/identifiers-type.js'; import { usecases } from '../domain/usecases/index.js'; @@ -12,6 +12,37 @@ const TWENTY_MEGABYTES = 1048576 * 20; const register = async function (server) { server.route([ + { + method: 'POST', + path: '/api/sco-organization-learners/association', + config: { + handler: scoOrganizationManagementController.reconcileScoOrganizationLearnerManually, + validate: { + options: { + allowUnknown: false, + }, + payload: Joi.object({ + data: { + attributes: { + 'first-name': Joi.string().empty(Joi.string().regex(/^\s*$/)).required(), + 'last-name': Joi.string().empty(Joi.string().regex(/^\s*$/)).required(), + birthdate: Joi.date().format('YYYY-MM-DD').required(), + 'campaign-code': Joi.string().empty(Joi.string().regex(/^\s*$/)).required(), + }, + type: 'sco-organization-learners', + }, + }), + failAction: (request, h) => { + return sendJsonApiError(new UnprocessableEntityError('Un des champs saisis n’est pas valide.'), h); + }, + }, + notes: [ + '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + + '- Elle associe des données saisies par l’utilisateur à l’inscription de l’élève dans cette organisation', + ], + tags: ['api', 'sco-organization-learners'], + }, + }, { method: 'POST', path: '/api/organizations/{id}/sco-organization-learners/import-siecle', diff --git a/api/tests/acceptance/application/sco-organization-learners/sco-organization-learner-controller_test.js b/api/tests/acceptance/application/sco-organization-learners/sco-organization-learner-controller_test.js index a3ef255713b..e17c7356d26 100644 --- a/api/tests/acceptance/application/sco-organization-learners/sco-organization-learner-controller_test.js +++ b/api/tests/acceptance/application/sco-organization-learners/sco-organization-learner-controller_test.js @@ -15,103 +15,6 @@ describe('Acceptance | Controller | sco-organization-learners', function () { server = await createServer(); }); - describe('POST /api/sco-organization-learners/association', function () { - let organization; - let campaign; - let options; - let organizationLearner; - let user; - - beforeEach(async function () { - // given - options = { - method: 'POST', - url: '/api/sco-organization-learners/association', - headers: {}, - payload: {}, - }; - - user = databaseBuilder.factory.buildUser(); - organization = databaseBuilder.factory.buildOrganization({ type: 'SCO' }); - organizationLearner = databaseBuilder.factory.buildOrganizationLearner({ - firstName: 'france', - lastName: 'gall', - birthdate: '2001-01-01', - organizationId: organization.id, - userId: null, - nationalStudentId: 'francegall123', - }); - campaign = databaseBuilder.factory.buildCampaign({ organizationId: organization.id }); - - await databaseBuilder.commit(); - }); - - context('associate user with firstName, lastName and birthdate', function () { - it('should return an 200 status after having successfully associated user to organizationLearner', async function () { - // given - options.headers.authorization = generateValidRequestAuthorizationHeader(user.id); - options.payload.data = { - attributes: { - 'campaign-code': campaign.code, - 'first-name': organizationLearner.firstName, - 'last-name': organizationLearner.lastName, - birthdate: organizationLearner.birthdate, - }, - }; - options.url += '?withReconciliation=true'; - - // when - const response = await server.inject(options); - - // then - expect(response.statusCode).to.equal(200); - }); - - context('when user is not authenticated', function () { - it('should respond with a 401 - unauthorized access', async function () { - // given - options.headers.authorization = 'invalid.access.token'; - - // when - const response = await server.inject(options); - - // then - expect(response.statusCode).to.equal(401); - }); - }); - - context('When withReconciliation query param is set to false', function () { - it('should not reconcile user and return a 204 No Content', async function () { - // given - options.headers.authorization = generateValidRequestAuthorizationHeader(user.id); - options.payload.data = { - attributes: { - 'campaign-code': campaign.code, - 'first-name': organizationLearner.firstName, - 'last-name': organizationLearner.lastName, - birthdate: organizationLearner.birthdate, - }, - }; - options.url += '?withReconciliation=false'; - - // when - const response = await server.inject(options); - - // then - expect(response.statusCode).to.equal(204); - const organizationLearnerInDB = await knex('organization-learners') - .where({ - firstName: organizationLearner.firstName, - lastName: organizationLearner.lastName, - birthdate: organizationLearner.birthdate, - }) - .select(); - expect(organizationLearnerInDB.userId).to.be.undefined; - }); - }); - }); - }); - describe('PUT /api/sco-organization-learners/possibilities', function () { it('returns the organizationLearner linked to the user and a 200 status code response', async function () { //given diff --git a/api/tests/integration/application/sco-organization-learners/index_test.js b/api/tests/integration/application/sco-organization-learners/index_test.js index ef6048f193a..24f4c9b189e 100644 --- a/api/tests/integration/application/sco-organization-learners/index_test.js +++ b/api/tests/integration/application/sco-organization-learners/index_test.js @@ -7,10 +7,6 @@ describe('Integration | Application | Route | sco-organization-learners', functi let httpTestServer; beforeEach(async function () { - sinon - .stub(scoOrganizationLearnerController, 'reconcileScoOrganizationLearnerManually') - .callsFake((request, h) => h.response('ok').code(204)); - sinon .stub(scoOrganizationLearnerController, 'generateUsername') .callsFake((request, h) => h.response('ok').code(200)); @@ -34,190 +30,6 @@ describe('Integration | Application | Route | sco-organization-learners', functi await httpTestServer.register(moduleUnderTest); }); - describe('POST /api/sco-organization-learners/association', function () { - const method = 'POST'; - const url = '/api/sco-organization-learners/association'; - - context('User association with firstName, lastName, birthdate and campaignCode', function () { - it('should succeed', async function () { - // given - const payload = { - data: { - attributes: { - 'first-name': 'Robert', - 'last-name': 'Smith', - birthdate: '2012-12-12', - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(204); - }); - - it('should succeed when there is a space', async function () { - // given - const payload = { - data: { - attributes: { - 'first-name': 'Robert ', - 'last-name': 'Smith', - birthdate: '2012-12-12', - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(204); - expect(response.request.payload.data.attributes['first-name']).to.equal('Robert '); - }); - - it('should return an error when there is no payload', async function () { - // when - const response = await httpTestServer.request(method, url); - - // then - expect(response.statusCode).to.equal(422); - }); - - it('should return an error when there is an invalid first name attribute in the payload', async function () { - // given - const INVALID_FIRSTNAME = ' '; - const payload = { - data: { - attributes: { - 'first-name': INVALID_FIRSTNAME, - 'last-name': 'Smith', - birthdate: '2012-12-12', - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(422); - }); - - it('should return an error when there is an invalid last name attribute in the payload', async function () { - // given - const INVALID_LASTNAME = ''; - const payload = { - data: { - attributes: { - 'first-name': 'Robert', - 'last-name': INVALID_LASTNAME, - birthdate: '2012-12-12', - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(422); - }); - - it('should return an error when there is an invalid a birthdate attribute (with space) in the payload', async function () { - // given - const INVALID_BIRTHDATE = '2012- 12-12'; - - // when - const payload = { - data: { - attributes: { - 'first-name': 'Robert', - 'last-name': 'Smith', - birthdate: INVALID_BIRTHDATE, - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(422); - }); - - it('should return an error when there is an invalid birthdate attribute (with extra zeros) in the payload', async function () { - // given - const INVALID_BIRTHDATE = '2012-012-12'; - const payload = { - data: { - attributes: { - 'first-name': 'Robert', - 'last-name': 'Smith', - birthdate: INVALID_BIRTHDATE, - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(422); - }); - - it('should return an error when there is an invalid birthdate attribute (not a proper date) in the payload', async function () { - // given - const INVALID_BIRTHDATE = '1999-99-99'; - const payload = { - data: { - attributes: { - 'first-name': 'Robert', - 'last-name': 'Smith', - birthdate: INVALID_BIRTHDATE, - 'campaign-code': 'RESTRICTD', - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(422); - }); - - it('should return an error when there is an invalid campaign code attribute in the payload', async function () { - // given - const INVALID_CAMPAIGNCODE = ''; - const payload = { - data: { - attributes: { - 'first-name': 'Robert', - 'last-name': 'Smith', - birthdate: '2012-12-12', - 'campaign-code': INVALID_CAMPAIGNCODE, - }, - }, - }; - - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(422); - }); - }); - }); - describe('PUT /api/sco-organization-learners/possibilities', function () { const method = 'PUT'; const url = '/api/sco-organization-learners/possibilities'; diff --git a/api/tests/prescription/learner-management/acceptance/application/sco-organization-management-route_test.js b/api/tests/prescription/learner-management/acceptance/application/sco-organization-management-route_test.js new file mode 100644 index 00000000000..cd149890916 --- /dev/null +++ b/api/tests/prescription/learner-management/acceptance/application/sco-organization-management-route_test.js @@ -0,0 +1,112 @@ +import { + createServer, + databaseBuilder, + expect, + generateValidRequestAuthorizationHeader, + knex, +} from '../../../../test-helper.js'; + +describe('Acceptance | Route | sco-organization-management-route', function () { + let server; + + beforeEach(async function () { + server = await createServer(); + }); + + describe('POST /api/sco-organization-learners/association', function () { + let organization; + let campaign; + let options; + let organizationLearner; + let user; + + beforeEach(async function () { + // given + options = { + method: 'POST', + url: '/api/sco-organization-learners/association', + headers: {}, + payload: {}, + }; + + user = databaseBuilder.factory.buildUser(); + organization = databaseBuilder.factory.buildOrganization({ type: 'SCO' }); + organizationLearner = databaseBuilder.factory.buildOrganizationLearner({ + firstName: 'france', + lastName: 'gall', + birthdate: '2001-01-01', + organizationId: organization.id, + userId: null, + nationalStudentId: 'francegall123', + }); + campaign = databaseBuilder.factory.buildCampaign({ organizationId: organization.id }); + + await databaseBuilder.commit(); + }); + + context('associate user with firstName, lastName and birthdate', function () { + it('should return an 200 status after having successfully associated user to organizationLearner', async function () { + // given + options.headers.authorization = generateValidRequestAuthorizationHeader(user.id); + options.payload.data = { + attributes: { + 'campaign-code': campaign.code, + 'first-name': organizationLearner.firstName, + 'last-name': organizationLearner.lastName, + birthdate: organizationLearner.birthdate, + }, + }; + options.url += '?withReconciliation=true'; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(200); + }); + + context('when user is not authenticated', function () { + it('should respond with a 401 - unauthorized access', async function () { + // given + options.headers.authorization = 'invalid.access.token'; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(401); + }); + }); + + context('When withReconciliation query param is set to false', function () { + it('should not reconcile user and return a 204 No Content', async function () { + // given + options.headers.authorization = generateValidRequestAuthorizationHeader(user.id); + options.payload.data = { + attributes: { + 'campaign-code': campaign.code, + 'first-name': organizationLearner.firstName, + 'last-name': organizationLearner.lastName, + birthdate: organizationLearner.birthdate, + }, + }; + options.url += '?withReconciliation=false'; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(204); + const organizationLearnerInDB = await knex('organization-learners') + .where({ + firstName: organizationLearner.firstName, + lastName: organizationLearner.lastName, + birthdate: organizationLearner.birthdate, + }) + .select(); + expect(organizationLearnerInDB.userId).to.be.undefined; + }); + }); + }); + }); +}); diff --git a/api/tests/prescription/learner-management/integration/application/sco-organization-management-route_test.js b/api/tests/prescription/learner-management/integration/application/sco-organization-management-route_test.js new file mode 100644 index 00000000000..a72afe34312 --- /dev/null +++ b/api/tests/prescription/learner-management/integration/application/sco-organization-management-route_test.js @@ -0,0 +1,200 @@ +import { scoOrganizationManagementController } from '../../../../../src/prescription/learner-management/application/sco-organization-management-controller.js'; +import * as moduleUnderTest from '../../../../../src/prescription/learner-management/application/sco-organization-management-route.js'; +import { expect, HttpTestServer, sinon } from '../../../../test-helper.js'; + +describe('Integration | Application | Route | sco-organization-learners', function () { + let httpTestServer; + + beforeEach(async function () { + sinon + .stub(scoOrganizationManagementController, 'reconcileScoOrganizationLearnerManually') + .callsFake((request, h) => h.response('ok').code(204)); + + httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + }); + + describe('POST /api/sco-organization-learners/association', function () { + const method = 'POST'; + const url = '/api/sco-organization-learners/association'; + + context('User association with firstName, lastName, birthdate and campaignCode', function () { + it('should succeed', async function () { + // given + const payload = { + data: { + attributes: { + 'first-name': 'Robert', + 'last-name': 'Smith', + birthdate: '2012-12-12', + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(204); + }); + + it('should succeed when there is a space', async function () { + // given + const payload = { + data: { + attributes: { + 'first-name': 'Robert ', + 'last-name': 'Smith', + birthdate: '2012-12-12', + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(204); + expect(response.request.payload.data.attributes['first-name']).to.equal('Robert '); + }); + + it('should return an error when there is no payload', async function () { + // when + const response = await httpTestServer.request(method, url); + + // then + expect(response.statusCode).to.equal(422); + }); + + it('should return an error when there is an invalid first name attribute in the payload', async function () { + // given + const INVALID_FIRSTNAME = ' '; + const payload = { + data: { + attributes: { + 'first-name': INVALID_FIRSTNAME, + 'last-name': 'Smith', + birthdate: '2012-12-12', + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(422); + }); + + it('should return an error when there is an invalid last name attribute in the payload', async function () { + // given + const INVALID_LASTNAME = ''; + const payload = { + data: { + attributes: { + 'first-name': 'Robert', + 'last-name': INVALID_LASTNAME, + birthdate: '2012-12-12', + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(422); + }); + + it('should return an error when there is an invalid a birthdate attribute (with space) in the payload', async function () { + // given + const INVALID_BIRTHDATE = '2012- 12-12'; + + // when + const payload = { + data: { + attributes: { + 'first-name': 'Robert', + 'last-name': 'Smith', + birthdate: INVALID_BIRTHDATE, + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(422); + }); + + it('should return an error when there is an invalid birthdate attribute (with extra zeros) in the payload', async function () { + // given + const INVALID_BIRTHDATE = '2012-012-12'; + const payload = { + data: { + attributes: { + 'first-name': 'Robert', + 'last-name': 'Smith', + birthdate: INVALID_BIRTHDATE, + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(422); + }); + + it('should return an error when there is an invalid birthdate attribute (not a proper date) in the payload', async function () { + // given + const INVALID_BIRTHDATE = '1999-99-99'; + const payload = { + data: { + attributes: { + 'first-name': 'Robert', + 'last-name': 'Smith', + birthdate: INVALID_BIRTHDATE, + 'campaign-code': 'RESTRICTD', + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(422); + }); + + it('should return an error when there is an invalid campaign code attribute in the payload', async function () { + // given + const INVALID_CAMPAIGNCODE = ''; + const payload = { + data: { + attributes: { + 'first-name': 'Robert', + 'last-name': 'Smith', + birthdate: '2012-12-12', + 'campaign-code': INVALID_CAMPAIGNCODE, + }, + }, + }; + + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(422); + }); + }); + }); +});