mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
fix: account for expired attempt without mod record (#63317)
This commit is contained in:
@@ -24,11 +24,9 @@ import {
|
||||
examEnvironmentPostExamGeneratedExam
|
||||
} from '../schemas/index.js';
|
||||
import * as mock from '../../../__mocks__/exam-environment-exam.js';
|
||||
import {
|
||||
constructUserExam,
|
||||
ExamAttemptStatus
|
||||
} from '../utils/exam-environment.js';
|
||||
import { constructUserExam } from '../utils/exam-environment.js';
|
||||
import { JWT_SECRET } from '../../utils/env.js';
|
||||
import { ExamAttemptStatus } from '../schemas/exam-environment-exam-attempt.js';
|
||||
|
||||
vi.mock('../../utils/env', async importOriginal => {
|
||||
const actual = await importOriginal<typeof import('../../utils/env.js')>();
|
||||
|
||||
@@ -26,6 +26,19 @@ export const examEnvironmentPostExamAttempt = {
|
||||
}
|
||||
};
|
||||
|
||||
export enum ExamAttemptStatus {
|
||||
// Attempt has not expired yet.
|
||||
InProgress = 'InProgress',
|
||||
// Moderation record is not created for practice exam. Also, it might not exist until exam service cron is run.
|
||||
Expired = 'Expired',
|
||||
// Attempt has expired && moderation record has been created but not yet moderated
|
||||
PendingModeration = 'PendingModeration',
|
||||
// Attempt has been approved
|
||||
Approved = 'Approved',
|
||||
// Attempt has been denied
|
||||
Denied = 'Denied'
|
||||
}
|
||||
|
||||
const examEnvAttempt = Type.Object({
|
||||
id: Type.String(),
|
||||
examId: Type.String(),
|
||||
@@ -50,7 +63,7 @@ const examEnvAttempt = Type.Object({
|
||||
})
|
||||
]),
|
||||
version: Type.Number(),
|
||||
status: Type.Enum(['InProgress', 'PendingModeration', 'Approved', 'Denied'])
|
||||
status: Type.Enum(ExamAttemptStatus)
|
||||
});
|
||||
|
||||
export const examEnvironmentGetExamAttempts = {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { type Static } from '@fastify/type-provider-typebox';
|
||||
import { omit } from 'lodash-es';
|
||||
import * as schemas from '../schemas/index.js';
|
||||
import { mapErr } from '../../utils/index.js';
|
||||
import { ExamAttemptStatus } from '../schemas/exam-environment-exam-attempt.js';
|
||||
import { ERRORS } from './errors.js';
|
||||
|
||||
interface CompletedChallengeId {
|
||||
@@ -64,21 +65,35 @@ export function constructUserExam(
|
||||
// Map generated exam to user exam (a.k.a. public exam information for user)
|
||||
const userQuestionSets = generatedExam.questionSets.map(gqs => {
|
||||
// Get matching question from `exam`, but remove `is_correct` from `exam.questions[].answers[]`
|
||||
const examQuestionSet = exam.questionSets.find(eqs => eqs.id === gqs.id)!;
|
||||
const examQuestionSet = exam.questionSets.find(eqs => eqs.id === gqs.id);
|
||||
if (!examQuestionSet) {
|
||||
throw new Error(
|
||||
`Unreachable. Generated question set id ${gqs.id} not found in exam ${exam.id}.`
|
||||
);
|
||||
}
|
||||
|
||||
const { questions } = examQuestionSet;
|
||||
|
||||
const userQuestions = gqs.questions.map(gq => {
|
||||
const examQuestion = questions.find(eq => eq.id === gq.id)!;
|
||||
const examQuestion = questions.find(eq => eq.id === gq.id);
|
||||
if (!examQuestion) {
|
||||
throw new Error(
|
||||
`Unreachable. Generated question id ${gq.id} not found in exam question set ${examQuestionSet.id}.`
|
||||
);
|
||||
}
|
||||
|
||||
// Remove `isCorrect` from question answers
|
||||
const answers = gq.answers.map(generatedAnswerId => {
|
||||
const examAnswer = examQuestion.answers.find(
|
||||
ea => ea.id === generatedAnswerId
|
||||
)!;
|
||||
);
|
||||
if (!examAnswer) {
|
||||
throw new Error(
|
||||
`Unreachable. Generated answer id ${generatedAnswerId} not found in exam question ${examQuestion.id}.`
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { isCorrect, ...answer } = examAnswer;
|
||||
const { isCorrect: _, ...answer } = examAnswer;
|
||||
return answer;
|
||||
});
|
||||
|
||||
@@ -768,13 +783,6 @@ export function shuffleArray<T>(array: Array<T>) {
|
||||
}
|
||||
/* eslint-enable jsdoc/require-description-complete-sentence */
|
||||
|
||||
export enum ExamAttemptStatus {
|
||||
InProgress = 'InProgress',
|
||||
PendingModeration = 'PendingModeration',
|
||||
Approved = 'Approved',
|
||||
Denied = 'Denied'
|
||||
}
|
||||
|
||||
/**
|
||||
* From an exam attempt, construct the attempt with result (if ready).
|
||||
*
|
||||
@@ -862,19 +870,15 @@ export async function constructEnvExamAttempt(
|
||||
|
||||
const moderation = maybeMod.data;
|
||||
|
||||
// Attempt has expired, but moderation record does not exist
|
||||
if (moderation === null) {
|
||||
const error = {
|
||||
data: { examAttemptId: attempt.id },
|
||||
message:
|
||||
'Unreachable. ExamModeration record should exist for expired attempt'
|
||||
};
|
||||
logger.error(error.data, error.message);
|
||||
fastify.Sentry.captureException(error);
|
||||
return {
|
||||
error: {
|
||||
code: 500,
|
||||
data: ERRORS.FCC_ERR_EXAM_ENVIRONMENT(error.message)
|
||||
}
|
||||
examEnvironmentExamAttempt: {
|
||||
...omitAttemptReferenceIds(attempt),
|
||||
result: null,
|
||||
status: ExamAttemptStatus.Expired
|
||||
},
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ export function Attempts({ examChallengeId }: AttemptsProps) {
|
||||
return t('exam.in-progress');
|
||||
case 'PendingModeration':
|
||||
return t('exam.pending');
|
||||
case 'Expired':
|
||||
return t('exam.pending');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +74,8 @@ export function Attempts({ examChallengeId }: AttemptsProps) {
|
||||
return t('exam.in-progress');
|
||||
case 'PendingModeration':
|
||||
return t('exam.pending');
|
||||
case 'Expired':
|
||||
return t('exam.pending');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ export type Attempt = {
|
||||
} & (
|
||||
| {
|
||||
result: null;
|
||||
status: 'InProgress' | 'PendingModeration' | 'Denied';
|
||||
status: 'InProgress' | 'Expired' | 'PendingModeration' | 'Denied';
|
||||
}
|
||||
| {
|
||||
status: 'Approved';
|
||||
|
||||
Reference in New Issue
Block a user