diff --git a/api/lib/application/cache/cache-controller.js b/api/lib/application/cache/cache-controller.js deleted file mode 100644 index 8d9e4fe74fd..00000000000 --- a/api/lib/application/cache/cache-controller.js +++ /dev/null @@ -1,25 +0,0 @@ -import _ from 'lodash'; - -import { sharedUsecases as usecases } from '../../../src/shared/domain/usecases/index.js'; -import * as LearningContentDatasources from '../../../src/shared/infrastructure/datasources/learning-content/index.js'; - -const refreshCacheEntries = async function (request, h) { - const { userId } = request.auth.credentials; - - await usecases.refreshLearningContentCache({ userId }); - return h.response({}).code(202); -}; - -const refreshCacheEntry = async function (request, h) { - const updatedRecord = request.payload; - const recordId = request.params.id; - const datasource = - // eslint-disable-next-line import/namespace - LearningContentDatasources[_.findKey(LearningContentDatasources, { modelName: request.params.model })]; - await datasource.refreshLearningContentCacheRecord(recordId, updatedRecord); - return h.response().code(204); -}; - -const cacheController = { refreshCacheEntries, refreshCacheEntry }; - -export { cacheController }; diff --git a/api/lib/application/cache/index.js b/api/lib/application/cache/index.js deleted file mode 100644 index 8086d0d581c..00000000000 --- a/api/lib/application/cache/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import { securityPreHandlers } from '../../../src/shared/application/security-pre-handlers.js'; -import { cacheController } from './cache-controller.js'; - -const register = async function (server) { - server.route([ - { - method: 'PATCH', - path: '/api/cache/{model}/{id}', - config: { - pre: [ - { - method: securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, - assign: 'hasRoleSuperAdmin', - }, - ], - handler: cacheController.refreshCacheEntry, - tags: ['api', 'cache'], - notes: [ - 'Cette route est restreinte aux utilisateurs authentifiés avec le rôle Super Admin', - 'Elle permet de mettre à jour une entrée du cache de l’application\n' + - 'Attention : pour un état cohérent des objets stockés en cache, utiliser PATCH /api/cache', - ], - }, - }, - { - method: 'PATCH', - path: '/api/cache', - config: { - pre: [ - { - method: securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, - assign: 'hasRoleSuperAdmin', - }, - ], - handler: cacheController.refreshCacheEntries, - tags: ['api', 'cache'], - notes: [ - 'Cette route est restreinte aux utilisateurs authentifiés avec le rôle Super Admin', - 'Elle permet de précharger les entrées du cache de l’application (les requêtes les plus longues)', - ], - }, - }, - ]); -}; - -const name = 'cache-api'; -export { name, register }; diff --git a/api/lib/routes.js b/api/lib/routes.js index b752e14e9e9..e2f6df7fe5e 100644 --- a/api/lib/routes.js +++ b/api/lib/routes.js @@ -1,6 +1,5 @@ import * as healthcheck from '../src/shared/application/healthcheck/index.js'; import * as authentication from './application/authentication/index.js'; -import * as cache from './application/cache/index.js'; import * as campaignParticipations from './application/campaign-participations/index.js'; import * as certificationCenterInvitations from './application/certification-center-invitations/index.js'; import * as certificationCenterMemberships from './application/certification-center-memberships/index.js'; @@ -22,7 +21,6 @@ import * as users from './application/users/index.js'; const routes = [ authentication, - cache, campaignParticipations, certificationCenters, certificationCenterInvitations, diff --git a/api/src/shared/application/lcms/index.js b/api/src/shared/application/lcms/index.js index 675332c1cdf..08d1f90ae15 100644 --- a/api/src/shared/application/lcms/index.js +++ b/api/src/shared/application/lcms/index.js @@ -21,6 +21,43 @@ const register = async function (server) { ], }, }, + { + method: 'PATCH', + path: '/api/cache/{model}/{id}', + config: { + pre: [ + { + method: securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, + assign: 'hasRoleSuperAdmin', + }, + ], + handler: lcmsController.refreshCacheEntry, + tags: ['api', 'cache'], + notes: [ + 'Cette route est restreinte aux utilisateurs authentifiés avec le rôle Super Admin', + 'Elle permet de mettre à jour une entrée du cache de l’application\n' + + 'Attention : pour un état cohérent des objets stockés en cache, utiliser PATCH /api/cache', + ], + }, + }, + { + method: 'PATCH', + path: '/api/cache', + config: { + pre: [ + { + method: securityPreHandlers.checkAdminMemberHasRoleSuperAdmin, + assign: 'hasRoleSuperAdmin', + }, + ], + handler: lcmsController.refreshCacheEntries, + tags: ['api', 'cache'], + notes: [ + 'Cette route est restreinte aux utilisateurs authentifiés avec le rôle Super Admin', + 'Elle permet de précharger les entrées du cache de l’application (les requêtes les plus longues)', + ], + }, + }, ]); }; diff --git a/api/src/shared/application/lcms/lcms-controller.js b/api/src/shared/application/lcms/lcms-controller.js index 4aac92b8860..2855886158d 100644 --- a/api/src/shared/application/lcms/lcms-controller.js +++ b/api/src/shared/application/lcms/lcms-controller.js @@ -1,4 +1,7 @@ +import _ from 'lodash'; + import { sharedUsecases as usecases } from '../../domain/usecases/index.js'; +import * as LearningContentDatasources from '../../infrastructure/datasources/learning-content/index.js'; import { logger } from '../../infrastructure/utils/logger.js'; const createRelease = async function (request, h) { @@ -13,6 +16,22 @@ const createRelease = async function (request, h) { return h.response({}).code(204); }; -const lcmsController = { createRelease }; +const refreshCacheEntries = async function (request, h) { + const { userId } = request.auth.credentials; + + await usecases.refreshLearningContentCache({ userId }); + return h.response({}).code(202); +}; + +const refreshCacheEntry = async function (request, h) { + const updatedRecord = request.payload; + const recordId = request.params.id; + const datasource = + LearningContentDatasources[_.findKey(LearningContentDatasources, { modelName: request.params.model })]; + await datasource.refreshLearningContentCacheRecord(recordId, updatedRecord); + return h.response().code(204); +}; + +const lcmsController = { createRelease, refreshCacheEntries, refreshCacheEntry }; export { lcmsController }; diff --git a/api/src/shared/domain/usecases/index.js b/api/src/shared/domain/usecases/index.js index be1ea2fb625..46d45370e13 100644 --- a/api/src/shared/domain/usecases/index.js +++ b/api/src/shared/domain/usecases/index.js @@ -3,9 +3,9 @@ import { fileURLToPath } from 'node:url'; import * as complementaryCertificationBadgeRepository from '../../../certification/complementary-certification/infrastructure/repositories/complementary-certification-badge-repository.js'; import * as badgeRepository from '../../../evaluation/infrastructure/repositories/badge-repository.js'; -import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js'; -import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; import { lcmsRefreshCacheJobRepository } from '../../infrastructure/repositories/jobs/lcms-refresh-cache-job-repository.js'; +import { injectDependencies } from '../../infrastructure/utils/dependency-injection.js'; +import { importNamedExportsFromDirectory } from '../../infrastructure/utils/import-named-exports-from-directory.js'; const path = dirname(fileURLToPath(import.meta.url)); diff --git a/api/tests/acceptance/application/cache/cache-controller_test.js b/api/tests/shared/acceptance/application/lcms/lcms-controller_test.js similarity index 92% rename from api/tests/acceptance/application/cache/cache-controller_test.js rename to api/tests/shared/acceptance/application/lcms/lcms-controller_test.js index de0f28f4df8..61e2c7fa170 100644 --- a/api/tests/acceptance/application/cache/cache-controller_test.js +++ b/api/tests/shared/acceptance/application/lcms/lcms-controller_test.js @@ -1,18 +1,18 @@ import Redis from 'ioredis'; -import { PIX_ADMIN } from '../../../../src/authorization/domain/constants.js'; -import { LearningContentCache } from '../../../../src/shared/infrastructure/caches/learning-content-cache.js'; +import { PIX_ADMIN } from '../../../../../src/authorization/domain/constants.js'; +import { LearningContentCache } from '../../../../../src/shared/infrastructure/caches/learning-content-cache.js'; import { createServer, databaseBuilder, expect, generateValidRequestAuthorizationHeader, mockLearningContent, -} from '../../../test-helper.js'; +} from '../../../../test-helper.js'; const { ROLES } = PIX_ADMIN; -describe('Acceptance | Controller | cache-controller', function () { +describe('Acceptance | Controller | lcms-controller', function () { let server; beforeEach(async function () { diff --git a/api/tests/shared/unit/application/lcms/index_test.js b/api/tests/shared/unit/application/lcms/index_test.js index 07288cb7079..c5b905729c6 100644 --- a/api/tests/shared/unit/application/lcms/index_test.js +++ b/api/tests/shared/unit/application/lcms/index_test.js @@ -8,8 +8,9 @@ describe('Unit | Router | lcms-router', function () { beforeEach(function () { sinon.stub(securityPreHandlers, 'checkAdminMemberHasRoleSuperAdmin').callsFake((request, h) => h.response(true)); + sinon.stub(lcmsController, 'refreshCacheEntry').callsFake((request, h) => h.response().code(204)); sinon.stub(lcmsController, 'createRelease').callsFake((request, h) => h.response().code(204)); - + sinon.stub(lcmsController, 'refreshCacheEntries').callsFake((request, h) => h.response().code(204)); httpTestServer = new HttpTestServer(); httpTestServer.register(moduleUnderTest); }); @@ -24,4 +25,27 @@ describe('Unit | Router | lcms-router', function () { expect(lcmsController.createRelease).to.have.been.called; }); }); + + describe('PATCH /api/cache/{model}/{id}', function () { + it('should exist', async function () { + //given + const updatedRecord = { id: 'recId', param: 'updatedValue' }; + + // when + const response = await httpTestServer.request('PATCH', '/api/cache/table/recXYZ1234', updatedRecord); + + // then + expect(response.statusCode).to.equal(204); + }); + }); + + describe('PATCH /api/cache', function () { + it('should exist', async function () { + // when + const response = await httpTestServer.request('PATCH', '/api/cache'); + + // then + expect(response.statusCode).to.equal(204); + }); + }); }); diff --git a/api/tests/shared/unit/application/lcms/lcms-controller_test.js b/api/tests/shared/unit/application/lcms/lcms-controller_test.js index 8f1fd205ab3..ca783c797d8 100644 --- a/api/tests/shared/unit/application/lcms/lcms-controller_test.js +++ b/api/tests/shared/unit/application/lcms/lcms-controller_test.js @@ -1,5 +1,6 @@ import { lcmsController } from '../../../../../src/shared/application/lcms/lcms-controller.js'; import { sharedUsecases as usecases } from '../../../../../src/shared/domain/usecases/index.js'; +import * as learningContentDatasources from '../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; import { expect, hFake, sinon } from '../../../../test-helper.js'; describe('Unit | Controller | lcms-controller', function () { @@ -16,4 +17,104 @@ describe('Unit | Controller | lcms-controller', function () { expect(usecases.createLcmsRelease).to.have.been.called; }); }); + + describe('#refreshCacheEntry', function () { + const request = { + params: { + model: 'challenges', + id: 'recId', + }, + payload: { + property: 'updatedValue', + }, + }; + + for (const entity of [ + 'area', + 'challenge', + 'competence', + 'course', + 'framework', + 'skill', + 'thematic', + 'tube', + 'tutorial', + ]) { + it(`should reply 204 when patching ${entity}`, async function () { + // given + + // eslint-disable-next-line import/namespace + sinon.stub(learningContentDatasources[`${entity}Datasource`], 'refreshLearningContentCacheRecord').resolves(); + const request = { + params: { + model: `${entity}s`, + id: 'recId', + }, + payload: { + property: 'updatedValue', + }, + }; + + // when + const response = await lcmsController.refreshCacheEntry(request, hFake); + + // then + expect(response.statusCode).to.equal(204); + }); + } + + it('should reply with null when the cache key exists', async function () { + // given + sinon.stub(learningContentDatasources.challengeDatasource, 'refreshLearningContentCacheRecord').resolves(); + + // when + const response = await lcmsController.refreshCacheEntry(request, hFake); + + // then + expect( + learningContentDatasources.challengeDatasource.refreshLearningContentCacheRecord, + ).to.have.been.calledWithExactly('recId', { property: 'updatedValue' }); + expect(response.statusCode).to.equal(204); + }); + + it('should reply with null when the cache key does not exist', async function () { + // given + sinon.stub(learningContentDatasources.challengeDatasource, 'refreshLearningContentCacheRecord').resolves(); + + // when + const response = await lcmsController.refreshCacheEntry(request, hFake); + + // Then + expect( + learningContentDatasources.challengeDatasource.refreshLearningContentCacheRecord, + ).to.have.been.calledWithExactly('recId', { property: 'updatedValue' }); + expect(response.statusCode).to.equal(204); + }); + }); + + describe('#refreshCacheEntries', function () { + context('nominal case', function () { + it('should reply with http status 202', async function () { + // given + sinon.stub(usecases, 'refreshLearningContentCache').resolves(); + + // when + const response = await lcmsController.refreshCacheEntries( + { + auth: { + credentials: { + userId: 123, + }, + }, + }, + hFake, + ); + + // then + expect(usecases.refreshLearningContentCache).to.have.been.calledOnce; + expect(usecases.refreshLearningContentCache).to.have.been.calledWithExactly({ userId: 123 }); + expect(response.statusCode).to.equal(202); + }); + }); + }); }); diff --git a/api/tests/unit/application/cache/cache-controller_test.js b/api/tests/unit/application/cache/cache-controller_test.js deleted file mode 100644 index b0b48d1b15d..00000000000 --- a/api/tests/unit/application/cache/cache-controller_test.js +++ /dev/null @@ -1,106 +0,0 @@ -import { cacheController } from '../../../../lib/application/cache/cache-controller.js'; -import { sharedUsecases as usecases } from '../../../../src/shared/domain/usecases/index.js'; -import * as learningContentDatasources from '../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, hFake, sinon } from '../../../test-helper.js'; - -describe('Unit | Controller | cache-controller', function () { - describe('#refreshCacheEntry', function () { - const request = { - params: { - model: 'challenges', - id: 'recId', - }, - payload: { - property: 'updatedValue', - }, - }; - - for (const entity of [ - 'area', - 'challenge', - 'competence', - 'course', - 'framework', - 'skill', - 'thematic', - 'tube', - 'tutorial', - ]) { - it(`should reply 204 when patching ${entity}`, async function () { - // given - - // eslint-disable-next-line import/namespace - sinon.stub(learningContentDatasources[`${entity}Datasource`], 'refreshLearningContentCacheRecord').resolves(); - const request = { - params: { - model: `${entity}s`, - id: 'recId', - }, - payload: { - property: 'updatedValue', - }, - }; - - // when - const response = await cacheController.refreshCacheEntry(request, hFake); - - // then - expect(response.statusCode).to.equal(204); - }); - } - - it('should reply with null when the cache key exists', async function () { - // given - sinon.stub(learningContentDatasources.challengeDatasource, 'refreshLearningContentCacheRecord').resolves(); - - // when - const response = await cacheController.refreshCacheEntry(request, hFake); - - // then - expect( - learningContentDatasources.challengeDatasource.refreshLearningContentCacheRecord, - ).to.have.been.calledWithExactly('recId', { property: 'updatedValue' }); - expect(response.statusCode).to.equal(204); - }); - - it('should reply with null when the cache key does not exist', async function () { - // given - sinon.stub(learningContentDatasources.challengeDatasource, 'refreshLearningContentCacheRecord').resolves(); - - // when - const response = await cacheController.refreshCacheEntry(request, hFake); - - // Then - expect( - learningContentDatasources.challengeDatasource.refreshLearningContentCacheRecord, - ).to.have.been.calledWithExactly('recId', { property: 'updatedValue' }); - expect(response.statusCode).to.equal(204); - }); - }); - - describe('#refreshCacheEntries', function () { - context('nominal case', function () { - it('should reply with http status 202', async function () { - // given - sinon.stub(usecases, 'refreshLearningContentCache').resolves(); - - // when - const response = await cacheController.refreshCacheEntries( - { - auth: { - credentials: { - userId: 123, - }, - }, - }, - hFake, - ); - - // then - expect(usecases.refreshLearningContentCache).to.have.been.calledOnce; - expect(usecases.refreshLearningContentCache).to.have.been.calledWithExactly({ userId: 123 }); - expect(response.statusCode).to.equal(202); - }); - }); - }); -}); diff --git a/api/tests/unit/application/cache/index_test.js b/api/tests/unit/application/cache/index_test.js deleted file mode 100644 index b1ad6ee466b..00000000000 --- a/api/tests/unit/application/cache/index_test.js +++ /dev/null @@ -1,40 +0,0 @@ -import { cacheController } from '../../../../lib/application/cache/cache-controller.js'; -import * as moduleUnderTest from '../../../../lib/application/cache/index.js'; -import { securityPreHandlers } from '../../../../src/shared/application/security-pre-handlers.js'; -import { expect, HttpTestServer, sinon } from '../../../test-helper.js'; - -describe('Unit | Router | cache-router', function () { - describe('PATCH /api/cache/{model}/{id}', function () { - it('should exist', async function () { - //given - sinon.stub(securityPreHandlers, 'checkAdminMemberHasRoleSuperAdmin').callsFake((request, h) => h.response(true)); - sinon.stub(cacheController, 'refreshCacheEntry').callsFake((request, h) => h.response().code(204)); - const httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - - const updatedRecord = { id: 'recId', param: 'updatedValue' }; - - // when - const response = await httpTestServer.request('PATCH', '/api/cache/table/recXYZ1234', updatedRecord); - - // then - expect(response.statusCode).to.equal(204); - }); - }); - - describe('PATCH /api/cache', function () { - it('should exist', async function () { - //given - sinon.stub(securityPreHandlers, 'checkAdminMemberHasRoleSuperAdmin').callsFake((request, h) => h.response(true)); - sinon.stub(cacheController, 'refreshCacheEntries').callsFake((request, h) => h.response().code(204)); - const httpTestServer = new HttpTestServer(); - await httpTestServer.register(moduleUnderTest); - - // when - const response = await httpTestServer.request('PATCH', '/api/cache'); - - // then - expect(response.statusCode).to.equal(204); - }); - }); -});