diff --git a/api/src/routes/challenge.test.ts b/api/src/routes/challenge.test.ts index 239c023695d..1d999f0b008 100644 --- a/api/src/routes/challenge.test.ts +++ b/api/src/routes/challenge.test.ts @@ -362,7 +362,33 @@ describe('challengeRoutes', () => { expect(response.body).toStrictEqual( isValidChallengeCompletionErrorMsg ); - expect(response.statusCode).toBe(400); + expect(response.statusCode).toBe(403); + }); + + it('POST rejects backendProject requests without URL githubLinks', async () => { + const response = await superPost('/project-completed').send({ + id: id1, + challengeType: challengeTypes.backEndProject, + // Solution is allowed to be localhost for backEndProject + solution: 'http://localhost:3000' + }); + + expect(response.body).toStrictEqual( + isValidChallengeCompletionErrorMsg + ); + expect(response.statusCode).toBe(403); + + const response_2 = await superPost('/project-completed').send({ + id: id1, + challengeType: challengeTypes.backEndProject, + solution: 'http://localhost:3000', + githubLink: 'not-a-valid-url' + }); + + expect(response_2.body).toStrictEqual( + isValidChallengeCompletionErrorMsg + ); + expect(response_2.statusCode).toBe(403); }); it('POST rejects CodeRoad/CodeAlly projects when the user has not completed the required challenges', async () => { diff --git a/api/src/routes/challenge.ts b/api/src/routes/challenge.ts index 7133b0ac3ee..52e9fa1dd44 100644 --- a/api/src/routes/challenge.ts +++ b/api/src/routes/challenge.ts @@ -2,6 +2,7 @@ import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebo import jwt from 'jsonwebtoken'; import { uniqBy } from 'lodash'; import { CompletedExam, ExamResults } from '@prisma/client'; +import isURL from 'validator/lib/isURL'; import { challengeTypes } from '../../../shared/config/challenge-types'; import { schemas } from '../schemas'; @@ -192,9 +193,28 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = ( } }, async (req, reply) => { + // TODO: considering validation is determined by `challengeType`, it should not come from the client + // Determine `challengeType` by `id` const { id: projectId, challengeType, solution, githubLink } = req.body; const userId = req.user?.id; + // If `backEndProject`: + // - `solution` needs to exist, but does not have to be valid URL + // - `githubLink` needs to exist and be valid URL + if (challengeType === challengeTypes.backEndProject) { + if (!solution || !isURL(githubLink + '')) { + return void reply.code(403).send({ + type: 'error', + message: 'That does not appear to be a valid challenge submission.' + }); + } + } else if (solution && !isURL(solution + '')) { + return void reply.code(403).send({ + type: 'error', + message: 'That does not appear to be a valid challenge submission.' + }); + } + try { const user = await fastify.prisma.user.findUniqueOrThrow({ where: { id: userId }, @@ -221,6 +241,7 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = ( const oldChallenge = user.completedChallenges?.find( ({ id }) => id === projectId ); + const updatedChallenge = { challengeType, solution, diff --git a/api/src/schemas.ts b/api/src/schemas.ts index 2b615003390..a108834b404 100644 --- a/api/src/schemas.ts +++ b/api/src/schemas.ts @@ -526,8 +526,8 @@ export const schemas = { body: Type.Object({ id: Type.String({ format: 'objectid', maxLength: 24, minLength: 24 }), challengeType: Type.Optional(Type.Number()), - solution: Type.String({ format: 'url', maxLength: 1024 }), - // TODO(Post-MVP): require format: 'url' for githubLink + // The solution must be a valid URL only if it is a `backEndProject`. + solution: Type.String({ maxLength: 1024 }), githubLink: Type.Optional(Type.String()) }), response: { @@ -551,9 +551,14 @@ export const schemas = { }), 403: Type.Object({ type: Type.Literal('error'), - message: Type.Literal( - 'You have to complete the project before you can submit a URL.' - ) + message: Type.Union([ + Type.Literal( + 'You have to complete the project before you can submit a URL.' + ), + Type.Literal( + 'That does not appear to be a valid challenge submission.' + ) + ]) }), 500: Type.Object({ message: Type.Literal(