From eadc7db0e7a8c9c67135b5f234dd4f1aea34803f Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:06:49 +0100 Subject: [PATCH 01/24] feat(api): allows logging at debug level for specific sections --- api/sample.env | 9 ++++++++ api/src/shared/config.js | 1 + api/src/shared/infrastructure/utils/logger.js | 21 ++++++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api/sample.env b/api/sample.env index 11097b42d85..a53cf2aaa45 100644 --- a/api/sample.env +++ b/api/sample.env @@ -410,6 +410,15 @@ LOG_FOR_HUMANS=true # LOG_FOR_HUMANS_FORMAT=compact +# Enables debug log level for a list of sections. +# Sections must be given separated by commas. +# micromatch syntax may be used to match several sections. +# presence: optional +# type: string +# default: none +# LOG_DEBUG=learningcontent:*,foo,bar + + # Trace email sending in the mailer # DEBUG="pix:mailer:email" diff --git a/api/src/shared/config.js b/api/src/shared/config.js index 6f254a5efad..cb0dac00bb4 100644 --- a/api/src/shared/config.js +++ b/api/src/shared/config.js @@ -265,6 +265,7 @@ const configuration = (function () { enableLogStartingEventDispatch: toBoolean(process.env.LOG_STARTING_EVENT_DISPATCH), enableLogEndingEventDispatch: toBoolean(process.env.LOG_ENDING_EVENT_DISPATCH), opsEventIntervalInSeconds: process.env.OPS_EVENT_INTERVAL_IN_SECONDS || 15, + debugSections: process.env.LOG_DEBUG?.split(',') ?? [], }, login: { temporaryBlockingThresholdFailureCount: _getNumber( diff --git a/api/src/shared/infrastructure/utils/logger.js b/api/src/shared/infrastructure/utils/logger.js index d02359db69b..495669c05a2 100644 --- a/api/src/shared/infrastructure/utils/logger.js +++ b/api/src/shared/infrastructure/utils/logger.js @@ -1,5 +1,6 @@ import isEmpty from 'lodash/isEmpty.js'; import omit from 'lodash/omit.js'; +import micromatch from 'micromatch'; import pino from 'pino'; import pretty from 'pino-pretty'; @@ -20,7 +21,7 @@ if (logging.logForHumans) { }); } -const logger = pino( +export const logger = pino( { level: logging.logLevel, redact: ['req.headers.authorization'], @@ -29,6 +30,22 @@ const logger = pino( prettyPrint, ); +/** + * Creates a child logger for a section. + * Debug may be enabled for a section using LOG_DEBUG. + * @param {string} section + * @param {pino.Bindings} bindings + * @param {pino.ChildLoggerOptions} options + */ +export function child(section, bindings, options) { + /** @type{Partial} */ + const optionsOverride = {}; + if (micromatch.isMatch(section, logging.debugSections)) { + optionsOverride.level = 'debug'; + } + return logger.child(bindings, { ...options, ...optionsOverride }); +} + function messageFormatCompact(log, messageKey, _logLevel, { colors }) { const message = log[messageKey]; const { err, req, res, responseTime } = log; @@ -71,5 +88,3 @@ function messageFormatCompact(log, messageKey, _logLevel, { colors }) { const details = !isEmpty(compactLog) ? colors.gray(JSON.stringify(compactLog)) : ''; return `${message} ${details}`; } - -export { logger }; From c9d47fcaeb8fb00bee478612d26622455febaf3d Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:42:13 +0100 Subject: [PATCH 02/24] feat(api): seeds learning content in pg --- .../data/common/learningcontent-builder.js | 44 +++++++++++++++++++ api/db/seeds/seed.js | 5 +++ 2 files changed, 49 insertions(+) create mode 100644 api/db/seeds/data/common/learningcontent-builder.js diff --git a/api/db/seeds/data/common/learningcontent-builder.js b/api/db/seeds/data/common/learningcontent-builder.js new file mode 100644 index 00000000000..37118c8f83c --- /dev/null +++ b/api/db/seeds/data/common/learningcontent-builder.js @@ -0,0 +1,44 @@ +import { lcmsClient } from '../../../../src/shared/infrastructure/lcms-client.js'; +import { logger } from '../../../../src/shared/infrastructure/utils/logger.js'; + +const MAX_SKILL_ALTERNATIVES_COUNT = 1; +const SKILL_STATUSES = ['actif', 'archivé']; +const CHALLENGE_STATUSES = ['validé', 'archivé']; +const GENEALOGY_PROTOTYPE = 'Prototype 1'; + +export async function learningContentBuilder({ databaseBuilder }) { + const learningContent = await lcmsClient.getLatestRelease(); + + const totalSkillsCount = learningContent.skills.length; + const totalChallengesCount = learningContent.challenges.length; + const totalTutorialsCount = learningContent.tutorials.length; + + learningContent.skills = learningContent.skills.filter((skill) => SKILL_STATUSES.includes(skill.status)); + + const skillIds = new Set(learningContent.skills.map((skill) => skill.id)); + const skillAlternativesCount = new Map(); + + learningContent.challenges = learningContent.challenges.filter((challenge) => { + const { skillId } = challenge; + if (!skillIds.has(skillId)) return false; + if (!CHALLENGE_STATUSES.includes(challenge.status)) return false; + if (challenge.genealogy === GENEALOGY_PROTOTYPE) return true; + const alternativeKey = `${skillId}:${challenge.locales[0]}`; + if (skillAlternativesCount.get(alternativeKey) ?? 0 >= MAX_SKILL_ALTERNATIVES_COUNT) return false; + skillAlternativesCount.set(alternativeKey, skillAlternativesCount.get(alternativeKey) + 1); + return true; + }); + + const tutorialIds = new Set( + learningContent.skills.flatMap((skill) => [...skill.tutorialIds, skill.learningMoreTutorialIds]), + ); + + learningContent.tutorials = learningContent.tutorials.filter((tutorial) => tutorialIds.has(tutorial.id)); + + logger.debug(`inserting ${learningContent.skills.length} skills out of ${totalSkillsCount}`); + logger.debug(`inserting ${learningContent.challenges.length} challenges out of ${totalChallengesCount}`); + logger.debug(`inserting ${learningContent.tutorials.length} tutorials out of ${totalTutorialsCount}`); + + databaseBuilder.factory.learningContent.build(learningContent); + await databaseBuilder.commit(); +} diff --git a/api/db/seeds/seed.js b/api/db/seeds/seed.js index 445f7f57667..0b8fc142264 100644 --- a/api/db/seeds/seed.js +++ b/api/db/seeds/seed.js @@ -3,6 +3,7 @@ import { DatabaseBuilder } from '../database-builder/database-builder.js'; import { commonBuilder } from './data/common/common-builder.js'; import { complementaryCertificationBuilder } from './data/common/complementary-certification-builder.js'; import { featuresBuilder } from './data/common/feature-builder.js'; +import { learningContentBuilder } from './data/common/learningcontent-builder.js'; import { organizationBuilder } from './data/common/organization-builder.js'; import { organizationLearnerImportFormat } from './data/common/organization-learner-import-formats.js'; import { tagsBuilder } from './data/common/tag-builder.js'; @@ -23,6 +24,10 @@ const seed = async function (knex) { const databaseBuilder = new DatabaseBuilder({ knex }); + // Learning content + logger.info('Seeding: Learning content'); + await learningContentBuilder({ databaseBuilder }); + // Common await commonBuilder({ databaseBuilder }); await tagsBuilder({ databaseBuilder }); From 3d4875c6073c1334669476b032dfb80ab8ab9960 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 11:26:26 +0100 Subject: [PATCH 03/24] feat(api): setup class to handle distributed caching and memoization Co-authored-by: Nicolas Lepage --- api/package-lock.json | 71 +++++++++++ api/package.json | 3 + .../caches/learning-content-pubsub.js | 45 +++++++ .../learning-content-repository.js | 120 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 api/src/shared/infrastructure/caches/learning-content-pubsub.js create mode 100644 api/src/shared/infrastructure/repositories/learning-content-repository.js diff --git a/api/package-lock.json b/api/package-lock.json index 519022dc287..60a6713eece 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -15,6 +15,8 @@ "@aws-sdk/lib-storage": "^3.121.0", "@aws-sdk/s3-request-presigner": "^3.145.0", "@getbrevo/brevo": "^2.1.1", + "@graphql-yoga/redis-event-target": "^3.0.1", + "@graphql-yoga/subscription": "^5.0.1", "@hapi/accept": "^6.0.0", "@hapi/boom": "^10.0.1", "@hapi/hapi": "^21.0.0", @@ -29,6 +31,7 @@ "axios": "^1.0.0", "bcrypt": "^5.0.1", "cron-parser": "^4.9.0", + "dataloader": "^2.2.2", "dayjs": "^1.11.5", "debug": "^4.3.4", "dotenv": "^16.0.1", @@ -1796,6 +1799,50 @@ "rewire": "^7.0.0" } }, + "node_modules/@graphql-yoga/redis-event-target": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@graphql-yoga/redis-event-target/-/redis-event-target-3.0.1.tgz", + "integrity": "sha512-AUQAQdJcJuRmvNdWh/v8ub8Pxq3J+++IOzAPHpzMOs81GEYQkatSrBiiESyiUdILazYoE4AYnKBVeKUVrH0Iyw==", + "license": "MIT", + "dependencies": { + "@graphql-yoga/typed-event-target": "^3.0.0", + "@whatwg-node/events": "^0.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "ioredis": "^5.0.6" + } + }, + "node_modules/@graphql-yoga/subscription": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-5.0.1.tgz", + "integrity": "sha512-1wCB1DfAnaLzS+IdoOzELGGnx1ODEg9nzQXFh4u2j02vAnne6d+v4A7HIH9EqzVdPLoAaMKXCZUUdKs+j3z1fg==", + "license": "MIT", + "dependencies": { + "@graphql-yoga/typed-event-target": "^3.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "^0.1.0", + "tslib": "^2.5.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@graphql-yoga/typed-event-target": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.0.tgz", + "integrity": "sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==", + "license": "MIT", + "dependencies": { + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.5.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@hapi/accept": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", @@ -2667,6 +2714,12 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@repeaterjs/repeater": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", + "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==", + "license": "MIT" + }, "node_modules/@sentry-internal/tracing": { "version": "7.118.0", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.118.0.tgz", @@ -3887,6 +3940,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@whatwg-node/events": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.2.tgz", + "integrity": "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.3.tgz", @@ -5068,6 +5133,12 @@ "node": ">=0.10" } }, + "node_modules/dataloader": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", + "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", + "license": "MIT" + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", diff --git a/api/package.json b/api/package.json index 78e59d21d7a..d44238b6f38 100644 --- a/api/package.json +++ b/api/package.json @@ -21,6 +21,8 @@ "@aws-sdk/lib-storage": "^3.121.0", "@aws-sdk/s3-request-presigner": "^3.145.0", "@getbrevo/brevo": "^2.1.1", + "@graphql-yoga/redis-event-target": "^3.0.1", + "@graphql-yoga/subscription": "^5.0.1", "@hapi/accept": "^6.0.0", "@hapi/boom": "^10.0.1", "@hapi/hapi": "^21.0.0", @@ -35,6 +37,7 @@ "axios": "^1.0.0", "bcrypt": "^5.0.1", "cron-parser": "^4.9.0", + "dataloader": "^2.2.2", "dayjs": "^1.11.5", "debug": "^4.3.4", "dotenv": "^16.0.1", diff --git a/api/src/shared/infrastructure/caches/learning-content-pubsub.js b/api/src/shared/infrastructure/caches/learning-content-pubsub.js new file mode 100644 index 00000000000..406cca6b33a --- /dev/null +++ b/api/src/shared/infrastructure/caches/learning-content-pubsub.js @@ -0,0 +1,45 @@ +import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'; +import { createPubSub } from '@graphql-yoga/subscription'; +import { Redis } from 'ioredis'; + +import { config } from '../../config.js'; + +/** + * @typedef {import('@graphql-yoga/subscription').PubSub<{ + * [key: string]: [message: object] + * }>} LearningContentPubSub + */ + +/** @type {LearningContentPubSub} */ +let pubSub; + +/** @type {import('ioredis').Redis} */ +let publishClient; +/** @type {import('ioredis').Redis} */ +let subscribeClient; + +export function getPubSub() { + if (pubSub) return pubSub; + + if (!config.caching.redisUrl) { + pubSub = createPubSub(); + return pubSub; + } + + publishClient = new Redis(config.caching.redisUrl); + subscribeClient = new Redis(config.caching.redisUrl); + + pubSub = createPubSub({ + eventTarget: createRedisEventTarget({ + publishClient, + subscribeClient, + }), + }); + + return pubSub; +} + +export async function quit() { + await publishClient?.quit(); + await subscribeClient?.quit(); +} diff --git a/api/src/shared/infrastructure/repositories/learning-content-repository.js b/api/src/shared/infrastructure/repositories/learning-content-repository.js new file mode 100644 index 00000000000..8a19ed2059c --- /dev/null +++ b/api/src/shared/infrastructure/repositories/learning-content-repository.js @@ -0,0 +1,120 @@ +import Dataloader from 'dataloader'; + +import { knex } from '../../../../db/knex-database-connection.js'; +import * as learningContentPubSub from '../caches/learning-content-pubsub.js'; + +export class LearningContentRepository { + #tableName; + #idType; + #dataloader; + #findCache; + #findCacheMiss; + + constructor({ tableName, idType = 'text', pubSub = learningContentPubSub.getPubSub() }) { + this.#tableName = tableName; + this.#idType = idType; + + this.#dataloader = new Dataloader((ids) => this.#batchLoad(ids), { + cacheMap: new LearningContentCache({ + name: `${tableName}:entities`, + pubSub, + }), + }); + + this.#findCache = new LearningContentCache({ + name: `${tableName}:results`, + pubSub, + }); + + this.#findCacheMiss = new Map(); + } + + async find(cacheKey, callback) { + return this.#findDtos(callback, cacheKey); + } + + async load(id) { + return this.#dataloader.load(id); + } + + async loadMany(ids) { + return this.#dataloader.loadMany(ids); + } + + #findDtos(callback, cacheKey) { + let dtos = this.#findCache.get(cacheKey); + if (dtos) return dtos; + + dtos = this.#findCacheMiss.get(cacheKey); + if (dtos) return dtos; + + dtos = this.#loadDtos(callback, cacheKey).finally(() => { + this.#findCacheMiss.delete(cacheKey); + }); + this.#findCacheMiss.set(cacheKey, dtos); + + return dtos; + } + + async #loadDtos(callback, cacheKey) { + const ids = await callback(knex.pluck(`${this.#tableName}.id`).from(this.#tableName)); + const dtos = await this.#dataloader.loadMany(ids); + this.#findCache.set(cacheKey, dtos); + return dtos; + } + + async #batchLoad(ids) { + const dtos = await knex + .select(`${this.#tableName}.*`) + .from(knex.raw(`unnest(?::${this.#idType}[]) with ordinality as ids(id, idx)`, [ids])) // eslint-disable-line knex/avoid-injections + .leftJoin(this.#tableName, `${this.#tableName}.id`, 'ids.id') + .orderBy('ids.idx'); + return dtos.map((dto) => (dto.id ? dto : null)); + } + + clearCache() { + this.#dataloader.clearAll(); + this.#findCache.clear(); + } +} + +class LearningContentCache { + #map = new Map(); + #pubSub; + #name; + + /** + * @param {{ + * pubSub: import('../caches/learning-content-pubsub.js').LearningContentPubSub + * name: string + * }} config + * @returns + */ + constructor({ pubSub, name }) { + this.#pubSub = pubSub; + this.#name = name; + + (async () => { + for await (const message of pubSub.subscribe(name)) { + if (message.type === 'clear') this.#map.clear(); + if (message.type === 'delete') this.#map.delete(this.message.key); + } + })(); + } + + get(key) { + return this.#map.get(key); + } + + set(key, value) { + return this.#map.set(key, value); + } + + delete(key) { + return this.#pubSub.publish(this.#name, { type: 'delete', key }); + } + + clear() { + return this.#pubSub.publish(this.#name, { type: 'clear' }); + } +} From f93628022315aa0d3470d0b35316539ddc21f9b7 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 11:27:17 +0100 Subject: [PATCH 04/24] feat(api): refacto frameworkRepository with new cache and to use PG Co-authored-by: Nicolas Lepage --- .../repositories/framework-repository.js | 65 ++++++++++----- .../learning-content/framework-datasource.js | 17 ---- .../datasources/learning-content/index.js | 2 - .../repositories/framework-repository_test.js | 54 +++++++++---- .../framework-datasource_test.js | 79 ------------------- api/tests/test-helper.js | 3 + 6 files changed, 85 insertions(+), 135 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/framework-datasource.js delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/framework-datasource_test.js diff --git a/api/lib/infrastructure/repositories/framework-repository.js b/api/lib/infrastructure/repositories/framework-repository.js index d11e9efd33c..56a1ee0ca85 100644 --- a/api/lib/infrastructure/repositories/framework-repository.js +++ b/api/lib/infrastructure/repositories/framework-repository.js @@ -1,15 +1,39 @@ -import _ from 'lodash'; - import { NotFoundError } from '../../../src/shared/domain/errors.js'; -import { Framework } from '../../../src/shared/domain/models/Framework.js'; -import { frameworkDatasource } from '../../../src/shared/infrastructure/datasources/learning-content/framework-datasource.js'; +import { Framework } from '../../../src/shared/domain/models/index.js'; +import { LearningContentRepository } from '../../../src/shared/infrastructure/repositories/learning-content-repository.js'; + +const TABLE_NAME = 'learningcontent.frameworks'; + +export async function list() { + const cacheKey = 'list'; + const listCallback = (knex) => knex.orderBy('name'); + const frameworkDtos = await getInstance().find(cacheKey, listCallback); + return frameworkDtos.map(toDomain); +} + +export async function getByName(name) { + const cacheKey = `getByName(${name})`; + const findByNameCallback = (knex) => knex.where('name', name).limit(1); + const [frameworkDto] = await getInstance().find(cacheKey, findByNameCallback); + if (!frameworkDto) { + throw new NotFoundError(`Framework not found for name ${name}`); + } + return toDomain(frameworkDto); +} -async function list() { - const frameworkDataObjects = await frameworkDatasource.list(); - return frameworkDataObjects.map(_toDomain); +export async function findByRecordIds(ids) { + const frameworkDtos = await getInstance().loadMany(ids); + return frameworkDtos + .filter((frameworkDto) => frameworkDto) + .sort(byId) + .map(toDomain); } -function _toDomain(frameworkData) { +export function clear() { + return getInstance().clear(); +} + +function toDomain(frameworkData) { return new Framework({ id: frameworkData.id, name: frameworkData.name, @@ -17,19 +41,20 @@ function _toDomain(frameworkData) { }); } -async function getByName(name) { - const framework = await frameworkDatasource.getByName(name); - - if (framework === undefined) { - throw new NotFoundError(`Framework not found for name ${name}`); - } - return _toDomain(framework); +export function clearCache() { + return getInstance().clearCache(); } -async function findByRecordIds(frameworkIds) { - const frameworkDatas = await frameworkDatasource.findByRecordIds(frameworkIds); - const frameworks = _.map(frameworkDatas, (frameworkData) => _toDomain(frameworkData)); - return _.orderBy(frameworks, (framework) => framework.name.toLowerCase()); +function byId(entityA, entityB) { + return entityA.id < entityB.id ? -1 : 1; } -export { findByRecordIds, getByName, list }; +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; +} diff --git a/api/src/shared/infrastructure/datasources/learning-content/framework-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/framework-datasource.js deleted file mode 100644 index 51417f11d7d..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/framework-datasource.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as datasource from './datasource.js'; - -const frameworkDatasource = datasource.extend({ - modelName: 'frameworks', - - async getByName(frameworkName) { - const framework = await this.list(); - return framework.find((frameworkData) => frameworkName === frameworkData.name); - }, - - async findByRecordIds(frameworkIds) { - const frameworks = await this.list(); - return frameworks.filter(({ id }) => frameworkIds.includes(id)); - }, -}); - -export { frameworkDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index 65d4bfe164c..aae263bde31 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -3,7 +3,6 @@ import { areaDatasource } from './area-datasource.js'; import { challengeDatasource } from './challenge-datasource.js'; import { competenceDatasource } from './competence-datasource.js'; import { courseDatasource } from './course-datasource.js'; -import { frameworkDatasource } from './framework-datasource.js'; import { skillDatasource } from './skill-datasource.js'; import { thematicDatasource } from './thematic-datasource.js'; import { tubeDatasource } from './tube-datasource.js'; @@ -13,7 +12,6 @@ export { challengeDatasource, competenceDatasource, courseDatasource, - frameworkDatasource, skillDatasource, thematicDatasource, tubeDatasource, diff --git a/api/tests/integration/infrastructure/repositories/framework-repository_test.js b/api/tests/integration/infrastructure/repositories/framework-repository_test.js index 82dabf2cd6b..b0cce183f8e 100644 --- a/api/tests/integration/infrastructure/repositories/framework-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/framework-repository_test.js @@ -1,38 +1,49 @@ import * as frameworkRepository from '../../../../lib/infrastructure/repositories/framework-repository.js'; import { NotFoundError } from '../../../../src/shared/domain/errors.js'; -import { catchErr, domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect, knex } from '../../../test-helper.js'; describe('Integration | Repository | framework-repository', function () { - const framework0 = { + const frameworkData0 = { id: 'recId0', name: 'mon framework 0', }; - const framework1 = { + const frameworkData1 = { id: 'recId1', name: 'mon framework 1', }; - const framework2 = { + const frameworkData2 = { id: 'recId2', name: 'mon framework 2', }; - const learningContent = { frameworks: [framework0, framework1, framework2] }; - beforeEach(async function () { - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildFramework(frameworkData0); + databaseBuilder.factory.learningContent.buildFramework(frameworkData2); + databaseBuilder.factory.learningContent.buildFramework(frameworkData1); + await databaseBuilder.commit(); }); describe('#list', function () { - it('should return all frameworks', async function () { + it('should return all frameworks when there are some in DB', async function () { // when const frameworks = await frameworkRepository.list(); // then - const expectedFramework0 = domainBuilder.buildFramework({ ...framework0, areas: [] }); - const expectedFramework1 = domainBuilder.buildFramework({ ...framework1, areas: [] }); - const expectedFramework2 = domainBuilder.buildFramework({ ...framework2, areas: [] }); + const expectedFramework0 = domainBuilder.buildFramework({ ...frameworkData0, areas: [] }); + const expectedFramework1 = domainBuilder.buildFramework({ ...frameworkData1, areas: [] }); + const expectedFramework2 = domainBuilder.buildFramework({ ...frameworkData2, areas: [] }); expect(frameworks).to.deepEqualArray([expectedFramework0, expectedFramework1, expectedFramework2]); }); + + it('should return an empty array when no frameworks in DB', async function () { + await knex('learningcontent.frameworks').truncate(); + + // when + const frameworks = await frameworkRepository.list(); + + // then + expect(frameworks).to.deep.equal([]); + }); }); describe('#getByName', function () { @@ -41,21 +52,21 @@ describe('Integration | Repository | framework-repository', function () { const framework = await frameworkRepository.getByName('mon framework 1'); // then - const expectedFramework1 = domainBuilder.buildFramework({ ...framework1, areas: [] }); - expect(framework).to.deepEqualInstance(expectedFramework1); + const expectedFramework = domainBuilder.buildFramework({ ...frameworkData1, areas: [] }); + expect(framework).to.deepEqualInstance(expectedFramework); }); context('when framework is not found', function () { it('should return a rejection', async function () { //given - const frameworkName = 'framework123'; + const frameworkName = 'frameworkData123'; // when const error = await catchErr(frameworkRepository.getByName)(frameworkName); // then expect(error).to.be.an.instanceof(NotFoundError); - expect(error.message).to.equal('Framework not found for name framework123'); + expect(error.message).to.equal('Framework not found for name frameworkData123'); }); }); }); @@ -66,11 +77,20 @@ describe('Integration | Repository | framework-repository', function () { const frameworks = await frameworkRepository.findByRecordIds(['recId2', 'recId0']); // then - const expectedFramework0 = domainBuilder.buildFramework({ ...framework0, areas: [] }); - const expectedFramework2 = domainBuilder.buildFramework({ ...framework2, areas: [] }); + const expectedFramework0 = domainBuilder.buildFramework({ ...frameworkData0, areas: [] }); + const expectedFramework2 = domainBuilder.buildFramework({ ...frameworkData2, areas: [] }); expect(frameworks).to.deepEqualArray([expectedFramework0, expectedFramework2]); }); + it('should return frameworks it managed to find', async function () { + // when + const frameworks = await frameworkRepository.findByRecordIds(['recId2', 'recIdCAVA']); + + // then + const expectedFramework2 = domainBuilder.buildFramework({ ...frameworkData2, areas: [] }); + expect(frameworks).to.deepEqualArray([expectedFramework2]); + }); + it('should return an empty array when no frameworks found for ids', async function () { // when const frameworks = await frameworkRepository.findByRecordIds(['recIdCOUCOU', 'recIdCAVA']); diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/framework-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/framework-datasource_test.js deleted file mode 100644 index 16448529e3f..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/framework-datasource_test.js +++ /dev/null @@ -1,79 +0,0 @@ -import { frameworkDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | FrameworkDatasource', function () { - describe('#list', function () { - it('should return an array of learning content frameworks data objects', async function () { - // given - const records = [{ id: 'recFramework0' }, { id: 'recFramework1' }, { id: 'recFramework2' }]; - mockLearningContent({ frameworks: records }); - - // when - const foundFrameworks = await frameworkDatasource.list(); - - // then - expect(foundFrameworks).to.deep.equal(records); - }); - }); - - describe('#getByName', function () { - it('should return a framework', async function () { - // given - const frameworks = [ - { id: 'recFramework0', name: 'Framework0' }, - { id: 'recFramework1', name: 'Framework1' }, - { id: 'recFramework2', name: 'Framework2' }, - ]; - mockLearningContent({ frameworks }); - - // when - const foundFramework = await frameworkDatasource.getByName('Framework0'); - - // then - expect(foundFramework).to.deep.equal({ id: 'recFramework0', name: 'Framework0' }); - }); - - describe('when framework not found', function () { - it('should return undefined', async function () { - const frameworks = [ - { id: 'recFramework0', name: 'Framework0' }, - { id: 'recFramework1', name: 'Framework1' }, - { id: 'recFramework2', name: 'Framework2' }, - ]; - mockLearningContent({ frameworks }); - - // when - const foundFramework = await frameworkDatasource.getByName('Framework3'); - - // then - expect(foundFramework).to.be.undefined; - }); - }); - }); - - describe('#findByRecordIds', function () { - it('should return an array of learning content frameworks data objects by ids', async function () { - // given - const records = [{ id: 'recFramework0' }, { id: 'recFramework1' }, { id: 'recFramework2' }]; - mockLearningContent({ frameworks: records }); - - // when - const foundFrameworks = await frameworkDatasource.findByRecordIds(['recFramework0', 'recFramework2']); - - // then - expect(foundFrameworks).to.deep.equal([{ id: 'recFramework0' }, { id: 'recFramework2' }]); - }); - - it('should return an empty array when no frameworks data objects found for ids', async function () { - // given - const records = [{ id: 'recFramework0' }, { id: 'recFramework1' }, { id: 'recFramework2' }]; - mockLearningContent({ frameworks: records }); - - // when - const foundFrameworks = await frameworkDatasource.findByRecordIds(['recFrameworkCOUCOU']); - - // then - expect(foundFrameworks).to.deep.equal([]); - }); - }); -}); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index b488f1b30d1..c7997b045e5 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -18,11 +18,13 @@ import sinonChai from 'sinon-chai'; import { DatabaseBuilder } from '../db/database-builder/database-builder.js'; import { disconnect, knex } from '../db/knex-database-connection.js'; +import * as frameworkRepository from '../lib/infrastructure/repositories/framework-repository.js'; import { PIX_ADMIN } from '../src/authorization/domain/constants.js'; import { config } from '../src/shared/config.js'; import { Membership } from '../src/shared/domain/models/index.js'; import * as tokenService from '../src/shared/domain/services/token-service.js'; import { LearningContentCache } from '../src/shared/infrastructure/caches/learning-content-cache.js'; +import * as areaRepository from '../src/shared/infrastructure/repositories/area-repository.js'; import * as customChaiHelpers from './tooling/chai-custom-helpers/index.js'; import * as domainBuilder from './tooling/domain-builder/factory/index.js'; import { jobChai } from './tooling/jobs/expect-job.js'; @@ -71,6 +73,7 @@ afterEach(function () { restore(); LearningContentCache.instance.flushAll(); nock.cleanAll(); + frameworkRepository.clearCache(); return databaseBuilder.clean(); }); From 4593badd2799191859f3738bec6173c65c09c2ec Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 11:28:10 +0100 Subject: [PATCH 05/24] feat(api): refacto areaRepository with new cache and to use PG --- .../learning-content/area-datasource.js | 22 - .../datasources/learning-content/index.js | 2 - .../repositories/area-repository.js | 161 ++- .../repositories/area-repository_test.js | 1044 ++++++++--------- .../learning-content/area-datasource_test.js | 98 -- api/tests/test-helper.js | 1 + .../domain-builder/factory/build-area.js | 8 +- 7 files changed, 606 insertions(+), 730 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/area-datasource.js delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/area-datasource_test.js diff --git a/api/src/shared/infrastructure/datasources/learning-content/area-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/area-datasource.js deleted file mode 100644 index 9a0c9bbb071..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/area-datasource.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as datasource from './datasource.js'; -const areaDatasource = datasource.extend({ - modelName: 'areas', - - async findByRecordIds(areaIds) { - const areas = await this.list(); - return areas.filter(({ id }) => areaIds.includes(id)); - }, - - async findByFrameworkId(frameworkId) { - const areas = await this.list(); - return areas.filter((area) => area.frameworkId === frameworkId); - }, - - async findOneFromCompetenceId(competenceId) { - const areas = await this.list(); - const area = areas.filter((area) => area.competenceIds?.includes(competenceId)); - return area.length > 0 ? area[0] : {}; - }, -}); - -export { areaDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index aae263bde31..dde23f45252 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -1,5 +1,4 @@ import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js'; -import { areaDatasource } from './area-datasource.js'; import { challengeDatasource } from './challenge-datasource.js'; import { competenceDatasource } from './competence-datasource.js'; import { courseDatasource } from './course-datasource.js'; @@ -8,7 +7,6 @@ import { thematicDatasource } from './thematic-datasource.js'; import { tubeDatasource } from './tube-datasource.js'; export { - areaDatasource, challengeDatasource, competenceDatasource, courseDatasource, diff --git a/api/src/shared/infrastructure/repositories/area-repository.js b/api/src/shared/infrastructure/repositories/area-repository.js index 602753d3b5e..9df37ab619a 100644 --- a/api/src/shared/infrastructure/repositories/area-repository.js +++ b/api/src/shared/infrastructure/repositories/area-repository.js @@ -1,79 +1,126 @@ -import _ from 'lodash'; - +import { PIX_ORIGIN } from '../../domain/constants.js'; import { NotFoundError } from '../../domain/errors.js'; import { Area } from '../../domain/models/Area.js'; import { getTranslatedKey } from '../../domain/services/get-translated-text.js'; -import { areaDatasource } from '../../infrastructure/datasources/learning-content/area-datasource.js'; import * as competenceRepository from './competence-repository.js'; +import { LearningContentRepository } from './learning-content-repository.js'; -function _toDomain({ areaData, locale }) { - const translatedTitle = getTranslatedKey(areaData.title_i18n, locale); - return new Area({ - id: areaData.id, - code: areaData.code, - name: areaData.name, - title: translatedTitle, - color: areaData.color, - frameworkId: areaData.frameworkId, - }); -} +const TABLE_NAME = 'learningcontent.areas'; -async function list({ locale } = {}) { - const areaDataObjects = await areaDatasource.list(); - return areaDataObjects.map((areaData) => _toDomain({ areaData, locale })); +export async function list({ locale } = {}) { + const cacheKey = 'list()'; + const listCallback = (knex) => knex.orderBy('id'); + const areaDtos = await getInstance().find(cacheKey, listCallback); + return areaDtos.map((areaDto) => toDomain(areaDto, locale)); } -async function listWithPixCompetencesOnly({ locale } = {}) { - const [areas, competences] = await Promise.all([ - list({ locale }), - competenceRepository.listPixCompetencesOnly({ locale }), - ]); - areas.forEach((area) => { - area.competences = _.filter(competences, { areaId: area.id }); - }); - return _.filter(areas, ({ competences }) => !_.isEmpty(competences)); +export async function listWithPixCompetencesOnly({ locale } = {}) { + const cacheKey = 'listWithPixCompetencesOnly()'; + const listPixAreasCallback = (knex) => + knex + .join('learningcontent.frameworks', 'learningcontent.frameworks.id', `${TABLE_NAME}.frameworkId`) + .where('learningcontent.frameworks.name', PIX_ORIGIN) + .orderBy(`${TABLE_NAME}.name`); + const areaDtos = await getInstance().find(cacheKey, listPixAreasCallback); + return toDomainWithPixCompetences(areaDtos, locale); } -async function findByFrameworkIdWithCompetences({ frameworkId, locale }) { - const areaDatas = await areaDatasource.findByFrameworkId(frameworkId); - const areas = areaDatas.map((areaData) => _toDomain({ areaData, locale })); - const competences = await competenceRepository.list({ locale }); - areas.forEach((area) => { - area.competences = _.filter(competences, { areaId: area.id }); - }); - return areas; +export async function findByFrameworkIdWithCompetences({ frameworkId, locale }) { + const cacheKey = `findByFrameworkIdWithCompetences({ frameworkId: ${frameworkId} })`; + const findAreasByFrameworkIdCallback = (knex) => knex.where('frameworkId', frameworkId).orderBy('id'); + const areaDtos = await getInstance().find(cacheKey, findAreasByFrameworkIdCallback); + return toDomainWithCompetences(areaDtos, locale); } -async function findByFrameworkId({ frameworkId, locale }) { - const areaDatas = await areaDatasource.findByFrameworkId(frameworkId); - return areaDatas.map((areaData) => _toDomain({ areaData, locale })); +export async function findByFrameworkId({ frameworkId, locale }) { + const cacheKey = `findByFrameworkId({ frameworkId: ${frameworkId} })`; + const findAreasByFrameworkIdCallback = (knex) => knex.where('frameworkId', frameworkId).orderBy('id'); + const areaDtos = await getInstance().find(cacheKey, findAreasByFrameworkIdCallback); + return areaDtos.map((areaDto) => toDomain(areaDto, locale)); } -async function findByRecordIds({ areaIds, locale }) { - const areaDataObjects = await areaDatasource.list(); - return areaDataObjects.filter(({ id }) => areaIds.includes(id)).map((areaData) => _toDomain({ areaData, locale })); +export async function findByRecordIds({ areaIds, locale }) { + const areaDtos = await getInstance().loadMany(areaIds); + return areaDtos + .filter((areaDto) => areaDto) + .sort(byId) + .map((areaDto) => toDomain(areaDto, locale)); } -async function getAreaCodeByCompetenceId(competenceId) { - const area = await areaDatasource.findOneFromCompetenceId(competenceId); - return area.code; +export async function getAreaCodeByCompetenceId(competenceId) { + // todo : est-ce qu'on veut cache ça ? + const cacheKey = `getAreaCodeByCompetenceId(${competenceId})`; + const findByCompetenceIdCallback = (knex) => knex.whereRaw('?=ANY(??)', [competenceId, 'competenceIds']).limit(1); + const [areaDto] = await getInstance().find(cacheKey, findByCompetenceIdCallback); + return areaDto?.code; } -async function get({ id, locale }) { - const areaDataObjects = await areaDatasource.list(); - const areaData = areaDataObjects.find((area) => area.id === id); - if (!areaData) { +export async function get({ id, locale }) { + const areaDto = await getInstance().load(id); + if (!areaDto) { throw new NotFoundError(`Area "${id}" not found.`); } - return _toDomain({ areaData, locale }); + return toDomain(areaDto, locale); +} + +export function clearCache() { + return getInstance().clearCache(); +} + +function byId(entityA, entityB) { + return entityA.id < entityB.id ? -1 : 1; +} + +function toDomain(areaDto, locale) { + const translatedTitle = getTranslatedKey(areaDto.title_i18n, locale); + return new Area({ + id: areaDto.id, + code: areaDto.code, + name: areaDto.name, + title: translatedTitle, + color: areaDto.color, + frameworkId: areaDto.frameworkId, + }); +} + +async function toDomainWithPixCompetences(areaDtos, locale) { + const areas = []; + for (const areaDto of areaDtos) { + const competences = []; + for (const competenceId of areaDto.competenceIds) { + const competence = await competenceRepository.get({ id: competenceId, locale }); + if (competence.origin === PIX_ORIGIN) { + competences.push(competence); + } + } + const area = toDomain(areaDto, locale); + area.competences = competences; + areas.push(area); + } + return areas; +} + +async function toDomainWithCompetences(areaDtos, locale) { + const areas = []; + for (const areaDto of areaDtos) { + const competences = []; + for (const competenceId of areaDto.competenceIds) { + const competence = await competenceRepository.get({ id: competenceId, locale }); + competences.push(competence); + } + const area = toDomain(areaDto, locale); + area.competences = competences; + areas.push(area); + } + return areas; } -export { - findByFrameworkId, - findByFrameworkIdWithCompetences, - findByRecordIds, - get, - getAreaCodeByCompetenceId, - list, - listWithPixCompetencesOnly, -}; +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; +} diff --git a/api/tests/integration/infrastructure/repositories/area-repository_test.js b/api/tests/integration/infrastructure/repositories/area-repository_test.js index 7fc10615498..97e54128947 100644 --- a/api/tests/integration/infrastructure/repositories/area-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/area-repository_test.js @@ -1,636 +1,584 @@ -import _ from 'lodash'; - +import { PIX_ORIGIN } from '../../../../src/shared/domain/constants.js'; import { NotFoundError } from '../../../../src/shared/domain/errors.js'; -import { Area } from '../../../../src/shared/domain/models/Area.js'; import * as areaRepository from '../../../../src/shared/infrastructure/repositories/area-repository.js'; -import { catchErr, domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; describe('Integration | Repository | area-repository', function () { - describe('#list', function () { - const area0 = { - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title_i18n: { - fr: 'area0titleFr', - en: 'area0titleEn,', - }, - color: 'area0color', - frameworkId: 'recFmk123', - competenceIds: ['recCompetence0'], - }; - const area1 = { - id: 'recArea1', - code: 'area1code', - name: 'area1name', - title_i18n: { - fr: 'area1titleFr', - en: 'area1titleEn,', - }, - color: 'area1color', - frameworkId: 'recFmk456', - competenceIds: [], - }; - - const learningContent = { areas: [area0, area1] }; + const areaData0 = { + id: 'recArea0', + code: 'area0code', + name: 'area0name', + title_i18n: { + fr: 'area0titleFr', + en: 'area0titleEn', + }, + color: 'area0color', + frameworkId: 'recFmk123', + competenceIds: ['recCompetence1_pix', 'recCompetence4_pix'], + }; + const areaData1 = { + id: 'recArea1', + code: 'area1code', + name: 'area1name', + title_i18n: { + fr: 'area1titleFr', + nl: 'area1titleNl', + }, + color: 'area1color', + frameworkId: 'recFmk456', + competenceIds: ['recCompetence2_pasPix'], + }; + const areaData2 = { + id: 'recArea2', + code: 'area2code', + name: 'area2name', + title_i18n: { + fr: 'area2titleFr', + nl: 'area2titleNl', + }, + color: 'area2color', + frameworkId: 'recFmk123', + competenceIds: ['recCompetence3_pix'], + }; + const competenceData1 = { + id: 'recCompetence1_pix', + name_i18n: { fr: 'name FR recCompetence1_pix', en: 'name EN recCompetence1_pix' }, + description_i18n: { fr: 'description FR recCompetence1_pix', nl: 'description NL recCompetence1_pix' }, + index: 'index recCompetence1_pix', + areaId: 'recArea0', + origin: PIX_ORIGIN, + skillIds: ['skillIdA'], + thematicIds: ['thematicIdA'], + }; + const competenceData2 = { + id: 'recCompetence2_pasPix', + name_i18n: { fr: 'name FR recCompetence2_pasPix', en: 'name EN recCompetence2_pasPix' }, + description_i18n: { fr: 'description FR recCompetence2_pasPix', en: 'description EN recCompetence2_pasPix' }, + index: 'index recCompetence2_pasPix', + areaId: 'recArea1', + origin: 'PasPix', + skillIds: ['skillIdB'], + thematicIds: ['thematicIdB'], + }; + const competenceData3 = { + id: 'recCompetence3_pix', + name_i18n: { fr: 'name FR recCompetence3_pix', nl: 'name NL recCompetence3_pix' }, + description_i18n: { fr: 'description FR recCompetence3_pix', en: 'description EN recCompetence3_pix' }, + index: 'index recCompetence3_pix', + areaId: 'recArea2', + origin: PIX_ORIGIN, + skillIds: ['skillIdC'], + thematicIds: ['thematicIdC'], + }; + const competenceData4 = { + id: 'recCompetence4_pix', + name_i18n: { fr: 'name FR recCompetence4_pix', en: 'name EN recCompetence4_pix' }, + description_i18n: { fr: 'description FR recCompetence4_pix', en: 'description EN recCompetence4_pix' }, + index: 'index recCompetence4_pix', + areaId: 'recArea0', + origin: PIX_ORIGIN, + skillIds: ['skillIdD'], + thematicIds: ['thematicIdD'], + }; + describe('#list', function () { beforeEach(async function () { - await mockLearningContent(learningContent); - }); + databaseBuilder.factory.learningContent.buildFramework({ id: 'recFmk123', name: PIX_ORIGIN }); + databaseBuilder.factory.learningContent.buildFramework({ id: 'recFmk456', name: 'Un framework pas Pix' }); + databaseBuilder.factory.learningContent.buildArea(areaData1); + databaseBuilder.factory.learningContent.buildArea(areaData0); + databaseBuilder.factory.learningContent.buildArea(areaData2); - it('should return all areas without fetching competences', async function () { - // when - const areas = await areaRepository.list(); - - // then - expect(areas).to.have.lengthOf(2); - expect(areas[0]).to.be.instanceof(Area); - expect(areas).to.deep.include.members([ - { - id: area0.id, - code: area0.code, - name: area0.name, - title: area0.title_i18n.fr, - color: area0.color, - frameworkId: area0.frameworkId, - competences: [], - }, - { - id: area1.id, - code: area1.code, - name: area1.name, - title: area1.title_i18n.fr, - color: area1.color, - frameworkId: area1.frameworkId, - competences: [], - }, - ]); + await databaseBuilder.commit(); }); - describe('when locale is "en"', function () { - it('should return all areas with english title', async function () { - // given - const locale = 'en'; + context('when no locale provided', function () { + it('should return all areas translated in default locale FR', async function () { // when - const areas = await areaRepository.list({ locale }); + const areas = await areaRepository.list(); // then - expect(areas[0].title).to.equal(area0.title_i18n.en); - expect(areas[1].title).to.equal(area1.title_i18n.en); + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.fr, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData1, + title: areaData1.title_i18n.fr, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [], + }), + ]); }); }); - describe('when locale is not "en"', function () { - it('should return all areas with french title', async function () { - // given - const locale = 'fr'; - + context('when a locale is provided', function () { + it('should return all areas translated in the given locale or with fallback translations', async function () { // when - const areas = await areaRepository.list({ locale }); + const areas = await areaRepository.list({ locale: 'en' }); // then - expect(areas[0].title).to.equal(area0.title_i18n.fr); - expect(areas[1].title).to.equal(area1.title_i18n.fr); + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.en, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData1, + title: areaData1.title_i18n.fr, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [], + }), + ]); }); }); }); describe('#getAreaCodeByCompetenceId', function () { - const area0 = { - id: 'recArea0', - code: 3, - competenceIds: ['competenceId_01', 'competenceId_02'], - }; - const area1 = { - id: 'recArea1', - code: 5, - competenceIds: ['competenceId_03', 'competenceId_04', 'competenceId_05'], - }; - - const learningContent = { areas: [area0, area1] }; - beforeEach(async function () { - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildArea(areaData1); + databaseBuilder.factory.learningContent.buildArea(areaData0); + databaseBuilder.factory.learningContent.buildArea(areaData2); + await databaseBuilder.commit(); }); - it('should return the code area', async function () { - // when - const result = await areaRepository.getAreaCodeByCompetenceId('competenceId_02'); - - // then - expect(result).to.deep.equal(3); - }); - }); - - describe('#listWithPixCompetencesOnly', function () { - context('when there are areas that do not have pix competences', function () { - const learningContent = { - areas: [ - { - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title_i18n: { - fr: 'area0titleFr', - en: 'area0titleEn', - }, - color: 'area0color', - frameworkId: 'recFmk123', - competenceIds: ['recCompetence0'], - }, - ], - competences: [{ id: 'recCompetence0', origin: 'NotPix' }], - }; + context('when competenceId refers to an existing Area', function () { + it('should return the code of the corresponding area', async function () { + // when + const result = await areaRepository.getAreaCodeByCompetenceId('recCompetence1_pix'); - beforeEach(async function () { - await mockLearningContent(learningContent); + // then + expect(result).to.deep.equal(areaData0.code); }); + }); - it('should ignore the area', async function () { + context('when competenceId is not referenced in any area', function () { + it('should return undefined', async function () { // when - const areas = await areaRepository.listWithPixCompetencesOnly(); + const result = await areaRepository.getAreaCodeByCompetenceId('competenceId_66'); // then - expect(areas).to.be.empty; + expect(result).to.be.undefined; }); }); + }); - context('when there are areas that have pix competences', function () { - const area0 = { - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title_i18n: { - fr: 'area0titleFr', - en: 'area0titleEn', - }, - color: 'area0color', - frameworkId: 'recFmk123', - competenceIds: ['recCompetence0', 'recCompetence1'], - }; - - const area1 = { - id: 'recArea1', - code: 'area1code', - name: 'area1name', - title_i18n: { - fr: 'area1titleFr', - en: 'area1titleEn', - }, - color: 'area1color', - frameworkId: 'recFmk456', - competenceIds: ['recCompetence2', 'recCompetence3'], - }; - - const learningContent = { - areas: [area0, area1], - competences: [ - { id: 'recCompetence0', origin: 'NotPix', areaId: 'recArea0' }, - { id: 'recCompetence1', origin: 'Pix', areaId: 'recArea0' }, - { id: 'recCompetence2', origin: 'NotPix', areaId: 'recArea1' }, - { id: 'recCompetence3', origin: 'Pix', areaId: 'recArea1' }, - ], - }; - + describe('#listWithPixCompetencesOnly', function () { + context('when there are some area that have pix competences', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildArea(areaData2); + databaseBuilder.factory.learningContent.buildArea(areaData0); + databaseBuilder.factory.learningContent.buildArea(areaData1); + await mockLearningContent({ + competences: [competenceData1, competenceData2, competenceData3, competenceData4], + }); + // Décommentez-moi quand on aura traité le competence repository + /*databaseBuilder.factory.learningContent.buildCompetence(competenceData1); + databaseBuilder.factory.learningContent.buildCompetence(competenceData2); + databaseBuilder.factory.learningContent.buildCompetence(competenceData3); + databaseBuilder.factory.learningContent.buildCompetence(competenceData4);*/ + await databaseBuilder.commit(); + }); + context('when a locale is provided', function () { + it('should return only areas with pix competences with entities translated in given locale when possible or fallback to default locale FR', async function () { + // when + const areas = await areaRepository.listWithPixCompetencesOnly({ locale: 'en' }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.en, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.en, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData4, + name: competenceData4.name_i18n.en, + description: competenceData4.description_i18n.en, + }), + ], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ], + }), + ]); + }); + }); + context('when no locale is provided', function () { + it('should return only areas with pix competences with entities translated in default locale FR', async function () { + // when + const areas = await areaRepository.listWithPixCompetencesOnly(); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.fr, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.fr, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData4, + name: competenceData4.name_i18n.fr, + description: competenceData4.description_i18n.fr, + }), + ], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ], + }), + ]); + }); + }); + }); + context('when there are no areas that have pix competences', function () { beforeEach(async function () { - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildArea(areaData1); + await mockLearningContent({ + competences: [competenceData2], + }); + // Décommentez-moi quand on aura traité le competence repository + /*databaseBuilder.factory.learningContent.buildCompetence(competenceData2);*/ + await databaseBuilder.commit(); }); - it('should return the areas with only pix competences in it', async function () { + it('should return an empty array', async function () { // when const areas = await areaRepository.listWithPixCompetencesOnly(); // then - expect(areas).to.have.lengthOf(2); - expect(areas[0]).to.be.instanceof(Area); - expect(_.omit(areas[0], 'competences')).to.deep.equal({ - id: area0.id, - code: area0.code, - name: area0.name, - title: area0.title_i18n.fr, - color: area0.color, - frameworkId: area0.frameworkId, - }); - expect(areas[0].competences).to.have.lengthOf(1); - expect(areas[0].competences[0].id).to.equal('recCompetence1'); - expect(_.omit(areas[1], 'competences')).to.deep.equal({ - id: area1.id, - code: area1.code, - name: area1.name, - title: area1.title_i18n.fr, - color: area1.color, - frameworkId: area1.frameworkId, - }); - expect(areas[1].competences).to.have.lengthOf(1); - expect(areas[1].competences[0].id).to.equal('recCompetence3'); + expect(areas).to.deep.equal([]); }); }); }); describe('#findByFrameworkIdWithCompetences', function () { - const area0 = { - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title_i18n: { - fr: 'area0titleFr', - en: 'area0titleEn', - }, - color: 'area0color', - frameworkId: 'framework1', - competenceIds: ['recCompetence0', 'recCompetence1'], - }; - - const area1 = { - id: 'recArea1', - code: 'area1code', - name: 'area1name', - title_i18n: { - fr: 'area1titleFr', - en: 'area1titleEn', - }, - color: 'area1color', - frameworkId: 'framework2', - competenceIds: ['recCompetence2', 'recCompetence3'], - }; - - const learningContent = { - areas: [area0, area1], - competences: [ - { - id: 'recCompetence0', - areaId: 'recArea0', - name_i18n: { - fr: 'competence0NameFr', - en: 'competence0NameEn', - }, - description_i18n: { - fr: 'competence0DescriptionFr', - en: 'competence0DescriptionEn', - }, - }, - { - id: 'recCompetence1', - areaId: 'recArea0', - name_i18n: { - fr: 'competence1NameFr', - en: 'competence1NameEn', - }, - description_i18n: { - fr: 'competence1DescriptionFr', - en: 'competence1DescriptionEn', - }, - }, - { - id: 'recCompetence2', - areaId: 'recArea1', - name_i18n: { - fr: 'competence2NameFr', - en: 'competence2NameEn', - }, - description_i18n: { - fr: 'competence2DescriptionFr', - en: 'competence2DescriptionEn', - }, - }, - { - id: 'recCompetence3', - areaId: 'recArea1', - name_i18n: { - fr: 'competence3NameFr', - en: 'competence3NameEn', - }, - description_i18n: { - fr: 'competence3DescriptionFr', - en: 'competence3DescriptionEn', - }, - }, - ], - }; - beforeEach(async function () { - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildArea(areaData2); + databaseBuilder.factory.learningContent.buildArea(areaData0); + databaseBuilder.factory.learningContent.buildArea(areaData1); + await mockLearningContent({ + competences: [competenceData1, competenceData2, competenceData3, competenceData4], + }); + // Décommentez-moi quand on aura traité le competence repository + /*databaseBuilder.factory.learningContent.buildCompetence(competenceData1); + databaseBuilder.factory.learningContent.buildCompetence(competenceData2); + databaseBuilder.factory.learningContent.buildCompetence(competenceData3); + databaseBuilder.factory.learningContent.buildCompetence(competenceData4);*/ + await databaseBuilder.commit(); }); - it('should return a list of areas from the proper framework', async function () { - // when - const areas = await areaRepository.findByFrameworkIdWithCompetences({ frameworkId: 'framework1' }); - - // then - expect(areas).to.have.lengthOf(1); - expect(areas[0]).to.be.instanceof(Area); - expect(_.omit(areas[0], 'competences')).to.deep.equal({ - id: area0.id, - code: area0.code, - name: area0.name, - title: area0.title_i18n.fr, - color: area0.color, - frameworkId: area0.frameworkId, + context('when some areas have the given framework id', function () { + context('when a locale is provided', function () { + it('should return the areas with competences with all entities translated in given locale or fallback to default locale FR', async function () { + // when + const areas = await areaRepository.findByFrameworkIdWithCompetences({ + frameworkId: 'recFmk123', + locale: 'en', + }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.en, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.en, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData4, + name: competenceData4.name_i18n.en, + description: competenceData4.description_i18n.en, + }), + ], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ], + }), + ]); + }); + }); + context('when no locale is provided', function () { + it('should return the areas with competences with all entities translated in default locale FR', async function () { + // when + const areas = await areaRepository.findByFrameworkIdWithCompetences({ frameworkId: 'recFmk123' }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.fr, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.fr, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData4, + name: competenceData4.name_i18n.fr, + description: competenceData4.description_i18n.fr, + }), + ], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [ + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ], + }), + ]); + }); }); - expect(areas[0].competences).to.have.lengthOf(2); - expect(areas[0].competences[0].id).to.equal('recCompetence0'); - expect(areas[0].competences[0].name).to.equal('competence0NameFr'); - expect(areas[0].competences[0].description).to.equal('competence0DescriptionFr'); - expect(areas[0].competences[1].id).to.equal('recCompetence1'); - expect(areas[0].competences[1].name).to.equal('competence1NameFr'); - expect(areas[0].competences[1].description).to.equal('competence1DescriptionFr'); }); + context('when no areas exist for given framework id', function () { + it('should return an empty array', async function () { + // when + const areas = await areaRepository.findByFrameworkIdWithCompetences({ + frameworkId: 'BLOUBLOU', + }); - it('should return a list of areas in english', async function () { - // when - const areas = await areaRepository.findByFrameworkIdWithCompetences({ frameworkId: 'framework1', locale: 'en' }); - - // then - expect(areas).to.have.lengthOf(1); - expect(areas[0]).to.be.instanceof(Area); - expect(_.omit(areas[0], 'competences')).to.deep.equal({ - id: area0.id, - code: area0.code, - name: area0.name, - title: area0.title_i18n.en, - color: area0.color, - frameworkId: area0.frameworkId, + // then + expect(areas).to.deep.equal([]); }); - expect(areas[0].competences).to.have.lengthOf(2); - expect(areas[0].competences[0].id).to.equal('recCompetence0'); - expect(areas[0].competences[0].name).to.equal('competence0NameEn'); - expect(areas[0].competences[0].description).to.equal('competence0DescriptionEn'); - expect(areas[0].competences[1].id).to.equal('recCompetence1'); - expect(areas[0].competences[1].name).to.equal('competence1NameEn'); - expect(areas[0].competences[1].description).to.equal('competence1DescriptionEn'); }); }); describe('#findByRecordIds', function () { - it('should return a list of areas', async function () { - // given - const area1 = domainBuilder.buildArea({ - id: 'recArea1', - code: 4, - name: 'area_name1', - title: 'area_title1FR', - color: 'blue1', - frameworkId: 'recFwkId1', - }); - const area2 = domainBuilder.buildArea({ - id: 'recArea2', - code: 6, - name: 'area_name2', - title: 'area_title2FR', - color: 'blue2', - frameworkId: 'recFwkId2', - }); - - const learningContentArea0 = { - id: 'recArea0', - code: 1, - name: 'area_name0', - title_i18n: { - fr: 'area_title0FR', - en: 'area_title0EN', - }, - color: 'blue0', - frameworkId: 'recFwkId0', - }; - - const learningContentArea1 = { - id: 'recArea1', - code: 4, - name: 'area_name1', - title_i18n: { - fr: 'area_title1FR', - en: 'area_title1EN', - }, - color: 'blue1', - frameworkId: 'recFwkId1', - }; - - const learningContentArea2 = { - id: 'recArea2', - code: 6, - name: 'area_name2', - title_i18n: { - fr: 'area_title2FR', - en: 'area_title2EN', - }, - color: 'blue2', - frameworkId: 'recFwkId2', - }; - - await mockLearningContent({ areas: [learningContentArea0, learningContentArea1, learningContentArea2] }); - - // when - const areas = await areaRepository.findByRecordIds({ areaIds: ['recArea1', 'recArea2'] }); - - // then - expect(areas).to.deepEqualArray([area1, area2]); + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildArea(areaData1); + databaseBuilder.factory.learningContent.buildArea(areaData2); + databaseBuilder.factory.learningContent.buildArea(areaData0); + await databaseBuilder.commit(); }); - it('should return a list of english areas', async function () { - // given - const area1 = domainBuilder.buildArea({ - id: 'recArea1', - code: 4, - name: 'area_name1', - title: 'area_title1EN', - color: 'blue1', - frameworkId: 'recFwkId1', + context('when areas found by ids', function () { + context('when no locale provided', function () { + it('should return all areas found translated in default locale FR given by their ids', async function () { + // when + const areas = await areaRepository.findByRecordIds({ areaIds: ['recArea2', 'recArea0'] }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.fr, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [], + }), + ]); + }); }); - const area2 = domainBuilder.buildArea({ - id: 'recArea2', - code: 6, - name: 'area_name2', - title: 'area_title2EN', - color: 'blue2', - frameworkId: 'recFwkId2', + + context('when a locale is provided', function () { + it('should return all areas found translated in provided locale or fallback to default locale FR given by their ids', async function () { + // when + const areas = await areaRepository.findByRecordIds({ areaIds: ['recArea2', 'recArea0'], locale: 'en' }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.en, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [], + }), + ]); + }); }); + }); + + context('when no areas found for given ids', function () { + it('should return an empty array', async function () { + // when + const areas = await areaRepository.findByRecordIds({ areaIds: ['recAreaCOUCOU', 'recAreaMAMAN'] }); - const learningContentArea0 = { - id: 'recArea0', - code: 1, - name: 'area_name0', - title_i18n: { - fr: 'area_title0FR', - en: 'area_title0EN', - }, - color: 'blue0', - frameworkId: 'recFwkId0', - }; - - const learningContentArea1 = { - id: 'recArea1', - code: 4, - name: 'area_name1', - title_i18n: { - fr: 'area_title1FR', - en: 'area_title1EN', - }, - color: 'blue1', - frameworkId: 'recFwkId1', - }; - - const learningContentArea2 = { - id: 'recArea2', - code: 6, - name: 'area_name2', - title_i18n: { - fr: 'area_title2FR', - en: 'area_title2EN', - }, - color: 'blue2', - frameworkId: 'recFwkId2', - }; - - await mockLearningContent({ areas: [learningContentArea0, learningContentArea1, learningContentArea2] }); - - // when - const areas = await areaRepository.findByRecordIds({ areaIds: ['recArea1', 'recArea2'], locale: 'en' }); - - // then - expect(areas).to.deepEqualArray([area1, area2]); + // then + expect(areas).to.deep.equal([]); + }); }); }); describe('#get', function () { beforeEach(async function () { - const learningContentArea0 = { - id: 'recArea0', - code: 1, - name: 'area_name0', - title_i18n: { - fr: 'area_title0FR', - en: 'area_title0EN', - }, - color: 'blue0', - frameworkId: 'recFwkId0', - }; - const learningContentArea1 = { - id: 'recArea1', - code: 4, - name: 'area_name1', - title_i18n: { - fr: 'area_title1FR', - en: 'area_title1EN', - }, - color: 'blue1', - frameworkId: 'recFwkId1', - }; - await mockLearningContent({ areas: [learningContentArea0, learningContentArea1] }); + databaseBuilder.factory.learningContent.buildArea(areaData1); + databaseBuilder.factory.learningContent.buildArea(areaData0); + await databaseBuilder.commit(); }); - it('should return the area', async function () { - // when - const area = await areaRepository.get({ id: 'recArea1' }); - - // then - const expectedArea = domainBuilder.buildArea({ - id: 'recArea1', - code: 4, - name: 'area_name1', - title: 'area_title1FR', - color: 'blue1', - frameworkId: 'recFwkId1', + context('when area is found', function () { + context('when a locale is provided', function () { + it('should return the area translated with the provided locale of fallback to default locale FR', async function () { + // given + const area0 = await areaRepository.get({ id: 'recArea0', locale: 'nl' }); + const area1 = await areaRepository.get({ id: 'recArea1', locale: 'nl' }); + + // then + expect(area0).to.deepEqualInstance( + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.fr, + competences: [], + }), + ); + expect(area1).to.deepEqualInstance( + domainBuilder.buildArea({ + ...areaData1, + title: areaData1.title_i18n.nl, + competences: [], + }), + ); + }); + }); + context('when no locale is provided', function () { + it('should return the area translated with default locale FR', async function () { + // when + const area1 = await areaRepository.get({ id: 'recArea1' }); + + // then + expect(area1).to.deepEqualInstance( + domainBuilder.buildArea({ + ...areaData1, + title: areaData1.title_i18n.fr, + competences: [], + }), + ); + }); }); - expect(area).to.deepEqualInstance(expectedArea); }); + context('when no area found', function () { + it('should throw a NotFound error', async function () { + // when + const err = await catchErr(areaRepository.get, areaRepository)({ id: 'recCouCouPapa' }); - it('should throw a NotFound error', async function () { - // when - const error = await catchErr(areaRepository.get)({ id: 'jexistepas' }); - - // then - expect(error).to.be.instanceOf(NotFoundError); - expect(error.message).to.equal('Area "jexistepas" not found.'); + // then + expect(err).to.be.instanceOf(NotFoundError); + expect(err.message).to.equal('Area "recCouCouPapa" not found.'); + }); }); }); describe('#findByFrameworkId', function () { beforeEach(async function () { - const area0 = { - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title_i18n: { - fr: 'area0titleFr', - en: 'area0titleEn', - }, - color: 'area0color', - frameworkId: 'framework1', - }; - const area1 = { - id: 'recArea1', - code: 'area1code', - name: 'area1name', - title_i18n: { - fr: 'area1titleFr', - en: 'area1titleEn', - }, - color: 'area1color', - frameworkId: 'framework2', - }; - const area2 = { - id: 'recArea2', - code: 'area2code', - name: 'area2name', - title_i18n: { - fr: 'area2titleFr', - en: 'area2titleEn', - }, - color: 'area2color', - frameworkId: 'framework1', - }; - const learningContent = { - areas: [area0, area1, area2], - }; - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildArea(areaData2); + databaseBuilder.factory.learningContent.buildArea(areaData0); + databaseBuilder.factory.learningContent.buildArea(areaData1); + await databaseBuilder.commit(); }); - it('should return a list of areas from the proper framework', async function () { - // when - const areas = await areaRepository.findByFrameworkId({ frameworkId: 'framework1' }); - - // then - const area0 = domainBuilder.buildArea({ - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title: 'area0titleFr', - color: 'area0color', - frameworkId: 'framework1', + context('when some areas have the given framework id', function () { + context('when a locale is provided', function () { + it('should return the areas translated in given locale or fallback to default locale FR', async function () { + // when + const areas = await areaRepository.findByFrameworkId({ + frameworkId: 'recFmk123', + locale: 'en', + }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.en, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [], + }), + ]); + }); }); - const area2 = domainBuilder.buildArea({ - id: 'recArea2', - code: 'area2code', - name: 'area2name', - title: 'area2titleFr', - color: 'area2color', - frameworkId: 'framework1', + context('when no locale is provided', function () { + it('should return the areas translated in default locale FR', async function () { + // when + const areas = await areaRepository.findByFrameworkId({ frameworkId: 'recFmk123' }); + + // then + expect(areas).to.deepEqualArray([ + domainBuilder.buildArea({ + ...areaData0, + title: areaData0.title_i18n.fr, + competences: [], + }), + domainBuilder.buildArea({ + ...areaData2, + title: areaData2.title_i18n.fr, + competences: [], + }), + ]); + }); }); - expect(areas).to.deepEqualArray([area0, area2]); }); + context('when no areas exist for given framework id', function () { + it('should return an empty array', async function () { + // when + const areas = await areaRepository.findByFrameworkId({ + frameworkId: 'BLOUBLOU', + }); - it('should return a list of areas in english', async function () { - // when - const areas = await areaRepository.findByFrameworkId({ frameworkId: 'framework1', locale: 'en' }); - - // then - const area0 = domainBuilder.buildArea({ - id: 'recArea0', - code: 'area0code', - name: 'area0name', - title: 'area0titleEn', - color: 'area0color', - frameworkId: 'framework1', - }); - const area2 = domainBuilder.buildArea({ - id: 'recArea2', - code: 'area2code', - name: 'area2name', - title: 'area2titleEn', - color: 'area2color', - frameworkId: 'framework1', + // then + expect(areas).to.deep.equal([]); }); - expect(areas).to.deepEqualArray([area0, area2]); }); }); }); diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/area-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/area-datasource_test.js deleted file mode 100644 index 46715fa58c5..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/area-datasource_test.js +++ /dev/null @@ -1,98 +0,0 @@ -import { areaDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | AreaDatasource', function () { - describe('#findByRecordIds', function () { - it('should return an array of matching learning content area data objects', async function () { - // given - const records = [{ id: 'recArea0' }, { id: 'recArea1' }, { id: 'recArea2' }]; - await mockLearningContent({ areas: records }); - const expectedAreaIds = ['recArea0', 'recArea1']; - - // when - const foundAreas = await areaDatasource.findByRecordIds(expectedAreaIds); - // then - expect(foundAreas.map(({ id }) => id)).to.deep.equal(expectedAreaIds); - }); - - it('should return an empty array when there are no objects matching the ids', async function () { - // given - const records = [{ id: 'recArea0' }]; - await mockLearningContent({ areas: records }); - - // when - const foundAreas = await areaDatasource.findByRecordIds(['some_other_id']); - - // then - expect(foundAreas).to.be.empty; - }); - }); - - describe('#findOneFromCompetenceId', function () { - it('should return the corresponding area', async function () { - // given - const areas = [ - { - id: 'area_1', - competenceIds: ['competenceId_1', 'competenceId_2', 'competenceId_3'], - }, - { - id: 'area_2', - competenceIds: undefined, - }, - { - id: 'area_3', - competenceIds: ['competenceId_4'], - }, - ]; - await mockLearningContent({ areas }); - - // when - const foundArea = await areaDatasource.findOneFromCompetenceId('competenceId_1'); - // then - expect(foundArea).to.deep.equal({ - id: 'area_1', - competenceIds: ['competenceId_1', 'competenceId_2', 'competenceId_3'], - }); - }); - - it('should return an object when no match', async function () { - // given - const areas = [ - { - id: 'area_1', - competenceIds: ['competenceId_1', 'competenceId_2', 'competenceId_3'], - }, - { - id: 'area_2', - competenceIds: ['competenceId_4'], - }, - ]; - await mockLearningContent({ areas }); - - // when - const foundArea = await areaDatasource.findOneFromCompetenceId('competenceId_10'); - // then - expect(foundArea).to.deep.equal({}); - }); - }); - - describe('#findByFrameworkId', function () { - it('should return an array of matching learning content area data objects by framework id', async function () { - // given - const records = [ - { id: 'recArea0', frameworkId: 'framework1' }, - { id: 'recArea1', frameworkId: 'framework2' }, - { id: 'recArea2', frameworkId: 'framework1' }, - ]; - await mockLearningContent({ areas: records }); - const expectedAreaIds = ['recArea0', 'recArea2']; - const frameworkId = 'framework1'; - - // when - const foundAreas = await areaDatasource.findByFrameworkId(frameworkId); - // then - expect(foundAreas.map(({ id }) => id)).to.deep.equal(expectedAreaIds); - }); - }); -}); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index c7997b045e5..8e8cf1441da 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -74,6 +74,7 @@ afterEach(function () { LearningContentCache.instance.flushAll(); nock.cleanAll(); frameworkRepository.clearCache(); + areaRepository.clearCache(); return databaseBuilder.clean(); }); diff --git a/api/tests/tooling/domain-builder/factory/build-area.js b/api/tests/tooling/domain-builder/factory/build-area.js index eb404a87fc0..189e1fe0b14 100644 --- a/api/tests/tooling/domain-builder/factory/build-area.js +++ b/api/tests/tooling/domain-builder/factory/build-area.js @@ -19,9 +19,11 @@ const buildArea = function ({ color, frameworkId, }); - competences.forEach((competence) => { - competence.area = area; - }); + + // c koi ce truc + //competences.forEach((competence) => { + // competence.area = area; + //}); return area; }; From 377c930026798ada7e727f6df1ff44f321d72d1c Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 14:27:44 +0100 Subject: [PATCH 06/24] feat(api): refacto competenceRepository with new cache and to use PG --- .../learning-content/competence-datasource.js | 12 - .../datasources/learning-content/index.js | 2 - .../repositories/area-repository.js | 1 - .../repositories/competence-repository.js | 134 ++-- .../repositories/area-repository_test.js | 81 +-- .../competence-datasource_test.js | 37 - .../competence-repository_test.js | 673 ++++++++---------- api/tests/test-helper.js | 2 + 8 files changed, 393 insertions(+), 549 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/competence-datasource.js delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/competence-datasource_test.js diff --git a/api/src/shared/infrastructure/datasources/learning-content/competence-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/competence-datasource.js deleted file mode 100644 index 3966dfc8172..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/competence-datasource.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as datasource from './datasource.js'; - -const competenceDatasource = datasource.extend({ - modelName: 'competences', - - async findByRecordIds(competenceIds) { - const competences = await this.list(); - return competences.filter(({ id }) => competenceIds.includes(id)); - }, -}); - -export { competenceDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index dde23f45252..14f9010a956 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -1,6 +1,5 @@ import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js'; import { challengeDatasource } from './challenge-datasource.js'; -import { competenceDatasource } from './competence-datasource.js'; import { courseDatasource } from './course-datasource.js'; import { skillDatasource } from './skill-datasource.js'; import { thematicDatasource } from './thematic-datasource.js'; @@ -8,7 +7,6 @@ import { tubeDatasource } from './tube-datasource.js'; export { challengeDatasource, - competenceDatasource, courseDatasource, skillDatasource, thematicDatasource, diff --git a/api/src/shared/infrastructure/repositories/area-repository.js b/api/src/shared/infrastructure/repositories/area-repository.js index 9df37ab619a..a05a1820b7a 100644 --- a/api/src/shared/infrastructure/repositories/area-repository.js +++ b/api/src/shared/infrastructure/repositories/area-repository.js @@ -48,7 +48,6 @@ export async function findByRecordIds({ areaIds, locale }) { } export async function getAreaCodeByCompetenceId(competenceId) { - // todo : est-ce qu'on veut cache ça ? const cacheKey = `getAreaCodeByCompetenceId(${competenceId})`; const findByCompetenceIdCallback = (knex) => knex.whereRaw('?=ANY(??)', [competenceId, 'competenceIds']).limit(1); const [areaDto] = await getInstance().find(cacheKey, findByCompetenceIdCallback); diff --git a/api/src/shared/infrastructure/repositories/competence-repository.js b/api/src/shared/infrastructure/repositories/competence-repository.js index 4a2912d4cb6..d01db961b09 100644 --- a/api/src/shared/infrastructure/repositories/competence-repository.js +++ b/api/src/shared/infrastructure/repositories/competence-repository.js @@ -1,84 +1,84 @@ -import _ from 'lodash'; - import { LOCALE, PIX_ORIGIN } from '../../domain/constants.js'; import { NotFoundError } from '../../domain/errors.js'; -import { Competence } from '../../domain/models/Competence.js'; -import { competenceDatasource } from '../datasources/learning-content/competence-datasource.js'; -import { LearningContentResourceNotFound } from '../datasources/learning-content/LearningContentResourceNotFound.js'; +import { Competence } from '../../domain/models/index.js'; +import { getTranslatedKey } from '../../domain/services/get-translated-text.js'; +import { LearningContentRepository } from './learning-content-repository.js'; const { FRENCH_FRANCE } = LOCALE; +const TABLE_NAME = 'learningcontent.competences'; -import { getTranslatedKey } from '../../domain/services/get-translated-text.js'; +export async function list({ locale = FRENCH_FRANCE } = {}) { + const cacheKey = 'list()'; + const listOrderByIndexCallback = (knex) => knex.orderBy('index'); + const competenceDtos = await getInstance().find(cacheKey, listOrderByIndexCallback); + return competenceDtos.map((competenceDto) => toDomain({ competenceDto, locale })); +} -function _toDomain({ competenceData, locale }) { - const translatedCompetenceName = getTranslatedKey(competenceData.name_i18n, locale); - const translatedCompetenceDescription = getTranslatedKey(competenceData.description_i18n, locale); +export async function listPixCompetencesOnly({ locale = FRENCH_FRANCE } = {}) { + const cacheKey = 'listPixCompetencesOnly()'; + const listPixOrderByIndexCallback = (knex) => knex.where('origin', PIX_ORIGIN).orderBy('index'); + const competenceDtos = await getInstance().find(cacheKey, listPixOrderByIndexCallback); + return competenceDtos.map((competenceDto) => toDomain({ competenceDto, locale })); +} - return new Competence({ - id: competenceData.id, - name: translatedCompetenceName, - index: competenceData.index, - description: translatedCompetenceDescription, - origin: competenceData.origin, - skillIds: competenceData.skillIds, - thematicIds: competenceData.thematicIds, - areaId: competenceData.areaId, - }); +export async function get({ id, locale }) { + const competenceDto = await getInstance().load(id); + if (!competenceDto) { + throw new NotFoundError('La compétence demandée n’existe pas'); + } + return toDomain({ competenceDto, locale }); +} + +export async function getCompetenceName({ id, locale }) { + const competence = await get({ id, locale }); + return competence.name; } -const list = function ({ locale } = { locale: FRENCH_FRANCE }) { - return _list({ locale: locale || FRENCH_FRANCE }); -}; +export async function findByRecordIds({ competenceIds, locale }) { + const competenceDtos = await getInstance().loadMany(competenceIds); + return competenceDtos + .filter((competenceDto) => competenceDto) + .sort(byId) + .map((competenceDto) => toDomain({ competenceDto, locale })); +} -const listPixCompetencesOnly = async function ({ locale } = { locale: FRENCH_FRANCE }) { - const allCompetences = await _list({ locale }); - return allCompetences.filter((competence) => competence.origin === PIX_ORIGIN); -}; +export async function findByAreaId({ areaId, locale }) { + const cacheKey = `findByAreaId({ areaId: ${areaId}, locale: ${locale} })`; + const findByAreaIdCallback = (knex) => knex.where('areaId', areaId).orderBy('id'); + const competenceDtos = await getInstance().find(cacheKey, findByAreaIdCallback); + return competenceDtos.map((competenceDto) => toDomain({ competenceDto, locale })); +} -const get = async function ({ id, locale }) { - try { - const competenceData = await competenceDatasource.get(id); - return _toDomain({ competenceData, locale }); - } catch (err) { - if (err instanceof LearningContentResourceNotFound) { - throw new NotFoundError('La compétence demandée n’existe pas'); - } - throw err; - } -}; +export function clearCache() { + return getInstance().clearCache(); +} -const getCompetenceName = async function ({ id, locale }) { - try { - const competence = await competenceDatasource.get(id); - return getTranslatedKey(competence.name_i18n, locale); - } catch (err) { - if (err instanceof LearningContentResourceNotFound) { - throw new NotFoundError('La compétence demandée n’existe pas'); - } - throw err; - } -}; +function byId(entityA, entityB) { + return entityA.id < entityB.id ? -1 : 1; +} -const findByRecordIds = async function ({ competenceIds, locale }) { - const competenceDatas = await competenceDatasource.list(); - return competenceDatas - .filter(({ id }) => competenceIds.includes(id)) - .map((competenceData) => _toDomain({ competenceData, locale })); -}; +function toDomain({ competenceDto, locale }) { + const translatedCompetenceName = getTranslatedKey(competenceDto.name_i18n, locale); + const translatedCompetenceDescription = getTranslatedKey(competenceDto.description_i18n, locale); -const findByAreaId = async function ({ areaId, locale }) { - const competenceDatas = await competenceDatasource.list(); - return competenceDatas - .filter((competenceData) => competenceData.areaId === areaId) - .map((competenceData) => _toDomain({ competenceData, locale })); -}; + return new Competence({ + id: competenceDto.id, + name: translatedCompetenceName, + index: competenceDto.index, + description: translatedCompetenceDescription, + origin: competenceDto.origin, + skillIds: competenceDto.skillIds ? [...competenceDto.skillIds] : null, + thematicIds: competenceDto.thematicIds ? [...competenceDto.thematicIds] : null, + areaId: competenceDto.areaId, + }); +} -export { findByAreaId, findByRecordIds, get, getCompetenceName, list, listPixCompetencesOnly }; +/** @type {LearningContentRepository} */ +let instance; -async function _list({ locale }) { - const competenceDatas = await competenceDatasource.list(); - return _.sortBy( - competenceDatas.map((competenceData) => _toDomain({ competenceData, locale })), - 'index', - ); +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; } diff --git a/api/tests/integration/infrastructure/repositories/area-repository_test.js b/api/tests/integration/infrastructure/repositories/area-repository_test.js index 97e54128947..acd6a293f2a 100644 --- a/api/tests/integration/infrastructure/repositories/area-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/area-repository_test.js @@ -1,7 +1,7 @@ import { PIX_ORIGIN } from '../../../../src/shared/domain/constants.js'; import { NotFoundError } from '../../../../src/shared/domain/errors.js'; import * as areaRepository from '../../../../src/shared/infrastructure/repositories/area-repository.js'; -import { catchErr, databaseBuilder, domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect, knex } from '../../../test-helper.js'; describe('Integration | Repository | area-repository', function () { const areaData0 = { @@ -81,17 +81,20 @@ describe('Integration | Repository | area-repository', function () { thematicIds: ['thematicIdD'], }; - describe('#list', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildFramework({ id: 'recFmk123', name: PIX_ORIGIN }); + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildFramework({ id: 'recFmk123', name: PIX_ORIGIN }); databaseBuilder.factory.learningContent.buildFramework({ id: 'recFmk456', name: 'Un framework pas Pix' }); databaseBuilder.factory.learningContent.buildArea(areaData1); databaseBuilder.factory.learningContent.buildArea(areaData0); databaseBuilder.factory.learningContent.buildArea(areaData2); + databaseBuilder.factory.learningContent.buildCompetence(competenceData1); + databaseBuilder.factory.learningContent.buildCompetence(competenceData2); + databaseBuilder.factory.learningContent.buildCompetence(competenceData3); + databaseBuilder.factory.learningContent.buildCompetence(competenceData4); + await databaseBuilder.commit(); + }); - await databaseBuilder.commit(); - }); - + describe('#list', function () { context('when no locale provided', function () { it('should return all areas translated in default locale FR', async function () { // when @@ -146,13 +149,6 @@ describe('Integration | Repository | area-repository', function () { }); describe('#getAreaCodeByCompetenceId', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildArea(areaData1); - databaseBuilder.factory.learningContent.buildArea(areaData0); - databaseBuilder.factory.learningContent.buildArea(areaData2); - await databaseBuilder.commit(); - }); - context('when competenceId refers to an existing Area', function () { it('should return the code of the corresponding area', async function () { // when @@ -176,20 +172,6 @@ describe('Integration | Repository | area-repository', function () { describe('#listWithPixCompetencesOnly', function () { context('when there are some area that have pix competences', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildArea(areaData2); - databaseBuilder.factory.learningContent.buildArea(areaData0); - databaseBuilder.factory.learningContent.buildArea(areaData1); - await mockLearningContent({ - competences: [competenceData1, competenceData2, competenceData3, competenceData4], - }); - // Décommentez-moi quand on aura traité le competence repository - /*databaseBuilder.factory.learningContent.buildCompetence(competenceData1); - databaseBuilder.factory.learningContent.buildCompetence(competenceData2); - databaseBuilder.factory.learningContent.buildCompetence(competenceData3); - databaseBuilder.factory.learningContent.buildCompetence(competenceData4);*/ - await databaseBuilder.commit(); - }); context('when a locale is provided', function () { it('should return only areas with pix competences with entities translated in given locale when possible or fallback to default locale FR', async function () { // when @@ -267,12 +249,10 @@ describe('Integration | Repository | area-repository', function () { }); context('when there are no areas that have pix competences', function () { beforeEach(async function () { + await knex('learningcontent.areas').truncate(); + await knex('learningcontent.competences').truncate(); databaseBuilder.factory.learningContent.buildArea(areaData1); - await mockLearningContent({ - competences: [competenceData2], - }); - // Décommentez-moi quand on aura traité le competence repository - /*databaseBuilder.factory.learningContent.buildCompetence(competenceData2);*/ + databaseBuilder.factory.learningContent.buildCompetence(competenceData2); await databaseBuilder.commit(); }); @@ -287,21 +267,6 @@ describe('Integration | Repository | area-repository', function () { }); describe('#findByFrameworkIdWithCompetences', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildArea(areaData2); - databaseBuilder.factory.learningContent.buildArea(areaData0); - databaseBuilder.factory.learningContent.buildArea(areaData1); - await mockLearningContent({ - competences: [competenceData1, competenceData2, competenceData3, competenceData4], - }); - // Décommentez-moi quand on aura traité le competence repository - /*databaseBuilder.factory.learningContent.buildCompetence(competenceData1); - databaseBuilder.factory.learningContent.buildCompetence(competenceData2); - databaseBuilder.factory.learningContent.buildCompetence(competenceData3); - databaseBuilder.factory.learningContent.buildCompetence(competenceData4);*/ - await databaseBuilder.commit(); - }); - context('when some areas have the given framework id', function () { context('when a locale is provided', function () { it('should return the areas with competences with all entities translated in given locale or fallback to default locale FR', async function () { @@ -395,13 +360,6 @@ describe('Integration | Repository | area-repository', function () { }); describe('#findByRecordIds', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildArea(areaData1); - databaseBuilder.factory.learningContent.buildArea(areaData2); - databaseBuilder.factory.learningContent.buildArea(areaData0); - await databaseBuilder.commit(); - }); - context('when areas found by ids', function () { context('when no locale provided', function () { it('should return all areas found translated in default locale FR given by their ids', async function () { @@ -458,12 +416,6 @@ describe('Integration | Repository | area-repository', function () { }); describe('#get', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildArea(areaData1); - databaseBuilder.factory.learningContent.buildArea(areaData0); - await databaseBuilder.commit(); - }); - context('when area is found', function () { context('when a locale is provided', function () { it('should return the area translated with the provided locale of fallback to default locale FR', async function () { @@ -517,13 +469,6 @@ describe('Integration | Repository | area-repository', function () { }); describe('#findByFrameworkId', function () { - beforeEach(async function () { - databaseBuilder.factory.learningContent.buildArea(areaData2); - databaseBuilder.factory.learningContent.buildArea(areaData0); - databaseBuilder.factory.learningContent.buildArea(areaData1); - await databaseBuilder.commit(); - }); - context('when some areas have the given framework id', function () { context('when a locale is provided', function () { it('should return the areas translated in given locale or fallback to default locale FR', async function () { diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/competence-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/competence-datasource_test.js deleted file mode 100644 index 49b4c307089..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/competence-datasource_test.js +++ /dev/null @@ -1,37 +0,0 @@ -import { competenceDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | CompetenceDatasource', function () { - describe('#findByRecordIds', function () { - it('should return an array of matching competence data objects', async function () { - // given - const rawCompetence1 = { id: 'RECORD_ID_RAW_COMPETENCE_1' }; - const rawCompetence2 = { id: 'RECORD_ID_RAW_COMPETENCE_2' }; - const rawCompetence3 = { id: 'RECORD_ID_RAW_COMPETENCE_3' }; - const rawCompetence4 = { id: 'RECORD_ID_RAW_COMPETENCE_4' }; - - const records = [rawCompetence1, rawCompetence2, rawCompetence3, rawCompetence4]; - await mockLearningContent({ competences: records }); - const expectedCompetenceIds = [rawCompetence1.id, rawCompetence2.id, rawCompetence4.id]; - - // when - const foundCompetences = await competenceDatasource.findByRecordIds(expectedCompetenceIds); - // then - expect(foundCompetences.map(({ id }) => id)).to.deep.equal(expectedCompetenceIds); - }); - - it('should return an empty array when there are no objects matching the ids', async function () { - // given - const rawCompetence1 = { id: 'RECORD_ID_RAW_COMPETENCE_1' }; - - const records = [rawCompetence1]; - await mockLearningContent({ competences: records }); - - // when - const foundCompetences = await competenceDatasource.findByRecordIds(['some_other_id']); - - // then - expect(foundCompetences).to.be.empty; - }); - }); -}); diff --git a/api/tests/shared/integration/infrastructure/repositories/competence-repository_test.js b/api/tests/shared/integration/infrastructure/repositories/competence-repository_test.js index 1825cc99605..198bee42e46 100644 --- a/api/tests/shared/integration/infrastructure/repositories/competence-repository_test.js +++ b/api/tests/shared/integration/infrastructure/repositories/competence-repository_test.js @@ -1,408 +1,357 @@ +import { PIX_ORIGIN } from '../../../../../src/shared/domain/constants.js'; +import { NotFoundError } from '../../../../../src/shared/domain/errors.js'; import * as competenceRepository from '../../../../../src/shared/infrastructure/repositories/competence-repository.js'; -import { domainBuilder, expect, mockLearningContent } from '../../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Repository | competence-repository', function () { + const competenceData1 = { + id: 'recCompetence1_pix', + name_i18n: { fr: 'name FR recCompetence1_pix', en: 'name EN recCompetence1_pix' }, + description_i18n: { fr: 'description FR recCompetence1_pix', nl: 'description NL recCompetence1_pix' }, + index: 'index recCompetence1_pix', + areaId: 'recArea1', + origin: PIX_ORIGIN, + skillIds: ['skillIdA'], + thematicIds: ['thematicIdA'], + }; + const competenceData2 = { + id: 'recCompetence2_pasPix', + name_i18n: { fr: 'name FR recCompetence2_pasPix', en: 'name EN recCompetence2_pasPix' }, + description_i18n: { fr: 'description FR recCompetence2_pasPix', en: 'description EN recCompetence2_pasPix' }, + index: 'index recCompetence2_pasPix', + areaId: 'recArea0', + origin: 'PasPix', + skillIds: ['skillIdB'], + thematicIds: ['thematicIdB'], + }; + const competenceData3 = { + id: 'recCompetence3_pix', + name_i18n: { fr: 'name FR recCompetence3_pix', nl: 'name NL recCompetence3_pix' }, + description_i18n: { fr: 'description FR recCompetence3_pix', en: 'description EN recCompetence3_pix' }, + index: 'index recCompetence3_pix', + areaId: 'recArea1', + origin: PIX_ORIGIN, + skillIds: ['skillIdC'], + thematicIds: ['thematicIdC'], + }; + + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildCompetence(competenceData3); + databaseBuilder.factory.learningContent.buildCompetence(competenceData2); + databaseBuilder.factory.learningContent.buildCompetence(competenceData1); + await databaseBuilder.commit(); + }); + describe('#get', function () { - it('should return the competence with full area (minus name)', async function () { - // given - const expectedCompetence = domainBuilder.buildCompetence(); - const learningContent = { - competences: [ - { - ...expectedCompetence, - description_i18n: { - fr: expectedCompetence.description, - }, - name_i18n: { - fr: expectedCompetence.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competence = await competenceRepository.get({ id: expectedCompetence.id }); - - // then - expect(competence).to.deepEqualInstance(expectedCompetence); + context('when competence found for given id', function () { + context('when locale is provided', function () { + it('should return the competence translated in the provided locale or fallback to default locale FR', async function () { + // when + const competence = await competenceRepository.get({ id: 'recCompetence3_pix', locale: 'en' }); + + // then + expect(competence).to.deepEqualInstance( + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ); + }); + }); + context('when no locale provided', function () { + it('should return the competence translated in default locale FR', async function () { + // when + const competence = await competenceRepository.get({ id: 'recCompetence3_pix' }); + + // then + expect(competence).to.deepEqualInstance( + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ); + }); + }); }); - it('should return the competence with appropriate translations', async function () { - // given - const locale = 'en'; - const expectedCompetence = domainBuilder.buildCompetence(); - const learningContent = { - competences: [ - { - ...expectedCompetence, - description_i18n: { - en: expectedCompetence.description, - }, - name_i18n: { - en: expectedCompetence.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competence = await competenceRepository.get({ id: expectedCompetence.id, locale }); - - // then - expect(competence).to.deepEqualInstance(expectedCompetence); + context('when no competence found', function () { + it('should throw a NotFound error', async function () { + // when + const err = await catchErr( + competenceRepository.get, + competenceRepository, + )({ id: 'CoucouLesZamis', locale: 'en' }); + + // then + expect(err).to.be.instanceOf(NotFoundError); + expect(err.message).to.equal('La compétence demandée n’existe pas'); + }); }); }); describe('#getCompetenceName', function () { - it('should return the competence name with appropriate translations', async function () { - // given - const locale = 'en'; - const expectedCompetence = domainBuilder.buildCompetence(); - const learningContent = { - competences: [ - { - ...expectedCompetence, - description_i18n: { - en: expectedCompetence.description, - }, - name_i18n: { - en: expectedCompetence.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competenceName = await competenceRepository.getCompetenceName({ id: expectedCompetence.id, locale }); - - // then - expect(competenceName).to.equal(expectedCompetence.name); + context('when competence found for given id', function () { + context('when locale is provided', function () { + it('should return the competence name translated in the provided locale or fallback to default locale FR', async function () { + // when + const competenceName = await competenceRepository.getCompetenceName({ + id: 'recCompetence1_pix', + locale: 'en', + }); + + // then + expect(competenceName).to.equal(competenceData1.name_i18n.en); + }); + }); + context('when no locale provided', function () { + it('should return the competence name translated in default locale FR', async function () { + // when + const competenceName = await competenceRepository.getCompetenceName({ id: 'recCompetence1_pix' }); + + // then + expect(competenceName).to.equal(competenceData1.name_i18n.fr); + }); + }); + }); + + context('when no competence found', function () { + it('should throw a NotFound error', async function () { + // when + const err = await catchErr( + competenceRepository.getCompetenceName, + competenceRepository, + )({ id: 'CoucouLesZamis', locale: 'en' }); + + // then + expect(err).to.be.instanceOf(NotFoundError); + expect(err.message).to.equal('La compétence demandée n’existe pas'); + }); }); }); describe('#list', function () { - it('should return the competences', async function () { - // given - const competence1 = domainBuilder.buildCompetence({ id: 'competence1' }); - const competence2 = domainBuilder.buildCompetence({ id: 'competence2' }); - const learningContent = { - competences: [ - { - ...competence1, - description_i18n: { - fr: competence1.description, - }, - name_i18n: { - fr: competence1.name, - }, - }, - { - ...competence2, - description_i18n: { - fr: competence2.description, - }, - name_i18n: { - fr: competence2.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competences = await competenceRepository.list(); - - // then - expect(competences).to.deepEqualArray([competence1, competence2]); + context('when no locale provided', function () { + it('should return all competences translated by default with locale FR-FR ordered by index', async function () { + // when + const competences = await competenceRepository.list(); + + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.fr, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData2, + name: competenceData2.name_i18n.fr, + description: competenceData2.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ]); + }); }); - it('should return the competences with appropriate translations', async function () { - // given - const locale = 'en'; - const competence = domainBuilder.buildCompetence(); - const learningContent = { - competences: [ - { - ...competence, - description_i18n: { - en: competence.description, - }, - name_i18n: { - en: competence.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competences = await competenceRepository.list({ locale }); - - // then - expect(competences).to.deepEqualArray([competence]); + context('when a locale is provided', function () { + it('should return all competences translated in the given locale or with fallback FR-FR', async function () { + // when + const competences = await competenceRepository.list({ locale: 'en' }); + + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.en, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData2, + name: competenceData2.name_i18n.en, + description: competenceData2.description_i18n.en, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ]); + }); }); }); describe('#listPixCompetencesOnly', function () { - it('should return the competences with only Pix as origin', async function () { - // given - const pixCompetence = domainBuilder.buildCompetence({ id: 'competence1', origin: 'Pix' }); - const nonPixCompetence = domainBuilder.buildCompetence({ id: 'competence2', origin: 'Continuum Espace temps' }); - const learningContent = { - competences: [ - { - ...pixCompetence, - description_i18n: { - fr: pixCompetence.description, - }, - name_i18n: { - fr: pixCompetence.name, - }, - }, - { - ...nonPixCompetence, - description_i18n: { - fr: nonPixCompetence.description, - }, - name_i18n: { - fr: nonPixCompetence.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competences = await competenceRepository.listPixCompetencesOnly(); - - // then - expect(competences).to.deepEqualArray([pixCompetence]); + context('when no locale provided', function () { + it('should return all pix competences translated by default with locale FR-FR ordered by index', async function () { + // when + const competences = await competenceRepository.listPixCompetencesOnly(); + + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.fr, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ]); + }); }); - it('should return the competences with appropriate translations', async function () { - // given - const locale = 'en'; - const competence = domainBuilder.buildCompetence({ origin: 'Pix' }); - const learningContent = { - competences: [ - { - ...competence, - description_i18n: { - en: competence.description, - }, - name_i18n: { - en: competence.name, - }, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const competences = await competenceRepository.listPixCompetencesOnly({ locale }); - - // then - expect(competences).to.deepEqualArray([competence]); + context('when a locale is provided', function () { + it('should return all pix competences translated in the given locale or with fallback FR-FR', async function () { + // when + const competences = await competenceRepository.listPixCompetencesOnly({ locale: 'en' }); + + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.en, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ]); + }); }); }); describe('#findByRecordIds', function () { - beforeEach(async function () { - const learningContent = { - competences: [ - { - id: 'competence1', - name_i18n: { fr: 'competence1 name fr', en: 'competence1 name en' }, - index: '1.1', - description_i18n: { fr: 'competence1 description fr', en: 'competence1 description en' }, - origin: 'competence1 origin', - skillIds: ['skillA'], - thematicIds: ['thematicA'], - areaId: 'area1', - }, - { - id: 'competence2', - name_i18n: { fr: 'competence2 name fr', en: 'competence2 name en' }, - index: '2.2', - description_i18n: { fr: 'competence2 description fr', en: 'competence2 description en' }, - origin: 'competence2 origin', - skillIds: ['skillB'], - thematicIds: ['thematicB'], - areaId: 'area2', - }, - { - id: 'competence3', - name_i18n: { fr: 'competence3 name fr', en: 'competence3 name en' }, - index: '3.3', - description_i18n: { fr: 'competence3 description fr', en: 'competence3 description en' }, - origin: 'competence3 origin', - skillIds: ['skillC'], - thematicIds: ['thematicC'], - areaId: 'area3', - }, - ], - }; - await mockLearningContent(learningContent); - }); + context('when competences found by ids', function () { + context('when no locale provided', function () { + it('should return all competences found translated in default locale FR given by their ids', async function () { + // when + const competences = await competenceRepository.findByRecordIds({ + competenceIds: ['recCompetence3_pix', 'recCompetence2_pasPix'], + }); - it('should return competences given by id with default locale', async function () { - // when - const competences = await competenceRepository.findByRecordIds({ - competenceIds: ['competence1', 'competence3'], + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData2, + name: competenceData2.name_i18n.fr, + description: competenceData2.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ]); + }); }); - // then - const competence1 = domainBuilder.buildCompetence({ - id: 'competence1', - name: 'competence1 name fr', - index: '1.1', - description: 'competence1 description fr', - areaId: 'area1', - skillIds: ['skillA'], - thematicIds: ['thematicA'], - origin: 'competence1 origin', - }); - const competence3 = domainBuilder.buildCompetence({ - id: 'competence3', - name: 'competence3 name fr', - index: '3.3', - description: 'competence3 description fr', - areaId: 'area3', - skillIds: ['skillC'], - thematicIds: ['thematicC'], - origin: 'competence3 origin', + context('when a locale is provided', function () { + it('should return all competences found translated in provided locale of fallback to default locale FR', async function () { + // when + const competences = await competenceRepository.findByRecordIds({ + competenceIds: ['recCompetence3_pix', 'recCompetence2_pasPix'], + locale: 'en', + }); + + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData2, + name: competenceData2.name_i18n.en, + description: competenceData2.description_i18n.en, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ]); + }); }); - expect(competences).to.deepEqualArray([competence1, competence3]); }); - it('should return competences in given locale', async function () { - // when - const competences = await competenceRepository.findByRecordIds({ - competenceIds: ['competence1', 'competence3'], - locale: 'en', - }); + context('when no competences found for given ids', function () { + it('should return an empty array', async function () { + // when + const competences = await competenceRepository.findByRecordIds({ + competenceIds: ['recCompetenceCOUCOU', 'recCompetenceMAMAN'], + }); - // then - const competence1 = domainBuilder.buildCompetence({ - id: 'competence1', - name: 'competence1 name en', - index: '1.1', - description: 'competence1 description en', - areaId: 'area1', - skillIds: ['skillA'], - thematicIds: ['thematicA'], - origin: 'competence1 origin', + // then + expect(competences).to.deep.equal([]); }); - const competence3 = domainBuilder.buildCompetence({ - id: 'competence3', - name: 'competence3 name en', - index: '3.3', - description: 'competence3 description en', - areaId: 'area3', - skillIds: ['skillC'], - thematicIds: ['thematicC'], - origin: 'competence3 origin', - }); - expect(competences).to.deepEqualArray([competence1, competence3]); }); }); - describe('#findByAreaIds', function () { - beforeEach(async function () { - const learningContent = { - competences: [ - { - id: 'competence1', - name_i18n: { fr: 'competence1 name fr', en: 'competence1 name en' }, - index: '1.1', - description_i18n: { fr: 'competence1 description fr', en: 'competence1 description en' }, - origin: 'competence1 origin', - skillIds: ['skillA'], - thematicIds: ['thematicA'], - areaId: 'area1', - }, - { - id: 'competence2', - name_i18n: { fr: 'competence2 name fr', en: 'competence2 name en' }, - index: '2.2', - description_i18n: { fr: 'competence2 description fr', en: 'competence2 description en' }, - origin: 'competence2 origin', - skillIds: ['skillB'], - thematicIds: ['thematicB'], - areaId: 'area2', - }, - { - id: 'competence3', - name_i18n: { fr: 'competence3 name fr', en: 'competence3 name en' }, - index: '1.3', - description_i18n: { fr: 'competence3 description fr', en: 'competence3 description en' }, - origin: 'competence3 origin', - skillIds: ['skillC'], - thematicIds: ['thematicC'], - areaId: 'area1', - }, - ], - }; - await mockLearningContent(learningContent); - }); + describe('#findByAreaId', function () { + context('when competences found for area id', function () { + context('when no locale provided', function () { + it('should return all competences found translated in default locale FR given by their ids', async function () { + // when + const competences = await competenceRepository.findByAreaId({ + areaId: 'recArea1', + }); - it('should return competences given by areaId with default locale', async function () { - // when - const competences = await competenceRepository.findByAreaId({ areaId: 'area1' }); - - // then - const competence1 = domainBuilder.buildCompetence({ - id: 'competence1', - name: 'competence1 name fr', - index: '1.1', - description: 'competence1 description fr', - areaId: 'area1', - skillIds: ['skillA'], - thematicIds: ['thematicA'], - origin: 'competence1 origin', + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.fr, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.fr, + }), + ]); + }); }); - const competence3 = domainBuilder.buildCompetence({ - id: 'competence3', - name: 'competence3 name fr', - index: '1.3', - description: 'competence3 description fr', - areaId: 'area1', - skillIds: ['skillC'], - thematicIds: ['thematicC'], - origin: 'competence3 origin', + + context('when a locale is provided', function () { + it('should return all competences found translated in provided locale of fallback to default locale FR', async function () { + // when + const competences = await competenceRepository.findByAreaId({ + areaId: 'recArea1', + locale: 'en', + }); + + // then + expect(competences).to.deepEqualArray([ + domainBuilder.buildCompetence({ + ...competenceData1, + name: competenceData1.name_i18n.en, + description: competenceData1.description_i18n.fr, + }), + domainBuilder.buildCompetence({ + ...competenceData3, + name: competenceData3.name_i18n.fr, + description: competenceData3.description_i18n.en, + }), + ]); + }); }); - expect(competences).to.deepEqualArray([competence1, competence3]); }); - it('should return competences in given locale', async function () { - // when - const competences = await competenceRepository.findByAreaId({ areaId: 'area1', locale: 'en' }); - - // then - const competence1 = domainBuilder.buildCompetence({ - id: 'competence1', - name: 'competence1 name en', - index: '1.1', - description: 'competence1 description en', - areaId: 'area1', - skillIds: ['skillA'], - thematicIds: ['thematicA'], - origin: 'competence1 origin', - }); - const competence3 = domainBuilder.buildCompetence({ - id: 'competence3', - name: 'competence3 name en', - index: '1.3', - description: 'competence3 description en', - areaId: 'area1', - skillIds: ['skillC'], - thematicIds: ['thematicC'], - origin: 'competence3 origin', + context('when no competences found for given area id', function () { + it('should return an empty array', async function () { + // when + const competences = await competenceRepository.findByAreaId({ + areaId: 'recCoucouRoro', + }); + + // then + expect(competences).to.deep.equal([]); }); - expect(competences).to.deepEqualArray([competence1, competence3]); }); }); }); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index 8e8cf1441da..f293fdd813f 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -25,6 +25,7 @@ import { Membership } from '../src/shared/domain/models/index.js'; import * as tokenService from '../src/shared/domain/services/token-service.js'; import { LearningContentCache } from '../src/shared/infrastructure/caches/learning-content-cache.js'; import * as areaRepository from '../src/shared/infrastructure/repositories/area-repository.js'; +import * as competenceRepository from '../src/shared/infrastructure/repositories/competence-repository.js'; import * as customChaiHelpers from './tooling/chai-custom-helpers/index.js'; import * as domainBuilder from './tooling/domain-builder/factory/index.js'; import { jobChai } from './tooling/jobs/expect-job.js'; @@ -75,6 +76,7 @@ afterEach(function () { nock.cleanAll(); frameworkRepository.clearCache(); areaRepository.clearCache(); + competenceRepository.clearCache(); return databaseBuilder.clean(); }); From f7b65cc803d0c05af004427a8a548454c8668ef0 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 15:07:32 +0100 Subject: [PATCH 07/24] feat(api): refacto thematicRepository with new cache and to use PG --- .../repositories/thematic-repository.js | 72 ++-- .../datasources/learning-content/index.js | 10 +- .../learning-content/thematic-datasource.js | 17 - .../repositories/thematic-repository_test.js | 374 +++++++++--------- .../thematic-datasource_test.js | 39 -- api/tests/test-helper.js | 2 + 6 files changed, 227 insertions(+), 287 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/thematic-datasource.js delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/thematic-datasource_test.js diff --git a/api/lib/infrastructure/repositories/thematic-repository.js b/api/lib/infrastructure/repositories/thematic-repository.js index c4a2f47893d..b880033bcb1 100644 --- a/api/lib/infrastructure/repositories/thematic-repository.js +++ b/api/lib/infrastructure/repositories/thematic-repository.js @@ -1,37 +1,59 @@ -import _ from 'lodash'; - import { LOCALE } from '../../../src/shared/domain/constants.js'; import { Thematic } from '../../../src/shared/domain/models/Thematic.js'; import { getTranslatedKey } from '../../../src/shared/domain/services/get-translated-text.js'; -import { thematicDatasource } from '../../../src/shared/infrastructure/datasources/learning-content/thematic-datasource.js'; +import { LearningContentRepository } from '../../../src/shared/infrastructure/repositories/learning-content-repository.js'; const { FRENCH_FRANCE } = LOCALE; +const TABLE_NAME = 'learningcontent.thematics'; + +export async function list({ locale = FRENCH_FRANCE } = {}) { + const cacheKey = 'list()'; + const listCallback = (knex) => knex.orderBy('id'); + const thematicDtos = await getInstance().find(cacheKey, listCallback); + return thematicDtos.map((thematicDto) => toDomain(thematicDto, locale)); +} + +export async function findByCompetenceIds(competenceIds, locale) { + const cacheKey = `findByCompetenceIds([${competenceIds.sort()}])`; + const findByCompetenceIdsCallback = (knex) => knex.whereIn('competenceId', competenceIds).orderBy('id'); + const thematicDtos = await getInstance().find(cacheKey, findByCompetenceIdsCallback); + return thematicDtos.map((thematicDto) => toDomain(thematicDto, locale)); +} + +export async function findByRecordIds(ids, locale) { + const thematicDtos = await getInstance().loadMany(ids); + return thematicDtos + .filter((thematic) => thematic) + .map((thematicDto) => toDomain(thematicDto, locale)) + .sort(byLocalizedName(locale)); +} -function _toDomain(thematicData, locale) { - const translatedName = getTranslatedKey(thematicData.name_i18n, locale); +export function clearCache() { + return getInstance().clearCache(); +} + +function byLocalizedName(locale) { + const collator = new Intl.Collator(locale, { usage: 'sort' }); + return (thematic1, thematic2) => collator.compare(thematic1.name, thematic2.name); +} + +function toDomain(thematicDto, locale) { + const translatedName = getTranslatedKey(thematicDto.name_i18n, locale); return new Thematic({ - id: thematicData.id, + id: thematicDto.id, name: translatedName, - index: thematicData.index, - tubeIds: thematicData.tubeIds, - competenceId: thematicData.competenceId, + index: thematicDto.index, + tubeIds: thematicDto.tubeIds ? [...thematicDto.tubeIds] : null, + competenceId: thematicDto.competenceId, }); } -const list = async function ({ locale } = { locale: FRENCH_FRANCE }) { - const thematicDatas = await thematicDatasource.list(); - return thematicDatas.map((thematicData) => _toDomain(thematicData, locale)); -}; +/** @type {LearningContentRepository} */ +let instance; -const findByCompetenceIds = async function (competenceIds, locale) { - const thematicDatas = await thematicDatasource.findByCompetenceIds(competenceIds); - return thematicDatas.map((thematicData) => _toDomain(thematicData, locale)); -}; - -const findByRecordIds = async function (thematicIds, locale) { - const thematicDatas = await thematicDatasource.findByRecordIds(thematicIds); - const thematics = thematicDatas.map((thematicData) => _toDomain(thematicData, locale)); - return _.orderBy(thematics, (thematic) => thematic.name.toLowerCase()); -}; - -export { findByCompetenceIds, findByRecordIds, list }; +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; +} diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index 14f9010a956..ecf3659bd74 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -2,14 +2,6 @@ import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasourc import { challengeDatasource } from './challenge-datasource.js'; import { courseDatasource } from './course-datasource.js'; import { skillDatasource } from './skill-datasource.js'; -import { thematicDatasource } from './thematic-datasource.js'; import { tubeDatasource } from './tube-datasource.js'; -export { - challengeDatasource, - courseDatasource, - skillDatasource, - thematicDatasource, - tubeDatasource, - tutorialDatasource, -}; +export { challengeDatasource, courseDatasource, skillDatasource, tubeDatasource, tutorialDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/thematic-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/thematic-datasource.js deleted file mode 100644 index c1e60491803..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/thematic-datasource.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as datasource from '../../../infrastructure/datasources/learning-content/datasource.js'; - -const thematicDatasource = datasource.extend({ - modelName: 'thematics', - - async findByCompetenceIds(competenceIds) { - const thematics = await this.list(); - return thematics.filter((thematic) => competenceIds.includes(thematic.competenceId)); - }, - - async findByRecordIds(thematicIds) { - const thematics = await this.list(); - return thematics.filter(({ id }) => thematicIds.includes(id)); - }, -}); - -export { thematicDatasource }; diff --git a/api/tests/integration/infrastructure/repositories/thematic-repository_test.js b/api/tests/integration/infrastructure/repositories/thematic-repository_test.js index e6777d51e9a..f20e9efbcb3 100644 --- a/api/tests/integration/infrastructure/repositories/thematic-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/thematic-repository_test.js @@ -1,229 +1,209 @@ import * as thematicRepository from '../../../../lib/infrastructure/repositories/thematic-repository.js'; -import { Thematic } from '../../../../src/shared/domain/models/Thematic.js'; -import { domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; +import { databaseBuilder, domainBuilder, expect } from '../../../test-helper.js'; describe('Integration | Repository | thematic-repository', function () { - describe('#list', function () { - it('should return thematics with FR default language', async function () { - // given - const thematic = domainBuilder.buildThematic({ id: 'recThematic1', name: 'frName' }); - - const learningContent = { - thematics: [{ ...thematic, name_i18n: { fr: 'frName' } }], - }; - - await mockLearningContent(learningContent); + const thematicData0 = { + id: 'thematicId0', + name_i18n: { fr: 'name FR thematicId0', en: 'name EN thematicId0' }, + index: 5, + competenceId: 'competenceIdA', + tubeIds: ['tubeIdA'], + }; + const thematicData1 = { + id: 'thematicId1', + name_i18n: { fr: 'name FR thematicId1', nl: 'name NL thematicId1' }, + index: 15, + competenceId: 'competenceIdB', + tubeIds: ['tubeIdA'], + }; + const thematicData2 = { + id: 'thematicId2', + name_i18n: { fr: 'name FR thematicId2', en: 'name EN thematicId2' }, + index: 9, + competenceId: 'competenceIdA', + tubeIds: ['tubeIdB'], + }; + const thematicData3 = { + id: 'thematicId3', + name_i18n: { fr: 'name FR thematicId3', nl: 'name NL thematicId3' }, + index: 2, + competenceId: 'competenceIdC', + tubeIds: ['tubeIdC'], + }; + + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildThematic(thematicData0); + databaseBuilder.factory.learningContent.buildThematic(thematicData3); + databaseBuilder.factory.learningContent.buildThematic(thematicData2); + databaseBuilder.factory.learningContent.buildThematic(thematicData1); + await databaseBuilder.commit(); + }); - // when - const actualThematics = await thematicRepository.list(); + describe('#list', function () { + context('when no locale provided', function () { + it('should return all thematics translated by default with locale FR-FR', async function () { + // when + const thematics = await thematicRepository.list(); - // then - expect(actualThematics).to.deepEqualArray([thematic]); + // then + expect(thematics).to.deepEqualArray([ + domainBuilder.buildThematic({ + ...thematicData0, + name: thematicData0.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData1, + name: thematicData1.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData2, + name: thematicData2.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData3, + name: thematicData3.name_i18n.fr, + }), + ]); + }); }); - it('should return thematics translated in given language', async function () { - // given - const locale = 'en'; - const thematic = domainBuilder.buildThematic({ id: 'recThematic1', name: 'enName' }); - - const learningContent = { - thematics: [{ ...thematic, name_i18n: { fr: 'frName', en: 'enName' } }], - }; - - await mockLearningContent(learningContent); - - // when - const actualThematics = await thematicRepository.list({ locale }); + context('when a locale is provided', function () { + it('should return all thematics translated in the given locale or with fallback FR-FR', async function () { + // when + const thematics = await thematicRepository.list({ locale: 'en' }); - // then - expect(actualThematics).to.deepEqualArray([thematic]); + // then + expect(thematics).to.deepEqualArray([ + domainBuilder.buildThematic({ + ...thematicData0, + name: thematicData0.name_i18n.en, + }), + domainBuilder.buildThematic({ + ...thematicData1, + name: thematicData1.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData2, + name: thematicData2.name_i18n.en, + }), + domainBuilder.buildThematic({ + ...thematicData3, + name: thematicData3.name_i18n.fr, + }), + ]); + }); }); }); - describe('#findByCompetenceId', function () { - const competenceId = 'competence0'; - - // given - const thematic0 = { - id: 'recThematic0', - name_i18n: { - fr: 'thematic0', - en: 'thematic0EnUs', - }, - index: 1, - tubeIds: ['recTube0'], - competenceId, - }; - - const thematic1 = { - id: 'recThematic1', - name_i18n: { - fr: 'thematic1', - en: 'thematic1EnUs', - }, - index: 1, - tubeIds: ['recTube1'], - competenceId, - }; - - const thematics = [ - thematic0, - thematic1, - { - id: 'recThematic2', - name_i18n: { - fr: 'thematic2', - en: 'thematic2EnUs', - }, - index: 1, - tubeIds: ['recTube2'], - competenceId: 'competence1', - }, - ]; - - const learningContent = { - thematics, - }; - - beforeEach(async function () { - await mockLearningContent(learningContent); - }); - - it('should return thematics of a competence', async function () { - // when - const foundThematics = await thematicRepository.findByCompetenceIds([competenceId]); - - // then - expect(foundThematics).to.have.lengthOf(2); - expect(foundThematics[0]).to.deep.equal({ - id: 'recThematic0', - name: 'thematic0', - index: 1, - tubeIds: ['recTube0'], - competenceId: 'competence0', + describe('#findByCompetenceIds', function () { + context('when thematics found by competence ids', function () { + context('when no locale provided', function () { + it('should return all thematics found translated in default locale FR given by their ids', async function () { + // when + const thematics = await thematicRepository.findByCompetenceIds(['competenceIdB', 'competenceIdA']); + + // then + expect(thematics).to.deepEqualArray([ + domainBuilder.buildThematic({ + ...thematicData0, + name: thematicData0.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData1, + name: thematicData1.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData2, + name: thematicData2.name_i18n.fr, + }), + ]); + }); }); - expect(foundThematics[0]).to.be.instanceOf(Thematic); - expect(foundThematics[1]).to.deep.equal({ - id: 'recThematic1', - name: 'thematic1', - index: 1, - tubeIds: ['recTube1'], - competenceId: 'competence0', + + context('when a locale is provided', function () { + it('should return all thematics found translated in provided locale of fallback to default locale FR', async function () { + // when + const thematics = await thematicRepository.findByCompetenceIds(['competenceIdB', 'competenceIdA'], 'en'); + + // then + expect(thematics).to.deepEqualArray([ + domainBuilder.buildThematic({ + ...thematicData0, + name: thematicData0.name_i18n.en, + }), + domainBuilder.buildThematic({ + ...thematicData1, + name: thematicData1.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData2, + name: thematicData2.name_i18n.en, + }), + ]); + }); }); - expect(foundThematics[1]).to.be.instanceOf(Thematic); }); - describe('When locale is en', function () { - it('should return the translated name in english', async function () { - const locale = 'en'; + context('when no thematics found for given competence ids', function () { + it('should return an empty array', async function () { // when - const foundThematics = await thematicRepository.findByCompetenceIds([competenceId], locale); + const thematics = await thematicRepository.findByCompetenceIds(['recCouCouRoRo', 'recCouCouGabDou']); // then - expect(foundThematics).to.have.lengthOf(2); - expect(foundThematics[0]).to.deep.equal({ - id: 'recThematic0', - name: 'thematic0EnUs', - index: 1, - tubeIds: ['recTube0'], - competenceId: 'competence0', - }); - expect(foundThematics[0]).to.be.instanceOf(Thematic); - expect(foundThematics[1]).to.deep.equal({ - id: 'recThematic1', - name: 'thematic1EnUs', - index: 1, - tubeIds: ['recTube1'], - competenceId: 'competence0', - }); - expect(foundThematics[1]).to.be.instanceOf(Thematic); + expect(thematics).to.deep.equal([]); }); }); }); describe('#findByRecordIds', function () { - beforeEach(async function () { - const learningContentThematic0 = { - id: 'recThematic0', - name_i18n: { - fr: 'nameThemaFR0', - en: 'nameThemaEN0', - }, - index: 0, - description: 'tubeDescription0', - competenceId: 'recComp0', - }; - const learningContentThematic1 = { - id: 'recThematic1', - name_i18n: { - fr: 'nameThemaFR1', - en: 'nameThemaEN1', - }, - index: 1, - description: 'tubeDescription1', - competenceId: 'recComp1', - }; - const learningContentThematic2 = { - id: 'recThematic2', - name_i18n: { - fr: 'nameThemaFR2', - en: 'nameThemaEN2', - }, - index: 2, - description: 'tubeDescription2', - competenceId: 'recComp2', - }; - await mockLearningContent({ - thematics: [learningContentThematic0, learningContentThematic1, learningContentThematic2], + context('when thematics found by ids', function () { + context('when no locale provided', function () { + it('should return all thematics found translated in default locale FR given by their ids ordered by name', async function () { + // when + const thematics = await thematicRepository.findByRecordIds(['thematicId3', 'thematicId0']); + + // then + expect(thematics).to.deepEqualArray([ + domainBuilder.buildThematic({ + ...thematicData0, + name: thematicData0.name_i18n.fr, + }), + domainBuilder.buildThematic({ + ...thematicData3, + name: thematicData3.name_i18n.fr, + }), + ]); + }); }); - }); - it('should return a list of thematics (locale FR - default)', async function () { - // given - const thematic1 = new Thematic({ - id: 'recThematic1', - name: 'nameThemaFR1', - index: 1, - competenceId: 'recComp1', - tubeIds: [], - }); - const thematic2 = new Thematic({ - id: 'recThematic2', - name: 'nameThemaFR2', - index: 2, - competenceId: 'recComp2', - tubeIds: [], + context('when a locale is provided', function () { + it('should return all thematics found translated in provided locale of fallback to default locale FR', async function () { + // when + const thematics = await thematicRepository.findByRecordIds(['thematicId3', 'thematicId0'], 'en'); + + // then + expect(thematics).to.deepEqualArray([ + domainBuilder.buildThematic({ + ...thematicData0, + name: thematicData0.name_i18n.en, + }), + domainBuilder.buildThematic({ + ...thematicData3, + name: thematicData3.name_i18n.fr, + }), + ]); + }); }); - - // when - const thematics = await thematicRepository.findByRecordIds(['recThematic2', 'recThematic1']); - - // then - expect(thematics).to.deepEqualArray([thematic1, thematic2]); }); - it('should return a list of thematics with locale EN', async function () { - // given - const thematic1 = new Thematic({ - id: 'recThematic1', - name: 'nameThemaEN1', - index: 1, - competenceId: 'recComp1', - tubeIds: [], - }); - const thematic2 = new Thematic({ - id: 'recThematic2', - name: 'nameThemaEN2', - index: 2, - competenceId: 'recComp2', - tubeIds: [], - }); - - // when - const thematics = await thematicRepository.findByRecordIds(['recThematic2', 'recThematic1'], 'en'); + context('when no thematics found for given ids', function () { + it('should return an empty array', async function () { + // when + const thematics = await thematicRepository.findByRecordIds(['recCouCouRoRo', 'recCouCouGabDou']); - // then - expect(thematics).to.deepEqualArray([thematic1, thematic2]); + // then + expect(thematics).to.deep.equal([]); + }); }); }); }); diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/thematic-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/thematic-datasource_test.js deleted file mode 100644 index 932367b11b2..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/thematic-datasource_test.js +++ /dev/null @@ -1,39 +0,0 @@ -import { thematicDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | ThematicDatasource', function () { - describe('#findByCompetenceIds', function () { - it('should return an array of matching learning content thematics data objects by competence ids', async function () { - // given - const records = [ - { id: 'recThematic0', competenceId: 'competence1' }, - { id: 'recThematic1', competenceId: 'competence2' }, - { id: 'recThematic3', competenceId: 'competence3' }, - { id: 'recThematic2', competenceId: 'competence1' }, - ]; - await mockLearningContent({ thematics: records }); - const expectedThematicIds = ['recThematic0', 'recThematic1', 'recThematic2']; - const competenceIds = ['competence1', 'competence2']; - - // when - const foundThematics = await thematicDatasource.findByCompetenceIds(competenceIds); - // then - expect(foundThematics.map(({ id }) => id)).to.deep.equal(expectedThematicIds); - }); - }); - - describe('#findByRecordIds', function () { - it('should return an array of matching learning content thematics data objects by ids', async function () { - // given - const records = [{ id: 'recThematic0' }, { id: 'recThematic1' }, { id: 'recThematic3' }, { id: 'recThematic2' }]; - await mockLearningContent({ thematics: records }); - - // when - const foundThematics = await thematicDatasource.findByRecordIds(['recThematic1', 'recThematic3']); - - // then - const expectedThematicIds = ['recThematic1', 'recThematic3']; - expect(foundThematics.map(({ id }) => id)).to.deep.equal(expectedThematicIds); - }); - }); -}); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index f293fdd813f..f340010df98 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -19,6 +19,7 @@ import sinonChai from 'sinon-chai'; import { DatabaseBuilder } from '../db/database-builder/database-builder.js'; import { disconnect, knex } from '../db/knex-database-connection.js'; import * as frameworkRepository from '../lib/infrastructure/repositories/framework-repository.js'; +import * as thematicRepository from '../lib/infrastructure/repositories/thematic-repository.js'; import { PIX_ADMIN } from '../src/authorization/domain/constants.js'; import { config } from '../src/shared/config.js'; import { Membership } from '../src/shared/domain/models/index.js'; @@ -77,6 +78,7 @@ afterEach(function () { frameworkRepository.clearCache(); areaRepository.clearCache(); competenceRepository.clearCache(); + thematicRepository.clearCache(); return databaseBuilder.clean(); }); From e8d04c50c3738c126b6ba2ab5d14441551f31395 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 15:50:23 +0100 Subject: [PATCH 08/24] feat(api): refacto tubeRepository with new cache and to use PG --- .../repositories/tube-repository.js | 127 +-- .../datasources/learning-content/index.js | 3 +- .../learning-content/tube-datasource.js | 24 - .../repositories/tube-repository_test.js | 838 ++++++------------ .../learning-content/tube-datasource_test.js | 92 -- api/tests/test-helper.js | 2 + 6 files changed, 354 insertions(+), 732 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/tube-datasource.js delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/tube-datasource_test.js diff --git a/api/lib/infrastructure/repositories/tube-repository.js b/api/lib/infrastructure/repositories/tube-repository.js index cf04376ad4a..a08d1104dc7 100644 --- a/api/lib/infrastructure/repositories/tube-repository.js +++ b/api/lib/infrastructure/repositories/tube-repository.js @@ -1,68 +1,89 @@ -import _ from 'lodash'; - +import { knex } from '../../../db/knex-database-connection.js'; import { Tube } from '../../../src/shared/domain/models/Tube.js'; import { getTranslatedKey } from '../../../src/shared/domain/services/get-translated-text.js'; -import { - skillDatasource, - tubeDatasource, -} from '../../../src/shared/infrastructure/datasources/learning-content/index.js'; +import { LearningContentResourceNotFound } from '../../../src/shared/infrastructure/datasources/learning-content/LearningContentResourceNotFound.js'; +import { LearningContentRepository } from '../../../src/shared/infrastructure/repositories/learning-content-repository.js'; -function _toDomain({ tubeData, locale }) { - const translatedPracticalTitle = getTranslatedKey(tubeData.practicalTitle_i18n, locale); - const translatedPracticalDescription = getTranslatedKey(tubeData.practicalDescription_i18n, locale); +const TABLE_NAME = 'learningcontent.tubes'; +const ACTIVE_STATUS = 'actif'; - return new Tube({ - id: tubeData.id, - name: tubeData.name, - practicalTitle: translatedPracticalTitle, - practicalDescription: translatedPracticalDescription, - isMobileCompliant: tubeData.isMobileCompliant, - isTabletCompliant: tubeData.isTabletCompliant, - competenceId: tubeData.competenceId, - thematicId: tubeData.thematicId, - skillIds: tubeData.skillIds, - }); +export async function get(id) { + const tubeDto = await getInstance().load(id); + if (!tubeDto) { + throw new LearningContentResourceNotFound(); + } + return toDomain(tubeDto); } -async function _findActive(tubes) { - const skillsByTubesIndex = await Promise.all( - tubes.map(async ({ id: tubeId }) => skillDatasource.findActiveByTubeId(tubeId)), +export async function list() { + const cacheKey = `list()`; + const listCallback = (knex) => knex; + const tubeDtos = await getInstance().find(cacheKey, listCallback); + return toDomainList(tubeDtos); +} + +export async function findByNames({ tubeNames, locale }) { + const ids = await knex.pluck('id').from(TABLE_NAME).whereIn('name', tubeNames).orderBy('name'); + const tubeDtos = await getInstance().loadMany(ids); + return toDomainList(tubeDtos, locale); +} + +export async function findByRecordIds(ids, locale) { + const tubeDtos = await getInstance().loadMany(ids); + return toDomainList( + tubeDtos.filter((tubeDto) => tubeDto), + locale, ); +} - return tubes.filter((_, index) => { - const hasActiveSkills = skillsByTubesIndex[index].length > 0; - return hasActiveSkills; - }); +export async function findActiveByRecordIds(ids, locale) { + const activeTubeIds = await knex + .pluck('tubeId') + .distinct() + .from('learningcontent.skills') + .whereIn('tubeId', ids) + .where('status', ACTIVE_STATUS) + .orderBy('tubeId'); + const tubeDtos = await getInstance().loadMany(activeTubeIds); + return toDomainList(tubeDtos, locale); } -const get = async function (id) { - const tubeData = await tubeDatasource.get(id); - return _toDomain({ tubeData }); -}; +export function clearCache() { + return getInstance().clearCache(); +} -const list = async function () { - const tubeDatas = await tubeDatasource.list(); - const tubes = _.map(tubeDatas, (tubeData) => _toDomain({ tubeData })); - return _.orderBy(tubes, (tube) => tube.name.toLowerCase()); -}; +function toDomainList(tubeDtos, locale) { + return tubeDtos.sort(byName).map((tubeDto) => toDomain(tubeDto, locale)); +} -const findByNames = async function ({ tubeNames, locale }) { - const tubeDatas = await tubeDatasource.findByNames(tubeNames); - const tubes = _.map(tubeDatas, (tubeData) => _toDomain({ tubeData, locale })); - return _.orderBy(tubes, (tube) => tube.name.toLowerCase()); -}; +function byName(tube1, tube2) { + return tube1.name < tube2.name ? -1 : 1; +} -const findByRecordIds = async function (tubeIds, locale) { - const tubeDatas = await tubeDatasource.findByRecordIds(tubeIds); - const tubes = _.map(tubeDatas, (tubeData) => _toDomain({ tubeData, locale })); - return _.orderBy(tubes, (tube) => tube.name.toLowerCase()); -}; +function toDomain(tubeDto, locale) { + const translatedPracticalTitle = getTranslatedKey(tubeDto.practicalTitle_i18n, locale); + const translatedPracticalDescription = getTranslatedKey(tubeDto.practicalDescription_i18n, locale); + + return new Tube({ + id: tubeDto.id, + name: tubeDto.name, + practicalTitle: translatedPracticalTitle, + practicalDescription: translatedPracticalDescription, + isMobileCompliant: tubeDto.isMobileCompliant, + isTabletCompliant: tubeDto.isTabletCompliant, + competenceId: tubeDto.competenceId, + thematicId: tubeDto.thematicId, + skillIds: tubeDto.skillIds ? [...tubeDto.skillIds] : null, + skills: [], + }); +} -const findActiveByRecordIds = async function (tubeIds, locale) { - const tubeDatas = await tubeDatasource.findByRecordIds(tubeIds); - const activeTubes = await _findActive(tubeDatas); - const tubes = _.map(activeTubes, (tubeData) => _toDomain({ tubeData, locale })); - return _.orderBy(tubes, (tube) => tube.name.toLowerCase()); -}; +/** @type {LearningContentRepository} */ +let instance; -export { findActiveByRecordIds, findByNames, findByRecordIds, get, list }; +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; +} diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index ecf3659bd74..b0ff6e941de 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -2,6 +2,5 @@ import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasourc import { challengeDatasource } from './challenge-datasource.js'; import { courseDatasource } from './course-datasource.js'; import { skillDatasource } from './skill-datasource.js'; -import { tubeDatasource } from './tube-datasource.js'; -export { challengeDatasource, courseDatasource, skillDatasource, tubeDatasource, tutorialDatasource }; +export { challengeDatasource, courseDatasource, skillDatasource, tutorialDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/tube-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/tube-datasource.js deleted file mode 100644 index 2db5b48ac06..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/tube-datasource.js +++ /dev/null @@ -1,24 +0,0 @@ -import _ from 'lodash'; - -import * as datasource from './datasource.js'; - -const tubeDatasource = datasource.extend({ - modelName: 'tubes', - - async findByNames(tubeNames) { - const tubes = await this.list(); - return tubes.filter((tubeData) => _.includes(tubeNames, tubeData.name)); - }, - - async findByRecordIds(tubeIds) { - const tubes = await this.list(); - return tubes.filter(({ id }) => tubeIds.includes(id)); - }, - - async findByThematicId(thematicId) { - const tubes = await this.list(); - return tubes.filter((tubeData) => tubeData.thematicId === thematicId); - }, -}); - -export { tubeDatasource }; diff --git a/api/tests/integration/infrastructure/repositories/tube-repository_test.js b/api/tests/integration/infrastructure/repositories/tube-repository_test.js index bc71889c661..128b00a1d10 100644 --- a/api/tests/integration/infrastructure/repositories/tube-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/tube-repository_test.js @@ -1,611 +1,327 @@ import * as tubeRepository from '../../../../lib/infrastructure/repositories/tube-repository.js'; -import { domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; +import { LearningContentResourceNotFound } from '../../../../src/shared/infrastructure/datasources/learning-content/LearningContentResourceNotFound.js'; +import { catchErr, databaseBuilder, domainBuilder, expect } from '../../../test-helper.js'; describe('Integration | Repository | tube-repository', function () { + const tubeData0 = { + id: 'tubeId0', + name: 'name Tube 0', + title: 'title Tube 0', + description: 'description Tube 0', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube 0', en: 'practicalTitle EN Tube 0' }, + practicalDescription_i18n: { fr: 'practicalDescription FR Tube 0', en: 'practicalDescription EN Tube 0' }, + competenceId: 'competenceId0', + thematicId: 'thematicId0', + skillIds: ['skillIdActive0'], + isMobileCompliant: true, + isTabletCompliant: true, + }; + const tubeData1 = { + id: 'tubeId1', + name: 'name Tube 1', + title: 'title Tube 1', + description: 'description Tube 1', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube 1', nl: 'practicalTitle NL Tube 1' }, + practicalDescription_i18n: { fr: 'practicalDescription FR Tube 1', en: 'practicalDescription EN Tube 1' }, + competenceId: 'competenceId1', + thematicId: 'thematicId1', + skillIds: ['skillIdPasActive1'], + isMobileCompliant: false, + isTabletCompliant: true, + }; + const tubeData2 = { + id: 'tubeId2', + name: 'name Tube 2', + title: 'title Tube 2', + description: 'description Tube 2', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube 2', nl: 'practicalTitle NL Tube 2' }, + practicalDescription_i18n: { fr: 'practicalDescription FR Tube 2', en: 'practicalDescription EN Tube 2' }, + competenceId: 'competenceId2', + thematicId: 'thematicId2', + skillIds: ['skillIdActive2', 'skillIdPasActive3'], + isMobileCompliant: false, + isTabletCompliant: true, + }; + const skillActive0 = { + id: 'skillIdActive0', + status: 'actif', + tubeId: 'tubeId0', + }; + const skillPasActive1 = { + id: 'skillIdPasActive1', + status: 'pas actif', + tubeId: 'tubeId1', + }; + const skillActive2 = { + id: 'skillIdActive2', + status: 'actif', + tubeId: 'tubeId2', + }; + const skillPasActive3 = { + id: 'skillIdPasActive3', + status: 'pas actif', + tubeId: 'tubeId2', + }; + + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildTube(tubeData0); + databaseBuilder.factory.learningContent.buildTube(tubeData1); + databaseBuilder.factory.learningContent.buildTube(tubeData2); + databaseBuilder.factory.learningContent.buildSkill(skillActive0); + databaseBuilder.factory.learningContent.buildSkill(skillPasActive1); + databaseBuilder.factory.learningContent.buildSkill(skillActive2); + databaseBuilder.factory.learningContent.buildSkill(skillPasActive3); + await databaseBuilder.commit(); + }); + describe('#get', function () { - it('should return the tube', async function () { - // given - const expectedTube = domainBuilder.buildTube({ - id: 'recTube0', - name: 'tubeName', - practicalTitle: 'translatedPracticalTitle', - practicalDescription: 'translatedPracticalDescription', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - skills: [], + context('when tube found for given id', function () { + it('should return the tube translated with default locale FR', async function () { + // when + const tube = await tubeRepository.get('tubeId0'); + + // then + expect(tube).to.deepEqualInstance( + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.fr, + practicalDescription: tubeData0.practicalDescription_i18n.fr, + skills: [], + }), + ); }); - const learningContent = { - tubes: [ - { - id: 'recTube0', - name: 'tubeName', - title: 'tubeTitle', - description: 'tubeDescription', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }, - ], - }; - await mockLearningContent(learningContent); + }); - // when - const tube = await tubeRepository.get(expectedTube.id); + context('when no tube found', function () { + it('should throw a LearningContentResourceNotFound error', async function () { + // when + const err = await catchErr(tubeRepository.get, tubeRepository)('recCoucouZouZou'); - // then - expect(tube).to.deepEqualInstance(expectedTube); + // then + expect(err).to.be.instanceOf(LearningContentResourceNotFound); + }); }); }); describe('#list', function () { - it('should return the tubes', async function () { - // given - const tube0 = domainBuilder.buildTube({ - id: 'recTube0', - name: 'tubeName0', - practicalTitle: 'translatedPracticalTitle0', - practicalDescription: 'translatedPracticalDescription0', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - skills: [], - }); - const tube1 = domainBuilder.buildTube({ - id: 'recTube1', - name: 'tubeName1', - practicalTitle: 'translatedPracticalTitle1', - practicalDescription: 'translatedPracticalDescription1', - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillPoire', 'skillPeche'], - skills: [], - }); - const learningContentTube0 = { - id: 'recTube0', - name: 'tubeName0', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle0', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription0', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }; - - const learningContentTube1 = { - id: 'recTube1', - name: 'tubeName1', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle1', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription1', - }, - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillPoire', 'skillPeche'], - }; - await mockLearningContent({ tubes: [learningContentTube0, learningContentTube1] }); - + it('should return all tubes translated by default locale FR ordered by name', async function () { // when const tubes = await tubeRepository.list(); // then - expect(tubes).to.have.lengthOf(2); - expect(tubes[0]).to.deep.equal(tube0); - expect(tubes[1]).to.deep.equal(tube1); + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.fr, + practicalDescription: tubeData0.practicalDescription_i18n.fr, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData1, + practicalTitle: tubeData1.practicalTitle_i18n.fr, + practicalDescription: tubeData1.practicalDescription_i18n.fr, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.fr, + skills: [], + }), + ]); }); }); describe('#findByNames', function () { - it('should return the tubes ordered by name', async function () { - // given - const tube0 = domainBuilder.buildTube({ - id: 'recTube0', - name: 'tubeName0', - practicalTitle: 'translatedPracticalTitle0', - practicalDescription: 'translatedPracticalDescription0', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - skills: [], - }); - - const tube1 = domainBuilder.buildTube({ - id: 'recTube1', - name: 'tubeName1', - practicalTitle: 'translatedPracticalTitle1', - practicalDescription: 'translatedPracticalDescription1', - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillPoire', 'skillPeche'], - skills: [], + context('when tubes found by names', function () { + context('when no locale provided', function () { + it('should return all tubes found translated in default locale FR given by their name', async function () { + // when + const tubes = await tubeRepository.findByNames({ + tubeNames: ['name Tube 2', 'name Tube 0', 'non existant mais on sen fiche'], + }); + + // then + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.fr, + practicalDescription: tubeData0.practicalDescription_i18n.fr, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.fr, + skills: [], + }), + ]); + }); }); - const learningContentTube0 = { - id: 'recTube0', - name: 'tubeName0', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle0', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription0', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }; - - const learningContentTube1 = { - id: 'recTube1', - name: 'tubeName1', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle1', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription1', - }, - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillPoire', 'skillPeche'], - }; - await mockLearningContent({ tubes: [learningContentTube1, learningContentTube0] }); - - // when - const tubes = await tubeRepository.findByNames({ tubeNames: ['tubeName1', 'tubeName0'] }); - - // then - expect(tubes[0]).to.deep.equal(tube0); - expect(tubes[1]).to.deep.equal(tube1); - }); - - context('when no locale is provided (using default locale)', function () { - it('should return the tubes with default locale translation', async function () { - // given - const expectedTube = domainBuilder.buildTube({ - id: 'recTube0', - name: 'tubeName', - practicalTitle: 'translatedPracticalTitle', - practicalDescription: 'translatedPracticalDescription', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - skills: [], + context('when a locale is provided', function () { + it('should return all tubes found translated in default locale FR given by their name', async function () { + // when + const tubes = await tubeRepository.findByNames({ tubeNames: ['name Tube 2', 'name Tube 0'], locale: 'en' }); + + // then + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.en, + practicalDescription: tubeData0.practicalDescription_i18n.en, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.en, + skills: [], + }), + ]); }); - const learningContent = { - tubes: [ - { - id: 'recTube0', - name: 'tubeName', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const tubes = await tubeRepository.findByNames({ tubeNames: ['tubeName'] }); - - // then - expect(tubes[0].practicalTitle).to.equal(expectedTube.practicalTitle); - expect(tubes[0].practicalDescription).to.equal(expectedTube.practicalDescription); }); }); - context('when specifying a locale', function () { - it('should return the tubes with appropriate translation', async function () { - // given - const expectedTube = domainBuilder.buildTube({ - id: 'recTube0', - name: 'tubeName', - practicalTitle: 'translatedPracticalTitleEnUs', - practicalDescription: 'translatedPracticalDescriptionEnUs', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - skills: [], - }); - const learningContent = { - tubes: [ - { - id: 'recTube0', - name: 'tubeName', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle', - en: 'translatedPracticalTitleEnUs', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription', - en: 'translatedPracticalDescriptionEnUs', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }, - ], - }; - await mockLearningContent(learningContent); - const locale = 'en'; - + context('when no tubes found for given names', function () { + it('should return an empty array', async function () { // when - const tubes = await tubeRepository.findByNames({ tubeNames: 'tubeName', locale }); + const tubes = await tubeRepository.findByNames({ tubeNames: ['name Tube 888888'] }); // then - expect(tubes[0].practicalTitle).to.equal(expectedTube.practicalTitle); - expect(tubes[0].practicalDescription).to.equal(expectedTube.practicalDescription); + expect(tubes).to.deep.equal([]); }); }); }); describe('#findByRecordIds', function () { - beforeEach(async function () { - const learningContentTube0 = { - id: 'recTube0', - name: 'tubeName0', - practicalTitle_i18n: { - fr: 'practicalTitreFR0', - en: 'practicalTitreEN0', - }, - practicalDescription_i18n: { - fr: 'practicalDescriptionFR0', - en: 'practicalDescriptionEN0', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }; - const learningContentTube1 = { - id: 'recTube1', - name: 'tubeName1', - practicalTitle_i18n: { - fr: 'practicalTitreFR1', - en: 'practicalTitreEN1', - }, - practicalDescription_i18n: { - fr: 'practicalDescriptionFR1', - en: 'practicalDescriptionEN1', - }, - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillBien'], - }; - const learningContentTube2 = { - id: 'recTube2', - name: 'tubeName2', - practicalTitle_i18n: { - fr: 'practicalTitreFR2', - en: 'practicalTitreEN2', - }, - practicalDescription_i18n: { - fr: 'practicalDescriptionFR2', - en: 'practicalDescriptionEN2', - }, - isMobileCompliant: true, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCuisse', - skillIds: ['skillPoulet'], - }; - const skills = [ - { - id: 'skillId0', - tubeId: 'recTube0', - }, - { - id: 'skillId1', - tubeId: 'recTube1', - }, - { - id: 'skillId2', - tubeId: 'recTube2', - }, - ]; - await mockLearningContent({ tubes: [learningContentTube1, learningContentTube0, learningContentTube2], skills }); - }); - - it('should return a list of tubes (locale FR - default)', async function () { - // given - const tube1 = domainBuilder.buildTube({ - id: 'recTube1', - name: 'tubeName1', - practicalTitle: 'practicalTitreFR1', - practicalDescription: 'practicalDescriptionFR1', - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillBien'], - skills: [], - }); - const tube2 = domainBuilder.buildTube({ - id: 'recTube2', - name: 'tubeName2', - practicalTitle: 'practicalTitreFR2', - practicalDescription: 'practicalDescriptionFR2', - isMobileCompliant: true, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCuisse', - skillIds: ['skillPoulet'], - skills: [], + context('when tubes found by ids', function () { + context('when no locale provided', function () { + it('should return all tubes found translated in default locale FR given by their name', async function () { + // when + const tubes = await tubeRepository.findByRecordIds(['tubeId2', 'tubeId0', 'non existant mais on sen fiche']); + + // then + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.fr, + practicalDescription: tubeData0.practicalDescription_i18n.fr, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.fr, + skills: [], + }), + ]); + }); }); - // when - const tubes = await tubeRepository.findByRecordIds(['recTube2', 'recTube1']); - - // then - expect(tubes).to.deepEqualArray([tube1, tube2]); - }); - - it('should return a list of tubes (locale EN)', async function () { - // given - const tube1 = domainBuilder.buildTube({ - id: 'recTube1', - name: 'tubeName1', - practicalTitle: 'practicalTitreEN1', - practicalDescription: 'practicalDescriptionEN1', - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillBien'], - skills: [], - }); - const tube2 = domainBuilder.buildTube({ - id: 'recTube2', - name: 'tubeName2', - practicalTitle: 'practicalTitreEN2', - practicalDescription: 'practicalDescriptionEN2', - isMobileCompliant: true, - isTabletCompliant: false, - competenceId: 'recCompetence1', - thematicId: 'thematicCuisse', - skillIds: ['skillPoulet'], - skills: [], + context('when a locale is provided', function () { + it('should return all tubes found translated in default locale FR given by their name', async function () { + // when + const tubes = await tubeRepository.findByRecordIds( + ['tubeId2', 'tubeId0', 'non existant mais on sen fiche'], + 'en', + ); + + // then + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.en, + practicalDescription: tubeData0.practicalDescription_i18n.en, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.en, + skills: [], + }), + ]); + }); }); + }); - // when - const tubes = await tubeRepository.findByRecordIds(['recTube2', 'recTube1'], 'en'); + context('when no tubes found for given ids', function () { + it('should return an empty array', async function () { + // when + const tubes = await tubeRepository.findByRecordIds(['name Tube 888888']); - // then - expect(tubes).to.deepEqualArray([tube1, tube2]); + // then + expect(tubes).to.deep.equal([]); + }); }); }); describe('#findActiveByRecordIds', function () { - it('should return a list of active tubes', async function () { - // given - const tube1 = domainBuilder.buildTube({ - id: 'recTube1', - name: 'tubeName1', - practicalTitle: 'translatedPracticalTitle1', - practicalDescription: 'translatedPracticalDescription1', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillCool'], - skills: [], + context('when active tubes found by ids', function () { + context('when no locale provided', function () { + it('should return all tubes that have at least one active skill found translated in default locale FR given by their ids', async function () { + // when + const tubes = await tubeRepository.findActiveByRecordIds([ + 'tubeId2', + 'tubeId0', + 'tubeId1', + 'non existant mais on sen fiche', + ]); + + // then + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.fr, + practicalDescription: tubeData0.practicalDescription_i18n.fr, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.fr, + skills: [], + }), + ]); + }); }); - const learningContentTube0 = { - id: 'recTube0', - name: 'tubeName0', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle0', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription0', - }, - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }; - - const learningContentTube1 = { - id: 'recTube1', - name: 'tubeName1', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle1', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription1', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillCool'], - }; - - const learningContentTube2 = { - id: 'recTube2', - name: 'tubeName2', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle2', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription2', - }, - isMobileCompliant: true, - isTabletCompliant: false, - competenceId: 'recCompetence2', - thematicId: 'thematicFruit', - skillIds: [], - }; - - const skills = [ - { - id: 'skillId0', - status: 'actif', - tubeId: 'recTube0', - }, - { - id: 'skillId1', - status: 'actif', - tubeId: 'recTube1', - }, - { - id: 'skillId2', - status: 'archivé', - tubeId: 'recTube2', - }, - ]; - await mockLearningContent({ tubes: [learningContentTube1, learningContentTube0, learningContentTube2], skills }); - - // when - const tubes = await tubeRepository.findActiveByRecordIds(['recTube1', 'recTube2']); - - // then - expect(tubes).to.have.lengthOf(1); - expect(tubes[0]).to.deep.equal(tube1); - }); - - it('should return a list of english active tubes', async function () { - // given - const tube1 = domainBuilder.buildTube({ - id: 'recTube1', - name: 'tubeName1', - practicalTitle: 'translatedPracticalTitle1EnUs', - practicalDescription: 'translatedPracticalDescription1EnUs', - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillCool'], - skills: [], + context('when a locale is provided', function () { + it('should return all tubes found translated in default locale FR given by their name', async function () { + // when + const tubes = await tubeRepository.findActiveByRecordIds( + ['tubeId2', 'tubeId0', 'tubeId1', 'non existant mais on sen fiche'], + 'en', + ); + + // then + expect(tubes).to.deepEqualArray([ + domainBuilder.buildTube({ + ...tubeData0, + practicalTitle: tubeData0.practicalTitle_i18n.en, + practicalDescription: tubeData0.practicalDescription_i18n.en, + skills: [], + }), + domainBuilder.buildTube({ + ...tubeData2, + practicalTitle: tubeData2.practicalTitle_i18n.fr, + practicalDescription: tubeData2.practicalDescription_i18n.en, + skills: [], + }), + ]); + }); }); + }); - const learningContentTube0 = { - id: 'recTube0', - name: 'tubeName0', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle0', - en: 'translatedPracticalTitle0EnUs', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription0', - en: 'translatedPracticalDescription0EnUs', - }, - isMobileCompliant: false, - isTabletCompliant: false, - competenceId: 'recCompetence0', - thematicId: 'thematicCoucou', - skillIds: ['skillSuper', 'skillGenial'], - }; - - const learningContentTube1 = { - id: 'recTube1', - name: 'tubeName1', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle1', - en: 'translatedPracticalTitle1EnUs', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription1', - en: 'translatedPracticalDescription1EnUs', - }, - isMobileCompliant: true, - isTabletCompliant: true, - competenceId: 'recCompetence1', - thematicId: 'thematicCava', - skillIds: ['skillCool'], - }; - - const learningContentTube2 = { - id: 'recTube2', - name: 'tubeName2', - practicalTitle_i18n: { - fr: 'translatedPracticalTitle2', - en: 'translatedPracticalTitle2EnUs', - }, - practicalDescription_i18n: { - fr: 'translatedPracticalDescription2', - en: 'translatedPracticalDescription2EnUs', - }, - isMobileCompliant: true, - isTabletCompliant: false, - competenceId: 'recCompetence2', - thematicId: 'thematicFruit', - skillIds: [], - }; - - const skills = [ - { - id: 'skillId0', - status: 'actif', - tubeId: 'recTube0', - }, - { - id: 'skillId1', - status: 'actif', - tubeId: 'recTube1', - }, - { - id: 'skillId2', - status: 'archivé', - tubeId: 'recTube2', - }, - ]; - await mockLearningContent({ tubes: [learningContentTube1, learningContentTube0, learningContentTube2], skills }); - - // when - const tubes = await tubeRepository.findActiveByRecordIds(['recTube1', 'recTube2'], 'en'); + context('when no active tubes found for given ids', function () { + it('should return an empty array', async function () { + // when + const tubes = await tubeRepository.findActiveByRecordIds(['tubeId1']); - // then - expect(tubes).to.have.lengthOf(1); - expect(tubes[0]).to.deep.equal(tube1); + // then + expect(tubes).to.deep.equal([]); + }); }); }); }); diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/tube-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/tube-datasource_test.js deleted file mode 100644 index 58a47364920..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/tube-datasource_test.js +++ /dev/null @@ -1,92 +0,0 @@ -import _ from 'lodash'; - -import { tubeDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | TubeDatasource', function () { - describe('#findByNames', function () { - it('should return an array of matching tube data objects', async function () { - // given - const rawTube1 = { id: 'rectTube1', name: 'FAKE_NAME_RAW_TUBE_1' }; - const rawTube2 = { id: 'rectTube2', name: 'FAKE_NAME_RAW_TUBE_2' }; - const rawTube3 = { id: 'rectTube3', name: 'FAKE_NAME_RAW_TUBE_3' }; - const rawTube4 = { id: 'rectTube4', name: 'FAKE_NAME_RAW_TUBE_4' }; - - const records = [rawTube1, rawTube2, rawTube3, rawTube4]; - await mockLearningContent({ tubes: records }); - - // when - const foundTubes = await tubeDatasource.findByNames([rawTube1.name, rawTube2.name, rawTube4.name]); - - // then - expect(foundTubes).to.be.an('array'); - expect(_.map(foundTubes, 'name')).to.deep.equal([rawTube1.name, rawTube2.name, rawTube4.name]); - }); - }); - - describe('#findByRecordIds', function () { - it('should return an array of matching tube data objects', async function () { - // given - const rawTube1 = { id: 'RECORD_ID_RAW_TUBE_1' }; - const rawTube2 = { id: 'RECORD_ID_RAW_TUBE_2' }; - const rawTube3 = { id: 'RECORD_ID_RAW_TUBE_3' }; - const rawTube4 = { id: 'RECORD_ID_RAW_TUBE_4' }; - - const records = [rawTube1, rawTube2, rawTube3, rawTube4]; - await mockLearningContent({ tubes: records }); - const expectedTubeIds = [rawTube1.id, rawTube2.id, rawTube4.id]; - - // when - const foundTubes = await tubeDatasource.findByRecordIds(expectedTubeIds); - // then - expect(foundTubes.map(({ id }) => id)).to.deep.equal(expectedTubeIds); - }); - - it('should return an empty array when there are no objects matching the ids', async function () { - // given - const rawTube1 = { id: 'RECORD_ID_RAW_TUBE_1' }; - - const records = [rawTube1]; - await mockLearningContent({ tubes: records }); - - // when - const foundTubes = await tubeDatasource.findByRecordIds(['some_other_id']); - - // then - expect(foundTubes).to.be.empty; - }); - }); - - describe('#findByThematicId', function () { - it('should return an array of matching tube data objects', async function () { - // given - const rawTube1 = { id: 'RECORD_ID_RAW_TUBE_1', thematicId: 'thematicId1' }; - const rawTube2 = { id: 'RECORD_ID_RAW_TUBE_2', thematicId: 'thematicId1' }; - const rawTube3 = { id: 'RECORD_ID_RAW_TUBE_3', thematicId: 'thematicId2' }; - const rawTube4 = { id: 'RECORD_ID_RAW_TUBE_4', thematicId: 'thematicId1' }; - - const records = [rawTube1, rawTube2, rawTube3, rawTube4]; - await mockLearningContent({ tubes: records }); - const expectedTubeIds = [rawTube1.id, rawTube2.id, rawTube4.id]; - - // when - const foundTubes = await tubeDatasource.findByThematicId('thematicId1'); - // then - expect(foundTubes.map(({ id }) => id)).to.deep.equal(expectedTubeIds); - }); - - it('should return an empty array when there are no objects matching the ids', async function () { - // given - const rawTube1 = { id: 'RECORD_ID_RAW_TUBE_1', thematicId: 'thematicId2' }; - - const records = [rawTube1]; - await mockLearningContent({ tubes: records }); - - // when - const foundTubes = await tubeDatasource.findByThematicId('thematicId1'); - - // then - expect(foundTubes).to.be.empty; - }); - }); -}); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index f340010df98..13db856addc 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -20,6 +20,7 @@ import { DatabaseBuilder } from '../db/database-builder/database-builder.js'; import { disconnect, knex } from '../db/knex-database-connection.js'; import * as frameworkRepository from '../lib/infrastructure/repositories/framework-repository.js'; import * as thematicRepository from '../lib/infrastructure/repositories/thematic-repository.js'; +import * as tubeRepository from '../lib/infrastructure/repositories/tube-repository.js'; import { PIX_ADMIN } from '../src/authorization/domain/constants.js'; import { config } from '../src/shared/config.js'; import { Membership } from '../src/shared/domain/models/index.js'; @@ -79,6 +80,7 @@ afterEach(function () { areaRepository.clearCache(); competenceRepository.clearCache(); thematicRepository.clearCache(); + tubeRepository.clearCache(); return databaseBuilder.clean(); }); From 3c9b1cba1c2a83218dd837ab9203802c0aae45ca Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 17:26:41 +0100 Subject: [PATCH 09/24] feat(api): refacto skillRepository with new cache and to use PG --- .../data/common/tooling/campaign-tooling.js | 2 +- .../data/common/tooling/learning-content.js | 4 +- .../data/common/tooling/session-tooling.js | 2 +- .../repositories/correction-repository.js | 47 +- api/src/shared/domain/models/Skill.js | 6 + .../domain/services/get-translated-text.js | 4 +- .../repositories/skill-repository.js | 190 ++-- ...-campaign-parameters-for-simulator_test.js | 51 +- .../repositories/campaign-repository_test.js | 80 +- ...tions-from-existing-target-profile_test.js | 7 +- ...ch-organizations-to-target-profile_test.js | 7 +- .../repositories/skill-repository_test.js | 822 +++++++++++++----- .../services/get-translated-text_test.js | 27 +- api/tests/test-helper.js | 2 + .../domain-builder/factory/build-skill.js | 6 + .../skillLearningContentDataObjectFixture.js | 45 - .../correction-repository_test.js | 75 +- 17 files changed, 911 insertions(+), 466 deletions(-) delete mode 100644 api/tests/tooling/fixtures/infrastructure/skillLearningContentDataObjectFixture.js diff --git a/api/db/seeds/data/common/tooling/campaign-tooling.js b/api/db/seeds/data/common/tooling/campaign-tooling.js index 5afbc237eec..993222888cb 100644 --- a/api/db/seeds/data/common/tooling/campaign-tooling.js +++ b/api/db/seeds/data/common/tooling/campaign-tooling.js @@ -426,7 +426,7 @@ async function _buildCampaignSkills({ databaseBuilder, campaignId, targetProfile for (const cappedTube of cappedTubes) { const skillsForTube = await learningContent.findActiveSkillsByTubeId(cappedTube.tubeId); - const skillsCapped = skillsForTube.filter((skill) => skill.level <= parseInt(cappedTube.level)); + const skillsCapped = skillsForTube.filter((skill) => skill.difficulty <= parseInt(cappedTube.level)); skillsCapped.forEach((skill) => { skills.push(skill); databaseBuilder.factory.buildCampaignSkill({ campaignId, skillId: skill.id }); diff --git a/api/db/seeds/data/common/tooling/learning-content.js b/api/db/seeds/data/common/tooling/learning-content.js index 90076050a2d..d717348936f 100644 --- a/api/db/seeds/data/common/tooling/learning-content.js +++ b/api/db/seeds/data/common/tooling/learning-content.js @@ -1,8 +1,8 @@ import _ from 'lodash'; -import { skillDatasource } from '../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; import * as challengeRepository from '../../../../../src/shared/infrastructure/repositories/challenge-repository.js'; import * as competenceRepository from '../../../../../src/shared/infrastructure/repositories/competence-repository.js'; +import * as skillRepository from '../../../../../src/shared/infrastructure/repositories/skill-repository.js'; import { logger } from '../../../../../src/shared/infrastructure/utils/logger.js'; let ALL_COMPETENCES, ALL_ACTIVE_SKILLS, ALL_CHALLENGES, ACTIVE_SKILLS_BY_COMPETENCE, ACTIVE_SKILLS_BY_TUBE; @@ -35,7 +35,7 @@ async function getCoreCompetences() { async function getAllActiveSkills() { if (!ALL_ACTIVE_SKILLS) { - ALL_ACTIVE_SKILLS = await skillDatasource.findActive(); + ALL_ACTIVE_SKILLS = (await skillRepository.list()).filter((skill) => skill.status === 'actif'); } return ALL_ACTIVE_SKILLS; } diff --git a/api/db/seeds/data/common/tooling/session-tooling.js b/api/db/seeds/data/common/tooling/session-tooling.js index 69688152cd3..a0c93a524a9 100644 --- a/api/db/seeds/data/common/tooling/session-tooling.js +++ b/api/db/seeds/data/common/tooling/session-tooling.js @@ -1076,7 +1076,7 @@ async function _makeCandidatesComplementaryCertificationCertifiable( for (const [areaId, { fourMostDifficultSkillsAndChallenges }] of Object.entries(complementaryProfileData)) { complementaryProfileData[areaId].fourMostDifficultSkillsAndChallenges = _.orderBy( fourMostDifficultSkillsAndChallenges, - ({ skill }) => skill.level, + ({ skill }) => skill.difficulty, ); complementaryProfileData[areaId].fourMostDifficultSkillsAndChallenges = _.takeRight( complementaryProfileData[areaId].fourMostDifficultSkillsAndChallenges, diff --git a/api/lib/infrastructure/repositories/correction-repository.js b/api/lib/infrastructure/repositories/correction-repository.js index 997f4a323b7..c6e4eff360c 100644 --- a/api/lib/infrastructure/repositories/correction-repository.js +++ b/api/lib/infrastructure/repositories/correction-repository.js @@ -1,14 +1,11 @@ import _ from 'lodash'; -// TODO modifier les dépendances avec datasource en meme temps que la PR de lecture, pour des considérations de perf import { Answer } from '../../../src/evaluation/domain/models/Answer.js'; -import { Challenge } from '../../../src/shared/domain/models/Challenge.js'; -import { Correction } from '../../../src/shared/domain/models/Correction.js'; import { Hint } from '../../../src/shared/domain/models/Hint.js'; -import { - challengeDatasource, - skillDatasource, -} from '../../../src/shared/infrastructure/datasources/learning-content/index.js'; +import { Challenge } from '../../../src/shared/domain/models/index.js'; +import { Correction } from '../../../src/shared/domain/models/index.js'; +import { challengeDatasource } from '../../../src/shared/infrastructure/datasources/learning-content/index.js'; +import * as skillRepository from '../../../src/shared/infrastructure/repositories/skill-repository.js'; const VALIDATED_HINT_STATUSES = ['Validé', 'pré-validé']; @@ -22,8 +19,8 @@ const getByChallengeId = async function ({ getCorrection, } = {}) { const challenge = await challengeDatasource.get(challengeId); - const skill = await _getSkill(challenge); - const hint = await _getHint({ skill, locale }); + const skill = await _getSkill(challenge, locale); + const hint = await _getHint(skill); const solution = fromDatasourceObject(challenge); let correctionDetails; @@ -59,32 +56,22 @@ const getByChallengeId = async function ({ }; export { getByChallengeId }; -async function _getHint({ skill, locale }) { - if (_hasValidatedHint(skill)) { - return _convertSkillToHint({ skill, locale }); +async function _getHint(skill) { + if (_hasValidatedHint(skill) && skill.hint) { + return new Hint({ + skillName: skill.name, + value: skill.hint, + }); } + return null; } -function _getSkill(challengeDataObject) { - return skillDatasource.get(challengeDataObject.skillId); +function _getSkill(challengeDataObject, locale) { + return skillRepository.get(challengeDataObject.skillId, { locale: locale?.slice(0, 2), useFallback: false }); } -function _hasValidatedHint(skillDataObject) { - return VALIDATED_HINT_STATUSES.includes(skillDataObject.hintStatus); -} - -function _convertSkillToHint({ skill, locale }) { - const matches = locale.match(/^([a-z]{2,3})-?[a-z]{0,3}$/); - const language = matches?.[1]; - const translation = skill.hint_i18n?.[language]; - if (!translation) { - return null; - } - - return new Hint({ - skillName: skill.name, - value: translation, - }); +function _hasValidatedHint(skill) { + return VALIDATED_HINT_STATUSES.includes(skill.hintStatus); } async function _getTutorials({ userId, skill, tutorialIdsProperty, locale, tutorialRepository }) { diff --git a/api/src/shared/domain/models/Skill.js b/api/src/shared/domain/models/Skill.js index a2ef891991d..61617b1e48d 100644 --- a/api/src/shared/domain/models/Skill.js +++ b/api/src/shared/domain/models/Skill.js @@ -9,6 +9,9 @@ class Skill { tubeId, version, difficulty, + status, + hintStatus, + hint, } = {}) { this.id = id; this.name = name; @@ -19,6 +22,9 @@ class Skill { this.tubeId = tubeId; this.version = version; this.difficulty = difficulty; + this.status = status; + this.hintStatus = hintStatus; + this.hint = hint; } get tubeName() { diff --git a/api/src/shared/domain/services/get-translated-text.js b/api/src/shared/domain/services/get-translated-text.js index e3680f995ef..cd9ef9e4728 100644 --- a/api/src/shared/domain/services/get-translated-text.js +++ b/api/src/shared/domain/services/get-translated-text.js @@ -2,8 +2,8 @@ import { LOCALE } from '../constants.js'; const { FRENCH_SPOKEN } = LOCALE; -function getTranslatedKey(key, locale) { - return key?.[locale] || key?.[FRENCH_SPOKEN]; +function getTranslatedKey(key, locale, useFallback = true) { + return key?.[locale] || (useFallback ? key?.[FRENCH_SPOKEN] : null); } export { getTranslatedKey }; diff --git a/api/src/shared/infrastructure/repositories/skill-repository.js b/api/src/shared/infrastructure/repositories/skill-repository.js index 28caddbc607..5286b7364bd 100644 --- a/api/src/shared/infrastructure/repositories/skill-repository.js +++ b/api/src/shared/infrastructure/repositories/skill-repository.js @@ -1,83 +1,123 @@ import { NotFoundError } from '../../domain/errors.js'; import { Skill } from '../../domain/models/Skill.js'; -import { skillDatasource } from '../datasources/learning-content/index.js'; +import { getTranslatedKey } from '../../domain/services/get-translated-text.js'; +import { LearningContentRepository } from './learning-content-repository.js'; -function _toDomain(skillData) { +const TABLE_NAME = 'learningcontent.skills'; +const ACTIVE_STATUS = 'actif'; +const ARCHIVED_STATUS = 'archivé'; +const OPERATIVE_STATUSES = [ACTIVE_STATUS, ARCHIVED_STATUS]; + +export async function get(id, { locale, useFallback } = { locale: null, useFallback: true }) { + const skillDto = await getInstance().load(id); + if (!skillDto) { + throw new NotFoundError('Erreur, acquis introuvable'); + } + return toDomain(skillDto, locale, useFallback); +} + +export async function list() { + const cacheKey = 'list()'; + const listCallback = (knex) => knex.orderBy('id'); + const skillDtos = await getInstance().find(cacheKey, listCallback); + return skillDtos.map(toDomain); +} + +export async function findActiveByTubeId(tubeId) { + const cacheKey = `findActiveByTubeId(${tubeId})`; + const findActiveByTubeIdCallback = (knex) => knex.where({ tubeId, status: ACTIVE_STATUS }).orderBy('id'); + const skillDtos = await getInstance().find(cacheKey, findActiveByTubeIdCallback); + return skillDtos.map(toDomain); +} + +export async function findOperativeByTubeId(tubeId) { + const cacheKey = `findOperativeByTubeId(${tubeId})`; + const findOperativeByTubeIdCallback = (knex) => + knex.where({ tubeId }).whereIn('status', OPERATIVE_STATUSES).orderBy('id'); + const skillDtos = await getInstance().find(cacheKey, findOperativeByTubeIdCallback); + return skillDtos.map(toDomain); +} + +export async function findActiveByCompetenceId(competenceId) { + const cacheKey = `findActiveByCompetenceId(${competenceId})`; + const findActiveByCompetenceIdCallback = (knex) => knex.where({ competenceId, status: ACTIVE_STATUS }).orderBy('id'); + const skillDtos = await getInstance().find(cacheKey, findActiveByCompetenceIdCallback); + return skillDtos.map(toDomain); +} + +export async function findOperativeByCompetenceId(competenceId) { + const cacheKey = `findOperativeByCompetenceId(${competenceId})`; + const findOperativeByCompetenceIdCallback = (knex) => + knex.where({ competenceId }).whereIn('status', OPERATIVE_STATUSES).orderBy('id'); + const skillDtos = await getInstance().find(cacheKey, findOperativeByCompetenceIdCallback); + return skillDtos.map(toDomain); +} + +export async function findOperativeByCompetenceIds(competenceIds) { + const skills = []; + for (const competenceId of competenceIds) { + const skillsForCompetence = await findOperativeByCompetenceId(competenceId); + skills.push(...skillsForCompetence); + } + return skills.sort(byId); +} + +export async function findOperativeByIds(ids) { + const skillDtos = await getInstance().loadMany(ids); + return skillDtos + .filter((skillDto) => skillDto && OPERATIVE_STATUSES.includes(skillDto.status)) + .sort(byId) + .map(toDomain); +} + +export async function findByRecordIds(ids) { + const skillDtos = await getInstance().loadMany(ids); + return skillDtos + .filter((skillDto) => skillDto) + .sort(byId) + .map(toDomain); +} + +export async function findActiveByRecordIds(ids) { + const skillDtos = await getInstance().loadMany(ids); + return skillDtos + .filter((skillDto) => skillDto && skillDto.status === ACTIVE_STATUS) + .sort(byId) + .map(toDomain); +} + +export function clearCache() { + return getInstance().clearCache(); +} + +function byId(entityA, entityB) { + return entityA.id < entityB.id ? -1 : 1; +} + +function toDomain(skillDto, locale, useFallback) { + const translatedHint = getTranslatedKey(skillDto.hint_i18n, locale, useFallback); return new Skill({ - id: skillData.id, - name: skillData.name, - pixValue: skillData.pixValue, - competenceId: skillData.competenceId, - tutorialIds: skillData.tutorialIds, - tubeId: skillData.tubeId, - version: skillData.version, - difficulty: skillData.level, - learningMoreTutorialIds: skillData.learningMoreTutorialIds, + id: skillDto.id, + name: skillDto.name, + pixValue: skillDto.pixValue, + competenceId: skillDto.competenceId, + tutorialIds: skillDto.tutorialIds ? [...skillDto.tutorialIds] : null, + tubeId: skillDto.tubeId, + version: skillDto.version, + difficulty: skillDto.level, + learningMoreTutorialIds: skillDto.learningMoreTutorialIds ? [...skillDto.learningMoreTutorialIds] : null, + status: skillDto.status, + hintStatus: skillDto.hintStatus, + hint: translatedHint, }); } -const get = async function (id) { - try { - return _toDomain(await skillDatasource.get(id)); - } catch (e) { - throw new NotFoundError('Erreur, compétence introuvable'); +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); } -}; - -const list = async function () { - const skillDatas = await skillDatasource.list(); - return skillDatas.map(_toDomain); -}; - -const findActiveByTubeId = async function (tubeId) { - const skillDatas = await skillDatasource.findActiveByTubeId(tubeId); - return skillDatas.map(_toDomain); -}; - -const findOperativeByTubeId = async function (tubeId) { - const skillDatas = await skillDatasource.findOperativeByTubeId(tubeId); - return skillDatas.map(_toDomain); -}; - -const findActiveByCompetenceId = async function (competenceId) { - const skillDatas = await skillDatasource.findActiveByCompetenceId(competenceId); - return skillDatas.map(_toDomain); -}; - -const findOperativeByCompetenceId = async function (competenceId) { - const skillDatas = await skillDatasource.findOperativeByCompetenceId(competenceId); - return skillDatas.map(_toDomain); -}; - -const findOperativeByCompetenceIds = async function (competenceIds) { - const skillDatas = await skillDatasource.findOperativeByCompetenceIds(competenceIds); - return skillDatas.map(_toDomain); -}; - -const findOperativeByIds = async function (skillIds) { - const skillDatas = await skillDatasource.findOperativeByRecordIds(skillIds); - return skillDatas.map(_toDomain); -}; - -const findByRecordIds = async function (skillIds) { - const skillDatas = await skillDatasource.findByRecordIds(skillIds); - return skillDatas.map(_toDomain); -}; - -const findActiveByRecordIds = async function (skillIds) { - const skillDatas = await skillDatasource.findActiveByRecordIds(skillIds); - return skillDatas.map(_toDomain); -}; - -export { - findActiveByCompetenceId, - findActiveByRecordIds, - findActiveByTubeId, - findByRecordIds, - findOperativeByCompetenceId, - findOperativeByCompetenceIds, - findOperativeByIds, - findOperativeByTubeId, - get, - list, -}; + return instance; +} diff --git a/api/tests/evaluation/integration/domain/usecases/get-campaign-parameters-for-simulator_test.js b/api/tests/evaluation/integration/domain/usecases/get-campaign-parameters-for-simulator_test.js index c1d173b3836..78714ae808c 100644 --- a/api/tests/evaluation/integration/domain/usecases/get-campaign-parameters-for-simulator_test.js +++ b/api/tests/evaluation/integration/domain/usecases/get-campaign-parameters-for-simulator_test.js @@ -1,12 +1,6 @@ import { evaluationUsecases } from '../../../../../src/evaluation/domain/usecases/index.js'; import { NotFoundError } from '../../../../../src/shared/domain/errors.js'; -import { databaseBuilder, domainBuilder, expect, learningContentBuilder } from '../../../../test-helper.js'; - -function buildLearningContent() { - const learningContent = domainBuilder.buildCampaignLearningContent.withSimpleContent(); - const learningContentObjects = learningContentBuilder([learningContent]); - databaseBuilder.factory.learningContent.build(learningContentObjects); -} +import { databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Domain | UseCases | get-campaign-parameters-for-simulator', function () { describe('when the campaign does not exist', function () { @@ -24,11 +18,25 @@ describe('Integration | Domain | UseCases | get-campaign-parameters-for-simulato it('should return skills and challenges', async function () { // given const campaignId = 100000; - + const skillData0 = { + id: 'skillId0Perime', + status: 'périmé', + }; + const skillData1 = { + id: 'skillId1Archive', + status: 'archivé', + }; + const skillData2 = { + id: 'skillId2Actif', + status: 'actif', + }; + databaseBuilder.factory.learningContent.buildSkill(skillData0); + const skill1DB = databaseBuilder.factory.learningContent.buildSkill(skillData1); + const skill2DB = databaseBuilder.factory.learningContent.buildSkill(skillData2); databaseBuilder.factory.buildCampaign({ id: campaignId }); - databaseBuilder.factory.buildCampaignSkill({ campaignId, skillId: 'skillId' }); - buildLearningContent(); - + databaseBuilder.factory.buildCampaignSkill({ campaignId, skillId: skillData0.id }); + databaseBuilder.factory.buildCampaignSkill({ campaignId, skillId: skillData1.id }); + databaseBuilder.factory.buildCampaignSkill({ campaignId, skillId: skillData2.id }); await databaseBuilder.commit(); // when @@ -40,17 +48,16 @@ describe('Integration | Domain | UseCases | get-campaign-parameters-for-simulato // then expect(result).to.deep.equal({ skills: [ - { - id: 'skillId', - name: '@sau6', - pixValue: 3, - competenceId: 'competenceId', - tutorialIds: [], - learningMoreTutorialIds: [], - tubeId: 'tubeId', - version: 1, - difficulty: undefined, - }, + domainBuilder.buildSkill({ + ...skill1DB, + difficulty: skill1DB.level, + hint: skill1DB.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skill2DB, + difficulty: skill2DB.level, + hint: skill2DB.hint_i18n.fr, + }), ], challenges: [], }); diff --git a/api/tests/integration/infrastructure/repositories/campaign-repository_test.js b/api/tests/integration/infrastructure/repositories/campaign-repository_test.js index 8194bf76ac1..52ab8ed1804 100644 --- a/api/tests/integration/infrastructure/repositories/campaign-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/campaign-repository_test.js @@ -5,16 +5,7 @@ import { CampaignExternalIdTypes } from '../../../../src/prescription/shared/dom import { CAMPAIGN_FEATURES } from '../../../../src/shared/domain/constants.js'; import { NotFoundError } from '../../../../src/shared/domain/errors.js'; import { Campaign } from '../../../../src/shared/domain/models/Campaign.js'; -import { databaseBuilder, domainBuilder, expect, mockLearningContent } from '../../../test-helper.js'; -import { - buildArea, - buildCompetence, - buildFramework, - buildSkill, - buildThematic, - buildTube, -} from '../../../tooling/domain-builder/factory/index.js'; -import { buildLearningContent } from '../../../tooling/learning-content-builder/index.js'; +import { databaseBuilder, domainBuilder, expect } from '../../../test-helper.js'; describe('Integration | Repository | Campaign', function () { describe('#areKnowledgeElementsResettable', function () { @@ -76,57 +67,62 @@ describe('Integration | Repository | Campaign', function () { describe('#findAllSkills', function () { it('should return the skills for the campaign', async function () { // given - const framework = buildFramework({ id: 'frameworkId', name: 'someFramework' }); - const competenceId = 'competenceId'; - const skill1 = { + databaseBuilder.factory.learningContent.buildFramework({ id: 'frameworkId', name: 'someFramework' }); + databaseBuilder.factory.learningContent.buildArea({ id: 'areaId', frameworkId: 'frameworkId' }); + databaseBuilder.factory.learningContent.buildCompetence({ id: 'competenceId', areaId: 'areaId' }); + databaseBuilder.factory.learningContent.buildThematic({ + id: 'thematicId', + competenceId: 'competenceId', + tubeIds: ['tubeId1', 'tubeId2', 'tubeId3'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'tubeId1', + competenceId: 'competenceId', + skillIds: ['recSK123'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'tubeId2', + competenceId: 'competenceId', + skillIds: ['recSK456'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'tubeId3', + competenceId: 'competenceId', + skillIds: ['recSK789'], + }); + const skill1DB = databaseBuilder.factory.learningContent.buildSkill({ id: 'recSK123', name: '@sau3', pixValue: 3, - competenceId, + competenceId: 'competenceId', tutorialIds: [], learningMoreTutorialIds: [], tubeId: 'tubeId1', version: 1, level: 3, - }; - const skill2 = { + }); + const skill2DB = databaseBuilder.factory.learningContent.buildSkill({ id: 'recSK456', name: '@sau4', pixValue: 3, - competenceId, + competenceId: 'competenceId', tutorialIds: [], learningMoreTutorialIds: [], tubeId: 'tubeId2', version: 1, level: 4, - }; - const skill3 = { + }); + databaseBuilder.factory.learningContent.buildSkill({ id: 'recSK789', name: '@sau7', pixValue: 3, - competenceId, + competenceId: 'competenceId', tutorialIds: [], learningMoreTutorialIds: [], tubeId: 'tubeId3', version: 1, level: 7, - }; - const tube1 = buildTube({ id: 'tubeId1', competenceId, skills: [skill1] }); - const tube2 = buildTube({ id: 'tubeId2', competenceId, skills: [skill2] }); - const tube3 = buildTube({ id: 'tubeId3', competenceId, skills: [skill3] }); - const area = buildArea({ id: 'areaId', frameworkId: framework.id }); - const competence = buildCompetence({ id: 'competenceId', area, tubes: [tube1, tube2, tube3] }); - const thematic = buildThematic({ - id: 'thematicId', - competenceId: 'competenceId', - tubeIds: ['tubeId1', 'tubeId2', 'tubeId3'], }); - competence.thematics = [thematic]; - area.competences = [competence]; - framework.areas = [area]; - const learningContent = buildLearningContent([framework]); - await mockLearningContent(learningContent); - const targetProfileId = databaseBuilder.factory.buildTargetProfile().id; databaseBuilder.factory.buildTargetProfileTube({ targetProfileId, tubeId: 'tubeId1' }); databaseBuilder.factory.buildTargetProfileTube({ targetProfileId, tubeId: 'tubeId2' }); @@ -145,8 +141,16 @@ describe('Integration | Repository | Campaign', function () { // Then expect(skills).to.have.lengthOf(2); - const expectedSkill1 = buildSkill({ ...skill1, difficulty: skill1.level }); - const expectedSkill2 = buildSkill({ ...skill2, difficulty: skill2.level }); + const expectedSkill1 = domainBuilder.buildSkill({ + ...skill1DB, + difficulty: skill1DB.level, + hint: skill1DB.hint_i18n.fr, + }); + const expectedSkill2 = domainBuilder.buildSkill({ + ...skill2DB, + difficulty: skill2DB.level, + hint: skill2DB.hint_i18n.fr, + }); expect(skills).to.have.deep.members([expectedSkill1, expectedSkill2]); }); }); diff --git a/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-from-existing-target-profile_test.js b/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-from-existing-target-profile_test.js index 86d9d003104..ddafc815d34 100644 --- a/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-from-existing-target-profile_test.js +++ b/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-from-existing-target-profile_test.js @@ -2,14 +2,9 @@ import * as targetProfileRepository from '../../../../../../lib/infrastructure/r import { attachOrganizationsFromExistingTargetProfile } from '../../../../../../src/prescription/target-profile/domain/usecases/attach-organizations-from-existing-target-profile.js'; import * as organizationsToAttachToTargetProfileRepository from '../../../../../../src/prescription/target-profile/infrastructure/repositories/organizations-to-attach-to-target-profile-repository.js'; import { NoOrganizationToAttach, NotFoundError } from '../../../../../../src/shared/domain/errors.js'; -import { skillDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/skill-datasource.js'; -import { catchErr, databaseBuilder, expect, knex, sinon } from '../../../../../test-helper.js'; +import { catchErr, databaseBuilder, expect, knex } from '../../../../../test-helper.js'; describe('Integration | UseCase | attach-organizations-from-existing-target-profile', function () { - beforeEach(function () { - sinon.stub(skillDatasource, 'findOperativeByRecordIds').resolves([]); - }); - describe('#attachOrganizationsFromExistingTargetProfile', function () { it('attaches organizations to target profile with given existing target profile', async function () { const existingTargetProfileId = databaseBuilder.factory.buildTargetProfile().id; diff --git a/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-to-target-profile_test.js b/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-to-target-profile_test.js index 0a5105889ab..5b0c0bb0193 100644 --- a/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-to-target-profile_test.js +++ b/api/tests/prescription/target-profile/integration/domain/usecases/attach-organizations-to-target-profile_test.js @@ -1,13 +1,8 @@ import { attachOrganizationsToTargetProfile } from '../../../../../../src/prescription/target-profile/domain/usecases/attach-organizations-to-target-profile.js'; import * as organizationsToAttachToTargetProfileRepository from '../../../../../../src/prescription/target-profile/infrastructure/repositories/organizations-to-attach-to-target-profile-repository.js'; -import { skillDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/skill-datasource.js'; -import { databaseBuilder, expect, knex, sinon } from '../../../../../test-helper.js'; +import { databaseBuilder, expect, knex } from '../../../../../test-helper.js'; describe('Integration | UseCase | attach-organizations-to-target-profile', function () { - beforeEach(function () { - sinon.stub(skillDatasource, 'findOperativeByRecordIds').resolves([]); - }); - describe('#attachOrganizationsToTargetProfile', function () { it('attaches organization to target profile', async function () { const targetProfile = databaseBuilder.factory.buildTargetProfile(); diff --git a/api/tests/shared/integration/infrastructure/repositories/skill-repository_test.js b/api/tests/shared/integration/infrastructure/repositories/skill-repository_test.js index bf0b316c942..5b3a0d4d9be 100644 --- a/api/tests/shared/integration/infrastructure/repositories/skill-repository_test.js +++ b/api/tests/shared/integration/infrastructure/repositories/skill-repository_test.js @@ -1,263 +1,691 @@ import { NotFoundError } from '../../../../../src/shared/domain/errors.js'; -import { Skill } from '../../../../../src/shared/domain/models/Skill.js'; import * as skillRepository from '../../../../../src/shared/infrastructure/repositories/skill-repository.js'; -import { catchErr, domainBuilder, expect, mockLearningContent } from '../../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Repository | skill-repository', function () { + const skillData00_tubeAcompetenceA_actif = { + id: 'skillId00', + name: 'name Acquis 0', + status: 'actif', + pixValue: 2.9, + version: 5, + level: 2, + hintStatus: 'hintStatus Acquis 0', + competenceId: 'competenceIdA', + tubeId: 'tubeIdA', + tutorialIds: ['tutorialIdA'], + learningMoreTutorialIds: [], + hint_i18n: { fr: 'hint FR skillId00', en: 'hint EN skillId00' }, + }; + const skillData01_tubeAcompetenceA_archive = { + id: 'skillId01', + name: 'name Acquis 1', + status: 'archivé', + pixValue: 4.2, + version: 8, + level: 3, + hintStatus: 'hintStatus Acquis 1', + competenceId: 'competenceIdA', + tubeId: 'tubeIdA', + tutorialIds: ['tutorialIdA'], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId01', en: 'hint EN skillId01' }, + }; + const skillData02_tubeAcompetenceA_perime = { + id: 'skillId02', + name: 'name Acquis 2', + status: 'périmé', + pixValue: 5.1, + version: 9, + level: 1, + hintStatus: 'hintStatus Acquis 2', + competenceId: 'competenceIdA', + tubeId: 'tubeIdA', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId02', en: 'hint EN skillId02' }, + }; + const skillData03_tubeBcompetenceA_actif = { + id: 'skillId03', + name: 'name Acquis 3', + status: 'actif', + pixValue: 1.2, + version: 11, + level: 5, + hintStatus: 'hintStatus Acquis 3', + competenceId: 'competenceIdA', + tubeId: 'tubeIdB', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId03', en: 'hint EN skillId03' }, + }; + const skillData04_tubeBcompetenceA_archive = { + id: 'skillId04', + name: 'name Acquis 4', + status: 'archivé', + pixValue: 1.3, + version: 5, + level: 7, + hintStatus: 'hintStatus Acquis 4', + competenceId: 'competenceIdA', + tubeId: 'tubeIdB', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId04', en: 'hint EN skillId04' }, + }; + const skillData05_tubeBcompetenceA_perime = { + id: 'skillId05', + name: 'name Acquis 5', + status: 'périmé', + pixValue: 7, + version: 25, + level: 3, + hintStatus: 'hintStatus Acquis 5', + competenceId: 'competenceIdA', + tubeId: 'tubeIdB', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId05', en: 'hint EN skillId05' }, + }; + const skillData06_tubeCcompetenceB_actif = { + id: 'skillId06', + name: 'name Acquis 6', + status: 'actif', + pixValue: 4, + version: 5, + level: 6, + hintStatus: 'hintStatus Acquis 6', + competenceId: 'competenceIdB', + tubeId: 'tubeIdC', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId06', en: 'hint EN skillId06' }, + }; + const skillData07_tubeCcompetenceB_archive = { + id: 'skillId07', + name: 'name Acquis 7', + status: 'archivé', + pixValue: 4.2, + version: 4, + level: 2, + hintStatus: 'hintStatus Acquis 7', + competenceId: 'competenceIdB', + tubeId: 'tubeIdC', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId07', en: 'hint EN skillId07' }, + }; + const skillData08_tubeCcompetenceB_perime = { + id: 'skillId08', + name: 'name Acquis 8', + status: 'périmé', + pixValue: 5.2, + version: 1, + level: 4, + hintStatus: 'hintStatus Acquis 8', + competenceId: 'competenceIdB', + tubeId: 'tubeIdC', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId08', en: 'hint EN skillId08' }, + }; + const skillData09_tubeDcompetenceB_actif = { + id: 'skillId09', + name: 'name Acquis 9', + status: 'actif', + pixValue: 60, + version: 10, + level: 1, + hintStatus: 'hintStatus Acquis 9', + competenceId: 'competenceIdB', + tubeId: 'tubeIdD', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId09', en: 'hint EN skillId09' }, + }; + const skillData10_tubeDcompetenceB_archive = { + id: 'skillId10', + name: 'name Acquis 10', + status: 'archivé', + pixValue: 1.1, + version: 25, + level: 2, + hintStatus: 'hintStatus Acquis 10', + competenceId: 'competenceIdB', + tubeId: 'tubeIdD', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId10', en: 'hint EN skillId10' }, + }; + const skillData11_tubeDcompetenceB_perime = { + id: 'skillId11', + name: 'name Acquis 11', + status: 'périmé', + pixValue: 2.32, + version: 11, + level: 5, + hintStatus: 'hintStatus Acquis 11', + competenceId: 'competenceIdB', + tubeId: 'tubeIdD', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId11', en: 'hint EN skillId11' }, + }; + const skillData12_tubeEcompetenceC_perime = { + id: 'skillId12', + name: 'name Acquis 12', + status: 'périmé', + pixValue: 4.4, + version: 12, + level: 4, + hintStatus: 'hintStatus Acquis 12', + competenceId: 'competenceIdC', + tubeId: 'tubeIdD', + tutorialIds: [], + learningMoreTutorialIds: ['tutorialIdB'], + hint_i18n: { fr: 'hint FR skillId12', en: 'hint EN skillId12' }, + }; + + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildSkill(skillData06_tubeCcompetenceB_actif); + databaseBuilder.factory.learningContent.buildSkill(skillData03_tubeBcompetenceA_actif); + databaseBuilder.factory.learningContent.buildSkill(skillData10_tubeDcompetenceB_archive); + databaseBuilder.factory.learningContent.buildSkill(skillData00_tubeAcompetenceA_actif); + databaseBuilder.factory.learningContent.buildSkill(skillData04_tubeBcompetenceA_archive); + databaseBuilder.factory.learningContent.buildSkill(skillData01_tubeAcompetenceA_archive); + databaseBuilder.factory.learningContent.buildSkill(skillData02_tubeAcompetenceA_perime); + databaseBuilder.factory.learningContent.buildSkill(skillData11_tubeDcompetenceB_perime); + databaseBuilder.factory.learningContent.buildSkill(skillData08_tubeCcompetenceB_perime); + databaseBuilder.factory.learningContent.buildSkill(skillData05_tubeBcompetenceA_perime); + databaseBuilder.factory.learningContent.buildSkill(skillData12_tubeEcompetenceC_perime); + databaseBuilder.factory.learningContent.buildSkill(skillData07_tubeCcompetenceB_archive); + databaseBuilder.factory.learningContent.buildSkill(skillData09_tubeDcompetenceB_actif); + await databaseBuilder.commit(); + }); + describe('#list', function () { - it('should resolve all skills', async function () { - // given - const competenceId = 'recCompetenceId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', competenceId }); - const archivedSkill = domainBuilder.buildSkill({ id: 'archivedSkill', competenceId }); - const activeSkill_otherCompetence = domainBuilder.buildSkill({ - id: 'activeSkill_otherCompetence', - competenceId: 'recAnotherCompetence', - }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...archivedSkill, status: 'archivé', level: archivedSkill.difficulty }, - { ...activeSkill_otherCompetence, status: 'actif', level: activeSkill_otherCompetence.difficulty }, - ], - }; - await mockLearningContent(learningContent); + it('should return all skills', async function () { // when const skills = await skillRepository.list(); // then - expect(skills).to.deep.equal([activeSkill, archivedSkill, activeSkill_otherCompetence]); + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData00_tubeAcompetenceA_actif, + difficulty: skillData00_tubeAcompetenceA_actif.level, + hint: skillData00_tubeAcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData01_tubeAcompetenceA_archive, + difficulty: skillData01_tubeAcompetenceA_archive.level, + hint: skillData01_tubeAcompetenceA_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData02_tubeAcompetenceA_perime, + difficulty: skillData02_tubeAcompetenceA_perime.level, + hint: skillData02_tubeAcompetenceA_perime.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData04_tubeBcompetenceA_archive, + difficulty: skillData04_tubeBcompetenceA_archive.level, + hint: skillData04_tubeBcompetenceA_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData05_tubeBcompetenceA_perime, + difficulty: skillData05_tubeBcompetenceA_perime.level, + hint: skillData05_tubeBcompetenceA_perime.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData06_tubeCcompetenceB_actif, + difficulty: skillData06_tubeCcompetenceB_actif.level, + hint: skillData06_tubeCcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData07_tubeCcompetenceB_archive, + difficulty: skillData07_tubeCcompetenceB_archive.level, + hint: skillData07_tubeCcompetenceB_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData08_tubeCcompetenceB_perime, + difficulty: skillData08_tubeCcompetenceB_perime.level, + hint: skillData08_tubeCcompetenceB_perime.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData09_tubeDcompetenceB_actif, + difficulty: skillData09_tubeDcompetenceB_actif.level, + hint: skillData09_tubeDcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData10_tubeDcompetenceB_archive, + difficulty: skillData10_tubeDcompetenceB_archive.level, + hint: skillData10_tubeDcompetenceB_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData11_tubeDcompetenceB_perime, + difficulty: skillData11_tubeDcompetenceB_perime.level, + hint: skillData11_tubeDcompetenceB_perime.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData12_tubeEcompetenceC_perime, + difficulty: skillData12_tubeEcompetenceC_perime.level, + hint: skillData12_tubeEcompetenceC_perime.hint_i18n.fr, + }), + ]); }); }); describe('#findActiveByCompetenceId', function () { - it('should return all skills in the given competence', async function () { - // given - const competenceId = 'recCompetenceId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', competenceId }); - const nonActiveSkill = domainBuilder.buildSkill({ id: 'nonActiveSkill', competenceId }); - const activeSkill_otherCompetence = domainBuilder.buildSkill({ - id: 'activeSkill_otherCompetence', - competenceId: 'recAnotherCompetence', + context('when no active skills for given competence id', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findActiveByCompetenceId('competenceIdC'); + + // then + expect(skills).to.deep.equal([]); }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...nonActiveSkill, status: 'archivé', level: nonActiveSkill.difficulty }, - { ...activeSkill_otherCompetence, status: 'actif', level: activeSkill_otherCompetence.difficulty }, - ], - }; - await mockLearningContent(learningContent); - // when - const skills = await skillRepository.findActiveByCompetenceId(competenceId); + }); - // then - expect(skills).to.have.lengthOf(1); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills[0]).to.be.deep.equal(activeSkill); + context('when active skills for given competence id', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findActiveByCompetenceId('competenceIdB'); + + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData06_tubeCcompetenceB_actif, + difficulty: skillData06_tubeCcompetenceB_actif.level, + hint: skillData06_tubeCcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData09_tubeDcompetenceB_actif, + difficulty: skillData09_tubeDcompetenceB_actif.level, + hint: skillData09_tubeDcompetenceB_actif.hint_i18n.fr, + }), + ]); + }); }); }); describe('#findOperativeByCompetenceId', function () { - it('should resolve all skills for one competence', async function () { - // given - const competenceId = 'recCompetenceId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', competenceId }); - const archivedSkill = domainBuilder.buildSkill({ id: 'archivedSkill', competenceId }); - const nonOperativeSkill = domainBuilder.buildSkill({ id: 'nonOperativeSkill', competenceId }); - const activeSkill_otherCompetence = domainBuilder.buildSkill({ - id: 'activeSkill_otherCompetence', - competenceId: 'recAnotherCompetence', + context('when no operative skills for given competence id', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findOperativeByCompetenceId('competenceIdC'); + + // then + expect(skills).to.deep.equal([]); }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...archivedSkill, status: 'archivé', level: archivedSkill.difficulty }, - { ...nonOperativeSkill, status: 'BLABLA', level: nonOperativeSkill.difficulty }, - { ...activeSkill_otherCompetence, status: 'actif', level: activeSkill_otherCompetence.difficulty }, - ], - }; - await mockLearningContent(learningContent); + }); - // when - const skills = await skillRepository.findOperativeByCompetenceId(competenceId); + context('when operative skills for given competence id', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findOperativeByCompetenceId('competenceIdB'); - // then - expect(skills).to.have.lengthOf(2); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills).to.deep.include.members([activeSkill, archivedSkill]); + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData06_tubeCcompetenceB_actif, + difficulty: skillData06_tubeCcompetenceB_actif.level, + hint: skillData06_tubeCcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData07_tubeCcompetenceB_archive, + difficulty: skillData07_tubeCcompetenceB_archive.level, + hint: skillData07_tubeCcompetenceB_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData09_tubeDcompetenceB_actif, + difficulty: skillData09_tubeDcompetenceB_actif.level, + hint: skillData09_tubeDcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData10_tubeDcompetenceB_archive, + difficulty: skillData10_tubeDcompetenceB_archive.level, + hint: skillData10_tubeDcompetenceB_archive.hint_i18n.fr, + }), + ]); + }); }); }); describe('#findOperativeByCompetenceIds', function () { - it('should resolve all skills for all competences', async function () { - // given - const competenceId1 = 'recCompetenceId'; - const competenceId2 = 'recCompetenceId'; - const activeSkill1 = domainBuilder.buildSkill({ id: 'activeSkill1', competenceId: competenceId1 }); - const activeSkill2 = domainBuilder.buildSkill({ id: 'activeSkill2', competenceId: competenceId2 }); - const archivedSkill = domainBuilder.buildSkill({ id: 'archivedSkill', competenceId: competenceId1 }); - const nonOperativeSkill = domainBuilder.buildSkill({ id: 'nonOperativeSkill', competenceId: competenceId1 }); - const activeSkill_otherCompetence = domainBuilder.buildSkill({ - id: 'activeSkill_otherCompetence', - competenceId: 'recAnotherCompetence', + context('when some operative skills find for competence ids', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findOperativeByCompetenceIds([ + 'competenceIdA', + 'competenceIdB', + 'competenceInconnue', + ]); + + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData00_tubeAcompetenceA_actif, + difficulty: skillData00_tubeAcompetenceA_actif.level, + hint: skillData00_tubeAcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData01_tubeAcompetenceA_archive, + difficulty: skillData01_tubeAcompetenceA_archive.level, + hint: skillData01_tubeAcompetenceA_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData04_tubeBcompetenceA_archive, + difficulty: skillData04_tubeBcompetenceA_archive.level, + hint: skillData04_tubeBcompetenceA_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData06_tubeCcompetenceB_actif, + difficulty: skillData06_tubeCcompetenceB_actif.level, + hint: skillData06_tubeCcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData07_tubeCcompetenceB_archive, + difficulty: skillData07_tubeCcompetenceB_archive.level, + hint: skillData07_tubeCcompetenceB_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData09_tubeDcompetenceB_actif, + difficulty: skillData09_tubeDcompetenceB_actif.level, + hint: skillData09_tubeDcompetenceB_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData10_tubeDcompetenceB_archive, + difficulty: skillData10_tubeDcompetenceB_archive.level, + hint: skillData10_tubeDcompetenceB_archive.hint_i18n.fr, + }), + ]); }); - const learningContent = { - skills: [ - { ...activeSkill1, status: 'actif', level: activeSkill1.difficulty }, - { ...activeSkill2, status: 'actif', level: activeSkill2.difficulty }, - { ...archivedSkill, status: 'archivé', level: archivedSkill.difficulty }, - { ...nonOperativeSkill, status: 'BLABLA', level: nonOperativeSkill.difficulty }, - { ...activeSkill_otherCompetence, status: 'actif', level: activeSkill_otherCompetence.difficulty }, - ], - }; - await mockLearningContent(learningContent); + }); - // when - const skills = await skillRepository.findOperativeByCompetenceIds([competenceId1, competenceId2]); + context('when no operative skills find for competence ids', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findOperativeByCompetenceIds(['competenceIdC', 'competenceInconnue']); - // then - expect(skills).to.have.lengthOf(3); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills).to.deep.include.members([activeSkill1, activeSkill2, archivedSkill]); + // then + expect(skills).to.deep.equal([]); + }); }); }); describe('#findActiveByTubeId', function () { - it('should return all active skills in the given tube', async function () { - // given - const tubeId = 'recTubeId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', tubeId }); - const nonActiveSkill = domainBuilder.buildSkill({ id: 'nonActiveSkill', tubeId }); - const activeSkill_otherTube = domainBuilder.buildSkill({ id: 'activeSkill_otherTube', tubeId: 'recAnotherTube' }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...nonActiveSkill, status: 'archivé', level: nonActiveSkill.difficulty }, - { ...activeSkill_otherTube, status: 'actif', level: activeSkill_otherTube.difficulty }, - ], - }; - await mockLearningContent(learningContent); + context('when no active skills for given tube id', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findActiveByTubeId('tubeD'); - // when - const skills = await skillRepository.findActiveByTubeId(tubeId); + // then + expect(skills).to.deep.equal([]); + }); + }); - // then - expect(skills).to.have.lengthOf(1); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills[0]).to.be.deep.equal(activeSkill); + context('when active skills for given tube id', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findActiveByTubeId('tubeIdB'); + + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + ]); + }); }); }); describe('#findOperativeByTubeId', function () { - it('should resolve all operative skills for one tube', async function () { - // given - const tubeId = 'recTubeId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', tubeId }); - const archivedSkill = domainBuilder.buildSkill({ id: 'archivedSkill', tubeId }); - const nonOperativeSkill = domainBuilder.buildSkill({ id: 'nonOperativeSkill', tubeId }); - const activeSkill_otherTube = domainBuilder.buildSkill({ id: 'activeSkill_otherTube', tubeId: 'recAnotherTube' }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...archivedSkill, status: 'archivé', level: archivedSkill.difficulty }, - { ...nonOperativeSkill, status: 'BLABLA', level: nonOperativeSkill.difficulty }, - { ...activeSkill_otherTube, status: 'actif', level: activeSkill_otherTube.difficulty }, - ], - }; - await mockLearningContent(learningContent); + context('when no operative skills for given tube id', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findOperativeByTubeId('tubeD'); - // when - const skills = await skillRepository.findOperativeByTubeId(tubeId); + // then + expect(skills).to.deep.equal([]); + }); + }); - // then - expect(skills).to.have.lengthOf(2); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills).to.deep.include.members([activeSkill, archivedSkill]); + context('when operative skills for given tube id', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findOperativeByTubeId('tubeIdB'); + + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData04_tubeBcompetenceA_archive, + difficulty: skillData04_tubeBcompetenceA_archive.level, + hint: skillData04_tubeBcompetenceA_archive.hint_i18n.fr, + }), + ]); + }); }); }); describe('#findOperativeByIds', function () { - it('should resolve operative skills passed by ids', async function () { - // given - const competenceId = 'recCompetenceId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', competenceId }); - const archivedSkill = domainBuilder.buildSkill({ id: 'archivedSkill', competenceId }); - const nonOperativeSkill = domainBuilder.buildSkill({ id: 'nonOperativeSkill', competenceId }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...archivedSkill, status: 'archivé', level: archivedSkill.difficulty }, - { ...nonOperativeSkill, status: 'BLABLA', level: nonOperativeSkill.difficulty }, - ], - }; - await mockLearningContent(learningContent); - // when - const skills = await skillRepository.findOperativeByIds([activeSkill.id, archivedSkill.id, nonOperativeSkill.id]); + context('when no operative skills for given ids', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findOperativeByIds(['skillId02', 'skillCoucou']); - // then - expect(skills).to.have.lengthOf(2); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills).to.deep.include.members([activeSkill, archivedSkill]); + // then + expect(skills).to.deep.equal([]); + }); + }); + + context('when operative skills for given ids', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findOperativeByIds([ + 'skillId02', + 'skillId03', + 'skillId01', + 'skillIdCoucou', + ]); + + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData01_tubeAcompetenceA_archive, + difficulty: skillData01_tubeAcompetenceA_archive.level, + hint: skillData01_tubeAcompetenceA_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + ]); + }); }); }); describe('#get', function () { - let skill; - - beforeEach(async function () { - skill = domainBuilder.buildSkill(); - const learningContent = { - skills: [{ ...skill, level: skill.difficulty }], - }; - await mockLearningContent(learningContent); + context('when no skill for given id', function () { + it('should throw a NotFoundError', async function () { + // when + const err = await catchErr(skillRepository.get, skillRepository)('skillCoucou'); + + // then + expect(err).to.be.instanceOf(NotFoundError); + expect(err.message).to.equal('Erreur, acquis introuvable'); + }); }); - it('should return a skill by id', async function () { - // when - const actualSkill = await skillRepository.get(skill.id); + context('when skill for given id', function () { + context('when locale provided', function () { + context('when no translations for provided locale', function () { + context('when no fallback asked', function () { + it('should return skill with null translations', async function () { + // when + const skills = await skillRepository.get('skillId03', { locale: 'nl', useFallback: false }); - // then - expect(actualSkill).to.deep.equal(skill); + // then + expect(skills).to.deepEqualInstance( + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: null, + }), + ); + }); + }); + context('when fallback asked', function () { + it('should return skill with fallback default locale FR', async function () { + // when + const skills = await skillRepository.get('skillId03', { locale: 'nl', useFallback: true }); + + // then + expect(skills).to.deepEqualInstance( + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + ); + }); + }); + }); + context('when translation exist for provided locale', function () { + it('should return skill translated', async function () { + // when + const skills = await skillRepository.get('skillId03', { locale: 'en', useFallback: false }); + + // then + expect(skills).to.deepEqualInstance( + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.en, + }), + ); + }); + }); + }); + context('when no locale provided', function () { + it('should return skill with default translation in locale FR', async function () { + // when + const skills = await skillRepository.get('skillId03'); + + // then + expect(skills).to.deepEqualInstance( + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + ); + }); + }); + }); + }); + + describe('#findActiveByRecordIds', function () { + context('when no active skills for given ids', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findActiveByRecordIds(['skillId01', 'skillCoucou']); + + // then + expect(skills).to.deep.equal([]); + }); }); - describe('when skillId is not found', function () { - it('should throw a Domain error', async function () { + context('when active skills for given ids', function () { + it('should return skills', async function () { // when - const error = await catchErr(skillRepository.get)('skillIdNotFound'); + const skills = await skillRepository.findActiveByRecordIds([ + 'skillId02', + 'skillId03', + 'skillId01', + 'skillId00', + 'skillIdCoucou', + ]); // then - expect(error).to.be.instanceOf(NotFoundError).and.have.property('message', 'Erreur, compétence introuvable'); + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData00_tubeAcompetenceA_actif, + difficulty: skillData00_tubeAcompetenceA_actif.level, + hint: skillData00_tubeAcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + ]); }); }); }); - describe('#findActiveByRecordIds', function () { - it('should resolve active skills passed by ids', async function () { - // given - const competenceId = 'recCompetenceId'; - const activeSkill = domainBuilder.buildSkill({ id: 'activeSkill', competenceId }); - const archivedSkill = domainBuilder.buildSkill({ id: 'archivedSkill', competenceId }); - const nonOperativeSkill = domainBuilder.buildSkill({ id: 'nonOperativeSkill', competenceId }); - const learningContent = { - skills: [ - { ...activeSkill, status: 'actif', level: activeSkill.difficulty }, - { ...archivedSkill, status: 'archivé', level: archivedSkill.difficulty }, - { ...nonOperativeSkill, status: 'BLABLA', level: nonOperativeSkill.difficulty }, - ], - }; - await mockLearningContent(learningContent); - // when - const skills = await skillRepository.findActiveByRecordIds([ - activeSkill.id, - archivedSkill.id, - nonOperativeSkill.id, - ]); + describe('#findByRecordIds', function () { + context('when no skills for given ids', function () { + it('should return an empty array', async function () { + // when + const skills = await skillRepository.findByRecordIds(['skillCoucou']); - // then - expect(skills).to.have.lengthOf(1); - expect(skills[0]).to.be.instanceof(Skill); - expect(skills).to.deep.include.members([activeSkill]); + // then + expect(skills).to.deep.equal([]); + }); + }); + + context('when skills for given ids', function () { + it('should return skills', async function () { + // when + const skills = await skillRepository.findByRecordIds([ + 'skillId02', + 'skillId03', + 'skillId01', + 'skillId00', + 'skillIdCoucou', + ]); + + // then + expect(skills).to.deepEqualArray([ + domainBuilder.buildSkill({ + ...skillData00_tubeAcompetenceA_actif, + difficulty: skillData00_tubeAcompetenceA_actif.level, + hint: skillData00_tubeAcompetenceA_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData01_tubeAcompetenceA_archive, + difficulty: skillData01_tubeAcompetenceA_archive.level, + hint: skillData01_tubeAcompetenceA_archive.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData02_tubeAcompetenceA_perime, + difficulty: skillData02_tubeAcompetenceA_perime.level, + hint: skillData02_tubeAcompetenceA_perime.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData03_tubeBcompetenceA_actif, + difficulty: skillData03_tubeBcompetenceA_actif.level, + hint: skillData03_tubeBcompetenceA_actif.hint_i18n.fr, + }), + ]); + }); }); }); }); diff --git a/api/tests/shared/unit/domain/services/get-translated-text_test.js b/api/tests/shared/unit/domain/services/get-translated-text_test.js index 2896d246ae0..f4e0584624f 100644 --- a/api/tests/shared/unit/domain/services/get-translated-text_test.js +++ b/api/tests/shared/unit/domain/services/get-translated-text_test.js @@ -1,5 +1,6 @@ import * as service from '../../../../../src/shared/domain/services/get-translated-text.js'; import { expect } from '../../../../test-helper.js'; + describe('Unit | Domain | Services | get-translated-text', function () { describe('#getTranslatedKey', function () { const translatedKey = { @@ -29,15 +30,27 @@ describe('Unit | Domain | Services | get-translated-text', function () { expect(result).to.equal('My key'); }); - it('returns by default then french key', function () { - // given - const locale = 'fr-fr-'; + context('when key not present', function () { + it('returns by default the french key when fallback is enabled', function () { + // given + const locale = 'fr-fr-'; - // when - const result = service.getTranslatedKey(translatedKey, locale); + // when + const result = service.getTranslatedKey(translatedKey, locale); - // then - expect(result).to.equal('Ma clef'); + // then + expect(result).to.equal('Ma clef'); + }); + it('returns null when fallback is not enabled', function () { + // given + const locale = 'fr-fr-'; + + // when + const result = service.getTranslatedKey(translatedKey, locale, false); + + // then + expect(result).to.be.null; + }); }); it('returns undefined when the key is undefined', function () { diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index 13db856addc..7a623e46c82 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -28,6 +28,7 @@ import * as tokenService from '../src/shared/domain/services/token-service.js'; import { LearningContentCache } from '../src/shared/infrastructure/caches/learning-content-cache.js'; import * as areaRepository from '../src/shared/infrastructure/repositories/area-repository.js'; import * as competenceRepository from '../src/shared/infrastructure/repositories/competence-repository.js'; +import * as skillRepository from '../src/shared/infrastructure/repositories/skill-repository.js'; import * as customChaiHelpers from './tooling/chai-custom-helpers/index.js'; import * as domainBuilder from './tooling/domain-builder/factory/index.js'; import { jobChai } from './tooling/jobs/expect-job.js'; @@ -81,6 +82,7 @@ afterEach(function () { competenceRepository.clearCache(); thematicRepository.clearCache(); tubeRepository.clearCache(); + skillRepository.clearCache(); return databaseBuilder.clean(); }); diff --git a/api/tests/tooling/domain-builder/factory/build-skill.js b/api/tests/tooling/domain-builder/factory/build-skill.js index 96767898284..df7e4f8219c 100644 --- a/api/tests/tooling/domain-builder/factory/build-skill.js +++ b/api/tests/tooling/domain-builder/factory/build-skill.js @@ -10,6 +10,9 @@ const buildSkill = function buildSkill({ tubeId = 'recTUB123', version = 1, difficulty = 6, + status = 'some status', + hintStatus = 'some hint status', + hint = 'some hint', } = {}) { return new Skill({ id, @@ -21,6 +24,9 @@ const buildSkill = function buildSkill({ tubeId, version, difficulty, + status, + hintStatus, + hint, }); }; diff --git a/api/tests/tooling/fixtures/infrastructure/skillLearningContentDataObjectFixture.js b/api/tests/tooling/fixtures/infrastructure/skillLearningContentDataObjectFixture.js deleted file mode 100644 index e7d78d54acd..00000000000 --- a/api/tests/tooling/fixtures/infrastructure/skillLearningContentDataObjectFixture.js +++ /dev/null @@ -1,45 +0,0 @@ -const ACTIVE_STATUS = 'actif'; - -const DEFAULT_ID = 'recSK0X22abcdefgh', - DEFAULT_HINT_FR_FR = 'Peut-on géo-localiser un lapin sur la banquise ?', - DEFAULT_HINT_EN_US = 'Can we geo-locate a rabbit on the ice floe?', - DEFAULT_HINT_STATUS = 'Validé', - DEFAULT_NAME = '@accesDonnées1', - DEFAULT_TUTORIAL_ID = 'recCO0X22abcdefgh', - DEFAULT_LEARNING_TUTORIAL_IDS = ['recSP0X22abcdefgh', 'recSP0X23abcdefgh'], - DEFAULT_COMPETENCE_ID = 'recCT0X22abcdefgh', - DEFAULT_STATUS = ACTIVE_STATUS, - DEFAULT_PIX_VALUE = 2.4, - DEFAULT_TUBE_ID = 'recTU0X22abcdefgh'; - -const SkillLearningContentDataObjectFixture = function ({ - id = DEFAULT_ID, - name = DEFAULT_NAME, - hintEnUs = DEFAULT_HINT_EN_US, - hintFrFr = DEFAULT_HINT_FR_FR, - hintStatus = DEFAULT_HINT_STATUS, - tutorialIds = [DEFAULT_TUTORIAL_ID], - learningMoreTutorialIds = DEFAULT_LEARNING_TUTORIAL_IDS, - competenceId = DEFAULT_COMPETENCE_ID, - pixValue = DEFAULT_PIX_VALUE, - status = DEFAULT_STATUS, - tubeId = DEFAULT_TUBE_ID, -} = {}) { - return { - id, - name, - hint_i18n: { - en: hintEnUs, - fr: hintFrFr, - }, - hintStatus, - tutorialIds, - learningMoreTutorialIds, - competenceId, - pixValue, - status, - tubeId, - }; -}; - -export { SkillLearningContentDataObjectFixture }; diff --git a/api/tests/unit/infrastructure/repositories/correction-repository_test.js b/api/tests/unit/infrastructure/repositories/correction-repository_test.js index 03b60d16871..e8a55cb598c 100644 --- a/api/tests/unit/infrastructure/repositories/correction-repository_test.js +++ b/api/tests/unit/infrastructure/repositories/correction-repository_test.js @@ -1,20 +1,15 @@ import * as correctionRepository from '../../../../lib/infrastructure/repositories/correction-repository.js'; import { Answer } from '../../../../src/evaluation/domain/models/Answer.js'; -import { Correction } from '../../../../src/shared/domain/models/Correction.js'; -import { - challengeDatasource, - skillDatasource, -} from '../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { domainBuilder, expect, sinon } from '../../../test-helper.js'; +import { Correction } from '../../../../src/shared/domain/models/index.js'; +import { challengeDatasource } from '../../../../src/shared/infrastructure/datasources/learning-content/index.js'; +import { databaseBuilder, domainBuilder, expect, sinon } from '../../../test-helper.js'; import { ChallengeLearningContentDataObjectFixture } from '../../../tooling/fixtures/infrastructure/challengeLearningContentDataObjectFixture.js'; -import { SkillLearningContentDataObjectFixture } from '../../../tooling/fixtures/infrastructure/skillLearningContentDataObjectFixture.js'; describe('Unit | Repository | correction-repository', function () { let tutorialRepository; beforeEach(function () { sinon.stub(challengeDatasource, 'get'); - sinon.stub(skillDatasource, 'get'); tutorialRepository = { findByRecordIdsForCurrentUser: sinon.stub(), }; @@ -61,30 +56,42 @@ describe('Unit | Repository | correction-repository', function () { context('normal challenge', function () { let challengeDataObject; - beforeEach(function () { + beforeEach(async function () { // given - const skillDatas = [ - SkillLearningContentDataObjectFixture({ - name: '@web1', - hintStatus: 'Validé', - tutorialIds: ['recTuto1'], - learningMoreTutorialIds: ['recTuto3'], - }), - SkillLearningContentDataObjectFixture({ - name: '@web2', - hintStatus: 'Proposé', - tutorialIds: ['recTuto2'], - learningMoreTutorialIds: ['recTuto4'], - }), - SkillLearningContentDataObjectFixture({ - name: '@web3', - hintStatus: 'pré-validé', - tutorialIds: [], - learningMoreTutorialIds: [], - }), - ]; - - skillDatas.forEach((skillData, index) => skillDatasource.get.onCall(index).resolves(skillData)); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recIdSkill003', + name: '@web1', + hintStatus: 'Validé', + tutorialIds: ['recTuto1'], + learningMoreTutorialIds: ['recTuto3'], + hint_i18n: { + en: 'Can we geo-locate a rabbit on the ice floe?', + fr: 'Peut-on géo-localiser un lapin sur la banquise ?', + }, + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'skill2', + name: '@web2', + hintStatus: 'Proposé', + tutorialIds: ['recTuto2'], + learningMoreTutorialIds: ['recTuto4'], + hint_i18n: { + en: 'Can we geo-locate a rabbit on the ice floe?', + fr: 'Peut-on géo-localiser un lapin sur la banquise ?', + }, + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'skill3', + name: '@web3', + hintStatus: 'pré-validé', + tutorialIds: [], + learningMoreTutorialIds: [], + hint_i18n: { + en: 'Can we geo-locate a rabbit on the ice floe?', + fr: 'Peut-on géo-localiser un lapin sur la banquise ?', + }, + }); + await databaseBuilder.commit(); tutorialRepository.findByRecordIdsForCurrentUser .withArgs({ ids: ['recTuto1'], userId, locale }) .resolves(expectedTutorials); @@ -128,8 +135,8 @@ describe('Unit | Repository | correction-repository', function () { expect(result).to.deep.equal(expectedCorrection); expect(challengeDatasource.get).to.have.been.calledWithExactly(recordId); expect(expectedCorrection.tutorials.map(({ skillId }) => skillId)).to.deep.equal([ - 'recSK0X22abcdefgh', - 'recSK0X22abcdefgh', + 'recIdSkill003', + 'recIdSkill003', ]); }); @@ -301,7 +308,7 @@ describe('Unit | Repository | correction-repository', function () { it('should return null value as hint', async function () { // given const userId = 1; - const providedLocale = 'frop-fr'; + const providedLocale = 'efr'; const challengeId = 'recTuto1'; const challengeId3 = 'recTuto3'; challengeDataObject = ChallengeLearningContentDataObjectFixture({ From 91b20d6df7bd058f5a24cdbd27a66e61f4b6c852 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Fri, 29 Nov 2024 23:59:11 +0100 Subject: [PATCH 10/24] feat(api): refacto challengeRepository with new cache and to use PG --- .../repositories/correction-repository.js | 23 +- .../infrastructure/adapters/skill-adapter.js | 16 - .../adapters/solution-adapter.js | 1 + .../learning-content/challenge-datasource.js | 112 - .../datasources/learning-content/index.js | 4 +- .../learning-content/skill-datasource.js | 87 - .../repositories/challenge-repository.js | 361 ++- ...mplementary-certifications-scoring_test.js | 39 +- .../correction-repository_test.js | 103 +- .../challenge-datasource_test.js | 482 ---- .../learning-content/skill-datasource_test.js | 275 -- .../repositories/challenge-repository_test.js | 2509 ++++++++++++----- .../adapters/skill-adapter_test.js | 29 - api/tests/test-helper.js | 2 + .../domain-builder/factory/build-challenge.js | 4 + .../domain-builder/factory/build-solution.js | 2 + ...allengeLearningContentDataObjectFixture.js | 62 - 17 files changed, 2150 insertions(+), 1961 deletions(-) delete mode 100644 api/src/shared/infrastructure/adapters/skill-adapter.js delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/challenge-datasource.js delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/skill-datasource.js rename api/tests/{unit => integration}/infrastructure/repositories/correction-repository_test.js (79%) delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/challenge-datasource_test.js delete mode 100644 api/tests/shared/integration/infrastructure/datasources/learning-content/skill-datasource_test.js delete mode 100644 api/tests/shared/unit/infrastructure/adapters/skill-adapter_test.js delete mode 100644 api/tests/tooling/fixtures/infrastructure/challengeLearningContentDataObjectFixture.js diff --git a/api/lib/infrastructure/repositories/correction-repository.js b/api/lib/infrastructure/repositories/correction-repository.js index c6e4eff360c..2d97d8aa1f7 100644 --- a/api/lib/infrastructure/repositories/correction-repository.js +++ b/api/lib/infrastructure/repositories/correction-repository.js @@ -4,7 +4,7 @@ import { Answer } from '../../../src/evaluation/domain/models/Answer.js'; import { Hint } from '../../../src/shared/domain/models/Hint.js'; import { Challenge } from '../../../src/shared/domain/models/index.js'; import { Correction } from '../../../src/shared/domain/models/index.js'; -import { challengeDatasource } from '../../../src/shared/infrastructure/datasources/learning-content/index.js'; +import * as challengeRepository from '../../../src/shared/infrastructure/repositories/challenge-repository.js'; import * as skillRepository from '../../../src/shared/infrastructure/repositories/skill-repository.js'; const VALIDATED_HINT_STATUSES = ['Validé', 'pré-validé']; @@ -18,10 +18,10 @@ const getByChallengeId = async function ({ fromDatasourceObject, getCorrection, } = {}) { - const challenge = await challengeDatasource.get(challengeId); - const skill = await _getSkill(challenge, locale); + const challengeForCorrection = await challengeRepository.get(challengeId, { forCorrection: true }); + const skill = await _getSkill(challengeForCorrection, locale); const hint = await _getHint(skill); - const solution = fromDatasourceObject(challenge); + const solution = fromDatasourceObject(challengeForCorrection); let correctionDetails; const tutorials = await _getTutorials({ @@ -39,14 +39,17 @@ const getByChallengeId = async function ({ tutorialRepository, }); - if (challenge.type === Challenge.Type.QROCM_DEP && answerValue !== Answer.FAKE_VALUE_FOR_SKIPPED_QUESTIONS) { + if ( + challengeForCorrection.type === Challenge.Type.QROCM_DEP && + answerValue !== Answer.FAKE_VALUE_FOR_SKIPPED_QUESTIONS + ) { correctionDetails = getCorrection({ solution, answerValue }); } return new Correction({ - id: challenge.id, - solution: challenge.solution, - solutionToDisplay: challenge.solutionToDisplay, + id: challengeForCorrection.id, + solution: challengeForCorrection.solution, + solutionToDisplay: challengeForCorrection.solutionToDisplay, hint, tutorials, learningMoreTutorials: learningMoreTutorials, @@ -66,8 +69,8 @@ async function _getHint(skill) { return null; } -function _getSkill(challengeDataObject, locale) { - return skillRepository.get(challengeDataObject.skillId, { locale: locale?.slice(0, 2), useFallback: false }); +function _getSkill(challenge, locale) { + return skillRepository.get(challenge.skillId, { locale: locale?.slice(0, 2), useFallback: false }); } function _hasValidatedHint(skill) { diff --git a/api/src/shared/infrastructure/adapters/skill-adapter.js b/api/src/shared/infrastructure/adapters/skill-adapter.js deleted file mode 100644 index 77297b8bbb5..00000000000 --- a/api/src/shared/infrastructure/adapters/skill-adapter.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Skill } from '../../domain/models/Skill.js'; - -const fromDatasourceObject = function (datasourceObject) { - return new Skill({ - id: datasourceObject.id, - name: datasourceObject.name, - pixValue: datasourceObject.pixValue, - competenceId: datasourceObject.competenceId, - tutorialIds: datasourceObject.tutorialIds, - tubeId: datasourceObject.tubeId, - version: datasourceObject.version, - difficulty: datasourceObject.level, - }); -}; - -export { fromDatasourceObject }; diff --git a/api/src/shared/infrastructure/adapters/solution-adapter.js b/api/src/shared/infrastructure/adapters/solution-adapter.js index ae94c5a358a..c2396d02254 100644 --- a/api/src/shared/infrastructure/adapters/solution-adapter.js +++ b/api/src/shared/infrastructure/adapters/solution-adapter.js @@ -34,6 +34,7 @@ function _extractTypeOfQroc(datasourceObject) { } const fromDatasourceObject = function (datasourceObject) { + // TODO scoring n'existe plus dans challenge const scoring = _.ensureString(datasourceObject.scoring).replace(/@/g, ''); // XXX YAML ne supporte pas @ const qrocBlocksTypes = _extractTypeOfQroc(datasourceObject); return new Solution({ diff --git a/api/src/shared/infrastructure/datasources/learning-content/challenge-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/challenge-datasource.js deleted file mode 100644 index 76baf6aa51f..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/challenge-datasource.js +++ /dev/null @@ -1,112 +0,0 @@ -import isEmpty from 'lodash/isEmpty.js'; - -import * as datasource from './datasource.js'; -import { LearningContentResourceNotFound } from './LearningContentResourceNotFound.js'; - -const VALIDATED_CHALLENGE = 'validé'; -// donnée temporaire pour pix1d le temps d'arriver en « prod » -const PROPOSED_CHALLENGE = 'proposé'; -const OBSOLETE_CHALLENGE = 'périmé'; -const OPERATIVE_CHALLENGES = [VALIDATED_CHALLENGE, 'archivé']; - -function _challengeHasStatus(challenge, statuses) { - return statuses.includes(challenge.status); -} - -function _challengeHasLocale(challenge, locale) { - return challenge.locales.includes(locale); -} - -const challengeDatasource = datasource.extend({ - modelName: 'challenges', - - async listByLocale(locale) { - const allChallenges = await this.list(); - return allChallenges.filter((challenge) => _challengeHasLocale(challenge, locale)); - }, - - async getManyByLocale(challengeIds, locale) { - const allChallenges = await this.getMany(challengeIds); - return allChallenges.filter((challenge) => _challengeHasLocale(challenge, locale)); - }, - - async findOperativeBySkillIds(skillIds, locale) { - const foundInSkillIds = (skillId) => skillIds.includes(skillId); - const challenges = await this.findOperative(locale); - return challenges.filter((challengeData) => foundInSkillIds(challengeData.skillId)); - }, - - async findOperative(locale) { - const challenges = await this.listByLocale(locale); - return challenges.filter((challenge) => _challengeHasStatus(challenge, OPERATIVE_CHALLENGES)); - }, - - async findValidatedByCompetenceId(competenceId, locale) { - const challenges = await this.findValidated(locale); - return challenges.filter( - (challengeData) => !isEmpty(challengeData.skillId) && challengeData.competenceId === competenceId, - ); - }, - - async findValidatedBySkillId(id, locale) { - const validatedChallenges = await this.findValidated(locale); - return validatedChallenges.filter((challenge) => challenge.skillId === id); - }, - - async findValidated(locale) { - const challenges = await this.listByLocale(locale); - return challenges.filter((challenge) => _challengeHasStatus(challenge, [VALIDATED_CHALLENGE])); - }, - - async getBySkillId(skillId, locale) { - const challenges = await this.listByLocale(locale); - const filteredChallenges = challenges.filter( - (challenge) => - challenge.skillId === skillId && _challengeHasStatus(challenge, [VALIDATED_CHALLENGE, PROPOSED_CHALLENGE]), - ); - - if (isEmpty(filteredChallenges)) { - throw new LearningContentResourceNotFound({ skillId }); - } - return filteredChallenges; - }, - - async findActiveFlashCompatible(locale) { - const flashChallenges = await this.findFlashCompatible({ locale }); - return flashChallenges.filter((challengeData) => _challengeHasStatus(challengeData, [VALIDATED_CHALLENGE])); - }, - - async findFlashCompatible({ locale, useObsoleteChallenges }) { - const challenges = await this.listByLocale(locale); - - const acceptedStatuses = useObsoleteChallenges - ? [OBSOLETE_CHALLENGE, ...OPERATIVE_CHALLENGES] - : OPERATIVE_CHALLENGES; - - return challenges.filter( - (challengeData) => - challengeData.alpha != null && - challengeData.delta != null && - challengeData.skillId && - _challengeHasStatus(challengeData, acceptedStatuses), - ); - }, - - async findFlashCompatibleWithoutLocale({ useObsoleteChallenges } = {}) { - const challenges = await this.list(); - - const acceptedStatuses = useObsoleteChallenges - ? [OBSOLETE_CHALLENGE, ...OPERATIVE_CHALLENGES] - : OPERATIVE_CHALLENGES; - - return challenges.filter( - (challengeData) => - challengeData.alpha != null && - challengeData.delta != null && - challengeData.skillId && - _challengeHasStatus(challengeData, acceptedStatuses), - ); - }, -}); - -export { challengeDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index b0ff6e941de..b36fa763a4f 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -1,6 +1,4 @@ import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js'; -import { challengeDatasource } from './challenge-datasource.js'; import { courseDatasource } from './course-datasource.js'; -import { skillDatasource } from './skill-datasource.js'; -export { challengeDatasource, courseDatasource, skillDatasource, tutorialDatasource }; +export { courseDatasource, tutorialDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/skill-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/skill-datasource.js deleted file mode 100644 index 367a1d95ed5..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/skill-datasource.js +++ /dev/null @@ -1,87 +0,0 @@ -import _ from 'lodash'; - -import * as datasource from './datasource.js'; - -const ACTIVE_STATUS = 'actif'; -const IN_BUILD_STATUS = 'en construction'; -const OPERATIVE_STATUSES = ['actif', 'archivé']; - -const skillDatasource = datasource.extend({ - modelName: 'skills', - - async findActive() { - const skills = await this.list(); - return _.filter(skills, { status: ACTIVE_STATUS }); - }, - - async findAllSkillsByNameForPix1d(name) { - const skills = await this.list(); - const filteredSkills = _.filter(skills, function (skill) { - return _.isEqual(skill.name, name) && _.includes([ACTIVE_STATUS, IN_BUILD_STATUS], skill.status); - }); - return filteredSkills; - }, - - async findOperative() { - const skills = await this.list(); - return _.filter(skills, (skill) => _.includes(OPERATIVE_STATUSES, skill.status)); - }, - - async findByRecordIds(skillIds) { - const skills = await this.list(); - return skills.filter((skillData) => _.includes(skillIds, skillData.id)); - }, - - async findOperativeByRecordIds(skillIds) { - const skills = await this.list(); - return skills.filter( - (skillData) => _.includes(OPERATIVE_STATUSES, skillData.status) && _.includes(skillIds, skillData.id), - ); - }, - - async findActiveByRecordIds(skillIds) { - const skills = await this.list(); - return _.filter(skills, (skillData) => skillData.status === ACTIVE_STATUS && _.includes(skillIds, skillData.id)); - }, - - async findByTubeIdFor1d(tubeId) { - const skills = await this.list(); - return skills.filter( - (skillData) => - _.includes([ACTIVE_STATUS, IN_BUILD_STATUS], skillData.status) && _.includes(tubeId, skillData.tubeId), - ); - }, - - async findActiveByTubeId(tubeId) { - const skills = await this.list(); - return _.filter(skills, { status: ACTIVE_STATUS, tubeId }); - }, - - async findOperativeByTubeId(tubeId) { - const skills = await this.list(); - return _.filter(skills, (skill) => skill.tubeId === tubeId && _.includes(OPERATIVE_STATUSES, skill.status)); - }, - - async findActiveByCompetenceId(competenceId) { - const skills = await this.list(); - return _.filter(skills, { status: ACTIVE_STATUS, competenceId }); - }, - - async findOperativeByCompetenceId(competenceId) { - const skills = await this.list(); - return _.filter( - skills, - (skill) => skill.competenceId === competenceId && _.includes(OPERATIVE_STATUSES, skill.status), - ); - }, - - async findOperativeByCompetenceIds(competenceIds) { - const skills = await this.list(); - return _.filter( - skills, - (skill) => competenceIds.includes(skill.competenceId) && _.includes(OPERATIVE_STATUSES, skill.status), - ); - }, -}); - -export { ACTIVE_STATUS, skillDatasource }; diff --git a/api/src/shared/infrastructure/repositories/challenge-repository.js b/api/src/shared/infrastructure/repositories/challenge-repository.js index dded7afcebd..8d9c6d44227 100644 --- a/api/src/shared/infrastructure/repositories/challenge-repository.js +++ b/api/src/shared/infrastructure/repositories/challenge-repository.js @@ -1,205 +1,256 @@ -import _ from 'lodash'; - import { httpAgent } from '../../../../lib/infrastructure/http/http-agent.js'; +import * as skillRepository from '../../../shared/infrastructure/repositories/skill-repository.js'; import { config } from '../../config.js'; import { NotFoundError } from '../../domain/errors.js'; -import { Accessibility } from '../../domain/models/Challenge.js'; import { Challenge } from '../../domain/models/index.js'; import * as solutionAdapter from '../../infrastructure/adapters/solution-adapter.js'; -import * as skillAdapter from '../adapters/skill-adapter.js'; -import { challengeDatasource, skillDatasource } from '../datasources/learning-content/index.js'; -import { LearningContentResourceNotFound } from '../datasources/learning-content/LearningContentResourceNotFound.js'; - -const get = async function (id) { - try { - const challenge = await challengeDatasource.get(id); - if (challenge.embedUrl != null && challenge.embedUrl.endsWith('.json')) { - const webComponentResponse = await httpAgent.get({ url: challenge.embedUrl }); - if (!webComponentResponse.isSuccessful) { - throw new NotFoundError( - `Embed webcomponent config with URL ${challenge.embedUrl} in challenge ${challenge.id} not found`, - ); - } - - challenge.webComponentTagName = webComponentResponse.data.name; - challenge.webComponentProps = webComponentResponse.data.props; - } - - const skill = await skillDatasource.get(challenge.skillId); - return _toDomain({ challengeDataObject: challenge, skillDataObject: skill }); - } catch (error) { - if (error instanceof LearningContentResourceNotFound) { - throw new NotFoundError(); - } - throw error; +import { LearningContentRepository } from './learning-content-repository.js'; + +const TABLE_NAME = 'learningcontent.challenges'; +const VALIDATED_STATUS = 'validé'; +const ARCHIVED_STATUS = 'archivé'; +const OBSOLETE_STATUS = 'périmé'; +const OPERATIVE_STATUSES = [VALIDATED_STATUS, ARCHIVED_STATUS]; +const ACCESSIBLE_STATUSES = ['OK', 'RAS']; + +export async function get(id, { forCorrection = false } = {}) { + const challengeDto = await getInstance().load(id); + if (!challengeDto) { + throw new NotFoundError(); } -}; - -const getMany = async function (ids, locale) { - try { - const challengeDataObjects = locale - ? await challengeDatasource.getManyByLocale(ids, locale) - : await challengeDatasource.getMany(ids); - const skills = await skillDatasource.getMany(challengeDataObjects.map(({ skillId }) => skillId)); - return _toDomainCollection({ challengeDataObjects, skills }); - } catch (error) { - if (error instanceof LearningContentResourceNotFound) { - throw new NotFoundError(); - } - throw error; + if (forCorrection) { + return { + id: challengeDto.id, + skillId: challengeDto.skillId, + type: challengeDto.type, + solution: challengeDto.solution, + solutionToDisplay: challengeDto.solutionToDisplay, + proposals: challengeDto.proposals, + t1Status: challengeDto.t1Status, + t2Status: challengeDto.t2Status, + t3Status: challengeDto.t3Status, + }; } -}; + let webComponentInfo; + if (!forCorrection) { + webComponentInfo = await loadWebComponentInfo(challengeDto); + } + const skill = await skillRepository.get(challengeDto.skillId); + return toDomain({ challengeDto, skill, ...webComponentInfo }); +} -const list = async function (locale) { +export async function getMany(ids, locale) { + const challengeDtos = await getInstance().loadMany(ids); + if (challengeDtos.some((challengeDto) => !challengeDto)) { + throw new NotFoundError(); + } + const localeChallengeDtos = locale + ? challengeDtos.filter((challengeDto) => challengeDto.locales.includes(locale)) + : challengeDtos; + localeChallengeDtos.sort(byId); + const challengesDtosWithSkills = await loadChallengeDtosSkills(localeChallengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} + +export async function list(locale) { _assertLocaleIsDefined(locale); - const challengeDataObjects = await challengeDatasource.listByLocale(locale); - const skills = await skillDatasource.list(); - return _toDomainCollection({ challengeDataObjects, skills }); -}; + const cacheKey = `list(${locale})`; + const findByLocaleCallback = (knex) => knex.whereRaw('?=ANY(??)', [locale, 'locales']).orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findByLocaleCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} -const findValidated = async function (locale) { +export async function findValidated(locale) { _assertLocaleIsDefined(locale); - const challengeDataObjects = await challengeDatasource.findValidated(locale); - const activeSkills = await skillDatasource.findActive(); - return _toDomainCollection({ challengeDataObjects, skills: activeSkills }); -}; + const cacheKey = `findValidated(${locale})`; + const findValidatedByLocaleCallback = (knex) => + knex.whereRaw('?=ANY(??)', [locale, 'locales']).where('status', VALIDATED_STATUS).orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findValidatedByLocaleCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} -const findOperative = async function (locale) { +export async function findOperative(locale) { _assertLocaleIsDefined(locale); - const challengeDataObjects = await challengeDatasource.findOperative(locale); - const operativeSkills = await skillDatasource.findOperative(); - return _toDomainCollection({ challengeDataObjects, skills: operativeSkills }); -}; + const cacheKey = `findOperative(${locale})`; + const findOperativeByLocaleCallback = (knex) => + knex.whereRaw('?=ANY(??)', [locale, 'locales']).whereIn('status', OPERATIVE_STATUSES).orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findOperativeByLocaleCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} -const findValidatedByCompetenceId = async function (competenceId, locale) { +export async function findValidatedByCompetenceId(competenceId, locale) { _assertLocaleIsDefined(locale); - const challengeDataObjects = await challengeDatasource.findValidatedByCompetenceId(competenceId, locale); - const activeSkills = await skillDatasource.findActive(); - return _toDomainCollection({ challengeDataObjects, skills: activeSkills }); -}; + const cacheKey = `findValidatedByCompetenceId(${competenceId}, ${locale})`; + const findValidatedByLocaleByCompetenceIdCallback = (knex) => + knex.whereRaw('?=ANY(??)', [locale, 'locales']).where({ competenceId, status: VALIDATED_STATUS }).orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findValidatedByLocaleByCompetenceIdCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} -const findOperativeBySkills = async function (skills, locale) { +export async function findOperativeBySkills(skills, locale) { _assertLocaleIsDefined(locale); const skillIds = skills.map((skill) => skill.id); - const challengeDataObjects = await challengeDatasource.findOperativeBySkillIds(skillIds, locale); - const operativeSkills = await skillDatasource.findOperative(); - return _toDomainCollection({ challengeDataObjects, skills: operativeSkills }); -}; + const cacheKey = `findOperativeBySkillIds([${skillIds.sort()}], ${locale})`; + const findOperativeByLocaleBySkillIdsCallback = (knex) => + knex + .whereRaw('?=ANY(??)', [locale, 'locales']) + .whereIn('status', OPERATIVE_STATUSES) + .whereIn('skillId', skillIds) + .orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findOperativeByLocaleBySkillIdsCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} -const findActiveFlashCompatible = async function ({ +export async function findActiveFlashCompatible({ locale, successProbabilityThreshold = config.features.successProbabilityThreshold, accessibilityAdjustmentNeeded = false, } = {}) { _assertLocaleIsDefined(locale); - let challengeDataObjects = await challengeDatasource.findActiveFlashCompatible(locale); + const cacheKey = `findActiveFlashCompatible({ locale: ${locale}, accessibilityAdjustmentNeeded: ${accessibilityAdjustmentNeeded} })`; + let findCallback; if (accessibilityAdjustmentNeeded) { - challengeDataObjects = challengeDataObjects.filter((challengeDataObject) => { - return ( - (challengeDataObject.accessibility1 === Accessibility.OK || - challengeDataObject.accessibility1 === Accessibility.RAS) && - (challengeDataObject.accessibility2 === Accessibility.OK || - challengeDataObject.accessibility2 === Accessibility.RAS) - ); - }); + findCallback = (knex) => + knex + .whereRaw('?=ANY(??)', [locale, 'locales']) + .where('status', VALIDATED_STATUS) + .whereNotNull('alpha') + .whereNotNull('delta') + .orderBy('id'); + } else { + findCallback = (knex) => + knex + .whereRaw('?=ANY(??)', [locale, 'locales']) + .where('status', VALIDATED_STATUS) + .whereNotNull('alpha') + .whereNotNull('delta') + .whereIn('accessibility1', ACCESSIBLE_STATUSES) + .whereIn('accessibility2', ACCESSIBLE_STATUSES) + .orderBy('id'); } - const activeSkills = await skillDatasource.findActive(); - return _toDomainCollection({ challengeDataObjects, skills: activeSkills, successProbabilityThreshold }); -}; + const challengeDtos = await getInstance().find(cacheKey, findCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => + toDomain({ challengeDto, skill, successProbabilityThreshold }), + ); +} -const findFlashCompatibleWithoutLocale = async function ({ useObsoleteChallenges } = {}) { - const challengeDataObjects = await challengeDatasource.findFlashCompatibleWithoutLocale({ useObsoleteChallenges }); - const skills = await skillDatasource.list(); - return _toDomainCollection({ challengeDataObjects, skills }); -}; +export async function findFlashCompatibleWithoutLocale({ useObsoleteChallenges } = {}) { + const acceptedStatuses = useObsoleteChallenges ? [OBSOLETE_STATUS, ...OPERATIVE_STATUSES] : OPERATIVE_STATUSES; + const cacheKey = `findFlashCompatibleByStatuses({ useObsoleteChallenges: ${Boolean(useObsoleteChallenges)} })`; + const findFlashCompatibleByStatusesCallback = (knex) => + knex.whereIn('status', acceptedStatuses).whereNotNull('alpha').whereNotNull('delta').orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findFlashCompatibleByStatusesCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} -const findValidatedBySkillId = async function (skillId, locale) { +export async function findValidatedBySkillId(skillId, locale) { _assertLocaleIsDefined(locale); - const challengeDataObjects = await challengeDatasource.findValidatedBySkillId(skillId, locale); - const activeSkills = await skillDatasource.findActive(); - return _toDomainCollection({ challengeDataObjects, skills: activeSkills }); -}; + const cacheKey = `findValidatedBySkillId(${skillId}, ${locale})`; + const findValidatedByLocaleBySkillIdCallback = (knex) => + knex.whereRaw('?=ANY(??)', [locale, 'locales']).where({ skillId, status: VALIDATED_STATUS }).orderBy('id'); + const challengeDtos = await getInstance().find(cacheKey, findValidatedByLocaleBySkillIdCallback); + const challengesDtosWithSkills = await loadChallengeDtosSkills(challengeDtos); + return challengesDtosWithSkills.map(([challengeDto, skill]) => toDomain({ challengeDto, skill })); +} export async function getManyTypes(ids) { - const challenges = await challengeDatasource.getMany(ids); - return Object.fromEntries(challenges.map(({ id, type }) => [id, type])); -} - -export { - findActiveFlashCompatible, - findFlashCompatibleWithoutLocale, - findOperative, - findOperativeBySkills, - findValidated, - findValidatedByCompetenceId, - findValidatedBySkillId, - get, - getMany, - list, -}; - -function _assertLocaleIsDefined(locale) { - if (!locale) { - throw new Error('Locale shall be defined'); + const challengeDtos = await getInstance().loadMany(ids); + if (challengeDtos.some((challengeDto) => !challengeDto)) { + throw new NotFoundError(); } + return Object.fromEntries(challengeDtos.map(({ id, type }) => [id, type])); } -function _toDomainCollection({ challengeDataObjects, skills, successProbabilityThreshold }) { - const skillMap = _.keyBy(skills, 'id'); - const lookupSkill = (id) => skillMap[id]; - const challenges = challengeDataObjects.map((challengeDataObject) => { - const skillDataObject = lookupSkill(challengeDataObject.skillId); +export function clearCache() { + return getInstance().clearCache(); +} - return _toDomain({ - challengeDataObject, - skillDataObject, - successProbabilityThreshold, - }); - }); +async function loadWebComponentInfo(challengeDto) { + if (challengeDto.embedUrl == null || !challengeDto.embedUrl.endsWith('.json')) return null; - return challenges; + const response = await httpAgent.get({ url: challengeDto.embedUrl }); + if (!response.isSuccessful) { + throw new NotFoundError( + `Embed webcomponent config with URL ${challengeDto.embedUrl} in challenge ${challengeDto.id} not found`, + ); + } + + return { + webComponentTagName: response.data.name, + webComponentProps: response.data.props, + }; +} + +async function loadChallengeDtosSkills(challengeDtos) { + return Promise.all( + challengeDtos.map(async (challengeDto) => [challengeDto, await skillRepository.get(challengeDto.skillId)]), + ); } -function _toDomain({ challengeDataObject, skillDataObject, successProbabilityThreshold }) { - const skill = skillDataObject ? skillAdapter.fromDatasourceObject(skillDataObject) : null; +function _assertLocaleIsDefined(locale) { + if (!locale) { + throw new Error('Locale shall be defined'); + } +} - const solution = solutionAdapter.fromDatasourceObject(challengeDataObject); +function byId(challenge1, challenge2) { + return challenge1.id < challenge2.id ? -1 : 1; +} +function toDomain({ challengeDto, webComponentTagName, webComponentProps, skill, successProbabilityThreshold }) { + const solution = solutionAdapter.fromDatasourceObject(challengeDto); const validator = Challenge.createValidatorForChallengeType({ - challengeType: challengeDataObject.type, + challengeType: challengeDto.type, solution, }); return new Challenge({ - id: challengeDataObject.id, - type: challengeDataObject.type, - status: challengeDataObject.status, - instruction: challengeDataObject.instruction, - alternativeInstruction: challengeDataObject.alternativeInstruction, - proposals: challengeDataObject.proposals, - timer: challengeDataObject.timer, - illustrationUrl: challengeDataObject.illustrationUrl, - attachments: challengeDataObject.attachments, - embedUrl: challengeDataObject.embedUrl, - embedTitle: challengeDataObject.embedTitle, - embedHeight: challengeDataObject.embedHeight, - webComponentTagName: challengeDataObject.webComponentTagName, - webComponentProps: challengeDataObject.webComponentProps, + id: challengeDto.id, + type: challengeDto.type, + status: challengeDto.status, + instruction: challengeDto.instruction, + alternativeInstruction: challengeDto.alternativeInstruction, + proposals: challengeDto.proposals, + timer: challengeDto.timer, + illustrationUrl: challengeDto.illustrationUrl, + attachments: challengeDto.attachments ? [...challengeDto.attachments] : null, + embedUrl: challengeDto.embedUrl, + embedTitle: challengeDto.embedTitle, + embedHeight: challengeDto.embedHeight, + webComponentTagName, + webComponentProps, skill, validator, - competenceId: challengeDataObject.competenceId, - illustrationAlt: challengeDataObject.illustrationAlt, - format: challengeDataObject.format, - locales: challengeDataObject.locales, - autoReply: challengeDataObject.autoReply, - focused: challengeDataObject.focusable, - discriminant: challengeDataObject.alpha, - difficulty: challengeDataObject.delta, - responsive: challengeDataObject.responsive, - shuffled: challengeDataObject.shuffled, + competenceId: challengeDto.competenceId, + illustrationAlt: challengeDto.illustrationAlt, + format: challengeDto.format, + locales: challengeDto.locales ? [...challengeDto.locales] : null, + autoReply: challengeDto.autoReply, + focused: challengeDto.focusable, + discriminant: challengeDto.alpha, + difficulty: challengeDto.delta, + responsive: challengeDto.responsive, + shuffled: challengeDto.shuffled, + alternativeVersion: challengeDto.alternativeVersion, + blindnessCompatibility: challengeDto.accessibility1, + colorBlindnessCompatibility: challengeDto.accessibility2, successProbabilityThreshold, - alternativeVersion: challengeDataObject.alternativeVersion, - blindnessCompatibility: challengeDataObject.accessibility1, - colorBlindnessCompatibility: challengeDataObject.accessibility2, }); } + +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; +} diff --git a/api/tests/integration/domain/event/handle-complementary-certifications-scoring_test.js b/api/tests/integration/domain/event/handle-complementary-certifications-scoring_test.js index 94483aeee55..a0a248614a2 100644 --- a/api/tests/integration/domain/event/handle-complementary-certifications-scoring_test.js +++ b/api/tests/integration/domain/event/handle-complementary-certifications-scoring_test.js @@ -8,9 +8,9 @@ import { AutoJuryCommentKeys } from '../../../../src/certification/shared/domain import * as certificationAssessmentRepository from '../../../../src/certification/shared/infrastructure/repositories/certification-assessment-repository.js'; import * as certificationCourseRepository from '../../../../src/certification/shared/infrastructure/repositories/certification-course-repository.js'; import * as complementaryCertificationBadgesRepository from '../../../../src/certification/shared/infrastructure/repositories/complementary-certification-badge-repository.js'; -import { AnswerStatus } from '../../../../src/shared/domain/models/AnswerStatus.js'; +import { AnswerStatus } from '../../../../src/shared/domain/models/index.js'; import * as assessmentResultRepository from '../../../../src/shared/infrastructure/repositories/assessment-result-repository.js'; -import { databaseBuilder, expect, knex, mockLearningContent } from '../../../test-helper.js'; +import { databaseBuilder, expect, knex } from '../../../test-helper.js'; describe('Integration | Event | Handle Complementary Certifications Scoring', function () { describe('#handleComplementaryCertificationsScoring', function () { @@ -157,24 +157,23 @@ describe('Integration | Event | Handle Complementary Certifications Scoring', fu describe('when the lower level is acquired', function () { beforeEach(async function () { - const learningContent = { - challenges: [ - { - id: 'recCompetence0_Tube1_Skill1_Challenge1', - competenceId: 'recCompetence0', - }, - { - id: 'recCompetence0_Tube1_Skill2_Challenge2', - competenceId: 'recCompetence0', - }, - { - id: 'recCompetence0_Tube1_Skill2_Challenge3', - competenceId: 'recCompetence0', - }, - ], - }; - - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildChallenge({ + id: 'recCompetence0_Tube1_Skill1_Challenge1', + competenceId: 'recCompetence0', + skillId: 'someSkillId', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + id: 'recCompetence0_Tube1_Skill2_Challenge2', + competenceId: 'recCompetence0', + skillId: 'someSkillId', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + id: 'recCompetence0_Tube1_Skill2_Challenge3', + competenceId: 'recCompetence0', + skillId: 'someSkillId', + }); + databaseBuilder.factory.learningContent.buildSkill({ id: 'someSkillId' }); + await databaseBuilder.commit(); }); it('should save a result', async function () { diff --git a/api/tests/unit/infrastructure/repositories/correction-repository_test.js b/api/tests/integration/infrastructure/repositories/correction-repository_test.js similarity index 79% rename from api/tests/unit/infrastructure/repositories/correction-repository_test.js rename to api/tests/integration/infrastructure/repositories/correction-repository_test.js index e8a55cb598c..3069f8cadcc 100644 --- a/api/tests/unit/infrastructure/repositories/correction-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/correction-repository_test.js @@ -1,15 +1,12 @@ import * as correctionRepository from '../../../../lib/infrastructure/repositories/correction-repository.js'; import { Answer } from '../../../../src/evaluation/domain/models/Answer.js'; import { Correction } from '../../../../src/shared/domain/models/index.js'; -import { challengeDatasource } from '../../../../src/shared/infrastructure/datasources/learning-content/index.js'; import { databaseBuilder, domainBuilder, expect, sinon } from '../../../test-helper.js'; -import { ChallengeLearningContentDataObjectFixture } from '../../../tooling/fixtures/infrastructure/challengeLearningContentDataObjectFixture.js'; -describe('Unit | Repository | correction-repository', function () { +describe('Integration | Repository | correction-repository', function () { let tutorialRepository; beforeEach(function () { - sinon.stub(challengeDatasource, 'get'); tutorialRepository = { findByRecordIdsForCurrentUser: sinon.stub(), }; @@ -17,6 +14,37 @@ describe('Unit | Repository | correction-repository', function () { describe('#getByChallengeId', function () { const recordId = 'rec-challengeId'; + const challengeBaseData = { + id: recordId, + instruction: + "Les moteurs de recherche affichent certains liens en raison d'un accord commercial.\n\nDans quels encadrés se trouvent ces liens ?", + proposals: '- 1\n- 2\n- 3\n- 4\n- 5', + type: 'QCM', + solution: '1, 5', + solutionToDisplay: '1', + t1Status: true, + t2Status: false, + t3Status: true, + status: 'validé', + skillId: 'recIdSkill003', + timer: 1234, + illustrationUrl: 'https://dl.airtable.com/2MGErxGTQl2g2KiqlYgV_venise4.png', + illustrationAlt: 'Texte alternatif de l’illustration', + attachments: [ + 'https://dl.airtable.com/nHWKNZZ7SQeOKsOvVykV_navigationdiaporama5.pptx', + 'https://dl.airtable.com/rsXNJrSPuepuJQDByFVA_navigationdiaporama5.odp', + ], + competenceId: 'recsvLz0W2ShyfD63', + embedUrl: 'https://github.io/page/epreuve.html', + embedTitle: 'Epreuve de selection de dossier', + embedHeight: 500, + format: 'petit', + locales: ['fr'], + autoReply: false, + alternativeInstruction: '', + accessibility1: 'OK', + accessibility2: 'RAS', + }; const userId = 'userId'; const locale = 'en'; let fromDatasourceObject; @@ -103,20 +131,15 @@ describe('Unit | Repository | correction-repository', function () { it('should return a correction with the solution and solutionToDisplay', async function () { // given const expectedCorrection = new Correction({ - id: 'recwWzTquPlvIl4So', + id: recordId, solution: '1, 5', solutionToDisplay: '1', hint: expectedHint, tutorials: expectedTutorials, learningMoreTutorials: expectedLearningMoreTutorials, }); - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', - solution: '1, 5', - solutionToDisplay: '1', - type: 'QCM', - }); - challengeDatasource.get.resolves(challengeDataObject); + databaseBuilder.factory.learningContent.buildChallenge(challengeBaseData); + await databaseBuilder.commit(); const getCorrectionStub = sinon.stub(); // when @@ -133,7 +156,6 @@ describe('Unit | Repository | correction-repository', function () { expect(getCorrectionStub).not.to.have.been.called; expect(result).to.be.an.instanceof(Correction); expect(result).to.deep.equal(expectedCorrection); - expect(challengeDatasource.get).to.have.been.calledWithExactly(recordId); expect(expectedCorrection.tutorials.map(({ skillId }) => skillId)).to.deep.equal([ 'recIdSkill003', 'recIdSkill003', @@ -142,10 +164,8 @@ describe('Unit | Repository | correction-repository', function () { it('should return the correction with validated hint', async function () { // given - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', - }); - challengeDatasource.get.resolves(challengeDataObject); + databaseBuilder.factory.learningContent.buildChallenge(challengeBaseData); + await databaseBuilder.commit(); const getCorrectionStub = sinon.stub(); // when @@ -166,12 +186,11 @@ describe('Unit | Repository | correction-repository', function () { context('when answer is skipped', function () { it('should not call getCorrection service', async function () { // given - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', - solution: '1, 5', + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeBaseData, type: 'QROCM-dep', }); - challengeDatasource.get.resolves(challengeDataObject); + await databaseBuilder.commit(); const answerValue = Answer.FAKE_VALUE_FOR_SKIPPED_QUESTIONS; const solution = Symbol('solution'); @@ -197,16 +216,27 @@ describe('Unit | Repository | correction-repository', function () { it('should call solution service and return solution blocks', async function () { // given - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', - solution: '1, 5', + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeBaseData, type: 'QROCM-dep', }); - challengeDatasource.get.resolves(challengeDataObject); + await databaseBuilder.commit(); const answerValue = Symbol('answerValue'); const solution = Symbol('solution'); - fromDatasourceObject.withArgs(challengeDataObject).returns(solution); + fromDatasourceObject + .withArgs({ + id: challengeBaseData.id, + skillId: challengeBaseData.skillId, + type: 'QROCM-dep', + solution: challengeBaseData.solution, + solutionToDisplay: challengeBaseData.solutionToDisplay, + proposals: challengeBaseData.proposals, + t1Status: challengeBaseData.t1Status, + t2Status: challengeBaseData.t2Status, + t3Status: challengeBaseData.t3Status, + }) + .returns(solution); const getCorrectionStub = sinon.stub(); const answersEvaluation = Symbol('answersEvaluation'); const solutionsWithoutGoodAnswers = Symbol('solutionsWithoutGoodAnswers'); @@ -239,10 +269,11 @@ describe('Unit | Repository | correction-repository', function () { const providedLocale = 'fr-fr'; const challengeId = 'recTuto1'; const challengeId3 = 'recTuto3'; - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeBaseData, + id: challengeId, }); - challengeDatasource.get.resolves(challengeDataObject); + await databaseBuilder.commit(); const getCorrectionStub = sinon.stub(); tutorialRepository.findByRecordIdsForCurrentUser .withArgs({ ids: [challengeId], userId, locale: providedLocale }) @@ -277,10 +308,11 @@ describe('Unit | Repository | correction-repository', function () { const locale = 'jp'; const challengeId = 'recTuto1'; const challengeId3 = 'recTuto3'; - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeBaseData, + id: challengeId, }); - challengeDatasource.get.resolves(challengeDataObject); + await databaseBuilder.commit(); const getCorrectionStub = sinon.stub(); tutorialRepository.findByRecordIdsForCurrentUser .withArgs({ ids: [challengeId], userId, locale }) @@ -311,10 +343,11 @@ describe('Unit | Repository | correction-repository', function () { const providedLocale = 'efr'; const challengeId = 'recTuto1'; const challengeId3 = 'recTuto3'; - challengeDataObject = ChallengeLearningContentDataObjectFixture({ - skillId: 'recIdSkill003', + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeBaseData, + id: challengeId, }); - challengeDatasource.get.resolves(challengeDataObject); + await databaseBuilder.commit(); const getCorrectionStub = sinon.stub(); tutorialRepository.findByRecordIdsForCurrentUser .withArgs({ ids: [challengeId], userId, locale: providedLocale }) diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/challenge-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/challenge-datasource_test.js deleted file mode 100644 index 9030916a5fe..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/challenge-datasource_test.js +++ /dev/null @@ -1,482 +0,0 @@ -import _ from 'lodash'; - -import { challengeDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { LearningContentResourceNotFound } from '../../../../../../src/shared/infrastructure/datasources/learning-content/LearningContentResourceNotFound.js'; -import { catchErr, expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | ChallengeDatasource', function () { - let competence1, - competence2, - web1, - web2, - web3, - challenge_competence1, - challenge_competence1_en, - challenge_competence1_noSkills, - challenge_competence1_notValidated, - challenge_competence1_obsolete, - challenge_competence2, - challenge_web1, - challenge_web1_notValidated, - challenge_web1_archived, - challenge_web2_en, - challenge_web3, - challenge_web3_archived; - - beforeEach(function () { - competence1 = { id: 'competence1' }; - competence2 = { id: 'competence2' }; - web1 = { id: 'skill-web1' }; - web2 = { id: 'skill-web2' }; - web3 = { id: 'skill-web3' }; - challenge_competence1 = { - id: 'challenge-competence1', - competenceId: competence1.id, - skillId: web1.id, - status: 'validé', - locales: ['fr', 'fr-fr'], - alpha: 2.11, - delta: -3.56, - }; - challenge_competence1_en = { - id: 'challenge-competence1-en', - competenceId: competence1.id, - skillId: web1.id, - status: 'validé', - locales: ['en'], - alpha: 2.11, - delta: -3.56, - }; - challenge_competence1_noSkills = { - id: 'challenge-competence1-noSkills', - competenceId: competence1.id, - skillId: undefined, - status: 'validé', - locales: ['fr', 'fr-fr'], - alpha: 8.11, - delta: 0.95, - }; - challenge_competence1_notValidated = { - id: 'challenge-competence1-notValidated', - competenceId: competence1.id, - skillId: web1.id, - locales: ['fr', 'fr-fr'], - status: 'proposé', - alpha: -0, - delta: 0, - }; - - challenge_competence1_obsolete = { - id: 'challenge-competence1-obsolete', - competenceId: competence1.id, - skillId: web1.id, - locales: ['fr', 'fr-fr'], - status: 'périmé', - alpha: -0, - delta: 0, - }; - - challenge_competence2 = { - id: 'challenge-competence2', - competenceId: competence2.id, - skillId: web1.id, - status: 'validé', - locales: ['fr', 'fr-fr'], - alpha: 8.21, - delta: -4.23, - }; - challenge_web1 = { - id: 'challenge-web1', - skillId: web1.id, - locales: ['fr', 'fr-fr'], - status: 'validé', - }; - challenge_web1_notValidated = { - id: 'challenge-web1-notValidated', - skillId: web1.id, - status: 'proposé', - locales: ['fr', 'fr-fr'], - }; - challenge_web1_archived = { - id: 'challenge_web1_archived', - skillId: web1.id, - status: 'archivé', - locales: ['fr', 'fr-fr'], - }; - challenge_web2_en = { - id: 'challenge-web2', - skillId: web2.id, - locales: ['en'], - status: 'validé', - alpha: 1, - delta: -2, - }; - challenge_web3 = { - id: 'challenge-web3', - skillId: web3.id, - status: 'validé', - locales: ['fr', 'fr-fr'], - alpha: 1.83, - delta: 0.27, - }; - challenge_web3_archived = { - id: 'challenge-web3-archived', - skillId: web3.id, - status: 'archivé', - locales: ['fr-fr'], - alpha: -8.1, - delta: 0, - }; - }); - - describe('#listBylocale', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [challenge_web1, challenge_web1_notValidated, challenge_web2_en, challenge_web3], - }); - }); - - it('should return a list of all challenges having locale', async function () { - // given - const locale = 'fr'; - - // when - const results = await challengeDatasource.listByLocale(locale); - - // then - expect(results.map((result) => result.id)).to.deep.equal([ - 'challenge-web1', - 'challenge-web1-notValidated', - 'challenge-web3', - ]); - }); - }); - - describe('#getManyByLocale', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [challenge_web1, challenge_web1_notValidated, challenge_web2_en, challenge_web3], - }); - }); - - it('should return a list of all challenges having locale by id', async function () { - // given - const locale = 'fr'; - const challengeIdList = ['challenge-web1', 'challenge-web1-notValidated', 'challenge-web2', 'challenge-web3']; - - // when - const results = await challengeDatasource.getManyByLocale(challengeIdList, locale); - - // then - expect(results.map((result) => result.id)).to.deep.equal([ - 'challenge-web1', - 'challenge-web1-notValidated', - 'challenge-web3', - ]); - }); - }); - - describe('#findOperativeBySkillIds', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [challenge_web1, challenge_web1_notValidated, challenge_web2_en, challenge_web3], - }); - }); - - it('should resolve an array of matching Challenges from learning content', async function () { - // given - const skillIds = ['skill-web1', 'skill-web2']; - const locale = 'fr'; - - // when - const result = await challengeDatasource.findOperativeBySkillIds(skillIds, locale); - - // then - expect(_.map(result, 'id')).to.deep.equal(['challenge-web1']); - }); - }); - - describe('#findValidatedByCompetenceId', function () { - let result; - - beforeEach(async function () { - // given - await mockLearningContent({ - challenges: [ - challenge_competence1, - challenge_competence1_en, - challenge_competence1_noSkills, - challenge_competence1_notValidated, - challenge_competence2, - ], - }); - - // when - result = await challengeDatasource.findValidatedByCompetenceId(competence1.id, 'fr'); - }); - - it('should resolve to an array of matching Challenges from learning content', function () { - // then - expect(_.map(result, 'id')).to.deep.equal(['challenge-competence1']); - }); - }); - - describe('#findOperative', function () { - it('should retrieve the operative Challenges of given locale only', async function () { - // given - const locale = 'fr-fr'; - await mockLearningContent({ - challenges: [challenge_web1, challenge_web1_notValidated, challenge_web2_en, challenge_web3_archived], - }); - - // when - const result = await challengeDatasource.findOperative(locale); - - // then - expect(_.map(result, 'id')).to.deep.equal(['challenge-web1', 'challenge-web3-archived']); - }); - }); - - describe('#findValidated', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [challenge_web1, challenge_web1_notValidated, challenge_web2_en, challenge_web3_archived], - }); - }); - - it('should resolve an array of matching Challenges from learning content', async function () { - // when - const result = await challengeDatasource.findValidated('fr'); - - // then - expect(_.map(result, 'id')).to.deep.equal(['challenge-web1']); - }); - }); - - describe('#findFlashCompatible', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [ - challenge_competence1, - challenge_competence1_notValidated, - challenge_competence1_noSkills, - challenge_competence1_obsolete, - challenge_competence2, - challenge_web1, - challenge_web1_notValidated, - challenge_web2_en, - challenge_web3, - challenge_web3_archived, - ], - }); - }); - - context('when not requesting obsolete challenges', function () { - it('should resolve an array of matching Challenges from learning content', async function () { - // when - const locale = 'fr-fr'; - const result = await challengeDatasource.findFlashCompatible({ locale }); - - // then - expect(_.map(result, 'id').sort()).to.deep.equal( - [challenge_competence1.id, challenge_competence2.id, challenge_web3.id, challenge_web3_archived.id].sort(), - ); - }); - }); - - context('when requesting obsolete challenges', function () { - it('should resolve an array of matching Challenges from learning content', async function () { - // when - const locale = 'fr-fr'; - const result = await challengeDatasource.findFlashCompatible({ locale, useObsoleteChallenges: true }); - - // then - expect(_.map(result, 'id').sort()).to.deep.equal( - [ - challenge_competence1.id, - challenge_competence1_obsolete.id, - challenge_competence2.id, - challenge_web3.id, - challenge_web3_archived.id, - ].sort(), - ); - }); - }); - }); - - describe('#findFlashCompatibleWithoutLocale', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [ - challenge_competence1, - challenge_competence1_notValidated, - challenge_competence1_noSkills, - challenge_competence1_obsolete, - challenge_competence2, - challenge_web1, - challenge_web1_notValidated, - challenge_web2_en, - challenge_web3, - challenge_web3_archived, - ], - }); - }); - - context('when not requesting obsolete challenges', function () { - it('should resolve an array of matching Challenges from learning content', async function () { - // when - const result = await challengeDatasource.findFlashCompatibleWithoutLocale(); - - // then - expect(_.map(result, 'id')).to.have.members([ - challenge_competence1.id, - challenge_competence2.id, - challenge_web2_en.id, - challenge_web3.id, - challenge_web3_archived.id, - ]); - }); - }); - - context('when requesting obsolete challenges', function () { - it('should resolve an array of matching Challenges from learning content', async function () { - // when - const result = await challengeDatasource.findFlashCompatibleWithoutLocale({ useObsoleteChallenges: true }); - - // then - expect(_.map(result, 'id')).to.have.members([ - challenge_competence1.id, - challenge_competence1_obsolete.id, - challenge_competence2.id, - challenge_web2_en.id, - challenge_web3.id, - challenge_web3_archived.id, - ]); - }); - }); - }); - - describe('#findActiveFlashCompatible', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [ - challenge_competence1, - challenge_competence1_noSkills, - challenge_competence2, - challenge_web1, - challenge_web1_notValidated, - challenge_web2_en, - challenge_web3, - challenge_web3_archived, - ], - }); - }); - - describe('when a locale is set', function () { - it('should resolve an array of matching Challenges from learning content containing the locale', async function () { - // when - const locale = 'fr-fr'; - const result = await challengeDatasource.findActiveFlashCompatible(locale); - - // then - expect(_.map(result, 'id')).to.deep.equal([ - challenge_competence1.id, - challenge_competence2.id, - challenge_web3.id, - ]); - }); - }); - }); - - describe('#findValidatedBySkillId', function () { - beforeEach(async function () { - await mockLearningContent({ - challenges: [challenge_web1, challenge_web1_notValidated, challenge_web1_archived, challenge_competence2], - }); - }); - - it('should resolve an array of validated challenge of a skill from learning content ', async function () { - // when - const result = await challengeDatasource.findValidatedBySkillId('skill-web1', 'fr'); - - // then - expect(result).to.deep.equal([challenge_web1, challenge_competence2]); - }); - }); - - describe('#getBySkillId', function () { - let validated_challenge_pix1d; - let proposed_challenge_pix1d; - let obsolete_challenge_pix1d; - - const skillId = '@didacticiel1'; - const locale = 'fr'; - - beforeEach(function () { - validated_challenge_pix1d = { - id: 'challenge-pix1d1', - competenceId: competence1.id, - locales: ['fr'], - skillId, - status: 'validé', - }; - proposed_challenge_pix1d = { - id: 'challenge-pix1d2', - competenceId: competence1.id, - locales: ['fr'], - status: 'proposé', - skillId, - }; - obsolete_challenge_pix1d = { - id: 'challenge-pix1d3', - competenceId: competence1.id, - locales: ['fr'], - status: 'périmé', - skillId, - }; - }); - - context('when there are several challenges for the skillId', function () { - it('should return an array of validated or proposed challenges', async function () { - // when - await mockLearningContent({ - challenges: [ - challenge_web1, - challenge_competence2, - validated_challenge_pix1d, - proposed_challenge_pix1d, - obsolete_challenge_pix1d, - ], - }); - const result = await challengeDatasource.getBySkillId(skillId, locale); - - // then - expect(result).to.deep.equal([validated_challenge_pix1d, proposed_challenge_pix1d]); - }); - }); - - context('when there is only one challenge for the skillId', function () { - it('should return a challenge from learning content', async function () { - // when - await mockLearningContent({ - challenges: [challenge_web1, challenge_competence2, validated_challenge_pix1d], - }); - const result = await challengeDatasource.getBySkillId(skillId, locale); - - // then - expect(result).to.deep.equal([validated_challenge_pix1d]); - }); - }); - - it('should return an error if there is no challenge for the given skillId', async function () { - // when - await mockLearningContent({ - challenges: [challenge_web1, challenge_competence2, validated_challenge_pix1d, proposed_challenge_pix1d], - }); - const error = await catchErr(challengeDatasource.getBySkillId, locale)('falseId'); - - // then - expect(error).to.be.instanceOf(LearningContentResourceNotFound); - }); - }); -}); diff --git a/api/tests/shared/integration/infrastructure/datasources/learning-content/skill-datasource_test.js b/api/tests/shared/integration/infrastructure/datasources/learning-content/skill-datasource_test.js deleted file mode 100644 index 818e56b31dd..00000000000 --- a/api/tests/shared/integration/infrastructure/datasources/learning-content/skill-datasource_test.js +++ /dev/null @@ -1,275 +0,0 @@ -import _ from 'lodash'; - -import { skillDatasource } from '../../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; -import { expect, mockLearningContent } from '../../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | LearningContent | SkillDatasource', function () { - describe('#findOperativeByRecordIds', function () { - it('should return an array of skill data objects', async function () { - // given - const rawSkill1 = { id: 'recSkill1', status: 'actif' }; - const rawSkill2 = { id: 'recSkill2', status: 'archivé' }; - const rawSkill3 = { id: 'recSkill3', status: 'actif' }; - const rawSkill4 = { id: 'recSkill4', status: 'périmé' }; - - const records = [rawSkill1, rawSkill2, rawSkill3, rawSkill4]; - await mockLearningContent({ skills: records }); - - // when - const foundSkills = await skillDatasource.findOperativeByRecordIds([rawSkill1.id, rawSkill2.id, rawSkill4.id]); - - // then - expect(foundSkills).to.be.an('array'); - expect(_.map(foundSkills, 'id')).to.deep.equal([rawSkill1.id, rawSkill2.id]); - }); - }); - - describe('#findByRecordIds', function () { - it('should return an array of skill data objects', async function () { - // given - const rawSkill1 = { id: 'recSkill1', status: 'actif' }; - const rawSkill2 = { id: 'recSkill2', status: 'archivé' }; - const rawSkill3 = { id: 'recSkill3', status: 'actif' }; - const rawSkill4 = { id: 'recSkill4', status: 'périmé' }; - - const records = [rawSkill1, rawSkill2, rawSkill3, rawSkill4]; - await mockLearningContent({ skills: records }); - - // when - const foundSkills = await skillDatasource.findByRecordIds([rawSkill1.id, rawSkill2.id, rawSkill4.id]); - - // then - expect(foundSkills).to.be.an('array'); - expect(_.map(foundSkills, 'id')).to.deep.equal([rawSkill1.id, rawSkill2.id, rawSkill4.id]); - }); - }); - - describe('#findAllSkillsByNameForPix1d', function () { - it('should return the corresponding skills', async function () { - // given - const rawSkill1 = { id: 'recSkill1', name: '@rechercher_didacticiel1', status: 'actif' }; - const rawSkill2 = { id: 'recSkill2', name: '@rechercher_didacticiel1', status: 'actif' }; - const rawSkill3 = { id: 'recSkill3', name: '@rechercher_entrainement1', status: 'en construction' }; - const rawSkill4 = { id: 'recSkill4', name: '@rechercher_didacticiel2', status: 'actif' }; - const rawSkill5 = { id: 'recSkill5', name: '@rechercher_didacticiel12', status: 'en construction' }; - await mockLearningContent({ skills: [rawSkill1, rawSkill2, rawSkill3, rawSkill4, rawSkill5] }); - - // when - const result = await skillDatasource.findAllSkillsByNameForPix1d('@rechercher_didacticiel1'); - - // then - expect(result).to.deep.equal([rawSkill1, rawSkill2]); - }); - - it('should return the skills with active or building status ', async function () { - // given - const rawSkill1 = { id: 'recSkill1', name: '@rechercher_didacticiel1', status: 'actif' }; - const rawSkill2 = { id: 'recSkill2', name: '@rechercher_didacticiel1', status: 'en construction' }; - const rawSkill3 = { id: 'recSkill3', name: '@rechercher_didacticiel1', status: 'archivé' }; - await mockLearningContent({ skills: [rawSkill1, rawSkill2, rawSkill3] }); - - // when - const result = await skillDatasource.findAllSkillsByNameForPix1d('@rechercher_didacticiel1'); - - // then - expect(result).to.deep.equal([rawSkill1, rawSkill2]); - }); - - context('when there is no skill found', function () { - it('should return an empty array', async function () { - // given - await mockLearningContent({ skills: [] }); - - // when - const result = await skillDatasource.findAllSkillsByNameForPix1d('@rechercher_validation'); - - // then - expect(result).to.deep.equal([]); - }); - }); - }); - - describe('#findActive', function () { - it('should resolve an array of Skills from LCMS', async function () { - // given - const rawSkill1 = { id: 'recSkill1', status: 'actif' }, - rawSkill2 = { id: 'recSkill2', status: 'actif' }; - await mockLearningContent({ skills: [rawSkill1, rawSkill2] }); - - // when - const foundSkills = await skillDatasource.findActive(); - - // then - expect(_.map(foundSkills, 'id')).to.deep.equal([rawSkill1.id, rawSkill2.id]); - }); - - it('should resolve an array of Skills with only activated Skillfrom learning content', async function () { - // given - const rawSkill1 = { id: 'recSkill1', status: 'actif' }, - rawSkill2 = { id: 'recSkill2', status: 'actif' }, - rawSkill3 = { id: 'recSkill3', status: 'périmé' }; - await mockLearningContent({ skills: [rawSkill1, rawSkill2, rawSkill3] }); - - // when - const foundSkills = await skillDatasource.findActive(); - - // then - expect(_.map(foundSkills, 'id')).to.deep.equal([rawSkill1.id, rawSkill2.id]); - }); - }); - - describe('#findOperative', function () { - it('should resolve an array of Skills from learning content', async function () { - // given - const rawSkill1 = { id: 'recSkill1', status: 'actif' }, - rawSkill2 = { id: 'recSkill2', status: 'actif' }; - await mockLearningContent({ skills: [rawSkill1, rawSkill2] }); - - // when - const foundSkills = await skillDatasource.findOperative(); - - // then - expect(_.map(foundSkills, 'id')).to.deep.equal([rawSkill1.id, rawSkill2.id]); - }); - - it('should resolve an array of Skills with only activated Skillfrom learning content', async function () { - // given - const rawSkill1 = { id: 'recSkill1', status: 'actif' }, - rawSkill2 = { id: 'recSkill2', status: 'archivé' }, - rawSkill3 = { id: 'recSkill3', status: 'périmé' }; - await mockLearningContent({ skills: [rawSkill1, rawSkill2, rawSkill3] }); - - // when - const foundSkills = await skillDatasource.findOperative(); - - // then - expect(_.map(foundSkills, 'id')).to.deep.equal([rawSkill1.id, rawSkill2.id]); - }); - }); - - describe('#findActiveByCompetenceId', function () { - beforeEach(async function () { - const skill1 = { id: 'recSkill1', status: 'actif', competenceId: 'recCompetence' }; - const skill2 = { id: 'recSkill2', status: 'actif', competenceId: 'recCompetence' }; - const skill3 = { id: 'recSkill3', status: 'périmé', competenceId: 'recCompetence' }; - const skill4 = { id: 'recSkill4', status: 'actif', competenceId: 'recOtherCompetence' }; - await mockLearningContent({ skills: [skill1, skill2, skill3, skill4] }); - }); - - it('should retrieve all skills from learning content for one competence', async function () { - // when - const skills = await skillDatasource.findActiveByCompetenceId('recCompetence'); - - // then - expect(_.map(skills, 'id')).to.have.members(['recSkill1', 'recSkill2']); - }); - }); - - describe('#findActiveByTubeId', function () { - beforeEach(async function () { - const skill1 = { id: 'recSkill1', status: 'actif', competenceId: 'recCompetence', tubeId: 'recTube' }; - const skill2 = { id: 'recSkill2', status: 'actif', competenceId: 'recCompetence', tubeId: 'recTube' }; - const skill3 = { id: 'recSkill3', status: 'périmé', competenceId: 'recCompetence', tubeId: 'recTube' }; - const skill4 = { id: 'recSkill4', status: 'actif', competenceId: 'recOtherCompetence', tubeId: 'recOtherTube' }; - await mockLearningContent({ skills: [skill1, skill2, skill3, skill4] }); - }); - - it('should retrieve all skills from learning content for one competence', async function () { - // when - const skills = await skillDatasource.findActiveByTubeId('recTube'); - - // then - expect(_.map(skills, 'id')).to.have.members(['recSkill1', 'recSkill2']); - }); - }); - - describe('#findByTubeIdFor1d', function () { - beforeEach(async function () { - const skillActive = { id: 'recSkillActive', status: 'actif', competenceId: 'recCompetence', tubeId: 'recTube' }; - const skillInBuild = { - id: 'recSkillInBuild', - status: 'en construction', - competenceId: 'recCompetence', - tubeId: 'recTube', - }; - const skillExpired = { - id: 'recSkillExpired', - status: 'périmé', - competenceId: 'recCompetence', - tubeId: 'recTube', - }; - const skillOtherTube = { - id: 'recSkillOtherTube', - status: 'actif', - competenceId: 'recOtherCompetence', - tubeId: 'recOtherTube', - }; - await mockLearningContent({ skills: [skillActive, skillInBuild, skillExpired, skillOtherTube] }); - }); - - it('should retrieve all skills from learning content for one competence', async function () { - // when - const skills = await skillDatasource.findByTubeIdFor1d('recTube'); - - // then - expect(_.map(skills, 'id')).to.have.members(['recSkillActive', 'recSkillInBuild']); - }); - }); - - describe('#findOperativeByCompetenceId', function () { - beforeEach(async function () { - const skill1 = { id: 'recSkill1', status: 'actif', competenceId: 'recCompetence' }; - const skill2 = { id: 'recSkill2', status: 'archivé', competenceId: 'recCompetence' }; - const skill3 = { id: 'recSkill3', status: 'périmé', competenceId: 'recCompetence' }; - const skill4 = { id: 'recSkill4', status: 'actif', competenceId: 'recOtherCompetence' }; - await mockLearningContent({ skills: [skill1, skill2, skill3, skill4] }); - }); - - it('should retrieve all skills from learning content for one competence', async function () { - // when - const skills = await skillDatasource.findOperativeByCompetenceId('recCompetence'); - - // then - expect(_.map(skills, 'id')).to.have.members(['recSkill1', 'recSkill2']); - }); - }); - - describe('#findOperativeByCompetenceIds', function () { - beforeEach(async function () { - const skill1 = { id: 'recSkill1', status: 'actif', competenceId: 'recCompetence1' }; - const skill2 = { id: 'recSkill2', status: 'archivé', competenceId: 'recCompetence1' }; - const skill3 = { id: 'recSkill3', status: 'périmé', competenceId: 'recCompetence1' }; - const skill4 = { id: 'recSkill4', status: 'actif', competenceId: 'recOtherCompetence1' }; - const skill5 = { id: 'recSkill5', status: 'actif', competenceId: 'recCompetence2' }; - const skill6 = { id: 'recSkill6', status: 'archivé', competenceId: 'recCompetence2' }; - const skill7 = { id: 'recSkill7', status: 'périmé', competenceId: 'recCompetence2' }; - await mockLearningContent({ skills: [skill1, skill2, skill3, skill4, skill5, skill6, skill7] }); - }); - - it('should retrieve all skills from learning content for competences', async function () { - // when - const skills = await skillDatasource.findOperativeByCompetenceIds(['recCompetence1', 'recCompetence2']); - - // then - expect(_.map(skills, 'id')).to.have.members(['recSkill1', 'recSkill2', 'recSkill5', 'recSkill6']); - }); - }); - - describe('#findOperativeByTubeId', function () { - beforeEach(async function () { - const skill1 = { id: 'recSkill1', status: 'actif', tubeId: 'recTube' }; - const skill2 = { id: 'recSkill2', status: 'archivé', tubeId: 'recTube' }; - const skill3 = { id: 'recSkill3', status: 'périmé', tubeId: 'recTube' }; - const skill4 = { id: 'recSkill4', status: 'actif', tubeId: 'recOtherTube' }; - await mockLearningContent({ skills: [skill1, skill2, skill3, skill4] }); - }); - - it('should retrieve all operative skills from learning content for one tube', async function () { - // when - const skills = await skillDatasource.findOperativeByTubeId('recTube'); - - // then - expect(_.map(skills, 'id')).to.have.members(['recSkill1', 'recSkill2']); - }); - }); -}); diff --git a/api/tests/shared/integration/infrastructure/repositories/challenge-repository_test.js b/api/tests/shared/integration/infrastructure/repositories/challenge-repository_test.js index 52b68fbe223..ec0ebe488ff 100644 --- a/api/tests/shared/integration/infrastructure/repositories/challenge-repository_test.js +++ b/api/tests/shared/integration/infrastructure/repositories/challenge-repository_test.js @@ -1,798 +1,1957 @@ -import _ from 'lodash'; - -import { Validator } from '../../../../../src/evaluation/domain/models/Validator.js'; +import { ValidatorQCM } from '../../../../../src/evaluation/domain/models/ValidatorQCM.js'; +import { ValidatorQCU } from '../../../../../src/evaluation/domain/models/ValidatorQCU.js'; +import { config } from '../../../../../src/shared/config.js'; import { NotFoundError } from '../../../../../src/shared/domain/errors.js'; -import { Challenge } from '../../../../../src/shared/domain/models/Challenge.js'; import * as challengeRepository from '../../../../../src/shared/infrastructure/repositories/challenge-repository.js'; -import { catchErr, domainBuilder, expect, mockLearningContent, nock } from '../../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect, knex, nock } from '../../../../test-helper.js'; describe('Integration | Repository | challenge-repository', function () { - describe('#get', function () { - it('should return the challenge with skill', async function () { - // given - const challengeId = 'recCHAL1'; - - const skill = _buildSkill({ id: 'recSkill1' }); - - const challenge = _buildChallenge({ id: challengeId, skill }); + const challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson = { + id: 'challengeId00', + instruction: 'instruction challengeId00', + alternativeInstruction: 'alternativeInstruction challengeId00', + proposals: 'proposals challengeId00', + type: 'QCU', + solution: 'solution challengeId00', + solutionToDisplay: 'solutionToDisplay challengeId00', + t1Status: true, + t2Status: false, + t3Status: true, + status: 'validé', + genealogy: 'genealogy challengeId00', + accessibility1: 'accessibility1 challengeId00', + accessibility2: 'accessibility2 challengeId00', + requireGafamWebsiteAccess: true, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId00', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId00', + illustrationUrl: 'illustrationUrl challengeId00', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive challengeId00', + alpha: 1.1, + delta: 3.3, + autoReply: true, + focusable: true, + format: 'format challengeId00', + timer: 180, + embedHeight: 800, + embedUrl: 'embedUrl challengeId00', + embedTitle: 'embedTitle challengeId00', + locales: ['fr', 'nl'], + competenceId: 'competenceId00', + skillId: 'skillId00', + }; + const challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson = { + id: 'challengeId01', + instruction: 'instruction challengeId01', + alternativeInstruction: 'alternativeInstruction challengeId01', + proposals: 'proposals challengeId01', + type: 'QCU', + solution: 'solution challengeId01', + solutionToDisplay: 'solutionToDisplay challengeId01', + t1Status: false, + t2Status: true, + t3Status: true, + status: 'validé', + genealogy: 'genealogy challengeId01', + accessibility1: 'accessibility1 challengeId01', + accessibility2: 'accessibility2 challengeId01', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId01', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 20, + shuffled: false, + illustrationAlt: 'illustrationAlt challengeId01', + illustrationUrl: 'illustrationUrl challengeId01', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive challengeId01', + alpha: 1.2, + delta: 3.4, + autoReply: true, + focusable: false, + format: 'format challengeId01', + timer: 180, + embedHeight: 801, + embedUrl: 'https://example.com/embed.json', + embedTitle: 'embedTitle challengeId01', + locales: ['fr', 'en'], + competenceId: 'competenceId00', + skillId: 'skillId00', + }; + const challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson = { + id: 'challengeId02', + instruction: 'instruction challengeId02', + alternativeInstruction: 'alternativeInstruction challengeId02', + proposals: 'proposals challengeId02', + type: 'QCM', + solution: 'solution challengeId02', + solutionToDisplay: 'solutionToDisplay challengeId02', + t1Status: false, + t2Status: true, + t3Status: false, + status: 'archivé', + genealogy: 'genealogy challengeId02', + accessibility1: 'accessibility1 challengeId02', + accessibility2: 'accessibility2 challengeId02', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId02', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 30, + shuffled: false, + illustrationAlt: 'illustrationAlt challengeId02', + illustrationUrl: 'illustrationUrl challengeId02', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive challengeId02', + alpha: 1.3, + delta: 3.5, + autoReply: false, + focusable: false, + format: 'format challengeId02', + timer: null, + embedHeight: 802, + embedUrl: 'embed url challengeId02', + embedTitle: 'embedTitle challengeId02', + locales: ['en'], + competenceId: 'competenceId00', + skillId: 'skillId00', + }; + const challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson = { + id: 'challengeId03', + instruction: 'instruction challengeId03', + alternativeInstruction: 'alternativeInstruction challengeId03', + proposals: 'proposals challengeId03', + type: 'QCM', + solution: 'solution challengeId03', + solutionToDisplay: 'solutionToDisplay challengeId03', + t1Status: true, + t2Status: true, + t3Status: false, + status: 'validé', + genealogy: 'genealogy challengeId03', + accessibility1: 'accessibility1 challengeId03', + accessibility2: 'accessibility2 challengeId03', + requireGafamWebsiteAccess: true, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId03', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 40, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId03', + illustrationUrl: 'illustrationUrl challengeId03', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive challengeId03', + alpha: 1.4, + delta: 3.6, + autoReply: true, + focusable: false, + format: 'format challengeId03', + timer: null, + embedHeight: 803, + embedUrl: 'embed url challengeId03', + embedTitle: 'embedTitle challengeId03', + locales: ['nl'], + competenceId: 'competenceId00', + skillId: 'skillId00', + }; + const skillData00_tube00competence00_actif = { + id: 'skillId00', + name: 'name skillId00', + status: 'actif', + pixValue: 2.9, + version: 5, + level: 2, + hintStatus: 'hintStatus Acquis 0', + competenceId: 'competenceId00', + tubeId: 'tubeId00', + tutorialIds: [], + learningMoreTutorialIds: [], + hint_i18n: { fr: 'hint FR skillId00', en: 'hint EN skillId00' }, + }; + const challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson = { + id: 'challengeId04', + instruction: 'instruction challengeId04', + alternativeInstruction: 'alternativeInstruction challengeId04', + proposals: 'proposals challengeId04', + type: 'QCU', + solution: 'solution challengeId04', + solutionToDisplay: 'solutionToDisplay challengeId04', + t1Status: true, + t2Status: false, + t3Status: false, + status: 'validé', + genealogy: 'genealogy challengeId04', + accessibility1: 'accessibility1 challengeId04', + accessibility2: 'accessibility2 challengeId04', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId04', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: false, + illustrationAlt: 'illustrationAlt challengeId04', + illustrationUrl: 'illustrationUrl challengeId04', + attachments: ['attachment1'], + responsive: 'responsive challengeId04', + alpha: 1.5, + delta: 3.7, + autoReply: true, + focusable: false, + format: 'format challengeId04', + timer: 555, + embedHeight: 804, + embedUrl: 'embedUrl challengeId04', + embedTitle: 'embedTitle challengeId04', + locales: ['en', 'nl'], + competenceId: 'competenceId00', + skillId: 'skillId01', + }; + const skillData01_tube01competence00_actif = { + id: 'skillId01', + name: 'name skillId01', + status: 'actif', + pixValue: 3.9, + version: 5, + level: 1, + hintStatus: 'hintStatus Acquis 1', + competenceId: 'competenceId00', + tubeId: 'tubeId01', + tutorialIds: [], + learningMoreTutorialIds: [], + hint_i18n: { fr: 'hint FR skillId01', en: 'hint EN skillId01' }, + }; + const challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson = { + id: 'challengeId05', + instruction: 'instruction challengeId05', + alternativeInstruction: 'alternativeInstruction challengeId05', + proposals: 'proposals challengeId05', + type: 'QCM', + solution: 'solution challengeId05', + solutionToDisplay: 'solutionToDisplay challengeId05', + t1Status: true, + t2Status: false, + t3Status: true, + status: 'périmé', + genealogy: 'genealogy challengeId05', + accessibility1: 'accessibility1 challengeId05', + accessibility2: 'accessibility2 challengeId05', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId05', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId05', + illustrationUrl: 'illustrationUrl challengeId05', + attachments: ['attachment1'], + responsive: 'responsive challengeId05', + alpha: 1.6, + delta: 3.8, + autoReply: true, + focusable: true, + format: 'format challengeId05', + timer: null, + embedHeight: 805, + embedUrl: 'embedUrl challengeId05', + embedTitle: 'embedTitle challengeId05', + locales: ['en', 'fr'], + competenceId: 'competenceId01', + skillId: 'skillId02', + }; + const challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson = { + id: 'challengeId06', + instruction: 'instruction challengeId06', + alternativeInstruction: 'alternativeInstruction challengeId06', + proposals: 'proposals challengeId06', + type: 'QCM', + solution: 'solution challengeId06', + solutionToDisplay: 'solutionToDisplay challengeId06', + t1Status: true, + t2Status: false, + t3Status: true, + status: 'périmé', + genealogy: 'genealogy challengeId06', + accessibility1: 'accessibility1 challengeId06', + accessibility2: 'accessibility2 challengeId06', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId06', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId06', + illustrationUrl: 'illustrationUrl challengeId06', + attachments: ['attachment1'], + responsive: 'responsive challengeId06', + alpha: null, + delta: 3.8, + autoReply: true, + focusable: true, + format: 'format challengeId06', + timer: null, + embedHeight: 806, + embedUrl: 'embedUrl challengeId06', + embedTitle: 'embedTitle challengeId06', + locales: ['en', 'fr'], + competenceId: 'competenceId01', + skillId: 'skillId02', + }; + const skillData02_tube02competence01_perime = { + id: 'skillId02', + name: 'name skillId02', + status: 'périmé', + pixValue: 2, + version: 1, + level: 3, + hintStatus: 'hintStatus Acquis 2', + competenceId: 'competenceId01', + tubeId: 'tubeId02', + tutorialIds: [], + learningMoreTutorialIds: [], + hint_i18n: { fr: 'hint FR skillId02', en: 'hint EN skillId02' }, + }; + const challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson = { + id: 'challengeId07', + instruction: 'instruction challengeId07', + alternativeInstruction: 'alternativeInstruction challengeId07', + proposals: 'proposals challengeId07', + type: 'QCM', + solution: 'solution challengeId07', + solutionToDisplay: 'solutionToDisplay challengeId07', + t1Status: true, + t2Status: true, + t3Status: true, + status: 'validé', + genealogy: 'genealogy challengeId07', + accessibility1: 'accessibility1 challengeId07', + accessibility2: 'accessibility2 challengeId07', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId07', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId07', + illustrationUrl: 'illustrationUrl challengeId07', + attachments: ['attachment1'], + responsive: 'responsive challengeId07', + alpha: 0.8, + delta: null, + autoReply: false, + focusable: true, + format: 'format challengeId07', + timer: null, + embedHeight: null, + embedUrl: 'embedUrl challengeId07', + embedTitle: 'embedTitle challengeId07', + locales: ['nl', 'fr'], + competenceId: 'competenceId01', + skillId: 'skillId03', + }; + const challengeData08_skill03_qcu_archive_notFlashCompatible_fr_noEmbedJson = { + id: 'challengeId08', + instruction: 'instruction challengeId08', + alternativeInstruction: 'alternativeInstruction challengeId08', + proposals: 'proposals challengeId08', + type: 'QCU', + solution: 'solution challengeId08', + solutionToDisplay: 'solutionToDisplay challengeId08', + t1Status: false, + t2Status: false, + t3Status: true, + status: 'archivé', + genealogy: 'genealogy challengeId08', + accessibility1: 'accessibility1 challengeId08', + accessibility2: 'accessibility2 challengeId08', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId08', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId08', + illustrationUrl: 'illustrationUrl challengeId08', + attachments: ['attachment1'], + responsive: 'responsive challengeId08', + alpha: null, + delta: null, + autoReply: false, + focusable: true, + format: 'format challengeId08', + timer: null, + embedHeight: null, + embedUrl: 'embedUrl challengeId08', + embedTitle: 'embedTitle challengeId08', + locales: ['fr'], + competenceId: 'competenceId01', + skillId: 'skillId03', + }; + const challengeData09_skill03_qcu_archive_flashCompatible_fr_noEmbedJson = { + id: 'challengeId09', + instruction: 'instruction challengeId09', + alternativeInstruction: 'alternativeInstruction challengeId09', + proposals: 'proposals challengeId09', + type: 'QCU', + solution: 'solution challengeId09', + solutionToDisplay: 'solutionToDisplay challengeId09', + t1Status: false, + t2Status: false, + t3Status: false, + status: 'archivé', + genealogy: 'genealogy challengeId09', + accessibility1: 'accessibility1 challengeId09', + accessibility2: 'accessibility2 challengeId09', + requireGafamWebsiteAccess: true, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing challengeId09', + isAwarenessChallenge: true, + toRephrase: false, + alternativeVersion: 10, + shuffled: true, + illustrationAlt: 'illustrationAlt challengeId09', + illustrationUrl: 'illustrationUrl challengeId09', + attachments: ['attachment1'], + responsive: 'responsive challengeId09', + alpha: 1.0, + delta: 2.0, + autoReply: true, + focusable: true, + format: 'format challengeId09', + timer: null, + embedHeight: null, + embedUrl: 'embedUrl challengeId09', + embedTitle: 'embedTitle challengeId09', + locales: ['fr'], + competenceId: 'competenceId01', + skillId: 'skillId03', + }; + const skillData03_tube02competence01_actif = { + id: 'skillId03', + name: 'name skillId03', + status: 'actif', + pixValue: 5, + version: 8, + level: 7, + hintStatus: 'hintStatus Acquis 3', + competenceId: 'competenceId01', + tubeId: 'tubeId03', + tutorialIds: [], + learningMoreTutorialIds: [], + hint_i18n: { fr: 'hint FR skillId03', en: 'hint EN skillId03' }, + }; - const learningContent = { - skills: [{ ...skill, status: 'actif' }], - challenges: [{ ...challenge, skillId: 'recSkill1', alpha: 1, delta: 0 }], - }; + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildSkill(skillData00_tube00competence00_actif); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData01_tube01competence00_actif); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData02_tube02competence01_perime); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData03_tube02competence01_actif); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData08_skill03_qcu_archive_notFlashCompatible_fr_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData09_skill03_qcu_archive_flashCompatible_fr_noEmbedJson, + ); + + await databaseBuilder.commit(); + }); - await mockLearningContent(learningContent); + describe('#get', function () { + context('when no challenge found for id', function () { + it('should throw a NotFound error', async function () { + // when + const err = await catchErr(challengeRepository.get)('challengeIdPipeauPipette'); - const expectedChallenge = domainBuilder.buildChallenge({ - ...challenge, - focused: challenge.focusable, - skill: domainBuilder.buildSkill({ ...skill, difficulty: skill.level }), - blindnessCompatibility: challenge.accessibility1, - colorBlindnessCompatibility: challenge.accessibility2, + // then + expect(err).to.be.instanceOf(NotFoundError); }); - - // when - const actualChallenge = await challengeRepository.get(challengeId); - - // then - expect(actualChallenge).to.be.instanceOf(Challenge); - expect(_.omit(actualChallenge, 'validator')).to.deep.equal(_.omit(expectedChallenge, 'validator')); - }); - - it('should setup the expected validator and solution', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const challenge = domainBuilder.buildChallenge({ type: Challenge.Type.QCM, skill }); - - const learningContent = { - skills: [{ ...skill, status: 'actif' }], - challenges: [{ ...challenge, skillId: 'recSkill1', t1Status: true, t2Status: true, t3Status: false }], - }; - await mockLearningContent(learningContent); - - // when - const actualChallenge = await challengeRepository.get(challenge.id); - - // then - expect(actualChallenge.validator).to.be.instanceOf(Validator); - expect(actualChallenge.validator.solution.id).to.equal(challenge.id); - expect(actualChallenge.validator.solution.isT1Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT2Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT3Enabled).to.equal(false); - expect(actualChallenge.validator.solution.type).to.equal(challenge.type); - expect(actualChallenge.validator.solution.value).to.equal(challenge.solution); }); - describe('when has a web component embedURL', function () { - it('should add webComponentTagName and webComponentProps', async function () { - // given - const webComponentServerCall = nock('https://example.com') - .get('/embed.json') - .reply(200, JSON.stringify({ name: 'web-component', props: { prop1: 'value1', prop2: 'value2' } })); - const challengeId = 'recCHAL1'; - - const skill = _buildSkill({ id: 'recSkill1' }); - - const challenge = _buildChallenge({ id: challengeId, skill }); - challenge.embedUrl = 'https://example.com/embed.json'; - - const learningContent = { - skills: [{ ...skill, status: 'actif' }], - challenges: [{ ...challenge, skillId: 'recSkill1', alpha: 1, delta: 0 }], - }; - - await mockLearningContent(learningContent); - - const expectedChallenge = domainBuilder.buildChallengeWithWebComponent({ - ...challenge, - webComponentTagName: 'web-component', - webComponentProps: { prop1: 'value1', prop2: 'value2' }, - focused: challenge.focusable, - skill: domainBuilder.buildSkill({ ...skill, difficulty: skill.level }), - blindnessCompatibility: challenge.accessibility1, - colorBlindnessCompatibility: challenge.accessibility2, + context('when challenge found for id', function () { + context('when the challenge has an embed as webcomponent', function () { + context('when retrieving the resource successfully', function () { + it('should return the challenge with the loaded web component', async function () { + // given + const webComponentServerCall = nock('https://example.com') + .get('/embed.json') + .reply(200, JSON.stringify({ name: 'web-component', props: { prop1: 'value1', prop2: 'value2' } })); + + // when + const challenge = await challengeRepository.get('challengeId01'); + + // then + expect(webComponentServerCall.isDone()).to.equal(true); + expect(challenge).to.deepEqualInstance( + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + webComponentTagName: 'web-component', + webComponentProps: { prop1: 'value1', prop2: 'value2' }, + }), + ); + }); + }); + context('when we fail retrieving the resource', function () { + it('should throw a NotFound error', async function () { + // given + const webComponentServerCall = nock('https://example.com').get('/embed.json').reply(404); + + // when + const err = await catchErr(challengeRepository.get)('challengeId01'); + + // then + expect(webComponentServerCall.isDone()).to.equal(true); + expect(err).to.be.instanceOf(NotFoundError); + expect(err.message).to.equal( + `Embed webcomponent config with URL ${challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.embedUrl} in challenge ${challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id} not found`, + ); + }); }); - - // when - const actualChallenge = await challengeRepository.get(challengeId); - - // then - expect(actualChallenge).to.be.instanceOf(Challenge); - expect(webComponentServerCall.isDone()).to.equal(true); - expect(_.omit(actualChallenge, 'validator')).to.deep.equal(_.omit(expectedChallenge, 'validator')); }); + context('when the challenge has no embed as webcomponent', function () { + it('should return the challenge', async function () { + // when + const challenge = await challengeRepository.get('challengeId00'); - describe('when .json file is not found', function () { - it('should throw a NotFoundError', async function () { - // given - const webComponentServerCall = nock('https://example.com').get('/embed.json').reply(404); - const challengeId = 'recCHAL1'; - - const skill = _buildSkill({ id: 'recSkill1' }); - - const challenge = _buildChallenge({ id: challengeId, skill }); - challenge.embedUrl = 'https://example.com/embed.json'; - - const learningContent = { - skills: [{ ...skill, status: 'actif' }], - challenges: [ - { - ...challenge, - skillId: 'recSkill1', - alpha: 1, - delta: 0, - blindnessCompatibility: challenge.accessibility1, - colorBlindnessCompatibility: challenge.accessibility2, - }, - ], - }; - - await mockLearningContent(learningContent); - + // then + expect(challenge).to.deepEqualInstance( + domainBuilder.buildChallenge({ + ...challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson, + blindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility2, + focused: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.focusable, + discriminant: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.alpha, + difficulty: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.id, + type: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.type, + value: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.solution, + isT1Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t1Status, + isT2Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t2Status, + isT3Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ); + }); + }); + context('when asking a challenge "for correction"', function () { + it('should return a dedicated DTO for correction', async function () { // when - const error = await catchErr(challengeRepository.get)(challengeId); + const challengeForCorrection = await challengeRepository.get('challengeId00', { forCorrection: true }); // then - expect(webComponentServerCall.isDone()).to.equal(true); - expect(error).to.be.instanceOf(NotFoundError); + expect(challengeForCorrection).to.deep.equal({ + id: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.id, + skillId: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.skillId, + type: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.type, + solution: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.solution, + solutionToDisplay: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.solutionToDisplay, + proposals: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.proposals, + t1Status: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t1Status, + t2Status: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t2Status, + t3Status: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t3Status, + }); }); }); }); }); describe('#getMany', function () { - it('should return the challenges by their id and locale', async function () { - const skill1 = domainBuilder.buildSkill({ id: 'recSkill1' }); - const challenge1 = domainBuilder.buildChallenge({ id: 'recChal1', skill: skill1, locales: ['fr'] }); - const skill2 = domainBuilder.buildSkill({ id: 'recSkill2' }); - const challenge2 = domainBuilder.buildChallenge({ id: 'recChal2', skill: skill2, locales: ['fr'] }); - const skill3 = domainBuilder.buildSkill({ id: 'recSkill3' }); - const challenge3 = domainBuilder.buildChallenge({ id: 'recChal3', skill: skill3, locales: ['fr'] }); - const skill4 = domainBuilder.buildSkill({ id: 'recSkill4' }); - const challenge4 = domainBuilder.buildChallenge({ id: 'recChal4', skill: skill4, locales: ['en'] }); - - const learningContent = { - skills: [ - { ...skill1, level: skill1.difficulty }, - { ...skill2, level: skill2.difficulty }, - { ...skill3, level: skill3.difficulty }, - { ...skill4, level: skill4.difficulty }, - ], - challenges: [ - { ...challenge1, skillId: 'recSkill1' }, - { ...challenge2, skillId: 'recSkill2' }, - { ...challenge3, skillId: 'recSkill3' }, - { ...challenge4, skillId: 'recSkill4' }, - ], - }; - await mockLearningContent(learningContent); + context('when no locale provided', function () { + context('when at least one challenge is not found amongst the provided ids', function () { + it('should throw a NotFound error', async function () { + // when + const err = await catchErr(challengeRepository.getMany)(['challengeIdPipeauPipette', 'challengeId00']); - // when - const actualChallenges = await challengeRepository.getMany(['recChal1', 'recChal2', 'recChal4'], 'fr'); + // then + expect(err).to.be.instanceOf(NotFoundError); + }); + }); + context('when all challenges are found', function () { + it('should return the challenges', async function () { + // when + const challenges = await challengeRepository.getMany(['challengeId02', 'challengeId00']); - // then - const actualChallenge1 = _.find(actualChallenges, { skill: skill1, id: 'recChal1' }); - const actualChallenge2 = _.find(actualChallenges, { skill: skill2, id: 'recChal2' }); - expect(actualChallenges).to.have.lengthOf(2); - expect(Boolean(actualChallenge1)).to.be.true; - expect(Boolean(actualChallenge2)).to.be.true; + // then + expect(challenges).to.deepEqualArray([ + domainBuilder.buildChallenge({ + ...challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson, + blindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility2, + focused: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.focusable, + discriminant: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.alpha, + difficulty: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.id, + type: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.type, + value: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.solution, + isT1Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t1Status, + isT2Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t2Status, + isT3Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); + context('when locale is provided', function () { + context('when at least one challenge is not found amongst the provided ids', function () { + it('should throw a NotFound error', async function () { + // when + const err = await catchErr(challengeRepository.getMany)(['challengeIdPipeauPipette', 'challengeId00']); - it('should throw a NotFoundError error when resource not found', async function () { - const skill1 = domainBuilder.buildSkill({ id: 'recSkill1' }); - const challenge1 = domainBuilder.buildChallenge({ id: 'recChal1', skill: skill1 }); - const skill2 = domainBuilder.buildSkill({ id: 'recSkill2' }); - const challenge2 = domainBuilder.buildChallenge({ id: 'recChal2', skill: skill2 }); - const skill3 = domainBuilder.buildSkill({ id: 'recSkill3' }); - const challenge3 = domainBuilder.buildChallenge({ id: 'recChal3', skill: skill3 }); - const learningContent = { - skills: [ - { ...skill1, level: skill1.difficulty }, - { ...skill2, level: skill2.difficulty }, - { ...skill3, level: skill3.difficulty }, - ], - challenges: [ - { ...challenge1, skillId: 'recSkill1' }, - { ...challenge2, skillId: 'recSkill2' }, - { ...challenge3, skillId: 'recSkill3' }, - ], - }; - await mockLearningContent(learningContent); - - // when - const error = await catchErr(challengeRepository.getMany)(['someChallengeId'], 'fr'); + // then + expect(err).to.be.instanceOf(NotFoundError); + }); + }); + context('when all challenges are found', function () { + it('should return only the challenges for given locale', async function () { + // when + const challenges = await challengeRepository.getMany( + ['challengeId02', 'challengeId00', 'challengeId01'], + 'en', + ); - // then - expect(error).to.be.instanceOf(NotFoundError); + // then + expect(challenges).to.deepEqualArray([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); - describe('#list', function () { - it('should return all the challenges', async function () { - // given - const skill1 = domainBuilder.buildSkill({ id: 'recSkill1' }); - const challenge1 = domainBuilder.buildChallenge({ id: 'recChal1', skill: skill1 }); - const skill2 = domainBuilder.buildSkill({ id: 'recSkill2' }); - const challenge2 = domainBuilder.buildChallenge({ id: 'recChal2', skill: skill2 }); - const skill3 = domainBuilder.buildSkill({ id: 'recSkill3' }); - const challenge3 = domainBuilder.buildChallenge({ id: 'recChal3', skill: skill3 }); - const skill4 = domainBuilder.buildSkill({ id: 'recSkill4' }); - const challenge4 = domainBuilder.buildChallenge({ id: 'recChal4', skill: skill4 }); - const learningContent = { - skills: [ - { ...skill1, level: skill1.difficulty }, - { ...skill2, level: skill2.difficulty }, - { ...skill3, level: skill3.difficulty }, - { ...skill4, level: skill4.difficulty }, - ], - challenges: [ - { ...challenge1, locales: ['fr'], skillId: 'recSkill1' }, - { ...challenge2, locales: ['fr'], skillId: 'recSkill2' }, - { ...challenge3, locales: ['fr'], skillId: 'recSkill3' }, - { ...challenge4, locales: ['fr-fr'], skillId: 'recSkill4' }, - ], - }; - await mockLearningContent(learningContent); + describe('list', function () { + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.list)(); - // when - const actualChallenges = await challengeRepository.list('fr'); + // then + expect(err.message).to.equal('Locale shall be defined'); + }); + }); + context('when locale is defined', function () { + context('when no challenges found for locale', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.list('catalan'); - // then - const actualChallenge1 = _.find(actualChallenges, { skill: skill1, id: 'recChal1', locales: ['fr'] }); - const actualChallenge2 = _.find(actualChallenges, { skill: skill2, id: 'recChal2', locales: ['fr'] }); - const actualChallenge3 = _.find(actualChallenges, { skill: skill3, id: 'recChal3', locales: ['fr'] }); - expect(actualChallenges).to.have.lengthOf(3); - expect(Boolean(actualChallenge1)).to.be.true; - expect(Boolean(actualChallenge2)).to.be.true; - expect(Boolean(actualChallenge3)).to.be.true; + // then + expect(challenges).to.deep.equal([]); + }); + }); + + context('when challenges found for locale', function () { + it('should return the challenges', async function () { + // when + const challenges = await challengeRepository.list('en'); + + // then + expect(challenges).to.deepEqualArray([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson, + blindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility2, + focused: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.focusable, + discriminant: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.alpha, + difficulty: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.id, + type: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.type, + value: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.solution, + isT1Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t1Status, + isT2Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t2Status, + isT3Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData01_tube01competence00_actif, + difficulty: skillData01_tube01competence00_actif.level, + hint: skillData01_tube01competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson, + blindnessCompatibility: + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.accessibility2, + focused: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.focusable, + discriminant: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.alpha, + difficulty: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.id, + type: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.type, + value: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.solution, + isT1Enabled: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.t1Status, + isT2Enabled: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.t2Status, + isT3Enabled: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData02_tube02competence01_perime, + difficulty: skillData02_tube02competence01_perime.level, + hint: skillData02_tube02competence01_perime.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson, + blindnessCompatibility: + challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.accessibility2, + focused: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.focusable, + discriminant: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.alpha, + difficulty: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.id, + type: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.type, + value: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.solution, + isT1Enabled: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.t1Status, + isT2Enabled: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.t2Status, + isT3Enabled: challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData02_tube02competence01_perime, + difficulty: skillData02_tube02competence01_perime.level, + hint: skillData02_tube02competence01_perime.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); describe('#findValidated', function () { - it('should return only validated challenges with skills', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const validatedChallenge = domainBuilder.buildChallenge({ id: 'recChallenge1', skill, status: 'validé' }); - const nonValidatedChallenge = domainBuilder.buildChallenge({ id: 'recChallenge2', skill, status: 'PAS validé' }); - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { ...validatedChallenge, skillId: 'recSkill1' }, - { ...nonValidatedChallenge, skillId: 'recSkill1' }, - ], - }; - await mockLearningContent(learningContent); - - // when - const actualChallenges = await challengeRepository.findValidated('fr'); + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.findValidated)(); - // then - expect(actualChallenges).to.have.lengthOf(1); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(_.omit(actualChallenges[0], 'validator')).to.deep.equal(_.omit(actualChallenges[0], 'validator')); + // then + expect(err.message).to.equal('Locale shall be defined'); + }); }); + context('when locale is defined', function () { + context('when no validated challenges found for given locale', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findValidated('catalan'); - it('should setup the expected validator and solution on found challenges', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const validatedChallenge = domainBuilder.buildChallenge({ - type: Challenge.Type.QCM, - skills: [skill], - status: 'validé', - }); - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { - ...validatedChallenge, - skillId: 'recSkill1', - t1Status: true, - t2Status: true, - t3Status: false, - }, - ], - }; - await mockLearningContent(learningContent); - - // when - const [actualChallenge] = await challengeRepository.findValidated('fr'); + // then + expect(challenges).to.deep.equal([]); + }); + }); + context('when validated challenges are found for given locale', function () { + it('should return the challenges', async function () { + // when + const challenges = await challengeRepository.findValidated('nl'); - // then - expect(actualChallenge.validator).to.be.instanceOf(Validator); - expect(actualChallenge.validator.solution.id).to.equal(validatedChallenge.id); - expect(actualChallenge.validator.solution.isT1Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT2Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT3Enabled).to.equal(false); - expect(actualChallenge.validator.solution.type).to.equal(validatedChallenge.type); - expect(actualChallenge.validator.solution.value).to.equal(validatedChallenge.solution); + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson, + blindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility2, + focused: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.focusable, + discriminant: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.alpha, + difficulty: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.id, + type: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.type, + value: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.solution, + isT1Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t1Status, + isT2Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t2Status, + isT3Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + blindnessCompatibility: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.accessibility2, + focused: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.focusable, + discriminant: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.alpha, + difficulty: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.id, + type: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.type, + value: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.solution, + isT1Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t1Status, + isT2Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t2Status, + isT3Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson, + blindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility2, + focused: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.focusable, + discriminant: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.alpha, + difficulty: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.id, + type: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.type, + value: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.solution, + isT1Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t1Status, + isT2Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t2Status, + isT3Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData01_tube01competence00_actif, + difficulty: skillData01_tube01competence00_actif.level, + hint: skillData01_tube01competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson, + blindnessCompatibility: + challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.accessibility2, + focused: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.focusable, + discriminant: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.alpha, + difficulty: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.id, + type: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.type, + value: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.solution, + isT1Enabled: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.t1Status, + isT2Enabled: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.t2Status, + isT3Enabled: challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData03_tube02competence01_actif, + difficulty: skillData03_tube02competence01_actif.level, + hint: skillData03_tube02competence01_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); describe('#findOperative', function () { - it('should return only french france operative challenges with skills', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const frfrOperativeChallenge = domainBuilder.buildChallenge({ id: 'recChallenge1', skill, locales: ['fr-fr'] }); - const nonFrfrOperativeChallenge = domainBuilder.buildChallenge({ id: 'recChallenge2', skill, locales: ['en'] }); - const locale = 'fr-fr'; - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { ...frfrOperativeChallenge, skillId: 'recSkill1' }, - { ...nonFrfrOperativeChallenge, skillId: 'recSkill1' }, - ], - }; - await mockLearningContent(learningContent); + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.findOperative)(); - // when - const actualChallenges = await challengeRepository.findOperative(locale); + // then + expect(err.message).to.equal('Locale shall be defined'); + }); + }); + context('when locale is defined', function () { + context('when no operative challenges found for given locale', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findOperative('catalan'); - // then - expect(actualChallenges).to.have.lengthOf(1); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(_.omit(actualChallenges[0], 'validator')).to.deep.equal(_.omit(actualChallenges[0], 'validator')); + // then + expect(challenges).to.deep.equal([]); + }); + }); + context('when operative challenges are found for given locale', function () { + it('should return the challenges', async function () { + // when + const challenges = await challengeRepository.findOperative('en'); + + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson, + blindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility2, + focused: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.focusable, + discriminant: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.alpha, + difficulty: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.id, + type: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.type, + value: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.solution, + isT1Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t1Status, + isT2Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t2Status, + isT3Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData01_tube01competence00_actif, + difficulty: skillData01_tube01competence00_actif.level, + hint: skillData01_tube01competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); describe('#findValidatedByCompetenceId', function () { - it('should return only validated challenges with skills', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const competenceId = 'recCompetenceId'; - const validatedChallenge = domainBuilder.buildChallenge({ - id: 'recChallenge1', - skill, - status: 'validé', - competenceId, - }); - const nonValidatedChallenge = domainBuilder.buildChallenge({ - id: 'recChallenge2', - skill, - status: 'PAS validé', - competenceId, - }); - const notInCompetenceValidatedChallenge = domainBuilder.buildChallenge({ - id: 'recChallenge3', - skills: [skill], - status: 'validé', - }); - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { ...validatedChallenge, skillId: 'recSkill1' }, - { ...nonValidatedChallenge, skillId: 'recSkill1' }, - { ...notInCompetenceValidatedChallenge, skillId: 'recSkill1' }, - ], - }; - await mockLearningContent(learningContent); - - // when - const actualChallenges = await challengeRepository.findValidatedByCompetenceId(competenceId, 'fr'); + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.findValidatedByCompetenceId)('competenceId00'); - // then - expect(actualChallenges).to.have.lengthOf(1); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(_.omit(actualChallenges[0], 'validator')).to.deep.equal(_.omit(actualChallenges[0], 'validator')); + // then + expect(err.message).to.equal('Locale shall be defined'); + }); }); + context('when locale is defined', function () { + context('when no validated challenges found for given locale and competenceId', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findValidatedByCompetenceId('competenceId00', 'es'); - it('should setup the expected validator and solution on found challenges', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const validatedChallenge = domainBuilder.buildChallenge({ - type: Challenge.Type.QCM, - skill, - status: 'validé', - }); - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { - ...validatedChallenge, - skillId: 'recSkill1', - t1Status: true, - t2Status: true, - t3Status: false, - }, - ], - }; - await mockLearningContent(learningContent); - // when - const [actualChallenge] = await challengeRepository.findValidatedByCompetenceId( - validatedChallenge.competenceId, - 'fr', - ); + // then + expect(challenges).to.deep.equal([]); + }); + }); + context('when validated challenges are found for given locale and competenceId', function () { + it('should return the challenges', async function () { + // when + const challenges = await challengeRepository.findValidatedByCompetenceId('competenceId00', 'en'); - // then - expect(actualChallenge.validator).to.be.instanceOf(Validator); - expect(actualChallenge.validator.solution.id).to.equal(validatedChallenge.id); - expect(actualChallenge.validator.solution.isT1Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT2Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT3Enabled).to.equal(false); - expect(actualChallenge.validator.solution.type).to.equal(validatedChallenge.type); - expect(actualChallenge.validator.solution.value).to.equal(validatedChallenge.solution); + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson, + blindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility2, + focused: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.focusable, + discriminant: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.alpha, + difficulty: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.id, + type: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.type, + value: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.solution, + isT1Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t1Status, + isT2Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t2Status, + isT3Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData01_tube01competence00_actif, + difficulty: skillData01_tube01competence00_actif.level, + hint: skillData01_tube01competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); describe('#findOperativeBySkills', function () { - it('should return only operative challenges with skills', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const anotherSkill = domainBuilder.buildSkill({ id: 'recAnotherSkill' }); - const operativeInSkillChallenge = domainBuilder.buildChallenge({ id: 'recChallenge1', skill, status: 'archivé' }); - const nonOperativeInSkillChallenge = domainBuilder.buildChallenge({ - id: 'recChallenge2', - skill, - status: 'PAS opérative', - }); - const operativeNotInSkillChallenge = domainBuilder.buildChallenge({ - id: 'recChallenge3', - skill: anotherSkill, - status: 'validé', - }); - const locale = 'fr'; - const learningContent = { - skills: [ - { ...skill, status: 'actif', level: skill.difficulty }, - { ...anotherSkill, status: 'actif', level: anotherSkill.difficulty }, - ], - challenges: [ - { ...operativeInSkillChallenge, skillId: 'recSkill1' }, - { ...nonOperativeInSkillChallenge, skillId: 'recSkill1' }, - { ...operativeNotInSkillChallenge, skillId: 'recAnotherSkill' }, - ], - }; - await mockLearningContent(learningContent); - - // when - const actualChallenges = await challengeRepository.findOperativeBySkills([skill], locale); + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.findOperativeBySkills)(domainBuilder.buildSkill()); - // then - expect(actualChallenges).to.have.lengthOf(1); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(_.omit(actualChallenges[0], 'validator')).to.deep.equal(_.omit(actualChallenges[0], 'validator')); + // then + expect(err.message).to.equal('Locale shall be defined'); + }); }); + context('when locale is defined', function () { + context('when no operative challenges found for given locale', function () { + it('should return an empty array', async function () { + // given + const skill00 = domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }); - it('should setup the expected validator and solution on found challenges', async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const operativeChallenge = domainBuilder.buildChallenge({ - type: Challenge.Type.QCM, - skill, - status: 'validé', - }); - const locale = 'fr'; - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { - ...operativeChallenge, - skillId: 'recSkill1', - t1Status: true, - t2Status: true, - t3Status: false, - }, - ], - }; - await mockLearningContent(learningContent); + // when + const challenges = await challengeRepository.findOperative([skill00], 'catalan'); - // when - const [actualChallenge] = await challengeRepository.findOperativeBySkills([skill], locale); + // then + expect(challenges).to.deep.equal([]); + }); + }); + context('when operative challenges are found for given locale', function () { + it('should return the challenges', async function () { + // given + const skills = [ + domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData02_tube02competence01_perime, + difficulty: skillData02_tube02competence01_perime.level, + hint: skillData02_tube02competence01_perime.hint_i18n.fr, + }), + domainBuilder.buildSkill({ + ...skillData01_tube01competence00_actif, + difficulty: skillData01_tube01competence00_actif.level, + hint: skillData01_tube01competence00_actif.hint_i18n.fr, + }), + ]; - // then - expect(actualChallenge.validator).to.be.instanceOf(Validator); - expect(actualChallenge.validator.solution.id).to.equal(operativeChallenge.id); - expect(actualChallenge.validator.solution.isT1Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT2Enabled).to.equal(true); - expect(actualChallenge.validator.solution.isT3Enabled).to.equal(false); - expect(actualChallenge.validator.solution.type).to.equal(operativeChallenge.type); - expect(actualChallenge.validator.solution.value).to.equal(operativeChallenge.solution); + // when + const challenges = await challengeRepository.findOperativeBySkills(skills, 'en'); + + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson, + blindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.accessibility2, + focused: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.focusable, + discriminant: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.alpha, + difficulty: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.id, + type: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.type, + value: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.solution, + isT1Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t1Status, + isT2Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t2Status, + isT3Enabled: challengeData04_skill01_qcu_valide_flashCompatible_ennl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData01_tube01competence00_actif, + difficulty: skillData01_tube01competence00_actif.level, + hint: skillData01_tube01competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); describe('#findFlashCompatibleWithoutLocale', function () { beforeEach(async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - - const activeChallenge = domainBuilder.buildChallenge({ - id: 'activeChallenge', - skill, - status: 'validé', - locales: ['en'], - }); - const archivedChallenge = domainBuilder.buildChallenge({ - id: 'archivedChallenge', - skill, - status: 'archivé', - locales: ['fr-fr'], - }); - const outdatedChallenge = domainBuilder.buildChallenge({ - id: 'outdatedChallenge', - skill, - status: 'périmé', - locales: ['nl'], - }); - - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { ...activeChallenge, skillId: 'recSkill1', alpha: 3.57, delta: -8.99 }, - { ...archivedChallenge, skillId: 'recSkill1', alpha: 3.2, delta: 1.06 }, - { ...outdatedChallenge, skillId: 'recSkill1', alpha: 4.1, delta: -2.08 }, - ], - }; - await mockLearningContent(learningContent); + await knex('learningcontent.challenges').truncate(); + await knex('learningcontent.skills').truncate(); + databaseBuilder.factory.learningContent.buildSkill(skillData02_tube02competence01_perime); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData03_tube02competence01_actif); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData08_skill03_qcu_archive_notFlashCompatible_fr_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData00_tube00competence00_actif); + await databaseBuilder.commit(); }); + context('when including obsolete challenges', function () { + context('when no flash compatible challenges found', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findFlashCompatibleWithoutLocale({ + useObsoleteChallenges: true, + }); - context('without requesting obsolete challenges', function () { - it('should return all flash compatible challenges with skills', async function () { - // when - const actualChallenges = await challengeRepository.findFlashCompatibleWithoutLocale(); - - // then - expect(actualChallenges).to.have.lengthOf(2); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(actualChallenges[0]).to.deep.contain({ - status: 'validé', + // then + expect(challenges).to.deep.equal([]); }); - expect(actualChallenges[1]).to.deep.contain({ - status: 'archivé', + }); + context('when flash compatible challenges found', function () { + it('should return the challenges', async function () { + // given + databaseBuilder.factory.learningContent.buildChallenge( + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + ); + await databaseBuilder.commit(); + + // when + const challenges = await challengeRepository.findFlashCompatibleWithoutLocale({ + useObsoleteChallenges: true, + }); + + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + blindnessCompatibility: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.accessibility2, + focused: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.focusable, + discriminant: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.alpha, + difficulty: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.id, + type: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.type, + value: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.solution, + isT1Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t1Status, + isT2Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t2Status, + isT3Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson, + blindnessCompatibility: + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.accessibility2, + focused: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.focusable, + discriminant: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.alpha, + difficulty: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.id, + type: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.type, + value: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.solution, + isT1Enabled: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.t1Status, + isT2Enabled: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.t2Status, + isT3Enabled: challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData02_tube02competence01_perime, + difficulty: skillData02_tube02competence01_perime.level, + hint: skillData02_tube02competence01_perime.hint_i18n.fr, + }), + }), + ]); }); }); }); + context('when excluding obsolete challenges', function () { + context('when no flash compatible challenges found', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findFlashCompatibleWithoutLocale({ + useObsoleteChallenges: false, + }); - context('when requesting obsolete challenges', function () { - it('should return all flash compatible challenges with skills', async function () { - // when - const actualChallenges = await challengeRepository.findFlashCompatibleWithoutLocale({ - useObsoleteChallenges: true, + // then + expect(challenges).to.deep.equal([]); }); + }); + context('when flash compatible challenges found', function () { + it('should return the challenges', async function () { + // given + databaseBuilder.factory.learningContent.buildChallenge( + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData05_skill02_qcm_perime_flashCompatible_fren_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + ); + await databaseBuilder.commit(); - // then - expect(actualChallenges).to.have.lengthOf(3); - expect(actualChallenges[0]).to.deep.contain({ - status: 'validé', - }); - expect(actualChallenges[1]).to.deep.contain({ - status: 'archivé', - }); - expect(actualChallenges[2]).to.deep.contain({ - status: 'périmé', + // when + const challenges = await challengeRepository.findFlashCompatibleWithoutLocale({ + useObsoleteChallenges: false, + }); + + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + blindnessCompatibility: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.accessibility2, + focused: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.focusable, + discriminant: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.alpha, + difficulty: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.id, + type: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.type, + value: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.solution, + isT1Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t1Status, + isT2Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t2Status, + isT3Enabled: challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + blindnessCompatibility: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.accessibility2, + focused: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.focusable, + discriminant: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.alpha, + difficulty: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.delta, + validator: new ValidatorQCM({ + solution: domainBuilder.buildSolution({ + id: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.id, + type: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.type, + value: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.solution, + isT1Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t1Status, + isT2Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t2Status, + isT3Enabled: challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ]); }); }); }); }); describe('#findActiveFlashCompatible', function () { + let defaultSuccessProbabilityThreshold; + beforeEach(async function () { - // given - const skill = domainBuilder.buildSkill({ id: 'recSkill1' }); - const locales = ['fr-fr']; - const activeChallenge = domainBuilder.buildChallenge({ - id: 'activeChallenge', - skill, - status: 'validé', - locales, - }); - const archivedChallenge = domainBuilder.buildChallenge({ - id: 'archivedChallenge', - skill, - status: 'archivé', - locales, - }); - const outdatedChallenge = domainBuilder.buildChallenge({ - id: 'outdatedChallenge', - skill, - status: 'périmé', - locales, - }); - const nonAccessibleChallenge = domainBuilder.buildChallenge({ - id: 'nonAccessibleChallenge', - skill, - status: 'validé', - locales, - }); - const learningContent = { - skills: [{ ...skill, status: 'actif', level: skill.difficulty }], - challenges: [ - { - ...activeChallenge, - accessibility1: 'OK', - accessibility2: 'OK', - skillId: 'recSkill1', - alpha: 3.57, - delta: -8.99, - }, - { ...archivedChallenge, accessibility1: 'OK', accessibility2: 'OK', skillId: 'recSkill1' }, - { ...outdatedChallenge, accessibility1: 'OK', accessibility2: 'OK', skillId: 'recSkill1' }, - { - ...nonAccessibleChallenge, - accessibility1: 'KO', - accessibility2: 'OK', - skillId: 'recSkill1', - alpha: 3.57, - delta: -8.99, - }, - ], - }; - await mockLearningContent(learningContent); + defaultSuccessProbabilityThreshold = config.features.successProbabilityThreshold; + await knex('learningcontent.challenges').truncate(); + await knex('learningcontent.skills').truncate(); + databaseBuilder.factory.learningContent.buildSkill(skillData02_tube02competence01_perime); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData06_skill02_qcm_perime_notFlashCompatible_fren_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData03_tube02competence01_actif); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData07_skill03_qcm_valide_notFlashCompatible_frnl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData08_skill03_qcu_archive_notFlashCompatible_fr_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildSkill(skillData00_tube00competence00_actif); + await databaseBuilder.commit(); }); - it('should return only flash compatible challenges with skills', async function () { - // given - const locale = 'fr-fr'; - const successProbabilityThreshold = 0.95; + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.findActiveFlashCompatible)(); - // when - const actualChallenges = await challengeRepository.findActiveFlashCompatible({ - locale, - successProbabilityThreshold, + // then + expect(err.message).to.equal('Locale shall be defined'); }); + }); + context('when locale is defined', function () { + context('when no active flash compatible challenges found', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findActiveFlashCompatible({ + locale: 'fr', + }); - // then - expect(actualChallenges).to.have.lengthOf(2); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(actualChallenges[0]).to.deep.contain({ - id: 'activeChallenge', - status: 'validé', - locales: ['fr-fr'], - difficulty: -8.99, - discriminant: 3.57, - minimumCapability: -8.165227176704079, - }); - expect(actualChallenges[0].skill).to.contain({ - id: 'recSkill1', + // then + expect(challenges).to.deep.equal([]); + }); }); - }); + context('when active flash compatible challenges found', function () { + it('should return the challenges', async function () { + // given + databaseBuilder.factory.learningContent.buildChallenge( + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData03_skill00_qcm_valide_flashCompatible_nl_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData02_skill00_qcm_archive_flashCompatible_en_noEmbedJson, + ); + databaseBuilder.factory.learningContent.buildChallenge( + challengeData09_skill03_qcu_archive_flashCompatible_fr_noEmbedJson, + ); + await databaseBuilder.commit(); - it('should allow overriding success probability threshold default value', async function () { - // given - const successProbabilityThreshold = 0.75; + // when + const challenges = await challengeRepository.findActiveFlashCompatible({ + locale: 'fr', + }); - // when - const actualChallenges = await challengeRepository.findActiveFlashCompatible({ - locale: 'fr-fr', - successProbabilityThreshold, + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson, + successProbabilityThreshold: defaultSuccessProbabilityThreshold, + blindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.accessibility2, + focused: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.focusable, + discriminant: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.alpha, + difficulty: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.id, + type: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.type, + value: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.solution, + isT1Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t1Status, + isT2Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t2Status, + isT3Enabled: challengeData00_skill00_qcu_valide_flashCompatible_frnl_noEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + successProbabilityThreshold: defaultSuccessProbabilityThreshold, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); }); + context('when successProbabilityThreshold is passed in parameters', function () { + it('should override default successProbabilityThreshold with the one given in parameter', async function () { + // given + databaseBuilder.factory.learningContent.buildChallenge( + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + ); + await databaseBuilder.commit(); - // then - expect(actualChallenges).to.have.lengthOf(2); - expect(actualChallenges[0]).to.be.instanceOf(Challenge); - expect(actualChallenges[0].minimumCapability).to.equal(-8.682265465359073); - }); - - context('when requesting only accessible challenges', function () { - it('should return all accessible flash compatible challenges with skills', async function () { - // given - const successProbabilityThreshold = 0.95; + // when + const challenges = await challengeRepository.findActiveFlashCompatible({ + locale: 'fr', + successProbabilityThreshold: 0.75, + }); - // when - const actualChallenges = await challengeRepository.findActiveFlashCompatible({ - locale: 'fr-fr', - successProbabilityThreshold, - accessibilityAdjustmentNeeded: true, + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + successProbabilityThreshold: 0.75, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ]); }); + }); + context('when accessibilityAdjustmentNeeded is true', function () { + it('should keep accessible challenges', async function () { + // given + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1RasA2Ras', + accessibility1: 'RAS', + accessibility2: 'RAS', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1RasA2Ok', + accessibility1: 'RAS', + accessibility2: 'OK', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1RasA2Ko', + accessibility1: 'RAS', + accessibility2: 'KO', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1OkA2Ras', + accessibility1: 'OK', + accessibility2: 'RAS', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1OkA2Ko', + accessibility1: 'OK', + accessibility2: 'KO', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1OkA2Ok', + accessibility1: 'OK', + accessibility2: 'OK', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1KoA2Ras', + accessibility1: 'KO', + accessibility2: 'RAS', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1KoA2Ok', + accessibility1: 'KO', + accessibility2: 'OK', + }); + databaseBuilder.factory.learningContent.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + id: 'challengeA1KoA2Ko', + accessibility1: 'KO', + accessibility2: 'KO', + }); + await databaseBuilder.commit(); - // then - expect(actualChallenges).to.have.lengthOf(1); - expect(actualChallenges[0]).to.deep.contain({ - status: 'validé', + // when + const challenges = await challengeRepository.findActiveFlashCompatible({ + locale: 'fr', + accessibilityAdjustmentNeeded: true, + }); + + // then + expect(challenges.map((chal) => chal.id)).to.deep.equal([ + 'challengeA1OkA2Ok', + 'challengeA1OkA2Ras', + 'challengeA1RasA2Ok', + 'challengeA1RasA2Ras', + ]); }); }); }); }); describe('#findValidatedBySkillId', function () { - it('should return validated challenges of a skill', async function () { - // given - const skill = _buildSkill({ id: 'recSkill1' }); - - const challenge1 = _buildChallenge({ id: 'recChallenge1', skill }); - const challenge2 = _buildChallenge({ id: 'recChallenge2', skill, status: 'archivé' }); - const challenge3 = _buildChallenge({ id: 'recChallenge3', skill, status: 'périmé' }); + context('when locale is not defined', function () { + it('should throw an Error', async function () { + // when + const err = await catchErr(challengeRepository.findValidatedBySkillId)('skillId00'); - await mockLearningContent({ - skills: [skill], - challenges: [challenge1, challenge2, challenge3], + // then + expect(err.message).to.equal('Locale shall be defined'); }); + }); + context('when locale is defined', function () { + context('when no validated challenges found for given locale and skillId', function () { + it('should return an empty array', async function () { + // when + const challenges = await challengeRepository.findValidatedBySkillId('skillId00', 'es'); - const expectedValidatedChallenge = domainBuilder.buildChallenge({ - ...challenge1, - focused: challenge1.focusable, - skill: domainBuilder.buildSkill({ ...skill, difficulty: skill.level }), + // then + expect(challenges).to.deep.equal([]); + }); }); + context('when validated challenges are found for given locale and skillId', function () { + it('should return the challenges', async function () { + // when + const challenges = await challengeRepository.findValidatedBySkillId('skillId00', 'en'); - // when - const validatedChallenges = await challengeRepository.findValidatedBySkillId(skill.id, 'fr'); - - // then - expect(validatedChallenges).to.have.lengthOf(1); - expect(validatedChallenges[0]).to.be.instanceOf(Challenge); - expect(_.omit(validatedChallenges[0], 'validator')).to.deep.equal( - _.omit(expectedValidatedChallenge, 'validator'), - ); + // then + expect(challenges).to.deep.equal([ + domainBuilder.buildChallenge({ + ...challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson, + blindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility1, + colorBlindnessCompatibility: + challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.accessibility2, + focused: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.focusable, + discriminant: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.alpha, + difficulty: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.delta, + validator: new ValidatorQCU({ + solution: domainBuilder.buildSolution({ + id: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.id, + type: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.type, + value: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.solution, + isT1Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t1Status, + isT2Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t2Status, + isT3Enabled: challengeData01_skill00_qcu_valide_flashCompatible_fren_withEmbedJson.t3Status, + qrocBlocksTypes: {}, + }), + }), + skill: domainBuilder.buildSkill({ + ...skillData00_tube00competence00_actif, + difficulty: skillData00_tube00competence00_actif.level, + hint: skillData00_tube00competence00_actif.hint_i18n.fr, + }), + }), + ]); + }); + }); }); }); describe('#getManyTypes', function () { it('should return an object associating ids to type', async function () { - // given - const skill = _buildSkill({ id: 'recSkill1' }); - - const challenge1 = _buildChallenge({ id: 'recChallenge1', skill, locales: ['fr'], type: 'QROC' }); - const challenge2 = _buildChallenge({ id: 'recChallenge2', skill, locales: ['fr-fr'], type: 'QCU' }); - const challenge3 = _buildChallenge({ id: 'recChallenge3', skill, locales: ['en'], type: 'QROCM-dep' }); - - await mockLearningContent({ - skills: [skill], - challenges: [challenge1, challenge2, challenge3], - }); - // when const challengesType = await challengeRepository.getManyTypes([ - 'recChallenge1', - 'recChallenge2', - 'recChallenge3', + 'challengeId09', + 'challengeId04', + 'challengeId07', + 'challengeId02', + 'challengeId01', ]); // then expect(challengesType).to.deep.equal({ - recChallenge1: 'QROC', - recChallenge2: 'QCU', - recChallenge3: 'QROCM-dep', + challengeId01: 'QCU', + challengeId02: 'QCM', + challengeId04: 'QCU', + challengeId07: 'QCM', + challengeId09: 'QCU', }); }); }); }); - -function _buildSkill({ id, name = '@sau6', tubeId = 'recTUB123' }) { - return { - competenceId: 'recCOMP123', - id, - name, - pixValue: 3, - tubeId, - tutorialIds: [], - version: 1, - status: 'actif', - level: 1, - }; -} - -function _buildChallenge({ - id, - skill, - status = 'validé', - alternativeVersion, - type = Challenge.Type.QCM, - alpha = 1, - delta = 0, -}) { - return { - id, - attachments: ['URL pièce jointe'], - format: 'petit', - illustrationUrl: "Une URL vers l'illustration", - illustrationAlt: "Le texte de l'illustration", - instruction: 'Des instructions', - alternativeInstruction: 'Des instructions alternatives', - proposals: 'Une proposition', - status, - timer: undefined, - focusable: true, - type, - locales: ['fr'], - autoReply: false, - answer: undefined, - responsive: 'Smartphone/Tablette', - competenceId: 'recCOMP1', - skillId: skill.id, - alpha, - delta, - skill, - shuffled: false, - alternativeVersion: alternativeVersion || 1, - accessibility1: 'OK', - accessibility2: 'RAS', - }; -} diff --git a/api/tests/shared/unit/infrastructure/adapters/skill-adapter_test.js b/api/tests/shared/unit/infrastructure/adapters/skill-adapter_test.js deleted file mode 100644 index d86fa64cdea..00000000000 --- a/api/tests/shared/unit/infrastructure/adapters/skill-adapter_test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Skill } from '../../../../../src/shared/domain/models/Skill.js'; -import * as skillAdapter from '../../../../../src/shared/infrastructure/adapters/skill-adapter.js'; -import { domainBuilder, expect } from '../../../../test-helper.js'; - -describe('Unit | Infrastructure | Adapter | skillAdapter', function () { - describe('#fromDatasourceObject', function () { - it('should create a Skill model', function () { - // given - const skillDataObject = domainBuilder.buildSkillLearningContentDataObject(); - const expectedSkill = domainBuilder.buildSkill({ - id: skillDataObject.id, - name: skillDataObject.name, - pixValue: skillDataObject.pixValue, - competenceId: skillDataObject.competenceId, - tutorialIds: ['recCO0X22abcdefgh'], - tubeId: skillDataObject.tubeId, - version: skillDataObject.version, - difficulty: skillDataObject.level, - }); - - // when - const skill = skillAdapter.fromDatasourceObject(skillDataObject); - - // then - expect(skill).to.be.an.instanceOf(Skill); - expect(skill).to.deep.equal(expectedSkill); - }); - }); -}); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index 7a623e46c82..c57be05f795 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -27,6 +27,7 @@ import { Membership } from '../src/shared/domain/models/index.js'; import * as tokenService from '../src/shared/domain/services/token-service.js'; import { LearningContentCache } from '../src/shared/infrastructure/caches/learning-content-cache.js'; import * as areaRepository from '../src/shared/infrastructure/repositories/area-repository.js'; +import * as challengeRepository from '../src/shared/infrastructure/repositories/challenge-repository.js'; import * as competenceRepository from '../src/shared/infrastructure/repositories/competence-repository.js'; import * as skillRepository from '../src/shared/infrastructure/repositories/skill-repository.js'; import * as customChaiHelpers from './tooling/chai-custom-helpers/index.js'; @@ -83,6 +84,7 @@ afterEach(function () { thematicRepository.clearCache(); tubeRepository.clearCache(); skillRepository.clearCache(); + challengeRepository.clearCache(); return databaseBuilder.clean(); }); diff --git a/api/tests/tooling/domain-builder/factory/build-challenge.js b/api/tests/tooling/domain-builder/factory/build-challenge.js index 1b8428655fc..fc82a4cebf8 100644 --- a/api/tests/tooling/domain-builder/factory/build-challenge.js +++ b/api/tests/tooling/domain-builder/factory/build-challenge.js @@ -35,6 +35,8 @@ const buildChallenge = function ({ skill = buildSkill(), // references competenceId = 'recCOMP1', + webComponentTagName, + webComponentProps, } = {}) { return new Challenge({ id, @@ -63,6 +65,8 @@ const buildChallenge = function ({ alternativeVersion, blindnessCompatibility, colorBlindnessCompatibility, + webComponentProps, + webComponentTagName, // includes answer, validator, diff --git a/api/tests/tooling/domain-builder/factory/build-solution.js b/api/tests/tooling/domain-builder/factory/build-solution.js index ec6719410c8..a6666aa9f8b 100644 --- a/api/tests/tooling/domain-builder/factory/build-solution.js +++ b/api/tests/tooling/domain-builder/factory/build-solution.js @@ -7,6 +7,7 @@ const buildSolution = function ({ isT1Enabled = false, isT2Enabled = false, isT3Enabled = false, + qrocBlocksTypes = {}, } = {}) { return new Solution({ id, @@ -15,6 +16,7 @@ const buildSolution = function ({ isT1Enabled, isT2Enabled, isT3Enabled, + qrocBlocksTypes, }); }; diff --git a/api/tests/tooling/fixtures/infrastructure/challengeLearningContentDataObjectFixture.js b/api/tests/tooling/fixtures/infrastructure/challengeLearningContentDataObjectFixture.js deleted file mode 100644 index 19a9cb27812..00000000000 --- a/api/tests/tooling/fixtures/infrastructure/challengeLearningContentDataObjectFixture.js +++ /dev/null @@ -1,62 +0,0 @@ -const ChallengeLearningContentDataObjectFixture = function ({ - id = 'recwWzTquPlvIl4So', - instruction = "Les moteurs de recherche affichent certains liens en raison d'un accord commercial.\n\nDans quels encadrés se trouvent ces liens ?", - proposals = '- 1\n- 2\n- 3\n- 4\n- 5', - type = 'QCM', - solution = '1, 5', - solutionToDisplay = '1, 5', - t1Status = 'Activé', - t2Status = 'Désactivé', - t3Status = 'Activé', - scoring = '1: @outilsTexte2\n2: @outilsTexte4', - status = 'validé', - skillId = 'recUDrCWD76fp5MsE', - timer = 1234, - illustrationUrl = 'https://dl.airtable.com/2MGErxGTQl2g2KiqlYgV_venise4.png', - illustrationAlt = 'Texte alternatif de l’illustration', - attachments = [ - 'https://dl.airtable.com/nHWKNZZ7SQeOKsOvVykV_navigationdiaporama5.pptx', - 'https://dl.airtable.com/rsXNJrSPuepuJQDByFVA_navigationdiaporama5.odp', - ], - competenceId = 'recsvLz0W2ShyfD63', - embedUrl = 'https://github.io/page/epreuve.html', - embedTitle = 'Epreuve de selection de dossier', - embedHeight = 500, - format = 'petit', - locales = ['fr'], - autoReply = false, - alternativeInstruction = '', - accessibility1 = 'OK', - accessibility2 = 'RAS', -} = {}) { - return { - id, - instruction, - proposals, - type, - solution, - solutionToDisplay, - t1Status, - t2Status, - t3Status, - scoring, - status, - skillId, - timer, - illustrationUrl, - illustrationAlt, - attachments, - competenceId, - embedUrl, - embedTitle, - embedHeight, - format, - locales, - autoReply, - alternativeInstruction, - accessibility1, - accessibility2, - }; -}; - -export { ChallengeLearningContentDataObjectFixture }; From 97bcb953e6ca8c36c0e5aca35000ecd011f56dde Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Sat, 30 Nov 2024 22:46:44 +0100 Subject: [PATCH 11/24] feat(api): refacto courseRepository with new cache and to use PG --- .../learning-content/course-datasource.js | 7 -- .../datasources/learning-content/index.js | 3 +- .../repositories/course-repository.js | 63 ++++++++++-------- .../repositories/course-repository_test.js | 66 +++++++++++++------ api/tests/test-helper.js | 2 + 5 files changed, 82 insertions(+), 59 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/course-datasource.js diff --git a/api/src/shared/infrastructure/datasources/learning-content/course-datasource.js b/api/src/shared/infrastructure/datasources/learning-content/course-datasource.js deleted file mode 100644 index 38ec69dcdc8..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/course-datasource.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as datasource from './datasource.js'; - -const courseDatasource = datasource.extend({ - modelName: 'courses', -}); - -export { courseDatasource }; diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js index b36fa763a4f..0ba26489c35 100644 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ b/api/src/shared/infrastructure/datasources/learning-content/index.js @@ -1,4 +1,3 @@ import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js'; -import { courseDatasource } from './course-datasource.js'; -export { courseDatasource, tutorialDatasource }; +export { tutorialDatasource }; diff --git a/api/src/shared/infrastructure/repositories/course-repository.js b/api/src/shared/infrastructure/repositories/course-repository.js index e6606cd519f..cf36c5e5ada 100644 --- a/api/src/shared/infrastructure/repositories/course-repository.js +++ b/api/src/shared/infrastructure/repositories/course-repository.js @@ -1,42 +1,47 @@ import { NotFoundError } from '../../domain/errors.js'; import { Course } from '../../domain/models/Course.js'; -import { courseDatasource } from '../datasources/learning-content/course-datasource.js'; -import { LearningContentResourceNotFound } from '../datasources/learning-content/LearningContentResourceNotFound.js'; +import { LearningContentRepository } from './learning-content-repository.js'; -function _toDomain(courseDataObject) { - return new Course({ - id: courseDataObject.id, - name: courseDataObject.name, - description: courseDataObject.description, - isActive: courseDataObject.isActive, - challenges: courseDataObject.challenges, - competences: courseDataObject.competences, - }); -} +const TABLE_NAME = 'learningcontent.courses'; -async function _get(id) { - try { - const courseDataObject = await courseDatasource.get(id); - return _toDomain(courseDataObject); - } catch (error) { - if (error instanceof LearningContentResourceNotFound) { - throw new NotFoundError(); - } - throw error; +export async function get(id) { + const courseDto = await getInstance().load(id); + if (!courseDto) { + throw new NotFoundError(); } + return toDomain(courseDto); } -const get = async function (id) { - return _get(id); -}; - -const getCourseName = async function (id) { +export async function getCourseName(id) { try { - const course = await _get(id); + const course = await get(id); return course.name; } catch (err) { throw new NotFoundError("Le test demandé n'existe pas"); } -}; +} + +export function clearCache() { + return getInstance().clearCache(); +} -export { get, getCourseName }; +function toDomain(courseDto) { + return new Course({ + id: courseDto.id, + name: courseDto.name, + description: courseDto.description, + isActive: courseDto.isActive, + challenges: courseDto.challenges ? [...courseDto.challenges] : null, + competences: courseDto.competences ? [...courseDto.competences] : null, + }); +} + +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; +} diff --git a/api/tests/shared/integration/infrastructure/repositories/course-repository_test.js b/api/tests/shared/integration/infrastructure/repositories/course-repository_test.js index c4599d040fb..4e654d0d6d1 100644 --- a/api/tests/shared/integration/infrastructure/repositories/course-repository_test.js +++ b/api/tests/shared/integration/infrastructure/repositories/course-repository_test.js @@ -1,48 +1,72 @@ import { NotFoundError } from '../../../../../src/shared/domain/errors.js'; -import { Course } from '../../../../../src/shared/domain/models/Course.js'; import * as courseRepository from '../../../../../src/shared/infrastructure/repositories/course-repository.js'; -import { catchErr, domainBuilder, expect, mockLearningContent } from '../../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Repository | course-repository', function () { + const courseData0 = { + id: 'courseId0', + name: 'instruction courseData0', + description: 'description courseData0', + isActive: true, + competences: ['competenceId0'], + challenges: ['challengeId0'], + }; + const courseData1 = { + id: 'courseId1', + name: 'instruction courseData1', + description: 'description courseData1', + isActive: false, + competences: ['competenceId1'], + challenges: ['challengeId1'], + }; + + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildCourse(courseData0); + databaseBuilder.factory.learningContent.buildCourse(courseData1); + await databaseBuilder.commit(); + }); + describe('#get', function () { - context('when course exists', function () { + context('when course found for given id', function () { it('should return the course', async function () { - // given - const expectedCourse = domainBuilder.buildCourse(); - await mockLearningContent({ courses: [{ ...expectedCourse }] }); + // when + const course = await courseRepository.get('courseId1'); + // then + expect(course).to.deepEqualInstance(domainBuilder.buildCourse(courseData1)); + }); + }); + + context('when no course found', function () { + it('should throw a NotFound error', async function () { // when - const actualCourse = await courseRepository.get(expectedCourse.id); + const err = await catchErr(courseRepository.get, courseRepository)('coucouLoulou'); // then - expect(actualCourse).to.be.instanceOf(Course); - expect(actualCourse).to.deep.equal(expectedCourse); + expect(err).to.be.instanceOf(NotFoundError); }); }); }); describe('#getCourseName', function () { - context('when course does not exist', function () { - it('should return all areas without fetching competences', async function () { + context('when course found for given id', function () { + it('should return the course name', async function () { // when - const error = await catchErr(courseRepository.getCourseName)('illusion'); + const courseName = await courseRepository.getCourseName('courseId0'); // then - expect(error).to.be.instanceOf(NotFoundError); + expect(courseName).to.deep.equal(courseData0.name); }); }); - context('when course exists', function () { - it('should return the course name', async function () { - // given - const expectedCourse = domainBuilder.buildCourse(); - await mockLearningContent({ courses: [{ ...expectedCourse }] }); - + context('when no course found', function () { + it('should throw a NotFound error', async function () { // when - const actualCourseName = await courseRepository.getCourseName(expectedCourse.id); + const err = await catchErr(courseRepository.getCourseName, courseRepository)('coucouLoulou'); // then - expect(actualCourseName).to.equal(expectedCourse.name); + expect(err).to.be.instanceOf(NotFoundError); + expect(err.message).to.equal("Le test demandé n'existe pas"); }); }); }); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index c57be05f795..3462d941ab1 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -29,6 +29,7 @@ import { LearningContentCache } from '../src/shared/infrastructure/caches/learni import * as areaRepository from '../src/shared/infrastructure/repositories/area-repository.js'; import * as challengeRepository from '../src/shared/infrastructure/repositories/challenge-repository.js'; import * as competenceRepository from '../src/shared/infrastructure/repositories/competence-repository.js'; +import * as courseRepository from '../src/shared/infrastructure/repositories/course-repository.js'; import * as skillRepository from '../src/shared/infrastructure/repositories/skill-repository.js'; import * as customChaiHelpers from './tooling/chai-custom-helpers/index.js'; import * as domainBuilder from './tooling/domain-builder/factory/index.js'; @@ -85,6 +86,7 @@ afterEach(function () { tubeRepository.clearCache(); skillRepository.clearCache(); challengeRepository.clearCache(); + courseRepository.clearCache(); return databaseBuilder.clean(); }); From 622db7370c1a9767d60f730cfb85ac9db91bb60b Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Sat, 30 Nov 2024 23:23:35 +0100 Subject: [PATCH 12/24] feat(api): refacto tutorialRepository with new cache and to use PG --- .../learning-content/tutorial-datasource.js | 13 - .../repositories/tutorial-repository.js | 145 ++-- .../datasources/learning-content/index.js | 3 - .../datasources/tutorial-datasource_test.js | 25 - .../repositories/tutorial-repository_test.js | 692 ++++++++---------- ...patch-learning-content-cache-entry_test.js | 3 - api/tests/test-helper.js | 2 + 7 files changed, 403 insertions(+), 480 deletions(-) delete mode 100644 api/src/devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/index.js delete mode 100644 api/tests/devcomp/integration/infrastructure/datasources/tutorial-datasource_test.js diff --git a/api/src/devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js b/api/src/devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js deleted file mode 100644 index 56995b9f8db..00000000000 --- a/api/src/devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js +++ /dev/null @@ -1,13 +0,0 @@ -import _ from 'lodash'; - -import * as datasource from '../../../../shared/infrastructure/datasources/learning-content/datasource.js'; - -const tutorialDatasource = datasource.extend({ - modelName: 'tutorials', - - async findByRecordIds(tutorialRecordIds) { - const tutorials = await this.list(); - return tutorials.filter((tutorialData) => _.includes(tutorialRecordIds, tutorialData.id)); - }, -}); -export { tutorialDatasource }; diff --git a/api/src/devcomp/infrastructure/repositories/tutorial-repository.js b/api/src/devcomp/infrastructure/repositories/tutorial-repository.js index 868128ef3d9..2ac97cbcf6c 100644 --- a/api/src/devcomp/infrastructure/repositories/tutorial-repository.js +++ b/api/src/devcomp/infrastructure/repositories/tutorial-repository.js @@ -3,7 +3,7 @@ import _ from 'lodash'; import * as knowledgeElementRepository from '../../../../lib/infrastructure/repositories/knowledge-element-repository.js'; import { LOCALE } from '../../../shared/domain/constants.js'; import { NotFoundError } from '../../../shared/domain/errors.js'; -import { tutorialDatasource } from '../../../shared/infrastructure/datasources/learning-content/index.js'; +import { LearningContentRepository } from '../../../shared/infrastructure/repositories/learning-content-repository.js'; import * as skillRepository from '../../../shared/infrastructure/repositories/skill-repository.js'; import * as paginateModule from '../../../shared/infrastructure/utils/paginate.js'; import { Tutorial } from '../../domain/models/Tutorial.js'; @@ -12,31 +12,40 @@ import * as tutorialEvaluationRepository from './tutorial-evaluation-repository. import * as userSavedTutorialRepository from './user-saved-tutorial-repository.js'; const { FRENCH_FRANCE } = LOCALE; +const TABLE_NAME = 'learningcontent.tutorials'; -const findByRecordIdsForCurrentUser = async function ({ ids, userId, locale }) { - const tutorials = await _findByRecordIds({ ids, locale }); +export async function findByRecordIdsForCurrentUser({ ids, userId, locale }) { + let tutorialDtos = await getInstance().loadMany(ids); + tutorialDtos = tutorialDtos.filter((tutorialDto) => tutorialDto); + if (locale) { + const lang = extractLangFromLocale(locale); + tutorialDtos = tutorialDtos.filter((tutorialDto) => extractLangFromLocale(tutorialDto.locale) === lang); + } + tutorialDtos.sort(byId); + const tutorials = tutorialDtos.map(toDomain); const userSavedTutorials = await userSavedTutorialRepository.find({ userId }); const tutorialEvaluations = await tutorialEvaluationRepository.find({ userId }); - return _toTutorialsForUser({ tutorials, tutorialEvaluations, userSavedTutorials }); -}; + return toTutorialsForUser({ tutorials, tutorialEvaluations, userSavedTutorials }); +} -const findPaginatedFilteredForCurrentUser = async function ({ userId, filters = {}, page }) { +export async function findPaginatedFilteredForCurrentUser({ userId, filters = {}, page }) { const userSavedTutorials = await userSavedTutorialRepository.find({ userId }); - const [tutorials, tutorialEvaluations] = await Promise.all([ - tutorialDatasource.findByRecordIds(userSavedTutorials.map(({ tutorialId }) => tutorialId)), - tutorialEvaluationRepository.find({ userId }), - ]); + const tutorialIds = userSavedTutorials.map(({ tutorialId }) => tutorialId); + let tutorialDtos = await getInstance().loadMany(tutorialIds); + tutorialDtos = tutorialDtos.filter((tutorialDto) => tutorialDto).sort(byId); + const tutorialEvaluations = await tutorialEvaluationRepository.find({ userId }); - let filteredTutorials = [...tutorials]; + let filteredTutorials = [...tutorialDtos]; if (filters.competences?.length) { - const filteredSkills = await skillRepository.findOperativeByCompetenceIds(filters.competences); + const competenceIds = filters.competences.split(','); + const filteredSkills = await skillRepository.findOperativeByCompetenceIds(competenceIds); const filteredTutorialIds = filteredSkills.flatMap(({ tutorialIds }) => tutorialIds); - filteredTutorials = tutorials.filter(({ id }) => filteredTutorialIds.includes(id)); + filteredTutorials = tutorialDtos.filter(({ id }) => filteredTutorialIds.includes(id)); } - const tutorialsForUser = _toTutorialsForUser({ + const tutorialsForUser = toTutorialsForUser({ tutorials: filteredTutorials, tutorialEvaluations, userSavedTutorials, @@ -46,25 +55,25 @@ const findPaginatedFilteredForCurrentUser = async function ({ userId, filters = const { results: models, pagination: meta } = paginateModule.paginate(sortedTutorialsForUser, page); return { models, meta }; -}; +} -const get = async function ({ tutorialId }) { - try { - const tutorialData = await tutorialDatasource.get(tutorialId); - return _toDomain(tutorialData); - } catch (error) { +export async function get({ tutorialId }) { + const tutorialDto = await getInstance().load(tutorialId); + if (!tutorialDto) { throw new NotFoundError('Tutorial not found'); } -}; + return toDomain(tutorialDto); +} -const list = async function ({ locale = FRENCH_FRANCE }) { - let tutorialData = await tutorialDatasource.list(); - const lang = _extractLangFromLocale(locale); - tutorialData = tutorialData.filter((tutorial) => _extractLangFromLocale(tutorial.locale) === lang); - return _.map(tutorialData, _toDomain); -}; +export async function list({ locale = FRENCH_FRANCE }) { + const cacheKey = `list({ locale: ${locale} })`; + const lang = extractLangFromLocale(locale); + const listByLangCallback = (knex) => knex.whereLike('locale', `${lang}%`).orderBy('id'); + const tutorialDtos = await getInstance().find(cacheKey, listByLangCallback); + return tutorialDtos.map(toDomain); +} -const findPaginatedFilteredRecommendedByUserId = async function ({ +export async function findPaginatedFilteredRecommendedByUserId({ userId, filters = {}, page, @@ -83,44 +92,47 @@ const findPaginatedFilteredRecommendedByUserId = async function ({ filteredSkills = skills.filter(({ competenceId }) => filters.competences.includes(competenceId)); } - const tutorialsForUser = []; - - for (const skill of filteredSkills) { - const tutorials = await _findByRecordIds({ ids: skill.tutorialIds, locale }); - - tutorialsForUser.push( - ..._toTutorialsForUserForRecommandation({ + const tutorialsForUserBySkill = await Promise.all( + filteredSkills.map(async (skill) => { + let tutorialDtos = await getInstance().loadMany(skill.tutorialIds); + tutorialDtos = tutorialDtos.map((tutorialDto) => tutorialDto); + if (locale) { + const lang = extractLangFromLocale(locale); + tutorialDtos = tutorialDtos.filter((tutorialDto) => extractLangFromLocale(tutorialDto.locale) === lang); + } + tutorialDtos.sort(byId); + const tutorials = tutorialDtos.map(toDomain); + + return toTutorialsForUserForRecommandation({ tutorials, tutorialEvaluations, userSavedTutorials, skillId: skill.id, - }), - ); - } + }); + }), + ); + + const tutorialsForUser = tutorialsForUserBySkill.flat(); return paginateModule.paginate(tutorialsForUser, page); -}; +} -export { - findByRecordIdsForCurrentUser, - findPaginatedFilteredForCurrentUser, - findPaginatedFilteredRecommendedByUserId, - get, - list, -}; +function byId(tutorial1, tutorial2) { + return tutorial1.id < tutorial2.id ? -1 : 1; +} -function _toDomain(tutorialData) { +function toDomain(tutorialDto) { return new Tutorial({ - id: tutorialData.id, - duration: tutorialData.duration, - format: tutorialData.format, - link: tutorialData.link, - source: tutorialData.source, - title: tutorialData.title, + id: tutorialDto.id, + duration: tutorialDto.duration, + format: tutorialDto.format, + link: tutorialDto.link, + source: tutorialDto.source, + title: tutorialDto.title, }); } -function _toTutorialsForUser({ tutorials, tutorialEvaluations, userSavedTutorials }) { +function toTutorialsForUser({ tutorials, tutorialEvaluations, userSavedTutorials }) { return tutorials.map((tutorial) => { const userSavedTutorial = userSavedTutorials.find(({ tutorialId }) => tutorialId === tutorial.id); const tutorialEvaluation = tutorialEvaluations.find(({ tutorialId }) => tutorialId === tutorial.id); @@ -133,7 +145,7 @@ function _toTutorialsForUser({ tutorials, tutorialEvaluations, userSavedTutorial }); } -function _toTutorialsForUserForRecommandation({ tutorials, tutorialEvaluations, userSavedTutorials, skillId }) { +function toTutorialsForUserForRecommandation({ tutorials, tutorialEvaluations, userSavedTutorials, skillId }) { return tutorials.map((tutorial) => { const userSavedTutorial = userSavedTutorials.find(({ tutorialId }) => tutorialId === tutorial.id); const tutorialEvaluation = tutorialEvaluations.find(({ tutorialId }) => tutorialId === tutorial.id); @@ -141,15 +153,20 @@ function _toTutorialsForUserForRecommandation({ tutorials, tutorialEvaluations, }); } -async function _findByRecordIds({ ids, locale }) { - let tutorialData = await tutorialDatasource.findByRecordIds(ids); - if (locale) { - const lang = _extractLangFromLocale(locale); - tutorialData = tutorialData.filter((tutorial) => _extractLangFromLocale(tutorial.locale) === lang); - } - return _.map(tutorialData, (tutorialData) => _toDomain(tutorialData)); +function extractLangFromLocale(locale) { + return locale && locale.split('-')[0]; } -function _extractLangFromLocale(locale) { - return locale && locale.split('-')[0]; +export function clearCache() { + return getInstance().clearCache(); +} + +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME }); + } + return instance; } diff --git a/api/src/shared/infrastructure/datasources/learning-content/index.js b/api/src/shared/infrastructure/datasources/learning-content/index.js deleted file mode 100644 index 0ba26489c35..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { tutorialDatasource } from '../../../../devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js'; - -export { tutorialDatasource }; diff --git a/api/tests/devcomp/integration/infrastructure/datasources/tutorial-datasource_test.js b/api/tests/devcomp/integration/infrastructure/datasources/tutorial-datasource_test.js deleted file mode 100644 index cd861955186..00000000000 --- a/api/tests/devcomp/integration/infrastructure/datasources/tutorial-datasource_test.js +++ /dev/null @@ -1,25 +0,0 @@ -import _ from 'lodash'; - -import { tutorialDatasource } from '../../../../../src/devcomp/infrastructure/datasources/learning-content/tutorial-datasource.js'; -import { expect, mockLearningContent } from '../../../../test-helper.js'; - -describe('Integration | Infrastructure | Datasource | Learning Content | TutorialDatasource', function () { - describe('#findByRecordIds', function () { - it('should return an array of tutorial data objects', async function () { - // given - const rawTutorial1 = { id: 'FAKE_REC_ID_RAW_TUTORIAL_1' }; - const rawTutorial2 = { id: 'FAKE_REC_ID_RAW_TUTORIAL_2' }; - const rawTutorial3 = { id: 'FAKE_REC_ID_RAW_TUTORIAL_3' }; - const records = [rawTutorial1, rawTutorial2, rawTutorial3]; - const lcmsApiCall = await mockLearningContent({ tutorials: records }); - - // when - const foundTutorials = await tutorialDatasource.findByRecordIds([rawTutorial1.id, rawTutorial3.id]); - - // then - expect(foundTutorials).to.be.an('array'); - expect(_.map(foundTutorials, 'id')).to.deep.equal([rawTutorial1.id, rawTutorial3.id]); - expect(lcmsApiCall.isDone()).to.be.true; - }); - }); -}); diff --git a/api/tests/devcomp/integration/infrastructure/repositories/tutorial-repository_test.js b/api/tests/devcomp/integration/infrastructure/repositories/tutorial-repository_test.js index 6094220b02e..3ae66342c1e 100644 --- a/api/tests/devcomp/integration/infrastructure/repositories/tutorial-repository_test.js +++ b/api/tests/devcomp/integration/infrastructure/repositories/tutorial-repository_test.js @@ -39,8 +39,8 @@ describe('Integration | Repository | tutorial-repository', function () { tutorialEvaluation: undefined, }, ]; - const learningContent = { tutorials: tutorialsList }; - await mockLearningContent(learningContent); + tutorialsList.forEach(databaseBuilder.factory.learningContent.buildTutorial); + await databaseBuilder.commit(); // when const tutorials = await tutorialRepository.findByRecordIdsForCurrentUser({ @@ -58,8 +58,6 @@ describe('Integration | Repository | tutorial-repository', function () { // given const userId = databaseBuilder.factory.buildUser().id; const userSavedTutorial = databaseBuilder.factory.buildUserSavedTutorial({ userId, tutorialId: 'recTutorial0' }); - await databaseBuilder.commit(); - const tutorial = { duration: '00:00:54', format: 'video', @@ -68,8 +66,9 @@ describe('Integration | Repository | tutorial-repository', function () { title: 'tuto0', id: 'recTutorial0', }; - const learningContent = { tutorials: [tutorial] }; - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildTutorial(tutorial); + await databaseBuilder.commit(); + // when const tutorials = await tutorialRepository.findByRecordIdsForCurrentUser({ ids: ['recTutorial0'], userId }); @@ -85,8 +84,6 @@ describe('Integration | Repository | tutorial-repository', function () { userId, tutorialId: 'recTutorial0', }); - await databaseBuilder.commit(); - const tutorial = { duration: '00:00:54', format: 'video', @@ -95,8 +92,9 @@ describe('Integration | Repository | tutorial-repository', function () { title: 'tuto0', id: 'recTutorial0', }; - const learningContent = { tutorials: [tutorial] }; - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildTutorial(tutorial); + await databaseBuilder.commit(); + // when const tutorials = await tutorialRepository.findByRecordIdsForCurrentUser({ ids: ['recTutorial0'], userId }); @@ -118,12 +116,8 @@ describe('Integration | Repository | tutorial-repository', function () { // given const tutorialId1 = 'rec1Tutorial'; const tutorialId2 = 'rec2Tutorial'; - - const learningContent = { - tutorials: [{ id: tutorialId1 }, { id: tutorialId2 }], - }; - await mockLearningContent(learningContent); - + databaseBuilder.factory.learningContent.buildTutorial({ id: tutorialId1 }); + databaseBuilder.factory.learningContent.buildTutorial({ id: tutorialId2 }); const firstUserSavedTutorial = databaseBuilder.factory.buildUserSavedTutorial({ tutorialId: tutorialId1, userId, @@ -159,12 +153,7 @@ describe('Integration | Repository | tutorial-repository', function () { it('should return tutorial with evaluated tutorial belonging to given user', async function () { // given const tutorialId = 'recTutorial'; - - const learningContent = { - tutorials: [{ id: tutorialId }], - }; - await mockLearningContent(learningContent); - + databaseBuilder.factory.learningContent.buildTutorial({ id: tutorialId }); databaseBuilder.factory.buildUserSavedTutorial({ tutorialId, userId }); databaseBuilder.factory.buildTutorialEvaluation({ tutorialId, userId }); await databaseBuilder.commit(); @@ -185,32 +174,27 @@ describe('Integration | Repository | tutorial-repository', function () { const tutorialId1 = 'tutorial1'; const tutorialId2 = 'tutorial2'; const tutorialId3 = 'tutorial3'; - - const learningContent = { - tutorials: [{ id: tutorialId1 }, { id: tutorialId2 }, { id: tutorialId3 }], - skills: [ - { - id: 'skill1', - tutorialIds: [tutorialId1], - competenceId: 'competence1', - status: 'actif', - }, - { - id: 'skill2', - tutorialIds: [tutorialId2], - competenceId: 'competence2', - status: 'archivé', - }, - { - id: 'skill3', - tutorialIds: [tutorialId3], - competenceId: 'competence3', - status: 'actif', - }, - ], - }; - await mockLearningContent(learningContent); - + databaseBuilder.factory.learningContent.buildTutorial({ id: tutorialId1 }); + databaseBuilder.factory.learningContent.buildTutorial({ id: tutorialId2 }); + databaseBuilder.factory.learningContent.buildTutorial({ id: tutorialId3 }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'skill1', + tutorialIds: [tutorialId1], + competenceId: 'competence1', + status: 'actif', + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'skill2', + tutorialIds: [tutorialId2], + competenceId: 'competence2', + status: 'archivé', + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'skill3', + tutorialIds: [tutorialId3], + competenceId: 'competence3', + status: 'actif', + }); databaseBuilder.factory.buildUserSavedTutorial({ tutorialId: tutorialId1, userId, @@ -228,8 +212,7 @@ describe('Integration | Repository | tutorial-repository', function () { createdAt: new Date('2022-05-02'), }); await databaseBuilder.commit(); - - const filters = { competences: ['competence2', 'competence3'] }; + const filters = { competences: 'competence2,competence3' }; // when const { models: tutorialsForUser } = await tutorialRepository.findPaginatedFilteredForCurrentUser({ @@ -246,8 +229,6 @@ describe('Integration | Repository | tutorial-repository', function () { context('when user has not saved tutorial', function () { it('should return an empty list', async function () { - await mockLearningContent({ tutorials: [] }); - const { models: tutorialsForUser } = await tutorialRepository.findPaginatedFilteredForCurrentUser({ userId, }); @@ -259,7 +240,6 @@ describe('Integration | Repository | tutorial-repository', function () { context('when user has saved a tutorial which is not available anymore', function () { it('should return an empty list', async function () { - await mockLearningContent({ tutorials: [] }); databaseBuilder.factory.buildUserSavedTutorial({ tutorialId: 'recTutorial', userId }); await databaseBuilder.commit(); @@ -273,12 +253,10 @@ describe('Integration | Repository | tutorial-repository', function () { it('should return row count of existing tutorials', async function () { // given - const learningContent = { - tutorials: [{ id: 'tuto1' }, { id: 'tuto2' }, { id: 'tuto3' }, { id: 'tuto4' }], - }; - - await mockLearningContent(learningContent); - + databaseBuilder.factory.learningContent.buildTutorial({ id: 'tuto1' }); + databaseBuilder.factory.learningContent.buildTutorial({ id: 'tuto2' }); + databaseBuilder.factory.learningContent.buildTutorial({ id: 'tuto3' }); + databaseBuilder.factory.learningContent.buildTutorial({ id: 'tuto4' }); databaseBuilder.factory.buildUserSavedTutorial({ tutorialId: 'tuto1', userId, @@ -305,7 +283,6 @@ describe('Integration | Repository | tutorial-repository', function () { createdAt: new Date('2022-05-05'), }); await databaseBuilder.commit(); - const expectedTutorialIds = ['tuto4', 'tuto3', 'tuto2', 'tuto1']; // when @@ -330,10 +307,10 @@ describe('Integration | Repository | tutorial-repository', function () { domainBuilder.buildTutorial({ id: 'tutorialain' }), domainBuilder.buildTutorial({ id: 'tutorialadin' }), ]; - await mockLearningContent({ tutorials }); - tutorials.forEach((tutorial) => - databaseBuilder.factory.buildUserSavedTutorial({ userId, tutorialId: tutorial.id }), - ); + tutorials.forEach((tutorial) => { + databaseBuilder.factory.learningContent.buildTutorial(tutorial); + databaseBuilder.factory.buildUserSavedTutorial({ userId, tutorialId: tutorial.id }); + }); const expectedPagination = { page: 2, pageSize: 2, pageCount: 2, rowCount: 4 }; await databaseBuilder.commit(); @@ -365,24 +342,30 @@ describe('Integration | Repository | tutorial-repository', function () { context('when tutorial exists', function () { it('should return the tutorial', async function () { // given - const tutorials = [ - { + databaseBuilder.factory.learningContent.buildTutorial({ + duration: '00:00:54', + format: 'video', + link: 'https://tuto.fr', + source: 'tuto.fr', + title: 'tuto0', + id: 'recTutorial0', + }); + await databaseBuilder.commit(); + + // when + const tutorial = await tutorialRepository.get({ tutorialId: 'recTutorial0' }); + + // then + expect(tutorial).to.deep.equal( + domainBuilder.buildTutorial({ duration: '00:00:54', format: 'video', link: 'https://tuto.fr', source: 'tuto.fr', title: 'tuto0', id: 'recTutorial0', - }, - ]; - const learningContent = { tutorials: tutorials }; - await mockLearningContent(learningContent); - - // when - const tutorial = await tutorialRepository.get({ tutorialId: 'recTutorial0' }); - - // then - expect(tutorial).to.deep.equal(tutorials[0]); + }), + ); }); }); }); @@ -421,8 +404,9 @@ describe('Integration | Repository | tutorial-repository', function () { locale: 'en-us', }, ]; - const learningContent = { tutorials: [...frenchTutorials, ...englishTutorials] }; - await mockLearningContent(learningContent); + englishTutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); + frenchTutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); + await databaseBuilder.commit(); // when const tutorials = await tutorialRepository.list({}); @@ -455,8 +439,9 @@ describe('Integration | Repository | tutorial-repository', function () { id: 'recTutorial1', locale: 'en-us', }; - const learningContent = { tutorials: [frenchTutorial, englishTutorial] }; - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildTutorial(frenchTutorial); + databaseBuilder.factory.learningContent.buildTutorial(englishTutorial); + await databaseBuilder.commit(); // when const tutorials = await tutorialRepository.list({ locale }); @@ -478,8 +463,8 @@ describe('Integration | Repository | tutorial-repository', function () { title: 'tuto0', id: 'recTutorial0', }; - const learningContent = { tutorials: [tutorial] }; - await mockLearningContent(learningContent); + databaseBuilder.factory.learningContent.buildTutorial(tutorial); + await databaseBuilder.commit(); // when const tutorials = await tutorialRepository.list({ locale }); @@ -500,25 +485,20 @@ describe('Integration | Repository | tutorial-repository', function () { describe('when there are no invalidated and direct KE', function () { it('should return an empty page', async function () { // given - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - duration: '00:00:54', - format: 'video', - link: 'http://www.example.com/this-is-an-example.html', - source: 'tuto.com', - title: 'tuto1', - }, - ], - skills: [ - { - id: 'recSkill1', - tutorialIds: ['tuto1', 'tuto2'], - status: 'actif', - }, - ], + databaseBuilder.factory.learningContent.buildTutorial({ + id: 'tuto1', + duration: '00:00:54', + format: 'video', + link: 'http://www.example.com/this-is-an-example.html', + source: 'tuto.com', + title: 'tuto1', }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill1', + tutorialIds: ['tuto1', 'tuto2'], + status: 'actif', + }); + await databaseBuilder.commit(); // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId }); @@ -536,27 +516,20 @@ describe('Integration | Repository | tutorial-repository', function () { userId, status: KnowledgeElement.StatusType.VALIDATED, }); - await databaseBuilder.commit(); - - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - duration: '00:00:54', - format: 'video', - link: 'http://www.example.com/this-is-an-example.html', - source: 'tuto.com', - title: 'tuto1', - }, - ], - skills: [ - { - id: 'recSkill1', - tutorialIds: ['tuto1', 'tuto2'], - status: 'actif', - }, - ], + databaseBuilder.factory.learningContent.buildTutorial({ + id: 'tuto1', + duration: '00:00:54', + format: 'video', + link: 'http://www.example.com/this-is-an-example.html', + source: 'tuto.com', + title: 'tuto1', + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill1', + tutorialIds: ['tuto1', 'tuto2'], + status: 'actif', }); + await databaseBuilder.commit(); // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId }); @@ -569,6 +542,39 @@ describe('Integration | Repository | tutorial-repository', function () { describe('when there is one invalidated KE', function () { it('should return all fields from recommended tutorials', async function () { // given + const tutorials = [ + { + id: 'tuto1', + locale: 'fr-fr', + link: 'https//example.net/tuto1', + source: 'wikipedia', + title: 'Mon super tuto', + format: 'video', + duration: '2min', + }, + { + id: 'tuto2', + locale: 'fr-fr', + }, + { + id: 'tuto5', + locale: 'fr-fr', + }, + ]; + const skills = [ + { + id: 'recSkill1', + tutorialIds: ['tuto1', 'tuto2'], + status: 'actif', + }, + { + id: 'recSkill4', + tutorialIds: ['tuto5'], + status: 'archivé', + }, + ]; + tutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); + skills.forEach(databaseBuilder.factory.learningContent.buildSkill); databaseBuilder.factory.buildKnowledgeElement({ skillId: 'recSkill1', userId, @@ -581,40 +587,6 @@ describe('Integration | Repository | tutorial-repository', function () { }); await databaseBuilder.commit(); - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - locale: 'fr-fr', - link: 'https//example.net/tuto1', - source: 'wikipedia', - title: 'Mon super tuto', - format: 'video', - duration: '2min', - }, - { - id: 'tuto2', - locale: 'fr-fr', - }, - { - id: 'tuto5', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill1', - tutorialIds: ['tuto1', 'tuto2'], - status: 'actif', - }, - { - id: 'recSkill4', - tutorialIds: ['tuto5'], - status: 'archivé', - }, - ], - }); - // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId }); @@ -634,6 +606,34 @@ describe('Integration | Repository | tutorial-repository', function () { it('should return tutorial related to user locale', async function () { // given const locale = 'en-us'; + const tutorials = [ + { + id: 'tuto1', + locale: 'en-us', + }, + { + id: 'tuto2', + locale: 'en-us', + }, + { + id: 'tuto5', + locale: 'fr-fr', + }, + ]; + const skills = [ + { + id: 'recSkill1', + tutorialIds: ['tuto1', 'tuto2'], + status: 'actif', + }, + { + id: 'recSkill4', + tutorialIds: ['tuto5'], + status: 'archivé', + }, + ]; + tutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); + skills.forEach(databaseBuilder.factory.learningContent.buildSkill); databaseBuilder.factory.buildKnowledgeElement({ skillId: 'recSkill1', userId, @@ -646,34 +646,7 @@ describe('Integration | Repository | tutorial-repository', function () { }); await databaseBuilder.commit(); - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - locale: 'en-us', - }, - { - id: 'tuto2', - locale: 'en-us', - }, - { - id: 'tuto5', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill1', - tutorialIds: ['tuto1', 'tuto2'], - status: 'actif', - }, - { - id: 'recSkill4', - tutorialIds: ['tuto5'], - status: 'archivé', - }, - ], - }); + await mockLearningContent({}); // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId, locale }); @@ -691,23 +664,16 @@ describe('Integration | Repository | tutorial-repository', function () { userId, status: KnowledgeElement.StatusType.INVALIDATED, }); - await databaseBuilder.commit(); - - await mockLearningContent({ - tutorials: [ - { - id: 'tuto4', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill3', - tutorialIds: ['tuto4'], - status: 'périmé', - }, - ], + databaseBuilder.factory.learningContent.buildTutorial({ + id: 'tuto4', + locale: 'fr-fr', }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill3', + tutorialIds: ['tuto4'], + status: 'périmé', + }); + await databaseBuilder.commit(); // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId }); @@ -720,6 +686,18 @@ describe('Integration | Repository | tutorial-repository', function () { describe('when there is one invalidated KE and two skills referencing the same tutorial', function () { it('should return the same tutorial related to each skill', async function () { // given + const skills = [ + { + id: 'recSkill1', + tutorialIds: ['tuto1'], + status: 'actif', + }, + { + id: 'recSkill2', + tutorialIds: ['tuto1'], + status: 'actif', + }, + ]; databaseBuilder.factory.buildKnowledgeElement({ skillId: 'recSkill1', userId, @@ -730,28 +708,12 @@ describe('Integration | Repository | tutorial-repository', function () { userId, status: KnowledgeElement.StatusType.INVALIDATED, }); - await databaseBuilder.commit(); - - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill1', - tutorialIds: ['tuto1'], - status: 'actif', - }, - { - id: 'recSkill2', - tutorialIds: ['tuto1'], - status: 'actif', - }, - ], + skills.forEach(databaseBuilder.factory.learningContent.buildSkill); + databaseBuilder.factory.learningContent.buildTutorial({ + id: 'tuto1', + locale: 'fr-fr', }); + await databaseBuilder.commit(); // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId }); @@ -789,23 +751,16 @@ describe('Integration | Repository | tutorial-repository', function () { tutorialId: 'tuto4', userId, }).id; - await databaseBuilder.commit(); - - await mockLearningContent({ - tutorials: [ - { - id: 'tuto4', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill3', - tutorialIds: ['tuto4'], - status: 'actif', - }, - ], + databaseBuilder.factory.learningContent.buildTutorial({ + id: 'tuto4', + locale: 'fr-fr', + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill3', + tutorialIds: ['tuto4'], + status: 'actif', }); + await databaseBuilder.commit(); // when const { results } = await tutorialRepository.findPaginatedFilteredRecommendedByUserId({ userId }); @@ -830,36 +785,31 @@ describe('Integration | Repository | tutorial-repository', function () { it('should return page size number of tutorials', async function () { // given const page = { number: 2, size: 2 }; + const tutorials = [ + { + id: 'tuto4', + locale: 'fr-fr', + }, + { + id: 'tuto5', + locale: 'fr-fr', + }, + { + id: 'tuto6', + locale: 'fr-fr', + }, + ]; databaseBuilder.factory.buildKnowledgeElement({ skillId: 'recSkill3', userId, status: KnowledgeElement.StatusType.INVALIDATED, source: KnowledgeElement.SourceType.DIRECT, }); - await databaseBuilder.commit(); - - await mockLearningContent({ - tutorials: [ - { - id: 'tuto4', - locale: 'fr-fr', - }, - { - id: 'tuto5', - locale: 'fr-fr', - }, - { - id: 'tuto6', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill3', - tutorialIds: ['tuto4', 'tuto5', 'tuto6'], - status: 'actif', - }, - ], + tutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill3', + tutorialIds: ['tuto4', 'tuto5', 'tuto6'], + status: 'actif', }); const expectedPagination = { page: 2, pageSize: 2, pageCount: 2, rowCount: 3 }; await databaseBuilder.commit(); @@ -881,6 +831,58 @@ describe('Integration | Repository | tutorial-repository', function () { it('should return only tutorials for skills associated to competences', async function () { // given const page = { number: 1, size: 10 }; + const tutorials = [ + { + id: 'tuto1', + locale: 'fr-fr', + }, + { + id: 'tuto2', + locale: 'fr-fr', + }, + { + id: 'tuto3', + locale: 'fr-fr', + }, + { + id: 'tuto4', + locale: 'fr-fr', + }, + { + id: 'tuto5', + locale: 'fr-fr', + }, + { + id: 'tuto6', + locale: 'fr-fr', + }, + ]; + const skills = [ + { + id: 'recSkill1InCompetence1', + tutorialIds: ['tuto1', 'tuto2'], + status: 'actif', + competenceId: 'competence1', + }, + { + id: 'recSkill2InCompetence2', + tutorialIds: ['tuto3', 'tuto4'], + status: 'actif', + competenceId: 'competence2', + }, + { + id: 'recSkill3InCompetence2', + tutorialIds: ['tuto5'], + status: 'actif', + competenceId: 'competence2', + }, + { + id: 'recSkill4InCompetence3', + tutorialIds: ['tuto6'], + status: 'actif', + competenceId: 'competence3', + }, + ]; databaseBuilder.factory.buildKnowledgeElement({ skillId: 'recSkill1InCompetence1', userId, @@ -905,66 +907,12 @@ describe('Integration | Repository | tutorial-repository', function () { status: KnowledgeElement.StatusType.INVALIDATED, source: KnowledgeElement.SourceType.DIRECT, }); + skills.forEach(databaseBuilder.factory.learningContent.buildSkill); + tutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); await databaseBuilder.commit(); - - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - locale: 'fr-fr', - }, - { - id: 'tuto2', - locale: 'fr-fr', - }, - { - id: 'tuto3', - locale: 'fr-fr', - }, - { - id: 'tuto4', - locale: 'fr-fr', - }, - { - id: 'tuto5', - locale: 'fr-fr', - }, - { - id: 'tuto6', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill1InCompetence1', - tutorialIds: ['tuto1', 'tuto2'], - status: 'actif', - competenceId: 'competence1', - }, - { - id: 'recSkill2InCompetence2', - tutorialIds: ['tuto3', 'tuto4'], - status: 'actif', - competenceId: 'competence2', - }, - { - id: 'recSkill3InCompetence2', - tutorialIds: ['tuto5'], - status: 'actif', - competenceId: 'competence2', - }, - { - id: 'recSkill4InCompetence3', - tutorialIds: ['tuto6'], - status: 'actif', - competenceId: 'competence3', - }, - ], - }); const expectedPagination = { page: 1, pageSize: 10, pageCount: 1, rowCount: 4 }; - await databaseBuilder.commit(); - const filters = { competences: ['competence2', 'competence3'] }; + const filters = { competences: 'competence2,competence3' }; // when const { results: foundTutorials, pagination } = @@ -983,6 +931,58 @@ describe('Integration | Repository | tutorial-repository', function () { it('should return only tutorials for skills associated to competences for another page', async function () { // given const page = { number: 2, size: 2 }; + const tutorials = [ + { + id: 'tuto1', + locale: 'fr-fr', + }, + { + id: 'tuto2', + locale: 'fr-fr', + }, + { + id: 'tuto3', + locale: 'fr-fr', + }, + { + id: 'tuto4', + locale: 'fr-fr', + }, + { + id: 'tuto5', + locale: 'fr-fr', + }, + { + id: 'tuto6', + locale: 'fr-fr', + }, + ]; + const skills = [ + { + id: 'recSkill1InCompetence1', + tutorialIds: ['tuto1', 'tuto2'], + status: 'actif', + competenceId: 'competence1', + }, + { + id: 'recSkill2InCompetence2', + tutorialIds: ['tuto3', 'tuto4'], + status: 'actif', + competenceId: 'competence2', + }, + { + id: 'recSkill3InCompetence2', + tutorialIds: ['tuto5'], + status: 'actif', + competenceId: 'competence2', + }, + { + id: 'recSkill4InCompetence3', + tutorialIds: ['tuto6'], + status: 'actif', + competenceId: 'competence3', + }, + ]; databaseBuilder.factory.buildKnowledgeElement({ skillId: 'recSkill1InCompetence1', userId, @@ -1007,66 +1007,14 @@ describe('Integration | Repository | tutorial-repository', function () { status: KnowledgeElement.StatusType.INVALIDATED, source: KnowledgeElement.SourceType.DIRECT, }); + skills.forEach(databaseBuilder.factory.learningContent.buildSkill); + tutorials.forEach(databaseBuilder.factory.learningContent.buildTutorial); await databaseBuilder.commit(); - await mockLearningContent({ - tutorials: [ - { - id: 'tuto1', - locale: 'fr-fr', - }, - { - id: 'tuto2', - locale: 'fr-fr', - }, - { - id: 'tuto3', - locale: 'fr-fr', - }, - { - id: 'tuto4', - locale: 'fr-fr', - }, - { - id: 'tuto5', - locale: 'fr-fr', - }, - { - id: 'tuto6', - locale: 'fr-fr', - }, - ], - skills: [ - { - id: 'recSkill1InCompetence1', - tutorialIds: ['tuto1', 'tuto2'], - status: 'actif', - competenceId: 'competence1', - }, - { - id: 'recSkill2InCompetence2', - tutorialIds: ['tuto3', 'tuto4'], - status: 'actif', - competenceId: 'competence2', - }, - { - id: 'recSkill3InCompetence2', - tutorialIds: ['tuto5'], - status: 'actif', - competenceId: 'competence2', - }, - { - id: 'recSkill4InCompetence3', - tutorialIds: ['tuto6'], - status: 'actif', - competenceId: 'competence3', - }, - ], - }); + await mockLearningContent({}); const expectedPagination = { page: 2, pageSize: 2, pageCount: 2, rowCount: 4 }; - await databaseBuilder.commit(); - const filters = { competences: ['competence2', 'competence3'] }; + const filters = { competences: 'competence2,competence3' }; // when const { results: foundTutorials, pagination } = diff --git a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js index 7da6b811496..10c7fc2d64a 100644 --- a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js +++ b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js @@ -1,5 +1,4 @@ import { patchLearningContentCacheEntry } from '../../../../../src/learning-content/domain/usecases/patch-learning-content-cache-entry.js'; -import * as LearningContentDatasources from '../../../../../src/shared/infrastructure/datasources/learning-content/index.js'; import { expect, sinon } from '../../../../test-helper.js'; describe('Learning Content | Unit | Domain | Usecase | Patch learning content cache entry', function () { @@ -111,7 +110,6 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca updatedRecord, modelName, LearningContentCache, - LearningContentDatasources, ...repositories, }); @@ -200,7 +198,6 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca updatedRecord, modelName, LearningContentCache, - LearningContentDatasources, ...repositories, }); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index 3462d941ab1..dbeffc763e0 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -22,6 +22,7 @@ import * as frameworkRepository from '../lib/infrastructure/repositories/framewo import * as thematicRepository from '../lib/infrastructure/repositories/thematic-repository.js'; import * as tubeRepository from '../lib/infrastructure/repositories/tube-repository.js'; import { PIX_ADMIN } from '../src/authorization/domain/constants.js'; +import * as tutorialRepository from '../src/devcomp/infrastructure/repositories/tutorial-repository.js'; import { config } from '../src/shared/config.js'; import { Membership } from '../src/shared/domain/models/index.js'; import * as tokenService from '../src/shared/domain/services/token-service.js'; @@ -87,6 +88,7 @@ afterEach(function () { skillRepository.clearCache(); challengeRepository.clearCache(); courseRepository.clearCache(); + tutorialRepository.clearCache(); return databaseBuilder.clean(); }); From 12b99c6ed3e5c5ef02a2ab729d8a11d56cb8edb2 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Sat, 30 Nov 2024 23:32:02 +0100 Subject: [PATCH 13/24] feat(api): refacto missionRepository with new cache and to use PG --- .../learning-content/mission-datasource.js | 7 - .../repositories/mission-repository.js | 90 +++--- .../usecases/find-all-active-missions_test.js | 283 ++++++++---------- .../domain/usecases/get-mission_test.js | 59 ++-- .../usecases/get-next-challenge_test.js | 2 +- .../repositories/mission-repository_test.js | 132 ++++---- api/tests/test-helper.js | 2 + 7 files changed, 278 insertions(+), 297 deletions(-) delete mode 100644 api/src/school/infrastructure/datasources/learning-content/mission-datasource.js diff --git a/api/src/school/infrastructure/datasources/learning-content/mission-datasource.js b/api/src/school/infrastructure/datasources/learning-content/mission-datasource.js deleted file mode 100644 index f2afba3c599..00000000000 --- a/api/src/school/infrastructure/datasources/learning-content/mission-datasource.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as datasource from '../../../../shared/infrastructure/datasources/learning-content/datasource.js'; - -const missionDatasource = datasource.extend({ - modelName: 'missions', -}); - -export { missionDatasource }; diff --git a/api/src/school/infrastructure/repositories/mission-repository.js b/api/src/school/infrastructure/repositories/mission-repository.js index e3a17ccdec1..6791fd51915 100644 --- a/api/src/school/infrastructure/repositories/mission-repository.js +++ b/api/src/school/infrastructure/repositories/mission-repository.js @@ -1,57 +1,71 @@ import { config } from '../../../shared/config.js'; import { LOCALE } from '../../../shared/domain/constants.js'; import { getTranslatedKey } from '../../../shared/domain/services/get-translated-text.js'; +import { LearningContentRepository } from '../../../shared/infrastructure/repositories/learning-content-repository.js'; import { Mission, MissionContent, MissionStep } from '../../domain/models/Mission.js'; import { MissionNotFoundError } from '../../domain/school-errors.js'; -import { missionDatasource } from '../datasources/learning-content/mission-datasource.js'; - const { FRENCH_FRANCE } = LOCALE; +const TABLE_NAME = 'learningcontent.missions'; -function _toDomain(data, locale) { - const translatedName = getTranslatedKey(data.name_i18n, locale); - const translatedLearningObjectives = getTranslatedKey(data.learningObjectives_i18n, locale); - const translatedValidatedObjectives = getTranslatedKey(data.validatedObjectives_i18n, locale); - const translatedContent = getTranslatedContent(data.content, locale); - return new Mission({ - id: data.id, - name: translatedName, - cardImageUrl: data.cardImageUrl, - competenceId: data.competenceId, - thematicId: data.thematicId, - learningObjectives: translatedLearningObjectives, - validatedObjectives: translatedValidatedObjectives, - introductionMediaUrl: data.introductionMediaUrl, - introductionMediaType: data.introductionMediaType, - introductionMediaAlt: data.introductionMediaAlt, - documentationUrl: data.documentationUrl, - content: translatedContent, - }); -} - -async function get(id, locale = { locale: FRENCH_FRANCE }) { - try { - const missionData = await missionDatasource.get(parseInt(id, 10)); - return _toDomain(missionData, locale); - } catch (error) { +export async function get(id, locale = FRENCH_FRANCE) { + const parsedIntId = parseInt(id, 10); + if (isNaN(parsedIntId)) { + throw new MissionNotFoundError(id); + } + const missionDto = await getInstance().load(parsedIntId); + if (!missionDto) { throw new MissionNotFoundError(id); } + return toDomain(missionDto, locale); } -async function findAllActiveMissions(locale = { locale: FRENCH_FRANCE }) { - const allMissions = await missionDatasource.list(); - const allActiveMissions = allMissions.filter((mission) => { - return ( - mission.status === 'VALIDATED' || - (config.featureToggles.showExperimentalMissions && mission.status === 'EXPERIMENTAL') - ); - }); - return allActiveMissions.map((missionData) => _toDomain(missionData, locale)); +export async function findAllActiveMissions(locale = { locale: FRENCH_FRANCE }) { + const cacheKey = 'findAllActiveMissions()'; + const acceptedStatuses = config.featureToggles.showExperimentalMissions + ? ['VALIDATED', 'EXPERIMENTAL'] + : ['VALIDATED']; + const findActiveCallback = (knex) => knex.whereIn('status', acceptedStatuses).orderBy('id'); + const missionDtos = await getInstance().find(cacheKey, findActiveCallback); + return missionDtos.map((missionDto) => toDomain(missionDto, locale)); } -export { findAllActiveMissions, get }; +export function clearCache() { + return getInstance().clearCache(); +} function getTranslatedContent(content, locale) { const contentWithTranslatedSteps = content?.steps?.map((step) => new MissionStep({ ...step, name: getTranslatedKey(step.name_i18n, locale) })) || []; return new MissionContent({ ...content, steps: contentWithTranslatedSteps }); } + +function toDomain(missionDto, locale) { + const translatedName = getTranslatedKey(missionDto.name_i18n, locale); + const translatedLearningObjectives = getTranslatedKey(missionDto.learningObjectives_i18n, locale); + const translatedValidatedObjectives = getTranslatedKey(missionDto.validatedObjectives_i18n, locale); + const translatedIntroductionMediaAlt = getTranslatedKey(missionDto.introductionMediaAlt_i18n, locale); + const translatedContent = getTranslatedContent(missionDto.content, locale); + return new Mission({ + id: missionDto.id, + name: translatedName, + cardImageUrl: missionDto.cardImageUrl, + competenceId: missionDto.competenceId, + learningObjectives: translatedLearningObjectives, + validatedObjectives: translatedValidatedObjectives, + introductionMediaUrl: missionDto.introductionMediaUrl, + introductionMediaType: missionDto.introductionMediaType, + introductionMediaAlt: translatedIntroductionMediaAlt, + documentationUrl: missionDto.documentationUrl, + content: translatedContent, + }); +} + +/** @type {LearningContentRepository} */ +let instance; + +function getInstance() { + if (!instance) { + instance = new LearningContentRepository({ tableName: TABLE_NAME, idType: 'integer' }); + } + return instance; +} diff --git a/api/tests/school/integration/domain/usecases/find-all-active-missions_test.js b/api/tests/school/integration/domain/usecases/find-all-active-missions_test.js index 2b4c66f7f2d..ec52c780d34 100644 --- a/api/tests/school/integration/domain/usecases/find-all-active-missions_test.js +++ b/api/tests/school/integration/domain/usecases/find-all-active-missions_test.js @@ -1,199 +1,182 @@ import { Mission } from '../../../../../src/school/domain/models/Mission.js'; import { usecases } from '../../../../../src/school/domain/usecases/index.js'; import { config } from '../../../../../src/shared/config.js'; -import { databaseBuilder, expect, mockLearningContent } from '../../../../test-helper.js'; -import * as learningContentBuilder from '../../../../tooling/learning-content-builder/index.js'; +import { databaseBuilder, expect } from '../../../../test-helper.js'; describe('Integration | UseCases | find-all-active-missions', function () { - it('returns empty array without missions from LCMS', async function () { - const expectedMissions = []; - - await mockLearningContent({ - missions: [], + context('when no active missions', function () { + it('returns empty array without missions from LCMS', async function () { + const returnedMissions = await usecases.findAllActiveMissions(); + expect(returnedMissions).to.deep.equal([]); }); - - const returnedMissions = await usecases.findAllActiveMissions(); - expect(returnedMissions).to.deep.equal(expectedMissions); }); - context('when FT_SHOW_EXPERIMENTAL_MISSION is false', function () { - beforeEach(function () { - config.featureToggles.showExperimentalMissions = false; - }); - - it('returns validated missions from LCMS', async function () { - const validatedMission = learningContentBuilder.buildMission({ + context('when active missions', function () { + let missionValideeDB, missionExperimentaleDB; + let organizationId; + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildArea({ + id: 'areaId', + code: '3', + competenceIds: ['competenceId'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'competenceId', + index: '4.5', + name_i18n: { + fr: 'Competence', + }, + areaId: 'areaId', + }); + missionValideeDB = databaseBuilder.factory.learningContent.buildMission({ id: 12, - name_i18n: { fr: 'truc' }, + name_i18n: { fr: 'name fr missionValideeDB' }, competenceId: 'competenceId', thematicId: 'thematicId', status: 'VALIDATED', - learningObjectives_i18n: { fr: 'Il était une fois' }, - validatedObjectives_i18n: { fr: 'Bravo ! tu as réussi !' }, + learningObjectives_i18n: { fr: 'learningObjectives fr missionValideeDB' }, + validatedObjectives_i18n: { fr: 'validatedObjectives fr missionValideeDB' }, + introductionMediaAlt_i18n: { fr: 'introductionMediaAlt fr missionValideeDB' }, content: { steps: [ { - name_i18n: { fr: 'truc' }, + name_i18n: { fr: 'content step name fr missionValideeDB' }, }, ], dareChallenges: [], }, }); - - const experimentalMission = learningContentBuilder.buildMission({ + missionExperimentaleDB = databaseBuilder.factory.learningContent.buildMission({ id: 13, - name_i18n: { fr: 'truc' }, + name_i18n: { fr: 'name fr missionExperimentaleDB' }, competenceId: 'competenceId', thematicId: 'thematicId', status: 'EXPERIMENTAL', - learningObjectives_i18n: { fr: 'Il était une fois' }, - validatedObjectives_i18n: { fr: 'Bravo ! tu as réussi !' }, + learningObjectives_i18n: { fr: 'learningObjectives fr missionExperimentaleDB' }, + validatedObjectives_i18n: { fr: 'validatedObjectives fr missionExperimentaleDB' }, + introductionMediaAlt_i18n: { fr: 'introductionMediaAlt fr missionExperimentaleDB' }, + content: { + steps: [ + { + name_i18n: { fr: 'content step name fr missionExperimentaleDB' }, + }, + ], + dareChallenges: [], + }, }); - - const inactiveMission = learningContentBuilder.buildMission({ + databaseBuilder.factory.learningContent.buildMission({ id: 14, - name_i18n: { fr: 'truc' }, + name_i18n: { fr: 'name fr missionInactiveDB' }, competenceId: 'competenceId', thematicId: 'thematicId', status: 'INACTIVE', - learningObjectives_i18n: { fr: 'Il était une fois' }, - validatedObjectives_i18n: { fr: 'Bravo ! tu as réussi !' }, - }); - - const area = learningContentBuilder.buildArea({ - code: 3, - competenceIds: ['competenceId'], - }); - - const organizationId = databaseBuilder.factory.buildOrganization().id; - - const competence = { - id: 'competenceId', - index: '4.5', - name_i18n: { - fr: 'Competence', - }, - }; - - await mockLearningContent({ - missions: [validatedMission, experimentalMission, inactiveMission], - areas: [area], - competences: [competence], - }); - - const expectedMission = new Mission({ - id: 12, - name: 'truc', - competenceId: 'competenceId', - thematicId: 'thematicId', - competenceName: '4.5 Competence', - status: 'ACTIVE', - areaCode: 3, - learningObjectives: 'Il était une fois', - validatedObjectives: 'Bravo ! tu as réussi !', - startedBy: '', + learningObjectives_i18n: { fr: 'learningObjectives fr missionInactiveDB' }, + validatedObjectives_i18n: { fr: 'validatedObjectives fr missionInactiveDB' }, + introductionMediaAlt_i18n: { fr: 'introductionMediaAlt fr missionInactiveDB' }, content: { steps: [ { - name: 'truc', + name_i18n: { fr: 'content step name fr missionInactiveDB' }, }, ], dareChallenges: [], }, }); - const expectedMissions = [expectedMission]; - const returnedMissions = await usecases.findAllActiveMissions({ organizationId }); - - expect(returnedMissions).to.deep.equal(expectedMissions); - }); - }); - - context('when FT_SHOW_EXPERIMENTAL_MISSION is true', function () { - beforeEach(function () { - config.featureToggles.showExperimentalMissions = true; + organizationId = databaseBuilder.factory.buildOrganization().id; + await databaseBuilder.commit(); }); - it('returns validated and experimental missions from LCMS', async function () { - const validatedMission = learningContentBuilder.buildMission({ - id: 12, - name_i18n: { fr: 'truc' }, - competenceId: 'competenceId', - thematicId: 'thematicId', - status: 'VALIDATED', - learningObjectives_i18n: { fr: 'Il était une fois' }, - validatedObjectives_i18n: { fr: 'Bravo ! tu as réussi !' }, + context('when FT_SHOW_EXPERIMENTAL_MISSION is false', function () { + beforeEach(function () { + config.featureToggles.showExperimentalMissions = false; }); - const experimentalMission = learningContentBuilder.buildMission({ - id: 13, - name_i18n: { fr: 'truc' }, - competenceId: 'competenceId', - thematicId: 'thematicId', - status: 'EXPERIMENTAL', - learningObjectives_i18n: { fr: 'Il était une fois' }, - validatedObjectives_i18n: { fr: 'Bravo ! tu as réussi !' }, - }); - - const inactiveMission = learningContentBuilder.buildMission({ - id: 14, - name_i18n: { fr: 'truc' }, - competenceId: 'competenceId', - thematicId: 'thematicId', - status: 'INACTIVE', - learningObjectives_i18n: { fr: 'Il était une fois' }, - validatedObjectives_i18n: { fr: 'Bravo ! tu as réussi !' }, + it('returns validated missions from LCMS', async function () { + // when + const returnedMissions = await usecases.findAllActiveMissions({ organizationId }); + + // then + const expectedMission = new Mission({ + ...missionValideeDB, + name: missionValideeDB.name_i18n.fr, + learningObjectives: missionValideeDB.learningObjectives_i18n.fr, + validatedObjectives: missionValideeDB.validatedObjectives_i18n.fr, + introductionMediaAlt: missionValideeDB.introductionMediaAlt_i18n.fr, + content: { + steps: [ + { + name: missionValideeDB.content.steps[0].name_i18n.fr, + }, + ], + dareChallenges: [], + }, + }); + expect(returnedMissions).to.deep.equal([ + { + ...expectedMission, + areaCode: '3', + competenceName: '4.5 Competence', + startedBy: '', + }, + ]); }); + }); - const area = learningContentBuilder.buildArea({ - code: 3, - competenceIds: ['competenceId'], + context('when FT_SHOW_EXPERIMENTAL_MISSION is true', function () { + beforeEach(function () { + config.featureToggles.showExperimentalMissions = true; }); - const organizationId = databaseBuilder.factory.buildOrganization().id; - - const competence = { - id: 'competenceId', - index: '4.5', - name_i18n: { - fr: 'Competence', - }, - }; - - await mockLearningContent({ - missions: [validatedMission, experimentalMission, inactiveMission], - areas: [area], - competences: [competence], + it('returns validated and experimental missions from LCMS', async function () { + // when + const returnedMissions = await usecases.findAllActiveMissions({ organizationId }); + + // then + const expectedMissionValidee = new Mission({ + ...missionValideeDB, + name: missionValideeDB.name_i18n.fr, + learningObjectives: missionValideeDB.learningObjectives_i18n.fr, + validatedObjectives: missionValideeDB.validatedObjectives_i18n.fr, + introductionMediaAlt: missionValideeDB.introductionMediaAlt_i18n.fr, + content: { + steps: [ + { + name: missionValideeDB.content.steps[0].name_i18n.fr, + }, + ], + dareChallenges: [], + }, + }); + const expectedMissionExperimentale = new Mission({ + ...missionExperimentaleDB, + name: missionExperimentaleDB.name_i18n.fr, + learningObjectives: missionExperimentaleDB.learningObjectives_i18n.fr, + validatedObjectives: missionExperimentaleDB.validatedObjectives_i18n.fr, + introductionMediaAlt: missionExperimentaleDB.introductionMediaAlt_i18n.fr, + content: { + steps: [ + { + name: missionExperimentaleDB.content.steps[0].name_i18n.fr, + }, + ], + dareChallenges: [], + }, + }); + expect(returnedMissions).to.deep.equal([ + { + ...expectedMissionValidee, + areaCode: '3', + competenceName: '4.5 Competence', + startedBy: '', + }, + { + ...expectedMissionExperimentale, + areaCode: '3', + competenceName: '4.5 Competence', + startedBy: '', + }, + ]); }); - - const expectedMissions = [ - new Mission({ - id: 12, - name: 'truc', - competenceId: 'competenceId', - thematicId: 'thematicId', - competenceName: '4.5 Competence', - status: 'ACTIVE', - areaCode: 3, - learningObjectives: 'Il était une fois', - validatedObjectives: 'Bravo ! tu as réussi !', - startedBy: '', - }), - new Mission({ - id: 13, - name: 'truc', - competenceId: 'competenceId', - thematicId: 'thematicId', - competenceName: '4.5 Competence', - status: 'EXPERIMENTAL', - areaCode: 3, - learningObjectives: 'Il était une fois', - validatedObjectives: 'Bravo ! tu as réussi !', - startedBy: '', - }), - ]; - const returnedMissions = await usecases.findAllActiveMissions({ organizationId }); - - expect(returnedMissions).to.deep.equal(expectedMissions); }); }); }); diff --git a/api/tests/school/integration/domain/usecases/get-mission_test.js b/api/tests/school/integration/domain/usecases/get-mission_test.js index 9d10d35570d..12d9085a02f 100644 --- a/api/tests/school/integration/domain/usecases/get-mission_test.js +++ b/api/tests/school/integration/domain/usecases/get-mission_test.js @@ -1,11 +1,22 @@ import { Mission } from '../../../../../src/school/domain/models/Mission.js'; import { usecases } from '../../../../../src/school/domain/usecases/index.js'; -import { databaseBuilder, expect, mockLearningContent } from '../../../../test-helper.js'; -import * as learningContentBuilder from '../../../../tooling/learning-content-builder/index.js'; +import { databaseBuilder, expect } from '../../../../test-helper.js'; describe('Integration | UseCase | getMission', function () { it('Should return a mission', async function () { - const mission = learningContentBuilder.buildMission({ + // given + databaseBuilder.factory.learningContent.buildArea({ + id: 'areaId', + code: '3', + competenceIds: ['competenceId'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'competenceId', + name_i18n: { fr: 'Name' }, + index: '1.3', + areaId: 'areaId', + }); + const missionDB = databaseBuilder.factory.learningContent.buildMission({ id: 12, name_i18n: { fr: 'truc' }, competenceId: 'competenceId', @@ -22,31 +33,22 @@ describe('Integration | UseCase | getMission', function () { dareChallenges: [], }, }); - - const area = learningContentBuilder.buildArea({ - code: 3, - competenceIds: ['competenceId'], - }); - const organizationId = databaseBuilder.factory.buildOrganization().id; + await databaseBuilder.commit(); - await mockLearningContent({ - missions: [mission], - areas: [area], - competences: [{ id: 'competenceId', name_i18n: { fr: 'Name' }, index: '1.3' }], + // when + const returnedMission = await usecases.getMission({ + missionId: 12, + organizationId, }); + // then const expectedMission = new Mission({ - id: 12, - name: 'truc', - competenceId: 'competenceId', - competenceName: '1.3 Name', - thematicId: 'thematicId', - status: 'a status', - areaCode: 3, - learningObjectives: 'Il était une fois', - validatedObjectives: 'Bravo ! tu as réussi !', - startedBy: '', + ...missionDB, + name: missionDB.name_i18n.fr, + learningObjectives: missionDB.learningObjectives_i18n.fr, + validatedObjectives: missionDB.validatedObjectives_i18n.fr, + introductionMediaAlt: missionDB.introductionMediaAlt_i18n.fr, content: { steps: [ { @@ -56,12 +58,11 @@ describe('Integration | UseCase | getMission', function () { dareChallenges: [], }, }); - - const returnedMission = await usecases.getMission({ - missionId: 12, - organizationId, + expect(returnedMission).to.deep.equal({ + ...expectedMission, + areaCode: '3', + competenceName: '1.3 Name', + startedBy: '', }); - - expect(returnedMission).to.deep.equal(expectedMission); }); }); diff --git a/api/tests/school/integration/domain/usecases/get-next-challenge_test.js b/api/tests/school/integration/domain/usecases/get-next-challenge_test.js index 5430bb72317..5fe1d674045 100644 --- a/api/tests/school/integration/domain/usecases/get-next-challenge_test.js +++ b/api/tests/school/integration/domain/usecases/get-next-challenge_test.js @@ -11,7 +11,7 @@ import * as challengeRepository from '../../../../../src/shared/infrastructure/r import { databaseBuilder, expect, knex, mockLearningContent } from '../../../../test-helper.js'; import * as learningContentBuilder from '../../../../tooling/learning-content-builder/index.js'; -describe('Integration | Usecase | get-next-challenge', function () { +describe('Integration | School | Usecase | get-next-challenge', function () { describe('#getNextChallenge', function () { context('when last activity is succeeded', function () { it('should return null', async function () { diff --git a/api/tests/school/integration/infrastructure/repositories/mission-repository_test.js b/api/tests/school/integration/infrastructure/repositories/mission-repository_test.js index 42d851cf8b4..630da5d80e6 100644 --- a/api/tests/school/integration/infrastructure/repositories/mission-repository_test.js +++ b/api/tests/school/integration/infrastructure/repositories/mission-repository_test.js @@ -1,7 +1,7 @@ import { Mission } from '../../../../../src/school/domain/models/Mission.js'; import { MissionNotFoundError } from '../../../../../src/school/domain/school-errors.js'; import * as missionRepository from '../../../../../src/school/infrastructure/repositories/mission-repository.js'; -import { catchErr, expect, mockLearningContent } from '../../../../test-helper.js'; +import { catchErr, databaseBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Repository | mission-repository', function () { describe('#get', function () { @@ -12,9 +12,9 @@ describe('Integration | Repository | mission-repository', function () { id: 1, name: 'nameThemaFR1', competenceId: 'competenceId', - thematicId: 'thematicId', learningObjectives: 'learningObjectivesi18n', validatedObjectives: 'validatedObjectivesi18n', + cardImageUrl: 'http://cardimageUrl.de.ma.mission', introductionMediaUrl: 'http://monimage.pix.fr', introductionMediaType: 'image', introductionMediaAlt: "Alt à l'image", @@ -27,30 +27,26 @@ describe('Integration | Repository | mission-repository', function () { ], }, }); - - await mockLearningContent({ - missions: [ - { - id: 1, - name_i18n: { fr: 'nameThemaFR1' }, - competenceId: 'competenceId', - thematicId: 'thematicId', - learningObjectives_i18n: { fr: 'learningObjectivesi18n' }, - validatedObjectives_i18n: { fr: 'validatedObjectivesi18n' }, - introductionMediaUrl: 'http://monimage.pix.fr', - introductionMediaType: 'image', - introductionMediaAlt: "Alt à l'image", - documentationUrl: 'http://madoc.pix.fr', - content: { - steps: [ - { - name_i18n: { fr: 'step_name_1' }, - }, - ], + databaseBuilder.factory.learningContent.buildMission({ + id: 1, + name_i18n: { fr: 'nameThemaFR1' }, + competenceId: 'competenceId', + cardImageUrl: 'http://cardimageUrl.de.ma.mission', + learningObjectives_i18n: { fr: 'learningObjectivesi18n' }, + validatedObjectives_i18n: { fr: 'validatedObjectivesi18n' }, + introductionMediaUrl: 'http://monimage.pix.fr', + introductionMediaType: 'image', + introductionMediaAlt_i18n: { fr: "Alt à l'image" }, + documentationUrl: 'http://madoc.pix.fr', + content: { + steps: [ + { + name_i18n: { fr: 'step_name_1' }, }, - }, - ], + ], + }, }); + await databaseBuilder.commit(); // when const mission = await missionRepository.get('1'); @@ -62,10 +58,8 @@ describe('Integration | Repository | mission-repository', function () { context('when there is no mission for the given id', function () { it('should return the not found error', async function () { // given - await mockLearningContent({ - thematics: [], - }); const missionId = 'recThematic1'; + // when const error = await catchErr(missionRepository.get)(missionId); @@ -78,13 +72,12 @@ describe('Integration | Repository | mission-repository', function () { describe('#findAllActiveMissions', function () { context('when there are active missions', function () { - it('should return all active mission', async function () { + it('should return all active missions', async function () { // given const expectedMission = new Mission({ id: 1, name: 'nameThemaFR1', competenceId: 'competenceId', - thematicId: 'thematicId', cardImageUrl: 'super-url', learningObjectives: 'learningObjectivesi18n', validatedObjectives: 'validatedObjectivesi18n', @@ -101,52 +94,47 @@ describe('Integration | Repository | mission-repository', function () { }, }); - await mockLearningContent({ - missions: [ - { - id: 1, - status: 'VALIDATED', - name_i18n: { fr: 'nameThemaFR1' }, - competenceId: 'competenceId', - thematicId: 'thematicId', - cardImageUrl: 'super-url', - learningObjectives_i18n: { fr: 'learningObjectivesi18n' }, - validatedObjectives_i18n: { fr: 'validatedObjectivesi18n' }, - introductionMediaUrl: 'http://monimage.pix.fr', - introductionMediaType: 'image', - introductionMediaAlt: "Alt à l'image", - documentationUrl: 'http://madoc.pix.fr', - content: { - steps: [ - { - name_i18n: { fr: 'step_name_1' }, - }, - ], + databaseBuilder.factory.learningContent.buildMission({ + id: 1, + status: 'VALIDATED', + name_i18n: { fr: 'nameThemaFR1' }, + competenceId: 'competenceId', + cardImageUrl: 'super-url', + learningObjectives_i18n: { fr: 'learningObjectivesi18n' }, + validatedObjectives_i18n: { fr: 'validatedObjectivesi18n' }, + introductionMediaUrl: 'http://monimage.pix.fr', + introductionMediaType: 'image', + introductionMediaAlt_i18n: { fr: "Alt à l'image" }, + documentationUrl: 'http://madoc.pix.fr', + content: { + steps: [ + { + name_i18n: { fr: 'step_name_1' }, }, - }, - { - id: 2, - status: 'INACTIVE', - name_i18n: { fr: 'nameThemaFR1' }, - competenceId: 'competenceId', - thematicId: 'thematicId', - cardImageUrl: 'super-url', - learningObjectives_i18n: { fr: 'learningObjectivesi18n' }, - validatedObjectives_i18n: { fr: 'validatedObjectivesi18n' }, - introductionMediaUrl: 'http://monimage.pix.fr', - introductionMediaType: 'image', - introductionMediaAlt: "Alt à l'image", - documentationUrl: 'http://madoc.pix.fr', - content: { - steps: [ - { - name_i18n: { fr: 'step_name_1' }, - }, - ], + ], + }, + }); + databaseBuilder.factory.learningContent.buildMission({ + id: 2, + status: 'INACTIVE', + name_i18n: { fr: 'nameThemaFR1' }, + competenceId: 'competenceId', + cardImageUrl: 'super-url', + learningObjectives_i18n: { fr: 'learningObjectivesi18n' }, + validatedObjectives_i18n: { fr: 'validatedObjectivesi18n' }, + introductionMediaUrl: 'http://monimage.pix.fr', + introductionMediaType: 'image', + introductionMediaAlt_i18n: { fr: "Alt à l'image" }, + documentationUrl: 'http://madoc.pix.fr', + content: { + steps: [ + { + name_i18n: { fr: 'step_name_1' }, }, - }, - ], + ], + }, }); + await databaseBuilder.commit(); // when const missions = await missionRepository.findAllActiveMissions(); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index dbeffc763e0..697bcdeb169 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -23,6 +23,7 @@ import * as thematicRepository from '../lib/infrastructure/repositories/thematic import * as tubeRepository from '../lib/infrastructure/repositories/tube-repository.js'; import { PIX_ADMIN } from '../src/authorization/domain/constants.js'; import * as tutorialRepository from '../src/devcomp/infrastructure/repositories/tutorial-repository.js'; +import * as missionRepository from '../src/school/infrastructure/repositories/mission-repository.js'; import { config } from '../src/shared/config.js'; import { Membership } from '../src/shared/domain/models/index.js'; import * as tokenService from '../src/shared/domain/services/token-service.js'; @@ -89,6 +90,7 @@ afterEach(function () { challengeRepository.clearCache(); courseRepository.clearCache(); tutorialRepository.clearCache(); + missionRepository.clearCache(); return databaseBuilder.clean(); }); From e0824cc59f5bf7345a5feaf66dcf828baa348777 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Sun, 1 Dec 2024 21:45:51 +0100 Subject: [PATCH 14/24] fix various test by changing how learning content is initialized --- .../repositories/challenge-repository.js | 4 +- .../frameworks/frameworks-controller_test.js | 24 +- .../application/certification-route_test.js | 3 + .../application/livret-scolaire-route_test.js | 10 +- .../certificate-repository_test.js | 10 +- .../certification-candidate-route_test.js | 12 +- .../learning-content-repository_test.js | 523 +++++++++--------- .../target-profile-repository_test.js | 10 +- .../learner-participation-route_test.js | 6 +- .../participant-result-repository_test.js | 7 + .../application/campaign-route_test.js | 51 +- .../usecases/get-presentation-steps_test.js | 59 +- ...files-collection-results-to-stream_test.js | 73 ++- .../application/target-profile-route_test.js | 16 +- ...-profile-administration-repository_test.js | 8 +- .../placement-profile-service_test.js | 198 ++++--- ...tifications-results-for-livret-scolaire.js | 141 ++--- 17 files changed, 614 insertions(+), 541 deletions(-) diff --git a/api/src/shared/infrastructure/repositories/challenge-repository.js b/api/src/shared/infrastructure/repositories/challenge-repository.js index 8d9c6d44227..eec5422a990 100644 --- a/api/src/shared/infrastructure/repositories/challenge-repository.js +++ b/api/src/shared/infrastructure/repositories/challenge-repository.js @@ -121,6 +121,8 @@ export async function findActiveFlashCompatible({ .where('status', VALIDATED_STATUS) .whereNotNull('alpha') .whereNotNull('delta') + .whereIn('accessibility1', ACCESSIBLE_STATUSES) + .whereIn('accessibility2', ACCESSIBLE_STATUSES) .orderBy('id'); } else { findCallback = (knex) => @@ -129,8 +131,6 @@ export async function findActiveFlashCompatible({ .where('status', VALIDATED_STATUS) .whereNotNull('alpha') .whereNotNull('delta') - .whereIn('accessibility1', ACCESSIBLE_STATUSES) - .whereIn('accessibility2', ACCESSIBLE_STATUSES) .orderBy('id'); } const challengeDtos = await getInstance().find(cacheKey, findCallback); diff --git a/api/tests/acceptance/application/frameworks/frameworks-controller_test.js b/api/tests/acceptance/application/frameworks/frameworks-controller_test.js index 7037d68a699..800ca99c477 100644 --- a/api/tests/acceptance/application/frameworks/frameworks-controller_test.js +++ b/api/tests/acceptance/application/frameworks/frameworks-controller_test.js @@ -27,7 +27,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { areas: [ { id: 'areaPix1', - code: 1, + code: '1', title_i18n: { fr: 'areaPix1 title fr', }, @@ -37,7 +37,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { }, { id: 'areaFrance1', - code: 1, + code: '1', title_i18n: { fr: 'areaFrance1 title fr', }, @@ -47,7 +47,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { }, { id: 'areaCuisine1', - code: 1, + code: '1', title_i18n: { fr: 'areaCuisine1 title fr', }, @@ -64,7 +64,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { en: 'competencePix1_1 name en', }, areaId: 'areaPix1', - index: 0, + index: '0', origin: 'Pix', thematicIds: ['thematicPix1_1_1'], }, @@ -75,7 +75,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { en: 'competenceFrance1_1 name en', }, areaId: 'areaFrance1', - index: 0, + index: '0', origin: 'France', thematicIds: ['thematicFrance1_1_1'], }, @@ -86,7 +86,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { en: 'competenceCuisine1_1 name en', }, areaId: 'areaCuisine1', - index: 0, + index: '0', origin: 'Cuisine', thematicIds: ['thematicCuisine1_1_1'], }, @@ -212,7 +212,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { id: 'areaPix1', type: 'areas', attributes: { - code: 1, + code: '1', title: 'areaPix1 title fr', color: 'areaPix1 color', }, @@ -234,7 +234,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { type: 'competences', attributes: { name: 'competencePix1_1 name fr', - index: 0, + index: '0', }, }, ], @@ -514,7 +514,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { type: 'thematics', id: 'recThemA', attributes: { - index: '1', + index: 1, name: 'nameFRA', }, relationships: { @@ -560,7 +560,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { type: 'thematics', id: 'recThemB', attributes: { - index: '2', + index: 2, name: 'nameFRB', }, relationships: { @@ -628,7 +628,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { type: 'thematics', id: 'recThemC', attributes: { - index: '3', + index: 3, name: 'nameFRC', }, relationships: { @@ -674,7 +674,7 @@ describe('Acceptance | Controller | frameworks-controller', function () { type: 'thematics', id: 'recThemD', attributes: { - index: '4', + index: 4, name: 'nameFRD', }, relationships: { diff --git a/api/tests/certification/results/acceptance/application/certification-route_test.js b/api/tests/certification/results/acceptance/application/certification-route_test.js index 6f8f1bca3fd..4dd5c02bb8f 100644 --- a/api/tests/certification/results/acceptance/application/certification-route_test.js +++ b/api/tests/certification/results/acceptance/application/certification-route_test.js @@ -27,6 +27,7 @@ describe('Certification | Results | Acceptance | Application | Certification', f name: '1. Information et données', title_i18n: { fr: 'Information et données' }, color: 'jaffa', + frameworkId: 'Pix', competences: [ { id: 'recsvLz0W2ShyfD63', @@ -280,6 +281,7 @@ describe('Certification | Results | Acceptance | Application | Certification', f name: '1. Information et données', title_i18n: { fr: 'Information et données' }, color: 'jaffa', + frameworkId: 'Pix', competences: [ { id: 'recsvLz0W2ShyfD63', @@ -514,6 +516,7 @@ describe('Certification | Results | Acceptance | Application | Certification', f name: '1. Information et données', title_i18n: { fr: 'Information et données' }, color: 'jaffa', + frameworkId: 'Pix', competences: [ { id: 'recsvLz0W2ShyfD63', diff --git a/api/tests/certification/results/acceptance/application/livret-scolaire-route_test.js b/api/tests/certification/results/acceptance/application/livret-scolaire-route_test.js index f80866c5133..7f0f607daf6 100644 --- a/api/tests/certification/results/acceptance/application/livret-scolaire-route_test.js +++ b/api/tests/certification/results/acceptance/application/livret-scolaire-route_test.js @@ -1,4 +1,4 @@ -import { Assessment } from '../../../../../src/shared/domain/models/Assessment.js'; +import { Assessment } from '../../../../../src/shared/domain/models/index.js'; import { createServer, databaseBuilder, @@ -21,9 +21,7 @@ describe('Certification | Results | Acceptance | Application | Livret Scolaire', let organizationId; const pixScore = 400; const uai = '789567AA'; - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line mocha/no-setup-in-describe - const type = Assessment.types.CERTIFICATION; + let type; const verificationCode = 'P-123498NN'; const OSMOSE_CLIENT_ID = 'apimOsmoseClientId'; const OSMOSE_SCOPE = 'organizations-certifications-result'; @@ -194,9 +192,11 @@ describe('Certification | Results | Acceptance | Application | Livret Scolaire', }, ]; - beforeEach(function () { + beforeEach(async function () { + type = Assessment.types.CERTIFICATION; organizationId = buildOrganization(uai).id; mockLearningContentCompetences(); + await databaseBuilder.commit(); }); context('when the given uai is correct', function () { diff --git a/api/tests/certification/results/integration/infrastructure/repositories/certificate-repository_test.js b/api/tests/certification/results/integration/infrastructure/repositories/certificate-repository_test.js index b8d846ac5ac..95373d953d9 100644 --- a/api/tests/certification/results/integration/infrastructure/repositories/certificate-repository_test.js +++ b/api/tests/certification/results/integration/infrastructure/repositories/certificate-repository_test.js @@ -302,7 +302,7 @@ describe('Integration | Infrastructure | Repository | Certification', function ( { ...competence2, name_i18n: { fr: competence2.name } }, ], title: 'titre test', - framework: null, + frameworkId: 'Pix', }); const learningContentObjects = learningContentBuilder.fromAreas([{ ...area1, title_i18n: { fr: area1.title } }]); @@ -1854,7 +1854,7 @@ describe('Integration | Infrastructure | Repository | Certification', function ( code: '1', title: 'titre test', competences: [competence1, competence2], - framework: null, + frameworkId: 'Pix', }); const learningContentObjects = learningContentBuilder.fromAreas([ @@ -1965,7 +1965,7 @@ describe('Integration | Infrastructure | Repository | Certification', function ( code: '1', title: 'titre test', competences: [competence1, competence2], - framework: null, + frameworkId: 'Pix', }); const learningContentObjects = learningContentBuilder.fromAreas([ @@ -2403,7 +2403,7 @@ describe('Integration | Infrastructure | Repository | Certification', function ( code: '1', competences: [competence1, competence2], title: 'titre test', - framework: null, + frameworkId: 'Pix', }); const learningContentObjects = learningContentBuilder.fromAreas([ @@ -2516,7 +2516,7 @@ describe('Integration | Infrastructure | Repository | Certification', function ( code: '1', competences: [competence1, competence2], title: 'titre test', - framework: null, + frameworkId: 'Pix', }); const learningContentObjects = learningContentBuilder.fromAreas([ diff --git a/api/tests/certification/session-management/acceptance/application/certification-candidate-route_test.js b/api/tests/certification/session-management/acceptance/application/certification-candidate-route_test.js index e91b47140cb..2b226827599 100644 --- a/api/tests/certification/session-management/acceptance/application/certification-candidate-route_test.js +++ b/api/tests/certification/session-management/acceptance/application/certification-candidate-route_test.js @@ -3,7 +3,6 @@ import { databaseBuilder, expect, generateValidRequestAuthorizationHeader, - mockLearningContent, } from '../../../../test-helper.js'; describe('Certification | Session Management | Acceptance | Application | Routes | certification-candidate', function () { @@ -149,13 +148,10 @@ describe('Certification | Session Management | Acceptance | Application | Routes const certificationChallenge = databaseBuilder.factory.buildCertificationChallenge({ courseId: certificationCourseId, }); - await mockLearningContent({ - frameworks: [{ id: 'frameworkId' }], - challenges: [ - { - id: certificationChallenge.challengeId, - }, - ], + databaseBuilder.factory.learningContent.buildSkill({ id: 'skillId' }); + databaseBuilder.factory.learningContent.buildChallenge({ + id: certificationChallenge.challengeId, + skillId: 'skillId', }); const supervisorUserId = databaseBuilder.factory.buildUser({}).id; diff --git a/api/tests/integration/infrastructure/repositories/learning-content-repository_test.js b/api/tests/integration/infrastructure/repositories/learning-content-repository_test.js index 0d17055889c..068a8f15bf4 100644 --- a/api/tests/integration/infrastructure/repositories/learning-content-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/learning-content-repository_test.js @@ -1,241 +1,244 @@ import * as learningContentRepository from '../../../../lib/infrastructure/repositories/learning-content-repository.js'; import { NoSkillsInCampaignError, NotFoundError } from '../../../../src/shared/domain/errors.js'; -import { - catchErr, - databaseBuilder, - domainBuilder, - expect, - learningContentBuilder, - mockLearningContent, -} from '../../../test-helper.js'; +import { catchErr, databaseBuilder, domainBuilder, expect } from '../../../test-helper.js'; describe('Integration | Repository | learning-content', function () { - let learningContent; let framework1Fr, framework1En, framework2Fr, framework2En; let area1Fr, area1En, area2Fr, area2En; let competence1Fr, competence1En, competence2Fr, competence2En, competence3Fr, competence3En; let thematic1Fr, thematic1En, thematic2Fr, thematic2En, thematic3Fr, thematic3En; let tube1Fr, tube1En, tube2Fr, tube2En, tube4Fr, tube4En; - let skill1, skill2, skill3, skill8; + let skill1Fr, skill2Fr, skill3Fr, skill8Fr; beforeEach(async function () { - learningContent = learningContentBuilder([ - { - id: 'recFramework1', - name: 'Mon référentiel 1', - areas: [ - { - id: 'recArea1', - name: 'area1_name', - title_i18n: { fr: 'domaine1_TitreFr', en: 'area1_TitleEn' }, - color: 'area1_color', - code: 'area1_code', - frameworkId: 'recFramework1', - competences: [ - { - id: 'recCompetence1', - name_i18n: { fr: 'competence1_nomFr', en: 'competence1_nameEn' }, - index: 1, - description_i18n: { fr: 'competence1_descriptionFr', en: 'competence1_descriptionEn' }, - origin: 'Pix', - thematics: [ - { - id: 'recThematic1', - name_i18n: { - fr: 'thematique1_nomFr', - en: 'thematic1_nameEn', - }, - index: '10', - tubes: [ - { - id: 'recTube1', - name: '@tube1_name', - title: 'tube1_title', - description: 'tube1_description', - practicalTitle_i18n: { fr: 'tube1_practicalTitleFr', en: 'tube1_practicalTitleEn' }, - practicalDescription_i18n: { - fr: 'tube1_practicalDescriptionFr', - en: 'tube1_practicalDescriptionEn', - }, - isMobileCompliant: true, - isTabletCompliant: false, - skills: [ - { - id: 'recSkill1', - name: '@tube1_name4', - status: 'actif', - level: 4, - pixValue: 12, - version: 98, - }, - ], - }, - ], - }, - ], - }, - { - id: 'recCompetence2', - name_i18n: { fr: 'competence2_nomFr', en: 'competence2_nameEn' }, - index: 2, - description_i18n: { fr: 'competence2_descriptionFr', en: 'competence2_descriptionEn' }, - origin: 'Pix', - thematics: [ - { - id: 'recThematic2', - name_i18n: { - fr: 'thematique2_nomFr', - en: 'thematic2_nameEn', - }, - index: '20', - tubes: [ - { - id: 'recTube2', - name: '@tube2_name', - title: '@tube2_title', - description: '@tube2_description', - practicalTitle_i18n: { fr: 'tube2_practicalTitleFr', en: 'tube2_practicalTitleEn' }, - practicalDescription_i18n: { - fr: 'tube2_practicalDescriptionFr', - en: 'tube2_practicalDescriptionEn', - }, - isMobileCompliant: false, - isTabletCompliant: true, - skills: [ - { - id: 'recSkill2', - name: '@tube2_name1', - status: 'actif', - level: 1, - pixValue: 34, - version: 76, - }, - { - id: 'recSkill3', - name: '@tube2_name2', - status: 'archivé', - level: 2, - pixValue: 56, - version: 54, - }, - { - id: 'recSkill4', - status: 'périmé', - }, - ], - }, - { - id: 'recTube3', - name: '@tube3_name', - title: '@tube3_title', - description: '@tube3_description', - practicalTitle_i18n: { fr: 'tube3_practicalTitleFr', en: 'tube3_practicalTitleEn' }, - practicalDescription_i18n: { - fr: 'tube3_practicalDescriptionFr', - en: 'tube3_practicalDescriptionEn', - }, - isMobileCompliant: true, - isTabletCompliant: true, - skills: [ - { - id: 'recSkill5', - name: '@tube3_name5', - status: 'archivé', - level: 5, - pixValue: 44, - version: 55, - }, - { - id: 'recSkill6', - status: 'périmé', - }, - { - id: 'recSkill7', - status: 'périmé', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], + const framework1DB = databaseBuilder.factory.learningContent.buildFramework({ + id: 'recFramework1', + name: 'Mon référentiel 1', + }); + const framework2DB = databaseBuilder.factory.learningContent.buildFramework({ + id: 'recFramework2', + name: 'Mon référentiel 2', + }); + const area1DB = databaseBuilder.factory.learningContent.buildArea({ + id: 'recArea1', + name: 'area1_name', + title_i18n: { fr: 'domaine1_TitreFr', en: 'area1_TitleEn' }, + color: 'area1_color', + code: 'area1_code', + frameworkId: 'recFramework1', + competenceIds: ['recCompetence1', 'recCompetence2'], + }); + const area2DB = databaseBuilder.factory.learningContent.buildArea({ + id: 'recArea2', + name: 'area2_name', + title_i18n: { fr: 'domaine2_TitreFr', en: 'area2_TitleEn' }, + color: 'area2_color', + code: 'area2_code', + frameworkId: 'recFramework2', + competenceIds: ['recCompetence3'], + }); + const competence1DB = databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence1', + name_i18n: { fr: 'competence1_nomFr', en: 'competence1_nameEn' }, + index: '1', + description_i18n: { fr: 'competence1_descriptionFr', en: 'competence1_descriptionEn' }, + origin: 'Pix', + areaId: 'recArea1', + }); + const competence2DB = databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence2', + name_i18n: { fr: 'competence2_nomFr', en: 'competence2_nameEn' }, + index: '2', + description_i18n: { fr: 'competence2_descriptionFr', en: 'competence2_descriptionEn' }, + origin: 'Pix', + areaId: 'recArea1', + }); + const competence3DB = databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence3', + name_i18n: { fr: 'competence3_nomFr', en: 'competence3_nameEn' }, + index: '1', + description_i18n: { fr: 'competence3_descriptionFr', en: 'competence3_descriptionEn' }, + origin: 'Pix', + areaId: 'recArea2', + }); + const thematic1DB = databaseBuilder.factory.learningContent.buildThematic({ + id: 'recThematic1', + name_i18n: { + fr: 'thematique1_nomFr', + en: 'thematic1_nameEn', + }, + index: 10, + competenceId: 'recCompetence1', + tubeIds: ['recTube1'], + }); + const thematic2DB = databaseBuilder.factory.learningContent.buildThematic({ + id: 'recThematic2', + name_i18n: { + fr: 'thematique2_nomFr', + en: 'thematic2_nameEn', + }, + index: 20, + competenceId: 'recCompetence2', + tubeIds: ['recTube2', 'recTube3'], + }); + const thematic3DB = databaseBuilder.factory.learningContent.buildThematic({ + id: 'recThematic3', + name_i18n: { + fr: 'thematique3_nomFr', + en: 'thematic3_nameEn', + }, + index: 30, + competenceId: 'recCompetence3', + tubeIds: ['recTube4'], + }); + const tube1DB = databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube1', + name: '@tube1_name', + title: 'tube1_title', + description: 'tube1_description', + practicalTitle_i18n: { fr: 'tube1_practicalTitleFr', en: 'tube1_practicalTitleEn' }, + practicalDescription_i18n: { + fr: 'tube1_practicalDescriptionFr', + en: 'tube1_practicalDescriptionEn', + }, + isMobileCompliant: true, + isTabletCompliant: false, + competenceId: 'recCompetence1', + thematicId: 'recThematic1', + }); + const tube2DB = databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube2', + name: '@tube2_name', + title: '@tube2_title', + description: '@tube2_description', + practicalTitle_i18n: { fr: 'tube2_practicalTitleFr', en: 'tube2_practicalTitleEn' }, + practicalDescription_i18n: { + fr: 'tube2_practicalDescriptionFr', + en: 'tube2_practicalDescriptionEn', + }, + isMobileCompliant: false, + isTabletCompliant: true, + competenceId: 'recCompetence2', + thematicId: 'recThematic2', + }); + const tube3DB = databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube3', + name: '@tube3_name', + title: '@tube3_title', + description: '@tube3_description', + practicalTitle_i18n: { fr: 'tube3_practicalTitleFr', en: 'tube3_practicalTitleEn' }, + practicalDescription_i18n: { + fr: 'tube3_practicalDescriptionFr', + en: 'tube3_practicalDescriptionEn', }, - { - id: 'recFramework2', - name: 'Mon référentiel 2', - areas: [ - { - id: 'recArea2', - name: 'area2_name', - title_i18n: { fr: 'domaine2_TitreFr', en: 'area2_TitleEn' }, - color: 'area2_color', - code: 'area2_code', - frameworkId: 'recFramework2', - competences: [ - { - id: 'recCompetence3', - name_i18n: { fr: 'competence3_nomFr', en: 'competence3_nameEn' }, - index: 1, - description_i18n: { fr: 'competence3_descriptionFr', en: 'competence3_descriptionEn' }, - origin: 'Pix', - thematics: [ - { - id: 'recThematic3', - name_i18n: { - fr: 'thematique3_nomFr', - en: 'thematic3_nameEn', - }, - index: '30', - tubes: [ - { - id: 'recTube4', - name: '@tube4_name', - title: 'tube4_title', - description: 'tube4_description', - practicalTitle_i18n: { fr: 'tube4_practicalTitleFr', en: 'tube4_practicalTitleEn' }, - practicalDescription_i18n: { - fr: 'tube4_practicalDescriptionFr', - en: 'tube4_practicalDescriptionEn', - }, - isMobileCompliant: false, - isTabletCompliant: false, - skills: [ - { - id: 'recSkill8', - name: '@tube4_name8', - status: 'actif', - level: 7, - pixValue: 78, - version: 32, - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], + isMobileCompliant: true, + isTabletCompliant: true, + competenceId: 'recCompetence2', + thematicId: 'recThematic2', + }); + const tube4DB = databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube4', + name: '@tube4_name', + title: 'tube4_title', + description: 'tube4_description', + practicalTitle_i18n: { fr: 'tube4_practicalTitleFr', en: 'tube4_practicalTitleEn' }, + practicalDescription_i18n: { + fr: 'tube4_practicalDescriptionFr', + en: 'tube4_practicalDescriptionEn', }, - ]); - - [framework1Fr, framework2Fr] = _buildDomainFrameworksFromLearningContent(learningContent); - [framework1En, framework2En] = _buildDomainFrameworksFromLearningContent(learningContent); - [area1Fr, area2Fr] = _buildDomainAreasFromLearningContent(learningContent, 'fr'); - [area1En, area2En] = _buildDomainAreasFromLearningContent(learningContent, 'en'); - [competence1Fr, competence2Fr, competence3Fr] = _buildDomainCompetencesFromLearningContent(learningContent, 'fr'); - [competence1En, competence2En, competence3En] = _buildDomainCompetencesFromLearningContent(learningContent, 'en'); - [thematic1Fr, thematic2Fr, thematic3Fr] = _buildDomainThematicsFromLearningContent(learningContent, 'fr'); - [thematic1En, thematic2En, thematic3En] = _buildDomainThematicsFromLearningContent(learningContent, 'en'); - [tube1Fr, tube2Fr, , tube4Fr] = _buildDomainTubesFromLearningContent(learningContent, 'fr'); - [tube1En, tube2En, , tube4En] = _buildDomainTubesFromLearningContent(learningContent, 'en'); - [skill1, skill2, skill3, , , , , skill8] = _buildDomainSkillsFromLearningContent(learningContent); - - await mockLearningContent(learningContent); + isMobileCompliant: false, + isTabletCompliant: false, + competenceId: 'recCompetence3', + thematicId: 'recThematic3', + }); + const skill1DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill1', + name: '@tube1_name4', + status: 'actif', + level: 4, + pixValue: 12, + version: 98, + tubeId: 'recTube1', + }); + const skill2DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill2', + name: '@tube2_name1', + status: 'actif', + level: 1, + pixValue: 34, + version: 76, + tubeId: 'recTube2', + }); + const skill3DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill3', + name: '@tube2_name2', + status: 'archivé', + level: 2, + pixValue: 56, + version: 54, + tubeId: 'recTube2', + }); + const skill4DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill4', + status: 'périmé', + tubeId: 'recTube2', + }); + const skill5DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill5', + name: '@tube3_name5', + status: 'archivé', + level: 5, + pixValue: 44, + version: 55, + tubeId: 'recTube3', + }); + const skill6DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill6', + status: 'périmé', + tubeId: 'recTube3', + }); + const skill7DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill7', + status: 'périmé', + tubeId: 'recTube3', + }); + const skill8DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill8', + name: '@tube4_name8', + status: 'actif', + level: 7, + pixValue: 78, + version: 32, + tubeId: 'recTube4', + }); + await databaseBuilder.commit(); + + [framework1Fr, framework2Fr] = _buildDomainFrameworksFromDB([framework1DB, framework2DB]); + [framework1En, framework2En] = _buildDomainFrameworksFromDB([framework1DB, framework2DB]); + [area1Fr, area2Fr] = _buildDomainAreasFromDB([area1DB, area2DB], 'fr'); + [area1En, area2En] = _buildDomainAreasFromDB([area1DB, area2DB], 'en'); + [competence1Fr, competence2Fr, competence3Fr] = _buildDomainCompetencesFromDB( + [competence1DB, competence2DB, competence3DB], + 'fr', + ); + [competence1En, competence2En, competence3En] = _buildDomainCompetencesFromDB( + [competence1DB, competence2DB, competence3DB], + 'en', + ); + [thematic1Fr, thematic2Fr, thematic3Fr] = _buildDomainThematicsFromDB( + [thematic1DB, thematic2DB, thematic3DB], + 'fr', + ); + [thematic1En, thematic2En, thematic3En] = _buildDomainThematicsFromDB( + [thematic1DB, thematic2DB, thematic3DB], + 'en', + ); + [tube1Fr, tube2Fr, , tube4Fr] = _buildDomainTubesFromDB([tube1DB, tube2DB, tube3DB, tube4DB], 'fr'); + [tube1En, tube2En, , tube4En] = _buildDomainTubesFromDB([tube1DB, tube2DB, tube3DB, tube4DB], 'en'); + [skill1Fr, skill2Fr, skill3Fr, , , , , skill8Fr] = _buildDomainSkillsFromDB( + [skill1DB, skill2DB, skill3DB, skill4DB, skill5DB, skill6DB, skill7DB, skill8DB], + 'fr', + ); }); describe('#findByCampaignId', function () { @@ -254,7 +257,7 @@ describe('Integration | Repository | learning-content', function () { competence2Fr.thematics = [thematic2Fr]; competence2Fr.tubes = [tube2Fr]; thematic2Fr.tubes = [tube2Fr]; - tube2Fr.skills = [skill2, skill3]; + tube2Fr.skills = [skill2Fr, skill3Fr]; // when const learningContentFromCampaign = await learningContentRepository.findByCampaignId(campaignId); @@ -277,7 +280,7 @@ describe('Integration | Repository | learning-content', function () { competence2En.thematics = [thematic2En]; competence2En.tubes = [tube2En]; thematic2En.tubes = [tube2En]; - tube2En.skills = [skill2, skill3]; + tube2En.skills = [skill2Fr, skill3Fr]; // when const learningContentFromCampaign = await learningContentRepository.findByCampaignId(campaignId, 'en'); @@ -330,7 +333,7 @@ describe('Integration | Repository | learning-content', function () { competence2Fr.thematics = [thematic2Fr]; competence2Fr.tubes = [tube2Fr]; thematic2Fr.tubes = [tube2Fr]; - tube2Fr.skills = [skill2]; + tube2Fr.skills = [skill2Fr]; // when const targetProfileLearningContent = await learningContentRepository.findByTargetProfileId(targetProfileId); @@ -351,7 +354,7 @@ describe('Integration | Repository | learning-content', function () { competence2En.thematics = [thematic2En]; competence2En.tubes = [tube2En]; thematic2En.tubes = [tube2En]; - tube2En.skills = [skill2]; + tube2En.skills = [skill2Fr]; // when const targetProfileLearningContent = await learningContentRepository.findByTargetProfileId( @@ -382,9 +385,9 @@ describe('Integration | Repository | learning-content', function () { thematic1Fr.tubes = [tube1Fr]; thematic2Fr.tubes = [tube2Fr]; thematic3Fr.tubes = [tube4Fr]; - tube1Fr.skills = [skill1]; - tube2Fr.skills = [skill2]; - tube4Fr.skills = [skill8]; + tube1Fr.skills = [skill1Fr]; + tube2Fr.skills = [skill2Fr]; + tube4Fr.skills = [skill8Fr]; // when const learningContent = await learningContentRepository.findByFrameworkNames({ @@ -411,9 +414,9 @@ describe('Integration | Repository | learning-content', function () { thematic1En.tubes = [tube1En]; thematic2En.tubes = [tube2En]; thematic3En.tubes = [tube4En]; - tube1En.skills = [skill1]; - tube2En.skills = [skill2]; - tube4En.skills = [skill8]; + tube1En.skills = [skill1Fr]; + tube2En.skills = [skill2Fr]; + tube4En.skills = [skill8Fr]; // when const learningContent = await learningContentRepository.findByFrameworkNames({ @@ -428,54 +431,56 @@ describe('Integration | Repository | learning-content', function () { }); }); -function _buildDomainFrameworksFromLearningContent({ frameworks }) { - return frameworks.map((framework) => +function _buildDomainFrameworksFromDB(frameworksDB) { + return frameworksDB.map((frameworkDB) => domainBuilder.buildFramework({ - id: framework.id, - name: framework.name, + id: frameworkDB.id, + name: frameworkDB.name, areas: [], }), ); } -function _buildDomainAreasFromLearningContent({ areas }, locale) { - return areas.map((area) => +function _buildDomainAreasFromDB(areasDB, locale) { + return areasDB.map((areaDB) => domainBuilder.buildArea({ - ...area, - title: area.title_i18n[locale], + ...areaDB, + title: areaDB.title_i18n[locale], }), ); } -function _buildDomainCompetencesFromLearningContent({ competences }, locale) { - return competences.map((competence) => +function _buildDomainCompetencesFromDB(competencesDB, locale) { + return competencesDB.map((competenceDB) => domainBuilder.buildCompetence({ - ...competence, - name: competence.name_i18n[locale], - description: competence.description_i18n[locale], + ...competenceDB, + name: competenceDB.name_i18n[locale], + description: competenceDB.description_i18n[locale], }), ); } -function _buildDomainThematicsFromLearningContent({ thematics }, locale) { - return thematics.map((thematic) => +function _buildDomainThematicsFromDB(thematicsDB, locale) { + return thematicsDB.map((thematicDB) => domainBuilder.buildThematic({ - ...thematic, - name: thematic.name_i18n[locale], + ...thematicDB, + name: thematicDB.name_i18n[locale], }), ); } -function _buildDomainTubesFromLearningContent({ tubes }, locale) { - return tubes.map((tube) => +function _buildDomainTubesFromDB(tubesDB, locale) { + return tubesDB.map((tubeDB) => domainBuilder.buildTube({ - ...tube, - practicalTitle: tube.practicalTitle_i18n[locale], - practicalDescription: tube.practicalDescription_i18n[locale], + ...tubeDB, + practicalTitle: tubeDB.practicalTitle_i18n[locale], + practicalDescription: tubeDB.practicalDescription_i18n[locale], }), ); } -function _buildDomainSkillsFromLearningContent({ skills }) { - return skills.map((skill) => domainBuilder.buildSkill({ ...skill, difficulty: skill.level })); +function _buildDomainSkillsFromDB(skillsDB, locale) { + return skillsDB.map((skillDB) => + domainBuilder.buildSkill({ ...skillDB, difficulty: skillDB.level, hint: skillDB.hint_i18n[locale] }), + ); } diff --git a/api/tests/integration/infrastructure/repositories/target-profile-repository_test.js b/api/tests/integration/infrastructure/repositories/target-profile-repository_test.js index 9c6c45ce249..70a4ddc390b 100644 --- a/api/tests/integration/infrastructure/repositories/target-profile-repository_test.js +++ b/api/tests/integration/infrastructure/repositories/target-profile-repository_test.js @@ -17,15 +17,13 @@ describe('Integration | Repository | Target-profile', function () { await databaseBuilder.commit(); }); - it('should return the target profile with its associated skills and the list of organizations which could access it', function () { + it('should return the target profile with its associated skills and the list of organizations which could access it', async function () { // when - const promise = targetProfileRepository.get(targetProfile.id); + const foundTargetProfile = await targetProfileRepository.get(targetProfile.id); // then - return promise.then((foundTargetProfile) => { - expect(foundTargetProfile).to.be.an.instanceOf(TargetProfile); - expect(foundTargetProfile.id).to.be.equal(targetProfile.id); - }); + expect(foundTargetProfile).to.be.an.instanceOf(TargetProfile); + expect(foundTargetProfile.id).to.be.equal(targetProfile.id); }); context('when the targetProfile does not exist', function () { diff --git a/api/tests/prescription/campaign-participation/acceptance/application/learner-participation-route_test.js b/api/tests/prescription/campaign-participation/acceptance/application/learner-participation-route_test.js index 2082cfdc516..0242637d31e 100644 --- a/api/tests/prescription/campaign-participation/acceptance/application/learner-participation-route_test.js +++ b/api/tests/prescription/campaign-participation/acceptance/application/learner-participation-route_test.js @@ -1,4 +1,3 @@ -// import { createServer } from '../../../../../server.js'; import _ from 'lodash'; import { ParticipationResultCalculationJob } from '../../../../../src/prescription/campaign-participation/domain/models/ParticipationResultCalculationJob.js'; @@ -399,6 +398,9 @@ describe('Acceptance | Routes | Campaign Participations', function () { name_i18n: { fr: 'Mener une recherche et une veille d’information', }, + description_i18n: { + fr: 'Mener une recherche et une veille d’information description', + }, index: '1.1', origin: 'Pix', areaId: 'recvoGdo7z2z7pXWa', @@ -490,7 +492,7 @@ describe('Acceptance | Routes | Campaign Participations', function () { { attributes: { 'competence-id': 'recAbe382T0e1337', - description: undefined, + description: 'Mener une recherche et une veille d’information description', 'earned-pix': 2, index: '1.1', level: 0, diff --git a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js index 02649de61ac..ca66c6532f0 100644 --- a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/participant-result-repository_test.js @@ -19,6 +19,11 @@ describe('Integration | Repository | ParticipantResultRepository', function () { databaseBuilder.factory.buildTargetProfile(targetProfile); const learningContent = { + frameworks: [ + { + id: 'frameworkId', + }, + ], areas: [ { id: 'recArea1', @@ -26,6 +31,7 @@ describe('Integration | Repository | ParticipantResultRepository', function () { title_i18n: { fr: 'domaine1' }, competenceIds: ['rec1'], color: 'colorArea1', + frameworkId: 'frameworkId', }, { id: 'recArea2', @@ -33,6 +39,7 @@ describe('Integration | Repository | ParticipantResultRepository', function () { title_i18n: { fr: 'domaine2' }, competenceIds: ['rec2'], color: 'colorArea2', + frameworkId: 'frameworkId', }, ], competences: [ diff --git a/api/tests/prescription/campaign/acceptance/application/campaign-route_test.js b/api/tests/prescription/campaign/acceptance/application/campaign-route_test.js index f5693bec6e6..4de1b612cbf 100644 --- a/api/tests/prescription/campaign/acceptance/application/campaign-route_test.js +++ b/api/tests/prescription/campaign/acceptance/application/campaign-route_test.js @@ -1,8 +1,7 @@ -import { Membership } from '../../../../../src/shared/domain/models/Membership.js'; +import { Membership } from '../../../../../src/shared/domain/models/index.js'; import { createServer, databaseBuilder, - domainBuilder, expect, generateValidRequestAuthorizationHeader, learningContentBuilder, @@ -371,14 +370,42 @@ describe('Acceptance | API | Campaign Route', function () { targetProfileId: targetProfile.id, organizationId: organization.id, }); - - const learningContent = domainBuilder.buildLearningContent.withSimpleContent(); - const learningContentObjects = learningContentBuilder.fromAreas(learningContent.frameworks[0].areas); - await mockLearningContent(learningContentObjects); - + databaseBuilder.factory.learningContent.buildFramework({ + id: 'recFramework', + }); + databaseBuilder.factory.learningContent.buildArea({ + id: 'recArea', + frameworkId: 'recFramework', + competenceIds: ['recCompetence'], + }); + const competenceDB = databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence', + index: '2', + name_i18n: { fr: 'nom en français' }, + areaId: 'recArea', + skillIds: ['recSkill'], + thematicIds: ['recThematic'], + }); + databaseBuilder.factory.learningContent.buildThematic({ + id: 'recThematic', + competenceId: 'recCompetence', + tubeIds: ['recTube'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube', + competenceId: 'recCompetence', + thematicId: 'recThematic', + skillIds: ['recSkill'], + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill', + status: 'actif', + competenceId: 'recCompetence', + tubeId: 'recTube', + }); databaseBuilder.factory.buildCampaignSkill({ campaignId: campaign.id, - skillId: learningContentObjects.competences[0].skillIds[0], + skillId: 'recSkill', }); databaseBuilder.factory.buildCampaignParticipation({ @@ -416,7 +443,7 @@ describe('Acceptance | API | Campaign Route', function () { competences: { data: [ { - id: learningContentObjects.competences[0].id, + id: competenceDB.id, type: 'competences', }, ], @@ -439,10 +466,10 @@ describe('Acceptance | API | Campaign Route', function () { }, { type: 'competences', - id: learningContentObjects.competences[0].id, + id: competenceDB.id, attributes: { - index: learningContentObjects.competences[0].index, - name: learningContentObjects.competences[0].name, + index: competenceDB.index, + name: competenceDB.name_i18n.fr, }, }, ], diff --git a/api/tests/prescription/campaign/integration/domain/usecases/get-presentation-steps_test.js b/api/tests/prescription/campaign/integration/domain/usecases/get-presentation-steps_test.js index 457bbabe75f..9d77b0f5ff9 100644 --- a/api/tests/prescription/campaign/integration/domain/usecases/get-presentation-steps_test.js +++ b/api/tests/prescription/campaign/integration/domain/usecases/get-presentation-steps_test.js @@ -1,23 +1,13 @@ import { usecases } from '../../../../../../src/prescription/campaign/domain/usecases/index.js'; import { LOCALE } from '../../../../../../src/shared/domain/constants.js'; -import { - databaseBuilder, - domainBuilder, - expect, - learningContentBuilder, - mockLearningContent, -} from '../../../../../test-helper.js'; +import { databaseBuilder, expect } from '../../../../../test-helper.js'; const { FRENCH_SPOKEN } = LOCALE; describe('Integration | Campaign | UseCase | get-presentation-steps', function () { - let user, campaign, badges, competences; + let user, campaign, badges; beforeEach(async function () { - const learningContent = domainBuilder.buildLearningContent.withSimpleContent(); - const learningContentObjects = learningContentBuilder.fromAreas(learningContent.frameworks[0].areas); - await mockLearningContent(learningContentObjects); - const targetProfileId = databaseBuilder.factory.buildTargetProfile().id; campaign = databaseBuilder.factory.buildCampaign({ targetProfileId }); @@ -40,11 +30,42 @@ describe('Integration | Campaign | UseCase | get-presentation-steps', function ( databaseBuilder.factory.buildBadge({ targetProfileId }), ]; - competences = learningContentObjects.competences; - + databaseBuilder.factory.learningContent.buildFramework({ + id: 'recFramework', + }); + databaseBuilder.factory.learningContent.buildArea({ + id: 'recArea', + frameworkId: 'recFramework', + competenceIds: ['recCompetence'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence', + index: '2', + name_i18n: { fr: 'nom en français' }, + areaId: 'recArea', + skillIds: ['recSkill'], + thematicIds: ['recThematic'], + }); + databaseBuilder.factory.learningContent.buildThematic({ + id: 'recThematic', + competenceId: 'recCompetence', + tubeIds: ['recTube'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube', + competenceId: 'recCompetence', + thematicId: 'recThematic', + skillIds: ['recSkill'], + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recSkill', + status: 'actif', + competenceId: 'recCompetence', + tubeId: 'recTube', + }); databaseBuilder.factory.buildCampaignSkill({ campaignId: campaign.id, - skillId: competences[0].skillIds[0], + skillId: 'recSkill', }); await databaseBuilder.commit(); @@ -61,9 +82,9 @@ describe('Integration | Campaign | UseCase | get-presentation-steps', function ( // then expect(result.customLandingPageText).to.equal(campaign.customLandingPageText); expect(result.badges).to.deep.equal(badges); - expect(result.competences).to.have.lengthOf(competences.length); - expect(result.competences[0].id).to.equal(competences[0].id); - expect(result.competences[0].index).to.equal(competences[0].index); - expect(result.competences[0].name).to.equal(competences[0].name); + expect(result.competences).to.have.lengthOf(1); + expect(result.competences[0].id).to.equal('recCompetence'); + expect(result.competences[0].index).to.equal('2'); + expect(result.competences[0].name).to.equal('nom en français'); }); }); diff --git a/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js b/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js index e5d2a4ba74c..e498d771b3c 100644 --- a/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js +++ b/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js @@ -22,7 +22,7 @@ import * as placementProfileService from '../../../../../../src/shared/domain/se import { getI18n } from '../../../../../../src/shared/infrastructure/i18n/i18n.js'; import * as competenceRepository from '../../../../../../src/shared/infrastructure/repositories/competence-repository.js'; import * as organizationRepository from '../../../../../../src/shared/infrastructure/repositories/organization-repository.js'; -import { databaseBuilder, expect, mockLearningContent, streamToPromise } from '../../../../../test-helper.js'; +import { databaseBuilder, expect, streamToPromise } from '../../../../../test-helper.js'; describe('Integration | Domain | Use Cases | start-writing-profiles-collection-campaign-results-to-stream', function () { describe('#startWritingCampaignProfilesCollectionResultsToStream', function () { @@ -40,11 +40,11 @@ describe('Integration | Domain | Use Cases | start-writing-profiles-collection-c beforeEach(async function () { i18n = getI18n(); organization = databaseBuilder.factory.buildOrganization(); - const skillWeb1 = { id: 'recSkillWeb1', name: '@web1', competenceIds: ['recCompetence1'] }; - const skillWeb2 = { id: 'recSkillWeb2', name: '@web2', competenceIds: ['recCompetence1'] }; - const skillWeb3 = { id: 'recSkillWeb3', name: '@web3', competenceIds: ['recCompetence1'] }; - const skillUrl1 = { id: 'recSkillUrl1', name: '@url1', competenceIds: ['recCompetence2'] }; - const skillUrl8 = { id: 'recSkillUrl8', name: '@url8', competenceIds: ['recCompetence2'] }; + const skillWeb1 = { id: 'recSkillWeb1', name: '@web1', competenceIds: ['recCompetence1'], status: 'actif' }; + const skillWeb2 = { id: 'recSkillWeb2', name: '@web2', competenceIds: ['recCompetence1'], status: 'actif' }; + const skillWeb3 = { id: 'recSkillWeb3', name: '@web3', competenceIds: ['recCompetence1'], status: 'actif' }; + const skillUrl1 = { id: 'recSkillUrl1', name: '@url1', competenceIds: ['recCompetence2'], status: 'actif' }; + const skillUrl8 = { id: 'recSkillUrl8', name: '@url8', competenceIds: ['recCompetence2'], status: 'actif' }; const skills = [skillWeb1, skillWeb2, skillWeb3, skillUrl1, skillUrl8]; participant = databaseBuilder.factory.buildUser(); @@ -99,27 +99,44 @@ describe('Integration | Domain | Use Cases | start-writing-profiles-collection-c snapshot: JSON.stringify([ke1, ke2, ke3, ke4, ke5]), }); - await databaseBuilder.commit(); + databaseBuilder.factory.learningContent.buildFramework({ + id: 'recFramework', + }); + databaseBuilder.factory.learningContent.buildArea({ + id: 'recArea1', + frameworkId: 'recFramework', + competenceIds: ['recCompetence'], + }); + databaseBuilder.factory.learningContent.buildArea({ + id: 'recArea2', + frameworkId: 'recFramework', + competenceIds: ['recCompetence'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence1', + index: '2', + name_i18n: { fr: 'nom en français recCompetence1' }, + areaId: 'recArea1', + skillIds: [skillWeb1.id, skillWeb2.id, skillWeb3.id], + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'recCompetence2', + index: '3', + name_i18n: { fr: 'nom en français recCompetence2' }, + areaId: 'recArea2', + skillIds: [skillUrl1.id, skillUrl8.id], + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'recTube', + competenceId: 'recCompetence', + thematicId: 'recThematic', + skillIds: ['recSkill'], + }); + skills.forEach(databaseBuilder.factory.learningContent.buildSkill); - const learningContent = { - areas: [{ id: 'recArea1' }, { id: 'recArea2' }], - competences: [ - { - id: 'recCompetence1', - areaId: 'recArea1', - skillIds: [skillWeb1.id, skillWeb2.id, skillWeb3.id], - origin: 'Pix', - }, - { - id: 'recCompetence2', - areaId: 'recArea2', - skillIds: [skillUrl1.id, skillUrl8.id], - origin: 'Pix', - }, - ], - skills, - }; - await mockLearningContent(learningContent); + await databaseBuilder.commit(); writableStream = new PassThrough(); csvPromise = streamToPromise(writableStream); @@ -180,7 +197,7 @@ describe('Integration | Domain | Use Cases | start-writing-profiles-collection-c const cells = csv.split('\n'); expect(cells[0]).to.be.equals( - '\uFEFF"Nom de l\'organisation";"ID Campagne";"Code";"Nom de la campagne";"Nom du Participant";"Prénom du Participant";"Envoi (O/N)";"Date de l\'envoi";"Nombre de pix total";"Certifiable (O/N)";"Nombre de compétences certifiables";"Niveau pour la compétence ";"Nombre de pix pour la compétence ";"Niveau pour la compétence ";"Nombre de pix pour la compétence "', + '\uFEFF"Nom de l\'organisation";"ID Campagne";"Code";"Nom de la campagne";"Nom du Participant";"Prénom du Participant";"Envoi (O/N)";"Date de l\'envoi";"Nombre de pix total";"Certifiable (O/N)";"Nombre de compétences certifiables";"Niveau pour la compétence nom en français recCompetence1";"Nombre de pix pour la compétence nom en français recCompetence1";"Niveau pour la compétence nom en français recCompetence2";"Nombre de pix pour la compétence nom en français recCompetence2"', ); expect(cells[1]).to.be.equals( `"Observatoire de Pix";${campaign.id};"QWERTY456";"'@Campagne de Test N°2";"'=Bono";"'@Jean";"Oui";2019-03-01;52;"Non";2;1;12;5;40`, @@ -243,7 +260,7 @@ describe('Integration | Domain | Use Cases | start-writing-profiles-collection-c const cells = csv.split('\n'); expect(cells[0]).to.be.equals( - '\uFEFF"Nom de l\'organisation";"ID Campagne";"Code";"Nom de la campagne";"Nom du Participant";"Prénom du Participant";"Mail Perso";"Envoi (O/N)";"Date de l\'envoi";"Nombre de pix total";"Certifiable (O/N)";"Nombre de compétences certifiables";"Niveau pour la compétence ";"Nombre de pix pour la compétence ";"Niveau pour la compétence ";"Nombre de pix pour la compétence "', + '\uFEFF"Nom de l\'organisation";"ID Campagne";"Code";"Nom de la campagne";"Nom du Participant";"Prénom du Participant";"Mail Perso";"Envoi (O/N)";"Date de l\'envoi";"Nombre de pix total";"Certifiable (O/N)";"Nombre de compétences certifiables";"Niveau pour la compétence nom en français recCompetence1";"Nombre de pix pour la compétence nom en français recCompetence1";"Niveau pour la compétence nom en français recCompetence2";"Nombre de pix pour la compétence nom en français recCompetence2"', ); expect(cells[1]).to.be.equals( `"Observatoire de Pix";${campaign.id};"QWERTY456";"'@Campagne de Test N°2";"'=Bono";"'@Jean";"'+Mon mail pro";"Oui";2019-03-01;52;"Non";2;1;12;5;40`, diff --git a/api/tests/prescription/target-profile/acceptance/application/target-profile-route_test.js b/api/tests/prescription/target-profile/acceptance/application/target-profile-route_test.js index a5c0709e59b..7754f8f4435 100644 --- a/api/tests/prescription/target-profile/acceptance/application/target-profile-route_test.js +++ b/api/tests/prescription/target-profile/acceptance/application/target-profile-route_test.js @@ -101,7 +101,7 @@ describe('Acceptance | Route | target-profile', function () { areas: [ { id: 'areaPix1', - code: 1, + code: '1', title_i18n: { fr: 'areaPix1 title fr', }, @@ -111,7 +111,7 @@ describe('Acceptance | Route | target-profile', function () { }, { id: 'areaFrance1', - code: 1, + code: '1', title_i18n: { fr: 'areaFrance1 title fr', }, @@ -121,7 +121,7 @@ describe('Acceptance | Route | target-profile', function () { }, { id: 'areaCuisine1', - code: 1, + code: '1', title_i18n: { fr: 'areaCuisine1 title fr', }, @@ -138,7 +138,7 @@ describe('Acceptance | Route | target-profile', function () { en: 'competencePix1_1 name en', }, areaId: 'areaPix1', - index: 0, + index: '0', origin: 'Pix', thematicIds: ['thematicPix1_1_1'], }, @@ -149,7 +149,7 @@ describe('Acceptance | Route | target-profile', function () { en: 'competenceFrance1_1 name en', }, areaId: 'areaFrance1', - index: 0, + index: '0', origin: 'France', thematicIds: ['thematicFrance1_1_1'], }, @@ -160,7 +160,7 @@ describe('Acceptance | Route | target-profile', function () { en: 'competenceCuisine1_1 name en', }, areaId: 'areaCuisine1', - index: 0, + index: '0', origin: 'Cuisine', thematicIds: ['thematicCuisine1_1_1'], }, @@ -330,7 +330,7 @@ describe('Acceptance | Route | target-profile', function () { type: 'competences', id: 'competencePix1_1', attributes: { - index: 0, + index: '0', name: 'competencePix1_1 name fr', }, relationships: { @@ -348,7 +348,7 @@ describe('Acceptance | Route | target-profile', function () { type: 'areas', id: 'areaPix1', attributes: { - code: 1, + code: '1', color: 'areaPix1 color', title: 'areaPix1 title fr', }, diff --git a/api/tests/prescription/target-profile/integration/infrastructure/repositories/target-profile-administration-repository_test.js b/api/tests/prescription/target-profile/integration/infrastructure/repositories/target-profile-administration-repository_test.js index 337147f4246..aad67fa2249 100644 --- a/api/tests/prescription/target-profile/integration/infrastructure/repositories/target-profile-administration-repository_test.js +++ b/api/tests/prescription/target-profile/integration/infrastructure/repositories/target-profile-administration-repository_test.js @@ -398,19 +398,19 @@ describe('Integration | Repository | Target-profile', function () { const themA_compA_areaA = { id: 'recThemA', name: 'nameFRA', - index: '1', + index: 1, competenceId: 'recCompA', }; const themB_compA_areaA = { id: 'recThemB', name: 'nameFRB', - index: '2', + index: 2, competenceId: 'recCompA', }; const themC_compB_areaA = { id: 'recThemC', name: 'nameFRC', - index: '1', + index: 1, competenceId: 'recCompB', }; const compA_areaA = { @@ -587,7 +587,7 @@ describe('Integration | Repository | Target-profile', function () { const themA_compA_areaA = { id: 'recThemA', name: 'nameENA', - index: '1', + index: 1, competenceId: 'recCompA', }; const compA_areaA = { diff --git a/api/tests/shared/integration/domain/services/placement-profile-service_test.js b/api/tests/shared/integration/domain/services/placement-profile-service_test.js index 41c6dd0fa93..d554fa77582 100644 --- a/api/tests/shared/integration/domain/services/placement-profile-service_test.js +++ b/api/tests/shared/integration/domain/services/placement-profile-service_test.js @@ -1,92 +1,98 @@ import { LOCALE } from '../../../../../src/shared/domain/constants.js'; -import { KnowledgeElement } from '../../../../../src/shared/domain/models/KnowledgeElement.js'; +import { KnowledgeElement } from '../../../../../src/shared/domain/models/index.js'; import * as placementProfileService from '../../../../../src/shared/domain/services/placement-profile-service.js'; -import { databaseBuilder, expect, learningContentBuilder } from '../../../../test-helper.js'; +import { databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js'; const { ENGLISH_SPOKEN } = LOCALE; describe('Shared | Integration | Domain | Services | Placement Profile Service', function () { let userId, assessmentId; + let skillRemplir2DB; beforeEach(function () { - const learningContent = [ - { - id: 'areaOne', - code: '1', - color: 'jaffa', - frameworkId: 'recFmk123', - competences: [ - { - id: 'competenceRecordIdOne', - name_i18n: { fr: 'Construire un flipper', en: 'Build a pinball' }, - index: '1.1', - tubes: [ - { - id: 'recCitation', - skills: [ - { - id: 'recCitation4', - nom: '@citation4', - pixValue: 1, - version: 1, - level: 4, - }, - ], - }, - ], - }, - { - id: 'competenceRecordIdTwo', - name_i18n: { fr: 'Adopter un dauphin', en: 'Adopt a dolphin' }, - index: '1.2', - tubes: [ - { - id: 'Remplir', - skills: [ - { - id: 'recRemplir2', - nom: '@remplir2', - pixValue: 1, - version: 1, - level: 2, - }, - { - id: 'recRemplir4', - nom: '@remplir4', - pixValue: 1, - version: 1, - level: 4, - }, - ], - }, - ], - }, - { - id: 'competenceRecordIdThree', - name_i18n: { fr: 'Se faire manger par un requin', en: 'Getting eaten by a shark' }, - index: '1.3', - tubes: [ - { - id: 'Requin', - skills: [ - { - id: 'recRequin5', - nom: '@requin5', - pixValue: 1, - version: 1, - level: 5, - }, - ], - }, - ], - }, - ], - }, - ]; - - const learningContentObjects = learningContentBuilder.fromAreas(learningContent); - databaseBuilder.factory.learningContent.build(learningContentObjects); - + databaseBuilder.factory.learningContent.buildFramework({ id: 'recFmk123' }); + databaseBuilder.factory.learningContent.buildArea({ + id: 'areaOne', + frameworkId: 'recFmk123', + code: '1', + color: 'jaffa', + competenceIds: ['competenceRecordIdOne', 'competenceRecordIdTwo', 'competenceRecordIdThree'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'competenceRecordIdOne', + name_i18n: { fr: 'Construire un flipper', en: 'Build a pinball' }, + index: '1.1', + areaId: 'areaOne', + skillIds: ['recCitation4'], + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'competenceRecordIdTwo', + name_i18n: { fr: 'Adopter un dauphin', en: 'Adopt a dolphin' }, + index: '1.2', + areaId: 'areaOne', + skillIds: ['recRemplir2', 'recRemplir4'], + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'competenceRecordIdThree', + name_i18n: { fr: 'Se faire manger par un requin', en: 'Getting eaten by a shark' }, + index: '1.3', + areaId: 'areaOne', + skillIds: ['recRequin5'], + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'recCitation', + competenceId: 'competenceRecordIdOne', + skillIds: ['recCitation4'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'Remplir', + competenceId: 'competenceRecordIdTwo', + skillIds: ['recRemplir2', 'recRemplir4'], + }); + databaseBuilder.factory.learningContent.buildTube({ + id: 'Requin', + competenceId: 'competenceRecordIdThree', + skillIds: ['recRequin5'], + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recCitation4', + nom: '@citation4', + pixValue: 1, + version: 1, + level: 4, + competenceId: 'competenceRecordIdOne', + tubeId: 'recCitation', + }); + skillRemplir2DB = databaseBuilder.factory.learningContent.buildSkill({ + id: 'recRemplir2', + nom: '@remplir2', + pixValue: 1, + version: 1, + level: 2, + competenceId: 'competenceRecordIdTwo', + tubeId: 'Remplir', + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recRemplir4', + nom: '@remplir4', + pixValue: 1, + version: 1, + level: 4, + competenceId: 'competenceRecordIdTwo', + tubeId: 'Remplir', + }); + databaseBuilder.factory.learningContent.buildSkill({ + id: 'recRequin5', + nom: '@requin5', + pixValue: 1, + version: 1, + level: 5, + competenceId: 'competenceRecordIdThress', + tubeId: 'Requin', + }); userId = databaseBuilder.factory.buildUser().id; assessmentId = databaseBuilder.factory.buildAssessment({ userId }).id; return databaseBuilder.commit(); @@ -270,17 +276,11 @@ describe('Shared | Integration | Domain | Services | Placement Profile Service', pixScore: 23, estimatedLevel: 2, skills: [ - { - competenceId: 'competenceRecordIdTwo', - id: 'recRemplir2', - name: '@remplir2', - pixValue: 1, - tubeId: 'Remplir', - tutorialIds: [], - learningMoreTutorialIds: [], - version: 1, - difficulty: 2, - }, + domainBuilder.buildSkill({ + ...skillRemplir2DB, + difficulty: skillRemplir2DB.level, + hint: skillRemplir2DB.hint_i18n.fr, + }), ], }); expect(actualPlacementProfile.userCompetences[2]).to.deep.include({ @@ -417,17 +417,11 @@ describe('Shared | Integration | Domain | Services | Placement Profile Service', pixScore: 22, estimatedLevel: 2, skills: [ - { - competenceId: 'competenceRecordIdTwo', - id: 'recRemplir2', - name: '@remplir2', - pixValue: 1, - tubeId: 'Remplir', - tutorialIds: [], - learningMoreTutorialIds: [], - version: 1, - difficulty: 2, - }, + domainBuilder.buildSkill({ + ...skillRemplir2DB, + difficulty: skillRemplir2DB.level, + hint: skillRemplir2DB.hint_i18n.fr, + }), ], }); expect(actualPlacementProfile.userCompetences[2]).to.deep.include({ diff --git a/api/tests/tooling/domain-builder/factory/build-certifications-results-for-livret-scolaire.js b/api/tests/tooling/domain-builder/factory/build-certifications-results-for-livret-scolaire.js index 1d9e8e2eb97..f93587463f5 100644 --- a/api/tests/tooling/domain-builder/factory/build-certifications-results-for-livret-scolaire.js +++ b/api/tests/tooling/domain-builder/factory/build-certifications-results-for-livret-scolaire.js @@ -1,6 +1,6 @@ import { Assessment } from '../../../../src/shared/domain/models/Assessment.js'; import { status } from '../../../../src/shared/domain/models/AssessmentResult.js'; -import { databaseBuilder, learningContentBuilder } from '../../../test-helper.js'; +import { databaseBuilder } from '../../../test-helper.js'; const assessmentCreatedDate = new Date('2020-04-19'); const assessmentBeforeCreatedDate = new Date('2020-04-18'); @@ -292,74 +292,77 @@ function buildCertificationDataWithNoCompetenceMarks({ user, organizationLearner } function mockLearningContentCompetences() { - const learningContent = [ - { - id: 'rec99', - code: '2', - title_i18n: { fr: 'Communication et collaboration' }, - competences: [ - { - id: 'rec50', - index: '2.1', - name_i18n: { fr: 'Interagir' }, - tubes: [], - }, - { - id: 'rec51', - index: '2.2', - name_i18n: { fr: 'Partager et publier' }, - tubes: [], - }, - { - id: 'rec52', - index: '2.3', - name_i18n: { fr: 'Collaborer' }, - tubes: [], - }, - ], - }, - { - id: 'rec98', - code: '3', - title_i18n: { fr: 'Création de contenu' }, - competences: [ - { - id: 'rec53', - index: '3.1', - name_i18n: { fr: 'Développer des documents textuels' }, - tubes: [], - }, - { - id: 'rec54', - index: '3.2', - name_i18n: { fr: 'Développer des documents multimedia' }, - tubes: [], - }, - ], - }, - { - id: 'rec97', - code: '1', - title_i18n: { fr: 'Information et données' }, - competences: [ - { - id: 'rec55', - index: '1.1', - name_i18n: { fr: 'Mener une recherche et une veille d’information' }, - tubes: [], - }, - { - id: 'rec56', - index: '1.2', - name_i18n: { fr: 'Gérer des données' }, - tubes: [], - }, - ], - }, - ]; - - const learningContentObjects = learningContentBuilder.fromAreas(learningContent); - databaseBuilder.factory.learningContent.build(learningContentObjects); + databaseBuilder.factory.learningContent.buildFramework({ id: 'frameworkId' }); + databaseBuilder.factory.learningContent.buildArea({ + frameworkId: 'frameworkId', + id: 'rec99', + code: '2', + title_i18n: { fr: 'Communication et collaboration' }, + competenceIds: ['rec50', 'rec51', 'rec52'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec50', + index: '2.1', + name_i18n: { fr: 'Interagir' }, + areaId: 'rec99', + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec51', + index: '2.2', + name_i18n: { fr: 'Partager et publier' }, + areaId: 'rec99', + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec52', + index: '2.3', + name_i18n: { fr: 'Collaborer' }, + areaId: 'rec99', + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildArea({ + frameworkId: 'frameworkId', + id: 'rec98', + code: '3', + title_i18n: { fr: 'Création de contenu' }, + competenceIds: ['rec53', 'rec54'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec53', + index: '3.1', + name_i18n: { fr: 'Développer des documents textuels' }, + areaId: 'rec98', + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec54', + index: '3.2', + name_i18n: { fr: 'Développer des documents multimedia' }, + areaId: 'rec98', + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildArea({ + frameworkId: 'frameworkId', + id: 'rec97', + code: '1', + title_i18n: { fr: 'Information et données' }, + competenceIds: ['rec55', 'rec56'], + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec55', + index: '1.1', + name_i18n: { fr: 'Mener une recherche et une veille d’information' }, + areaId: 'rec97', + origin: 'Pix', + }); + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'rec56', + index: '1.2', + name_i18n: { fr: 'Gérer des données' }, + areaId: 'rec97', + origin: 'Pix', + }); } export { From c1067f28e894db0da6e105ae2068fbd6f46e69dc Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Sun, 1 Dec 2024 23:57:19 +0100 Subject: [PATCH 15/24] delete datasource (todo old delete learning content cache ?) --- .../learning-content/datasource.js | 42 ----- .../learning-content/datasource_test.js | 164 ------------------ 2 files changed, 206 deletions(-) delete mode 100644 api/src/shared/infrastructure/datasources/learning-content/datasource.js delete mode 100644 api/tests/shared/unit/infrastructure/datasources/learning-content/datasource_test.js diff --git a/api/src/shared/infrastructure/datasources/learning-content/datasource.js b/api/src/shared/infrastructure/datasources/learning-content/datasource.js deleted file mode 100644 index 68d7f5e49ba..00000000000 --- a/api/src/shared/infrastructure/datasources/learning-content/datasource.js +++ /dev/null @@ -1,42 +0,0 @@ -import _ from 'lodash'; - -import { LearningContentCache } from '../../caches/learning-content-cache.js'; -import { LearningContentResourceNotFound } from './LearningContentResourceNotFound.js'; - -const _DatasourcePrototype = { - async get(id) { - const modelObjects = await this.list(); - const foundObject = _.find(modelObjects, { id }); - - if (!foundObject) { - throw new LearningContentResourceNotFound(); - } - - return foundObject; - }, - - async getMany(ids) { - const modelObjects = await this.list(); - - return ids.map((id) => { - const foundObject = _.find(modelObjects, { id }); - - if (!foundObject) { - throw new LearningContentResourceNotFound(); - } - - return foundObject; - }); - }, - - async list() { - const learningContent = await LearningContentCache.instance.get(); - return learningContent[this.modelName]; - }, -}; - -export function extend(props) { - const result = Object.assign({}, _DatasourcePrototype, props); - _.bindAll(result, _.functions(result)); - return result; -} diff --git a/api/tests/shared/unit/infrastructure/datasources/learning-content/datasource_test.js b/api/tests/shared/unit/infrastructure/datasources/learning-content/datasource_test.js deleted file mode 100644 index 4261905c88b..00000000000 --- a/api/tests/shared/unit/infrastructure/datasources/learning-content/datasource_test.js +++ /dev/null @@ -1,164 +0,0 @@ -import { learningContentCache } from '../../../../../../src/shared/infrastructure/caches/learning-content-cache.js'; -import * as dataSource from '../../../../../../src/shared/infrastructure/datasources/learning-content/datasource.js'; -import { LearningContentResourceNotFound } from '../../../../../../src/shared/infrastructure/datasources/learning-content/LearningContentResourceNotFound.js'; -import { expect, sinon } from '../../../../../test-helper.js'; - -describe('Unit | Infrastructure | Datasource | Learning Content | datasource', function () { - let someDatasource; - - beforeEach(function () { - sinon.stub(learningContentCache, 'get'); - someDatasource = dataSource.extend({ - modelName: 'learningContentModel', - }); - }); - - describe('#get', function () { - let learningContent; - - beforeEach(function () { - learningContent = { - learningContentModel: [ - { id: 'rec1', property: 'value1' }, - { id: 'rec2', property: 'value2' }, - ], - }; - learningContentCache.get.resolves(learningContent); - }); - - context('(success cases)', function () { - it('should fetch a single record from the learning content cache', async function () { - // when - const record = await someDatasource.get('rec1'); - - // then - expect(record).to.deep.equal({ id: 'rec1', property: 'value1' }); - }); - - it('should correctly manage the `this` context', async function () { - // given - const unboundGet = someDatasource.get; - - // when - const record = await unboundGet('rec1'); - - // then - expect(record).to.deep.equal({ id: 'rec1', property: 'value1' }); - }); - - it('should be cachable', async function () { - // when - await someDatasource.get('rec1'); - - // then - expect(learningContentCache.get).to.have.been.called; - }); - }); - - context('(error cases)', function () { - it('should throw an LearningContentResourceNotFound if record was not found', function () { - // given - const learningContent = { - learningContentModel: [ - { id: 'rec1', property: 'value1' }, - { id: 'rec2', property: 'value2' }, - ], - }; - learningContentCache.get.resolves(learningContent); - - // when - const promise = someDatasource.get('UNKNOWN_RECORD_ID'); - - // then - return expect(promise).to.have.been.rejectedWith(LearningContentResourceNotFound); - }); - - it('should dispatch error in case of generic error', function () { - // given - const err = new Error(); - learningContentCache.get.rejects(err); - - // when - const promise = someDatasource.get('rec1'); - - // then - return expect(promise).to.have.been.rejectedWith(err); - }); - }); - }); - - describe('#getMany', function () { - let learningContent; - - beforeEach(function () { - learningContent = { - learningContentModel: [ - { id: 'rec1', property: 'value1' }, - { id: 'rec2', property: 'value2' }, - { id: 'rec3', property: 'value3' }, - ], - }; - learningContentCache.get.resolves(learningContent); - }); - - it('should fetch all records from Learning content cached corresponding to the ids passed', async function () { - // when - const result = await someDatasource.getMany(['rec1', 'rec2']); - - // then - expect(result).to.deep.equal([ - { id: 'rec1', property: 'value1' }, - { id: 'rec2', property: 'value2' }, - ]); - }); - - it('should throw an LearningContentResourceNotFound if no record was found', function () { - // when - const promise = someDatasource.getMany(['UNKNOWN_RECORD_ID']); - - // then - return expect(promise).to.have.been.rejectedWith(LearningContentResourceNotFound); - }); - }); - - describe('#list', function () { - let learningContent; - - beforeEach(function () { - learningContent = { - learningContentModel: [ - { id: 'rec1', property: 'value1' }, - { id: 'rec2', property: 'value2' }, - ], - }; - learningContentCache.get.resolves(learningContent); - }); - - it('should fetch all the records of a given type from Learning content cache', async function () { - // when - const learningContentModelObjects = await someDatasource.list(); - - // then - expect(learningContentModelObjects).to.deep.equal(learningContent.learningContentModel); - }); - - it('should correctly manage the `this` context', async function () { - // given - const unboundList = someDatasource.list; - - // when - const learningContentModelObjects = await unboundList(); - - // then - expect(learningContentModelObjects).to.deep.equal(learningContent.learningContentModel); - }); - - it('should be cachable', async function () { - // when - await someDatasource.list(); - - // then - expect(learningContentCache.get).to.have.been.called; - }); - }); -}); From f4efde0a2eb76894b60482179228a2bf21564d12 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Mon, 2 Dec 2024 00:33:44 +0100 Subject: [PATCH 16/24] try to fix e2e --- .circleci/config.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index fb7785929ab..7e1f9fb8aa9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -829,6 +829,13 @@ jobs: background: true command: npm start + - run: + name: Refresh cache + environment: + JOBS: 1 + working_directory: ~/pix/api + command: npm run cache:refresh + - run: name: Run tests environment: @@ -900,6 +907,13 @@ jobs: background: true command: npm start + - run: + name: Refresh cache + environment: + JOBS: 1 + working_directory: ~/pix/api + command: npm run cache:refresh + - run: name: Run tests environment: From a5fd17072f586ed47d6f283eaba1386cfea56715 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Mon, 2 Dec 2024 14:47:03 +0100 Subject: [PATCH 17/24] feat(api): clear cache when saving learning content --- api/lib/infrastructure/repositories/framework-repository.js | 4 ---- .../infrastructure/repositories/area-repository.js | 5 +++++ .../infrastructure/repositories/challenge-repository.js | 5 +++++ .../infrastructure/repositories/competence-repository.js | 5 +++++ .../infrastructure/repositories/course-repository.js | 5 +++++ .../infrastructure/repositories/framework-repository.js | 5 +++++ .../repositories/learning-content-repository.js | 5 +++++ .../infrastructure/repositories/mission-repository.js | 5 +++++ .../infrastructure/repositories/skill-repository.js | 5 +++++ .../infrastructure/repositories/thematic-repository.js | 5 +++++ .../infrastructure/repositories/tube-repository.js | 5 +++++ .../infrastructure/repositories/tutorial-repository.js | 5 +++++ 12 files changed, 55 insertions(+), 4 deletions(-) diff --git a/api/lib/infrastructure/repositories/framework-repository.js b/api/lib/infrastructure/repositories/framework-repository.js index 56a1ee0ca85..5c509682828 100644 --- a/api/lib/infrastructure/repositories/framework-repository.js +++ b/api/lib/infrastructure/repositories/framework-repository.js @@ -29,10 +29,6 @@ export async function findByRecordIds(ids) { .map(toDomain); } -export function clear() { - return getInstance().clear(); -} - function toDomain(frameworkData) { return new Framework({ id: frameworkData.id, diff --git a/api/src/learning-content/infrastructure/repositories/area-repository.js b/api/src/learning-content/infrastructure/repositories/area-repository.js index ea42cb66154..a862aec0502 100644 --- a/api/src/learning-content/infrastructure/repositories/area-repository.js +++ b/api/src/learning-content/infrastructure/repositories/area-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../shared/infrastructure/repositories/area-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class AreaRepository extends LearningContentRepository { @@ -16,6 +17,10 @@ class AreaRepository extends LearningContentRepository { competenceIds, }; } + + clearCache() { + clearCache(); + } } export const areaRepository = new AreaRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/challenge-repository.js b/api/src/learning-content/infrastructure/repositories/challenge-repository.js index 9b0cdef1230..f1e61810fd9 100644 --- a/api/src/learning-content/infrastructure/repositories/challenge-repository.js +++ b/api/src/learning-content/infrastructure/repositories/challenge-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../shared/infrastructure/repositories/challenge-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class ChallengeRepository extends LearningContentRepository { @@ -87,6 +88,10 @@ class ChallengeRepository extends LearningContentRepository { skillId, }; } + + clearCache() { + clearCache(); + } } export const challengeRepository = new ChallengeRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/competence-repository.js b/api/src/learning-content/infrastructure/repositories/competence-repository.js index 6e845c0b115..e52725117de 100644 --- a/api/src/learning-content/infrastructure/repositories/competence-repository.js +++ b/api/src/learning-content/infrastructure/repositories/competence-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../shared/infrastructure/repositories/competence-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class CompetenceRepository extends LearningContentRepository { @@ -17,6 +18,10 @@ class CompetenceRepository extends LearningContentRepository { thematicIds, }; } + + clearCache() { + clearCache(); + } } export const competenceRepository = new CompetenceRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/course-repository.js b/api/src/learning-content/infrastructure/repositories/course-repository.js index 5276dec77eb..b07edac04b2 100644 --- a/api/src/learning-content/infrastructure/repositories/course-repository.js +++ b/api/src/learning-content/infrastructure/repositories/course-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../shared/infrastructure/repositories/course-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class CourseRepository extends LearningContentRepository { @@ -15,6 +16,10 @@ class CourseRepository extends LearningContentRepository { challenges, }; } + + clearCache() { + clearCache(); + } } export const courseRepository = new CourseRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/framework-repository.js b/api/src/learning-content/infrastructure/repositories/framework-repository.js index ce3d5169fdb..3a3645323e3 100644 --- a/api/src/learning-content/infrastructure/repositories/framework-repository.js +++ b/api/src/learning-content/infrastructure/repositories/framework-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../../lib/infrastructure/repositories/framework-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class FrameworkRepository extends LearningContentRepository { @@ -11,6 +12,10 @@ class FrameworkRepository extends LearningContentRepository { name, }; } + + clearCache() { + clearCache(); + } } export const frameworkRepository = new FrameworkRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js index 9b6b58646aa..ef38a06e0da 100644 --- a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js +++ b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js @@ -28,6 +28,11 @@ export class LearningContentRepository { for (const chunk of chunks(dtos, this.#chunkSize)) { await knex.insert(chunk).into(this.#tableName).onConflict('id').merge(); } + this.clearCache(); + } + + clearCache() { + // must be overriden } /** diff --git a/api/src/learning-content/infrastructure/repositories/mission-repository.js b/api/src/learning-content/infrastructure/repositories/mission-repository.js index 57b21680d55..5a82848fd28 100644 --- a/api/src/learning-content/infrastructure/repositories/mission-repository.js +++ b/api/src/learning-content/infrastructure/repositories/mission-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../school/infrastructure/repositories/mission-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class MissionRepository extends LearningContentRepository { @@ -34,6 +35,10 @@ class MissionRepository extends LearningContentRepository { competenceId, }; } + + clearCache() { + clearCache(); + } } export const missionRepository = new MissionRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/skill-repository.js b/api/src/learning-content/infrastructure/repositories/skill-repository.js index ba92fe9cbb8..ac0e135f800 100644 --- a/api/src/learning-content/infrastructure/repositories/skill-repository.js +++ b/api/src/learning-content/infrastructure/repositories/skill-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../shared/infrastructure/repositories/skill-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class SkillRepository extends LearningContentRepository { @@ -34,6 +35,10 @@ class SkillRepository extends LearningContentRepository { learningMoreTutorialIds, }; } + + clearCache() { + clearCache(); + } } export const skillRepository = new SkillRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/thematic-repository.js b/api/src/learning-content/infrastructure/repositories/thematic-repository.js index a25845c6649..d5253cd7515 100644 --- a/api/src/learning-content/infrastructure/repositories/thematic-repository.js +++ b/api/src/learning-content/infrastructure/repositories/thematic-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../../lib/infrastructure/repositories/thematic-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class ThematicRepository extends LearningContentRepository { @@ -14,6 +15,10 @@ class ThematicRepository extends LearningContentRepository { tubeIds, }; } + + clearCache() { + clearCache(); + } } export const thematicRepository = new ThematicRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/tube-repository.js b/api/src/learning-content/infrastructure/repositories/tube-repository.js index fd673176a10..e92ea0f319d 100644 --- a/api/src/learning-content/infrastructure/repositories/tube-repository.js +++ b/api/src/learning-content/infrastructure/repositories/tube-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../../lib/infrastructure/repositories/tube-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class TubeRepository extends LearningContentRepository { @@ -32,6 +33,10 @@ class TubeRepository extends LearningContentRepository { isTabletCompliant, }; } + + clearCache() { + clearCache(); + } } export const tubeRepository = new TubeRepository(); diff --git a/api/src/learning-content/infrastructure/repositories/tutorial-repository.js b/api/src/learning-content/infrastructure/repositories/tutorial-repository.js index ba5b5388b25..f29d41368fd 100644 --- a/api/src/learning-content/infrastructure/repositories/tutorial-repository.js +++ b/api/src/learning-content/infrastructure/repositories/tutorial-repository.js @@ -1,3 +1,4 @@ +import { clearCache } from '../../../devcomp/infrastructure/repositories/tutorial-repository.js'; import { LearningContentRepository } from './learning-content-repository.js'; class TutorialRepository extends LearningContentRepository { @@ -16,6 +17,10 @@ class TutorialRepository extends LearningContentRepository { locale, }; } + + clearCache() { + clearCache(); + } } export const tutorialRepository = new TutorialRepository(); From 0d42099a780fa5ca1ff3f51842af7f432e8edc94 Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:59:39 +0100 Subject: [PATCH 18/24] refactor(api): rename LearningContentRepository.save to saveMany Co-authored-by: Laura Bergoens --- .../create-learning-content-release.js | 20 ++++---- .../patch-learning-content-cache-entry.js | 20 ++++---- .../refresh-learning-content-cache.js | 20 ++++---- .../learning-content-repository.js | 2 +- .../repositories/area-repository_test.js | 6 +-- .../repositories/challenge-repository_test.js | 6 +-- .../competence-repository_test.js | 6 +-- .../repositories/course-repository_test.js | 6 +-- .../repositories/framework-repository_test.js | 6 +-- .../learning-content-repository_test.js | 4 +- .../repositories/mission-repository_test.js | 6 +-- .../repositories/skill-repository_test.js | 6 +-- .../repositories/thematic-repository_test.js | 6 +-- .../repositories/tube-repository_test.js | 6 +-- .../repositories/tutorial-repository_test.js | 6 +-- .../create-learning-content-release_test.js | 40 ++++++++-------- ...patch-learning-content-cache-entry_test.js | 48 +++++++++---------- .../refresh-learning-content-cache_test.js | 40 ++++++++-------- 18 files changed, 127 insertions(+), 127 deletions(-) diff --git a/api/src/learning-content/domain/usecases/create-learning-content-release.js b/api/src/learning-content/domain/usecases/create-learning-content-release.js index 11ea87a7fcc..50ae4bde65a 100644 --- a/api/src/learning-content/domain/usecases/create-learning-content-release.js +++ b/api/src/learning-content/domain/usecases/create-learning-content-release.js @@ -17,15 +17,15 @@ export const createLearningContentRelease = withTransaction( }) { const learningContent = await LearningContentCache.instance.update(); - await frameworkRepository.save(learningContent.frameworks); - await areaRepository.save(learningContent.areas); - await competenceRepository.save(learningContent.competences); - await thematicRepository.save(learningContent.thematics); - await tubeRepository.save(learningContent.tubes); - await skillRepository.save(learningContent.skills); - await challengeRepository.save(learningContent.challenges); - await courseRepository.save(learningContent.courses); - await tutorialRepository.save(learningContent.tutorials); - await missionRepository.save(learningContent.missions); + await frameworkRepository.saveMany(learningContent.frameworks); + await areaRepository.saveMany(learningContent.areas); + await competenceRepository.saveMany(learningContent.competences); + await thematicRepository.saveMany(learningContent.thematics); + await tubeRepository.saveMany(learningContent.tubes); + await skillRepository.saveMany(learningContent.skills); + await challengeRepository.saveMany(learningContent.challenges); + await courseRepository.saveMany(learningContent.courses); + await tutorialRepository.saveMany(learningContent.tutorials); + await missionRepository.saveMany(learningContent.missions); }, ); diff --git a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js index 483f73cd03f..8203d020125 100644 --- a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js +++ b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js @@ -65,33 +65,33 @@ async function patchDatabase( missionRepository, ) { if (modelName === 'frameworks') { - await frameworkRepository.save([patchedRecord]); + await frameworkRepository.saveMany([patchedRecord]); } if (modelName === 'areas') { - await areaRepository.save([patchedRecord]); + await areaRepository.saveMany([patchedRecord]); } if (modelName === 'competences') { - await competenceRepository.save([patchedRecord]); + await competenceRepository.saveMany([patchedRecord]); } if (modelName === 'thematics') { - await thematicRepository.save([patchedRecord]); + await thematicRepository.saveMany([patchedRecord]); } if (modelName === 'tubes') { - await tubeRepository.save([patchedRecord]); + await tubeRepository.saveMany([patchedRecord]); } if (modelName === 'skills') { - await skillRepository.save([patchedRecord]); + await skillRepository.saveMany([patchedRecord]); } if (modelName === 'challenges') { - await challengeRepository.save([patchedRecord]); + await challengeRepository.saveMany([patchedRecord]); } if (modelName === 'courses') { - await courseRepository.save([patchedRecord]); + await courseRepository.saveMany([patchedRecord]); } if (modelName === 'tutorials') { - await tutorialRepository.save([patchedRecord]); + await tutorialRepository.saveMany([patchedRecord]); } if (modelName === 'missions') { - await missionRepository.save([patchedRecord]); + await missionRepository.saveMany([patchedRecord]); } } diff --git a/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js b/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js index 2c45df18508..02e0d7bb173 100644 --- a/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js +++ b/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js @@ -17,15 +17,15 @@ export const refreshLearningContentCache = withTransaction( }) { const learningContent = await LearningContentCache.instance.reset(); - await frameworkRepository.save(learningContent.frameworks); - await areaRepository.save(learningContent.areas); - await competenceRepository.save(learningContent.competences); - await thematicRepository.save(learningContent.thematics); - await tubeRepository.save(learningContent.tubes); - await skillRepository.save(learningContent.skills); - await challengeRepository.save(learningContent.challenges); - await courseRepository.save(learningContent.courses); - await tutorialRepository.save(learningContent.tutorials); - await missionRepository.save(learningContent.missions); + await frameworkRepository.saveMany(learningContent.frameworks); + await areaRepository.saveMany(learningContent.areas); + await competenceRepository.saveMany(learningContent.competences); + await thematicRepository.saveMany(learningContent.thematics); + await tubeRepository.saveMany(learningContent.tubes); + await skillRepository.saveMany(learningContent.skills); + await challengeRepository.saveMany(learningContent.challenges); + await courseRepository.saveMany(learningContent.courses); + await tutorialRepository.saveMany(learningContent.tutorials); + await missionRepository.saveMany(learningContent.missions); }, ); diff --git a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js index ef38a06e0da..b68d794f2cd 100644 --- a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js +++ b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js @@ -21,7 +21,7 @@ export class LearningContentRepository { /** * @param {object[]} objects */ - async save(objects) { + async saveMany(objects) { if (!objects) return; const dtos = objects.map(this.toDto); const knex = DomainTransaction.getConnection(); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js index 1445ddf8c63..aa368684c9c 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Area', function () { await knex('learningcontent.areas').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert areas', async function () { // given const areaDtos = [ @@ -40,7 +40,7 @@ describe('Learning Content | Integration | Repositories | Area', function () { ]; // when - await areaRepository.save(areaDtos); + await areaRepository.saveMany(areaDtos); // then const savedAreas = await knex.select('*').from('learningcontent.areas').orderBy('name'); @@ -125,7 +125,7 @@ describe('Learning Content | Integration | Repositories | Area', function () { ]; // when - await areaRepository.save(areaDtos); + await areaRepository.saveMany(areaDtos); // then const savedAreas = await knex.select('*').from('learningcontent.areas').orderBy('name'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js index e5a633c8cba..ef75f1dcd0f 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Challenge', function ( await knex('learningcontent.challenges').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert challenges', async function () { // given const challengeDtos = [ @@ -130,7 +130,7 @@ describe('Learning Content | Integration | Repositories | Challenge', function ( ]; // when - await challengeRepository.save(challengeDtos); + await challengeRepository.saveMany(challengeDtos); // then const savedChallenges = await knex.select('*').from('learningcontent.challenges').orderBy('id'); @@ -421,7 +421,7 @@ describe('Learning Content | Integration | Repositories | Challenge', function ( ]; // when - await challengeRepository.save(challengeDtos); + await challengeRepository.saveMany(challengeDtos); // then const savedChallenges = await knex.select('*').from('learningcontent.challenges').orderBy('id'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js index 6b346adff8d..3f239f9af17 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Competence', function await knex('learningcontent.competences').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert competences', async function () { // given const competenceDtos = [ @@ -77,7 +77,7 @@ describe('Learning Content | Integration | Repositories | Competence', function ]; // when - await competenceRepository.save(competenceDtos); + await competenceRepository.saveMany(competenceDtos); // then const savedCompetences = await knex.select('*').from('learningcontent.competences').orderBy(['origin', 'index']); @@ -287,7 +287,7 @@ describe('Learning Content | Integration | Repositories | Competence', function ]; // when - await competenceRepository.save(competenceDtos); + await competenceRepository.saveMany(competenceDtos); // then const savedCompetences = await knex diff --git a/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js index 60066a23ebf..447470598ec 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Course', function () { await knex('learningcontent.courses').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert courses', async function () { // given const courseDtos = [ @@ -37,7 +37,7 @@ describe('Learning Content | Integration | Repositories | Course', function () { ]; // when - await courseRepository.save(courseDtos); + await courseRepository.saveMany(courseDtos); // then const savedCourses = await knex.select('*').from('learningcontent.courses').orderBy('id'); @@ -111,7 +111,7 @@ describe('Learning Content | Integration | Repositories | Course', function () { ]; // when - await courseRepository.save(courseDtos); + await courseRepository.saveMany(courseDtos); // then const savedCourses = await knex.select('*').from('learningcontent.courses').orderBy('id'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js index 007f8b63450..bc27d207f05 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Framework', function ( await knex('learningcontent.frameworks').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert frameworks', async function () { // given const frameworkDtos = [ @@ -17,7 +17,7 @@ describe('Learning Content | Integration | Repositories | Framework', function ( ]; // when - await frameworkRepository.save(frameworkDtos); + await frameworkRepository.saveMany(frameworkDtos); // then const savedFrameworks = await knex.select('*').from('learningcontent.frameworks').orderBy('name'); @@ -49,7 +49,7 @@ describe('Learning Content | Integration | Repositories | Framework', function ( ]; // when - await frameworkRepository.save(frameworkDtos); + await frameworkRepository.saveMany(frameworkDtos); // then const savedFrameworks = await knex.select('*').from('learningcontent.frameworks').orderBy('name'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/learning-content-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/learning-content-repository_test.js index cad92a9eac7..ca2a45984fa 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/learning-content-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/learning-content-repository_test.js @@ -2,7 +2,7 @@ import { LearningContentRepository } from '../../../../../src/learning-content/i import { expect } from '../../../../test-helper.js'; describe('Learning Content | Integration | Repositories | Learning Content', function () { - describe('#save', function () { + describe('#saveMany', function () { describe('when dtos are nullish', function () { it('should do nothing', async function () { // given @@ -10,7 +10,7 @@ describe('Learning Content | Integration | Repositories | Learning Content', fun const dtos = undefined; // when - const result = await repository.save(dtos); + const result = await repository.saveMany(dtos); // then expect(result).to.be.undefined; diff --git a/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js index c7e3a14f808..f63ba1bd1af 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Mission', function () await knex('learningcontent.missions').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert missions', async function () { // given const missionDtos = [ @@ -64,7 +64,7 @@ describe('Learning Content | Integration | Repositories | Mission', function () ]; // when - await missionRepository.save(missionDtos); + await missionRepository.saveMany(missionDtos); // then const savedMissions = await knex.select('*').from('learningcontent.missions').orderBy('id'); @@ -208,7 +208,7 @@ describe('Learning Content | Integration | Repositories | Mission', function () ]; // when - await missionRepository.save(missionDtos); + await missionRepository.saveMany(missionDtos); // then const savedMissions = await knex.select('*').from('learningcontent.missions').orderBy('id'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js index f2b8795f087..97ad0229b51 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Skill', function () { await knex('learningcontent.skills').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert skills', async function () { // given const skillDtos = [ @@ -67,7 +67,7 @@ describe('Learning Content | Integration | Repositories | Skill', function () { ]; // when - await skillRepository.save(skillDtos); + await skillRepository.saveMany(skillDtos); // then const savedSkills = await knex.select('*').from('learningcontent.skills').orderBy('name'); @@ -248,7 +248,7 @@ describe('Learning Content | Integration | Repositories | Skill', function () { await databaseBuilder.commit(); // when - await skillRepository.save(skillDtos); + await skillRepository.saveMany(skillDtos); // then const savedSkills = await knex.select('*').from('learningcontent.skills').orderBy('name'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js index 9501df6d16d..6aa9741f27c 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Thematic', function () await knex('learningcontent.thematics').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert thematics', async function () { // given const thematicDtos = [ @@ -34,7 +34,7 @@ describe('Learning Content | Integration | Repositories | Thematic', function () ]; // when - await thematicRepository.save(thematicDtos); + await thematicRepository.saveMany(thematicDtos); // then const savedThematics = await knex.select('*').from('learningcontent.thematics').orderBy('id'); @@ -105,7 +105,7 @@ describe('Learning Content | Integration | Repositories | Thematic', function () ]; // when - await thematicRepository.save(thematicDtos); + await thematicRepository.saveMany(thematicDtos); // then const savedThematics = await knex.select('*').from('learningcontent.thematics').orderBy('id'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js index 3205b6b13a8..e1ff1d4d656 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Tube', function () { await knex('learningcontent.tubes').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert tubes', async function () { // given const tubeDtos = [ @@ -52,7 +52,7 @@ describe('Learning Content | Integration | Repositories | Tube', function () { ]; // when - await tubeRepository.save(tubeDtos); + await tubeRepository.saveMany(tubeDtos); // then const savedTubes = await knex.select('*').from('learningcontent.tubes').orderBy('id'); @@ -164,7 +164,7 @@ describe('Learning Content | Integration | Repositories | Tube', function () { ]; // when - await tubeRepository.save(tubeDtos); + await tubeRepository.saveMany(tubeDtos); // then const savedTubes = await knex.select('*').from('learningcontent.tubes').orderBy('id'); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js index 721a20f7783..23dc39a1526 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js @@ -6,7 +6,7 @@ describe('Learning Content | Integration | Repositories | Tutorial', function () await knex('learningcontent.tutorials').truncate(); }); - describe('#save', function () { + describe('#saveMany', function () { it('should insert tutorials', async function () { // given const tutorialDtos = [ @@ -40,7 +40,7 @@ describe('Learning Content | Integration | Repositories | Tutorial', function () ]; // when - await tutorialRepository.save(tutorialDtos); + await tutorialRepository.saveMany(tutorialDtos); // then const savedTutorials = await knex.select('*').from('learningcontent.tutorials').orderBy('id'); @@ -121,7 +121,7 @@ describe('Learning Content | Integration | Repositories | Tutorial', function () ]; // when - await tutorialRepository.save(tutorialDtos); + await tutorialRepository.saveMany(tutorialDtos); // then const savedTutorials = await knex.select('*').from('learningcontent.tutorials').orderBy('id'); diff --git a/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js b/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js index c2d889fd2ad..94bd4f18912 100644 --- a/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js +++ b/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js @@ -41,34 +41,34 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release', }; const frameworkRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const areaRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const competenceRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const thematicRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const tubeRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const skillRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const challengeRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const courseRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const tutorialRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const missionRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; // when @@ -88,16 +88,16 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release', // then expect(LearningContentCache.instance.update).to.have.been.calledOnce; - expect(frameworkRepository.save).to.have.been.calledOnceWithExactly(frameworks); - expect(areaRepository.save).to.have.been.calledOnceWithExactly(areas); - expect(competenceRepository.save).to.have.been.calledOnceWithExactly(competences); - expect(thematicRepository.save).to.have.been.calledOnceWithExactly(thematics); - expect(tubeRepository.save).to.have.been.calledOnceWithExactly(tubes); - expect(skillRepository.save).to.have.been.calledOnceWithExactly(skills); - expect(challengeRepository.save).to.have.been.calledOnceWithExactly(challenges); - expect(courseRepository.save).to.have.been.calledOnceWithExactly(courses); - expect(tutorialRepository.save).to.have.been.calledOnceWithExactly(tutorials); - expect(missionRepository.save).to.have.been.calledOnceWithExactly(missions); + expect(frameworkRepository.saveMany).to.have.been.calledOnceWithExactly(frameworks); + expect(areaRepository.saveMany).to.have.been.calledOnceWithExactly(areas); + expect(competenceRepository.saveMany).to.have.been.calledOnceWithExactly(competences); + expect(thematicRepository.saveMany).to.have.been.calledOnceWithExactly(thematics); + expect(tubeRepository.saveMany).to.have.been.calledOnceWithExactly(tubes); + expect(skillRepository.saveMany).to.have.been.calledOnceWithExactly(skills); + expect(challengeRepository.saveMany).to.have.been.calledOnceWithExactly(challenges); + expect(courseRepository.saveMany).to.have.been.calledOnceWithExactly(courses); + expect(tutorialRepository.saveMany).to.have.been.calledOnceWithExactly(tutorials); + expect(missionRepository.saveMany).to.have.been.calledOnceWithExactly(missions); }); }); }); diff --git a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js index 10c7fc2d64a..81d7304e67a 100644 --- a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js +++ b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js @@ -17,45 +17,45 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca beforeEach(function () { frameworkRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - frameworkRepository.save.rejects('I should not be called'); + frameworkRepository.saveMany.rejects('I should not be called'); areaRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - areaRepository.save.rejects('I should not be called'); + areaRepository.saveMany.rejects('I should not be called'); competenceRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - competenceRepository.save.rejects('I should not be called'); + competenceRepository.saveMany.rejects('I should not be called'); thematicRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - thematicRepository.save.rejects('I should not be called'); + thematicRepository.saveMany.rejects('I should not be called'); tubeRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - tubeRepository.save.rejects('I should not be called'); + tubeRepository.saveMany.rejects('I should not be called'); skillRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - skillRepository.save.rejects('I should not be called'); + skillRepository.saveMany.rejects('I should not be called'); challengeRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - challengeRepository.save.rejects('I should not be called'); + challengeRepository.saveMany.rejects('I should not be called'); courseRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - courseRepository.save.rejects('I should not be called'); + courseRepository.saveMany.rejects('I should not be called'); tutorialRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - tutorialRepository.save.rejects('I should not be called'); + tutorialRepository.saveMany.rejects('I should not be called'); missionRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; - missionRepository.save.rejects('I should not be called'); + missionRepository.saveMany.rejects('I should not be called'); repositories = { frameworkRepository, areaRepository, @@ -173,7 +173,7 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca 'tutorials', 'missions', ].forEach((modelName) => { - it(`should call save on appropriate repository for model ${modelName}`, async function () { + it(`should call saveMany on appropriate repository for model ${modelName}`, async function () { // given const recordId = 'recId'; const updatedRecord = Symbol('updated record'); @@ -190,7 +190,7 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca patch: sinon.stub().resolves(), }, }; - repositoriesByModel[modelName].save.withArgs([updatedRecord]).resolves(); + repositoriesByModel[modelName].saveMany.withArgs([updatedRecord]).resolves(); // when await patchLearningContentCacheEntry({ @@ -202,8 +202,8 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca }); // then - expect(repositoriesByModel[modelName].save).to.have.been.calledOnce; - expect(repositoriesByModel[modelName].save).to.have.been.calledWithExactly([updatedRecord]); + expect(repositoriesByModel[modelName].saveMany).to.have.been.calledOnce; + expect(repositoriesByModel[modelName].saveMany).to.have.been.calledWithExactly([updatedRecord]); }); }); }); diff --git a/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js b/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js index b266a371c04..6f05f906a7e 100644 --- a/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js +++ b/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js @@ -41,34 +41,34 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content }; const frameworkRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const areaRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const competenceRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const thematicRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const tubeRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const skillRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const challengeRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const courseRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const tutorialRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; const missionRepository = { - save: sinon.stub(), + saveMany: sinon.stub(), }; // when @@ -88,16 +88,16 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content // then expect(LearningContentCache.instance.reset).to.have.been.calledOnce; - expect(frameworkRepository.save).to.have.been.calledOnceWithExactly(frameworks); - expect(areaRepository.save).to.have.been.calledOnceWithExactly(areas); - expect(competenceRepository.save).to.have.been.calledOnceWithExactly(competences); - expect(thematicRepository.save).to.have.been.calledOnceWithExactly(thematics); - expect(tubeRepository.save).to.have.been.calledOnceWithExactly(tubes); - expect(skillRepository.save).to.have.been.calledOnceWithExactly(skills); - expect(challengeRepository.save).to.have.been.calledOnceWithExactly(challenges); - expect(courseRepository.save).to.have.been.calledOnceWithExactly(courses); - expect(tutorialRepository.save).to.have.been.calledOnceWithExactly(tutorials); - expect(missionRepository.save).to.have.been.calledOnceWithExactly(missions); + expect(frameworkRepository.saveMany).to.have.been.calledOnceWithExactly(frameworks); + expect(areaRepository.saveMany).to.have.been.calledOnceWithExactly(areas); + expect(competenceRepository.saveMany).to.have.been.calledOnceWithExactly(competences); + expect(thematicRepository.saveMany).to.have.been.calledOnceWithExactly(thematics); + expect(tubeRepository.saveMany).to.have.been.calledOnceWithExactly(tubes); + expect(skillRepository.saveMany).to.have.been.calledOnceWithExactly(skills); + expect(challengeRepository.saveMany).to.have.been.calledOnceWithExactly(challenges); + expect(courseRepository.saveMany).to.have.been.calledOnceWithExactly(courses); + expect(tutorialRepository.saveMany).to.have.been.calledOnceWithExactly(tutorials); + expect(missionRepository.saveMany).to.have.been.calledOnceWithExactly(missions); }); }); }); From 20c7ba55a5a4fa9781ee4e3a305dcf7ed9ebcc41 Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:18:12 +0100 Subject: [PATCH 19/24] refactor(api): patch learning content clears only patched entry Co-authored-by: Laura Bergoens --- .../patch-learning-content-cache-entry.js | 20 +- .../repositories/area-repository.js | 4 +- .../repositories/challenge-repository.js | 4 +- .../repositories/competence-repository.js | 4 +- .../repositories/course-repository.js | 4 +- .../repositories/framework-repository.js | 4 +- .../learning-content-repository.js | 12 +- .../repositories/mission-repository.js | 4 +- .../repositories/skill-repository.js | 4 +- .../repositories/thematic-repository.js | 4 +- .../repositories/tube-repository.js | 4 +- .../repositories/tutorial-repository.js | 4 +- .../learning-content-repository.js | 8 +- .../repositories/area-repository_test.js | 85 ++++ .../repositories/challenge-repository_test.js | 235 +++++++++ .../competence-repository_test.js | 120 +++++ .../repositories/course-repository_test.js | 72 +++ .../repositories/framework-repository_test.js | 45 ++ .../repositories/mission-repository_test.js | 125 +++++ .../repositories/skill-repository_test.js | 456 +++++++++++------- .../repositories/thematic-repository_test.js | 83 ++++ .../repositories/tube-repository_test.js | 103 ++++ .../repositories/tutorial-repository_test.js | 85 ++++ ...patch-learning-content-cache-entry_test.js | 48 +- 24 files changed, 1311 insertions(+), 226 deletions(-) diff --git a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js index 8203d020125..cc4073ab9a0 100644 --- a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js +++ b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js @@ -65,33 +65,33 @@ async function patchDatabase( missionRepository, ) { if (modelName === 'frameworks') { - await frameworkRepository.saveMany([patchedRecord]); + await frameworkRepository.save(patchedRecord); } if (modelName === 'areas') { - await areaRepository.saveMany([patchedRecord]); + await areaRepository.save(patchedRecord); } if (modelName === 'competences') { - await competenceRepository.saveMany([patchedRecord]); + await competenceRepository.save(patchedRecord); } if (modelName === 'thematics') { - await thematicRepository.saveMany([patchedRecord]); + await thematicRepository.save(patchedRecord); } if (modelName === 'tubes') { - await tubeRepository.saveMany([patchedRecord]); + await tubeRepository.save(patchedRecord); } if (modelName === 'skills') { - await skillRepository.saveMany([patchedRecord]); + await skillRepository.save(patchedRecord); } if (modelName === 'challenges') { - await challengeRepository.saveMany([patchedRecord]); + await challengeRepository.save(patchedRecord); } if (modelName === 'courses') { - await courseRepository.saveMany([patchedRecord]); + await courseRepository.save(patchedRecord); } if (modelName === 'tutorials') { - await tutorialRepository.saveMany([patchedRecord]); + await tutorialRepository.save(patchedRecord); } if (modelName === 'missions') { - await missionRepository.saveMany([patchedRecord]); + await missionRepository.save(patchedRecord); } } diff --git a/api/src/learning-content/infrastructure/repositories/area-repository.js b/api/src/learning-content/infrastructure/repositories/area-repository.js index a862aec0502..cb60ed81f04 100644 --- a/api/src/learning-content/infrastructure/repositories/area-repository.js +++ b/api/src/learning-content/infrastructure/repositories/area-repository.js @@ -18,8 +18,8 @@ class AreaRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/challenge-repository.js b/api/src/learning-content/infrastructure/repositories/challenge-repository.js index f1e61810fd9..62d40d68507 100644 --- a/api/src/learning-content/infrastructure/repositories/challenge-repository.js +++ b/api/src/learning-content/infrastructure/repositories/challenge-repository.js @@ -89,8 +89,8 @@ class ChallengeRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/competence-repository.js b/api/src/learning-content/infrastructure/repositories/competence-repository.js index e52725117de..540d059922f 100644 --- a/api/src/learning-content/infrastructure/repositories/competence-repository.js +++ b/api/src/learning-content/infrastructure/repositories/competence-repository.js @@ -19,8 +19,8 @@ class CompetenceRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/course-repository.js b/api/src/learning-content/infrastructure/repositories/course-repository.js index b07edac04b2..b4be1b42ef2 100644 --- a/api/src/learning-content/infrastructure/repositories/course-repository.js +++ b/api/src/learning-content/infrastructure/repositories/course-repository.js @@ -17,8 +17,8 @@ class CourseRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/framework-repository.js b/api/src/learning-content/infrastructure/repositories/framework-repository.js index 3a3645323e3..f03825ee3b9 100644 --- a/api/src/learning-content/infrastructure/repositories/framework-repository.js +++ b/api/src/learning-content/infrastructure/repositories/framework-repository.js @@ -13,8 +13,8 @@ class FrameworkRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js index b68d794f2cd..c9e4c20a96a 100644 --- a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js +++ b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js @@ -31,7 +31,17 @@ export class LearningContentRepository { this.clearCache(); } - clearCache() { + /** + * @param {object} object + */ + async save(object) { + const dto = this.toDto(object); + const knex = DomainTransaction.getConnection(); + await knex.insert(dto).into(this.#tableName).onConflict('id').merge(); + this.clearCache(dto.id); + } + + clearCache(_id) { // must be overriden } diff --git a/api/src/learning-content/infrastructure/repositories/mission-repository.js b/api/src/learning-content/infrastructure/repositories/mission-repository.js index 5a82848fd28..21256ae7495 100644 --- a/api/src/learning-content/infrastructure/repositories/mission-repository.js +++ b/api/src/learning-content/infrastructure/repositories/mission-repository.js @@ -36,8 +36,8 @@ class MissionRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/skill-repository.js b/api/src/learning-content/infrastructure/repositories/skill-repository.js index ac0e135f800..710c7e378b7 100644 --- a/api/src/learning-content/infrastructure/repositories/skill-repository.js +++ b/api/src/learning-content/infrastructure/repositories/skill-repository.js @@ -36,8 +36,8 @@ class SkillRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/thematic-repository.js b/api/src/learning-content/infrastructure/repositories/thematic-repository.js index d5253cd7515..8ad4d9983bc 100644 --- a/api/src/learning-content/infrastructure/repositories/thematic-repository.js +++ b/api/src/learning-content/infrastructure/repositories/thematic-repository.js @@ -16,8 +16,8 @@ class ThematicRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/tube-repository.js b/api/src/learning-content/infrastructure/repositories/tube-repository.js index e92ea0f319d..3fe7d80a42c 100644 --- a/api/src/learning-content/infrastructure/repositories/tube-repository.js +++ b/api/src/learning-content/infrastructure/repositories/tube-repository.js @@ -34,8 +34,8 @@ class TubeRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/learning-content/infrastructure/repositories/tutorial-repository.js b/api/src/learning-content/infrastructure/repositories/tutorial-repository.js index f29d41368fd..85e535bb867 100644 --- a/api/src/learning-content/infrastructure/repositories/tutorial-repository.js +++ b/api/src/learning-content/infrastructure/repositories/tutorial-repository.js @@ -18,8 +18,8 @@ class TutorialRepository extends LearningContentRepository { }; } - clearCache() { - clearCache(); + clearCache(id) { + clearCache(id); } } diff --git a/api/src/shared/infrastructure/repositories/learning-content-repository.js b/api/src/shared/infrastructure/repositories/learning-content-repository.js index 8a19ed2059c..dfeebc997bf 100644 --- a/api/src/shared/infrastructure/repositories/learning-content-repository.js +++ b/api/src/shared/infrastructure/repositories/learning-content-repository.js @@ -72,8 +72,12 @@ export class LearningContentRepository { return dtos.map((dto) => (dto.id ? dto : null)); } - clearCache() { - this.#dataloader.clearAll(); + clearCache(id) { + if (id) { + this.#dataloader.clear(id); + } else { + this.#dataloader.clearAll(); + } this.#findCache.clear(); } } diff --git a/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js index aa368684c9c..8c9f8f6fec4 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/area-repository_test.js @@ -166,4 +166,89 @@ describe('Learning Content | Integration | Repositories | Area', function () { }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildArea({ id: 'areaIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert area when it does not exist in DB', async function () { + // given + const areaDto = { + id: 'areaA', + name: 'name Domaine A', + code: 'code Domaine A', + title_i18n: { fr: 'title_i18n FR Domaine A', en: 'title_i18n EN Domaine A' }, + color: 'color Domaine A', + frameworkId: 'frameworkId Domaine A', + competenceIds: ['competenceId1 Domaine A'], + }; + + // when + await areaRepository.save(areaDto); + + // then + const savedArea = await knex.select('*').from('learningcontent.areas').where({ id: areaDto.id }).first(); + const [{ count }] = await knex('learningcontent.areas').count(); + expect(count).to.equal(2); + expect(savedArea).to.deep.equal({ + id: 'areaA', + name: 'name Domaine A', + code: 'code Domaine A', + title_i18n: { fr: 'title_i18n FR Domaine A', en: 'title_i18n EN Domaine A' }, + color: 'color Domaine A', + frameworkId: 'frameworkId Domaine A', + competenceIds: ['competenceId1 Domaine A'], + }); + }); + + it('should update area when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildArea({ + id: 'areaA', + name: 'name Domaine A', + code: 'code Domaine A', + title_i18n: { fr: 'title_i18n FR Domaine A', en: 'title_i18n EN Domaine A' }, + color: 'color Domaine A', + frameworkId: 'frameworkId Domaine A', + competenceIds: ['competenceId1 Domaine A'], + }); + await databaseBuilder.commit(); + const areaDto = { + id: 'areaA', + name: 'name Domaine A modified', + code: 'code Domaine A modified', + title_i18n: { + fr: 'title_i18n FR Domaine A modified', + en: 'title_i18n EN Domaine A modified', + nl: 'title_i18n NL Domaine A modified', + }, + color: 'color Domaine A modified', + frameworkId: 'frameworkId Domaine A modified', + competenceIds: ['competenceId1 Domaine A modified', 'competenceId2 Domaine A modified'], + }; + + // when + await areaRepository.save(areaDto); + + // then + const savedArea = await knex.select('*').from('learningcontent.areas').where({ id: areaDto.id }).first(); + const [{ count }] = await knex('learningcontent.areas').count(); + expect(count).to.equal(2); + expect(savedArea).to.deep.equal({ + id: 'areaA', + name: 'name Domaine A modified', + code: 'code Domaine A modified', + title_i18n: { + fr: 'title_i18n FR Domaine A modified', + en: 'title_i18n EN Domaine A modified', + nl: 'title_i18n NL Domaine A modified', + }, + color: 'color Domaine A modified', + frameworkId: 'frameworkId Domaine A modified', + competenceIds: ['competenceId1 Domaine A modified', 'competenceId2 Domaine A modified'], + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js index ef75f1dcd0f..90f095f20a0 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/challenge-repository_test.js @@ -548,4 +548,239 @@ describe('Learning Content | Integration | Repositories | Challenge', function ( }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildChallenge({ id: 'challengeIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert challenge when it does not exist in DB', async function () { + // given + const challengeDto = { + id: 'challengeIdA', + instruction: 'instruction Epreuve A', + alternativeInstruction: 'alternativeInstruction Epreuve A', + proposals: 'proposals Epreuve A', + type: 'QCU', + solution: 'solution Epreuve A', + solutionToDisplay: 'solutionToDisplay Epreuve A', + t1Status: true, + t2Status: true, + t3Status: true, + status: 'archivé', + genealogy: 'genealogy Epreuve A', + accessibility1: 'accessibility1 Epreuve A', + accessibility2: 'accessibility2 Epreuve B', + requireGafamWebsiteAccess: true, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing Epreuve A', + isAwarenessChallenge: true, + toRephrase: true, + alternativeVersion: 8, + shuffled: true, + illustrationAlt: 'illustrationAlt Epreuve A', + illustrationUrl: 'illustrationUrl Epreuve A', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive Epreuve A', + alpha: 1.1, + delta: 3.3, + autoReply: true, + focusable: true, + format: 'format Epreuve A', + timer: 180, + embedHeight: 800, + embedUrl: 'embedUrl Epreuve A', + embedTitle: 'embedTitle Epreuve A', + locales: ['fr'], + competenceId: 'competenceIdA', + skillId: 'skillIdA', + }; + + // when + await challengeRepository.save(challengeDto); + + // then + const savedChallenge = await knex + .select('*') + .from('learningcontent.challenges') + .where({ id: challengeDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.challenges').count(); + expect(count).to.equal(2); + expect(savedChallenge).to.deep.equal({ + id: 'challengeIdA', + instruction: 'instruction Epreuve A', + alternativeInstruction: 'alternativeInstruction Epreuve A', + proposals: 'proposals Epreuve A', + type: 'QCU', + solution: 'solution Epreuve A', + solutionToDisplay: 'solutionToDisplay Epreuve A', + t1Status: true, + t2Status: true, + t3Status: true, + status: 'archivé', + genealogy: 'genealogy Epreuve A', + accessibility1: 'accessibility1 Epreuve A', + accessibility2: 'accessibility2 Epreuve B', + requireGafamWebsiteAccess: true, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing Epreuve A', + isAwarenessChallenge: true, + toRephrase: true, + alternativeVersion: 8, + shuffled: true, + illustrationAlt: 'illustrationAlt Epreuve A', + illustrationUrl: 'illustrationUrl Epreuve A', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive Epreuve A', + alpha: 1.1, + delta: 3.3, + autoReply: true, + focusable: true, + format: 'format Epreuve A', + timer: 180, + embedHeight: 800, + embedUrl: 'embedUrl Epreuve A', + embedTitle: 'embedTitle Epreuve A', + locales: ['fr'], + competenceId: 'competenceIdA', + skillId: 'skillIdA', + }); + }); + + it('should update challenge when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildChallenge({ + id: 'challengeIdA', + instruction: 'instruction Epreuve A', + alternativeInstruction: 'alternativeInstruction Epreuve A', + proposals: 'proposals Epreuve A', + type: 'QCU', + solution: 'solution Epreuve A', + solutionToDisplay: 'solutionToDisplay Epreuve A', + t1Status: true, + t2Status: true, + t3Status: true, + status: 'archivé', + genealogy: 'genealogy Epreuve A', + accessibility1: 'accessibility1 Epreuve A', + accessibility2: 'accessibility2 Epreuve B', + requireGafamWebsiteAccess: true, + isIncompatibleIpadCertif: true, + deafAndHardOfHearing: 'deafAndHardOfHearing Epreuve A', + isAwarenessChallenge: true, + toRephrase: true, + alternativeVersion: 8, + shuffled: true, + illustrationAlt: 'illustrationAlt Epreuve A', + illustrationUrl: 'illustrationUrl Epreuve A', + attachments: ['attachment1', 'attachment2'], + responsive: 'responsive Epreuve A', + alpha: 1.1, + delta: 3.3, + autoReply: true, + focusable: true, + format: 'format Epreuve A', + timer: 180, + embedHeight: 800, + embedUrl: 'embedUrl Epreuve A', + embedTitle: 'embedTitle Epreuve A', + locales: ['fr'], + competenceId: 'competenceIdA', + skillId: 'skillIdA', + }); + await databaseBuilder.commit(); + const challengeDto = { + id: 'challengeIdA', + instruction: 'instruction Epreuve A modified', + alternativeInstruction: 'alternativeInstruction Epreuve A modified', + proposals: 'proposals Epreuve A modified', + type: 'QCU modified', + solution: 'solution Epreuve A modified', + solutionToDisplay: 'solutionToDisplay Epreuve A modified', + t1Status: false, + t2Status: false, + t3Status: false, + status: 'archivé modified', + genealogy: 'genealogy Epreuve A modified', + accessibility1: 'accessibility1 Epreuve A modified', + accessibility2: 'accessibility2 Epreuve A modified', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing Epreuve A modified', + isAwarenessChallenge: false, + toRephrase: true, + alternativeVersion: 11, + shuffled: true, + illustrationAlt: 'illustrationAlt Epreuve A modified', + illustrationUrl: 'illustrationUrl Epreuve A modified', + attachments: ['attachment4', 'attachment2', 'attachment3'], + responsive: 'responsive Epreuve A modified', + alpha: 8.0, + delta: 90.5, + autoReply: false, + focusable: false, + format: 'format Epreuve A modified', + timer: 250, + embedHeight: 1800, + embedUrl: 'embedUrl Epreuve A modified', + embedTitle: 'embedTitle Epreuve A modified', + locales: ['fr', 'fr-fr'], + competenceId: 'competenceIdA modified', + skillId: 'skillIdA modified', + }; + + // when + await challengeRepository.save(challengeDto); + + // then + const savedChallenge = await knex + .select('*') + .from('learningcontent.challenges') + .where({ id: challengeDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.challenges').count(); + expect(count).to.equal(2); + expect(savedChallenge).to.deep.equal({ + id: 'challengeIdA', + instruction: 'instruction Epreuve A modified', + alternativeInstruction: 'alternativeInstruction Epreuve A modified', + proposals: 'proposals Epreuve A modified', + type: 'QCU modified', + solution: 'solution Epreuve A modified', + solutionToDisplay: 'solutionToDisplay Epreuve A modified', + t1Status: false, + t2Status: false, + t3Status: false, + status: 'archivé modified', + genealogy: 'genealogy Epreuve A modified', + accessibility1: 'accessibility1 Epreuve A modified', + accessibility2: 'accessibility2 Epreuve A modified', + requireGafamWebsiteAccess: false, + isIncompatibleIpadCertif: false, + deafAndHardOfHearing: 'deafAndHardOfHearing Epreuve A modified', + isAwarenessChallenge: false, + toRephrase: true, + alternativeVersion: 11, + shuffled: true, + illustrationAlt: 'illustrationAlt Epreuve A modified', + illustrationUrl: 'illustrationUrl Epreuve A modified', + attachments: ['attachment4', 'attachment2', 'attachment3'], + responsive: 'responsive Epreuve A modified', + alpha: 8.0, + delta: 90.5, + autoReply: false, + focusable: false, + format: 'format Epreuve A modified', + timer: 250, + embedHeight: 1800, + embedUrl: 'embedUrl Epreuve A modified', + embedTitle: 'embedTitle Epreuve A modified', + locales: ['fr', 'fr-fr'], + competenceId: 'competenceIdA modified', + skillId: 'skillIdA modified', + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js index 3f239f9af17..e18b713cf29 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/competence-repository_test.js @@ -380,4 +380,124 @@ describe('Learning Content | Integration | Repositories | Competence', function }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildCompetence({ id: 'competenceIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert competence when it does not exist in DB', async function () { + // given + const competenceDto = { + id: 'competence11', + index: '1.1', + areaId: 'area1', + skillIds: ['skill1', 'skill2', 'skill3'], + thematicIds: ['thematic1', 'thematic2'], + origin: 'Pix', + name_i18n: { + fr: 'Compétence 1.1', + en: 'Competence 1.1', + }, + description_i18n: { + fr: 'C’est la compétence 1.1', + en: 'It’s competence 1.1', + }, + }; + + // when + await competenceRepository.save(competenceDto); + + // then + const savedCompetence = await knex + .select('*') + .from('learningcontent.competences') + .where({ id: competenceDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.competences').count(); + expect(count).to.equal(2); + expect(savedCompetence).to.deep.equal({ + id: 'competence11', + index: '1.1', + areaId: 'area1', + skillIds: ['skill1', 'skill2', 'skill3'], + thematicIds: ['thematic1', 'thematic2'], + origin: 'Pix', + name_i18n: { + fr: 'Compétence 1.1', + en: 'Competence 1.1', + }, + description_i18n: { + fr: 'C’est la compétence 1.1', + en: 'It’s competence 1.1', + }, + }); + }); + + it('should update competence when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildCompetence({ + id: 'competence11', + index: '1.1', + areaId: 'area1', + skillIds: ['skill1'], + thematicIds: ['thematic1'], + origin: 'Pix', + name_i18n: { + fr: 'Compétence 1.1 old', + en: 'Competence 1.1 old', + }, + description_i18n: { + fr: 'C’est la compétence 1.1 old', + en: 'It’s competence 1.1 old', + }, + }); + await databaseBuilder.commit(); + const competenceDto = { + id: 'competence11', + index: '1.1', + areaId: 'area1', + skillIds: ['skill1', 'skill2', 'skill3'], + thematicIds: ['thematic1', 'thematic2'], + origin: 'Pix', + name_i18n: { + fr: 'Compétence 1.1', + en: 'Competence 1.1', + }, + description_i18n: { + fr: 'C’est la compétence 1.1', + en: 'It’s competence 1.1', + }, + }; + + // when + await competenceRepository.save(competenceDto); + + // then + const savedCompetence = await knex + .select('*') + .from('learningcontent.competences') + .where({ id: competenceDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.competences').count(); + expect(count).to.equal(2); + expect(savedCompetence).to.deep.equal({ + id: 'competence11', + index: '1.1', + areaId: 'area1', + skillIds: ['skill1', 'skill2', 'skill3'], + thematicIds: ['thematic1', 'thematic2'], + origin: 'Pix', + name_i18n: { + fr: 'Compétence 1.1', + en: 'Competence 1.1', + }, + description_i18n: { + fr: 'C’est la compétence 1.1', + en: 'It’s competence 1.1', + }, + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js index 447470598ec..4b644acf76b 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/course-repository_test.js @@ -145,4 +145,76 @@ describe('Learning Content | Integration | Repositories | Course', function () { }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildCourse({ id: 'courseIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert course when it does not exist in DB', async function () { + // given + const courseDto = { + id: 'courseIdA', + name: 'instruction Test Statique A', + description: 'description Test Statique A', + isActive: true, + competences: ['competenceIdA'], + challenges: ['challengeIdA'], + }; + + // when + await courseRepository.save(courseDto); + + // then + const savedCourse = await knex.select('*').from('learningcontent.courses').where({ id: courseDto.id }).first(); + const [{ count }] = await knex('learningcontent.courses').count(); + expect(count).to.equal(2); + expect(savedCourse).to.deep.equal({ + id: 'courseIdA', + name: 'instruction Test Statique A', + description: 'description Test Statique A', + isActive: true, + competences: ['competenceIdA'], + challenges: ['challengeIdA'], + }); + }); + + it('should update course when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildCourse({ + id: 'courseIdA', + name: 'instruction Test Statique A', + description: 'description Test Statique A', + isActive: true, + competences: ['competenceIdA'], + challenges: ['challengeIdA'], + }); + await databaseBuilder.commit(); + const courseDto = { + id: 'courseIdA', + name: 'instruction Test Statique A modified', + description: 'description Test Statique A modified', + isActive: false, + competences: ['competenceIdA modified'], + challenges: ['challengeIdA1 modified', 'challengeIdA2 modified'], + }; + + // when + await courseRepository.save(courseDto); + + // then + const savedCourse = await knex.select('*').from('learningcontent.courses').where({ id: courseDto.id }).first(); + const [{ count }] = await knex('learningcontent.courses').count(); + expect(count).to.equal(2); + expect(savedCourse).to.deep.equal({ + id: 'courseIdA', + name: 'instruction Test Statique A modified', + description: 'description Test Statique A modified', + isActive: false, + competences: ['competenceIdA modified'], + challenges: ['challengeIdA1 modified', 'challengeIdA2 modified'], + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js index bc27d207f05..8b71eff1d2f 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/framework-repository_test.js @@ -64,4 +64,49 @@ describe('Learning Content | Integration | Repositories | Framework', function ( }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildFramework({ id: 'frameworkIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert framework when it does not exist in DB', async function () { + // given + const frameworkDto = { id: 'frameworkPix', name: 'Pix' }; + + // when + await frameworkRepository.save(frameworkDto); + + // then + const savedFramework = await knex + .select('*') + .from('learningcontent.frameworks') + .where({ id: frameworkDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.frameworks').count(); + expect(count).to.equal(2); + expect(savedFramework).to.deep.equal({ id: 'frameworkPix', name: 'Pix' }); + }); + + it('should update framework when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildFramework({ id: 'frameworkPix', name: 'Pix' }); + await databaseBuilder.commit(); + const frameworkDto = { id: 'frameworkPix', name: 'Pax' }; + + // when + await frameworkRepository.save(frameworkDto); + + // then + const savedFramework = await knex + .select('*') + .from('learningcontent.frameworks') + .where({ id: frameworkDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.frameworks').count(); + expect(count).to.equal(2); + expect(savedFramework).to.deep.equal({ id: 'frameworkPix', name: 'Pax' }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js index f63ba1bd1af..c6d99702bde 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/mission-repository_test.js @@ -279,4 +279,129 @@ describe('Learning Content | Integration | Repositories | Mission', function () }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildMission({ id: 66 }); + await databaseBuilder.commit(); + }); + + it('should insert mission when it does not exist in DB', async function () { + // given + const missionDto = { + id: 1, + status: 'status Mission A', + name_i18n: { fr: 'name FR Mission A', en: 'name EN Mission A' }, + content: { some: 'content' }, + learningObjectives_i18n: { fr: 'learningObjectives FR Mission A', en: 'learningObjectives EN Mission A' }, + validatedObjectives_i18n: { fr: 'validatedObjectives FR Mission A', en: 'validatedObjectives EN Mission A' }, + introductionMediaType: 'introductionMediaType Mission A', + introductionMediaUrl: 'introductionMediaUrl Mission A', + introductionMediaAlt_i18n: { + fr: 'introductionMediaAlt FR Mission A', + en: 'introductionMediaAlt EN Mission A', + }, + documentationUrl: 'documentationUrl Mission A', + cardImageUrl: 'cardImageUrl Mission A', + competenceId: 'competenceIdA', + }; + + // when + await missionRepository.save(missionDto); + + // then + const savedMission = await knex.select('*').from('learningcontent.missions').where({ id: missionDto.id }).first(); + const [{ count }] = await knex('learningcontent.missions').count(); + expect(count).to.equal(2); + expect(savedMission).to.deep.equal({ + id: 1, + status: 'status Mission A', + name_i18n: { fr: 'name FR Mission A', en: 'name EN Mission A' }, + content: { some: 'content' }, + learningObjectives_i18n: { fr: 'learningObjectives FR Mission A', en: 'learningObjectives EN Mission A' }, + validatedObjectives_i18n: { fr: 'validatedObjectives FR Mission A', en: 'validatedObjectives EN Mission A' }, + introductionMediaType: 'introductionMediaType Mission A', + introductionMediaUrl: 'introductionMediaUrl Mission A', + introductionMediaAlt_i18n: { + fr: 'introductionMediaAlt FR Mission A', + en: 'introductionMediaAlt EN Mission A', + }, + documentationUrl: 'documentationUrl Mission A', + cardImageUrl: 'cardImageUrl Mission A', + competenceId: 'competenceIdA', + }); + }); + + it('should update mission when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildMission({ + id: 1, + status: 'status Mission A', + name_i18n: { fr: 'name FR Mission A', en: 'name EN Mission A' }, + content: { some: 'content' }, + learningObjectives_i18n: { fr: 'learningObjectives FR Mission A', en: 'learningObjectives EN Mission A' }, + validatedObjectives_i18n: { fr: 'validatedObjectives FR Mission A', en: 'validatedObjectives EN Mission A' }, + introductionMediaType: 'introductionMediaType Mission A', + introductionMediaUrl: 'introductionMediaUrl Mission A', + introductionMediaAlt_i18n: { + fr: 'introductionMediaAlt FR Mission A', + en: 'introductionMediaAlt EN Mission A', + }, + documentationUrl: 'documentationUrl Mission A', + cardImageUrl: 'cardImageUrl Mission A', + competenceId: 'competenceIdA', + }); + await databaseBuilder.commit(); + const missionDto = { + id: 1, + status: 'status Mission A modified', + name_i18n: { fr: 'name FR Mission A modified', en: 'name EN Mission A modified' }, + content: { some: 'content' }, + learningObjectives_i18n: { + fr: 'learningObjectives FR Mission A modified', + nl: 'learningObjectives EN Mission A modified', + }, + validatedObjectives_i18n: { + fr: 'validatedObjectives FR Mission A modified', + }, + introductionMediaType: 'introductionMediaType Mission A modified', + introductionMediaUrl: 'introductionMediaUrl Mission A modified', + introductionMediaAlt_i18n: { + en: 'introductionMediaAlt EN Mission A modified', + }, + documentationUrl: 'documentationUrl Mission A modified', + cardImageUrl: 'cardImageUrl Mission A modified', + competenceId: 'competenceIdA modified', + }; + + // when + await missionRepository.save(missionDto); + + // then + const savedMission = await knex.select('*').from('learningcontent.missions').where({ id: missionDto.id }).first(); + const [{ count }] = await knex('learningcontent.missions').count(); + expect(count).to.equal(2); + expect(savedMission).to.deep.equal({ + id: 1, + status: 'status Mission A modified', + name_i18n: { fr: 'name FR Mission A modified', en: 'name EN Mission A modified' }, + content: { some: 'content' }, + learningObjectives_i18n: { + fr: 'learningObjectives FR Mission A modified', + nl: 'learningObjectives EN Mission A modified', + }, + validatedObjectives_i18n: { + fr: 'validatedObjectives FR Mission A modified', + }, + introductionMediaType: 'introductionMediaType Mission A modified', + introductionMediaUrl: 'introductionMediaUrl Mission A modified', + introductionMediaAlt_i18n: { + en: 'introductionMediaAlt EN Mission A modified', + }, + documentationUrl: 'documentationUrl Mission A modified', + cardImageUrl: 'cardImageUrl Mission A modified', + competenceId: 'competenceIdA modified', + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js index 97ad0229b51..b2d465210a6 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/skill-repository_test.js @@ -129,68 +129,11 @@ describe('Learning Content | Integration | Repositories | Skill', function () { }, ]); }); - }); - - describe('when some skills already exist', function () { - it('should upsert skills and keep missing ones', async function () { - // given - databaseBuilder.factory.learningContent.buildSkill({ - id: 'skill1', - name: '@cuiredespates2', - hintStatus: 'pré-validé', - tutorialIds: ['tuto1', 'tuto2'], - learningMoreTutorialIds: ['tutoMore1'], - pixValue: 10000, - competenceId: 'competence1', - status: 'actif', - tubeId: 'tube1', - version: 1, - level: 2, - hint_i18n: { - fr: 'Il faut une casserolle d’eau chaude', - en: 'A casserolle of hot water is needed', - nl: 'Aflugeublik', - }, - }); - databaseBuilder.factory.learningContent.buildSkill({ - id: 'skill2', - name: '@cuiredespates3', - hintStatus: 'validé', - tutorialIds: [], - learningMoreTutorialIds: [], - pixValue: 0, - competenceId: 'competence1', - status: 'actif', - tubeId: 'tube1', - version: 1, - level: 3, - hint_i18n: { - fr: 'Elle doivent être cuite à point', - en: 'These need to be cuite à point', - nl: 'Aflugeublik cuite à point', - }, - }); - databaseBuilder.factory.learningContent.buildSkill({ - id: 'skillDinosaure', - name: '@dinosaure1', - hintStatus: 'validé', - tutorialIds: ['tutoDino'], - learningMoreTutorialIds: ['tutoMoreDino'], - pixValue: 666, - competenceId: 'competenceDino', - status: 'actif', - tubeId: 'tubeDino', - version: 1, - level: 1, - hint_i18n: { - fr: 'Dinosaure', - en: 'Dinosaur', - nl: 'Dinosaurus', - }, - }); - const skillDtos = [ - { + context('when some skills already exist', function () { + it('should upsert skills and keep missing ones', async function () { + // given + databaseBuilder.factory.learningContent.buildSkill({ id: 'skill1', name: '@cuiredespates2', hintStatus: 'pré-validé', @@ -198,117 +141,35 @@ describe('Learning Content | Integration | Repositories | Skill', function () { learningMoreTutorialIds: ['tutoMore1'], pixValue: 10000, competenceId: 'competence1', - status: 'périmé', - tubeId: 'tube1', - version: 1, - level: 2, - hint_i18n: { - fr: 'Il faut une casserolle d’eau chaude', - en: 'A casserolle of hot water is needed', - nl: 'Aflugeublik', - }, - }, - { - id: 'skill1v2', - name: '@cuiredespates2', - hintStatus: 'pré-validé', - tutorialIds: ['tuto1', 'tuto2'], - learningMoreTutorialIds: ['tutoMore1'], - pixValue: 10000, - competenceId: 'competence1', - status: 'actif', - tubeId: 'tube1', - version: 2, - level: 2, - hint_i18n: { - fr: 'Il faut une casserolle d’eau chaude', - en: 'A casserolle of hot water is needed', - nl: 'Aflugeublik', - }, - }, - { - id: 'skill2', - name: '@cuiredespates3', - hintStatus: 'validé', - tutorialIds: ['tuto3'], - learningMoreTutorialIds: ['tutoMore2', 'tutoMore3'], - pixValue: 20000, - competenceId: 'competence1', status: 'actif', tubeId: 'tube1', version: 1, - level: 3, - hint_i18n: { - fr: 'Elle doivent être al dente', - en: 'These need to be al dente', - nl: 'Aflugeublik al dente', - }, - }, - ]; - await databaseBuilder.commit(); - - // when - await skillRepository.saveMany(skillDtos); - - // then - const savedSkills = await knex.select('*').from('learningcontent.skills').orderBy('name'); - - expect(savedSkills).to.deep.equal([ - { - id: 'skill1', - name: '@cuiredespates2', - hintStatus: 'pré-validé', - tutorialIds: ['tuto1', 'tuto2'], - learningMoreTutorialIds: ['tutoMore1'], - pixValue: 10000, - competenceId: 'competence1', - status: 'périmé', - tubeId: 'tube1', - version: 1, level: 2, hint_i18n: { fr: 'Il faut une casserolle d’eau chaude', en: 'A casserolle of hot water is needed', nl: 'Aflugeublik', }, - }, - { - id: 'skill1v2', - name: '@cuiredespates2', - hintStatus: 'pré-validé', - tutorialIds: ['tuto1', 'tuto2'], - learningMoreTutorialIds: ['tutoMore1'], - pixValue: 10000, - competenceId: 'competence1', - status: 'actif', - tubeId: 'tube1', - version: 2, - level: 2, - hint_i18n: { - fr: 'Il faut une casserolle d’eau chaude', - en: 'A casserolle of hot water is needed', - nl: 'Aflugeublik', - }, - }, - { + }); + databaseBuilder.factory.learningContent.buildSkill({ id: 'skill2', name: '@cuiredespates3', hintStatus: 'validé', - tutorialIds: ['tuto3'], - learningMoreTutorialIds: ['tutoMore2', 'tutoMore3'], - pixValue: 20000, + tutorialIds: [], + learningMoreTutorialIds: [], + pixValue: 0, competenceId: 'competence1', status: 'actif', tubeId: 'tube1', version: 1, level: 3, hint_i18n: { - fr: 'Elle doivent être al dente', - en: 'These need to be al dente', - nl: 'Aflugeublik al dente', + fr: 'Elle doivent être cuite à point', + en: 'These need to be cuite à point', + nl: 'Aflugeublik cuite à point', }, - }, - { + }); + databaseBuilder.factory.learningContent.buildSkill({ id: 'skillDinosaure', name: '@dinosaure1', hintStatus: 'validé', @@ -325,16 +186,275 @@ describe('Learning Content | Integration | Repositories | Skill', function () { en: 'Dinosaur', nl: 'Dinosaurus', }, - }, - ]); + }); + + const skillDtos = [ + { + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'périmé', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }, + { + id: 'skill1v2', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 2, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }, + { + id: 'skill2', + name: '@cuiredespates3', + hintStatus: 'validé', + tutorialIds: ['tuto3'], + learningMoreTutorialIds: ['tutoMore2', 'tutoMore3'], + pixValue: 20000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 1, + level: 3, + hint_i18n: { + fr: 'Elle doivent être al dente', + en: 'These need to be al dente', + nl: 'Aflugeublik al dente', + }, + }, + ]; + await databaseBuilder.commit(); + + // when + await skillRepository.saveMany(skillDtos); + + // then + const savedSkills = await knex.select('*').from('learningcontent.skills').orderBy('name'); + + expect(savedSkills).to.deep.equal([ + { + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'périmé', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }, + { + id: 'skill1v2', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 2, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }, + { + id: 'skill2', + name: '@cuiredespates3', + hintStatus: 'validé', + tutorialIds: ['tuto3'], + learningMoreTutorialIds: ['tutoMore2', 'tutoMore3'], + pixValue: 20000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 1, + level: 3, + hint_i18n: { + fr: 'Elle doivent être al dente', + en: 'These need to be al dente', + nl: 'Aflugeublik al dente', + }, + }, + { + id: 'skillDinosaure', + name: '@dinosaure1', + hintStatus: 'validé', + tutorialIds: ['tutoDino'], + learningMoreTutorialIds: ['tutoMoreDino'], + pixValue: 666, + competenceId: 'competenceDino', + status: 'actif', + tubeId: 'tubeDino', + version: 1, + level: 1, + hint_i18n: { + fr: 'Dinosaure', + en: 'Dinosaur', + nl: 'Dinosaurus', + }, + }, + ]); + }); }); }); - describe('when giving additionnal fields', function () { - it('should ignore these', async function () { + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildSkill({ id: 'skillIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert skill when it does not exist in DB', async function () { // given - const skillDtos = [ - { + const skillDto = { + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }; + + // when + await skillRepository.save(skillDto); + + // then + const savedSkill = await knex.select('*').from('learningcontent.skills').where({ id: skillDto.id }).first(); + const [{ count }] = await knex('learningcontent.skills').count(); + expect(count).to.equal(2); + expect(savedSkill).to.deep.equal({ + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }); + }); + + it('should update skill when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildSkill({ + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'actif', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }); + await databaseBuilder.commit(); + const skillDto = { + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'périmé', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }; + + // when + await skillRepository.save(skillDto); + + // then + const savedSkill = await knex.select('*').from('learningcontent.skills').where({ id: skillDto.id }).first(); + const [{ count }] = await knex('learningcontent.skills').count(); + expect(count).to.equal(2); + expect(savedSkill).to.deep.equal({ + id: 'skill1', + name: '@cuiredespates2', + hintStatus: 'pré-validé', + tutorialIds: ['tuto1', 'tuto2'], + learningMoreTutorialIds: ['tutoMore1'], + pixValue: 10000, + competenceId: 'competence1', + status: 'périmé', + tubeId: 'tube1', + version: 1, + level: 2, + hint_i18n: { + fr: 'Il faut une casserolle d’eau chaude', + en: 'A casserolle of hot water is needed', + nl: 'Aflugeublik', + }, + }); + }); + + describe('when giving additionnal fields', function () { + it('should ignore these', async function () { + // given + const skillDto = { id: 'skill1', name: '@cuiredespates2', hintStatus: 'pré-validé', @@ -355,17 +475,15 @@ describe('Learning Content | Integration | Repositories | Skill', function () { airtableId: 'recSkill1', foo: 'foo', bar: 'bar', - }, - ]; + }; - // when - await skillRepository.save(skillDtos); + // when + await skillRepository.save(skillDto); - // then - const savedSkills = await knex.select('*').from('learningcontent.skills').orderBy('name'); + // then + const savedSkill = await knex.select('*').from('learningcontent.skills').where({ id: skillDto.id }).first(); - expect(savedSkills).to.deep.equal([ - { + expect(savedSkill).to.deep.equal({ id: 'skill1', name: '@cuiredespates2', hintStatus: 'pré-validé', @@ -382,8 +500,8 @@ describe('Learning Content | Integration | Repositories | Skill', function () { en: 'A casserolle of hot water is needed', nl: 'Aflugeublik', }, - }, - ]); + }); + }); }); }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js index 6aa9741f27c..2126db1ef87 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/thematic-repository_test.js @@ -140,4 +140,87 @@ describe('Learning Content | Integration | Repositories | Thematic', function () }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildThematic({ id: 'thematicIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert thematic when it does not exist in DB', async function () { + // given + const thematicDto = { + id: 'thematicA', + name_i18n: { fr: 'name_i18n FR Thématique A', en: 'name_i18n EN Thématique A' }, + index: 1, + competenceId: 'competenceId Thématique A', + tubeIds: ['tubeId1 Thématique A'], + }; + + // when + await thematicRepository.save(thematicDto); + + // then + const savedThematic = await knex + .select('*') + .from('learningcontent.thematics') + .where({ id: thematicDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.thematics').count(); + expect(count).to.equal(2); + expect(savedThematic).to.deep.equal({ + id: 'thematicA', + name_i18n: { fr: 'name_i18n FR Thématique A', en: 'name_i18n EN Thématique A' }, + index: 1, + competenceId: 'competenceId Thématique A', + tubeIds: ['tubeId1 Thématique A'], + }); + }); + + it('should update thematic when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildThematic({ + id: 'thematicA', + name_i18n: { fr: 'name_i18n FR Thématique A', en: 'name_i18n EN Thématique A' }, + index: 1, + competenceId: 'competenceId Thématique A', + tubeIds: ['tubeId1 Thématique A'], + }); + await databaseBuilder.commit(); + const thematicDto = { + id: 'thematicA', + name_i18n: { + fr: 'name_i18n FR Thématique A modified', + en: 'name_i18n EN Thématique A', + nl: 'name_i18n NL Thématique A modified', + }, + index: 4, + competenceId: 'competenceId Thématique A modified', + tubeIds: ['tubeId1 Thématique A modified', 'tubeId3 Thématique A modified'], + }; + + // when + await thematicRepository.save(thematicDto); + + // then + const savedThematic = await knex + .select('*') + .from('learningcontent.thematics') + .where({ id: thematicDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.thematics').count(); + expect(count).to.equal(2); + expect(savedThematic).to.deep.equal({ + id: 'thematicA', + name_i18n: { + fr: 'name_i18n FR Thématique A modified', + en: 'name_i18n EN Thématique A', + nl: 'name_i18n NL Thématique A modified', + }, + index: 4, + competenceId: 'competenceId Thématique A modified', + tubeIds: ['tubeId1 Thématique A modified', 'tubeId3 Thématique A modified'], + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js index e1ff1d4d656..81e51909332 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/tube-repository_test.js @@ -216,4 +216,107 @@ describe('Learning Content | Integration | Repositories | Tube', function () { }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildTube({ id: 'tubeIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert tube when it does not exist in DB', async function () { + // given + const tubeDto = { + id: 'tubeIdA', + name: 'name Tube A', + title: 'title Tube A', + description: 'description Tube A', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube A', en: 'practicalTitle EN Tube A' }, + practicalDescription_i18n: { fr: 'practicalDescription FR Tube A', en: 'practicalDescription EN Tube A' }, + competenceId: 'competenceId Tube A', + thematicId: 'thematicId Tube A', + skillIds: ['skillId Tube A'], + isMobileCompliant: true, + isTabletCompliant: true, + }; + + // when + await tubeRepository.save(tubeDto); + + // then + const savedTube = await knex.select('*').from('learningcontent.tubes').where({ id: tubeDto.id }).first(); + const [{ count }] = await knex('learningcontent.tubes').count(); + expect(count).to.equal(2); + expect(savedTube).to.deep.equal({ + id: 'tubeIdA', + name: 'name Tube A', + title: 'title Tube A', + description: 'description Tube A', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube A', en: 'practicalTitle EN Tube A' }, + practicalDescription_i18n: { fr: 'practicalDescription FR Tube A', en: 'practicalDescription EN Tube A' }, + competenceId: 'competenceId Tube A', + thematicId: 'thematicId Tube A', + skillIds: ['skillId Tube A'], + isMobileCompliant: true, + isTabletCompliant: true, + }); + }); + + it('should update tube when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildTube({ + id: 'tubeIdA', + name: 'name Tube A', + title: 'title Tube A', + description: 'description Tube A', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube A', en: 'practicalTitle EN Tube A' }, + practicalDescription_i18n: { fr: 'practicalDescription FR Tube A', en: 'practicalDescription EN Tube A' }, + competenceId: 'competenceId Tube A', + thematicId: 'thematicId Tube A', + skillIds: ['skillId Tube A'], + isMobileCompliant: true, + isTabletCompliant: true, + }); + await databaseBuilder.commit(); + const tubeDto = { + id: 'tubeIdA', + name: 'name Tube A modified', + title: 'title Tube A modified', + description: 'description Tube A modified', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube A modified', nl: 'practicalTitle NL Tube A modified' }, + practicalDescription_i18n: { + fr: 'practicalDescription FR Tube A modified', + en: 'practicalDescription EN Tube A modified', + }, + competenceId: 'competenceId Tube A modified', + thematicId: 'thematicId Tube A modified', + skillIds: ['skillId1 Tube A', 'skillId2 Tube A'], + isMobileCompliant: false, + isTabletCompliant: false, + }; + + // when + await tubeRepository.save(tubeDto); + + // then + const savedTube = await knex.select('*').from('learningcontent.tubes').where({ id: tubeDto.id }).first(); + const [{ count }] = await knex('learningcontent.tubes').count(); + expect(count).to.equal(2); + expect(savedTube).to.deep.equal({ + id: 'tubeIdA', + name: 'name Tube A modified', + title: 'title Tube A modified', + description: 'description Tube A modified', + practicalTitle_i18n: { fr: 'practicalTitle FR Tube A modified', nl: 'practicalTitle NL Tube A modified' }, + practicalDescription_i18n: { + fr: 'practicalDescription FR Tube A modified', + en: 'practicalDescription EN Tube A modified', + }, + competenceId: 'competenceId Tube A modified', + thematicId: 'thematicId Tube A modified', + skillIds: ['skillId1 Tube A', 'skillId2 Tube A'], + isMobileCompliant: false, + isTabletCompliant: false, + }); + }); + }); }); diff --git a/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js b/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js index 23dc39a1526..af9511a6287 100644 --- a/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js +++ b/api/tests/learning-content/integration/infrastructure/repositories/tutorial-repository_test.js @@ -158,4 +158,89 @@ describe('Learning Content | Integration | Repositories | Tutorial', function () }); }); }); + + describe('#save', function () { + beforeEach(async function () { + databaseBuilder.factory.learningContent.buildTutorial({ id: 'tutorialIdB' }); + await databaseBuilder.commit(); + }); + + it('should insert tutorial when it does not exist in DB', async function () { + // given + const tutorialDto = { + id: 'tutorialIdA', + duration: 'duration Tutoriel A', + format: 'format Tutoriel A', + title: 'title Tutoriel A', + source: 'source Tutoriel A', + link: 'link Tutoriel A', + locale: 'fr', + }; + + // when + await tutorialRepository.save(tutorialDto); + + // then + const savedTutorial = await knex + .select('*') + .from('learningcontent.tutorials') + .where({ id: tutorialDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.tutorials').count(); + expect(count).to.equal(2); + expect(savedTutorial).to.deep.equal({ + id: 'tutorialIdA', + duration: 'duration Tutoriel A', + format: 'format Tutoriel A', + title: 'title Tutoriel A', + source: 'source Tutoriel A', + link: 'link Tutoriel A', + locale: 'fr', + }); + }); + + it('should update tutorial when it does exist in DB', async function () { + // given + databaseBuilder.factory.learningContent.buildTutorial({ + id: 'tutorialIdA', + duration: 'duration Tutoriel A', + format: 'format Tutoriel A', + title: 'title Tutoriel A', + source: 'source Tutoriel A', + link: 'link Tutoriel A', + locale: 'fr', + }); + await databaseBuilder.commit(); + const tutorialDto = { + id: 'tutorialIdA', + duration: 'duration Tutoriel A modified', + format: 'format Tutoriel A modified', + title: 'title Tutoriel A modified', + source: 'source Tutoriel A modified', + link: 'link Tutoriel A modified', + locale: 'es', + }; + + // when + await tutorialRepository.save(tutorialDto); + + // then + const savedTutorial = await knex + .select('*') + .from('learningcontent.tutorials') + .where({ id: tutorialDto.id }) + .first(); + const [{ count }] = await knex('learningcontent.tutorials').count(); + expect(count).to.equal(2); + expect(savedTutorial).to.deep.equal({ + id: 'tutorialIdA', + duration: 'duration Tutoriel A modified', + format: 'format Tutoriel A modified', + title: 'title Tutoriel A modified', + source: 'source Tutoriel A modified', + link: 'link Tutoriel A modified', + locale: 'es', + }); + }); + }); }); diff --git a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js index 81d7304e67a..311c0012d31 100644 --- a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js +++ b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js @@ -17,45 +17,45 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca beforeEach(function () { frameworkRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - frameworkRepository.saveMany.rejects('I should not be called'); + frameworkRepository.save.rejects('I should not be called'); areaRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - areaRepository.saveMany.rejects('I should not be called'); + areaRepository.save.rejects('I should not be called'); competenceRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - competenceRepository.saveMany.rejects('I should not be called'); + competenceRepository.save.rejects('I should not be called'); thematicRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - thematicRepository.saveMany.rejects('I should not be called'); + thematicRepository.save.rejects('I should not be called'); tubeRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - tubeRepository.saveMany.rejects('I should not be called'); + tubeRepository.save.rejects('I should not be called'); skillRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - skillRepository.saveMany.rejects('I should not be called'); + skillRepository.save.rejects('I should not be called'); challengeRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - challengeRepository.saveMany.rejects('I should not be called'); + challengeRepository.save.rejects('I should not be called'); courseRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - courseRepository.saveMany.rejects('I should not be called'); + courseRepository.save.rejects('I should not be called'); tutorialRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - tutorialRepository.saveMany.rejects('I should not be called'); + tutorialRepository.save.rejects('I should not be called'); missionRepository = { - saveMany: sinon.stub(), + save: sinon.stub(), }; - missionRepository.saveMany.rejects('I should not be called'); + missionRepository.save.rejects('I should not be called'); repositories = { frameworkRepository, areaRepository, @@ -173,7 +173,7 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca 'tutorials', 'missions', ].forEach((modelName) => { - it(`should call saveMany on appropriate repository for model ${modelName}`, async function () { + it(`should call save on appropriate repository for model ${modelName}`, async function () { // given const recordId = 'recId'; const updatedRecord = Symbol('updated record'); @@ -190,7 +190,7 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca patch: sinon.stub().resolves(), }, }; - repositoriesByModel[modelName].saveMany.withArgs([updatedRecord]).resolves(); + repositoriesByModel[modelName].save.withArgs(updatedRecord).resolves(); // when await patchLearningContentCacheEntry({ @@ -202,8 +202,8 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca }); // then - expect(repositoriesByModel[modelName].saveMany).to.have.been.calledOnce; - expect(repositoriesByModel[modelName].saveMany).to.have.been.calledWithExactly([updatedRecord]); + expect(repositoriesByModel[modelName].save).to.have.been.calledOnce; + expect(repositoriesByModel[modelName].save).to.have.been.calledWithExactly(updatedRecord); }); }); }); From 6ef42ac118e25939ab40983c620f4af6741b5a33 Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:54:38 +0100 Subject: [PATCH 20/24] refactor(api): replaces old learning content cache by new one --- api/scripts/refresh-cache.js | 2 +- .../create-learning-content-release.js | 4 +- .../domain/usecases/dependencies.js | 4 +- .../patch-learning-content-cache-entry.js | 71 ++------------ .../refresh-learning-content-cache.js | 4 +- .../usecases/init-learning-content-cache.js | 7 +- .../caches/learning-content-cache.js | 85 +++++++---------- .../learning-content-repository.js | 55 +---------- .../application/lcms-controller_test.js | 24 +---- .../create-learning-content-release_test.js | 32 +++---- ...patch-learning-content-cache-entry_test.js | 93 ------------------- .../refresh-learning-content-cache_test.js | 32 +++---- .../caches/learning-content-cache_test.js | 56 ----------- .../init-learning-content-cache_test.js | 21 ----- .../caches/learning-content-cache_test.js | 65 +++++++------ api/tests/test-helper.js | 2 - 16 files changed, 129 insertions(+), 428 deletions(-) delete mode 100644 api/tests/shared/integration/infrastructure/caches/learning-content-cache_test.js delete mode 100644 api/tests/shared/unit/domain/usecases/init-learning-content-cache_test.js diff --git a/api/scripts/refresh-cache.js b/api/scripts/refresh-cache.js index a710ddf2ee0..46c29d4801b 100755 --- a/api/scripts/refresh-cache.js +++ b/api/scripts/refresh-cache.js @@ -11,7 +11,7 @@ try { await usecases.refreshLearningContentCache(); logger.info('Learning Content refreshed'); } catch (e) { - logger.error('Error while reloading cache', e); + logger.error(e, 'Error while reloading cache'); } finally { await learningContentCache.quit(); await disconnect(); diff --git a/api/src/learning-content/domain/usecases/create-learning-content-release.js b/api/src/learning-content/domain/usecases/create-learning-content-release.js index 50ae4bde65a..8b99d87c474 100644 --- a/api/src/learning-content/domain/usecases/create-learning-content-release.js +++ b/api/src/learning-content/domain/usecases/create-learning-content-release.js @@ -3,7 +3,7 @@ import { withTransaction } from '../../../shared/domain/DomainTransaction.js'; export const createLearningContentRelease = withTransaction( /** @param {import('./dependencies.js').Dependencies} */ async function createLearningContentRelease({ - LearningContentCache, + lcmsClient, frameworkRepository, areaRepository, competenceRepository, @@ -15,7 +15,7 @@ export const createLearningContentRelease = withTransaction( tutorialRepository, missionRepository, }) { - const learningContent = await LearningContentCache.instance.update(); + const learningContent = await lcmsClient.createRelease(); await frameworkRepository.saveMany(learningContent.frameworks); await areaRepository.saveMany(learningContent.areas); diff --git a/api/src/learning-content/domain/usecases/dependencies.js b/api/src/learning-content/domain/usecases/dependencies.js index 79c7419f4a9..35ead947d2f 100644 --- a/api/src/learning-content/domain/usecases/dependencies.js +++ b/api/src/learning-content/domain/usecases/dependencies.js @@ -1,4 +1,4 @@ -import { LearningContentCache } from '../../../shared/infrastructure/caches/learning-content-cache.js'; +import { lcmsClient } from '../../../shared/infrastructure/lcms-client.js'; import { areaRepository } from '../../infrastructure/repositories/area-repository.js'; import { challengeRepository } from '../../infrastructure/repositories/challenge-repository.js'; import { competenceRepository } from '../../infrastructure/repositories/competence-repository.js'; @@ -25,7 +25,7 @@ export const dependencies = { missionRepository, lcmsRefreshCacheJobRepository, lcmsCreateReleaseJobRepository, - LearningContentCache, + lcmsClient, }; /** @typedef {typeof dependencies} Dependencies */ diff --git a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js index cc4073ab9a0..2056d62cb6b 100644 --- a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js +++ b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js @@ -1,9 +1,7 @@ /** @param {import('./dependencies.js').Dependencies} */ export async function patchLearningContentCacheEntry({ - recordId, updatedRecord, modelName, - LearningContentCache, frameworkRepository, areaRepository, competenceRepository, @@ -15,83 +13,34 @@ export async function patchLearningContentCacheEntry({ tutorialRepository, missionRepository, }) { - const currentLearningContent = await LearningContentCache.instance.get(); - const patch = generatePatch(currentLearningContent, recordId, updatedRecord, modelName); - await LearningContentCache.instance.patch(patch); - await patchDatabase( - modelName, - updatedRecord, - frameworkRepository, - areaRepository, - competenceRepository, - thematicRepository, - tubeRepository, - skillRepository, - challengeRepository, - courseRepository, - tutorialRepository, - missionRepository, - ); -} - -function generatePatch(currentLearningContent, id, newEntry, modelName) { - const index = currentLearningContent[modelName].findIndex((element) => element?.id === id); - if (index === -1) { - return { - operation: 'push', - path: modelName, - value: newEntry, - }; - } - return { - operation: 'assign', - path: `${modelName}[${index}]`, - value: newEntry, - }; -} - -async function patchDatabase( - modelName, - patchedRecord, - frameworkRepository, - areaRepository, - competenceRepository, - thematicRepository, - tubeRepository, - skillRepository, - challengeRepository, - courseRepository, - tutorialRepository, - missionRepository, -) { if (modelName === 'frameworks') { - await frameworkRepository.save(patchedRecord); + await frameworkRepository.save(updatedRecord); } if (modelName === 'areas') { - await areaRepository.save(patchedRecord); + await areaRepository.save(updatedRecord); } if (modelName === 'competences') { - await competenceRepository.save(patchedRecord); + await competenceRepository.save(updatedRecord); } if (modelName === 'thematics') { - await thematicRepository.save(patchedRecord); + await thematicRepository.save(updatedRecord); } if (modelName === 'tubes') { - await tubeRepository.save(patchedRecord); + await tubeRepository.save(updatedRecord); } if (modelName === 'skills') { - await skillRepository.save(patchedRecord); + await skillRepository.save(updatedRecord); } if (modelName === 'challenges') { - await challengeRepository.save(patchedRecord); + await challengeRepository.save(updatedRecord); } if (modelName === 'courses') { - await courseRepository.save(patchedRecord); + await courseRepository.save(updatedRecord); } if (modelName === 'tutorials') { - await tutorialRepository.save(patchedRecord); + await tutorialRepository.save(updatedRecord); } if (modelName === 'missions') { - await missionRepository.save(patchedRecord); + await missionRepository.save(updatedRecord); } } diff --git a/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js b/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js index 02e0d7bb173..c4e2417bcf5 100644 --- a/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js +++ b/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js @@ -3,7 +3,7 @@ import { withTransaction } from '../../../shared/domain/DomainTransaction.js'; export const refreshLearningContentCache = withTransaction( /** @param {import('./dependencies.js').Dependencies} */ async function refreshLearningContentCache({ - LearningContentCache, + lcmsClient, frameworkRepository, areaRepository, competenceRepository, @@ -15,7 +15,7 @@ export const refreshLearningContentCache = withTransaction( tutorialRepository, missionRepository, }) { - const learningContent = await LearningContentCache.instance.reset(); + const learningContent = await lcmsClient.getLatestRelease(); await frameworkRepository.saveMany(learningContent.frameworks); await areaRepository.saveMany(learningContent.areas); diff --git a/api/src/shared/domain/usecases/init-learning-content-cache.js b/api/src/shared/domain/usecases/init-learning-content-cache.js index 70cb8e70683..cb82c5c9db8 100644 --- a/api/src/shared/domain/usecases/init-learning-content-cache.js +++ b/api/src/shared/domain/usecases/init-learning-content-cache.js @@ -1,4 +1,3 @@ -const initLearningContentCache = async function ({ LearningContentCache }) { - await LearningContentCache.instance.get(); -}; -export { initLearningContentCache }; +export async function initLearningContentCache() { + // FIXME +} diff --git a/api/src/shared/infrastructure/caches/learning-content-cache.js b/api/src/shared/infrastructure/caches/learning-content-cache.js index 139865ab81c..cb57d595c4d 100644 --- a/api/src/shared/infrastructure/caches/learning-content-cache.js +++ b/api/src/shared/infrastructure/caches/learning-content-cache.js @@ -1,69 +1,52 @@ -import { config } from '../../config.js'; -import { lcmsClient } from '../lcms-client.js'; -import { DistributedCache } from './DistributedCache.js'; -import { InMemoryCache } from './InMemoryCache.js'; -import { LayeredCache } from './LayeredCache.js'; -import { RedisCache } from './RedisCache.js'; - -const LEARNING_CONTENT_CHANNEL = 'Learning content'; -const LEARNING_CONTENT_CACHE_KEY = 'LearningContent'; +import * as learningContentPubSub from '../caches/learning-content-pubsub.js'; export class LearningContentCache { - constructor(redisUrl) { - if (redisUrl) { - const distributedCache = new DistributedCache(new InMemoryCache(), redisUrl, LEARNING_CONTENT_CHANNEL); - const redisCache = new RedisCache(redisUrl); - - this._underlyingCache = new LayeredCache(distributedCache, redisCache); - } else { - this._underlyingCache = new InMemoryCache(); - } - this.generator = () => lcmsClient.getLatestRelease(); - } - - get() { - return this._underlyingCache.get(LEARNING_CONTENT_CACHE_KEY, this.generator); - } + #map; + #pubSub; + #name; - async reset() { - const object = await this.generator(); - return this._underlyingCache.set(LEARNING_CONTENT_CACHE_KEY, object); - } + /** + * @param {{ + * name: string + * pubSub: import('../caches/learning-content-pubsub.js').LearningContentPubSub + * map: Map + * }} config + * @returns + */ + constructor({ name, pubSub = learningContentPubSub.getPubSub(), map = new Map() }) { + this.#name = name; + this.#pubSub = pubSub; + this.#map = map; - async update() { - const newLearningContent = await lcmsClient.createRelease(); - return this._underlyingCache.set(LEARNING_CONTENT_CACHE_KEY, newLearningContent); + this.#subscribe(); } - patch(patch) { - return this._underlyingCache.patch(LEARNING_CONTENT_CACHE_KEY, patch); + get(key) { + return this.#map.get(key); } - flushAll() { - return this._underlyingCache.flushAll(); + set(key, value) { + return this.#map.set(key, value); } - quit() { - return this._underlyingCache.quit(); + delete(key) { + return this.#pubSub.publish(this.#name, { type: 'delete', key }); } - /** @type {LearningContentCache} */ - static _instance = null; - - static defaultInstance() { - return new LearningContentCache(config.caching.redisUrl); + clear() { + return this.#pubSub.publish(this.#name, { type: 'clear' }); } - static get instance() { - if (!this._instance) { - this._instance = this.defaultInstance(); + async #subscribe() { + for await (const message of this.#pubSub.subscribe(this.#name)) { + if (message.type === 'clear') this.#map.clear(); + if (message.type === 'delete') this.#map.delete(message.key); } - return this._instance; - } - - static set instance(_instance) { - this._instance = _instance; } } -export const learningContentCache = LearningContentCache.instance; +export const learningContentCache = { + async quit() { + return learningContentPubSub.quit(); + }, +}; diff --git a/api/src/shared/infrastructure/repositories/learning-content-repository.js b/api/src/shared/infrastructure/repositories/learning-content-repository.js index dfeebc997bf..650b50ee6d0 100644 --- a/api/src/shared/infrastructure/repositories/learning-content-repository.js +++ b/api/src/shared/infrastructure/repositories/learning-content-repository.js @@ -1,7 +1,7 @@ import Dataloader from 'dataloader'; import { knex } from '../../../../db/knex-database-connection.js'; -import * as learningContentPubSub from '../caches/learning-content-pubsub.js'; +import { LearningContentCache } from '../caches/learning-content-cache.js'; export class LearningContentRepository { #tableName; @@ -10,21 +10,15 @@ export class LearningContentRepository { #findCache; #findCacheMiss; - constructor({ tableName, idType = 'text', pubSub = learningContentPubSub.getPubSub() }) { + constructor({ tableName, idType = 'text' }) { this.#tableName = tableName; this.#idType = idType; this.#dataloader = new Dataloader((ids) => this.#batchLoad(ids), { - cacheMap: new LearningContentCache({ - name: `${tableName}:entities`, - pubSub, - }), + cacheMap: new LearningContentCache({ name: `${tableName}:entities` }), }); - this.#findCache = new LearningContentCache({ - name: `${tableName}:results`, - pubSub, - }); + this.#findCache = new LearningContentCache({ name: `${tableName}:results` }); this.#findCacheMiss = new Map(); } @@ -81,44 +75,3 @@ export class LearningContentRepository { this.#findCache.clear(); } } - -class LearningContentCache { - #map = new Map(); - #pubSub; - #name; - - /** - * @param {{ - * pubSub: import('../caches/learning-content-pubsub.js').LearningContentPubSub - * name: string - * }} config - * @returns - */ - constructor({ pubSub, name }) { - this.#pubSub = pubSub; - this.#name = name; - - (async () => { - for await (const message of pubSub.subscribe(name)) { - if (message.type === 'clear') this.#map.clear(); - if (message.type === 'delete') this.#map.delete(this.message.key); - } - })(); - } - - get(key) { - return this.#map.get(key); - } - - set(key, value) { - return this.#map.set(key, value); - } - - delete(key) { - return this.#pubSub.publish(this.#name, { type: 'delete', key }); - } - - clear() { - return this.#pubSub.publish(this.#name, { type: 'clear' }); - } -} diff --git a/api/tests/learning-content/acceptance/application/lcms-controller_test.js b/api/tests/learning-content/acceptance/application/lcms-controller_test.js index 7ed40f07489..fd52249ae98 100644 --- a/api/tests/learning-content/acceptance/application/lcms-controller_test.js +++ b/api/tests/learning-content/acceptance/application/lcms-controller_test.js @@ -1,7 +1,4 @@ -import Redis from 'ioredis'; - import { PIX_ADMIN } from '../../../../src/authorization/domain/constants.js'; -import { LearningContentCache } from '../../../../src/shared/infrastructure/caches/learning-content-cache.js'; import { createServer, databaseBuilder, @@ -59,16 +56,7 @@ describe('Acceptance | Controller | lcms-controller', function () { }); describe('nominal case', function () { - beforeEach(function () { - LearningContentCache.instance = new LearningContentCache(process.env.TEST_REDIS_URL); - }); - - afterEach(async function () { - await LearningContentCache.instance._underlyingCache.flushAll(); - LearningContentCache.instance = null; - }); - - it('should store patches in Redis and patch the DB for an assign operation', async function () { + it('should patch the DB for an assign operation', async function () { // given await mockLearningContent({ frameworks: [ @@ -95,10 +83,6 @@ describe('Acceptance | Controller | lcms-controller', function () { // then expect(response.statusCode).to.equal(204); - const redis = new Redis(process.env.TEST_REDIS_URL); - expect(await redis.lrange('cache:LearningContent:patches', 0, -1)).to.deep.equal([ - JSON.stringify({ operation: 'assign', path: `frameworks[0]`, value: payload }), - ]); const frameworksInDB = await knex.select('*').from('learningcontent.frameworks').orderBy('name'); expect(frameworksInDB).to.deep.equal([ { id: 'frameworkId', name: 'new name' }, @@ -106,7 +90,7 @@ describe('Acceptance | Controller | lcms-controller', function () { ]); }); - it('should store patches in Redis and patch the DB for a push operation', async function () { + it('should patch the DB for a push operation', async function () { // given await mockLearningContent({ frameworks: [ @@ -133,10 +117,6 @@ describe('Acceptance | Controller | lcms-controller', function () { // then expect(response.statusCode).to.equal(204); - const redis = new Redis(process.env.TEST_REDIS_URL); - expect(await redis.lrange('cache:LearningContent:patches', 0, -1)).to.deep.equal([ - JSON.stringify({ operation: 'push', path: `frameworks`, value: payload }), - ]); const frameworksInDB = await knex.select('*').from('learningcontent.frameworks').orderBy('name'); expect(frameworksInDB).to.deep.equal([ { id: 'frameworkId1', name: 'name 1' }, diff --git a/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js b/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js index 94bd4f18912..28a9ad499dc 100644 --- a/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js +++ b/api/tests/learning-content/unit/domain/usecases/create-learning-content-release_test.js @@ -23,21 +23,19 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release', const tutorials = Symbol('tutorials'); const missions = Symbol('missions'); - const LearningContentCache = { - instance: { - update: sinon.stub().resolves({ - frameworks, - areas, - competences, - thematics, - tubes, - skills, - challenges, - courses, - tutorials, - missions, - }), - }, + const lcmsClient = { + createRelease: sinon.stub().resolves({ + frameworks, + areas, + competences, + thematics, + tubes, + skills, + challenges, + courses, + tutorials, + missions, + }), }; const frameworkRepository = { @@ -73,7 +71,7 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release', // when await createLearningContentRelease({ - LearningContentCache, + lcmsClient, frameworkRepository, areaRepository, competenceRepository, @@ -87,7 +85,7 @@ describe('Learning Content | Unit | UseCase | create-learning-content-release', }); // then - expect(LearningContentCache.instance.update).to.have.been.calledOnce; + expect(lcmsClient.createRelease).to.have.been.calledOnce; expect(frameworkRepository.saveMany).to.have.been.calledOnceWithExactly(frameworks); expect(areaRepository.saveMany).to.have.been.calledOnceWithExactly(areas); expect(competenceRepository.saveMany).to.have.been.calledOnceWithExactly(competences); diff --git a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js index 311c0012d31..2a82d0e9cc8 100644 --- a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js +++ b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js @@ -83,83 +83,6 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca }); describe('#patchLearningContentCacheEntry', function () { - context('when entry is already in cache', function () { - it('should patch learning content cache with provided updated entry', async function () { - // given - const recordId = 'recId'; - const updatedRecord = Symbol('updated record'); - const modelName = 'someModelName'; - const LearningContentCache = { - instance: { - get: sinon.stub(), - patch: sinon.stub(), - }, - }; - const learningContent = { - someModelName: [ - { attr1: 'attr1 value index 0', id: 'otherRecordId' }, - { attr1: 'attr1 value index 1', id: recordId }, - ], - someOtherModelName: [{ other: 'entry', id: recordId }], - }; - LearningContentCache.instance.get.resolves(learningContent); - - // when - await patchLearningContentCacheEntry({ - recordId, - updatedRecord, - modelName, - LearningContentCache, - ...repositories, - }); - - // then - expect(LearningContentCache.instance.patch).to.have.been.calledWithExactly({ - operation: 'assign', - path: 'someModelName[1]', - value: updatedRecord, - }); - }); - }); - context('when entry is not in cache', function () { - it('should patch learning content cache by adding provided entry', async function () { - // given - const recordId = 'recId'; - const updatedRecord = Symbol('updated record'); - const modelName = 'someModelName'; - const LearningContentCache = { - instance: { - get: sinon.stub(), - patch: sinon.stub(), - }, - }; - const learningContent = { - someModelName: [ - { attr1: 'attr1 value index 0', id: 'otherRecordId' }, - { attr1: 'attr1 value index 1', id: 'yetAnotherRecordId' }, - ], - someOtherModelName: [{ other: 'entry', id: recordId }], - }; - LearningContentCache.instance.get.resolves(learningContent); - - // when - await patchLearningContentCacheEntry({ - recordId, - updatedRecord, - modelName, - LearningContentCache, - ...repositories, - }); - - // then - expect(LearningContentCache.instance.patch).to.have.been.calledWithExactly({ - operation: 'push', - path: 'someModelName', - value: updatedRecord, - }); - }); - }); - // eslint-disable-next-line mocha/no-setup-in-describe [ 'frameworks', @@ -175,29 +98,13 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca ].forEach((modelName) => { it(`should call save on appropriate repository for model ${modelName}`, async function () { // given - const recordId = 'recId'; const updatedRecord = Symbol('updated record'); - const learningContent = { - [modelName]: [ - { attr1: 'attr1 value index 0', id: 'otherRecordId' }, - { attr1: 'attr1 value index 1', id: recordId }, - ], - someOtherModelName: [{ other: 'entry', id: recordId }], - }; - const LearningContentCache = { - instance: { - get: sinon.stub().resolves(learningContent), - patch: sinon.stub().resolves(), - }, - }; repositoriesByModel[modelName].save.withArgs(updatedRecord).resolves(); // when await patchLearningContentCacheEntry({ - recordId, updatedRecord, modelName, - LearningContentCache, ...repositories, }); diff --git a/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js b/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js index 6f05f906a7e..6e3c9ff48e0 100644 --- a/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js +++ b/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js @@ -23,21 +23,19 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content const tutorials = Symbol('tutorials'); const missions = Symbol('missions'); - const LearningContentCache = { - instance: { - reset: sinon.stub().resolves({ - frameworks, - areas, - competences, - thematics, - tubes, - skills, - challenges, - courses, - tutorials, - missions, - }), - }, + const lcmsClient = { + getLatestRelease: sinon.stub().resolves({ + frameworks, + areas, + competences, + thematics, + tubes, + skills, + challenges, + courses, + tutorials, + missions, + }), }; const frameworkRepository = { @@ -73,7 +71,7 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content // when await refreshLearningContentCache({ - LearningContentCache, + lcmsClient, frameworkRepository, areaRepository, competenceRepository, @@ -87,7 +85,7 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content }); // then - expect(LearningContentCache.instance.reset).to.have.been.calledOnce; + expect(lcmsClient.getLatestRelease).to.have.been.calledOnce; expect(frameworkRepository.saveMany).to.have.been.calledOnceWithExactly(frameworks); expect(areaRepository.saveMany).to.have.been.calledOnceWithExactly(areas); expect(competenceRepository.saveMany).to.have.been.calledOnceWithExactly(competences); diff --git a/api/tests/shared/integration/infrastructure/caches/learning-content-cache_test.js b/api/tests/shared/integration/infrastructure/caches/learning-content-cache_test.js deleted file mode 100644 index ad68943366d..00000000000 --- a/api/tests/shared/integration/infrastructure/caches/learning-content-cache_test.js +++ /dev/null @@ -1,56 +0,0 @@ -import nock from 'nock'; - -import { learningContentCache } from '../../../../../src/shared/infrastructure/caches/learning-content-cache.js'; -import { expect, mockLearningContent, sinon } from '../../../../test-helper.js'; - -describe('Integration | Infrastructure | Caches | LearningContentCache', function () { - describe('#get', function () { - it('should get learning content from underlying cache (redis not used in test)', async function () { - // given - const learningContent = { models: [{ id: 'recId' }] }; - const lcmsApiCall = await mockLearningContent(learningContent); - - // when - const result = await learningContentCache.get(); - - // then - expect(result).to.deep.equal(learningContent); - expect(lcmsApiCall.isDone()).to.be.true; - }); - }); - - describe('#reset', function () { - it('should set learning content in underlying cache', async function () { - // given - const learningContent = { models: [{ id: 'recId' }] }; - const lcmsApiCall = await mockLearningContent(learningContent); - const underlyingCacheSpy = sinon.spy(learningContentCache._underlyingCache, 'set'); - - // when - await learningContentCache.reset(); - - // then - expect(underlyingCacheSpy).to.have.been.calledWith('LearningContent', learningContent); - expect(lcmsApiCall.isDone()).to.be.true; - }); - }); - - describe('#update', function () { - it('should update cache with new learning content retrieved from lcms client', async function () { - // given - const learningContent = { models: [{ id: 'recId' }] }; - const lcmsApiCall = nock('https://lcms-test.pix.fr/api') - .post('/releases') - .matchHeader('Authorization', 'Bearer test-api-key') - .reply(200, { content: learningContent }); - const underlyingCacheSpy = sinon.spy(learningContentCache._underlyingCache, 'set'); - - // when - await learningContentCache.update(); - - // then - expect(underlyingCacheSpy).to.have.been.calledWith('LearningContent', learningContent); - expect(lcmsApiCall.isDone()).to.be.true; - }); - }); -}); diff --git a/api/tests/shared/unit/domain/usecases/init-learning-content-cache_test.js b/api/tests/shared/unit/domain/usecases/init-learning-content-cache_test.js deleted file mode 100644 index 0b354d108e2..00000000000 --- a/api/tests/shared/unit/domain/usecases/init-learning-content-cache_test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { initLearningContentCache } from '../../../../../src/shared/domain/usecases/init-learning-content-cache.js'; -import { expect, sinon } from '../../../../test-helper.js'; - -describe('Unit | Domain | Usecase | Init learning content cache', function () { - describe('#initLearningContentCache', function () { - it('should init learning content cache ', async function () { - // given - const LearningContentCache = { - instance: { - get: sinon.stub(), - }, - }; - - // when - await initLearningContentCache({ LearningContentCache }); - - // then - expect(LearningContentCache.instance.get).to.have.been.calledOnce; - }); - }); -}); diff --git a/api/tests/shared/unit/infrastructure/caches/learning-content-cache_test.js b/api/tests/shared/unit/infrastructure/caches/learning-content-cache_test.js index 4084709c5b5..6f1e7838900 100644 --- a/api/tests/shared/unit/infrastructure/caches/learning-content-cache_test.js +++ b/api/tests/shared/unit/infrastructure/caches/learning-content-cache_test.js @@ -1,62 +1,75 @@ -import { learningContentCache } from '../../../../../src/shared/infrastructure/caches/learning-content-cache.js'; +import { LearningContentCache } from '../../../../../src/shared/infrastructure/caches/learning-content-cache.js'; import { expect, sinon } from '../../../../test-helper.js'; describe('Unit | Infrastructure | Caches | LearningContentCache', function () { - let originalUnderlyingCache; + let pubSub, map, learningContentCache; beforeEach(function () { - originalUnderlyingCache = learningContentCache._underlyingCache; + pubSub = { + subscribe: sinon.stub(), + publish: sinon.stub(), + }; - learningContentCache._underlyingCache = { + map = { get: sinon.stub(), set: sinon.stub(), - patch: sinon.stub(), - flushAll: sinon.stub(), - quit: sinon.stub(), + delete: sinon.stub(), + clear: sinon.stub(), }; - }); - afterEach(function () { - learningContentCache._underlyingCache = originalUnderlyingCache; + learningContentCache = new LearningContentCache({ name: 'test', pubSub, map }); }); - describe('#patch', function () { - it('should patch the learning content in underlying cache', async function () { + describe('#get', function () { + it('should call map.get() and return its value', async function () { // given - learningContentCache._underlyingCache.patch.resolves(); - const patch = { operation: 'assign', path: 'a', value: {} }; + const key = Symbol('key'); + const value = Symbol('value'); + map.get.withArgs(key).returns(value); // when - await learningContentCache.patch(patch); + const result = learningContentCache.get(key); // then - expect(learningContentCache._underlyingCache.patch).to.have.been.calledWith('LearningContent', patch); + expect(result).to.equal(value); + expect(map.get).to.have.been.calledOnceWithExactly(key); }); }); - describe('#flushAll', function () { - it('should flush all the underlying cache', async function () { + describe('#set', function () { + it('should call map.set()', async function () { // given - learningContentCache._underlyingCache.flushAll.resolves(); + const key = Symbol('key'); + const value = Symbol('value'); // when - await learningContentCache.flushAll(); + learningContentCache.set(key, value); // then - expect(learningContentCache._underlyingCache.flushAll).to.have.been.calledWith(); + expect(map.set).to.have.been.calledOnceWithExactly(key, value); }); }); - describe('#quit', function () { - it('should quit the underlying cache', async function () { + describe('#delete', function () { + it('should publish delete event on pubSub', async function () { // given - learningContentCache._underlyingCache.quit.resolves(); + const key = Symbol('key'); + + // when + learningContentCache.delete(key); + + // then + expect(pubSub.publish).to.have.been.calledOnceWithExactly('test', { type: 'delete', key }); + }); + }); + describe('#clear', function () { + it('should publish clear event on pubSub', async function () { // when - await learningContentCache.quit(); + learningContentCache.clear(); // then - expect(learningContentCache._underlyingCache.quit).to.have.been.calledWith(); + expect(pubSub.publish).to.have.been.calledOnceWithExactly('test', { type: 'clear' }); }); }); }); diff --git a/api/tests/test-helper.js b/api/tests/test-helper.js index 697bcdeb169..33f5268a9fb 100644 --- a/api/tests/test-helper.js +++ b/api/tests/test-helper.js @@ -27,7 +27,6 @@ import * as missionRepository from '../src/school/infrastructure/repositories/mi import { config } from '../src/shared/config.js'; import { Membership } from '../src/shared/domain/models/index.js'; import * as tokenService from '../src/shared/domain/services/token-service.js'; -import { LearningContentCache } from '../src/shared/infrastructure/caches/learning-content-cache.js'; import * as areaRepository from '../src/shared/infrastructure/repositories/area-repository.js'; import * as challengeRepository from '../src/shared/infrastructure/repositories/challenge-repository.js'; import * as competenceRepository from '../src/shared/infrastructure/repositories/competence-repository.js'; @@ -79,7 +78,6 @@ const { ROLES } = PIX_ADMIN; afterEach(function () { restore(); - LearningContentCache.instance.flushAll(); nock.cleanAll(); frameworkRepository.clearCache(); areaRepository.clearCache(); From aa5b9d27e6b26f51cb6d7b84f45414ff3d244bc6 Mon Sep 17 00:00:00 2001 From: Laura Bergoens Date: Mon, 2 Dec 2024 17:11:10 +0100 Subject: [PATCH 21/24] make seeds work --- .../database-builder/factory/learning-content/build-area.js | 4 ++-- .../factory/learning-content/build-challenge.js | 4 ++-- .../factory/learning-content/build-competence.js | 6 +++--- .../factory/learning-content/build-course.js | 4 ++-- .../factory/learning-content/build-mission.js | 2 +- .../factory/learning-content/build-skill.js | 6 +++--- .../factory/learning-content/build-thematic.js | 4 ++-- .../database-builder/factory/learning-content/build-tube.js | 6 +++--- .../infrastructure/repositories/challenge-repository.js | 5 ++++- .../repositories/learning-content-repository.js | 4 +++- 10 files changed, 25 insertions(+), 20 deletions(-) diff --git a/api/db/database-builder/factory/learning-content/build-area.js b/api/db/database-builder/factory/learning-content/build-area.js index 80f4368e01b..f2aa4633505 100644 --- a/api/db/database-builder/factory/learning-content/build-area.js +++ b/api/db/database-builder/factory/learning-content/build-area.js @@ -6,8 +6,8 @@ export function buildArea({ name = 'name Domaine A', title_i18n = { fr: 'title FR Domaine A', en: 'title EN Domaine A' }, color = 'color Domaine A', - frameworkId = 'frameworkPix', - competenceIds = ['competenceIdA'], + frameworkId = null, + competenceIds = [], } = {}) { const values = { id, diff --git a/api/db/database-builder/factory/learning-content/build-challenge.js b/api/db/database-builder/factory/learning-content/build-challenge.js index 5629ae9b4a6..d41b6a3bc7c 100644 --- a/api/db/database-builder/factory/learning-content/build-challenge.js +++ b/api/db/database-builder/factory/learning-content/build-challenge.js @@ -36,8 +36,8 @@ export function buildChallenge({ embedUrl = 'embedUrl Epreuve A', embedTitle = 'embedTitle Epreuve A', locales = ['fr'], - competenceId = 'competenceIdA', - skillId = 'skillIdA', + competenceId = null, + skillId = null, } = {}) { const values = { id, diff --git a/api/db/database-builder/factory/learning-content/build-competence.js b/api/db/database-builder/factory/learning-content/build-competence.js index 02786e8f01a..b4f90d2b998 100644 --- a/api/db/database-builder/factory/learning-content/build-competence.js +++ b/api/db/database-builder/factory/learning-content/build-competence.js @@ -6,9 +6,9 @@ export function buildCompetence({ description_i18n = { fr: 'description FR Compétence A', en: 'description EN Compétence A' }, index = 'index Compétence A', origin = 'origin Compétence A', - areaId = 'areaIdA', - skillIds = ['skillIdA'], - thematicIds = ['thematicIdA'], + areaId = null, + skillIds = [], + thematicIds = [], } = {}) { const values = { id, diff --git a/api/db/database-builder/factory/learning-content/build-course.js b/api/db/database-builder/factory/learning-content/build-course.js index 482db13cea3..06087f44b47 100644 --- a/api/db/database-builder/factory/learning-content/build-course.js +++ b/api/db/database-builder/factory/learning-content/build-course.js @@ -5,8 +5,8 @@ export function buildCourse({ name = 'instruction Test Statique A', description = 'description Test Statique A', isActive = true, - competences = ['competenceIdA'], - challenges = ['challengeIdA'], + competences = [], + challenges = [], } = {}) { const values = { id, diff --git a/api/db/database-builder/factory/learning-content/build-mission.js b/api/db/database-builder/factory/learning-content/build-mission.js index 3f874bffbf9..5daa4bed01a 100644 --- a/api/db/database-builder/factory/learning-content/build-mission.js +++ b/api/db/database-builder/factory/learning-content/build-mission.js @@ -12,7 +12,7 @@ export function buildMission({ introductionMediaAlt_i18n = { fr: 'introductionMediaAlt FR Mission A', en: 'introductionMediaAlt EN Mission A' }, documentationUrl = 'documentationUrl Mission A', cardImageUrl = 'cardImageUrl Mission A', - competenceId = 'competenceIdA', + competenceId = null, } = {}) { const values = { id, diff --git a/api/db/database-builder/factory/learning-content/build-skill.js b/api/db/database-builder/factory/learning-content/build-skill.js index aaadd08b318..2ddb0e44b1b 100644 --- a/api/db/database-builder/factory/learning-content/build-skill.js +++ b/api/db/database-builder/factory/learning-content/build-skill.js @@ -8,9 +8,9 @@ export function buildSkill({ version = 5, level = 2, hintStatus = 'hintStatus Acquis A', - competenceId = 'competenceIdA', - tubeId = 'tubeIdA', - tutorialIds = ['tutorialIdA'], + competenceId = null, + tubeId = null, + tutorialIds = [], learningMoreTutorialIds = [], hint_i18n = { fr: 'Un indice' }, } = {}) { diff --git a/api/db/database-builder/factory/learning-content/build-thematic.js b/api/db/database-builder/factory/learning-content/build-thematic.js index 76dedfa65ee..a5e0b402d3b 100644 --- a/api/db/database-builder/factory/learning-content/build-thematic.js +++ b/api/db/database-builder/factory/learning-content/build-thematic.js @@ -4,8 +4,8 @@ export function buildThematic({ id = 'thematicIdA', name_i18n = { fr: 'name FR Thématique A', en: 'name EN Thématique A' }, index = 8, - competenceId = 'competenceIdA', - tubeIds = ['tubeIdA'], + competenceId = null, + tubeIds = [], } = {}) { const values = { id, diff --git a/api/db/database-builder/factory/learning-content/build-tube.js b/api/db/database-builder/factory/learning-content/build-tube.js index fd7db356dea..cfba31e1385 100644 --- a/api/db/database-builder/factory/learning-content/build-tube.js +++ b/api/db/database-builder/factory/learning-content/build-tube.js @@ -7,9 +7,9 @@ export function buildTube({ description = 'description Tube A', practicalTitle_i18n = { fr: 'practicalTitle FR Tube A', en: 'practicalTitle EN Tube A' }, practicalDescription_i18n = { fr: 'practicalDescription FR Tube A', en: 'practicalDescription EN Tube A' }, - competenceId = 'competenceIdA', - thematicId = 'thematicIdA', - skillIds = ['skillIdA'], + competenceId = null, + thematicId = null, + skillIds = [], isMobileCompliant = true, isTabletCompliant = true, } = {}) { diff --git a/api/src/shared/infrastructure/repositories/challenge-repository.js b/api/src/shared/infrastructure/repositories/challenge-repository.js index eec5422a990..fbe669e55a6 100644 --- a/api/src/shared/infrastructure/repositories/challenge-repository.js +++ b/api/src/shared/infrastructure/repositories/challenge-repository.js @@ -190,7 +190,10 @@ async function loadWebComponentInfo(challengeDto) { async function loadChallengeDtosSkills(challengeDtos) { return Promise.all( - challengeDtos.map(async (challengeDto) => [challengeDto, await skillRepository.get(challengeDto.skillId)]), + challengeDtos.map(async (challengeDto) => [ + challengeDto, + challengeDto.skillId ? await skillRepository.get(challengeDto.skillId) : null, + ]), ); } diff --git a/api/src/shared/infrastructure/repositories/learning-content-repository.js b/api/src/shared/infrastructure/repositories/learning-content-repository.js index 650b50ee6d0..18b3c4913e7 100644 --- a/api/src/shared/infrastructure/repositories/learning-content-repository.js +++ b/api/src/shared/infrastructure/repositories/learning-content-repository.js @@ -28,11 +28,13 @@ export class LearningContentRepository { } async load(id) { + if (!id) return null; return this.#dataloader.load(id); } async loadMany(ids) { - return this.#dataloader.loadMany(ids); + const notNullIds = ids.filter((id) => id); + return this.#dataloader.loadMany(notNullIds); } #findDtos(callback, cacheKey) { From 648ee72d477ebf28be40143edb326def893094af Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:22:32 +0100 Subject: [PATCH 22/24] refactor(api): clears learning content cache after writing to PG --- .../patch-learning-content-cache-entry.js | 10 ++++ .../refresh-learning-content-cache.js | 49 ++++++++++++------- .../learning-content-repository.js | 2 - ...patch-learning-content-cache-entry_test.js | 18 +++++-- .../refresh-learning-content-cache_test.js | 20 ++++++++ 5 files changed, 75 insertions(+), 24 deletions(-) diff --git a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js index 2056d62cb6b..48f458d4936 100644 --- a/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js +++ b/api/src/learning-content/domain/usecases/patch-learning-content-cache-entry.js @@ -15,32 +15,42 @@ export async function patchLearningContentCacheEntry({ }) { if (modelName === 'frameworks') { await frameworkRepository.save(updatedRecord); + frameworkRepository.clearCache(updatedRecord.id); } if (modelName === 'areas') { await areaRepository.save(updatedRecord); + areaRepository.clearCache(updatedRecord.id); } if (modelName === 'competences') { await competenceRepository.save(updatedRecord); + competenceRepository.clearCache(updatedRecord.id); } if (modelName === 'thematics') { await thematicRepository.save(updatedRecord); + thematicRepository.clearCache(updatedRecord.id); } if (modelName === 'tubes') { await tubeRepository.save(updatedRecord); + tubeRepository.clearCache(updatedRecord.id); } if (modelName === 'skills') { await skillRepository.save(updatedRecord); + skillRepository.clearCache(updatedRecord.id); } if (modelName === 'challenges') { await challengeRepository.save(updatedRecord); + challengeRepository.clearCache(updatedRecord.id); } if (modelName === 'courses') { await courseRepository.save(updatedRecord); + courseRepository.clearCache(updatedRecord.id); } if (modelName === 'tutorials') { await tutorialRepository.save(updatedRecord); + tutorialRepository.clearCache(updatedRecord.id); } if (modelName === 'missions') { await missionRepository.save(updatedRecord); + missionRepository.clearCache(updatedRecord.id); } } diff --git a/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js b/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js index c4e2417bcf5..622a0336886 100644 --- a/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js +++ b/api/src/learning-content/domain/usecases/refresh-learning-content-cache.js @@ -1,22 +1,22 @@ -import { withTransaction } from '../../../shared/domain/DomainTransaction.js'; +import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; -export const refreshLearningContentCache = withTransaction( - /** @param {import('./dependencies.js').Dependencies} */ - async function refreshLearningContentCache({ - lcmsClient, - frameworkRepository, - areaRepository, - competenceRepository, - thematicRepository, - tubeRepository, - skillRepository, - challengeRepository, - courseRepository, - tutorialRepository, - missionRepository, - }) { - const learningContent = await lcmsClient.getLatestRelease(); +/** @param {import('./dependencies.js').Dependencies} */ +export async function refreshLearningContentCache({ + lcmsClient, + frameworkRepository, + areaRepository, + competenceRepository, + thematicRepository, + tubeRepository, + skillRepository, + challengeRepository, + courseRepository, + tutorialRepository, + missionRepository, +}) { + const learningContent = await lcmsClient.getLatestRelease(); + await DomainTransaction.execute(async () => { await frameworkRepository.saveMany(learningContent.frameworks); await areaRepository.saveMany(learningContent.areas); await competenceRepository.saveMany(learningContent.competences); @@ -27,5 +27,16 @@ export const refreshLearningContentCache = withTransaction( await courseRepository.saveMany(learningContent.courses); await tutorialRepository.saveMany(learningContent.tutorials); await missionRepository.saveMany(learningContent.missions); - }, -); + }); + + frameworkRepository.clearCache(); + areaRepository.clearCache(); + competenceRepository.clearCache(); + thematicRepository.clearCache(); + tubeRepository.clearCache(); + skillRepository.clearCache(); + challengeRepository.clearCache(); + courseRepository.clearCache(); + tutorialRepository.clearCache(); + missionRepository.clearCache(); +} diff --git a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js index c9e4c20a96a..817cd59ace7 100644 --- a/api/src/learning-content/infrastructure/repositories/learning-content-repository.js +++ b/api/src/learning-content/infrastructure/repositories/learning-content-repository.js @@ -28,7 +28,6 @@ export class LearningContentRepository { for (const chunk of chunks(dtos, this.#chunkSize)) { await knex.insert(chunk).into(this.#tableName).onConflict('id').merge(); } - this.clearCache(); } /** @@ -38,7 +37,6 @@ export class LearningContentRepository { const dto = this.toDto(object); const knex = DomainTransaction.getConnection(); await knex.insert(dto).into(this.#tableName).onConflict('id').merge(); - this.clearCache(dto.id); } clearCache(_id) { diff --git a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js index 2a82d0e9cc8..ad0382f828f 100644 --- a/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js +++ b/api/tests/learning-content/unit/domain/usecases/patch-learning-content-cache-entry_test.js @@ -18,42 +18,52 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca beforeEach(function () { frameworkRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; frameworkRepository.save.rejects('I should not be called'); areaRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; areaRepository.save.rejects('I should not be called'); competenceRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; competenceRepository.save.rejects('I should not be called'); thematicRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; thematicRepository.save.rejects('I should not be called'); tubeRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; tubeRepository.save.rejects('I should not be called'); skillRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; skillRepository.save.rejects('I should not be called'); challengeRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; challengeRepository.save.rejects('I should not be called'); courseRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; courseRepository.save.rejects('I should not be called'); tutorialRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; tutorialRepository.save.rejects('I should not be called'); missionRepository = { save: sinon.stub(), + clearCache: sinon.stub(), }; missionRepository.save.rejects('I should not be called'); repositories = { @@ -98,7 +108,9 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca ].forEach((modelName) => { it(`should call save on appropriate repository for model ${modelName}`, async function () { // given - const updatedRecord = Symbol('updated record'); + const updatedRecord = Object.freeze({ + id: Symbol('updated record id'), + }); repositoriesByModel[modelName].save.withArgs(updatedRecord).resolves(); // when @@ -109,8 +121,8 @@ describe('Learning Content | Unit | Domain | Usecase | Patch learning content ca }); // then - expect(repositoriesByModel[modelName].save).to.have.been.calledOnce; - expect(repositoriesByModel[modelName].save).to.have.been.calledWithExactly(updatedRecord); + expect(repositoriesByModel[modelName].save).to.have.been.calledOnceWithExactly(updatedRecord); + expect(repositoriesByModel[modelName].clearCache).to.have.been.calledOnceWithExactly(updatedRecord.id); }); }); }); diff --git a/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js b/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js index 6e3c9ff48e0..56a99ac718f 100644 --- a/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js +++ b/api/tests/learning-content/unit/domain/usecases/refresh-learning-content-cache_test.js @@ -40,33 +40,43 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content const frameworkRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const areaRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const competenceRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const thematicRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const tubeRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const skillRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const challengeRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const courseRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const tutorialRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; const missionRepository = { saveMany: sinon.stub(), + clearCache: sinon.stub(), }; // when @@ -96,6 +106,16 @@ describe('Learning Content | Unit | Domain | Usecase | Refresh learning content expect(courseRepository.saveMany).to.have.been.calledOnceWithExactly(courses); expect(tutorialRepository.saveMany).to.have.been.calledOnceWithExactly(tutorials); expect(missionRepository.saveMany).to.have.been.calledOnceWithExactly(missions); + expect(frameworkRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(areaRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(competenceRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(thematicRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(tubeRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(skillRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(challengeRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(courseRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(tutorialRepository.clearCache).to.have.been.calledOnceWithExactly(); + expect(missionRepository.clearCache).to.have.been.calledOnceWithExactly(); }); }); }); From adf16bd4aac5a8723a5ea6c3d80a8ffb975d65fd Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:26:09 +0100 Subject: [PATCH 23/24] cleanup(api): removes old cache classes --- api/src/shared/infrastructure/caches/Cache.js | 19 -- .../infrastructure/caches/DistributedCache.js | 66 ----- .../infrastructure/caches/InMemoryCache.js | 65 ----- .../infrastructure/caches/LayeredCache.js | 42 --- .../infrastructure/caches/RedisCache.js | 86 ------ .../infrastructure/caches/apply-patch.js | 10 - .../unit/infrastructure/caches/Cache_test.js | 48 ---- .../caches/DistributedCache_test.js | 139 --------- .../caches/InMemoryCache_test.js | 181 ------------ .../caches/LayeredCache_test.js | 94 ------- .../infrastructure/caches/RedisCache_test.js | 263 ------------------ 11 files changed, 1013 deletions(-) delete mode 100644 api/src/shared/infrastructure/caches/Cache.js delete mode 100644 api/src/shared/infrastructure/caches/DistributedCache.js delete mode 100644 api/src/shared/infrastructure/caches/InMemoryCache.js delete mode 100644 api/src/shared/infrastructure/caches/LayeredCache.js delete mode 100644 api/src/shared/infrastructure/caches/RedisCache.js delete mode 100644 api/src/shared/infrastructure/caches/apply-patch.js delete mode 100644 api/tests/shared/unit/infrastructure/caches/Cache_test.js delete mode 100644 api/tests/shared/unit/infrastructure/caches/DistributedCache_test.js delete mode 100644 api/tests/shared/unit/infrastructure/caches/InMemoryCache_test.js delete mode 100644 api/tests/shared/unit/infrastructure/caches/LayeredCache_test.js delete mode 100644 api/tests/shared/unit/infrastructure/caches/RedisCache_test.js diff --git a/api/src/shared/infrastructure/caches/Cache.js b/api/src/shared/infrastructure/caches/Cache.js deleted file mode 100644 index 0b60cd47f7a..00000000000 --- a/api/src/shared/infrastructure/caches/Cache.js +++ /dev/null @@ -1,19 +0,0 @@ -class Cache { - async get(/* key, generator */) { - throw new Error('Method #get(key, generator) must be overridden'); - } - - async set(/* key, object */) { - throw new Error('Method #set(key, object) must be overridden'); - } - - async patch(/* key, patch */) { - throw new Error('Method #patch(key, patch) must be overridden'); - } - - async flushAll() { - throw new Error('Method #flushAll() must be overridden'); - } -} - -export { Cache }; diff --git a/api/src/shared/infrastructure/caches/DistributedCache.js b/api/src/shared/infrastructure/caches/DistributedCache.js deleted file mode 100644 index 85a1c359e55..00000000000 --- a/api/src/shared/infrastructure/caches/DistributedCache.js +++ /dev/null @@ -1,66 +0,0 @@ -import { logger } from '../utils/logger.js'; -import { RedisClient } from '../utils/RedisClient.js'; -import { Cache } from './Cache.js'; - -class DistributedCache extends Cache { - constructor(underlyingCache, redisUrl, channel) { - super(); - - this._underlyingCache = underlyingCache; - - this._redisClientPublisher = new RedisClient(redisUrl, { name: 'distributed-cache-publisher' }); - this._redisClientSubscriber = new RedisClient(redisUrl, { name: 'distributed-cache-subscriber' }); - this._channel = channel; - - this._redisClientSubscriber.on('ready', () => { - this._redisClientSubscriber.subscribe(this._channel); - }); - this._redisClientSubscriber.on('message', this.clientSubscriberCallback.bind(this)); - } - - clientSubscriberCallback(_channel, rawMessage) { - const message = JSON.parse(rawMessage); - if (message.type === 'flushAll') { - logger.info({ event: 'cache-event' }, 'Flushing the local cache'); - return this._underlyingCache.flushAll(); - } else if (message.type === 'patch') { - logger.info({ event: 'cache-event' }, 'Patching the local cache'); - this._underlyingCache.patch(message.cacheKey, message.patch); - } - } - - get(key, generator) { - return this._underlyingCache.get(key, generator); - } - - set(key, object) { - return this._underlyingCache.set(key, object); - } - - patch(key, object) { - const message = { - patch: object, - cacheKey: key, - type: 'patch', - }; - const messageAsString = JSON.stringify(message); - this._redisClientPublisher.publish(this._channel, messageAsString); - } - - flushAll() { - const message = { - type: 'flushAll', - }; - return this._redisClientPublisher.publish(this._channel, JSON.stringify(message)); - } - - quit() { - return Promise.all([ - this._underlyingCache.quit(), - this._redisClientPublisher.quit(), - this._redisClientSubscriber.quit(), - ]); - } -} - -export { DistributedCache }; diff --git a/api/src/shared/infrastructure/caches/InMemoryCache.js b/api/src/shared/infrastructure/caches/InMemoryCache.js deleted file mode 100644 index f7550cbe4f0..00000000000 --- a/api/src/shared/infrastructure/caches/InMemoryCache.js +++ /dev/null @@ -1,65 +0,0 @@ -import NodeCache from 'node-cache'; - -import { applyPatch } from './apply-patch.js'; -import { Cache } from './Cache.js'; - -class InMemoryCache extends Cache { - constructor() { - super(); - this._cache = new NodeCache({ useClones: false }); - this._queue = Promise.resolve(); - } - - quit() { - this._cache.close(); - } - - async get(key, generator) { - return this._syncGet(key, () => - this._chainPromise(() => { - return this._syncGet(key, () => this._generateAndSet(key, generator)); - }), - ); - } - - async set(key, value) { - return this._chainPromise(() => { - this._cache.set(key, value); - return value; - }); - } - - patch(key, patch) { - const value = this._cache.get(key); - if (value === undefined) return; - applyPatch(value, patch); - } - - async flushAll() { - return this._chainPromise(() => { - this._cache.flushAll(); - }); - } - - async _generateAndSet(key, generator) { - const generatedValue = await generator(); - this._cache.set(key, generatedValue); - return generatedValue; - } - - async _chainPromise(fn) { - const queuedPromise = this._queue.then(fn); - this._queue = queuedPromise.catch(() => { - return; - }); - return queuedPromise; - } - - _syncGet(key, generator) { - const value = this._cache.get(key); - if (value) return value; - return generator(); - } -} - -export { InMemoryCache }; diff --git a/api/src/shared/infrastructure/caches/LayeredCache.js b/api/src/shared/infrastructure/caches/LayeredCache.js deleted file mode 100644 index 322a3f641f1..00000000000 --- a/api/src/shared/infrastructure/caches/LayeredCache.js +++ /dev/null @@ -1,42 +0,0 @@ -import { logger } from '../utils/logger.js'; -import { Cache } from './Cache.js'; - -class LayeredCache extends Cache { - constructor(firstLevelCache, secondLevelCache) { - super(); - this._firstLevelCache = firstLevelCache; - this._secondLevelCache = secondLevelCache; - } - - get(key, generator) { - return this._firstLevelCache.get(key, () => { - logger.info( - { event: 'cache-event', key }, - 'Cannot found the key from the firstLevelCache. Fetching on the second one.', - ); - return this._secondLevelCache.get(key, generator); - }); - } - - async set(key, object) { - const cachedObject = await this._secondLevelCache.set(key, object); - await this._firstLevelCache.flushAll(); - return cachedObject; - } - - async patch(key, patch) { - await this._firstLevelCache.patch(key, patch); - return this._secondLevelCache.patch(key, patch); - } - - async flushAll() { - await this._firstLevelCache.flushAll(); - return this._secondLevelCache.flushAll(); - } - - quit() { - return Promise.all([this._firstLevelCache.quit(), this._secondLevelCache.quit()]); - } -} - -export { LayeredCache }; diff --git a/api/src/shared/infrastructure/caches/RedisCache.js b/api/src/shared/infrastructure/caches/RedisCache.js deleted file mode 100644 index 3e5b418d7a7..00000000000 --- a/api/src/shared/infrastructure/caches/RedisCache.js +++ /dev/null @@ -1,86 +0,0 @@ -import Redlock from 'redlock'; - -import { config } from '../../config.js'; -import { logger } from '../utils/logger.js'; -import { RedisClient } from '../utils/RedisClient.js'; -import { applyPatch } from './apply-patch.js'; -import { Cache } from './Cache.js'; - -const REDIS_LOCK_PREFIX = 'locks:'; -export const PATCHES_KEY = 'patches'; - -class RedisCache extends Cache { - constructor(redis_url) { - super(); - this._client = RedisCache.createClient(redis_url); - } - - static createClient(redis_url) { - return new RedisClient(redis_url, { name: 'redis-cache-query-client', prefix: 'cache:' }); - } - - async get(key, generator) { - const value = await this._client.get(key); - - if (value) { - const parsed = JSON.parse(value); - const patches = await this._client.lrange(`${key}:${PATCHES_KEY}`, 0, -1); - patches.map((patchJSON) => JSON.parse(patchJSON)).forEach((patch) => applyPatch(parsed, patch)); - return parsed; - } - - return this._manageValueNotFoundInCache(key, generator); - } - - async _manageValueNotFoundInCache(key, generator) { - const keyToLock = REDIS_LOCK_PREFIX + key; - - let lock; - try { - lock = await this._client.lock(keyToLock, config.caching.redisCacheKeyLockTTL); - - logger.info({ key }, 'Executing generator for Redis key'); - const value = await generator(); - return this.set(key, value); - } catch (err) { - if (err instanceof Redlock.LockError) { - logger.trace({ keyToLock }, 'Could not lock Redis key, waiting'); - await new Promise((resolve) => setTimeout(resolve, config.caching.redisCacheLockedWaitBeforeRetry)); - return this.get(key, generator); - } - logger.error({ err }, 'Error while trying to update value in Redis cache'); - throw err; - } finally { - if (lock) await lock.unlock(); - } - } - - async set(key, object) { - const objectAsString = JSON.stringify(object); - - logger.info({ key, length: objectAsString.length }, 'Setting Redis key'); - - await this._client.set(key, objectAsString); - await this._client.del(`${key}:${PATCHES_KEY}`); - - return object; - } - - async patch(key, patch) { - const patchesKey = `${key}:${PATCHES_KEY}`; - - return this._client.rpush(patchesKey, JSON.stringify(patch)); - } - - flushAll() { - logger.info('Flushing Redis database'); - - return this._client.flushall(); - } - - async quit() { - await this._client.quit(); - } -} - -export { RedisCache }; diff --git a/api/src/shared/infrastructure/caches/apply-patch.js b/api/src/shared/infrastructure/caches/apply-patch.js deleted file mode 100644 index 0297f789038..00000000000 --- a/api/src/shared/infrastructure/caches/apply-patch.js +++ /dev/null @@ -1,10 +0,0 @@ -import _ from 'lodash'; - -export function applyPatch(value, patch) { - if (patch.operation === 'assign') { - _.set(value, patch.path, patch.value); - } else if (patch.operation === 'push') { - const arr = _.get(value, patch.path); - arr.push(patch.value); - } -} diff --git a/api/tests/shared/unit/infrastructure/caches/Cache_test.js b/api/tests/shared/unit/infrastructure/caches/Cache_test.js deleted file mode 100644 index 57fe6b64273..00000000000 --- a/api/tests/shared/unit/infrastructure/caches/Cache_test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { Cache } from '../../../../../src/shared/infrastructure/caches/Cache.js'; -import { expect } from '../../../../test-helper.js'; - -describe('Unit | Infrastructure | Caches | Cache', function () { - const cacheInstance = new Cache(); - - describe('#get', function () { - it('should reject an error (because this class actually mocks an interface)', function () { - // when - const result = cacheInstance.get('some-key', () => { - return; - }); - - // then - expect(result).to.be.rejected; - }); - }); - - describe('#set', function () { - it('should reject an error (because this class actually mocks an interface)', function () { - // when - const result = cacheInstance.set('some-key', {}); - - // then - expect(result).to.be.rejected; - }); - }); - - describe('#patch', function () { - it('should reject an error (because this class actually mocks an interface)', function () { - // when - const result = cacheInstance.patch('some-key', {}); - - // then - expect(result).to.be.rejected; - }); - }); - - describe('#flushAll', function () { - it('should reject an error (because this class actually mocks an interface)', function () { - // when - const result = cacheInstance.flushAll(); - - // then - expect(result).to.be.rejected; - }); - }); -}); diff --git a/api/tests/shared/unit/infrastructure/caches/DistributedCache_test.js b/api/tests/shared/unit/infrastructure/caches/DistributedCache_test.js deleted file mode 100644 index 29934f8cb94..00000000000 --- a/api/tests/shared/unit/infrastructure/caches/DistributedCache_test.js +++ /dev/null @@ -1,139 +0,0 @@ -import { DistributedCache } from '../../../../../src/shared/infrastructure/caches/DistributedCache.js'; -import { expect, sinon } from '../../../../test-helper.js'; - -describe('Unit | Infrastructure | Caches | DistributedCache', function () { - let distributedCacheInstance; - let underlyingCache; - const channel = 'channel'; - - beforeEach(function () { - underlyingCache = { - get: sinon.stub(), - set: sinon.stub(), - patch: sinon.stub(), - flushAll: sinon.stub(), - }; - const redisUrl = 'redis://url.example.net'; - distributedCacheInstance = new DistributedCache(underlyingCache, redisUrl, channel); - }); - - describe('#get', function () { - it('should resolve the underlying cache result for get() method', async function () { - // given - const cacheKey = 'cache-key'; - const cachedObject = { foo: 'bar' }; - const generator = () => cachedObject; - underlyingCache.get.withArgs(cacheKey, generator).resolves(cachedObject); - - // when - const result = await distributedCacheInstance.get(cacheKey, generator); - - // then - expect(result).to.deep.equal(cachedObject); - }); - }); - - describe('#set', function () { - it('should resolve the underlying cache result for set() method', async function () { - // given - const cacheKey = 'cache-key'; - const objectToCache = { foo: 'bar' }; - underlyingCache.set.withArgs(cacheKey, objectToCache).resolves(objectToCache); - - // when - const result = await distributedCacheInstance.set(cacheKey, objectToCache); - - // then - expect(result).to.deep.equal(objectToCache); - }); - }); - - describe('#patch', function () { - it('should publish the patch on the redis channel', async function () { - // given - distributedCacheInstance._redisClientPublisher = { - publish: sinon.stub(), - }; - const cacheKey = 'cache-key'; - const patch = { - operation: 'assign', - path: 'challenges[0]', - value: { id: 'recChallenge1', instruction: 'Nouvelle consigne' }, - }; - - const message = { - patch, - cacheKey, - type: 'patch', - }; - const messageAsString = JSON.stringify(message); - distributedCacheInstance._redisClientPublisher.publish.withArgs(channel, messageAsString).resolves(true); - - // when - await distributedCacheInstance.patch(cacheKey, patch); - - // then - expect(distributedCacheInstance._redisClientPublisher.publish).to.have.been.calledOnceWith( - channel, - messageAsString, - ); - }); - }); - - describe('#flushAll', function () { - it('shoud use Redis pub/sub notification mechanism to trigger the caches synchronization', async function () { - // given - distributedCacheInstance._redisClientPublisher = { - publish: sinon.stub(), - }; - const message = { - type: 'flushAll', - }; - distributedCacheInstance._redisClientPublisher.publish.withArgs(channel, JSON.stringify(message)).resolves(true); - - // when - const result = await distributedCacheInstance.flushAll(); - - // then - expect(result).to.be.true; - }); - }); - - describe('receive message', function () { - it('should flushAll when flush message is received', async function () { - // given - const message = { - type: 'flushAll', - }; - // when - await distributedCacheInstance.clientSubscriberCallback('channel', JSON.stringify(message)); - - // then - expect(distributedCacheInstance._underlyingCache.flushAll).to.have.been.calledOnce; - }); - - it('should patch when patch message is received', async function () { - // given - const cacheKey = 'cache-key'; - const patch = { - operation: 'assign', - path: 'challenges[0]', - value: { id: 'recChallenge1', instruction: 'Nouvelle consigne' }, - }; - - const message = { - type: 'patch', - patch, - cacheKey, - }; - const messageAsString = JSON.stringify(message); - - // when - await distributedCacheInstance.clientSubscriberCallback('channel', messageAsString); - - // then - expect(distributedCacheInstance._underlyingCache.flushAll).not.to.have.been.called; - expect(distributedCacheInstance._underlyingCache.patch).to.have.been.calledWith(cacheKey, patch); - }); - }); -}); diff --git a/api/tests/shared/unit/infrastructure/caches/InMemoryCache_test.js b/api/tests/shared/unit/infrastructure/caches/InMemoryCache_test.js deleted file mode 100644 index 9c92b2b0692..00000000000 --- a/api/tests/shared/unit/infrastructure/caches/InMemoryCache_test.js +++ /dev/null @@ -1,181 +0,0 @@ -import NodeCache from 'node-cache'; - -import { InMemoryCache } from '../../../../../src/shared/infrastructure/caches/InMemoryCache.js'; -import { expect, sinon } from '../../../../test-helper.js'; - -describe('Unit | Infrastructure | Cache | in-memory-cache', function () { - let inMemoryCache; - - const CACHE_KEY = 'cache_key'; - const NODE_CACHE_ERROR = new Error('A Node cache error'); - - beforeEach(function () { - inMemoryCache = new InMemoryCache(); - }); - - describe('#constructor', function () { - it('should create a NodeCache instance', function () { - // then - expect(inMemoryCache._cache).to.be.an.instanceOf(NodeCache); - }); - }); - - describe('#get', function () { - it('should resolve with the previously cached value when it exists', async function () { - // given - const cachedObject = { foo: 'bar' }; - inMemoryCache._cache.set(CACHE_KEY, cachedObject); - - // when - const result = await inMemoryCache.get(CACHE_KEY); - - // then - expect(result).to.deep.equal(cachedObject); - }); - - it('should call generator when no object was previously cached for given key', async function () { - // when - const generatorStub = sinon.stub().resolves('hello'); - const result = await inMemoryCache.get(CACHE_KEY, generatorStub); - - // then - expect(result).to.equal('hello'); - }); - - it('should reject when generator fails', function () { - // given - const generatorError = new Error('Generator failed'); - const generatorStub = sinon.stub().rejects(generatorError); - - // when - const promise = inMemoryCache.get(CACHE_KEY, generatorStub); - - // then - return expect(promise).to.have.been.rejectedWith(generatorError); - }); - - it('should not call generator again if same key is requested while generator is in progress', async function () { - // when - const generatorStub = sinon.stub().resolves('hello'); - const generatorStub2 = sinon.stub().resolves('hello'); - const promise = inMemoryCache.get(CACHE_KEY, generatorStub); - const promise2 = inMemoryCache.get(CACHE_KEY, generatorStub2); - - // then - await Promise.all([promise, promise2]); - expect(generatorStub2).to.not.have.been.called; - }); - - it('should not throw further get if one generator fails', async function () { - // when - const generatorError = new Error('Generator failed'); - const failingGenerator = sinon.stub().rejects(generatorError); - const successfulGenerator = sinon.stub().resolves('hello'); - const promise = inMemoryCache.get(CACHE_KEY, failingGenerator); - const promise2 = inMemoryCache.get(CACHE_KEY, successfulGenerator); - - // then - await expect(promise).to.have.been.rejectedWith(generatorError); - expect(await promise2).to.equal('hello'); - }); - - it('should reject when the Node cache throws an error', function () { - // given - inMemoryCache._cache.get = () => { - throw NODE_CACHE_ERROR; - }; - - // when - const promise = inMemoryCache.get(CACHE_KEY); - - // then - return expect(promise).to.have.been.rejectedWith(NODE_CACHE_ERROR); - }); - }); - - describe('#set', function () { - const objectToCache = { foo: 'bar' }; - - it('should resolve with the object to cache', async function () { - // when - const result = await inMemoryCache.set(CACHE_KEY, objectToCache); - - // then - expect(result).to.deep.equal(objectToCache); - expect(inMemoryCache._cache.get(CACHE_KEY)).to.equal(objectToCache); - }); - - it('should reject when the Node cache throws an error', function () { - // given - inMemoryCache._cache.set = () => { - throw NODE_CACHE_ERROR; - }; - - // when - const promise = inMemoryCache.set(CACHE_KEY, objectToCache); - - // then - return expect(promise).to.have.been.rejectedWith(NODE_CACHE_ERROR); - }); - }); - - describe('#patch', function () { - let getStub; - - beforeEach(function () { - getStub = sinon.stub(inMemoryCache._cache, 'get'); - }); - - it('should patch the value assigning to a path', async function () { - // given - const objectToCache = { - challenges: [{ id: 'recChallenge1', instruction: 'Ancienne consigne' }], - }; - getStub.withArgs(CACHE_KEY).returns(objectToCache); - const patch = { - operation: 'assign', - path: 'challenges[0]', - value: { id: 'recChallenge1', instruction: 'Nouvelle consigne' }, - }; - - // when - await inMemoryCache.patch(CACHE_KEY, patch); - - // then - expect(objectToCache).to.deep.equal({ - challenges: [{ id: 'recChallenge1', instruction: 'Nouvelle consigne' }], - }); - }); - - describe('when value is not in the cache', function () { - it('should do nothing', async function () { - // given - getStub.withArgs(CACHE_KEY).returns(undefined); - const patch = { - operation: 'push', - path: 'challenges', - value: { id: 'recChallenge1', instruction: 'Nouvelle consigne' }, - }; - - // when - await inMemoryCache.patch(CACHE_KEY, patch); - - // then - expect(getStub).to.have.been.calledOnceWithExactly(CACHE_KEY); - }); - }); - }); - - describe('#flushAll', function () { - it('should resolve', async function () { - // given - await inMemoryCache.set('foo', 'bar'); - - // when - await inMemoryCache.flushAll(); - - // then - expect(inMemoryCache._cache.getStats().keys).to.equal(0); - }); - }); -}); diff --git a/api/tests/shared/unit/infrastructure/caches/LayeredCache_test.js b/api/tests/shared/unit/infrastructure/caches/LayeredCache_test.js deleted file mode 100644 index c4ed9790cb1..00000000000 --- a/api/tests/shared/unit/infrastructure/caches/LayeredCache_test.js +++ /dev/null @@ -1,94 +0,0 @@ -import { LayeredCache } from '../../../../../src/shared/infrastructure/caches/LayeredCache.js'; -import { expect, sinon } from '../../../../test-helper.js'; - -describe('Unit | Infrastructure | Caches | LayeredCache', function () { - const layeredCacheInstance = new LayeredCache(); - - beforeEach(function () { - layeredCacheInstance._firstLevelCache = { - get: sinon.stub(), - set: sinon.stub(), - patch: sinon.stub(), - flushAll: sinon.stub(), - }; - layeredCacheInstance._secondLevelCache = { - get: sinon.stub(), - set: sinon.stub(), - patch: sinon.stub(), - flushAll: sinon.stub(), - }; - }); - - describe('#get', function () { - const cachedObject = { foo: 'bar' }; - const cacheKey = 'cache-key'; - const generator = () => cachedObject; - - it('should delegate to first level cache, by passing it the second level cache as generator', async function () { - // given - layeredCacheInstance._firstLevelCache.get.withArgs(cacheKey).callsFake((key, generator) => generator()); - layeredCacheInstance._secondLevelCache.get - .withArgs(cacheKey, generator) - .callsFake((key, generator) => generator()); - - // when - const result = await layeredCacheInstance.get(cacheKey, generator); - - // then - expect(result).to.deep.equal(cachedObject); - }); - }); - - describe('#set', function () { - const cacheKey = 'cache-key'; - const objectToCache = { foo: 'bar' }; - - it('should delegate to first level cache, by passing it the second level cache as generator', async function () { - // given - layeredCacheInstance._secondLevelCache.set.withArgs(cacheKey, objectToCache).resolves(objectToCache); - - // when - const result = await layeredCacheInstance.set(cacheKey, objectToCache); - - // then - expect(layeredCacheInstance._firstLevelCache.flushAll).to.have.been.calledOnce; - expect(result).to.deep.equal(objectToCache); - expect(layeredCacheInstance._secondLevelCache.set).to.have.been.calledBefore( - layeredCacheInstance._firstLevelCache.flushAll, - ); - }); - }); - - describe('#flushAll', function () { - it('should flush all entries for both first and second level caches', async function () { - // given - - // when - await layeredCacheInstance.flushAll(); - - // then - expect(layeredCacheInstance._firstLevelCache.flushAll).to.have.been.calledOnce; - expect(layeredCacheInstance._secondLevelCache.flushAll).to.have.been.calledOnce; - }); - }); - - describe('#patch', function () { - const cacheKey = 'learning-content'; - - it('should apply patch in first and second level cache', async function () { - // given - const patch = { - operation: 'assign', - path: 'challenges[0]', - value: { id: 'recChallenge1', instruction: 'Nouvelle consigne' }, - }; - - // when - await layeredCacheInstance.patch(cacheKey, patch); - - // then - expect(layeredCacheInstance._firstLevelCache.patch).to.have.been.calledWith(cacheKey, patch); - expect(layeredCacheInstance._secondLevelCache.patch).to.have.been.calledWith(cacheKey, patch); - }); - }); -}); diff --git a/api/tests/shared/unit/infrastructure/caches/RedisCache_test.js b/api/tests/shared/unit/infrastructure/caches/RedisCache_test.js deleted file mode 100644 index 959654e86d9..00000000000 --- a/api/tests/shared/unit/infrastructure/caches/RedisCache_test.js +++ /dev/null @@ -1,263 +0,0 @@ -import Redlock from 'redlock'; - -import { config as settings } from '../../../../../src/shared/config.js'; -import { PATCHES_KEY, RedisCache } from '../../../../../src/shared/infrastructure/caches/RedisCache.js'; -import { expect, sinon } from '../../../../test-helper.js'; - -describe('Unit | Infrastructure | Cache | redis-cache', function () { - let stubbedClient; - let redisCache; - - const REDIS_URL = 'redis_url'; - const CACHE_KEY = 'cache_key'; - const REDIS_CLIENT_ERROR = new Error('A Redis client error'); - - beforeEach(function () { - stubbedClient = { - lock: sinon.stub().resolves({ unlock: sinon.stub().resolves() }), - }; - sinon.stub(RedisCache, 'createClient').withArgs(REDIS_URL).returns(stubbedClient); - redisCache = new RedisCache(REDIS_URL); - }); - - describe('#get', function () { - beforeEach(function () { - stubbedClient.get = sinon.stub(); - stubbedClient.lrange = sinon.stub().resolves([]); - redisCache.set = sinon.stub(); - }); - - context('when the value is already in cache', function () { - it('should resolve with the existing value', async function () { - // given - const cachedData = { foo: 'bar' }; - const redisCachedData = JSON.stringify(cachedData); - stubbedClient.get.withArgs(CACHE_KEY).resolves(redisCachedData); - stubbedClient.lrange.withArgs(`${CACHE_KEY}:${PATCHES_KEY}`, 0, -1).resolves([]); - - // when - const result = await redisCache.get(CACHE_KEY); - - // then - expect(result).to.deep.equal(cachedData); - }); - - it('should resolve with the existing value and apply the patche if any', async function () { - // given - const redisCachedData = JSON.stringify({ foo: 'bar' }); - const cachedPatchesData = [JSON.stringify({ operation: 'assign', path: 'foo', value: 'roger' })]; - stubbedClient.get.withArgs(CACHE_KEY).resolves(redisCachedData); - stubbedClient.lrange.withArgs(`${CACHE_KEY}:${PATCHES_KEY}`, 0, -1).resolves(cachedPatchesData); - const finalResult = { foo: 'roger' }; - - // when - const result = await redisCache.get(CACHE_KEY); - - // then - expect(result).to.deep.equal(finalResult); - }); - - it('should resolve with the existing value and apply the patches if any', async function () { - // given - const redisCachedData = JSON.stringify({ foo: 'bar', fibonnaci: [1] }); - const cachedPatchesData = [ - JSON.stringify({ operation: 'assign', path: 'foo', value: 'roger' }), - JSON.stringify({ operation: 'push', path: 'fibonnaci', value: 2 }), - JSON.stringify({ operation: 'push', path: 'fibonnaci', value: 3 }), - JSON.stringify({ operation: 'assign', path: 'fibonnaci[2]', value: 5 }), - ]; - stubbedClient.get.withArgs(CACHE_KEY).resolves(redisCachedData); - stubbedClient.lrange.withArgs(`${CACHE_KEY}:${PATCHES_KEY}`, 0, -1).resolves(cachedPatchesData); - const finalResult = { foo: 'roger', fibonnaci: [1, 2, 5] }; - - // when - const result = await redisCache.get(CACHE_KEY); - - // then - expect(result).to.deep.equal(finalResult); - }); - }); - - context('when the value is not in cache', function () { - beforeEach(function () { - const cachedObject = { foo: 'bar' }; - const redisCachedValue = JSON.stringify(cachedObject); - stubbedClient.get.withArgs(CACHE_KEY).onCall(0).resolves(null); - stubbedClient.get.withArgs(CACHE_KEY).onCall(1).resolves(redisCachedValue); - redisCache.set.resolves(); - }); - - it('should try to lock the cache key', async function () { - // given - const expectedLockedKey = 'locks:' + CACHE_KEY; - const handler = sinon.stub().resolves(); - - // when - await redisCache.get(CACHE_KEY, handler); - - // then - expect(stubbedClient.lock).to.have.been.calledWith(expectedLockedKey, settings.caching.redisCacheKeyLockTTL); - }); - - context('and the cache key is not already locked', function () { - it('should add into the cache the value returned by the handler', async function () { - // given - const dataFromHandler = { name: 'data from learning content' }; - const handler = sinon.stub().resolves(dataFromHandler); - - // when - await redisCache.get(CACHE_KEY, handler); - - // then - expect(redisCache.set).to.have.been.calledWithExactly(CACHE_KEY, dataFromHandler); - }); - - it('should return the value', async function () { - // given - const dataFromHandler = { name: 'data from learning content' }; - const handler = sinon.stub().resolves(dataFromHandler); - redisCache.set.resolves(dataFromHandler); - - // when - const value = await redisCache.get(CACHE_KEY, handler); - - // then - expect(value).to.equal(dataFromHandler); - }); - }); - - context('and the cache key is already locked', function () { - it('should wait and retry to get the value from the cache', async function () { - // given - const dataFromHandler = { name: 'data from learning content' }; - const handler = sinon.stub().resolves(dataFromHandler); - stubbedClient.lock.rejects(new Redlock.LockError()); - - // when - await redisCache.get(CACHE_KEY, handler); - - // then - expect(stubbedClient.get).to.have.been.calledTwice; - }); - }); - }); - - it('should reject when the Redis cache client throws an error', function () { - // given - - stubbedClient.get.rejects(REDIS_CLIENT_ERROR); - - // when - const promise = redisCache.get(CACHE_KEY); - - // then - return expect(promise).to.have.been.rejectedWith(REDIS_CLIENT_ERROR); - }); - - it('should reject when the previously cached value can not be parsed as JSON', function () { - // given - const redisCachedValue = 'Unprocessable JSON object'; - stubbedClient.get.resolves(redisCachedValue); - - // when - const promise = redisCache.get(CACHE_KEY); - - // then - return expect(promise).to.have.been.rejectedWith(SyntaxError); - }); - }); - - describe('#set', function () { - const objectToCache = { foo: 'bar' }; - - beforeEach(function () { - stubbedClient.set = sinon.stub(); - stubbedClient.del = sinon.stub(); - }); - - it('should resolve with the object to cache', async function () { - // given - stubbedClient.set.resolves(); - - // when - const result = await redisCache.set(CACHE_KEY, objectToCache); - - // then - expect(result).to.deep.equal(objectToCache); - expect(stubbedClient.set).to.have.been.calledWithExactly(CACHE_KEY, JSON.stringify(objectToCache)); - }); - - it('should reject when the Redis cache client throws an error', function () { - // given - stubbedClient.set.rejects(REDIS_CLIENT_ERROR); - - // when - const promise = redisCache.set(CACHE_KEY, objectToCache); - - // then - return expect(promise).to.have.been.rejectedWith(REDIS_CLIENT_ERROR); - }); - - it('should empty patches key', async function () { - // given - stubbedClient.set.resolves(); - stubbedClient.del.resolves(); - - // when - await redisCache.set(CACHE_KEY, objectToCache); - - // then - expect(stubbedClient.del).to.have.been.calledWithExactly(`${CACHE_KEY}:patches`); - }); - }); - - describe('#flushAll', function () { - beforeEach(function () { - stubbedClient.flushall = sinon.stub(); - }); - - it('should resolve', function () { - // given - stubbedClient.flushall.resolves(); - - // when - const promise = redisCache.flushAll(); - - // then - return expect(promise).to.have.been.fulfilled; - }); - - it('should reject when the Redis cache client throws an error', function () { - // given - stubbedClient.flushall.rejects(REDIS_CLIENT_ERROR); - - // when - const promise = redisCache.flushAll(); - - // then - return expect(promise).to.have.been.rejectedWith(REDIS_CLIENT_ERROR); - }); - }); - - describe('#patch', function () { - beforeEach(function () { - stubbedClient.rpush = sinon.stub(); - }); - - it('should push patch in a separate patches key', async function () { - // given - const patch = { - operation: 'assign', - path: 'challenges[0]', - value: { id: 'recChallenge1', instruction: 'Consigne' }, - }; - const expectedPatchAsString = JSON.stringify(patch); - - // when - await redisCache.patch(CACHE_KEY, patch); - - // then - expect(stubbedClient.rpush).to.have.been.calledOnceWith(CACHE_KEY + ':patches', expectedPatchAsString); - }); - }); -}); From 2084c2ce97940af9213589c88d4966c905dc92d6 Mon Sep 17 00:00:00 2001 From: Nicolas Lepage <19571875+nlepage@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:36:38 +0100 Subject: [PATCH 24/24] WIP documentation and logs --- .../caches/learning-content-cache.js | 13 ++- .../learning-content-repository.js | 84 +++++++++++++++---- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/api/src/shared/infrastructure/caches/learning-content-cache.js b/api/src/shared/infrastructure/caches/learning-content-cache.js index cb57d595c4d..255c65b80fb 100644 --- a/api/src/shared/infrastructure/caches/learning-content-cache.js +++ b/api/src/shared/infrastructure/caches/learning-content-cache.js @@ -1,4 +1,7 @@ import * as learningContentPubSub from '../caches/learning-content-pubsub.js'; +import { child } from '../utils/logger.js'; + +const logger = child('learningcontent:cache', { event: 'learningcontent' }); export class LearningContentCache { #map; @@ -39,8 +42,14 @@ export class LearningContentCache { async #subscribe() { for await (const message of this.#pubSub.subscribe(this.#name)) { - if (message.type === 'clear') this.#map.clear(); - if (message.type === 'delete') this.#map.delete(message.key); + if (message.type === 'clear') { + logger.debug({ name: this.#name }, 'clearing cache'); + this.#map.clear(); + } + if (message.type === 'delete') { + logger.debug({ name: this.#name, key: message.key }, 'deleting cache key'); + this.#map.delete(message.key); + } } } } diff --git a/api/src/shared/infrastructure/repositories/learning-content-repository.js b/api/src/shared/infrastructure/repositories/learning-content-repository.js index 18b3c4913e7..121f1324b15 100644 --- a/api/src/shared/infrastructure/repositories/learning-content-repository.js +++ b/api/src/shared/infrastructure/repositories/learning-content-repository.js @@ -2,7 +2,18 @@ import Dataloader from 'dataloader'; import { knex } from '../../../../db/knex-database-connection.js'; import { LearningContentCache } from '../caches/learning-content-cache.js'; +import { child } from '../utils/logger.js'; +const logger = child('learningcontent:repository', { event: 'learningcontent' }); + +/** + * @typedef {(knex: import('knex').QueryBuilder) => Promise} QueryBuilderCallback + */ + +/** + * Datasource for learning content repositories. + * This datasource uses a {@link Dataloader} to load and cache entities. + */ export class LearningContentRepository { #tableName; #idType; @@ -10,6 +21,12 @@ export class LearningContentRepository { #findCache; #findCacheMiss; + /** + * @param {{ + * tableName: string + * idType?: 'text' | 'integer' + * }} config + */ constructor({ tableName, idType = 'text' }) { this.#tableName = tableName; this.#idType = idType; @@ -23,21 +40,15 @@ export class LearningContentRepository { this.#findCacheMiss = new Map(); } + /** + * Finds several entities using a request and caches results. + * The request is built using a knex query builder given to {@link callback}. + * {@link cacheKey} must vary according to params given to the query builder. + * @param {string} cacheKey + * @param {QueryBuilderCallback} callback + * @returns {Promise} + */ async find(cacheKey, callback) { - return this.#findDtos(callback, cacheKey); - } - - async load(id) { - if (!id) return null; - return this.#dataloader.load(id); - } - - async loadMany(ids) { - const notNullIds = ids.filter((id) => id); - return this.#dataloader.loadMany(notNullIds); - } - - #findDtos(callback, cacheKey) { let dtos = this.#findCache.get(cacheKey); if (dtos) return dtos; @@ -52,14 +63,51 @@ export class LearningContentRepository { return dtos; } + /** + * Loads one entity by ID. + * @param {string|number} id + * @returns {Promise} + */ + async load(id) { + if (!id) return null; + return this.#dataloader.load(id); + } + + /** + * Loads several entities by ID. + * @param {string[]|number[]} ids + * @returns {Promise} + */ + async loadMany(ids) { + const notNullIds = ids.filter((id) => id); + return this.#dataloader.loadMany(notNullIds); + } + + /** + * Loads entities from database using a request and writes result to cache. + * @param {string} cacheKey + * @param {QueryBuilderCallback} callback + * @returns {Promise} + */ async #loadDtos(callback, cacheKey) { const ids = await callback(knex.pluck(`${this.#tableName}.id`).from(this.#tableName)); const dtos = await this.#dataloader.loadMany(ids); + + logger.debug({ tableName: this.#tableName, cacheKey }, 'caching find result'); this.#findCache.set(cacheKey, dtos); + return dtos; } + /** + * Loads a batch of entities from database by ID. + * Entities are returned in the same order as {@link ids}. + * If an ID is not found, it is null in results. + * @param {string[]|number[]} ids + * @returns {Promise<(object|null)[]>} + */ async #batchLoad(ids) { + logger.debug({ tableName: this.#tableName, count: ids.length }, 'loading from PG'); const dtos = await knex .select(`${this.#tableName}.*`) .from(knex.raw(`unnest(?::${this.#idType}[]) with ordinality as ids(id, idx)`, [ids])) // eslint-disable-line knex/avoid-injections @@ -68,7 +116,15 @@ export class LearningContentRepository { return dtos.map((dto) => (dto.id ? dto : null)); } + /** + * Clears repository’s cache. + * If {@link id} is undefined, all cache is cleared. + * If {@link id} is given, cache is partially cleared. + * @param {string|number|undefined} id + */ clearCache(id) { + logger.debug({ tableName: this.#tableName, id }, 'trigerring cache clear'); + if (id) { this.#dataloader.clear(id); } else {