diff --git a/api/src/routes/helpers/certificate-utils.ts b/api/src/routes/helpers/certificate-utils.ts index 28a34d03017..d33cec87714 100644 --- a/api/src/routes/helpers/certificate-utils.ts +++ b/api/src/routes/helpers/certificate-utils.ts @@ -1,26 +1,18 @@ import { Prisma } from '@prisma/client'; import { certSlugTypeMap, - certIds + certToIdMap, + Certification } from '../../../../shared/config/certification-settings.js'; import { normalizeDate } from '../../utils/normalize.js'; -const { - legacyInfosecQaId, - respWebDesignId, - frontEndDevLibsId, - jsAlgoDataStructId, - dataVis2018Id, - apisMicroservicesId -} = certIds; - const fullStackCertificateIds = [ - respWebDesignId, - jsAlgoDataStructId, - frontEndDevLibsId, - dataVis2018Id, - apisMicroservicesId, - legacyInfosecQaId + certToIdMap[Certification.RespWebDesign], + certToIdMap[Certification.JsAlgoDataStruct], + certToIdMap[Certification.FrontEndDevLibs], + certToIdMap[Certification.DataVis], + certToIdMap[Certification.BackEndDevApis], + certToIdMap[Certification.LegacyInfoSecQa] ]; /** diff --git a/api/src/routes/protected/certificate.test.ts b/api/src/routes/protected/certificate.test.ts index 07a00256eef..773380bfb58 100644 --- a/api/src/routes/protected/certificate.test.ts +++ b/api/src/routes/protected/certificate.test.ts @@ -16,6 +16,8 @@ import { setupServer, superRequest } from '../../../vitest.utils.js'; +import { getChallenges } from '../../utils/get-challenges.js'; +import { createCertLookup } from './certificate.js'; describe('certificate routes', () => { setupServer(); @@ -461,3 +463,32 @@ describe('certificate routes', () => { }); }); }); + +describe('createCertLookup', () => { + let challenges: ReturnType; + + beforeAll(() => { + // TODO: create a mock challenges array specific to these tests. + challenges = getChallenges(); + }); + + test('should create a lookup for all certifications', () => { + const certLookup = createCertLookup(challenges); + + for (const cert of Object.values(Certification)) { + const certData = certLookup[cert]; + expect(certData).toHaveProperty('id'); + expect(certData).toHaveProperty('tests'); + expect(certData).toHaveProperty('challengeType'); + } + }); + + test('each certification should have a unique challenge id', () => { + const certLookup = createCertLookup(challenges); + const ids = Object.values(certLookup) + .map(({ id }) => id) + .sort(); + const uniqueIds = Array.from(new Set(ids)).sort(); + expect(uniqueIds).toEqual(ids); + }); +}); diff --git a/api/src/routes/protected/certificate.ts b/api/src/routes/protected/certificate.ts index 14610599b53..2b0383f4f46 100644 --- a/api/src/routes/protected/certificate.ts +++ b/api/src/routes/protected/certificate.ts @@ -4,9 +4,9 @@ import type { FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebo import { getChallenges } from '../../utils/get-challenges.js'; import { - certIds, Certification, certSlugTypeMap, + certToIdMap, certToTitleMap, currentCertifications, legacyCertifications, @@ -83,10 +83,11 @@ function assertTestsExist( } } -function getCertById( - challengeId: string, +function getCertBySlug( + cert: Certification, challenges: ReturnType ): { id: string; tests: { id: string }[]; challengeType: number } { + const challengeId = certToIdMap[cert]; const challengeById = challenges.filter(({ id }) => id === challengeId)[0]; if (!challengeById) { throw new Error(`Challenge with id '${challengeId}' not found`); @@ -96,112 +97,28 @@ function getCertById( return { id, tests, challengeType }; } -function createCertLookup( - challenges: ReturnType -): Record< +type CertLookup = Record< Certification, { id: string; tests: { id: string }[]; challengeType: number } -> { - return { - // legacy - [Certification.LegacyFrontEnd]: getCertById( - certIds.legacyFrontEndChallengeId, - challenges - ), - [Certification.JsAlgoDataStruct]: getCertById( - certIds.jsAlgoDataStructId, - challenges - ), +>; - [Certification.LegacyBackEnd]: getCertById( - certIds.legacyBackEndChallengeId, - challenges - ), - [Certification.LegacyDataVis]: getCertById( - certIds.legacyDataVisId, - challenges - ), - [Certification.LegacyInfoSecQa]: getCertById( - certIds.legacyInfosecQaId, - challenges - ), - [Certification.LegacyFullStack]: getCertById( - certIds.legacyFullStackId, - challenges - ), +/** + * Create a lookup from Certification enum values to their corresponding + * challenge metadata (id, tests and challengeType) using the provided + * challenges array. + * + * @param challenges - The array returned by getChallenges(). + * @returns A record mapping each Certification to an object with id, tests and challengeType. + */ +export function createCertLookup( + challenges: ReturnType +): CertLookup { + const certLookup = {} as CertLookup; - // modern - [Certification.RespWebDesign]: getCertById( - certIds.respWebDesignId, - challenges - ), - [Certification.FrontEndDevLibs]: getCertById( - certIds.frontEndDevLibsId, - challenges - ), - [Certification.DataVis]: getCertById(certIds.dataVis2018Id, challenges), - [Certification.JsAlgoDataStructNew]: getCertById( - certIds.jsAlgoDataStructV8Id, - challenges - ), - [Certification.BackEndDevApis]: getCertById( - certIds.apisMicroservicesId, - challenges - ), - [Certification.QualityAssurance]: getCertById(certIds.qaV7Id, challenges), - [Certification.InfoSec]: getCertById(certIds.infosecV7Id, challenges), - [Certification.SciCompPy]: getCertById(certIds.sciCompPyV7Id, challenges), - [Certification.DataAnalysisPy]: getCertById( - certIds.dataAnalysisPyV7Id, - challenges - ), - [Certification.MachineLearningPy]: getCertById( - certIds.machineLearningPyV7Id, - challenges - ), - [Certification.RelationalDb]: getCertById( - certIds.relationalDatabaseV8Id, - challenges - ), - [Certification.CollegeAlgebraPy]: getCertById( - certIds.collegeAlgebraPyV8Id, - challenges - ), - [Certification.FoundationalCSharp]: getCertById( - certIds.foundationalCSharpV8Id, - challenges - ), - - [Certification.JsV9]: getCertById(certIds.javascriptV9Id, challenges), - [Certification.RespWebDesignV9]: getCertById( - certIds.respWebDesignV9Id, - challenges - ), - [Certification.A2English]: getCertById(certIds.a2EnglishId, challenges), - - // upcoming - [Certification.FrontEndDevLibsV9]: getCertById( - certIds.frontEndLibsV9Id, - challenges - ), - [Certification.PythonV9]: getCertById(certIds.pythonV9Id, challenges), - [Certification.RelationalDbV9]: getCertById( - certIds.relationalDbV9Id, - challenges - ), - [Certification.BackEndDevApisV9]: getCertById( - certIds.backEndDevApisV9Id, - challenges - ), - [Certification.FullStackDeveloperV9]: getCertById( - certIds.fullStackDeveloperV9Id, - challenges - ), - [Certification.B1English]: getCertById(certIds.b1EnglishId, challenges), - [Certification.A2Spanish]: getCertById(certIds.a2SpanishId, challenges), - [Certification.A1Chinese]: getCertById(certIds.a1ChineseId, challenges), - [Certification.A2Chinese]: getCertById(certIds.a2ChineseId, challenges) - }; + for (const cert of Object.values(Certification)) { + certLookup[cert] = getCertBySlug(cert, challenges); + } + return certLookup; } interface CertI { diff --git a/api/src/routes/public/certificate.ts b/api/src/routes/public/certificate.ts index a3dfc10979e..d76a59ccb0f 100644 --- a/api/src/routes/public/certificate.ts +++ b/api/src/routes/public/certificate.ts @@ -5,7 +5,7 @@ import * as schemas from '../../schemas.js'; import { certSlugTypeMap, certToTitleMap, - certTypeIdMap, + certToIdMap, completionHours, oldDataVizId } from '../../../../shared/config/certification-settings.js'; @@ -52,7 +52,7 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = ( } const certType = certSlugTypeMap[certSlug]; - const certId = certTypeIdMap[certType]; + const certId = certToIdMap[certSlug]; const certTitle = certToTitleMap[certSlug]; const completionTime = completionHours[certType] || 300; const user = await fastify.prisma.user.findFirst({ diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js index 76c5832935e..f8ebf9fbbc0 100644 --- a/client/src/redux/selectors.js +++ b/client/src/redux/selectors.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import { liveCerts } from '../../config/cert-and-project-map'; import { - certTypeIdMap, + certSlugTypeMap, certToTitleMap } from '../../../shared-dist/config/certification-settings.js'; @@ -231,9 +231,9 @@ export const claimableCertsSelector = createSelector([userSelector], user => { ({ id }) => id ); - const isClaimedById = Object.entries(certTypeIdMap).reduce( - (acc, [userFlag, certId]) => { - acc[certId] = Boolean(user[userFlag]); + const isClaimedByCert = Object.entries(certSlugTypeMap).reduce( + (acc, [cert, userFlag]) => { + acc[cert] = Boolean(user[userFlag]); return acc; }, {} @@ -241,9 +241,9 @@ export const claimableCertsSelector = createSelector([userSelector], user => { const claimable = []; - for (const { id, projects, certSlug } of liveCerts) { + for (const { projects, certSlug } of liveCerts) { if (!projects) continue; - if (isClaimedById[id]) continue; + if (isClaimedByCert[certSlug]) continue; const projectIds = projects.map(p => p.id); const allProjectsComplete = projectIds.every(id => diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 33f165dc33a..8fee93ee4d4 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -10,10 +10,7 @@ import { import store from 'store'; import { navigate } from 'gatsby'; -import { - certTypeIdMap, - certTypes -} from '../../../../shared-dist/config/certification-settings'; +import { Certification } from '../../../../shared-dist/config/certification-settings'; import { createFlashMessage } from '../../components/Flash/redux'; import { liveCerts } from '../../../config/cert-and-project-map'; import { @@ -191,7 +188,7 @@ function* verifyCertificationSaga({ payload }) { // (20/06/2022) Full Stack client-side validation is already done here: // https://github.com/freeCodeCamp/freeCodeCamp/blob/main/client/src/components/settings/certification.js#L309 - if (currentCert?.id !== certTypeIdMap[certTypes.fullStack]) { + if (currentCert?.certSlug !== Certification.LegacyFullStack) { const flash = { type: 'info', message: 'flash.incomplete-steps', diff --git a/shared/config/certification-settings.test.ts b/shared/config/certification-settings.test.ts index f1dd690d79c..f5d274ef86e 100644 --- a/shared/config/certification-settings.test.ts +++ b/shared/config/certification-settings.test.ts @@ -3,7 +3,7 @@ import { Certification, linkedInCredentialIds, certToTitleMap, - certIds + certToIdMap } from './certification-settings'; describe('linkedInCredentialIds', () => { @@ -24,9 +24,9 @@ describe('certToTitleMap', () => { }); }); -describe('certIds', () => { +describe('certToIdMap', () => { it('should have no duplicate values', () => { - const ids = Object.values(certIds).sort(); + const ids = Object.values(certToIdMap).sort(); const uniqueIds = Array.from(new Set(ids)).sort(); expect(uniqueIds).toEqual(ids); diff --git a/shared/config/certification-settings.ts b/shared/config/certification-settings.ts index 5c611f2c7a7..a955c684e94 100644 --- a/shared/config/certification-settings.ts +++ b/shared/config/certification-settings.ts @@ -127,38 +127,43 @@ export const certTypes = { a2English: 'isA2EnglishCert' } as const; -export const certIds = { - legacyFrontEndChallengeId: '561add10cb82ac38a17513be', - legacyBackEndChallengeId: '660add10cb82ac38a17513be', - legacyDataVisId: '561add10cb82ac39a17513bc', - legacyInfosecQaId: '561add10cb82ac38a17213bc', - legacyFullStackId: '561add10cb82ac38a17213bd', - respWebDesignId: '561add10cb82ac38a17513bc', - frontEndDevLibsId: '561acd10cb82ac38a17513bc', - dataVis2018Id: '5a553ca864b52e1d8bceea14', - jsAlgoDataStructId: '561abd10cb81ac38a17513bc', - apisMicroservicesId: '561add10cb82ac38a17523bc', - qaV7Id: '5e611829481575a52dc59c0e', - infosecV7Id: '5e6021435ac9d0ecd8b94b00', - sciCompPyV7Id: '5e44431b903586ffb414c951', - dataAnalysisPyV7Id: '5e46fc95ac417301a38fb934', - machineLearningPyV7Id: '5e46fc95ac417301a38fb935', - relationalDatabaseV8Id: '606243f50267e718b1e755f4', - collegeAlgebraPyV8Id: '61531b20cc9dfa2741a5b800', - foundationalCSharpV8Id: '647f7da207d29547b3bee1ba', - jsAlgoDataStructV8Id: '658180220947283cdc0689ce', - respWebDesignV9Id: '68db314d3c11a8bff07c7535', - javascriptV9Id: '68c4069c1ef859270e17c495', - frontEndLibsV9Id: '68e008aa5f80c6099d47b3a2', - pythonV9Id: '68e6bd5020effa1586e79855', - relationalDbV9Id: '68e6bd5120effa1586e79856', - backEndDevApisV9Id: '68e6bd5120effa1586e79857', - fullStackDeveloperV9Id: '64514fda6c245de4d11eb7bb', - a2EnglishId: '651dd7e01d697d0aab7833b7', - b1EnglishId: '66607e53317411dd5e8aae21', - a2SpanishId: '681a6b22e5a782fe3459984a', - a1ChineseId: '68f1268149f045a650d4229e', - a2ChineseId: '682c3153086dd7cabe7f48bc' +export const certToIdMap: Record = { + // Legacy certifications + [Certification.LegacyFrontEnd]: '561add10cb82ac38a17513be', + [Certification.JsAlgoDataStruct]: '561abd10cb81ac38a17513bc', + [Certification.LegacyBackEnd]: '660add10cb82ac38a17513be', + [Certification.LegacyDataVis]: '561add10cb82ac39a17513bc', + [Certification.LegacyInfoSecQa]: '561add10cb82ac38a17213bc', + [Certification.LegacyFullStack]: '561add10cb82ac38a17213bd', + + // Current certifications + [Certification.RespWebDesign]: '561add10cb82ac38a17513bc', + [Certification.JsAlgoDataStructNew]: '658180220947283cdc0689ce', + [Certification.FrontEndDevLibs]: '561acd10cb82ac38a17513bc', + [Certification.DataVis]: '5a553ca864b52e1d8bceea14', + [Certification.BackEndDevApis]: '561add10cb82ac38a17523bc', + [Certification.QualityAssurance]: '5e611829481575a52dc59c0e', + [Certification.InfoSec]: '5e6021435ac9d0ecd8b94b00', + [Certification.SciCompPy]: '5e44431b903586ffb414c951', + [Certification.DataAnalysisPy]: '5e46fc95ac417301a38fb934', + [Certification.MachineLearningPy]: '5e46fc95ac417301a38fb935', + [Certification.RelationalDb]: '606243f50267e718b1e755f4', + [Certification.CollegeAlgebraPy]: '61531b20cc9dfa2741a5b800', + [Certification.FoundationalCSharp]: '647f7da207d29547b3bee1ba', + [Certification.A2English]: '651dd7e01d697d0aab7833b7', + + // Upcoming certifications + [Certification.RespWebDesignV9]: '68db314d3c11a8bff07c7535', + [Certification.JsV9]: '68c4069c1ef859270e17c495', + [Certification.FrontEndDevLibsV9]: '68e008aa5f80c6099d47b3a2', + [Certification.PythonV9]: '68e6bd5020effa1586e79855', + [Certification.RelationalDbV9]: '68e6bd5120effa1586e79856', + [Certification.BackEndDevApisV9]: '68e6bd5120effa1586e79857', + [Certification.FullStackDeveloperV9]: '64514fda6c245de4d11eb7bb', + [Certification.B1English]: '66607e53317411dd5e8aae21', + [Certification.A2Spanish]: '681a6b22e5a782fe3459984a', + [Certification.A2Chinese]: '682c3153086dd7cabe7f48bc', + [Certification.A1Chinese]: '68f1268149f045a650d4229e' }; export const completionHours = { @@ -248,31 +253,6 @@ export const superBlockCertTypeMap = { [SuperBlocks.A2English]: certTypes.a2English }; -export const certTypeIdMap = { - [certTypes.frontEnd]: certIds.legacyFrontEndChallengeId, - [certTypes.backEnd]: certIds.legacyBackEndChallengeId, - [certTypes.dataVis]: certIds.legacyDataVisId, - [certTypes.infosecQa]: certIds.legacyInfosecQaId, - [certTypes.fullStack]: certIds.legacyFullStackId, - [certTypes.respWebDesign]: certIds.respWebDesignId, - [certTypes.respWebDesignV9]: certIds.respWebDesignV9Id, - [certTypes.frontEndDevLibs]: certIds.frontEndDevLibsId, - [certTypes.jsAlgoDataStruct]: certIds.jsAlgoDataStructId, - [certTypes.dataVis2018]: certIds.dataVis2018Id, - [certTypes.apisMicroservices]: certIds.apisMicroservicesId, - [certTypes.qaV7]: certIds.qaV7Id, - [certTypes.infosecV7]: certIds.infosecV7Id, - [certTypes.sciCompPyV7]: certIds.sciCompPyV7Id, - [certTypes.dataAnalysisPyV7]: certIds.dataAnalysisPyV7Id, - [certTypes.machineLearningPyV7]: certIds.machineLearningPyV7Id, - [certTypes.relationalDatabaseV8]: certIds.relationalDatabaseV8Id, - [certTypes.collegeAlgebraPyV8]: certIds.collegeAlgebraPyV8Id, - [certTypes.foundationalCSharpV8]: certIds.foundationalCSharpV8Id, - [certTypes.jsAlgoDataStructV8]: certIds.jsAlgoDataStructV8Id, - [certTypes.javascriptV9]: certIds.javascriptV9Id, - [certTypes.a2English]: certIds.a2EnglishId -}; - // TODO: use i18n keys instead of hardcoded titles export const certToTitleMap: Record = { // Legacy certifications