mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): get certslug route (#50515)
Co-authored-by: Sboonny <muhammed@freecodecamp.org> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
+6
-2
@@ -30,6 +30,10 @@ import sessionAuth from './plugins/session-auth';
|
||||
import codeFlowAuth from './plugins/code-flow-auth';
|
||||
import { mobileAuth0Routes } from './routes/auth';
|
||||
import { devAuthRoutes } from './routes/auth-dev';
|
||||
import {
|
||||
protectedCertificateRoutes,
|
||||
unprotectedCertificateRoutes
|
||||
} from './routes/certificate';
|
||||
import { challengeRoutes } from './routes/challenge';
|
||||
import { deprecatedEndpoints } from './routes/deprecated-endpoints';
|
||||
import { unsubscribeDeprecated } from './routes/deprecated-unsubscribe';
|
||||
@@ -49,7 +53,6 @@ import {
|
||||
SESSION_SECRET
|
||||
} from './utils/env';
|
||||
import { isObjectID } from './utils/validation';
|
||||
import { certificateRoutes } from './routes/certificate';
|
||||
|
||||
export type FastifyInstanceWithTypeProvider = FastifyInstance<
|
||||
RawServerDefault,
|
||||
@@ -199,11 +202,12 @@ export const build = async (
|
||||
if (FCC_ENABLE_DEV_LOGIN_MODE) {
|
||||
void fastify.register(devAuthRoutes);
|
||||
}
|
||||
void fastify.register(certificateRoutes);
|
||||
void fastify.register(challengeRoutes);
|
||||
void fastify.register(settingRoutes);
|
||||
void fastify.register(donateRoutes);
|
||||
void fastify.register(userRoutes);
|
||||
void fastify.register(protectedCertificateRoutes);
|
||||
void fastify.register(unprotectedCertificateRoutes);
|
||||
void fastify.register(userGetRoutes);
|
||||
void fastify.register(deprecatedEndpoints);
|
||||
void fastify.register(statusRoute);
|
||||
|
||||
@@ -434,4 +434,198 @@ describe('certificate routes', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unauthenticated user', () => {
|
||||
describe('GET /certificate/showCert/:username/:certSlug', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: defaultUserEmail },
|
||||
data: {
|
||||
username: 'foobar',
|
||||
name: 'foobar',
|
||||
isHonest: true,
|
||||
isBanned: false,
|
||||
isCheater: false,
|
||||
profileUI: { isLocked: false, showCerts: true, showTimeLine: true }
|
||||
}
|
||||
});
|
||||
});
|
||||
test('should return user not found if the user cannot be found', async () => {
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/not-a-valid-user-name/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.username-not-found',
|
||||
variables: { username: 'not-a-valid-user-name' }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should ask user to add name if there is no name', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: { name: null }
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.add-name'
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should return not eligible if user is banned', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: { isBanned: true }
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.not-eligible'
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should return not eligible if user is cheater', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: { isCheater: true }
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.not-eligible'
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should return not honest if user is not honest', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: { isHonest: false }
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.not-honest',
|
||||
variables: { username: 'foobar' }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should return profile private if profile is private', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: {
|
||||
// All properties need to be defined, as this op SETs `profileUI`
|
||||
profileUI: { isLocked: true, showTimeLine: true, showCerts: true }
|
||||
}
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.profile-private',
|
||||
variables: { username: 'foobar' }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should return certs private if certs are private', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: {
|
||||
profileUI: { showCerts: false, showTimeLine: true, isLocked: false }
|
||||
}
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.certs-private',
|
||||
variables: { username: 'foobar' }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
test('should return timeline private if timeline is private', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: {
|
||||
profileUI: { showTimeLine: false, showCerts: true, isLocked: false }
|
||||
}
|
||||
});
|
||||
const response = await superRequest(
|
||||
'/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.timeline-private',
|
||||
variables: { username: 'foobar' }
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { find } from 'lodash';
|
||||
import { CompletedChallenge } from '@prisma/client';
|
||||
import { schemas } from '../schemas';
|
||||
import { getChallenges } from '../utils/get-challenges';
|
||||
import {
|
||||
@@ -10,11 +12,14 @@ import {
|
||||
currentCertifications,
|
||||
legacyCertifications,
|
||||
legacyFullStackCertification,
|
||||
certTypeIdMap,
|
||||
completionHours,
|
||||
oldDataVizId,
|
||||
upcomingCertifications
|
||||
} from '../../../shared/config/certification-settings';
|
||||
import { normalizeChallenges, removeNulls } from '../utils/normalize';
|
||||
import { CompletedChallenge } from '../utils/common-challenge-functions';
|
||||
import { SHOW_UPCOMING_CHANGES } from '../utils/env';
|
||||
import { formatCertificationValidation } from '../utils/error-formatting';
|
||||
|
||||
const {
|
||||
legacyFrontEndChallengeId,
|
||||
@@ -40,13 +45,13 @@ const {
|
||||
} = certIds;
|
||||
|
||||
/**
|
||||
* Plugin for the certificate endpoints.
|
||||
* Plugin for the protected certificate endpoints.
|
||||
*
|
||||
* @param fastify The Fastify instance.
|
||||
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`.
|
||||
* @param done The callback to signal that the plugin is ready.
|
||||
*/
|
||||
export const certificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
export const protectedCertificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
fastify,
|
||||
_options,
|
||||
done
|
||||
@@ -270,6 +275,244 @@ export const certificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
done();
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin for the unprotected certificate endpoints.
|
||||
*
|
||||
* @param fastify The Fastify instance.
|
||||
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`.
|
||||
* @param done The callback to signal that the plugin is ready.
|
||||
*/
|
||||
export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
fastify,
|
||||
_options,
|
||||
done
|
||||
) => {
|
||||
fastify.get(
|
||||
'/certificate/showCert/:username/:certSlug',
|
||||
{
|
||||
schema: schemas.certSlug,
|
||||
errorHandler(error, request, reply) {
|
||||
if (error.validation) {
|
||||
void reply.code(400);
|
||||
return formatCertificationValidation(error.validation);
|
||||
} else {
|
||||
fastify.errorHandler(error, request, reply);
|
||||
}
|
||||
}
|
||||
},
|
||||
async (req, reply) => {
|
||||
try {
|
||||
let username = req.params.username;
|
||||
const certSlug = req.params.certSlug;
|
||||
|
||||
username = username.toLowerCase();
|
||||
fastify.log.info(`certSlug: ${certSlug}`);
|
||||
|
||||
if (!assertCertSlugIsKeyofCertSlugTypeMap(certSlug)) {
|
||||
void reply.code(404);
|
||||
return reply.send({
|
||||
type: 'info',
|
||||
message: 'flash.cert-not-found',
|
||||
variables: { certSlug }
|
||||
});
|
||||
}
|
||||
|
||||
const certType = certSlugTypeMap[certSlug];
|
||||
const certId = certTypeIdMap[certType];
|
||||
const certTitle = certTypeTitleMap[certType];
|
||||
const completionTime = completionHours[certType] || 300;
|
||||
const user = await fastify.prisma.user.findFirst({
|
||||
where: { username },
|
||||
select: {
|
||||
isBanned: true,
|
||||
isCheater: true,
|
||||
isFrontEndCert: true,
|
||||
isBackEndCert: true,
|
||||
isFullStackCert: true,
|
||||
isRespWebDesignCert: true,
|
||||
isFrontEndLibsCert: true,
|
||||
isJsAlgoDataStructCert: true,
|
||||
isJsAlgoDataStructCertV8: true,
|
||||
isDataVisCert: true,
|
||||
is2018DataVisCert: true,
|
||||
isApisMicroservicesCert: true,
|
||||
isInfosecQaCert: true,
|
||||
isQaCertV7: true,
|
||||
isInfosecCertV7: true,
|
||||
isSciCompPyCertV7: true,
|
||||
isDataAnalysisPyCertV7: true,
|
||||
isMachineLearningPyCertV7: true,
|
||||
isRelationalDatabaseCertV8: true,
|
||||
isCollegeAlgebraPyCertV8: true,
|
||||
isFoundationalCSharpCertV8: true,
|
||||
isUpcomingPythonCertV8: true,
|
||||
isHonest: true,
|
||||
username: true,
|
||||
name: true,
|
||||
completedChallenges: true,
|
||||
profileUI: true
|
||||
}
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.username-not-found',
|
||||
variables: { username }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (user.isCheater || user.isBanned) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.not-eligible'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.isHonest) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.not-honest',
|
||||
variables: { username }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (user.profileUI?.isLocked) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.profile-private',
|
||||
variables: { username }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.name) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.add-name'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.profileUI?.showCerts) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.certs-private',
|
||||
variables: { username }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.profileUI?.showTimeLine) {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.timeline-private',
|
||||
variables: { username }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (user[certType]) {
|
||||
const { completedChallenges } = user;
|
||||
const certChallenge = find(
|
||||
completedChallenges,
|
||||
({ id }) => certId === id
|
||||
);
|
||||
|
||||
let { completedDate = Date.now() } = certChallenge || {};
|
||||
|
||||
// the challenge id has been rotated for isDataVisCert
|
||||
if (certType === 'isDataVisCert' && !certChallenge) {
|
||||
const oldDataVisIdChall = find(
|
||||
completedChallenges,
|
||||
({ id }) => oldDataVizId === id
|
||||
);
|
||||
|
||||
if (oldDataVisIdChall) {
|
||||
completedDate = oldDataVisIdChall.completedDate || completedDate;
|
||||
}
|
||||
}
|
||||
|
||||
// if fullcert is not found, return the latest completedDate
|
||||
if (certType === 'isFullStackCert' && !certChallenge) {
|
||||
completedDate = getFallbackFullStackDate(
|
||||
completedChallenges,
|
||||
completedDate
|
||||
);
|
||||
}
|
||||
|
||||
const { username, name } = user;
|
||||
|
||||
if (!user.profileUI.showName) {
|
||||
void reply.code(200);
|
||||
return reply.send({
|
||||
certSlug,
|
||||
certTitle,
|
||||
username,
|
||||
date: completedDate,
|
||||
completionTime
|
||||
});
|
||||
}
|
||||
|
||||
void reply.code(200);
|
||||
return reply.send({
|
||||
certSlug,
|
||||
certTitle,
|
||||
username,
|
||||
name,
|
||||
date: completedDate,
|
||||
completionTime
|
||||
});
|
||||
} else {
|
||||
return reply.send({
|
||||
messages: [
|
||||
{
|
||||
type: 'info',
|
||||
message: 'flash.user-not-certified',
|
||||
variables: { username, cert: certTypeTitleMap[certType] }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
void reply.code(500);
|
||||
return reply.send({
|
||||
message:
|
||||
'Oops! Something went wrong. Please try again in a moment or contact support@freecodecamp.org if the error persists.',
|
||||
type: 'danger'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
function isCertAllowed(certSlug: string): boolean {
|
||||
if (
|
||||
currentCertifications.includes(certSlug) ||
|
||||
@@ -467,3 +710,23 @@ function getUserIsCertMap(user: CertI) {
|
||||
isUpcomingPythonCertV8
|
||||
};
|
||||
}
|
||||
|
||||
function getFallbackFullStackDate(
|
||||
completedChallenges: CompletedChallenge[],
|
||||
completedDate: number
|
||||
) {
|
||||
const chalIds = [
|
||||
respWebDesignId,
|
||||
jsAlgoDataStructId,
|
||||
frontEndDevLibsId,
|
||||
dataVis2018Id,
|
||||
apisMicroservicesId,
|
||||
legacyInfosecQaId
|
||||
];
|
||||
|
||||
const latestCertDate = completedChallenges
|
||||
.filter(chal => chalIds.includes(chal.id))
|
||||
.sort((a, b) => b.completedDate - a.completedDate)[0]?.completedDate;
|
||||
|
||||
return latestCertDate ? latestCertDate : completedDate;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
import { Certification } from '../../shared/config/certification-settings';
|
||||
// import type { certTypes } from '../../shared/config/certification-settings';
|
||||
|
||||
// type CertTypes = keyof typeof certTypes;
|
||||
|
||||
const generic500 = Type.Object({
|
||||
message: Type.Literal(
|
||||
@@ -785,6 +789,133 @@ export const schemas = {
|
||||
})
|
||||
}
|
||||
},
|
||||
// certification
|
||||
certSlug: {
|
||||
params: Type.Object({
|
||||
certSlug: Type.String(),
|
||||
username: Type.String()
|
||||
}),
|
||||
response: {
|
||||
// TODO(POST_MVP): Most of these should not be 200s
|
||||
200: Type.Union([
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.username-not-found'),
|
||||
variables: Type.Object({
|
||||
username: Type.String()
|
||||
})
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.not-eligible')
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.not-honest'),
|
||||
variables: Type.Object({
|
||||
username: Type.String()
|
||||
})
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.profile-private'),
|
||||
variables: Type.Object({
|
||||
username: Type.String()
|
||||
})
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.add-name')
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.certs-private'),
|
||||
variables: Type.Object({
|
||||
username: Type.String()
|
||||
})
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.timeline-private'),
|
||||
variables: Type.Object({
|
||||
username: Type.String()
|
||||
})
|
||||
})
|
||||
)
|
||||
}),
|
||||
Type.Object({
|
||||
certSlug: Type.Enum(Certification),
|
||||
certTitle: Type.String(),
|
||||
username: Type.String(),
|
||||
date: Type.Number(),
|
||||
completionTime: Type.Number()
|
||||
}),
|
||||
Type.Object({
|
||||
certSlug: Type.Enum(Certification),
|
||||
certTitle: Type.String(),
|
||||
username: Type.String(),
|
||||
name: Type.String(),
|
||||
date: Type.Number(),
|
||||
completionTime: Type.Number()
|
||||
}),
|
||||
Type.Object({
|
||||
messages: Type.Array(
|
||||
Type.Object({
|
||||
type: Type.Literal('info'),
|
||||
message: Type.Literal('flash.user-not-certified'),
|
||||
variables: Type.Object({
|
||||
username: Type.String(),
|
||||
cert: Type.String()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
]),
|
||||
400: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.String()
|
||||
}),
|
||||
404: Type.Object({
|
||||
message: Type.Literal('flash.cert-not-found'),
|
||||
type: Type.Literal('info'),
|
||||
variables: Type.Object({
|
||||
certSlug: Type.String()
|
||||
})
|
||||
}),
|
||||
500: Type.Object({
|
||||
type: Type.Literal('danger'),
|
||||
message: Type.Literal(
|
||||
'Oops! Something went wrong. Please try again in a moment or contact support@freecodecamp.org if the error persists.'
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
postMsUsername: {
|
||||
body: Type.Object({
|
||||
msTranscriptUrl: Type.String({ maxLength: 1000 })
|
||||
|
||||
@@ -59,6 +59,7 @@ type CompletedChallengeFile = {
|
||||
path?: string | null;
|
||||
};
|
||||
|
||||
// TODO: Should probably prefer `import{CompletedChallenge}from'@prisma/client'` instead of defining it here
|
||||
export type CompletedChallenge = {
|
||||
id: string;
|
||||
solution?: string | null;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { ErrorObject } from 'ajv';
|
||||
import { certTypes } from '../../../shared/config/certification-settings';
|
||||
|
||||
type CertLogs = (typeof certTypes)[keyof typeof certTypes];
|
||||
|
||||
type FormattedError = {
|
||||
type: 'error';
|
||||
@@ -47,6 +50,30 @@ export const formatProjectCompletedValidation = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format validation errors for /project-completed.
|
||||
*
|
||||
* @param errors An array of validation errors.
|
||||
* @returns Formatted errors that can be used in the response.
|
||||
*/
|
||||
export const formatCertificationValidation = (
|
||||
errors: ErrorObject[]
|
||||
): FormattedError => {
|
||||
const error = getError(errors);
|
||||
|
||||
return error.instancePath === '' &&
|
||||
Object.values(certTypes).includes(error.params.missingProperty as CertLogs)
|
||||
? ({
|
||||
type: 'error',
|
||||
message:
|
||||
'You have not provided the valid param for us to display the certification.'
|
||||
} as const)
|
||||
: ({
|
||||
type: 'error',
|
||||
message: 'That does not appear to be a valid certification request.'
|
||||
} as const);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format validation errors for /coderoad-challenge-completed.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user