mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): reuse exam generations (#62459)
This commit is contained in:
@@ -270,8 +270,7 @@ describe('/exam-environment/', () => {
|
||||
});
|
||||
|
||||
describe('POST /exam-environment/generated-exam', () => {
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.deleteMany();
|
||||
beforeEach(async () => {
|
||||
// Add prerequisite id to user completed challenge
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
@@ -282,6 +281,9 @@ describe('/exam-environment/', () => {
|
||||
}
|
||||
});
|
||||
await mock.seedEnvExam();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await mock.clearEnvExam();
|
||||
const a =
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamModeration.findMany(
|
||||
{}
|
||||
@@ -471,27 +473,15 @@ describe('/exam-environment/', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the database has insufficient generated exams', async () => {
|
||||
// Add completed attempt for generated exam
|
||||
const submittedAttempt = structuredClone(mock.examAttempt);
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
// Long-enough ago to be considered "submitted", and not trigger cooldown
|
||||
submittedAttempt.startTimeInMS =
|
||||
Date.now() -
|
||||
24 * 60 * 60 * 1000 -
|
||||
examTotalTimeInMS -
|
||||
1 * 60 * 60 * 1000;
|
||||
submittedAttempt.startTime = new Date(
|
||||
Date.now() -
|
||||
24 * 60 * 60 * 1000 -
|
||||
examTotalTimeInMS -
|
||||
1 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: submittedAttempt
|
||||
it('should prioritise not-yet-taken generated exams, and reuse completed ones if necessary', async () => {
|
||||
// Create a second generated exams for the user
|
||||
const genExam1 = structuredClone(mock.generatedExam);
|
||||
genExam1.id = mock.oid();
|
||||
await fastifyTestInstance.prisma.examEnvironmentGeneratedExam.create({
|
||||
data: genExam1
|
||||
});
|
||||
|
||||
// Request generated exam, thereby creating an attempt with one of the generated exam ids
|
||||
const body: Static<typeof examEnvironmentPostExamGeneratedExam.body> = {
|
||||
examId: mock.examId
|
||||
};
|
||||
@@ -502,12 +492,76 @@ describe('/exam-environment/', () => {
|
||||
examEnvironmentAuthorizationToken
|
||||
);
|
||||
|
||||
expect(res).toMatchObject({
|
||||
status: 500,
|
||||
body: {
|
||||
code: 'FCC_ERR_EXAM_ENVIRONMENT'
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Finish attempt
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
where: { id: res.body.examAttempt.id },
|
||||
data: {
|
||||
startTime: new Date(
|
||||
Date.now() -
|
||||
mock.exam.config.totalTimeInMS -
|
||||
mock.exam.config.retakeTimeInMS
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
// Request generated exam again, which should use the other generated exam
|
||||
const res2 = await superPost('/exam-environment/exam/generated-exam')
|
||||
.send(body)
|
||||
.set(
|
||||
'exam-environment-authorization-token',
|
||||
examEnvironmentAuthorizationToken
|
||||
);
|
||||
|
||||
expect(res2.status).toBe(200);
|
||||
|
||||
// Expect examEnvironmentExamAttempt to include 2 records
|
||||
const eas =
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.findMany({
|
||||
where: {
|
||||
userId: defaultUserId
|
||||
}
|
||||
});
|
||||
|
||||
expect(eas).toHaveLength(2);
|
||||
// Expect eas[].generatedExamId to not be the same
|
||||
const geIds = eas.map(ea => ea.generatedExamId);
|
||||
expect(geIds[0]).not.toBe(geIds[1]);
|
||||
|
||||
// Finish attempt
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
where: { id: res2.body.examAttempt.id },
|
||||
data: {
|
||||
startTime: new Date(
|
||||
Date.now() -
|
||||
mock.exam.config.totalTimeInMS -
|
||||
mock.exam.config.retakeTimeInMS
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
// Request generated exam again, which should reuse one of the generated exams
|
||||
const res3 = await superPost('/exam-environment/exam/generated-exam')
|
||||
.send(body)
|
||||
.set(
|
||||
'exam-environment-authorization-token',
|
||||
examEnvironmentAuthorizationToken
|
||||
);
|
||||
|
||||
expect(res3.status).toBe(200);
|
||||
|
||||
// Expect examEnvironmentExamAttempt to include 3 records, with only 2 unique `generatedExamId`s
|
||||
const eas2 =
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.findMany({
|
||||
where: { userId: defaultUserId }
|
||||
});
|
||||
|
||||
expect(eas2).toHaveLength(3);
|
||||
const geIds2 = eas2.map(ea => ea.generatedExamId);
|
||||
expect(new Set(geIds2).size).toBe(2);
|
||||
});
|
||||
|
||||
it('should record the fact the user has started an exam by creating an exam attempt', async () => {
|
||||
|
||||
@@ -376,11 +376,7 @@ async function postExamGeneratedExamHandler(
|
||||
const maybeGeneratedExams = await mapErr(
|
||||
this.prisma.examEnvironmentGeneratedExam.findMany({
|
||||
where: {
|
||||
// Find generated exams user has not already seen
|
||||
examId: exam.id,
|
||||
id: {
|
||||
notIn: examAttempts.map(a => a.generatedExamId)
|
||||
},
|
||||
deprecated: false
|
||||
},
|
||||
select: {
|
||||
@@ -403,7 +399,7 @@ async function postExamGeneratedExamHandler(
|
||||
if (generatedExams.length === 0) {
|
||||
const error = {
|
||||
data: { examId: exam.id },
|
||||
message: `Unable to provide a generated exam. Either all generated exams have been exhausted, or all generated exams are deprecated.`
|
||||
message: `Unable to provide a generated exam. Either no generations exist, or all generated exams are deprecated.`
|
||||
};
|
||||
logger.error(error.data, error.message);
|
||||
this.Sentry.captureException(error);
|
||||
@@ -411,13 +407,28 @@ async function postExamGeneratedExamHandler(
|
||||
return reply.send(ERRORS.FCC_ERR_EXAM_ENVIRONMENT(error.message));
|
||||
}
|
||||
|
||||
const randomGeneratedExam =
|
||||
generatedExams[Math.floor(Math.random() * generatedExams.length)]!;
|
||||
// Randomly pick an exam from available generations, prioritising generations not already taken
|
||||
const untakenGeneratedExams = generatedExams.filter(
|
||||
ge => !examAttempts.find(ea => ea.generatedExamId === ge.id)
|
||||
);
|
||||
let randomGeneratedExamId: string;
|
||||
if (untakenGeneratedExams.length === 0) {
|
||||
logger.info(
|
||||
`User has taken all generated exams. Reusing previously taken generated exams.`
|
||||
);
|
||||
randomGeneratedExamId =
|
||||
generatedExams[Math.floor(Math.random() * generatedExams.length)]!.id;
|
||||
} else {
|
||||
randomGeneratedExamId =
|
||||
untakenGeneratedExams[
|
||||
Math.floor(Math.random() * untakenGeneratedExams.length)
|
||||
]!.id;
|
||||
}
|
||||
|
||||
const maybeGeneratedExam = await mapErr(
|
||||
this.prisma.examEnvironmentGeneratedExam.findFirst({
|
||||
where: {
|
||||
id: randomGeneratedExam.id
|
||||
id: randomGeneratedExamId
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -439,7 +450,7 @@ async function postExamGeneratedExamHandler(
|
||||
|
||||
if (generatedExam === null) {
|
||||
const error = {
|
||||
data: { generatedExamId: randomGeneratedExam.id },
|
||||
data: { generatedExamId: randomGeneratedExamId },
|
||||
message: 'Unreachable. Generated exam not found.'
|
||||
};
|
||||
logger.error(error.data, 'Unreachable. Generated exam not found.');
|
||||
|
||||
Reference in New Issue
Block a user