mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-20 12:03:11 -04:00
fix(api): shuffle generated exam answers each time (#61352)
Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
|
||||
@@ -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<typeof examEnvironmentPostExamGeneratedExam.body> = {
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<T>(array: Array<T>) {
|
||||
let m = array.length;
|
||||
export function shuffleArray<T>(array: Array<T>) {
|
||||
const arr = structuredClone(array);
|
||||
let m = arr.length;
|
||||
let t;
|
||||
let i;
|
||||
|
||||
@@ -742,12 +746,12 @@ function shuffleArray<T>(array: Array<T>) {
|
||||
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 */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user