From 68669046cc368f7657702d35ec4bf1fd78f77abf Mon Sep 17 00:00:00 2001 From: Mathieu Gilet Date: Fri, 10 Jan 2025 14:59:49 +0100 Subject: [PATCH 1/7] feat(banner): add information banners stored in Redis --- api/server.js | 2 + .../banner/application/banner-controller.js | 16 ++++ api/src/banner/application/banner-route.js | 17 ++++ .../domain/models/information-banner.js | 21 +++++ .../domain/usecases/get-information-banner.js | 5 ++ api/src/banner/domain/usecases/index.js | 33 ++++++++ .../information-banner-repository.js | 15 ++++ .../jsonapi/information-banner-serializer.js | 16 ++++ api/src/banner/routes.js | 3 + .../application/banner-route_test.js | 78 +++++++++++++++++++ .../information-banner-repository_test.js | 40 ++++++++++ .../information-banner-serializer_test.js | 50 ++++++++++++ .../usecases/get-information-banner_test.js | 18 +++++ .../banner/build-banner-information.js | 11 +++ .../tooling/domain-builder/factory/index.js | 9 ++- 15 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 api/src/banner/application/banner-controller.js create mode 100644 api/src/banner/application/banner-route.js create mode 100644 api/src/banner/domain/models/information-banner.js create mode 100644 api/src/banner/domain/usecases/get-information-banner.js create mode 100644 api/src/banner/domain/usecases/index.js create mode 100644 api/src/banner/infrastructure/repositories/information-banner-repository.js create mode 100644 api/src/banner/infrastructure/serializers/jsonapi/information-banner-serializer.js create mode 100644 api/src/banner/routes.js create mode 100644 api/tests/banner/acceptance/application/banner-route_test.js create mode 100644 api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js create mode 100644 api/tests/banner/unit/domain/serializers/information-banner-serializer_test.js create mode 100644 api/tests/banner/unit/domain/usecases/get-information-banner_test.js create mode 100644 api/tests/tooling/domain-builder/factory/banner/build-banner-information.js diff --git a/api/server.js b/api/server.js index e7577804030..f5d87a333b8 100644 --- a/api/server.js +++ b/api/server.js @@ -6,6 +6,7 @@ import { setupErrorHandling } from './config/server-setup-error-handling.js'; import { knex } from './db/knex-database-connection.js'; import { authentication } from './lib/infrastructure/authentication.js'; import { routes } from './lib/routes.js'; +import { bannerRoutes } from './src/banner/routes.js'; import { attachTargetProfileRoutes, complementaryCertificationRoutes, @@ -243,6 +244,7 @@ const setupRoutesAndPlugins = async function (server) { ...certificationRoutes, ...prescriptionRoutes, ...parcoursupRoutes, + bannerRoutes, { name: 'root', register: async function (server) { diff --git a/api/src/banner/application/banner-controller.js b/api/src/banner/application/banner-controller.js new file mode 100644 index 00000000000..5580e6a03fe --- /dev/null +++ b/api/src/banner/application/banner-controller.js @@ -0,0 +1,16 @@ +import { usecases } from '../domain/usecases/index.js'; +import * as informationBannerSerializer from '../infrastructure/serializers/jsonapi/information-banner-serializer.js'; + +const getInformationBanner = async function (request) { + const { target: id } = request.params; + + const informationBanner = await usecases.getInformationBanner({ id }); + + return informationBannerSerializer.serialize(informationBanner); +}; + +const bannerController = { + getInformationBanner, +}; + +export { bannerController }; diff --git a/api/src/banner/application/banner-route.js b/api/src/banner/application/banner-route.js new file mode 100644 index 00000000000..de1990b13bc --- /dev/null +++ b/api/src/banner/application/banner-route.js @@ -0,0 +1,17 @@ +import { bannerController } from './banner-controller.js'; + +const register = async function (server) { + server.route([ + { + method: 'GET', + path: '/api/information-banners/{target}', + config: { + auth: false, + handler: bannerController.getInformationBanner, + }, + }, + ]); +}; + +const name = 'src-banners-api'; +export { name, register }; diff --git a/api/src/banner/domain/models/information-banner.js b/api/src/banner/domain/models/information-banner.js new file mode 100644 index 00000000000..e21374ec42d --- /dev/null +++ b/api/src/banner/domain/models/information-banner.js @@ -0,0 +1,21 @@ +export class InformationBanner { + constructor({ id, banners }) { + this.id = id; + this.banners = + banners?.map((banner, index) => { + return new Banner({ ...banner, id: `${id}:${index + 1}` }); + }) ?? []; + } + + static empty({ id }) { + return new InformationBanner({ id }); + } +} + +class Banner { + constructor({ id, message, severity }) { + this.id = id; + this.message = message; + this.severity = severity; + } +} diff --git a/api/src/banner/domain/usecases/get-information-banner.js b/api/src/banner/domain/usecases/get-information-banner.js new file mode 100644 index 00000000000..c5425c820fe --- /dev/null +++ b/api/src/banner/domain/usecases/get-information-banner.js @@ -0,0 +1,5 @@ +const getInformationBanner = async ({ id, informationBannerRepository }) => { + return informationBannerRepository.get({ id }); +}; + +export { getInformationBanner }; diff --git a/api/src/banner/domain/usecases/index.js b/api/src/banner/domain/usecases/index.js new file mode 100644 index 00000000000..529099def07 --- /dev/null +++ b/api/src/banner/domain/usecases/index.js @@ -0,0 +1,33 @@ +// eslint-disable import/no-restricted-paths +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js'; +import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; +import * as informationBannerRepository from '../../infrastructure/repositories/information-banner-repository.js'; + +/** + * + * Using {@link https://jsdoc.app/tags-type "Closure Compiler's syntax"} to document injected dependencies + * + * @typedef {informationBannerRepository} InformationBannerRepository + **/ +const dependencies = { + informationBannerRepository, +}; + +const path = dirname(fileURLToPath(import.meta.url)); + +const usecasesWithoutInjectedDependencies = { + ...(await importNamedExportsFromDirectory({ + path: join(path, './'), + ignoredFileNames: 'index.js', + })), +}; + +const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies); + +/** + * @typedef {dependencies} dependencies + */ +export { usecases }; diff --git a/api/src/banner/infrastructure/repositories/information-banner-repository.js b/api/src/banner/infrastructure/repositories/information-banner-repository.js new file mode 100644 index 00000000000..6742e2cd1b1 --- /dev/null +++ b/api/src/banner/infrastructure/repositories/information-banner-repository.js @@ -0,0 +1,15 @@ +import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { InformationBanner } from '../../domain/models/information-banner.js'; + +const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); + +const get = async function ({ id }) { + const banners = await bannerTemporaryStorage.get(id); + if (!banners) { + return InformationBanner.empty({ id }); + } + + return new InformationBanner({ id, banners }); +}; + +export { get }; diff --git a/api/src/banner/infrastructure/serializers/jsonapi/information-banner-serializer.js b/api/src/banner/infrastructure/serializers/jsonapi/information-banner-serializer.js new file mode 100644 index 00000000000..acfad655fc9 --- /dev/null +++ b/api/src/banner/infrastructure/serializers/jsonapi/information-banner-serializer.js @@ -0,0 +1,16 @@ +import jsonapiSerializer from 'jsonapi-serializer'; + +const { Serializer } = jsonapiSerializer; + +const serialize = function (informationBanner) { + return new Serializer('information-banners', { + attributes: ['banners'], + banners: { + included: true, + ref: 'id', + attributes: ['severity', 'message'], + }, + }).serialize(informationBanner); +}; + +export { serialize }; diff --git a/api/src/banner/routes.js b/api/src/banner/routes.js new file mode 100644 index 00000000000..9bea4f0bd7c --- /dev/null +++ b/api/src/banner/routes.js @@ -0,0 +1,3 @@ +import * as bannerRoute from './application/banner-route.js'; + +export const bannerRoutes = [bannerRoute]; diff --git a/api/tests/banner/acceptance/application/banner-route_test.js b/api/tests/banner/acceptance/application/banner-route_test.js new file mode 100644 index 00000000000..f30d80de750 --- /dev/null +++ b/api/tests/banner/acceptance/application/banner-route_test.js @@ -0,0 +1,78 @@ +import { temporaryStorage } from '../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { createServer, expect } from '../../../test-helper.js'; + +let server; + +describe('Acceptance | Router | banner-route', function () { + beforeEach(async function () { + server = await createServer(); + }); + + describe('GET /api/information-banners/{target}', function () { + context('when no banners have been stored for given target', function () { + it('should return empty banners', async function () { + // given + const options = { + method: 'GET', + url: '/api/information-banners/pix-target', + }; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(200); + expect(response.result).to.deep.equal({ + data: { + type: 'information-banners', + id: 'pix-target', + attributes: {}, + relationships: { banners: { data: [] } }, + }, + }); + }); + }); + context('when a banner has been stored for given target', function () { + it('should return the stored banner', async function () { + // given + const target = 'pix-target'; + const bannerData = { message: '[fr]Texte de la bannière[/fr][en]Banner text[/en]', severity: 'info' }; + + const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); + await bannerTemporaryStorage.save({ key: target, value: [bannerData], expirationDelaySeconds: 10 }); + const options = { + method: 'GET', + url: '/api/information-banners/pix-target', + }; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(200); + expect(response.result).to.deep.equal({ + data: { + type: 'information-banners', + id: 'pix-target', + attributes: {}, + relationships: { + banners: { + data: [{ id: 'pix-target:1', type: 'banners' }], + }, + }, + }, + included: [ + { + attributes: { + message: '[fr]Texte de la bannière[/fr][en]Banner text[/en]', + severity: 'info', + }, + id: 'pix-target:1', + type: 'banners', + }, + ], + }); + }); + }); + }); +}); diff --git a/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js b/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js new file mode 100644 index 00000000000..601996000ea --- /dev/null +++ b/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js @@ -0,0 +1,40 @@ +import * as informationBannerRepository from '../../../../../src/banner/infrastructure/repositories/information-banner-repository.js'; +import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { domainBuilder, expect } from '../../../../test-helper.js'; + +describe('Integration | Infrastructure | Repository | Banner | information-banner-repository', function () { + context('#get', function () { + beforeEach(async function () { + const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); + bannerTemporaryStorage.flushAll(); + }); + context('when no information banners have been stored for given id', function () { + it('should return an empty information banner', async function () { + const id = 'pix-target'; + const emptyBanner = domainBuilder.banner.buildEmptyInformationBanner({ id }); + + const bannerInformation = await informationBannerRepository.get({ id }); + + expect(bannerInformation).to.deep.equal(emptyBanner); + }); + }); + context('when an information banner has been stored for given id', function () { + it('should return an empty information banner', async function () { + const id = 'pix-other-target'; + const storedBanner = { message: '[fr]Texte de la bannière[/fr][en]Banner text[/en]', severity: 'info' }; + + const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); + await bannerTemporaryStorage.save({ key: id, value: [storedBanner], expirationDelaySeconds: 10 }); + + const expectedInformationBanner = domainBuilder.banner.buildInformationBanner({ + id, + banners: [{ ...storedBanner, id: 'pix-other-target:1' }], + }); + + const bannerInformation = await informationBannerRepository.get({ id }); + + expect(bannerInformation).to.deep.equal(expectedInformationBanner); + }); + }); + }); +}); diff --git a/api/tests/banner/unit/domain/serializers/information-banner-serializer_test.js b/api/tests/banner/unit/domain/serializers/information-banner-serializer_test.js new file mode 100644 index 00000000000..d4c1bfd6651 --- /dev/null +++ b/api/tests/banner/unit/domain/serializers/information-banner-serializer_test.js @@ -0,0 +1,50 @@ +import * as serializer from '../../../../../src/banner/infrastructure/serializers/jsonapi/information-banner-serializer.js'; +import { domainBuilder, expect } from '../../../../test-helper.js'; + +describe('Unit | Serializer | JSONAPI | information-banner-serializer', function () { + describe('#serialize', function () { + it('should convert JSON API data', async function () { + // given + const bannerInformation = domainBuilder.banner.buildInformationBanner({ + id: 'pix-target', + banners: [ + { + id: 'pix-target:1', + message: [{ fr: 'message fr', en: 'message en' }], + severity: 'info', + }, + ], + }); + + const serializedBannerInformation = await serializer.serialize(bannerInformation); + + expect(serializedBannerInformation).to.deep.equal({ + data: { + type: 'information-banners', + id: 'pix-target', + attributes: {}, + relationships: { + banners: { + data: [{ id: 'pix-target:1', type: 'banners' }], + }, + }, + }, + included: [ + { + attributes: { + message: [ + { + fr: 'message fr', + en: 'message en', + }, + ], + severity: 'info', + }, + id: 'pix-target:1', + type: 'banners', + }, + ], + }); + }); + }); +}); diff --git a/api/tests/banner/unit/domain/usecases/get-information-banner_test.js b/api/tests/banner/unit/domain/usecases/get-information-banner_test.js new file mode 100644 index 00000000000..25d79a39d43 --- /dev/null +++ b/api/tests/banner/unit/domain/usecases/get-information-banner_test.js @@ -0,0 +1,18 @@ +import { getInformationBanner } from '../../../../../src/banner/domain/usecases/get-information-banner.js'; +import { expect, sinon } from '../../../../test-helper.js'; + +describe('Unit | UseCase | Get Information Banner', function () { + it('should use information banner repository to get information banner', async function () { + const id = 'pix-target'; + const informationBannerRepository = { + get: sinon.stub().rejects(new Error('get function called with wrong arguments')), + }; + const expectedInformationBanner = Symbol('information-banner'); + informationBannerRepository.get.withArgs({ id }).resolves(expectedInformationBanner); + + const informationBanner = await getInformationBanner({ id, informationBannerRepository }); + + // then + expect(informationBanner).to.equal(expectedInformationBanner); + }); +}); diff --git a/api/tests/tooling/domain-builder/factory/banner/build-banner-information.js b/api/tests/tooling/domain-builder/factory/banner/build-banner-information.js new file mode 100644 index 00000000000..6d41178e6b4 --- /dev/null +++ b/api/tests/tooling/domain-builder/factory/banner/build-banner-information.js @@ -0,0 +1,11 @@ +import { InformationBanner } from '../../../../../src/banner/domain/models/information-banner.js'; + +const buildEmptyInformationBanner = function ({ id }) { + return InformationBanner.empty({ id }); +}; + +const buildInformationBanner = function ({ id, banners }) { + return new InformationBanner({ id, banners }); +}; + +export { buildEmptyInformationBanner, buildInformationBanner }; diff --git a/api/tests/tooling/domain-builder/factory/index.js b/api/tests/tooling/domain-builder/factory/index.js index 7babc6ddd35..f70f63d3d96 100644 --- a/api/tests/tooling/domain-builder/factory/index.js +++ b/api/tests/tooling/domain-builder/factory/index.js @@ -1,4 +1,4 @@ -import { buildScoringAndCapacitySimulatorReport } from './/build-scoring-and-capacity-simulator-report.js'; +import { buildEmptyInformationBanner, buildInformationBanner } from './banner/build-banner-information.js'; import { buildAccountRecoveryDemand } from './build-account-recovery-demand.js'; import { buildActivity } from './build-activity.js'; import { buildActivityAnswer } from './build-activity-answer.js'; @@ -126,6 +126,7 @@ import { buildReproducibilityRate } from './build-reproducibility-rate.js'; import { buildResultCompetenceTree } from './build-result-competence-tree.js'; import { buildSchoolAssessment } from './build-school-assessment.js'; import { buildSCOCertificationCandidate } from './build-sco-certification-candidate.js'; +import { buildScoringAndCapacitySimulatorReport } from './build-scoring-and-capacity-simulator-report.js'; import { buildSessionForAttendanceSheet } from './build-session-for-attendance-sheet.js'; import { buildSessionForInvigilatorKit } from './build-session-for-invigilator-kit.js'; import { buildSessionForSupervising } from './build-session-for-supervising.js'; @@ -204,6 +205,11 @@ import { buildCampaignParticipation as boundedContextCampaignParticipationBuildC import { buildStageCollection as buildStageCollectionForTargetProfileManagement } from './target-profile-management/build-stage-collection.js'; import { buildStageCollection as buildStageCollectionForUserCampaignResults } from './user-campaign-results/build-stage-collection.js'; +const banner = { + buildEmptyInformationBanner, + buildInformationBanner, +}; + const certification = { configuration: { buildCenter: buildConfigurationCenter, @@ -268,6 +274,7 @@ const prescription = { }; export { + banner, buildAccountRecoveryDemand, buildActivity, buildActivityAnswer, From 0c21ef50777ec6177ce67ee7fcd233a2c966b92d Mon Sep 17 00:00:00 2001 From: Guillaume Lagorce Date: Wed, 15 Jan 2025 15:41:14 +0100 Subject: [PATCH 2/7] tech(api): extract temporary-storage to key-value-storage --- .../data/team-prescription/build-quests.js | 2 +- api/index.js | 12 +- ...nizations-with-tags-and-target-profiles.js | 3 +- .../data-generation/generate-certif-cli.js | 6 +- .../information-banner-repository.js | 6 +- ...essions-storage-for-mass-import-service.js | 6 +- .../repositories/answer-job-repository.js | 2 +- .../authentication-session.service.js | 2 +- .../services/oidc-authentication-service.js | 2 +- .../email-validation-demand.repository.js | 2 +- .../repositories/refresh-token.repository.js | 2 +- .../repositories/user-email.repository.js | 2 +- .../application/jobs/answer-job-controller.js | 2 +- .../infrastructure/repositories/index.js | 2 +- .../InMemoryKeyValueStorage.js} | 8 +- .../KeyValueStorage.js} | 6 +- .../RedisKeyValueStorage.js} | 28 +++-- .../key-value-storages/index.js | 17 +++ .../infrastructure/temporary-storage/index.js | 18 --- .../application/banner-route_test.js | 5 +- .../information-banner-repository_test.js | 8 +- ...ns-storage-for-mass-import-service_test.js | 2 +- .../fwb-oidc-authentication-service_test.js | 2 +- ...emploi-oidc-authentication-service_test.js | 2 +- ...user-email-with-validation.usecase.test.js | 2 +- .../refresh-token.repository.test.js | 2 +- ...e_test.js => RedisKeyValueStorage_test.js} | 35 ++++-- .../InMemoryKeyValueStorage_test.js} | 110 +++++++++--------- .../KeyValueStorage_test.js} | 60 +++++----- .../RedisKeyValueStorage_test.js} | 78 ++++++------- 30 files changed, 228 insertions(+), 206 deletions(-) rename api/src/shared/infrastructure/{temporary-storage/InMemoryTemporaryStorage.js => key-value-storages/InMemoryKeyValueStorage.js} (89%) rename api/src/shared/infrastructure/{temporary-storage/TemporaryStorage.js => key-value-storages/KeyValueStorage.js} (96%) rename api/src/shared/infrastructure/{temporary-storage/RedisTemporaryStorage.js => key-value-storages/RedisKeyValueStorage.js} (70%) create mode 100644 api/src/shared/infrastructure/key-value-storages/index.js delete mode 100644 api/src/shared/infrastructure/temporary-storage/index.js rename api/tests/shared/integration/infrastructure/temporary-storage/{RedisTemporaryStorage_test.js => RedisKeyValueStorage_test.js} (75%) rename api/tests/shared/unit/infrastructure/{temporary-storage/InMemoryTemporaryStorage_test.js => key-value-storages/InMemoryKeyValueStorage_test.js} (54%) rename api/tests/shared/unit/infrastructure/{temporary-storage/TemporaryStorage_test.js => key-value-storages/KeyValueStorage_test.js} (73%) rename api/tests/shared/unit/infrastructure/{temporary-storage/RedisTemporaryStorage_test.js => key-value-storages/RedisKeyValueStorage_test.js} (65%) diff --git a/api/db/seeds/data/team-prescription/build-quests.js b/api/db/seeds/data/team-prescription/build-quests.js index 51f3391e810..f24ab78dc4c 100644 --- a/api/db/seeds/data/team-prescription/build-quests.js +++ b/api/db/seeds/data/team-prescription/build-quests.js @@ -2,7 +2,7 @@ import { ATTESTATIONS } from '../../../../src/profile/domain/constants.js'; import { REWARD_TYPES } from '../../../../src/quest/domain/constants.js'; import { COMPARISON } from '../../../../src/quest/domain/models/Quest.js'; import { Assessment, CampaignParticipationStatuses, Membership } from '../../../../src/shared/domain/models/index.js'; -import { temporaryStorage } from '../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../src/shared/infrastructure/key-value-storages/index.js'; import { AEFE_TAG, FEATURE_ATTESTATIONS_MANAGEMENT_ID, diff --git a/api/index.js b/api/index.js index 9ce031938b6..b9cb6c632db 100644 --- a/api/index.js +++ b/api/index.js @@ -8,7 +8,7 @@ import { disconnect, prepareDatabaseConnection } from './db/knex-database-connec import { createServer } from './server.js'; import { config } from './src/shared/config.js'; import { learningContentCache } from './src/shared/infrastructure/caches/learning-content-cache.js'; -import { temporaryStorage } from './src/shared/infrastructure/temporary-storage/index.js'; +import { informationBannersStorage, temporaryStorage } from './src/shared/infrastructure/key-value-storages/index.js'; import { logger } from './src/shared/infrastructure/utils/logger.js'; import { redisMonitor } from './src/shared/infrastructure/utils/redis-monitor.js'; @@ -40,13 +40,15 @@ async function _exitOnSignal(signal) { logger.info('Stopping HAPI Oppsy server...'); await server.oppsy.stop(); } - logger.info('Closing connexions to database...'); + logger.info('Closing connections to database...'); await disconnect(); - logger.info('Closing connexions to cache...'); + logger.info('Closing connections to cache...'); await learningContentCache.quit(); - logger.info('Closing connexions to temporary storage...'); + logger.info('Closing connections to temporary storage...'); await temporaryStorage.quit(); - logger.info('Closing connexions to redis monitor...'); + logger.info('Closing connections to information banners storage...'); + await informationBannersStorage.quit(); + logger.info('Closing connections to redis monitor...'); await redisMonitor.quit(); logger.info('Exiting process...'); } diff --git a/api/scripts/create-organizations-with-tags-and-target-profiles.js b/api/scripts/create-organizations-with-tags-and-target-profiles.js index f69dd8f39f0..e3d9b33baa0 100644 --- a/api/scripts/create-organizations-with-tags-and-target-profiles.js +++ b/api/scripts/create-organizations-with-tags-and-target-profiles.js @@ -12,8 +12,8 @@ import * as targetProfileShareRepository from '../lib/infrastructure/repositorie import * as dataProtectionOfficerRepository from '../src/organizational-entities/infrastructure/repositories/data-protection-officer.repository.js'; import * as organizationTagRepository from '../src/organizational-entities/infrastructure/repositories/organization-tag.repository.js'; import { tagRepository } from '../src/organizational-entities/infrastructure/repositories/tag.repository.js'; +import { informationBannersStorage, temporaryStorage } from '../src/shared/infrastructure/key-value-storages/index.js'; import * as organizationRepository from '../src/shared/infrastructure/repositories/organization-repository.js'; -import { temporaryStorage } from '../src/shared/infrastructure/temporary-storage/index.js'; import { organizationInvitationRepository } from '../src/team/infrastructure/repositories/organization-invitation.repository.js'; import { checkCsvHeader, parseCsvWithHeader } from './helpers/csvHelpers.js'; @@ -137,6 +137,7 @@ async function main() { // l'import de OidcIdentityProviders dans les validateurs démarre le service redis // il faut donc stopper le process pour que celui ci s'arrête, il suffit d'avoir l'import du storage pour y avoir accès temporaryStorage.quit(); + informationBannersStorage.quit(); } } })(); diff --git a/api/scripts/data-generation/generate-certif-cli.js b/api/scripts/data-generation/generate-certif-cli.js index 6b14bcacaa6..761b935db4c 100755 --- a/api/scripts/data-generation/generate-certif-cli.js +++ b/api/scripts/data-generation/generate-certif-cli.js @@ -15,8 +15,11 @@ import { DatabaseBuilder } from '../../db/database-builder/database-builder.js'; import { getNewSessionCode } from '../../src/certification/enrolment/domain/services/session-code-service.js'; import { CampaignParticipationStatuses } from '../../src/shared/domain/models/index.js'; import { learningContentCache } from '../../src/shared/infrastructure/caches/learning-content-cache.js'; +import { + informationBannersStorage, + temporaryStorage, +} from '../../src/shared/infrastructure/key-value-storages/index.js'; import * as skillRepository from '../../src/shared/infrastructure/repositories/skill-repository.js'; -import { temporaryStorage } from '../../src/shared/infrastructure/temporary-storage/index.js'; import { logger } from '../../src/shared/infrastructure/utils/logger.js'; import { PromiseUtils } from '../../src/shared/infrastructure/utils/promise-utils.js'; import { @@ -396,6 +399,7 @@ async function _disconnect() { logger.info('Closing connexions to cache...'); await learningContentCache.quit(); await temporaryStorage.quit(); + await informationBannersStorage.quit(); logger.info('Exiting process gracefully...'); } diff --git a/api/src/banner/infrastructure/repositories/information-banner-repository.js b/api/src/banner/infrastructure/repositories/information-banner-repository.js index 6742e2cd1b1..62c3d02d0e5 100644 --- a/api/src/banner/infrastructure/repositories/information-banner-repository.js +++ b/api/src/banner/infrastructure/repositories/information-banner-repository.js @@ -1,10 +1,8 @@ -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { informationBannersStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { InformationBanner } from '../../domain/models/information-banner.js'; -const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); - const get = async function ({ id }) { - const banners = await bannerTemporaryStorage.get(id); + const banners = await informationBannersStorage.get(id); if (!banners) { return InformationBanner.empty({ id }); } diff --git a/api/src/certification/enrolment/domain/services/temporary-sessions-storage-for-mass-import-service.js b/api/src/certification/enrolment/domain/services/temporary-sessions-storage-for-mass-import-service.js index c9d64000efb..f724a426236 100644 --- a/api/src/certification/enrolment/domain/services/temporary-sessions-storage-for-mass-import-service.js +++ b/api/src/certification/enrolment/domain/services/temporary-sessions-storage-for-mass-import-service.js @@ -1,5 +1,5 @@ import { config } from '../../../../../src/shared/config.js'; -import { temporaryStorage } from '../../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../shared/infrastructure/key-value-storages/index.js'; const sessionMassImportTemporaryStorage = temporaryStorage.withPrefix('sessions-mass-import:'); import { randomUUID } from 'node:crypto'; @@ -19,9 +19,7 @@ const save = async function ({ sessions, userId }) { const getByKeyAndUserId = async function ({ cachedValidatedSessionsKey, userId }) { const key = `${userId}:${cachedValidatedSessionsKey}`; - const sessions = await sessionMassImportTemporaryStorage.get(key); - - return sessions; + return sessionMassImportTemporaryStorage.get(key); }; const remove = async function ({ cachedValidatedSessionsKey, userId }) { diff --git a/api/src/evaluation/infrastructure/repositories/answer-job-repository.js b/api/src/evaluation/infrastructure/repositories/answer-job-repository.js index ce2c57bd38a..7ec1838079b 100644 --- a/api/src/evaluation/infrastructure/repositories/answer-job-repository.js +++ b/api/src/evaluation/infrastructure/repositories/answer-job-repository.js @@ -1,8 +1,8 @@ import { AnswerJob } from '../../../quest/domain/models/AnwserJob.js'; import { config } from '../../../shared/config.js'; import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { JobRepository } from '../../../shared/infrastructure/repositories/jobs/job-repository.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; const profileRewardTemporaryStorage = temporaryStorage.withPrefix('profile-rewards:'); diff --git a/api/src/identity-access-management/domain/services/authentication-session.service.js b/api/src/identity-access-management/domain/services/authentication-session.service.js index 7b66a8101d6..ca8aaa62a9d 100644 --- a/api/src/identity-access-management/domain/services/authentication-session.service.js +++ b/api/src/identity-access-management/domain/services/authentication-session.service.js @@ -1,5 +1,5 @@ import { config } from '../../../shared/config.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; const authenticationSessionTemporaryStorage = temporaryStorage.withPrefix('authentication-session:'); const EXPIRATION_DELAY_SECONDS = config.authenticationSession.temporaryStorage.expirationDelaySeconds; diff --git a/api/src/identity-access-management/domain/services/oidc-authentication-service.js b/api/src/identity-access-management/domain/services/oidc-authentication-service.js index 558e9b83096..3fdb68ad99a 100644 --- a/api/src/identity-access-management/domain/services/oidc-authentication-service.js +++ b/api/src/identity-access-management/domain/services/oidc-authentication-service.js @@ -10,8 +10,8 @@ import { OIDC_ERRORS } from '../../../shared/domain/constants.js'; import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; import { OidcError, OidcMissingFieldsError } from '../../../shared/domain/errors.js'; import { AuthenticationMethod, AuthenticationSessionContent } from '../../../shared/domain/models/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { monitoringTools } from '../../../shared/infrastructure/monitoring-tools.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; import { logger } from '../../../shared/infrastructure/utils/logger.js'; import { DEFAULT_CLAIM_MAPPING } from '../constants/oidc-identity-providers.js'; import { ClaimManager } from '../models/ClaimManager.js'; diff --git a/api/src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js b/api/src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js index 5313dc04bfa..7f451f03aa2 100644 --- a/api/src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js +++ b/api/src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js @@ -1,7 +1,7 @@ import crypto from 'node:crypto'; import { config } from '../../../shared/config.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; const emailValidationDemandTemporaryStorage = temporaryStorage.withPrefix('email-validation-demand:'); diff --git a/api/src/identity-access-management/infrastructure/repositories/refresh-token.repository.js b/api/src/identity-access-management/infrastructure/repositories/refresh-token.repository.js index 4e953f30f4a..d42190aa791 100644 --- a/api/src/identity-access-management/infrastructure/repositories/refresh-token.repository.js +++ b/api/src/identity-access-management/infrastructure/repositories/refresh-token.repository.js @@ -1,4 +1,4 @@ -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { PromiseUtils } from '../../../shared/infrastructure/utils/promise-utils.js'; import { RefreshToken } from '../../domain/models/RefreshToken.js'; diff --git a/api/src/identity-access-management/infrastructure/repositories/user-email.repository.js b/api/src/identity-access-management/infrastructure/repositories/user-email.repository.js index 99d360cdfb4..7b45a9d60b2 100644 --- a/api/src/identity-access-management/infrastructure/repositories/user-email.repository.js +++ b/api/src/identity-access-management/infrastructure/repositories/user-email.repository.js @@ -1,5 +1,5 @@ import { config } from '../../../shared/config.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { EmailModificationDemand } from '../../domain/models/EmailModificationDemand.js'; const verifyEmailTemporaryStorage = temporaryStorage.withPrefix('verify-email:'); diff --git a/api/src/quest/application/jobs/answer-job-controller.js b/api/src/quest/application/jobs/answer-job-controller.js index 9dc2a27f00e..db5240aa999 100644 --- a/api/src/quest/application/jobs/answer-job-controller.js +++ b/api/src/quest/application/jobs/answer-job-controller.js @@ -1,6 +1,6 @@ import { JobController, JobGroup } from '../../../shared/application/jobs/job-controller.js'; import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { AnswerJob } from '../../domain/models/AnwserJob.js'; import { usecases } from '../../domain/usecases/index.js'; diff --git a/api/src/quest/infrastructure/repositories/index.js b/api/src/quest/infrastructure/repositories/index.js index e6e7273abd0..ef54f215ae0 100644 --- a/api/src/quest/infrastructure/repositories/index.js +++ b/api/src/quest/infrastructure/repositories/index.js @@ -2,7 +2,7 @@ import * as knowledgeElementsApi from '../../../evaluation/application/api/knowl import * as organizationLearnerWithParticipationApi from '../../../prescription/organization-learner/application/api/organization-learners-with-participations-api.js'; import * as profileRewardApi from '../../../profile/application/api/profile-reward-api.js'; import * as rewardApi from '../../../profile/application/api/reward-api.js'; -import { temporaryStorage } from '../../../shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../shared/infrastructure/key-value-storages/index.js'; import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js'; import * as eligibilityRepository from './eligibility-repository.js'; import * as rewardRepository from './reward-repository.js'; diff --git a/api/src/shared/infrastructure/temporary-storage/InMemoryTemporaryStorage.js b/api/src/shared/infrastructure/key-value-storages/InMemoryKeyValueStorage.js similarity index 89% rename from api/src/shared/infrastructure/temporary-storage/InMemoryTemporaryStorage.js rename to api/src/shared/infrastructure/key-value-storages/InMemoryKeyValueStorage.js index 892d5d0f328..52853db49d1 100644 --- a/api/src/shared/infrastructure/temporary-storage/InMemoryTemporaryStorage.js +++ b/api/src/shared/infrastructure/key-value-storages/InMemoryKeyValueStorage.js @@ -4,16 +4,16 @@ import NodeCache from 'node-cache'; const { trim, noop } = lodash; -import { TemporaryStorage } from './TemporaryStorage.js'; +import { KeyValueStorage } from './KeyValueStorage.js'; -class InMemoryTemporaryStorage extends TemporaryStorage { +class InMemoryKeyValueStorage extends KeyValueStorage { constructor() { super(); this._client = new NodeCache(); } async save({ key, value, expirationDelaySeconds }) { - const storageKey = trim(key) || InMemoryTemporaryStorage.generateKey(); + const storageKey = trim(key) || InMemoryKeyValueStorage.generateKey(); this._client.set(storageKey, value, expirationDelaySeconds); return storageKey; } @@ -89,4 +89,4 @@ class InMemoryTemporaryStorage extends TemporaryStorage { } } -export { InMemoryTemporaryStorage }; +export { InMemoryKeyValueStorage }; diff --git a/api/src/shared/infrastructure/temporary-storage/TemporaryStorage.js b/api/src/shared/infrastructure/key-value-storages/KeyValueStorage.js similarity index 96% rename from api/src/shared/infrastructure/temporary-storage/TemporaryStorage.js rename to api/src/shared/infrastructure/key-value-storages/KeyValueStorage.js index 1155d1aa77f..da8823b61f9 100644 --- a/api/src/shared/infrastructure/temporary-storage/TemporaryStorage.js +++ b/api/src/shared/infrastructure/key-value-storages/KeyValueStorage.js @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto'; -class TemporaryStorage { +class KeyValueStorage { static generateKey() { return randomUUID(); } @@ -65,7 +65,7 @@ class TemporaryStorage { const storage = this; return { async save({ key, ...args }) { - key = key ?? TemporaryStorage.generateKey(); + key = key ?? KeyValueStorage.generateKey(); await storage.save({ key: prefix + key, ...args }); return key; }, @@ -122,4 +122,4 @@ class TemporaryStorage { } } -export { TemporaryStorage }; +export { KeyValueStorage }; diff --git a/api/src/shared/infrastructure/temporary-storage/RedisTemporaryStorage.js b/api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js similarity index 70% rename from api/src/shared/infrastructure/temporary-storage/RedisTemporaryStorage.js rename to api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js index 5b2ebdd4ad3..937891ce14b 100644 --- a/api/src/shared/infrastructure/temporary-storage/RedisTemporaryStorage.js +++ b/api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js @@ -3,27 +3,33 @@ import lodash from 'lodash'; const { trim } = lodash; import { RedisClient } from '../utils/RedisClient.js'; -import { TemporaryStorage } from './TemporaryStorage.js'; +import { KeyValueStorage } from './KeyValueStorage.js'; const EXPIRATION_PARAMETER = 'ex'; const KEEPTTL_PARAMETER = 'keepttl'; -const PREFIX = 'temporary-storage:'; -class RedisTemporaryStorage extends TemporaryStorage { - constructor(redisUrl) { +class RedisKeyValueStorage extends KeyValueStorage { + #prefix; + + constructor(redisUrl, prefix) { super(); - this._client = RedisTemporaryStorage.createClient(redisUrl); + this.#prefix = prefix; + this._client = RedisKeyValueStorage.createClient(redisUrl, prefix); } - static createClient(redisUrl) { - return new RedisClient(redisUrl, { name: 'temporary-storage', prefix: PREFIX }); + static createClient(redisUrl, prefix) { + return new RedisClient(redisUrl, { name: 'temporary-storage', prefix }); } async save({ key, value, expirationDelaySeconds }) { - const storageKey = trim(key) || RedisTemporaryStorage.generateKey(); + const storageKey = trim(key) || RedisKeyValueStorage.generateKey(); const objectAsString = JSON.stringify(value); - await this._client.set(storageKey, objectAsString, EXPIRATION_PARAMETER, expirationDelaySeconds); + if (expirationDelaySeconds) { + await this._client.set(storageKey, objectAsString, EXPIRATION_PARAMETER, expirationDelaySeconds); + } else { + await this._client.set(storageKey, objectAsString); + } return storageKey; } @@ -81,7 +87,7 @@ class RedisTemporaryStorage extends TemporaryStorage { async keys(pattern) { const keys = await this._client.keys(pattern); - return keys.map((key) => key.slice(PREFIX.length)); + return keys.map((key) => key.slice(this.#prefix.length)); } async flushAll() { @@ -89,4 +95,4 @@ class RedisTemporaryStorage extends TemporaryStorage { } } -export { RedisTemporaryStorage }; +export { RedisKeyValueStorage }; diff --git a/api/src/shared/infrastructure/key-value-storages/index.js b/api/src/shared/infrastructure/key-value-storages/index.js new file mode 100644 index 00000000000..40e12e9f603 --- /dev/null +++ b/api/src/shared/infrastructure/key-value-storages/index.js @@ -0,0 +1,17 @@ +import { config } from '../../config.js'; + +const redisUrl = config.temporaryStorage.redisUrl; + +import { InMemoryKeyValueStorage } from './InMemoryKeyValueStorage.js'; +import { RedisKeyValueStorage } from './RedisKeyValueStorage.js'; + +function _createKeyValueStorage({ prefix }) { + if (redisUrl) { + return new RedisKeyValueStorage(redisUrl, prefix); + } else { + return new InMemoryKeyValueStorage(); + } +} + +export const temporaryStorage = _createKeyValueStorage({ prefix: 'temporary-storage:' }); +export const informationBannersStorage = _createKeyValueStorage({ prefix: 'information-banners:' }); diff --git a/api/src/shared/infrastructure/temporary-storage/index.js b/api/src/shared/infrastructure/temporary-storage/index.js deleted file mode 100644 index 296dd6604aa..00000000000 --- a/api/src/shared/infrastructure/temporary-storage/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { config } from '../../config.js'; - -const redisUrl = config.temporaryStorage.redisUrl; - -import { InMemoryTemporaryStorage } from './InMemoryTemporaryStorage.js'; -import { RedisTemporaryStorage } from './RedisTemporaryStorage.js'; - -function _createTemporaryStorage() { - if (redisUrl) { - return new RedisTemporaryStorage(redisUrl); - } else { - return new InMemoryTemporaryStorage(); - } -} - -const temporaryStorage = _createTemporaryStorage(); - -export { temporaryStorage }; diff --git a/api/tests/banner/acceptance/application/banner-route_test.js b/api/tests/banner/acceptance/application/banner-route_test.js index f30d80de750..4c5b171619b 100644 --- a/api/tests/banner/acceptance/application/banner-route_test.js +++ b/api/tests/banner/acceptance/application/banner-route_test.js @@ -1,4 +1,4 @@ -import { temporaryStorage } from '../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { informationBannersStorage } from '../../../../src/shared/infrastructure/key-value-storages/index.js'; import { createServer, expect } from '../../../test-helper.js'; let server; @@ -38,8 +38,7 @@ describe('Acceptance | Router | banner-route', function () { const target = 'pix-target'; const bannerData = { message: '[fr]Texte de la bannière[/fr][en]Banner text[/en]', severity: 'info' }; - const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); - await bannerTemporaryStorage.save({ key: target, value: [bannerData], expirationDelaySeconds: 10 }); + await informationBannersStorage.save({ key: target, value: [bannerData], expirationDelaySeconds: 10 }); const options = { method: 'GET', url: '/api/information-banners/pix-target', diff --git a/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js b/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js index 601996000ea..a20f31bf016 100644 --- a/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js +++ b/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js @@ -1,12 +1,11 @@ import * as informationBannerRepository from '../../../../../src/banner/infrastructure/repositories/information-banner-repository.js'; -import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { informationBannersStorage } from '../../../../../src/shared/infrastructure/key-value-storages/index.js'; import { domainBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Infrastructure | Repository | Banner | information-banner-repository', function () { context('#get', function () { beforeEach(async function () { - const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); - bannerTemporaryStorage.flushAll(); + informationBannersStorage.flushAll(); }); context('when no information banners have been stored for given id', function () { it('should return an empty information banner', async function () { @@ -23,8 +22,7 @@ describe('Integration | Infrastructure | Repository | Banner | information-banne const id = 'pix-other-target'; const storedBanner = { message: '[fr]Texte de la bannière[/fr][en]Banner text[/en]', severity: 'info' }; - const bannerTemporaryStorage = temporaryStorage.withPrefix('information-banners:'); - await bannerTemporaryStorage.save({ key: id, value: [storedBanner], expirationDelaySeconds: 10 }); + await informationBannersStorage.save({ key: id, value: [storedBanner], expirationDelaySeconds: 10 }); const expectedInformationBanner = domainBuilder.banner.buildInformationBanner({ id, diff --git a/api/tests/certification/enrolment/unit/domain/services/temporary-sessions-storage-for-mass-import-service_test.js b/api/tests/certification/enrolment/unit/domain/services/temporary-sessions-storage-for-mass-import-service_test.js index 6fe873d39cf..5eb9dc9b4ee 100644 --- a/api/tests/certification/enrolment/unit/domain/services/temporary-sessions-storage-for-mass-import-service_test.js +++ b/api/tests/certification/enrolment/unit/domain/services/temporary-sessions-storage-for-mass-import-service_test.js @@ -1,5 +1,5 @@ import * as temporarySessionsStorageForMassImportService from '../../../../../../src/certification/enrolment/domain/services/temporary-sessions-storage-for-mass-import-service.js'; -import { temporaryStorage } from '../../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../../../src/shared/infrastructure/key-value-storages/index.js'; import { expect } from '../../../../../test-helper.js'; const sessionMassImportTemporaryStorage = temporaryStorage.withPrefix('sessions-mass-import:'); diff --git a/api/tests/identity-access-management/integration/domain/services/fwb-oidc-authentication-service_test.js b/api/tests/identity-access-management/integration/domain/services/fwb-oidc-authentication-service_test.js index 82eb8e38d08..4e4655c37b4 100644 --- a/api/tests/identity-access-management/integration/domain/services/fwb-oidc-authentication-service_test.js +++ b/api/tests/identity-access-management/integration/domain/services/fwb-oidc-authentication-service_test.js @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'; import { FwbOidcAuthenticationService } from '../../../../../src/identity-access-management/domain/services/fwb-oidc-authentication-service.js'; import { config } from '../../../../../src/shared/config.js'; -import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../../src/shared/infrastructure/key-value-storages/index.js'; import { expect } from '../../../../test-helper.js'; const defaultSessionTemporaryStorage = temporaryStorage.withPrefix('oidc-session:'); diff --git a/api/tests/identity-access-management/integration/domain/services/pole-emploi-oidc-authentication-service_test.js b/api/tests/identity-access-management/integration/domain/services/pole-emploi-oidc-authentication-service_test.js index 098b3e2a53a..f6d90da0ea0 100644 --- a/api/tests/identity-access-management/integration/domain/services/pole-emploi-oidc-authentication-service_test.js +++ b/api/tests/identity-access-management/integration/domain/services/pole-emploi-oidc-authentication-service_test.js @@ -6,7 +6,7 @@ import { PoleEmploiOidcAuthenticationService } from '../../../../../src/identity import * as authenticationMethodRepository from '../../../../../src/identity-access-management/infrastructure/repositories/authentication-method.repository.js'; import { userToCreateRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/user-to-create.repository.js'; import { config } from '../../../../../src/shared/config.js'; -import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../../src/shared/infrastructure/key-value-storages/index.js'; import { expect, knex } from '../../../../test-helper.js'; const defaultSessionTemporaryStorage = temporaryStorage.withPrefix('oidc-session:'); diff --git a/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js b/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js index 11f91ec39bb..c11b1bbd088 100644 --- a/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js +++ b/api/tests/identity-access-management/integration/domain/usecases/update-user-email-with-validation.usecase.test.js @@ -9,7 +9,7 @@ import { InvalidVerificationCodeError, UserNotAuthorizedToUpdateEmailError, } from '../../../../../src/shared/domain/errors.js'; -import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../../src/shared/infrastructure/key-value-storages/index.js'; import { catchErr, databaseBuilder, knex, sinon } from '../../../../test-helper.js'; const verifyEmailTemporaryStorage = temporaryStorage.withPrefix('verify-email:'); diff --git a/api/tests/identity-access-management/integration/infrastructure/repositories/refresh-token.repository.test.js b/api/tests/identity-access-management/integration/infrastructure/repositories/refresh-token.repository.test.js index a9921c1ea0a..e78406f22b2 100644 --- a/api/tests/identity-access-management/integration/infrastructure/repositories/refresh-token.repository.test.js +++ b/api/tests/identity-access-management/integration/infrastructure/repositories/refresh-token.repository.test.js @@ -1,6 +1,6 @@ import { RefreshToken } from '../../../../../src/identity-access-management/domain/models/RefreshToken.js'; import { refreshTokenRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/refresh-token.repository.js'; -import { temporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/index.js'; +import { temporaryStorage } from '../../../../../src/shared/infrastructure/key-value-storages/index.js'; import { expect } from '../../../../test-helper.js'; const refreshTokenTemporaryStorage = temporaryStorage.withPrefix('refresh-tokens:'); diff --git a/api/tests/shared/integration/infrastructure/temporary-storage/RedisTemporaryStorage_test.js b/api/tests/shared/integration/infrastructure/temporary-storage/RedisKeyValueStorage_test.js similarity index 75% rename from api/tests/shared/integration/infrastructure/temporary-storage/RedisTemporaryStorage_test.js rename to api/tests/shared/integration/infrastructure/temporary-storage/RedisKeyValueStorage_test.js index 0b7e5db565c..37bca70dda1 100644 --- a/api/tests/shared/integration/infrastructure/temporary-storage/RedisTemporaryStorage_test.js +++ b/api/tests/shared/integration/infrastructure/temporary-storage/RedisKeyValueStorage_test.js @@ -1,12 +1,12 @@ import { randomUUID } from 'node:crypto'; import { config as settings } from '../../../../../src/shared/config.js'; -import { RedisTemporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/RedisTemporaryStorage.js'; +import { RedisKeyValueStorage } from '../../../../../src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js'; import { expect } from '../../../../test-helper.js'; const REDIS_URL = settings.redis.url; -describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorage', function () { +describe('Integration | Infrastructure | KeyValueStorage | RedisKeyValueStorage', function () { // this check is used to prevent failure when redis is not setup if (REDIS_URL !== undefined) { @@ -15,7 +15,7 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag // given const TWO_MINUTES_IN_SECONDS = 2 * 60; const value = { url: 'url' }; - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); const key = await storage.save({ value: 'c', expirationDelaySeconds: TWO_MINUTES_IN_SECONDS }); // when @@ -27,13 +27,30 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag expect(result).to.deep.equal({ url: 'url' }); expect(expirationDelaySeconds).to.equal(TWO_MINUTES_IN_SECONDS); }); + + it('should set new value with no expiration', async function () { + // given + const NO_EXPIRATION = -1; // See https://redis.io/docs/latest/commands/ttl/ + const value = { url: 'url' }; + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); + const key = await storage.save({ value: 'c' }); + + // when + await storage.update(key, value); + + // then + const result = await storage.get(key); + const expirationDelaySeconds = await storage._client.ttl(key); + expect(result).to.deep.equal({ url: 'url' }); + expect(expirationDelaySeconds).to.equal(NO_EXPIRATION); + }); }); describe('#expire', function () { it('should add an expiration time to the list', async function () { // given const key = randomUUID(); - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when await storage.lpush(key, 'value'); @@ -51,7 +68,7 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag it('should retrieve the remaining expiration time from a list', async function () { // given const key = randomUUID(); - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when await storage.lpush(key, 'value'); @@ -67,7 +84,7 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag it('should add a value to a list and return the length of the list', async function () { // given const key = randomUUID(); - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when const length = await storage.lpush(key, 'value'); @@ -82,7 +99,7 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag it('should remove a value from a list and return the number of removed elements', async function () { // given const key = randomUUID(); - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); await storage.lpush(key, 'value1'); await storage.lpush(key, 'value1'); @@ -101,7 +118,7 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag it('should return a list of values', async function () { // given const key = randomUUID(); - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when await storage.lpush(key, 'value1'); @@ -121,7 +138,7 @@ describe('Integration | Infrastructure | TemporaryStorage | RedisTemporaryStorag // given const oneMinute = 60; const pattern = 'prefix:*'; - const storage = new RedisTemporaryStorage(REDIS_URL); + const storage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); await storage.save({ key: 'prefix:key1', value: true, expirationDelaySeconds: oneMinute }); await storage.save({ key: 'prefix:key2', value: true, expirationDelaySeconds: oneMinute }); await storage.save({ key: 'prefix:key3', value: true, expirationDelaySeconds: oneMinute }); diff --git a/api/tests/shared/unit/infrastructure/temporary-storage/InMemoryTemporaryStorage_test.js b/api/tests/shared/unit/infrastructure/key-value-storages/InMemoryKeyValueStorage_test.js similarity index 54% rename from api/tests/shared/unit/infrastructure/temporary-storage/InMemoryTemporaryStorage_test.js rename to api/tests/shared/unit/infrastructure/key-value-storages/InMemoryKeyValueStorage_test.js index 9944ec7a9f3..9913c7199fa 100644 --- a/api/tests/shared/unit/infrastructure/temporary-storage/InMemoryTemporaryStorage_test.js +++ b/api/tests/shared/unit/infrastructure/key-value-storages/InMemoryKeyValueStorage_test.js @@ -1,24 +1,24 @@ -import { InMemoryTemporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/InMemoryTemporaryStorage.js'; +import { InMemoryKeyValueStorage } from '../../../../../src/shared/infrastructure/key-value-storages/InMemoryKeyValueStorage.js'; import { expect, sinon } from '../../../../test-helper.js'; -describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', function () { - let inMemoryTemporaryStorage; +describe('Unit | Infrastructure | key-value-storage | InMemoryKeyValueStorage', function () { + let inMemoryKeyValueStorage; beforeEach(function () { - inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); }); describe('#increment', function () { it('should call client incr to increment value', async function () { // given const key = 'valueKey'; - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when - await inMemoryTemporaryStorage.increment(key); + await inMemoryKeyValueStorage.increment(key); // then - expect(await inMemoryTemporaryStorage.get(key)).to.equal('1'); + expect(await inMemoryKeyValueStorage.get(key)).to.equal('1'); }); }); @@ -26,13 +26,13 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', it('should call client incr to decrement value', async function () { // given const key = 'valueKey'; - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when - await inMemoryTemporaryStorage.decrement(key); + await inMemoryKeyValueStorage.decrement(key); // then - expect(await inMemoryTemporaryStorage.get(key)).to.equal('-1'); + expect(await inMemoryKeyValueStorage.get(key)).to.equal('-1'); }); }); @@ -49,7 +49,7 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', it('should resolve with the generated key', async function () { // when - const key = await inMemoryTemporaryStorage.save({ value: {}, expirationDelaySeconds: 1000 }); + const key = await inMemoryKeyValueStorage.save({ value: {}, expirationDelaySeconds: 1000 }); // then expect(key).to.be.a.string; @@ -60,7 +60,7 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', const keyParameter = 'KEY-PARAMETER'; // when - const returnedKey = await inMemoryTemporaryStorage.save({ + const returnedKey = await inMemoryKeyValueStorage.save({ key: keyParameter, value: {}, expirationDelaySeconds: 1000, @@ -75,7 +75,7 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', const keyParameter = ' '; // when - const returnedKey = await inMemoryTemporaryStorage.save({ + const returnedKey = await inMemoryKeyValueStorage.save({ key: keyParameter, value: {}, expirationDelaySeconds: 1000, @@ -91,13 +91,13 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', const TWO_MINUTES_IN_MILLISECONDS = 2 * 60 * 1000; // when - const key = await inMemoryTemporaryStorage.save({ + const key = await inMemoryKeyValueStorage.save({ value: { name: 'name' }, expirationDelaySeconds: TWO_MINUTES_IN_SECONDS, }); // then - const expirationKeyInTimestamp = await inMemoryTemporaryStorage._client.getTtl(key); + const expirationKeyInTimestamp = await inMemoryKeyValueStorage._client.getTtl(key); expect(expirationKeyInTimestamp).to.equal(TWO_MINUTES_IN_MILLISECONDS); }); }); @@ -108,10 +108,10 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', const value = { name: 'name' }; const expirationDelaySeconds = 1000; - const key = await inMemoryTemporaryStorage.save({ value, expirationDelaySeconds }); + const key = await inMemoryKeyValueStorage.save({ value, expirationDelaySeconds }); // when - const result = await inMemoryTemporaryStorage.get(key); + const result = await inMemoryKeyValueStorage.get(key); // then expect(result).to.deep.equal(value); @@ -121,35 +121,35 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#update', function () { it('should set a new value', async function () { // given - const key = await inMemoryTemporaryStorage.save({ + const key = await inMemoryKeyValueStorage.save({ value: { name: 'name' }, }); // when - await inMemoryTemporaryStorage.update(key, { url: 'url' }); + await inMemoryKeyValueStorage.update(key, { url: 'url' }); // then - const result = await inMemoryTemporaryStorage.get(key); + const result = await inMemoryKeyValueStorage.get(key); expect(result).to.deep.equal({ url: 'url' }); }); it('should not change the time to live', async function () { // given - const keyWithTtl = await inMemoryTemporaryStorage.save({ + const keyWithTtl = await inMemoryKeyValueStorage.save({ value: {}, expirationDelaySeconds: 1, }); - const keyWithoutTtl = await inMemoryTemporaryStorage.save({ value: {} }); + const keyWithoutTtl = await inMemoryKeyValueStorage.save({ value: {} }); // when await new Promise((resolve) => setTimeout(resolve, 500)); - await inMemoryTemporaryStorage.update(keyWithTtl, {}); - await inMemoryTemporaryStorage.update(keyWithoutTtl, {}); + await inMemoryKeyValueStorage.update(keyWithTtl, {}); + await inMemoryKeyValueStorage.update(keyWithoutTtl, {}); await new Promise((resolve) => setTimeout(resolve, 600)); // then - expect(await inMemoryTemporaryStorage.get(keyWithTtl)).to.be.undefined; - expect(await inMemoryTemporaryStorage.get(keyWithoutTtl)).not.to.be.undefined; + expect(await inMemoryKeyValueStorage.get(keyWithTtl)).to.be.undefined; + expect(await inMemoryKeyValueStorage.get(keyWithoutTtl)).not.to.be.undefined; }); }); @@ -159,13 +159,13 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', const value = { name: 'name' }; const expirationDelaySeconds = 1000; - const key = await inMemoryTemporaryStorage.save({ value, expirationDelaySeconds }); + const key = await inMemoryKeyValueStorage.save({ value, expirationDelaySeconds }); // when - await inMemoryTemporaryStorage.delete(key); + await inMemoryKeyValueStorage.delete(key); // then - const savedKey = await inMemoryTemporaryStorage.get(key); + const savedKey = await inMemoryKeyValueStorage.get(key); expect(savedKey).to.be.undefined; }); }); @@ -173,14 +173,14 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#expire', function () { it('should add an expiration time to the list', async function () { // given - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when const key = 'key:lpush'; - await inMemoryTemporaryStorage.lpush(key, 'value'); - await inMemoryTemporaryStorage.expire({ key, expirationDelaySeconds: 1 }); + await inMemoryKeyValueStorage.lpush(key, 'value'); + await inMemoryKeyValueStorage.expire({ key, expirationDelaySeconds: 1 }); await new Promise((resolve) => setTimeout(resolve, 1200)); - const list = inMemoryTemporaryStorage.lrange(key); + const list = inMemoryKeyValueStorage.lrange(key); // then expect(list).to.be.empty; @@ -190,13 +190,13 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#ttl', function () { it('should retrieve the remaining expiration time from a list', async function () { // given - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when const key = 'key:lpush'; - await inMemoryTemporaryStorage.lpush(key, 'value'); - await inMemoryTemporaryStorage.expire({ key, expirationDelaySeconds: 120 }); - const remainingExpirationSeconds = await inMemoryTemporaryStorage.ttl(key); + await inMemoryKeyValueStorage.lpush(key, 'value'); + await inMemoryKeyValueStorage.expire({ key, expirationDelaySeconds: 120 }); + const remainingExpirationSeconds = await inMemoryKeyValueStorage.ttl(key); // then expect(remainingExpirationSeconds).to.be.above(Date.now()); @@ -206,10 +206,10 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#lpush', function () { it('should add value into key list', async function () { // given - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when - const length = await inMemoryTemporaryStorage.lpush('key:lpush', 'value'); + const length = await inMemoryKeyValueStorage.lpush('key:lpush', 'value'); // then expect(length).to.equal(1); @@ -219,15 +219,15 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#lrem', function () { it('should remove values into key list', async function () { // given - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when const key = 'key:lrem'; - await inMemoryTemporaryStorage.lpush(key, 'value1'); - await inMemoryTemporaryStorage.lpush(key, 'value2'); - await inMemoryTemporaryStorage.lpush(key, 'value1'); + await inMemoryKeyValueStorage.lpush(key, 'value1'); + await inMemoryKeyValueStorage.lpush(key, 'value2'); + await inMemoryKeyValueStorage.lpush(key, 'value1'); - const length = await inMemoryTemporaryStorage.lrem(key, 'value1'); + const length = await inMemoryKeyValueStorage.lrem(key, 'value1'); // then expect(length).to.equal(2); @@ -237,15 +237,15 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#lrange', function () { it('should return key values list', async function () { // given - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); // when const key = 'key:lrange'; - await inMemoryTemporaryStorage.lpush(key, 'value1'); - await inMemoryTemporaryStorage.lpush(key, 'value2'); - await inMemoryTemporaryStorage.lpush(key, 'value3'); + await inMemoryKeyValueStorage.lpush(key, 'value1'); + await inMemoryKeyValueStorage.lpush(key, 'value2'); + await inMemoryKeyValueStorage.lpush(key, 'value3'); - const values = await inMemoryTemporaryStorage.lrange(key); + const values = await inMemoryKeyValueStorage.lrange(key); // then expect(values).to.have.lengthOf(3); @@ -256,14 +256,14 @@ describe('Unit | Infrastructure | temporary-storage | InMemoryTemporaryStorage', describe('#keys', function () { it('should return matching keys', async function () { // given - const inMemoryTemporaryStorage = new InMemoryTemporaryStorage(); - inMemoryTemporaryStorage.save({ key: 'prefix:key1', value: true }); - inMemoryTemporaryStorage.save({ key: 'prefix:key2', value: true }); - inMemoryTemporaryStorage.save({ key: 'prefix:key3', value: true }); - inMemoryTemporaryStorage.save({ key: 'otherprefix:key4', value: true }); + const inMemoryKeyValueStorage = new InMemoryKeyValueStorage(); + inMemoryKeyValueStorage.save({ key: 'prefix:key1', value: true }); + inMemoryKeyValueStorage.save({ key: 'prefix:key2', value: true }); + inMemoryKeyValueStorage.save({ key: 'prefix:key3', value: true }); + inMemoryKeyValueStorage.save({ key: 'otherprefix:key4', value: true }); // when - const values = inMemoryTemporaryStorage.keys('prefix:*'); + const values = inMemoryKeyValueStorage.keys('prefix:*'); // then expect(values).to.deep.equal(['prefix:key1', 'prefix:key2', 'prefix:key3']); diff --git a/api/tests/shared/unit/infrastructure/temporary-storage/TemporaryStorage_test.js b/api/tests/shared/unit/infrastructure/key-value-storages/KeyValueStorage_test.js similarity index 73% rename from api/tests/shared/unit/infrastructure/temporary-storage/TemporaryStorage_test.js rename to api/tests/shared/unit/infrastructure/key-value-storages/KeyValueStorage_test.js index 2848e6fbfe7..cfd9d26cdb2 100644 --- a/api/tests/shared/unit/infrastructure/temporary-storage/TemporaryStorage_test.js +++ b/api/tests/shared/unit/infrastructure/key-value-storages/KeyValueStorage_test.js @@ -1,14 +1,14 @@ -import { TemporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/TemporaryStorage.js'; +import { KeyValueStorage } from '../../../../../src/shared/infrastructure/key-value-storages/KeyValueStorage.js'; import { expect, sinon } from '../../../../test-helper.js'; -describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', function () { +describe('Unit | Infrastructure | key-value-storage | KeyValueStorage', function () { describe('#save', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.save({ value: {}, expirationDelaySeconds: 1000 }); + const result = keyValueStorageInstance.save({ value: {}, expirationDelaySeconds: 1000 }); // then expect(result).to.be.rejected; @@ -18,10 +18,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#decrement', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.decrement('key'); + const result = keyValueStorageInstance.decrement('key'); // then expect(result).to.be.rejected; @@ -31,10 +31,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#increment', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.increment('key'); + const result = keyValueStorageInstance.increment('key'); // then expect(result).to.be.rejected; @@ -44,10 +44,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#get', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.get('key'); + const result = keyValueStorageInstance.get('key'); // then expect(result).to.be.rejected; @@ -57,10 +57,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#delete', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.delete('key'); + const result = keyValueStorageInstance.delete('key'); // then expect(result).to.be.rejected; @@ -70,7 +70,7 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#generateKey', function () { it('should return a key from static method', function () { // when - const result = TemporaryStorage.generateKey(); + const result = KeyValueStorage.generateKey(); // then expect(result).to.be.ok; @@ -82,7 +82,7 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio let prefixedStorage; beforeEach(function () { - class TestStorage extends TemporaryStorage { + class TestStorage extends KeyValueStorage { save = sinon.stub(); get = sinon.stub(); delete = sinon.stub(); @@ -171,10 +171,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#update', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.update('key', 'value'); + const result = keyValueStorageInstance.update('key', 'value'); // then expect(result).to.be.rejected; @@ -184,10 +184,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#quit', function () { it('should throw an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const call = () => temporaryStorageInstance.quit(); + const call = () => keyValueStorageInstance.quit(); // then expect(call).to.throw(); @@ -197,10 +197,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#ttl', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.ttl('key'); + const result = keyValueStorageInstance.ttl('key'); // then expect(result).to.be.rejected; @@ -210,10 +210,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#expire', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.expire({ key: 'key', expirationDelaySeconds: 120 }); + const result = keyValueStorageInstance.expire({ key: 'key', expirationDelaySeconds: 120 }); // then expect(result).to.be.rejected; @@ -223,10 +223,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#lpush', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.lpush({ key: 'key', value: 'value' }); + const result = keyValueStorageInstance.lpush({ key: 'key', value: 'value' }); // then expect(result).to.be.rejected; @@ -236,10 +236,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#lrem', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.lrem({ key: 'key', valueToRemove: 'valueToRemove' }); + const result = keyValueStorageInstance.lrem({ key: 'key', valueToRemove: 'valueToRemove' }); // then expect(result).to.be.rejected; @@ -249,10 +249,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#lrange', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.lrange('key'); + const result = keyValueStorageInstance.lrange('key'); // then expect(result).to.be.rejected; @@ -262,10 +262,10 @@ describe('Unit | Infrastructure | temporary-storage | TemporaryStorage', functio describe('#keys', function () { it('should reject an error (because this class actually mocks an interface)', function () { // given - const temporaryStorageInstance = new TemporaryStorage(); + const keyValueStorageInstance = new KeyValueStorage(); // when - const result = temporaryStorageInstance.keys('prefix:*'); + const result = keyValueStorageInstance.keys('prefix:*'); // then expect(result).to.be.rejected; diff --git a/api/tests/shared/unit/infrastructure/temporary-storage/RedisTemporaryStorage_test.js b/api/tests/shared/unit/infrastructure/key-value-storages/RedisKeyValueStorage_test.js similarity index 65% rename from api/tests/shared/unit/infrastructure/temporary-storage/RedisTemporaryStorage_test.js rename to api/tests/shared/unit/infrastructure/key-value-storages/RedisKeyValueStorage_test.js index c632fb3fe77..28003ea7d71 100644 --- a/api/tests/shared/unit/infrastructure/temporary-storage/RedisTemporaryStorage_test.js +++ b/api/tests/shared/unit/infrastructure/key-value-storages/RedisKeyValueStorage_test.js @@ -1,7 +1,7 @@ -import { RedisTemporaryStorage } from '../../../../../src/shared/infrastructure/temporary-storage/RedisTemporaryStorage.js'; +import { RedisKeyValueStorage } from '../../../../../src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js'; import { expect, sinon } from '../../../../test-helper.js'; -describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', function () { +describe('Unit | Infrastructure | key-value-storage | RedisKeyValueStorage', function () { const REDIS_URL = 'redis_url'; let clientStub; @@ -21,17 +21,17 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu keys: sinon.stub(), }; - sinon.stub(RedisTemporaryStorage, 'createClient').withArgs(REDIS_URL).returns(clientStub); + sinon.stub(RedisKeyValueStorage, 'createClient').withArgs(REDIS_URL, 'some-prefix:').returns(clientStub); }); describe('#constructor', function () { it('should call static method createClient', function () { // when - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // then - expect(RedisTemporaryStorage.createClient).to.have.been.called; - expect(redisTemporaryStorage._client).to.exist; + expect(RedisKeyValueStorage.createClient).to.have.been.called; + expect(redisKeyValueStorage._client).to.exist; }); }); @@ -41,19 +41,19 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const keyParameter = ' '; const value = { name: 'name' }; const expirationDelaySeconds = 1000; - sinon.spy(RedisTemporaryStorage, 'generateKey'); + sinon.spy(RedisKeyValueStorage, 'generateKey'); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.save({ + await redisKeyValueStorage.save({ key: keyParameter, value, expirationDelaySeconds, }); // then - expect(RedisTemporaryStorage.generateKey).to.have.been.called; + expect(RedisKeyValueStorage.generateKey).to.have.been.called; }); it('should use passed key parameter if valid', async function () { @@ -61,19 +61,19 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const keyParameter = 'KEY-PARAMETER'; const value = { name: 'name' }; const expirationDelaySeconds = 1000; - sinon.spy(RedisTemporaryStorage, 'generateKey'); + sinon.spy(RedisKeyValueStorage, 'generateKey'); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.save({ + await redisKeyValueStorage.save({ key: keyParameter, value, expirationDelaySeconds, }); // then - expect(RedisTemporaryStorage.generateKey).not.have.been.called; + expect(RedisKeyValueStorage.generateKey).not.have.been.called; }); it('should call client set with value and EX parameters', async function () { @@ -82,10 +82,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const value = { name: 'name' }; const expirationDelaySeconds = 1000; clientStub.set.resolves(); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.save({ value, expirationDelaySeconds }); + await redisKeyValueStorage.save({ value, expirationDelaySeconds }); // then expect(clientStub.set).to.have.been.calledWithExactly( @@ -101,10 +101,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu it('should call client incr to increment value', async function () { // given const key = 'valueKey'; - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.increment(key); + await redisKeyValueStorage.increment(key); // then expect(clientStub.incr).to.have.been.calledWith(key); @@ -115,10 +115,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu it('should call client incr to decrement value', async function () { // given const key = 'valueKey'; - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.decrement(key); + await redisKeyValueStorage.decrement(key); // then expect(clientStub.decr).to.have.been.calledWith(key); @@ -131,10 +131,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const key = 'valueKey'; const value = { name: 'name' }; clientStub.get.withArgs(key).resolves(JSON.stringify(value)); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - const result = await redisTemporaryStorage.get(key); + const result = await redisKeyValueStorage.get(key); // then expect(clientStub.get).to.have.been.called; @@ -148,10 +148,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const KEEPTTL_PARAMETER = 'keepttl'; const key = 'valueKey'; const value = { name: 'name' }; - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.update(key, value); + await redisKeyValueStorage.update(key, value); // then expect(clientStub.set).to.have.been.calledWithExactly(sinon.match.any, JSON.stringify(value), KEEPTTL_PARAMETER); @@ -163,10 +163,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu // given const key = 'valueKey'; clientStub.del.withArgs(key).resolves(); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.delete(key); + await redisKeyValueStorage.delete(key); // then expect(clientStub.del).to.have.been.called; @@ -179,10 +179,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const key = 'key'; const expirationDelaySeconds = 120; clientStub.expire.resolves(); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.expire({ key, expirationDelaySeconds }); + await redisKeyValueStorage.expire({ key, expirationDelaySeconds }); // then expect(clientStub.expire).to.have.been.calledWithExactly(key, expirationDelaySeconds); @@ -194,10 +194,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu // given const key = 'key'; clientStub.ttl.resolves(12); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - const remainingTtl = await redisTemporaryStorage.ttl(key); + const remainingTtl = await redisKeyValueStorage.ttl(key); // then expect(clientStub.ttl).to.have.been.calledWithExactly(key); @@ -211,10 +211,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const key = 'key'; const value = 'valueToAdd'; clientStub.lpush.resolves(); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.lpush(key, value); + await redisKeyValueStorage.lpush(key, value); // then expect(clientStub.lpush).to.have.been.calledWithExactly('key', 'valueToAdd'); @@ -227,10 +227,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const key = 'key'; const value = 'valueToRemove'; clientStub.lrem.resolves(); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.lrem(key, value); + await redisKeyValueStorage.lrem(key, value); // then expect(clientStub.lrem).to.have.been.calledWithExactly('key', 0, 'valueToRemove'); @@ -244,10 +244,10 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu const start = 0; const stop = -1; clientStub.lrange.resolves(['value']); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - await redisTemporaryStorage.lrange(key, start, stop); + await redisKeyValueStorage.lrange(key, start, stop); // then expect(clientStub.lrange).to.have.been.calledWithExactly('key', 0, -1); @@ -258,11 +258,11 @@ describe('Unit | Infrastructure | temporary-storage | RedisTemporaryStorage', fu it('should call client keys and return matching keys', async function () { // given const pattern = 'prefix:*'; - clientStub.keys.withArgs(pattern).resolves(['temporary-storage:key1', 'temporary-storage:key2']); - const redisTemporaryStorage = new RedisTemporaryStorage(REDIS_URL); + clientStub.keys.withArgs(pattern).resolves(['some-prefix:key1', 'some-prefix:key2']); + const redisKeyValueStorage = new RedisKeyValueStorage(REDIS_URL, 'some-prefix:'); // when - const actualKeys = await redisTemporaryStorage.keys(pattern); + const actualKeys = await redisKeyValueStorage.keys(pattern); // then expect(actualKeys).to.deep.equal(['key1', 'key2']); From 0b8e767d90819466c87052882f30fd0cf32cf7b7 Mon Sep 17 00:00:00 2001 From: Guillaume Lagorce Date: Wed, 15 Jan 2025 17:11:09 +0100 Subject: [PATCH 3/7] add script to ease adding information banners --- api/banner/scripts/add.js | 50 +++++++++++++++++++ api/banner/scripts/remove.js | 28 +++++++++++ api/package.json | 2 + .../application/scripts/script-runner.js | 2 + .../RedisKeyValueStorage.js | 2 +- .../key-value-storages/index.js | 5 ++ .../information-banner-repository_test.js | 2 +- 7 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 api/banner/scripts/add.js create mode 100644 api/banner/scripts/remove.js diff --git a/api/banner/scripts/add.js b/api/banner/scripts/add.js new file mode 100644 index 00000000000..b98bbde8b6d --- /dev/null +++ b/api/banner/scripts/add.js @@ -0,0 +1,50 @@ +import { Script } from '../../src/shared/application/scripts/script.js'; +import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js'; +import { informationBannersStorage } from '../../src/shared/infrastructure/key-value-storages/index.js'; + +export class AddInformationBanners extends Script { + constructor() { + super({ + description: 'Add information banners data to Redis', + permanent: true, + options: { + target: { + type: 'string', + describe: 'application name we want to add information banners to', + required: true, + requiresArg: true, + }, + severity: { + type: 'string', + describe: 'severity of the message', + choices: ['error', 'warning', 'information'], + required: true, + requiresArg: true, + }, + message_fr: { + type: 'string', + describe: 'message content in French', + required: true, + requiresArg: true, + }, + message_en: { + type: 'string', + describe: 'message content in English', + required: true, + requiresArg: true, + }, + }, + }); + } + + async handle({ options }) { + const { target, severity, message_fr, message_en } = options; + const banners = (await informationBannersStorage.get(target)) ?? []; + + banners.push({ severity, message: `[fr]${message_fr}[/fr][en]${message_en}[/en]` }); + + await informationBannersStorage.save({ key: target, value: banners }); + } +} + +await ScriptRunner.execute(import.meta.url, AddInformationBanners); diff --git a/api/banner/scripts/remove.js b/api/banner/scripts/remove.js new file mode 100644 index 00000000000..53a8a262d7b --- /dev/null +++ b/api/banner/scripts/remove.js @@ -0,0 +1,28 @@ +import { Script } from '../../src/shared/application/scripts/script.js'; +import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js'; +import { informationBannersStorage } from '../../src/shared/infrastructure/key-value-storages/index.js'; + +export class RemoveInformationBanners extends Script { + constructor() { + super({ + description: 'Remove information banners data from Redis', + permanent: true, + options: { + target: { + type: 'string', + describe: 'application name we want to remove information banners from', + required: true, + requiresArg: true, + }, + }, + }); + } + + async handle({ options }) { + const { target } = options; + + await informationBannersStorage.delete(target); + } +} + +await ScriptRunner.execute(import.meta.url, RemoveInformationBanners); diff --git a/api/package.json b/api/package.json index 7ef5f814317..3073f3860fb 100644 --- a/api/package.json +++ b/api/package.json @@ -127,6 +127,8 @@ "scripts": { "clean": "rm -rf node_modules", "cache:refresh": "node scripts/refresh-cache", + "banner:add": "node banner/scripts/add.js", + "banner:remove": "node banner/scripts/remove.js", "datamart:create": "node scripts/datamart/create-datamart", "datamart:delete": "node scripts/datamart/drop-datamart", "datamart:new-migration": "npx knex --knexfile datamart/knexfile.js migrate:make --stub $PWD/db/template.js $migrationname", diff --git a/api/src/shared/application/scripts/script-runner.js b/api/src/shared/application/scripts/script-runner.js index ab2f2eb4cbd..7b2d25afd4a 100644 --- a/api/src/shared/application/scripts/script-runner.js +++ b/api/src/shared/application/scripts/script-runner.js @@ -6,6 +6,7 @@ import yargs from 'yargs/yargs'; import { disconnect } from '../../../../db/knex-database-connection.js'; import { learningContentCache } from '../../infrastructure/caches/learning-content-cache.js'; +import { quitAllStorages } from '../../infrastructure/key-value-storages/index.js'; import { logger as defaultLogger } from '../../infrastructure/utils/logger.js'; function isRunningFromCli(scriptFileUrl) { @@ -55,6 +56,7 @@ export class ScriptRunner { } finally { await disconnect(); await learningContentCache.quit(); + await quitAllStorages(); } } } diff --git a/api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js b/api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js index 937891ce14b..643a4b4bdb2 100644 --- a/api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js +++ b/api/src/shared/infrastructure/key-value-storages/RedisKeyValueStorage.js @@ -18,7 +18,7 @@ class RedisKeyValueStorage extends KeyValueStorage { } static createClient(redisUrl, prefix) { - return new RedisClient(redisUrl, { name: 'temporary-storage', prefix }); + return new RedisClient(redisUrl, { name: prefix, prefix }); } async save({ key, value, expirationDelaySeconds }) { diff --git a/api/src/shared/infrastructure/key-value-storages/index.js b/api/src/shared/infrastructure/key-value-storages/index.js index 40e12e9f603..62743fea14c 100644 --- a/api/src/shared/infrastructure/key-value-storages/index.js +++ b/api/src/shared/infrastructure/key-value-storages/index.js @@ -15,3 +15,8 @@ function _createKeyValueStorage({ prefix }) { export const temporaryStorage = _createKeyValueStorage({ prefix: 'temporary-storage:' }); export const informationBannersStorage = _createKeyValueStorage({ prefix: 'information-banners:' }); + +export async function quitAllStorages() { + await temporaryStorage.quit(); + await informationBannersStorage.quit(); +} diff --git a/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js b/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js index a20f31bf016..68fa14f999d 100644 --- a/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js +++ b/api/tests/banner/integration/infrastructure/repositories/information-banner-repository_test.js @@ -22,7 +22,7 @@ describe('Integration | Infrastructure | Repository | Banner | information-banne const id = 'pix-other-target'; const storedBanner = { message: '[fr]Texte de la bannière[/fr][en]Banner text[/en]', severity: 'info' }; - await informationBannersStorage.save({ key: id, value: [storedBanner], expirationDelaySeconds: 10 }); + await informationBannersStorage.save({ key: id, value: [storedBanner] }); const expectedInformationBanner = domainBuilder.banner.buildInformationBanner({ id, From 2df9a88995b002f91163a3ddc064a81eb458904b Mon Sep 17 00:00:00 2001 From: Guillaume Lagorce Date: Wed, 15 Jan 2025 17:11:09 +0100 Subject: [PATCH 4/7] feat(certif): display information banners dynamically --- certif/app/components/information-banners.gjs | 11 +++++ certif/app/models/banner.js | 6 +++ certif/app/models/information-banner.js | 5 +++ certif/app/routes/application.js | 34 ++++++++++++++- certif/app/templates/application.hbs | 2 + certif/config/environment.js | 8 ++++ certif/mirage/config.js | 9 +++- certif/mirage/factories/information-banner.js | 12 ++++++ .../mirage/serializers/information-banner.js | 7 ++++ certif/tests/acceptance/application-test.js | 42 +++++++++++++++++++ .../components/information-banners-test.gjs | 37 ++++++++++++++++ 11 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 certif/app/components/information-banners.gjs create mode 100644 certif/app/models/banner.js create mode 100644 certif/app/models/information-banner.js create mode 100644 certif/mirage/factories/information-banner.js create mode 100644 certif/mirage/serializers/information-banner.js create mode 100644 certif/tests/acceptance/application-test.js create mode 100644 certif/tests/integration/components/information-banners-test.gjs diff --git a/certif/app/components/information-banners.gjs b/certif/app/components/information-banners.gjs new file mode 100644 index 00000000000..8fbbd8714f5 --- /dev/null +++ b/certif/app/components/information-banners.gjs @@ -0,0 +1,11 @@ +import PixBannerAlert from '@1024pix/pix-ui/components/pix-banner-alert'; + +import textWithMultipleLang from '../helpers/text-with-multiple-lang.js'; + + diff --git a/certif/app/models/banner.js b/certif/app/models/banner.js new file mode 100644 index 00000000000..512dde756b8 --- /dev/null +++ b/certif/app/models/banner.js @@ -0,0 +1,6 @@ +import Model, { attr } from '@ember-data/model'; + +export default class Banner extends Model { + @attr() severity; + @attr() message; +} diff --git a/certif/app/models/information-banner.js b/certif/app/models/information-banner.js new file mode 100644 index 00000000000..504ddaa4085 --- /dev/null +++ b/certif/app/models/information-banner.js @@ -0,0 +1,5 @@ +import Model, { hasMany } from '@ember-data/model'; + +export default class InformationBanner extends Model { + @hasMany('banner', { async: false, inverse: null }) banners; +} diff --git a/certif/app/routes/application.js b/certif/app/routes/application.js index df2ab107870..e30d26d9157 100644 --- a/certif/app/routes/application.js +++ b/certif/app/routes/application.js @@ -1,11 +1,14 @@ +import { action } from '@ember/object'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; +import ENV from 'pix-certif/config/environment'; export default class ApplicationRoute extends Route { @service featureToggles; @service currentDomain; @service currentUser; @service session; + @service store; async beforeModel(transition) { await this.session.setup(); @@ -17,10 +20,39 @@ export default class ApplicationRoute extends Route { await this.session.handleLocale({ isFranceDomain, localeFromQueryParam, userLocale }); } - model() { + async model() { + const informationBanner = await this.store.findRecord('information-banner', `${ENV.APP.APPLICATION_NAME}`); return { title: this.currentDomain.isFranceDomain ? 'Pix Certif (France)' : 'Pix Certif (hors France)', headElement: document.querySelector('head'), + informationBanner, }; } + + afterModel() { + this.poller = setInterval(async () => { + try { + this.store.findRecord('information-banner', `${ENV.APP.APPLICATION_NAME}`); + } catch { + this.#stopPolling(); + } + }, ENV.APP.INFORMATION_BANNER_POLLING_TIME); + } + + deactivate() { + this.#stopPolling(); + } + + @action + error() { + this.#stopPolling(); + return true; + } + + #stopPolling() { + if (this.poller) { + clearInterval(this.poller); + this.poller = null; + } + } } diff --git a/certif/app/templates/application.hbs b/certif/app/templates/application.hbs index 015e8d07eb7..67eae80a2f1 100644 --- a/certif/app/templates/application.hbs +++ b/certif/app/templates/application.hbs @@ -6,6 +6,8 @@ + + {{outlet}} \ No newline at end of file diff --git a/certif/config/environment.js b/certif/config/environment.js index fc72ba8643f..e0ff12e243e 100644 --- a/certif/config/environment.js +++ b/certif/config/environment.js @@ -36,10 +36,18 @@ module.exports = function (environment) { APP: { API_HOST: process.env.API_HOST || '', + APPLICATION_NAME: process.env.APP || 'pix-certif-local', BANNER: { CONTENT: process.env.BANNER_CONTENT || '', TYPE: process.env.BANNER_TYPE || '', }, + INFORMATION_BANNER_POLLING_TIME: + 1000 * + _getEnvironmentVariableAsNumber({ + environmentVariableName: process.env.INFORMATION_BANNER_POLLING_TIME, + defaultValue: 10, + minValue: 2, + }), PIX_APP_URL_WITHOUT_EXTENSION: process.env.PIX_APP_URL_WITHOUT_EXTENSION || 'https://app.pix.', API_ERROR_MESSAGES: { BAD_REQUEST: { diff --git a/certif/mirage/config.js b/certif/mirage/config.js index 2081869ed28..6c17346ffb9 100644 --- a/certif/mirage/config.js +++ b/certif/mirage/config.js @@ -22,8 +22,10 @@ export default function makeServer(config) { logging: true, urlPrefix: 'http://localhost:3000', }; + const server = createServer(finalConfig); + server.create('information-banner', 'withoutBanners'); - return createServer(finalConfig); + return server; } function routes() { @@ -388,6 +390,11 @@ function routes() { _configureCertificationCenterInvitationRoutes(this); _configureCertificationCenterMemberRoutes(this); + + this.get('/information-banners/:target', (schema, request) => { + const { target } = request.params; + return schema.informationBanners.find(target); + }); } function _configureCertificationCenterInvitationRoutes(context) { diff --git a/certif/mirage/factories/information-banner.js b/certif/mirage/factories/information-banner.js new file mode 100644 index 00000000000..408505d5c49 --- /dev/null +++ b/certif/mirage/factories/information-banner.js @@ -0,0 +1,12 @@ +import { Factory, trait } from 'miragejs'; +import ENV from 'pix-certif/config/environment'; + +export default Factory.extend({ + id() { + return ENV.APP.APPLICATION_NAME; + }, + + withoutBanners: trait({ + banners: [], + }), +}); diff --git a/certif/mirage/serializers/information-banner.js b/certif/mirage/serializers/information-banner.js new file mode 100644 index 00000000000..5f19361be56 --- /dev/null +++ b/certif/mirage/serializers/information-banner.js @@ -0,0 +1,7 @@ +import ApplicationSerializer from './application'; + +const include = ['banners']; + +export default ApplicationSerializer.extend({ + include, +}); diff --git a/certif/tests/acceptance/application-test.js b/certif/tests/acceptance/application-test.js new file mode 100644 index 00000000000..4b461beb82e --- /dev/null +++ b/certif/tests/acceptance/application-test.js @@ -0,0 +1,42 @@ +import { visit } from '@1024pix/ember-testing-library'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +module('Application', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'fr'); + + module('When there are no information banners', function () { + test('it should not display any banner', async function (assert) { + // given + server.create('information-banner', 'withoutBanners', { id: 'pix-certif-local' }); + + // when + const screen = await visit(`/`); + + // then + assert.dom(screen.queryByRole('alert')).doesNotExist(); + }); + }); + + module('When there is an information banner', function () { + test('it should display it', async function (assert) { + // given + const banner = server.create('banner', { + id: 'pix-certif-local:1', + severity: 'info', + message: '[en]some text[/en][fr]du texte[/fr]', + }); + server.create('information-banner', { id: 'pix-certif-local', banners: [banner] }); + + // when + const screen = await visit(`/`); + + // then + assert.dom(screen.getByRole('alert')).exists(); + }); + }); +}); diff --git a/certif/tests/integration/components/information-banners-test.gjs b/certif/tests/integration/components/information-banners-test.gjs new file mode 100644 index 00000000000..a83cde360a6 --- /dev/null +++ b/certif/tests/integration/components/information-banners-test.gjs @@ -0,0 +1,37 @@ +import { render } from '@1024pix/ember-testing-library'; +import InformationBanners from 'pix-certif/components/information-banners'; +import { module, test } from 'qunit'; + +import setupIntlRenderingTest from '../../helpers/setup-intl-rendering'; + +module('Integration | Component | information-banners', function (hooks) { + setupIntlRenderingTest(hooks); + + test('should not display the banner when no banners', async function (assert) { + // given + const banners = []; + + // when + const screen = await render(); + + // then + assert.dom(screen.queryByRole('alert')).doesNotExist(); + }); + + test('should display the information banner', async function (assert) { + // given + const banners = [ + { + severity: 'information', + message: '[fr]texte de la bannière d‘information[/fr][en]information banner text[/en]', + }, + ]; + + // when + const screen = await render(); + + // then + assert.dom(screen.getByText('texte de la bannière d‘information')).exists(); + assert.dom(screen.getByRole('alert')).exists(); + }); +}); From a40fc5fc96db970528a322f1482b95320e5abf05 Mon Sep 17 00:00:00 2001 From: Guillaume Lagorce Date: Thu, 16 Jan 2025 11:16:44 +0100 Subject: [PATCH 5/7] feat(mon-pix): display information banners dynamically --- .../app/components/information-banners.gjs | 11 +++++ mon-pix/app/models/banner.js | 6 +++ mon-pix/app/models/information-banner.js | 5 +++ mon-pix/app/routes/application.js | 27 +++++++++++- mon-pix/app/templates/application.hbs | 1 + mon-pix/config/environment.js | 8 ++++ mon-pix/mirage/config.js | 8 +++- .../mirage/factories/information-banner.js | 12 ++++++ .../mirage/routes/get-information-banners.js | 4 ++ .../mirage/serializers/information-banner.js | 7 ++++ mon-pix/tests/acceptance/application-test.js | 42 +++++++++++++++++++ .../components/information-banners-test.gjs | 37 ++++++++++++++++ 12 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 mon-pix/app/components/information-banners.gjs create mode 100644 mon-pix/app/models/banner.js create mode 100644 mon-pix/app/models/information-banner.js create mode 100644 mon-pix/mirage/factories/information-banner.js create mode 100644 mon-pix/mirage/routes/get-information-banners.js create mode 100644 mon-pix/mirage/serializers/information-banner.js create mode 100644 mon-pix/tests/acceptance/application-test.js create mode 100644 mon-pix/tests/integration/components/information-banners-test.gjs diff --git a/mon-pix/app/components/information-banners.gjs b/mon-pix/app/components/information-banners.gjs new file mode 100644 index 00000000000..8fbbd8714f5 --- /dev/null +++ b/mon-pix/app/components/information-banners.gjs @@ -0,0 +1,11 @@ +import PixBannerAlert from '@1024pix/pix-ui/components/pix-banner-alert'; + +import textWithMultipleLang from '../helpers/text-with-multiple-lang.js'; + + diff --git a/mon-pix/app/models/banner.js b/mon-pix/app/models/banner.js new file mode 100644 index 00000000000..512dde756b8 --- /dev/null +++ b/mon-pix/app/models/banner.js @@ -0,0 +1,6 @@ +import Model, { attr } from '@ember-data/model'; + +export default class Banner extends Model { + @attr() severity; + @attr() message; +} diff --git a/mon-pix/app/models/information-banner.js b/mon-pix/app/models/information-banner.js new file mode 100644 index 00000000000..504ddaa4085 --- /dev/null +++ b/mon-pix/app/models/information-banner.js @@ -0,0 +1,5 @@ +import Model, { hasMany } from '@ember-data/model'; + +export default class InformationBanner extends Model { + @hasMany('banner', { async: false, inverse: null }) banners; +} diff --git a/mon-pix/app/routes/application.js b/mon-pix/app/routes/application.js index 84e99e896a0..fa32dc1af86 100644 --- a/mon-pix/app/routes/application.js +++ b/mon-pix/app/routes/application.js @@ -1,6 +1,7 @@ import { action } from '@ember/object'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; +import ENV from 'mon-pix/config/environment'; export default class ApplicationRoute extends Route { @service authentication; @@ -10,6 +11,7 @@ export default class ApplicationRoute extends Route { @service session; @service splash; @service metrics; + @service store; activate() { this.splash.hide(); @@ -28,15 +30,38 @@ export default class ApplicationRoute extends Route { await this.session.handleUserLanguageAndLocale(transition); } - model() { + async model() { + const informationBanner = await this.store.findRecord('information-banner', `${ENV.APP.APPLICATION_NAME}`); return { headElement: document.querySelector('head'), + informationBanner, }; } + afterModel() { + this.poller = setInterval(async () => { + try { + this.store.findRecord('information-banner', `${ENV.APP.APPLICATION_NAME}`); + } catch { + this.#stopPolling(); + } + }, ENV.APP.INFORMATION_BANNER_POLLING_TIME); + } + + deactivate() { + this.#stopPolling(); + } @action error(error) { + this.#stopPolling(); const isUnauthorizedError = error?.errors?.some((err) => err.status === '401'); return !isUnauthorizedError; } + + #stopPolling() { + if (this.poller) { + clearInterval(this.poller); + this.poller = null; + } + } } diff --git a/mon-pix/app/templates/application.hbs b/mon-pix/app/templates/application.hbs index 9b70064bf78..55be14ff96d 100644 --- a/mon-pix/app/templates/application.hbs +++ b/mon-pix/app/templates/application.hbs @@ -9,6 +9,7 @@
+
{{outlet}} diff --git a/mon-pix/config/environment.js b/mon-pix/config/environment.js index e15fb8c9d72..b76674c7da3 100644 --- a/mon-pix/config/environment.js +++ b/mon-pix/config/environment.js @@ -42,6 +42,7 @@ module.exports = function (environment) { // Here you can pass flags/options to your application instance // when it is created API_HOST: process.env.API_HOST || '', + APPLICATION_NAME: process.env.APP || 'pix-app-local', FT_FOCUS_CHALLENGE_ENABLED: _isFeatureEnabled(process.env.FT_FOCUS_CHALLENGE_ENABLED) || false, isTimerCountdownEnabled: true, LOAD_EXTERNAL_SCRIPT: true, @@ -58,6 +59,13 @@ module.exports = function (environment) { }), BANNER_CONTENT: process.env.BANNER_CONTENT || '', BANNER_TYPE: process.env.BANNER_TYPE || '', + INFORMATION_BANNER_POLLING_TIME: + 1000 * + _getEnvironmentVariableAsNumber({ + environmentVariableName: process.env.INFORMATION_BANNER_POLLING_TIME, + defaultValue: 10, + minValue: 2, + }), IS_PROD_ENVIRONMENT: (process.env.REVIEW_APP === 'false' && environment === 'production') || false, EMBED_ALLOWED_ORIGINS: ( process.env.EMBED_ALLOWED_ORIGINS || 'https://epreuves.pix.fr,https://1024pix.github.io' diff --git a/mon-pix/mirage/config.js b/mon-pix/mirage/config.js index b48aff4d325..f6b6c8d3f7f 100644 --- a/mon-pix/mirage/config.js +++ b/mon-pix/mirage/config.js @@ -20,6 +20,7 @@ import getChallenge from './routes/get-challenge'; import getChallenges from './routes/get-challenges'; import getCompetenceEvaluationsByAssessment from './routes/get-competence-evaluations-by-assessment'; import getFeatureToggles from './routes/get-feature-toggles'; +import getInformationBanners from './routes/get-information-banners'; import getProgression from './routes/get-progression'; import getQuestResults from './routes/get-quest-results'; import getScorecard from './routes/get-scorecard'; @@ -50,7 +51,10 @@ export default function makeServer(config) { urlPrefix: 'http://localhost:3000', }; - return createServer(finalConfig); + const server = createServer(finalConfig); + server.create('information-banner', 'withoutBanners'); + + return server; } /* eslint max-statements: off */ @@ -119,4 +123,6 @@ function routes() { '/certification-candidates/:certificationCandidateId/validate-certification-instructions', updateCertificationCandidates, ); + + this.get('/information-banners/:target', getInformationBanners); } diff --git a/mon-pix/mirage/factories/information-banner.js b/mon-pix/mirage/factories/information-banner.js new file mode 100644 index 00000000000..1ff541ea01b --- /dev/null +++ b/mon-pix/mirage/factories/information-banner.js @@ -0,0 +1,12 @@ +import { Factory, trait } from 'miragejs'; +import ENV from 'mon-pix/config/environment'; + +export default Factory.extend({ + id() { + return ENV.APP.APPLICATION_NAME; + }, + + withoutBanners: trait({ + banners: [], + }), +}); diff --git a/mon-pix/mirage/routes/get-information-banners.js b/mon-pix/mirage/routes/get-information-banners.js new file mode 100644 index 00000000000..289a4f2251a --- /dev/null +++ b/mon-pix/mirage/routes/get-information-banners.js @@ -0,0 +1,4 @@ +export default function (schema, request) { + const { target } = request.params; + return schema.informationBanners.find(target); +} diff --git a/mon-pix/mirage/serializers/information-banner.js b/mon-pix/mirage/serializers/information-banner.js new file mode 100644 index 00000000000..5f19361be56 --- /dev/null +++ b/mon-pix/mirage/serializers/information-banner.js @@ -0,0 +1,7 @@ +import ApplicationSerializer from './application'; + +const include = ['banners']; + +export default ApplicationSerializer.extend({ + include, +}); diff --git a/mon-pix/tests/acceptance/application-test.js b/mon-pix/tests/acceptance/application-test.js new file mode 100644 index 00000000000..62e0806954a --- /dev/null +++ b/mon-pix/tests/acceptance/application-test.js @@ -0,0 +1,42 @@ +import { visit } from '@1024pix/ember-testing-library'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +module('Acceptance | Application', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'fr'); + + module('When there are no information banners', function () { + test('it should not display any banner', async function (assert) { + // given + server.create('information-banner', 'withoutBanners', { id: 'pix-app-local' }); + + // when + const screen = await visit(`/`); + + // then + assert.dom(screen.queryByRole('alert')).doesNotExist(); + }); + }); + + module('When there is an information banner', function () { + test('it should display it', async function (assert) { + // given + const banner = server.create('banner', { + id: 'pix-app-local:1', + severity: 'info', + message: '[en]some text[/en][fr]du texte[/fr]', + }); + server.create('information-banner', { id: 'pix-app-local', banners: [banner] }); + + // when + const screen = await visit(`/`); + + // then + assert.dom(screen.getByRole('alert')).exists(); + }); + }); +}); diff --git a/mon-pix/tests/integration/components/information-banners-test.gjs b/mon-pix/tests/integration/components/information-banners-test.gjs new file mode 100644 index 00000000000..c09db63b14d --- /dev/null +++ b/mon-pix/tests/integration/components/information-banners-test.gjs @@ -0,0 +1,37 @@ +import { render } from '@1024pix/ember-testing-library'; +import InformationBanners from 'mon-pix/components/information-banners'; +import { module, test } from 'qunit'; + +import setupIntlRenderingTest from '../../helpers/setup-intl-rendering'; + +module('Integration | Component | information-banners', function (hooks) { + setupIntlRenderingTest(hooks); + + test('should not display the banner when no banners', async function (assert) { + // given + const banners = []; + + // when + const screen = await render(); + + // then + assert.dom(screen.queryByRole('alert')).doesNotExist(); + }); + + test('should display the information banner', async function (assert) { + // given + const banners = [ + { + severity: 'information', + message: '[fr]texte de la bannière d‘information[/fr][en]information banner text[/en]', + }, + ]; + + // when + const screen = await render(); + + // then + assert.dom(screen.getByText('texte de la bannière d‘information')).exists(); + assert.dom(screen.getByRole('alert')).exists(); + }); +}); From c6a56550d02539dc58a2a7e7d594d4d757991484 Mon Sep 17 00:00:00 2001 From: Guillaume Lagorce Date: Thu, 16 Jan 2025 14:45:52 +0100 Subject: [PATCH 6/7] feat(orga): display information banners dynamically --- .../components/banner/information-banners.gjs | 11 +++++ orga/app/models/banner.js | 6 +++ orga/app/models/information-banner.js | 5 +++ orga/app/routes/application.js | 34 ++++++++++++++- orga/app/templates/application.hbs | 1 + orga/config/environment.js | 8 ++++ orga/mirage/config.js | 12 +++++- orga/mirage/factories/information-banner.js | 12 ++++++ orga/mirage/serializers/information-banner.js | 7 ++++ orga/tests/acceptance/application-test.js | 42 +++++++++++++++++++ .../components/information-banners-test.gjs | 37 ++++++++++++++++ 11 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 orga/app/components/banner/information-banners.gjs create mode 100644 orga/app/models/banner.js create mode 100644 orga/app/models/information-banner.js create mode 100644 orga/mirage/factories/information-banner.js create mode 100644 orga/mirage/serializers/information-banner.js create mode 100644 orga/tests/acceptance/application-test.js create mode 100644 orga/tests/integration/components/information-banners-test.gjs diff --git a/orga/app/components/banner/information-banners.gjs b/orga/app/components/banner/information-banners.gjs new file mode 100644 index 00000000000..aba962dd44f --- /dev/null +++ b/orga/app/components/banner/information-banners.gjs @@ -0,0 +1,11 @@ +import PixBannerAlert from '@1024pix/pix-ui/components/pix-banner-alert'; + +import textWithMultipleLang from '../../helpers/text-with-multiple-lang.js'; + + diff --git a/orga/app/models/banner.js b/orga/app/models/banner.js new file mode 100644 index 00000000000..512dde756b8 --- /dev/null +++ b/orga/app/models/banner.js @@ -0,0 +1,6 @@ +import Model, { attr } from '@ember-data/model'; + +export default class Banner extends Model { + @attr() severity; + @attr() message; +} diff --git a/orga/app/models/information-banner.js b/orga/app/models/information-banner.js new file mode 100644 index 00000000000..504ddaa4085 --- /dev/null +++ b/orga/app/models/information-banner.js @@ -0,0 +1,5 @@ +import Model, { hasMany } from '@ember-data/model'; + +export default class InformationBanner extends Model { + @hasMany('banner', { async: false, inverse: null }) banners; +} diff --git a/orga/app/routes/application.js b/orga/app/routes/application.js index b46c4097871..aef8586b75e 100644 --- a/orga/app/routes/application.js +++ b/orga/app/routes/application.js @@ -1,7 +1,10 @@ +import { action } from '@ember/object'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; +import ENV from 'pix-orga/config/environment'; export default class ApplicationRoute extends Route { + @service('store') store; @service featureToggles; @service currentDomain; @service currentUser; @@ -20,10 +23,39 @@ export default class ApplicationRoute extends Route { await this.session.handleLocale({ isFranceDomain, localeFromQueryParam, userLocale }); } - model() { + async model() { + const informationBanner = await this.store.findRecord('information-banner', `${ENV.APP.APPLICATION_NAME}`); return { title: this.currentDomain.isFranceDomain ? 'Pix Orga (France)' : 'Pix Orga (hors France)', headElement: document.querySelector('head'), + informationBanner, }; } + + afterModel() { + this.poller = setInterval(async () => { + try { + this.store.findRecord('information-banner', `${ENV.APP.APPLICATION_NAME}`); + } catch { + this.#stopPolling(); + } + }, ENV.APP.INFORMATION_BANNER_POLLING_TIME); + } + + deactivate() { + this.#stopPolling(); + } + + @action + error() { + this.#stopPolling(); + return true; + } + + #stopPolling() { + if (this.poller) { + clearInterval(this.poller); + this.poller = null; + } + } } diff --git a/orga/app/templates/application.hbs b/orga/app/templates/application.hbs index c193fa4bc9a..1f77d8f59f8 100644 --- a/orga/app/templates/application.hbs +++ b/orga/app/templates/application.hbs @@ -5,5 +5,6 @@ {{/in-element}} + {{outlet}} \ No newline at end of file diff --git a/orga/config/environment.js b/orga/config/environment.js index b92ace0b942..63dd022d6f2 100644 --- a/orga/config/environment.js +++ b/orga/config/environment.js @@ -31,9 +31,17 @@ module.exports = function (environment) { APP: { API_HOST: process.env.API_HOST || '', + APPLICATION_NAME: process.env.APP || 'pix-orga-local', BANNER_CONTENT: process.env.BANNER_CONTENT || '', CERTIFICATION_BANNER_DISPLAY_DATES: process.env.CERTIFICATION_BANNER_DISPLAY_DATES || '', BANNER_TYPE: process.env.BANNER_TYPE || '', + INFORMATION_BANNER_POLLING_TIME: + 1000 * + _getEnvironmentVariableAsNumber({ + environmentVariableName: process.env.INFORMATION_BANNER_POLLING_TIME, + defaultValue: 10, + minValue: 2, + }), CAMPAIGNS_ROOT_URL: process.env.CAMPAIGNS_ROOT_URL, MAX_CONCURRENT_AJAX_CALLS: _getEnvironmentVariableAsNumber({ environmentVariableName: 'MAX_CONCURRENT_AJAX_CALLS', diff --git a/orga/mirage/config.js b/orga/mirage/config.js index cc8c17a98f1..ffded1dff16 100644 --- a/orga/mirage/config.js +++ b/orga/mirage/config.js @@ -34,7 +34,10 @@ export default function makeServer(config) { urlPrefix: 'http://localhost:3000', }; - return createServer(finalConfig); + const server = createServer(finalConfig); + server.create('information-banner', 'withoutBanners'); + + return server; } /* eslint ember/no-get: off */ @@ -573,4 +576,11 @@ function routes() { return new Response(204); }); + + this.get('/information-banners/:target', (schema, request) => { + const { target } = request.params; + console.log(target); + console.log(schema.informationBanners.all()); + return schema.informationBanners.find(target); + }); } diff --git a/orga/mirage/factories/information-banner.js b/orga/mirage/factories/information-banner.js new file mode 100644 index 00000000000..446b61ecd85 --- /dev/null +++ b/orga/mirage/factories/information-banner.js @@ -0,0 +1,12 @@ +import { Factory, trait } from 'miragejs'; +import ENV from 'pix-orga/config/environment'; + +export default Factory.extend({ + id() { + return ENV.APP.APPLICATION_NAME; + }, + + withoutBanners: trait({ + banners: [], + }), +}); diff --git a/orga/mirage/serializers/information-banner.js b/orga/mirage/serializers/information-banner.js new file mode 100644 index 00000000000..5f19361be56 --- /dev/null +++ b/orga/mirage/serializers/information-banner.js @@ -0,0 +1,7 @@ +import ApplicationSerializer from './application'; + +const include = ['banners']; + +export default ApplicationSerializer.extend({ + include, +}); diff --git a/orga/tests/acceptance/application-test.js b/orga/tests/acceptance/application-test.js new file mode 100644 index 00000000000..aa853524c15 --- /dev/null +++ b/orga/tests/acceptance/application-test.js @@ -0,0 +1,42 @@ +import { visit } from '@1024pix/ember-testing-library'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupApplicationTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +module('Application', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'fr'); + + module('When there are no information banners', function () { + test('it should not display any banner', async function (assert) { + // given + server.create('information-banner', 'withoutBanners', { id: 'pix-orga-local' }); + + // when + const screen = await visit(`/`); + + // then + assert.dom(screen.queryByRole('alert')).doesNotExist(); + }); + }); + + module('When there is an information banner', function () { + test('it should display it', async function (assert) { + // given + const banner = server.create('banner', { + id: 'pix-orga-local:1', + severity: 'info', + message: '[en]some text[/en][fr]du texte[/fr]', + }); + server.create('information-banner', { id: 'pix-orga-local', banners: [banner] }); + + // when + const screen = await visit(`/`); + + // then + assert.dom(screen.getByRole('alert')).exists(); + }); + }); +}); diff --git a/orga/tests/integration/components/information-banners-test.gjs b/orga/tests/integration/components/information-banners-test.gjs new file mode 100644 index 00000000000..22f993c08ed --- /dev/null +++ b/orga/tests/integration/components/information-banners-test.gjs @@ -0,0 +1,37 @@ +import { render } from '@1024pix/ember-testing-library'; +import InformationBanners from 'pix-orga/components/banner/information-banners'; +import { module, test } from 'qunit'; + +import setupIntlRenderingTest from '../../helpers/setup-intl-rendering'; + +module('Integration | Component | Banner | information-banners', function (hooks) { + setupIntlRenderingTest(hooks); + + test('should not display the banner when no banners', async function (assert) { + // given + const banners = []; + + // when + const screen = await render(); + + // then + assert.dom(screen.queryByRole('alert')).doesNotExist(); + }); + + test('should display the information banner', async function (assert) { + // given + const banners = [ + { + severity: 'information', + message: '[fr]texte de la bannière d‘information[/fr][en]information banner text[/en]', + }, + ]; + + // when + const screen = await render(); + + // then + assert.dom(screen.getByText('texte de la bannière d‘information')).exists(); + assert.dom(screen.getByRole('alert')).exists(); + }); +}); From 68275fa3f37107bdf94e1484d11d9fdaff990d1f Mon Sep 17 00:00:00 2001 From: Guillaume Lagorce Date: Thu, 16 Jan 2025 15:50:34 +0100 Subject: [PATCH 7/7] tech(api): set 30s cache to banners and feature toggles --- api/src/banner/application/banner-route.js | 5 ++++- api/src/shared/application/feature-toggles/index.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/src/banner/application/banner-route.js b/api/src/banner/application/banner-route.js index de1990b13bc..9b66112d74d 100644 --- a/api/src/banner/application/banner-route.js +++ b/api/src/banner/application/banner-route.js @@ -5,9 +5,12 @@ const register = async function (server) { { method: 'GET', path: '/api/information-banners/{target}', - config: { + options: { auth: false, handler: bannerController.getInformationBanner, + cache: { + expiresIn: 30 * 1000, + }, }, }, ]); diff --git a/api/src/shared/application/feature-toggles/index.js b/api/src/shared/application/feature-toggles/index.js index 1cde476e4a3..66a36cf54ee 100644 --- a/api/src/shared/application/feature-toggles/index.js +++ b/api/src/shared/application/feature-toggles/index.js @@ -5,10 +5,11 @@ const register = async function (server) { { method: 'GET', path: '/api/feature-toggles', - config: { + options: { auth: false, handler: featureToggleController.getActiveFeatures, tags: ['api'], + cache: { expiresIn: 30 * 1000 }, }, }, // TODO: Test route to be removed soon