diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index aeb50cf47ff..823f31abb6b 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -492,28 +492,28 @@ type SurveyResponse { // ---------------------- model DailyCodingChallenges { - id String @id @default(auto()) @map("_id") @db.ObjectId - challengeNumber Int - date DateTime - title String - description String - javascript DailyCodingChallengeApiLanguage - python DailyCodingChallengeApiLanguage + id String @id @default(auto()) @map("_id") @db.ObjectId + challengeNumber Int + date DateTime + title String + description String + javascript DailyCodingChallengeApiLanguage + python DailyCodingChallengeApiLanguage } type DailyCodingChallengeApiLanguage { - tests DailyCodingChallengeApiLanguageTests[] - challengeFiles DailyCodingChallengeApiLanguageChallengeFiles[] + tests DailyCodingChallengeApiLanguageTests[] + challengeFiles DailyCodingChallengeApiLanguageChallengeFiles[] } type DailyCodingChallengeApiLanguageTests { - text String - testString String + text String + testString String } type DailyCodingChallengeApiLanguageChallengeFiles { - contents String - fileKey String + contents String + fileKey String } // ---------------------- diff --git a/api/src/exam-environment/routes/exam-environment.test.ts b/api/src/exam-environment/routes/exam-environment.test.ts index 637fc72b387..3ecdf606a29 100644 --- a/api/src/exam-environment/routes/exam-environment.test.ts +++ b/api/src/exam-environment/routes/exam-environment.test.ts @@ -546,6 +546,8 @@ describe('/exam-environment/', () => { }); it('should return the user exam with the exam attempt', async () => { + // Mock Math.random for `shuffleArray` to be equivalent between `/generated-exam` and `constructUserExam` + jest.spyOn(Math, 'random').mockReturnValue(0.123456789); const body: Static = { examId: mock.examId }; @@ -576,12 +578,9 @@ describe('/exam-environment/', () => { const userExam = constructUserExam(generatedExam!, mock.exam); - expect(res).toMatchObject({ - status: 200, - body: { - examAttempt, - exam: userExam - } + expect(res.body).toMatchObject({ + examAttempt, + exam: userExam }); }); }); diff --git a/api/src/exam-environment/utils/exam-environment.test.ts b/api/src/exam-environment/utils/exam-environment.test.ts index dd5ba9ec876..c86793735b7 100644 --- a/api/src/exam-environment/utils/exam-environment.test.ts +++ b/api/src/exam-environment/utils/exam-environment.test.ts @@ -13,7 +13,8 @@ import { generateExam, userAttemptToDatabaseAttemptQuestionSets, validateAttempt, - compareAnswers + compareAnswers, + shuffleArray } from './exam-environment'; // NOTE: Whilst the tests could be run against a single generation of exam, @@ -21,9 +22,13 @@ import { // This helps ensure the config/logic is _reasonably_ likely to be able to // generate a valid exam. // Another option is to call `generateExam` hundreds of times in a loop test :shrug: -describe('Exam Environment', () => { +describe('Exam Environment mocked Math.random', () => { + let spy: jest.SpyInstance; beforeAll(() => { - jest.spyOn(Math, 'random').mockReturnValue(0.123456789); + spy = jest.spyOn(Math, 'random').mockReturnValue(0.123456789); + }); + afterAll(() => { + spy.mockRestore(); }); describe('checkAttemptAgainstGeneratedExam()', () => { it('should return true if all questions are answered', () => { @@ -84,13 +89,6 @@ describe('Exam Environment', () => { }); }); - describe('constructUserExam()', () => { - it('should not provide the answers', () => { - const userExam = constructUserExam(generatedExam, exam); - expect(userExam).not.toHaveProperty('answers.isCorrect'); - }); - }); - describe('generateExam()', () => { it('should generate a randomized exam without throwing', () => { const _randomizedExam = generateExam(exam); @@ -385,3 +383,28 @@ describe('Exam Environment', () => { }); }); }); + +describe('Exam Environment', () => { + describe('constructUserExam()', () => { + it('should not provide the answers', () => { + const userExam = constructUserExam(generatedExam, exam); + expect(userExam).not.toHaveProperty('answers.isCorrect'); + }); + }); + + describe('shuffleArray()', () => { + it('reasonably shuffles an array', () => { + const unshuff = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + const shuff = shuffleArray(unshuff); + + expect(shuff).not.toEqual(unshuff); + }); + + it('does not mutate the input', () => { + const unshuff = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + shuffleArray(unshuff); + + expect(unshuff).toEqual(unshuff); + }); + }); +}); diff --git a/api/src/exam-environment/utils/exam-environment.ts b/api/src/exam-environment/utils/exam-environment.ts index 2bc55893c42..b2907b74a65 100644 --- a/api/src/exam-environment/utils/exam-environment.ts +++ b/api/src/exam-environment/utils/exam-environment.ts @@ -82,11 +82,14 @@ export function constructUserExam( return answer; }); + // NOTE: Shuffling here means when saved attempt is re-fetched, answers will be in different order. + const shuffledAnswers = shuffleArray(answers); + return { id: examQuestion.id, audio: examQuestion.audio, text: examQuestion.text, - answers + answers: shuffledAnswers }; }); @@ -731,8 +734,9 @@ function getRandomAnswers( * * https://bost.ocks.org/mike/shuffle/ */ -function shuffleArray(array: Array) { - let m = array.length; +export function shuffleArray(array: Array) { + const arr = structuredClone(array); + let m = arr.length; let t; let i; @@ -742,12 +746,12 @@ function shuffleArray(array: Array) { i = Math.floor(Math.random() * m--); // And swap it with the current element. - t = array[m]!; - array[m] = array[i]!; - array[i] = t; + t = arr[m]!; + arr[m] = arr[i]!; + arr[i] = t; } - return array; + return arr; } /* eslint-enable jsdoc/require-description-complete-sentence */