diff --git a/api/lib/application/target-profiles/index.js b/api/lib/application/target-profiles/index.js index 408223e9306..bd35f6752f1 100644 --- a/api/lib/application/target-profiles/index.js +++ b/api/lib/application/target-profiles/index.js @@ -34,54 +34,6 @@ const register = async function (server) { ], }, }, - { - method: 'PATCH', - path: '/api/admin/target-profiles/{id}', - config: { - pre: [ - { - method: (request, h) => - securityPreHandlers.hasAtLeastOneAccessOf([ - securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, - securityPreHandlers.checkAdminMemberHasRoleSupport, - securityPreHandlers.checkAdminMemberHasRoleMetier, - ])(request, h), - assign: 'hasAuthorizationToAccessAdminScope', - }, - ], - validate: { - params: Joi.object({ - id: identifiersType.targetProfileId, - }), - payload: Joi.object({ - data: { - attributes: { - 'are-knowledge-elements-resettable': Joi.boolean(), - category: Joi.string(), - comment: Joi.string().allow(null).max(500), - description: Joi.string().allow(null).max(500), - 'image-url': Joi.string().uri().allow(null), - name: Joi.string(), - tubes: Joi.array() - .optional() - .items( - Joi.object({ - id: Joi.string(), - level: Joi.number(), - }), - ), - }, - }, - }), - }, - handler: targetProfileController.updateTargetProfile, - tags: ['api', 'admin', 'target-profiles'], - notes: [ - "- **Cette route est restreinte aux utilisateurs authentifiés ayant les droits d'accès**\n" + - "- Elle permet de mettre à jour les attributs d'un profil cible", - ], - }, - }, ]); }; diff --git a/api/src/prescription/target-profile/application/admin-target-profile-controller.js b/api/src/prescription/target-profile/application/admin-target-profile-controller.js index 52e5ec4234e..35e948e24d6 100644 --- a/api/src/prescription/target-profile/application/admin-target-profile-controller.js +++ b/api/src/prescription/target-profile/application/admin-target-profile-controller.js @@ -21,12 +21,16 @@ const getTargetProfileForAdmin = async function (request, h, dependencies = { ta return dependencies.targetProfileForAdminSerializer.serialize({ targetProfile, filter }); }; -const updateTargetProfile = async function (request, h, dependencies = { usecases, targetProfileSerializer }) { - const targetProfileId = request.params.id; +const updateTargetProfile = async function ( + request, + h, + dependencies = { prescriptionTargetProfileUsecases, targetProfileSerializer }, +) { + const targetProfileId = request.params.targetProfileId; const attributesToUpdate = dependencies.targetProfileSerializer.deserialize(request.payload); await DomainTransaction.execute(async () => { - await dependencies.usecases.updateTargetProfile({ + await dependencies.prescriptionTargetProfileUsecases.updateTargetProfile({ id: targetProfileId, attributesToUpdate, }); diff --git a/api/src/prescription/target-profile/application/admin-target-profile-route.js b/api/src/prescription/target-profile/application/admin-target-profile-route.js index 9cc47729bd1..27b5a58fc30 100644 --- a/api/src/prescription/target-profile/application/admin-target-profile-route.js +++ b/api/src/prescription/target-profile/application/admin-target-profile-route.js @@ -478,6 +478,54 @@ const register = async function (server) { ], }, }, + { + method: 'PATCH', + path: '/api/admin/target-profiles/{targetProfileId}', + config: { + pre: [ + { + method: (request, h) => + securityPreHandlers.hasAtLeastOneAccessOf([ + securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, + securityPreHandlers.checkAdminMemberHasRoleSupport, + securityPreHandlers.checkAdminMemberHasRoleMetier, + ])(request, h), + assign: 'hasAuthorizationToAccessAdminScope', + }, + ], + validate: { + params: Joi.object({ + targetProfileId: identifiersType.targetProfileId, + }), + payload: Joi.object({ + data: { + attributes: { + 'are-knowledge-elements-resettable': Joi.boolean(), + category: Joi.string(), + comment: Joi.string().allow(null).max(500), + description: Joi.string().allow(null).max(500), + 'image-url': Joi.string().uri().allow(null), + name: Joi.string(), + tubes: Joi.array() + .optional() + .items( + Joi.object({ + id: Joi.string(), + level: Joi.number(), + }), + ), + }, + }, + }), + }, + handler: targetProfileController.updateTargetProfile, + tags: ['api', 'admin', 'target-profiles'], + notes: [ + "- **Cette route est restreinte aux utilisateurs authentifiés ayant les droits d'accès**\n" + + "- Elle permet de mettre à jour les attributs d'un profil cible", + ], + }, + }, ]); }; diff --git a/api/tests/acceptance/application/target-profiles/index_test.js b/api/tests/acceptance/application/target-profiles/index_test.js index ba9ab0d46fa..997aeae719a 100644 --- a/api/tests/acceptance/application/target-profiles/index_test.js +++ b/api/tests/acceptance/application/target-profiles/index_test.js @@ -3,131 +3,15 @@ import { databaseBuilder, expect, generateValidRequestAuthorizationHeader, - mockLearningContent, } from '../../../test-helper.js'; describe('Acceptance | Route | target-profiles', function () { let server; - const skillId = 'recArea1_Competence1_Tube1_Skill1'; - const tubeId = 'recArea1_Competence1_Tube1'; - - const learningContent = { - areas: [{ id: 'recArea1', competenceIds: ['recArea1_Competence1'] }], - competences: [ - { - id: 'recArea1_Competence1', - areaId: 'recArea1', - skillIds: [skillId], - origin: 'Pix', - }, - ], - thematics: [], - tubes: [ - { - id: 'recArea1_Competence1_Tube1', - competenceId: 'recArea1_Competence1', - }, - ], - skills: [ - { - id: skillId, - name: '@recArea1_Competence1_Tube1_Skill1', - status: 'actif', - tubeId: 'recArea1_Competence1_Tube1', - competenceId: 'recArea1_Competence1', - }, - ], - challenges: [ - { - id: 'recArea1_Competence1_Tube1_Skill1_Challenge1', - skillId: skillId, - competenceId: 'recArea1_Competence1', - status: 'validé', - locales: ['fr-fr'], - }, - ], - }; - beforeEach(async function () { server = await createServer(); }); - describe('PATCH /api/admin/target-profiles/{id}', function () { - beforeEach(async function () { - mockLearningContent(learningContent); - }); - - describe('when there is no tube to update', function () { - it('should return 204', async function () { - const targetProfile = databaseBuilder.factory.buildTargetProfile(); - const user = databaseBuilder.factory.buildUser.withRole(); - await databaseBuilder.commit(); - - const options = { - method: 'PATCH', - url: `/api/admin/target-profiles/${targetProfile.id}`, - headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, - payload: { - data: { - attributes: { - name: 'CoolPixer', - description: 'Amazing description', - comment: 'Amazing comment', - category: 'OTHER', - 'image-url': 'http://valid-uri.com/image.png', - 'are-knowledge-elements-resettable': false, - }, - }, - }, - }; - - // when - const response = await server.inject(options); - - // then - expect(response.statusCode).to.equal(204); - }); - }); - - describe('when there is some tube update and the target profile is not linked with campaign', function () { - it('should return 204', async function () { - const targetProfile = databaseBuilder.factory.buildTargetProfile(); - const targetProfileTube = databaseBuilder.factory.buildTargetProfileTube({ - targetProfileId: targetProfile.id, - tubeId, - level: 1, - }); - const user = databaseBuilder.factory.buildUser.withRole(); - await databaseBuilder.commit(); - - const options = { - method: 'PATCH', - url: `/api/admin/target-profiles/${targetProfile.id}`, - headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, - payload: { - data: { - attributes: { - name: 'nom changé', - category: 'COMPETENCES', - description: 'description changée.', - comment: 'commentaire changé.', - 'image-url': null, - tubes: [{ id: targetProfileTube.tubeId, level: 99 }], - }, - }, - }, - }; - - // when - const response = await server.inject(options); - - // then - expect(response.statusCode).to.equal(204); - }); - }); - }); - describe('GET /api/admin/target-profiles/{id}/training-summaries', function () { let user; let targetProfileId; diff --git a/api/tests/prescription/target-profile/acceptance/application/admin-target-profile-route_test.js b/api/tests/prescription/target-profile/acceptance/application/admin-target-profile-route_test.js index 7d13ff35a64..557825f026f 100644 --- a/api/tests/prescription/target-profile/acceptance/application/admin-target-profile-route_test.js +++ b/api/tests/prescription/target-profile/acceptance/application/admin-target-profile-route_test.js @@ -17,6 +17,121 @@ describe('Acceptance | TargetProfile | Application | Route | admin-target-profil server = await createServer(); }); + describe('PATCH /api/admin/target-profiles/{targetProfileId}', function () { + const skillId = 'recArea1_Competence1_Tube1_Skill1'; + const tubeId = 'recArea1_Competence1_Tube1'; + + const learningContent = { + areas: [{ id: 'recArea1', competenceIds: ['recArea1_Competence1'] }], + competences: [ + { + id: 'recArea1_Competence1', + areaId: 'recArea1', + skillIds: [skillId], + origin: 'Pix', + }, + ], + thematics: [], + tubes: [ + { + id: 'recArea1_Competence1_Tube1', + competenceId: 'recArea1_Competence1', + }, + ], + skills: [ + { + id: skillId, + name: '@recArea1_Competence1_Tube1_Skill1', + status: 'actif', + tubeId: 'recArea1_Competence1_Tube1', + competenceId: 'recArea1_Competence1', + }, + ], + challenges: [ + { + id: 'recArea1_Competence1_Tube1_Skill1_Challenge1', + skillId: skillId, + competenceId: 'recArea1_Competence1', + status: 'validé', + locales: ['fr-fr'], + }, + ], + }; + + beforeEach(async function () { + mockLearningContent(learningContent); + }); + + describe('when there is no tube to update', function () { + it('should return 204', async function () { + const targetProfile = databaseBuilder.factory.buildTargetProfile(); + const user = databaseBuilder.factory.buildUser.withRole(); + await databaseBuilder.commit(); + + const options = { + method: 'PATCH', + url: `/api/admin/target-profiles/${targetProfile.id}`, + headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, + payload: { + data: { + attributes: { + name: 'CoolPixer', + description: 'Amazing description', + comment: 'Amazing comment', + category: 'OTHER', + 'image-url': 'http://valid-uri.com/image.png', + 'are-knowledge-elements-resettable': false, + }, + }, + }, + }; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(204); + }); + }); + + describe('when there is some tube update and the target profile is not linked with campaign', function () { + it('should return 204', async function () { + const targetProfile = databaseBuilder.factory.buildTargetProfile(); + const targetProfileTube = databaseBuilder.factory.buildTargetProfileTube({ + targetProfileId: targetProfile.id, + tubeId, + level: 1, + }); + const user = databaseBuilder.factory.buildUser.withRole(); + await databaseBuilder.commit(); + + const options = { + method: 'PATCH', + url: `/api/admin/target-profiles/${targetProfile.id}`, + headers: { authorization: generateValidRequestAuthorizationHeader(user.id) }, + payload: { + data: { + attributes: { + name: 'nom changé', + category: 'COMPETENCES', + description: 'description changée.', + comment: 'commentaire changé.', + 'image-url': null, + tubes: [{ id: targetProfileTube.tubeId, level: 99 }], + }, + }, + }, + }; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(204); + }); + }); + }); + describe('GET /api/admin/target-profiles/{id}', function () { let user; const skillId = 'recArea1_Competence1_Tube1_Skill1'; diff --git a/api/tests/prescription/target-profile/unit/application/admin-target-profile-controller_test.js b/api/tests/prescription/target-profile/unit/application/admin-target-profile-controller_test.js index 0d22a7b5003..8a8f9afe570 100644 --- a/api/tests/prescription/target-profile/unit/application/admin-target-profile-controller_test.js +++ b/api/tests/prescription/target-profile/unit/application/admin-target-profile-controller_test.js @@ -22,7 +22,7 @@ describe('Unit | Controller | admin-target-profile-controller', function () { const request = { params: { - id: 123, + targetProfileId: 123, }, payload, }; @@ -30,7 +30,7 @@ describe('Unit | Controller | admin-target-profile-controller', function () { const attributesToUpdate = Symbol('deserialized attributes to update'); const dependencies = { - usecases: { + prescriptionTargetProfileUsecases: { updateTargetProfile: sinon.stub(), }, targetProfileSerializer: { @@ -45,8 +45,8 @@ describe('Unit | Controller | admin-target-profile-controller', function () { // then expect(response.statusCode).to.equal(204); expect(dependencies.targetProfileSerializer.deserialize).to.have.been.calledOnceWith(payload); - expect(dependencies.usecases.updateTargetProfile).to.have.been.calledOnceWithExactly({ - id: request.params.id, + expect(dependencies.prescriptionTargetProfileUsecases.updateTargetProfile).to.have.been.calledOnceWithExactly({ + id: request.params.targetProfileId, attributesToUpdate, }); }); diff --git a/api/tests/prescription/target-profile/unit/application/admin-target-profiles-route_test.js b/api/tests/prescription/target-profile/unit/application/admin-target-profiles-route_test.js index a1092e5a579..18e0265f334 100644 --- a/api/tests/prescription/target-profile/unit/application/admin-target-profiles-route_test.js +++ b/api/tests/prescription/target-profile/unit/application/admin-target-profiles-route_test.js @@ -11,6 +11,181 @@ describe('Unit | Application | Admin Target Profiles | Routes', function () { sinon.stub(securityPreHandlers, 'checkAdminMemberHasRoleMetier'); }); + describe('PATCH /api/admin/target-profiles/{targetProfileId}', function () { + const method = 'PATCH'; + const url = '/api/admin/target-profiles/123'; + const payload = { + data: { + attributes: { + 'are-knowledge-elements-resettable': false, + category: 'OTHER', + comment: 'commentaire changé.', + description: 'description changée.', + 'image-url': 'http://some-image.url', + name: 'test', + tubes: [{ id: 'some-id', level: 1 }], + }, + }, + }; + + context('when user has role "SUPER_ADMIN", "SUPPORT" or "METIER"', function () { + let httpTestServer; + + beforeEach(async function () { + // given + sinon + .stub(securityPreHandlers, 'hasAtLeastOneAccessOf') + .withArgs([ + securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, + securityPreHandlers.checkAdminMemberHasRoleSupport, + securityPreHandlers.checkAdminMemberHasRoleMetier, + ]) + .callsFake(() => (request, h) => h.response(true)); + sinon + .stub(targetProfileController, 'updateTargetProfile') + .callsFake((request, h) => h.response('ok').code(204)); + httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + }); + + it('should return a response with an HTTP status code 204', async function () { + // when + const response = await httpTestServer.request(method, url, payload); + + // then + expect(response.statusCode).to.equal(204); + }); + + context('payload validation', function () { + it('[are-knowledge-elements-resettable] should return a 400 error when it is not a boolean value', async function () { + // when + const response = await httpTestServer.request(method, url, { + data: { + attributes: { + 'are-knowledge-elements-resettable': String('not a boolean value'), + }, + }, + }); + + // then + expect(response.statusCode).to.equal(400); + }); + + it('[category] should return a 400 error when it is not a string', async function () { + // when + const response = await httpTestServer.request(method, url, { + data: { + attributes: { + category: 404, + }, + }, + }); + + // then + expect(response.statusCode).to.equal(400); + }); + + it('[comment] should return a 400 error when it has more than 500 characters', async function () { + // when + const response = await httpTestServer.request(method, url, { + data: { + attributes: { + comment: String('abcdef').repeat(100), + }, + }, + }); + + // then + expect(response.statusCode).to.equal(400); + }); + + it('[description] should return a 400 error when it has more than 500 characters', async function () { + // when + const { statusCode } = await httpTestServer.request(method, url, { + data: { + attributes: { + description: String('abcdef').repeat(100), + }, + }, + }); + + // then + expect(statusCode).to.equal(400); + }); + + it('[image-url] should return a 400 error when it is not a valid URI', async function () { + // when + const response = await httpTestServer.request(method, url, { + data: { + attributes: { + 'image-url': String('not-a-valid-URI.org'), + }, + }, + }); + + // then + expect(response.statusCode).to.equal(400); + }); + + it('[name] should return a 400 error when it is not a string', async function () { + // when + const response = await httpTestServer.request(method, url, { + data: { + attributes: { + name: 404, + }, + }, + }); + + // then + expect(response.statusCode).to.equal(400); + }); + + it('[tubes] should return a 400 error when it is not an array of accepted object', async function () { + // when + const response = await httpTestServer.request(method, url, { + data: { + attributes: { + tubes: [{ id: 'some-id', level: NaN }], + }, + }, + }); + + // then + expect(response.statusCode).to.equal(400); + }); + }); + }); + + context('when user has role "CERTIF"', function () { + it('should return a response with an HTTP status code 403', async function () { + // given + sinon + .stub(securityPreHandlers, 'hasAtLeastOneAccessOf') + .withArgs([ + securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, + securityPreHandlers.checkAdminMemberHasRoleSupport, + securityPreHandlers.checkAdminMemberHasRoleMetier, + ]) + .callsFake( + () => (request, h) => + h + .response({ errors: new Error('forbidden') }) + .code(403) + .takeover(), + ); + const httpTestServer = new HttpTestServer(); + await httpTestServer.register(moduleUnderTest); + + // when + const { statusCode } = await httpTestServer.request(method, url, payload); + + // then + expect(statusCode).to.equal(403); + }); + }); + }); + describe('GET /api/admin/target-profiles/{id}', function () { const method = 'GET'; const url = '/api/admin/target-profiles/1'; diff --git a/api/tests/unit/application/target-profiles/index_test.js b/api/tests/unit/application/target-profiles/index_test.js index de4057050fa..44c224c2746 100644 --- a/api/tests/unit/application/target-profiles/index_test.js +++ b/api/tests/unit/application/target-profiles/index_test.js @@ -4,181 +4,6 @@ import { securityPreHandlers } from '../../../../src/shared/application/security import { expect, HttpTestServer, sinon } from '../../../test-helper.js'; describe('Unit | Application | Target Profiles | Routes', function () { - describe('PATCH /api/admin/target-profiles/{id}', function () { - const method = 'PATCH'; - const url = '/api/admin/target-profiles/123'; - const payload = { - data: { - attributes: { - 'are-knowledge-elements-resettable': false, - category: 'OTHER', - comment: 'commentaire changé.', - description: 'description changée.', - 'image-url': 'http://some-image.url', - name: 'test', - tubes: [{ id: 'some-id', level: 1 }], - }, - }, - }; - - context('when user has role "SUPER_ADMIN", "SUPPORT" or "METIER"', function () { - let httpTestServer; - - beforeEach(async function () { - // given - sinon - .stub(securityPreHandlers, 'hasAtLeastOneAccessOf') - .withArgs([ - securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, - securityPreHandlers.checkAdminMemberHasRoleSupport, - securityPreHandlers.checkAdminMemberHasRoleMetier, - ]) - .callsFake(() => (request, h) => h.response(true)); - sinon - .stub(targetProfileController, 'updateTargetProfile') - .callsFake((request, h) => h.response('ok').code(204)); - httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - }); - - it('should return a response with an HTTP status code 204', async function () { - // when - const response = await httpTestServer.request(method, url, payload); - - // then - expect(response.statusCode).to.equal(204); - }); - - context('payload validation', function () { - it('[are-knowledge-elements-resettable] should return a 400 error when it is not a boolean value', async function () { - // when - const response = await httpTestServer.request(method, url, { - data: { - attributes: { - 'are-knowledge-elements-resettable': String('not a boolean value'), - }, - }, - }); - - // then - expect(response.statusCode).to.equal(400); - }); - - it('[category] should return a 400 error when it is not a string', async function () { - // when - const response = await httpTestServer.request(method, url, { - data: { - attributes: { - category: 404, - }, - }, - }); - - // then - expect(response.statusCode).to.equal(400); - }); - - it('[comment] should return a 400 error when it has more than 500 characters', async function () { - // when - const response = await httpTestServer.request(method, url, { - data: { - attributes: { - comment: String('abcdef').repeat(100), - }, - }, - }); - - // then - expect(response.statusCode).to.equal(400); - }); - - it('[description] should return a 400 error when it has more than 500 characters', async function () { - // when - const { statusCode } = await httpTestServer.request(method, url, { - data: { - attributes: { - description: String('abcdef').repeat(100), - }, - }, - }); - - // then - expect(statusCode).to.equal(400); - }); - - it('[image-url] should return a 400 error when it is not a valid URI', async function () { - // when - const response = await httpTestServer.request(method, url, { - data: { - attributes: { - 'image-url': String('not-a-valid-URI.org'), - }, - }, - }); - - // then - expect(response.statusCode).to.equal(400); - }); - - it('[name] should return a 400 error when it is not a string', async function () { - // when - const response = await httpTestServer.request(method, url, { - data: { - attributes: { - name: 404, - }, - }, - }); - - // then - expect(response.statusCode).to.equal(400); - }); - - it('[tubes] should return a 400 error when it is not an array of accepted object', async function () { - // when - const response = await httpTestServer.request(method, url, { - data: { - attributes: { - tubes: [{ id: 'some-id', level: NaN }], - }, - }, - }); - - // then - expect(response.statusCode).to.equal(400); - }); - }); - }); - - context('when user has role "CERTIF"', function () { - it('should return a response with an HTTP status code 403', async function () { - // given - sinon - .stub(securityPreHandlers, 'hasAtLeastOneAccessOf') - .withArgs([ - securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, - securityPreHandlers.checkAdminMemberHasRoleSupport, - securityPreHandlers.checkAdminMemberHasRoleMetier, - ]) - .callsFake( - () => (request, h) => - h - .response({ errors: new Error('forbidden') }) - .code(403) - .takeover(), - ); - const httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - - // when - const { statusCode } = await httpTestServer.request(method, url, payload); - - // then - expect(statusCode).to.equal(403); - }); - }); - }); - describe('GET /api/admin/target-profiles/{id}/training-summaries', function () { const method = 'GET'; const url = '/api/admin/target-profiles/1/training-summaries';