From 08bc09063631229540e41a2fd17a83e9f1610263 Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Thu, 24 Oct 2024 02:49:58 +0200 Subject: [PATCH] feat(api): add prerequisites to env exam (#56731) Co-authored-by: Oliver Eyton-Williams --- api/__mocks__/env-exam.ts | 3 +- api/prisma/schema.prisma | 8 +++-- .../routes/exam-environment.test.ts | 35 +++++++++++++++++-- .../routes/exam-environment.ts | 7 ++-- api/src/exam-environment/utils/exam.test.ts | 23 ++++++++++-- api/src/exam-environment/utils/exam.ts | 31 ++++++++++------ .../645147516c245de4d11eb7ba.md | 6 ++-- 7 files changed, 88 insertions(+), 25 deletions(-) diff --git a/api/__mocks__/env-exam.ts b/api/__mocks__/env-exam.ts index 7f690c2cc76..e27d61445cb 100644 --- a/api/__mocks__/env-exam.ts +++ b/api/__mocks__/env-exam.ts @@ -338,7 +338,8 @@ export const examAttemptSansSubmissionTimeInMS: Static< export const exam: EnvExam = { id: examId, config, - questionSets + questionSets, + prerequisites: ['67112fe1c994faa2c26d0b1d'] }; export async function seedEnvExam() { diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 4d823ea6af9..e9022dcd226 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -151,11 +151,13 @@ model user { /// An exam for the Exam Environment App as designed by the examiners model EnvExam { /// Globally unique exam id - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId /// All questions for a given exam - questionSets EnvQuestionSet[] + questionSets EnvQuestionSet[] /// Configuration for exam metadata - config EnvConfig + config EnvConfig + /// ObjectIds for required challenges/blocks to take the exam + prerequisites String[] @db.ObjectId // Relations generatedExams EnvGeneratedExam[] diff --git a/api/src/exam-environment/routes/exam-environment.test.ts b/api/src/exam-environment/routes/exam-environment.test.ts index d9532617f7e..d235046baf7 100644 --- a/api/src/exam-environment/routes/exam-environment.test.ts +++ b/api/src/exam-environment/routes/exam-environment.test.ts @@ -240,6 +240,15 @@ describe('/exam-environment/', () => { describe('POST /exam-environment/generated-exam', () => { afterEach(async () => { await fastifyTestInstance.prisma.envExamAttempt.deleteMany(); + // Add prerequisite id to user completed challenge + await fastifyTestInstance.prisma.user.update({ + where: { id: defaultUserId }, + data: { + completedChallenges: [ + { id: mock.exam.prerequisites.at(0)!, completedDate: Date.now() } + ] + } + }); await mock.seedEnvExam(); }); @@ -262,8 +271,30 @@ describe('/exam-environment/', () => { expect(res.status).toBe(404); }); - xit('should return an error if the exam prerequisites are not met', async () => { - // TODO: Waiting on prerequisites + it('should return an error if the exam prerequisites are not met', async () => { + await fastifyTestInstance.prisma.user.update({ + where: { id: defaultUserId }, + data: { + completedChallenges: [] + } + }); + + const body: Static = { + examId: mock.exam.id + }; + const res = await superPost('/exam-environment/exam/generated-exam') + .send(body) + .set( + 'exam-environment-authorization-token', + examEnvironmentAuthorizationToken + ); + + expect(res.body).toStrictEqual({ + code: 'FCC_EINVAL_EXAM_ENVIRONMENT_PREREQUISITES', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + message: expect.any(String) + }); + expect(res.status).toBe(403); }); it('should return an error if the exam has been attempted in the last 24 hours', async () => { diff --git a/api/src/exam-environment/routes/exam-environment.ts b/api/src/exam-environment/routes/exam-environment.ts index 5dfac214030..aa6832b48ab 100644 --- a/api/src/exam-environment/routes/exam-environment.ts +++ b/api/src/exam-environment/routes/exam-environment.ts @@ -170,7 +170,7 @@ async function postExamGeneratedExamHandler( // Check user has completed prerequisites const user = req.user!; - const isExamPrerequisitesMet = checkPrerequisites(user, true); + const isExamPrerequisitesMet = checkPrerequisites(user, exam.prerequisites); if (!isExamPrerequisitesMet) { void reply.code(403); @@ -582,12 +582,13 @@ async function getExams( const exams = await this.prisma.envExam.findMany({ select: { id: true, - config: true + config: true, + prerequisites: true } }); const availableExams = exams.map(exam => { - const isExamPrerequisitesMet = checkPrerequisites(user, true); + const isExamPrerequisitesMet = checkPrerequisites(user, exam.prerequisites); return { id: exam.id, diff --git a/api/src/exam-environment/utils/exam.test.ts b/api/src/exam-environment/utils/exam.test.ts index db223bfe137..76b5a63261d 100644 --- a/api/src/exam-environment/utils/exam.test.ts +++ b/api/src/exam-environment/utils/exam.test.ts @@ -3,6 +3,7 @@ import { exam, examAttempt, generatedExam } from '../../../__mocks__/env-exam'; import * as schemas from '../schemas'; import { checkAttemptAgainstGeneratedExam, + checkPrerequisites, constructUserExam, generateExam, userAttemptToDatabaseAttemptQuestionSets, @@ -56,9 +57,27 @@ describe('Exam Environment', () => { ).toBe(false); }); }); - xdescribe('checkPrequisites()', () => { - // TODO: Awaiting implementation + + describe('checkPrequisites()', () => { + it("should return true if all items in the second argument exist in the first argument's `.completedChallenges[].id`", () => { + const user = { + completedChallenges: [{ id: '1' }, { id: '2' }] + }; + const prerequisites = ['1', '2']; + + expect(checkPrerequisites(user, prerequisites)).toBe(true); + }); + + it("should return false if any items in the second argument do not exist in the first argument's `.completedChallenges[].id`", () => { + const user = { + completedChallenges: [{ id: '2' }] + }; + const prerequisites = ['1', '2']; + + expect(checkPrerequisites(user, prerequisites)).toBe(false); + }); }); + describe('constructUserExam()', () => { it('should not provide the answers', () => { const userExam = constructUserExam(generatedExam, exam); diff --git a/api/src/exam-environment/utils/exam.ts b/api/src/exam-environment/utils/exam.ts index d6e8b92da3c..3d207cddb24 100644 --- a/api/src/exam-environment/utils/exam.ts +++ b/api/src/exam-environment/utils/exam.ts @@ -7,22 +7,33 @@ import { EnvGeneratedExam, EnvMultipleChoiceQuestion, EnvQuestionSet, - EnvQuestionSetAttempt, - user + EnvQuestionSetAttempt } from '@prisma/client'; import { type Static } from '@fastify/type-provider-typebox'; import * as schemas from '../schemas'; -/** - * Checks if all exam prerequisites have been met by the user. - * - * TODO: This will be done by getting the challenges required from the curriculum. - */ -export function checkPrerequisites(_user: user, _prerequisites: unknown) { - return true; +interface CompletedChallengeId { + completedChallenges: { + id: string; + }[]; } -export type UserExam = Omit & { +/** + * Checks if all exam prerequisites have been met by the user. + */ +export function checkPrerequisites( + user: CompletedChallengeId, + prerequisites: EnvExam['prerequisites'] +) { + return prerequisites.every(p => + user.completedChallenges.some(c => c.id === p) + ); +} + +export type UserExam = Omit< + EnvExam, + 'questionSets' | 'config' | 'id' | 'prerequisites' +> & { config: Omit; questionSets: (Omit & { questions: (Omit< diff --git a/curriculum/challenges/english/25-front-end-development/front-end-development-certification-exam/645147516c245de4d11eb7ba.md b/curriculum/challenges/english/25-front-end-development/front-end-development-certification-exam/645147516c245de4d11eb7ba.md index 28af96694df..fffd0166651 100644 --- a/curriculum/challenges/english/25-front-end-development/front-end-development-certification-exam/645147516c245de4d11eb7ba.md +++ b/curriculum/challenges/english/25-front-end-development/front-end-development-certification-exam/645147516c245de4d11eb7ba.md @@ -1,15 +1,13 @@ --- id: 645147516c245de4d11eb7ba title: Front End Development Certification Exam -challengeType: 17 +challengeType: 24 dashedName: front-end-development-certification-exam --- # --description-- -Here are some rules: - -- click start +Start your exam in the exam environment app. # --instructions--