Files
freecodecamp--freecodecamp/api/src/exam-environment/utils/exam-environment.test.ts
T
wehub-resource-sync dde272c4b8
CD - Docker - GHCR Images / Build and Push Images (push) Waiting to run
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:55:53 +08:00

441 lines
12 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import type { MockInstance } from 'vitest';
import {
ExamEnvironmentAnswer,
ExamEnvironmentQuestionType
} from '@prisma/client';
import { type Static } from '@fastify/type-provider-typebox';
import {
exam,
examAttempt,
generatedExam,
oid
} from '../../../__fixtures__/exam-environment-exam.js';
import * as schemas from '../schemas/index.js';
import { setupServer } from '../../../vitest.utils.js';
import {
checkAttemptAgainstGeneratedExam,
checkPrerequisites,
constructUserExam,
userAttemptToDatabaseAttemptQuestionSets,
validateAttempt,
compareAnswers,
shuffleArray
} from './exam-environment.js';
// NOTE: Whilst the tests could be run against a single generation of exam,
// it is more useful to run the tests against a new generation each time.
// 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 mocked Math.random', () => {
let spy: MockInstance;
beforeAll(() => {
spy = vi.spyOn(Math, 'random').mockReturnValue(0.123456789);
});
afterAll(() => {
spy.mockRestore();
});
describe('checkAttemptAgainstGeneratedExam()', () => {
it('should return true if all questions are answered', () => {
expect(
checkAttemptAgainstGeneratedExam(
examAttempt.questionSets,
generatedExam
)
).toBe(true);
});
it('should return false if one or more questions are not answered', () => {
const badExamAttempt = structuredClone(examAttempt);
badExamAttempt.questionSets[0]!.questions[0]!.answers = [];
expect(
checkAttemptAgainstGeneratedExam(
badExamAttempt.questionSets,
generatedExam
)
).toBe(false);
badExamAttempt.questionSets[0]!.questions[0]!.answers = ['bad-answer'];
expect(
checkAttemptAgainstGeneratedExam(
badExamAttempt.questionSets,
generatedExam
)
).toBe(false);
badExamAttempt.questionSets[0]!.questions = [];
expect(
checkAttemptAgainstGeneratedExam(
badExamAttempt.questionSets,
generatedExam
)
).toBe(false);
});
});
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' }],
isHonest: true
};
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' }],
isHonest: false
};
const prerequisites = ['1', '2'];
expect(checkPrerequisites(user, prerequisites)).toBe(false);
});
});
describe('validateAttempt()', () => {
it('should validate a correct attempt', () => {
expect(() =>
validateAttempt(generatedExam, examAttempt.questionSets)
).not.toThrow();
});
it('should invalidate an incorrect attempt', () => {
const badExamAttempt = structuredClone(examAttempt);
badExamAttempt.questionSets[0]!.questions[0]!.answers = ['bad-answer'];
expect(() =>
validateAttempt(generatedExam, badExamAttempt.questionSets)
).toThrow();
});
});
describe('userAttemptToDatabaseAttemptQuestionSets()', () => {
it('should add submission time to all questions', () => {
const userAttempt: Static<
typeof schemas.examEnvironmentPostExamAttempt.body.properties.attempt
> = {
examId: '0',
questionSets: [
{
id: '0',
questions: [{ id: '00', answers: ['000'] }]
},
{
id: '1',
questions: [{ id: '10', answers: ['100'] }]
}
]
};
const latestAttempt = structuredClone(examAttempt);
latestAttempt.questionSets = [];
const databaseAttemptQuestionSets =
userAttemptToDatabaseAttemptQuestionSets(userAttempt, latestAttempt);
const allQuestions = databaseAttemptQuestionSets.flatMap(
qs => qs.questions
);
expect(allQuestions.every(q => q.submissionTime)).toBe(true);
});
it('should not change the submission time of any questions that have not changed', () => {
const userAttempt: Static<
typeof schemas.examEnvironmentPostExamAttempt.body.properties.attempt
> = {
examId: '0',
questionSets: [
{
id: '0',
questions: [{ id: '00', answers: ['000'] }]
},
{
id: '1',
questions: [{ id: '10', answers: ['100'] }]
}
]
};
const latestAttempt = structuredClone(examAttempt);
const databaseAttemptQuestionSets =
userAttemptToDatabaseAttemptQuestionSets(userAttempt, latestAttempt);
const submissionTimes = databaseAttemptQuestionSets.flatMap(qs =>
qs.questions.map(q => q.submissionTime)
);
const sameAttempt = userAttemptToDatabaseAttemptQuestionSets(
userAttempt,
{ ...latestAttempt, questionSets: databaseAttemptQuestionSets }
);
const sameSubmissionTimes = sameAttempt.flatMap(qs =>
qs.questions.map(q => q.submissionTime)
);
expect(submissionTimes).toEqual(sameSubmissionTimes);
});
it('should change all submission times of questions that have changed', async () => {
const userAttempt: Static<
typeof schemas.examEnvironmentPostExamAttempt.body.properties.attempt
> = {
examId: '0',
questionSets: [
{
id: '0',
questions: [{ id: '00', answers: ['000'] }]
},
{
id: '1',
questions: [{ id: '10', answers: ['100'] }]
}
]
};
const latestAttempt = structuredClone(examAttempt);
const databaseAttemptQuestionSets =
userAttemptToDatabaseAttemptQuestionSets(userAttempt, latestAttempt);
userAttempt.questionSets[0]!.questions[0]!.answers = ['001'];
// The `userAttemptToDatabaseAttemptQuestionSets` function uses `Date.now()`
// to set the submission time, so we need to wait a bit to ensure differences.
await new Promise(resolve => setTimeout(resolve, 10));
const newAttemptQuestionSets = userAttemptToDatabaseAttemptQuestionSets(
userAttempt,
{
...latestAttempt,
questionSets: databaseAttemptQuestionSets
}
);
expect(
newAttemptQuestionSets[0]?.questions[0]?.submissionTime
).not.toEqual(
databaseAttemptQuestionSets[0]?.questions[0]?.submissionTime
);
});
});
describe('compareAnswers()', () => {
it('should return true when only all correct answers are attempted', () => {
const examAnswers: ExamEnvironmentAnswer[] = [
{
id: '0',
isCorrect: true,
text: ''
},
{
id: '1',
isCorrect: true,
text: ''
},
{
id: '2',
isCorrect: false,
text: ''
},
{
id: '3',
isCorrect: false,
text: ''
}
];
const generatedAnswers = ['0', '1', '2', '3'];
const attemptAnswers = ['0', '1'];
const isCorrect = compareAnswers(
examAnswers,
generatedAnswers,
attemptAnswers
);
expect(isCorrect).toBe(true);
});
it('should return false when any incorrect answers are attempted', () => {
const examAnswers: ExamEnvironmentAnswer[] = [
{
id: '0',
isCorrect: true,
text: ''
},
{
id: '1',
isCorrect: true,
text: ''
},
{
id: '2',
isCorrect: false,
text: ''
},
{
id: '3',
isCorrect: false,
text: ''
}
];
const generatedAnswers = ['0', '1', '2', '3'];
const attemptAnswers = ['0', '2'];
const isCorrect = compareAnswers(
examAnswers,
generatedAnswers,
attemptAnswers
);
expect(isCorrect).toBe(false);
});
});
});
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);
});
});
});
describe('Exam Environment Schema', () => {
setupServer();
describe('ExamEnvironmentExam', () => {
afterAll(async () => {
await fastifyTestInstance.prisma.examEnvironmentExam.deleteMany({});
});
// eslint-disable-next-line vitest/expect-expect
it("If this test fails and you've deliberately altered the schema, then increment the `version` field by 1", async () => {
const configQuestionSets = [
{
numberOfCorrectAnswers: 0,
numberOfIncorrectAnswers: 0,
numberOfQuestions: 0,
numberOfSet: 0,
type: ExamEnvironmentQuestionType.MultipleChoice
}
];
const tags = [
{
group: [''],
numberOfQuestions: 0
}
];
const config = {
name: '',
note: '',
passingPercent: 0.0,
questionSets: configQuestionSets,
retakeTimeInS: 0,
tags,
totalTimeInS: 0
};
const questions = [
{
answers: [
{
id: oid(),
isCorrect: false,
text: ''
}
],
audio: { captions: '', url: '' },
deprecated: false,
id: oid(),
tags: [''],
text: ''
}
];
const questionSets = [
{
context: '',
id: oid(),
questions,
type: ExamEnvironmentQuestionType.MultipleChoice
}
];
const data = {
config,
deprecated: false,
prerequisites: [oid()],
questionSets
};
await fastifyTestInstance.prisma.examEnvironmentExam.create({
data
});
});
});
describe('ExamEnvironmentGeneratedExam', () => {
afterAll(async () => {
await fastifyTestInstance.prisma.examEnvironmentGeneratedExam.deleteMany(
{}
);
});
// eslint-disable-next-line vitest/expect-expect
it("If this test fails and you've deliberately altered the schema, then increment the `version` field by 1", async () => {
await fastifyTestInstance.prisma.examEnvironmentGeneratedExam.create({
data: {
deprecated: false,
examId: oid(),
questionSets: [
{ id: oid(), questions: [{ answers: [oid()], id: oid() }] }
]
}
});
});
});
describe('ExamEnvironmentExamAttempt', () => {
afterAll(async () => {
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.deleteMany(
{}
);
});
// eslint-disable-next-line vitest/expect-expect
it("If this test fails and you've deliberately altered the schema, then increment the `version` field by 1", async () => {
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
data: {
examId: oid(),
generatedExamId: oid(),
examModerationId: null,
questionSets: [
{
id: oid(),
questions: [
{
answers: [oid()],
id: oid(),
submissionTime: new Date()
}
]
}
],
startTime: new Date(),
userId: oid()
}
});
});
});
});