mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-01-10 09:05:55 -05:00
feat(api): add POST /exam-challenge-completed (#52395)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { user } from '@prisma/client';
|
||||
import { ExamResults, user } from '@prisma/client';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { omit, pick } from 'lodash';
|
||||
import { challengeTypes } from '../../../shared/config/challenge-types';
|
||||
@@ -63,6 +63,7 @@ export type CompletedChallenge = {
|
||||
completedDate: number;
|
||||
isManuallyApproved?: boolean | null;
|
||||
files?: CompletedChallengeFile[];
|
||||
examResults?: ExamResults | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ export function createUserInput(email: string): Prisma.userCreateInput {
|
||||
about: '',
|
||||
acceptedPrivacyTerms: false,
|
||||
completedChallenges: [], // TODO(Post-MVP): Omit this from the document? (prisma will always return [])
|
||||
completedExams: [], // TODO(Post-MVP): Omit this from the document? (prisma will always return [])
|
||||
currentChallengeId: '',
|
||||
donationEmails: [], // TODO(Post-MVP): Omit this from the document? (prisma will always return [])
|
||||
email,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Answer, Exam, Question } from '@prisma/client';
|
||||
import { Answer, Exam, Question, ExamResults } from '@prisma/client';
|
||||
import Joi from 'joi';
|
||||
import { ExamResults, GeneratedExam, UserExam } from './exam-types';
|
||||
import { GeneratedExam, UserExam } from './exam-types';
|
||||
|
||||
const nanoIdRE = new RegExp('[a-z0-9]{10}');
|
||||
const objectIdRE = new RegExp('^[0-9a-fA-F]{24}$');
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// types for a generated exam
|
||||
export interface GeneratedAnswer {
|
||||
export interface Answer {
|
||||
id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
// types for a generated exam
|
||||
export interface GeneratedQuestion {
|
||||
id: string;
|
||||
question: string;
|
||||
answers: GeneratedAnswer[];
|
||||
answers: Answer[];
|
||||
}
|
||||
|
||||
export type GeneratedExam = GeneratedQuestion[];
|
||||
@@ -16,19 +16,10 @@ export type GeneratedExam = GeneratedQuestion[];
|
||||
export interface UserQuestion {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: GeneratedAnswer;
|
||||
answer: Answer;
|
||||
}
|
||||
|
||||
export interface UserExam {
|
||||
userExamQuestions: UserQuestion[];
|
||||
examTimeInSeconds: number;
|
||||
}
|
||||
|
||||
export interface ExamResults {
|
||||
numberOfCorrectAnswers: number;
|
||||
numberOfQuestionsInExam: number;
|
||||
percentCorrect: number;
|
||||
passingPercent: number;
|
||||
passed: boolean;
|
||||
examTimeInSeconds: number;
|
||||
}
|
||||
|
||||
@@ -3,46 +3,52 @@ import {
|
||||
examJson,
|
||||
userExam1,
|
||||
userExam2,
|
||||
userExam3,
|
||||
userExam4,
|
||||
mockResults1,
|
||||
mockResults2
|
||||
mockResults2,
|
||||
mockResults3,
|
||||
mockResults4
|
||||
} from '../../__mocks__/exam';
|
||||
import { generateRandomExam, createExamResults } from './exam';
|
||||
import { GeneratedExam, GeneratedQuestion } from './exam-types';
|
||||
import { GeneratedExam } from './exam-types';
|
||||
|
||||
describe('Exam helpers', () => {
|
||||
describe('generateRandomExam()', () => {
|
||||
const randomizedExam: GeneratedExam = generateRandomExam(examJson as Exam);
|
||||
|
||||
it('should have one question', () => {
|
||||
expect(randomizedExam.length).toBe(1);
|
||||
it('should have three questions', () => {
|
||||
expect(randomizedExam.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should have five answers', () => {
|
||||
const firstQuestion = randomizedExam[0] as GeneratedQuestion;
|
||||
expect(firstQuestion.answers.length).toBe(5);
|
||||
it('should have five answers per question', () => {
|
||||
randomizedExam.forEach(question => {
|
||||
expect(question.answers.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have exactly one correct answer', () => {
|
||||
const question = randomizedExam[0] as GeneratedQuestion;
|
||||
const questionId = question.id;
|
||||
const originalQuestion = examJson.questions.find(
|
||||
q => q.id === questionId
|
||||
) as Question;
|
||||
const originalCorrectAnswer = originalQuestion.correctAnswers;
|
||||
const correctIds = originalCorrectAnswer.map(a => a.id);
|
||||
it('should have exactly one correct answer per question', () => {
|
||||
randomizedExam.forEach(question => {
|
||||
const originalQuestion = examJson.questions.find(
|
||||
q => q.id === question.id
|
||||
) as Question;
|
||||
const originalCorrectAnswer = originalQuestion.correctAnswers;
|
||||
const correctIds = originalCorrectAnswer.map(a => a.id);
|
||||
|
||||
const numberOfCorrectAnswers = question.answers.filter(a =>
|
||||
correctIds.includes(a.id)
|
||||
);
|
||||
const numberOfCorrectAnswers = question.answers.filter(a =>
|
||||
correctIds.includes(a.id)
|
||||
);
|
||||
|
||||
expect(numberOfCorrectAnswers).toHaveLength(1);
|
||||
expect(numberOfCorrectAnswers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createExamResults()', () => {
|
||||
examJson.numberOfQuestionsInExam = 2;
|
||||
const examResults1 = createExamResults(userExam1, examJson as Exam);
|
||||
const examResults2 = createExamResults(userExam2, examJson as Exam);
|
||||
const examResults3 = createExamResults(userExam3, examJson as Exam);
|
||||
const examResults4 = createExamResults(userExam4, examJson as Exam);
|
||||
|
||||
it('failing exam should return correct results', () => {
|
||||
expect(examResults1).toEqual(mockResults1);
|
||||
@@ -50,6 +56,8 @@ describe('Exam helpers', () => {
|
||||
|
||||
it('passing exam should return correct results', () => {
|
||||
expect(examResults2).toEqual(mockResults2);
|
||||
expect(examResults3).toEqual(mockResults3);
|
||||
expect(examResults4).toEqual(mockResults4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,7 @@ describe('normalize', () => {
|
||||
solution: null,
|
||||
githubLink: null,
|
||||
isManuallyApproved: null,
|
||||
examResults: null,
|
||||
files: [
|
||||
{
|
||||
contents: 'test',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* This module's job is to parse the database output and prepare it for
|
||||
serialization */
|
||||
import { ProfileUI, CompletedChallenge } from '@prisma/client';
|
||||
import { ProfileUI, CompletedChallenge, ExamResults } from '@prisma/client';
|
||||
import _ from 'lodash';
|
||||
|
||||
type NullToUndefined<T> = T extends null ? undefined : T;
|
||||
@@ -81,6 +81,7 @@ type NormalizedChallenge = {
|
||||
id: string;
|
||||
isManuallyApproved?: boolean;
|
||||
solution?: string;
|
||||
examResults?: ExamResults;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user