feat(api): add POST /exam-challenge-completed (#52395)

This commit is contained in:
Tom
2023-12-27 09:10:30 -06:00
committed by GitHub
parent 741ca9338e
commit 1efb22cd34
15 changed files with 842 additions and 64 deletions

View File

@@ -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;
};
/**

View File

@@ -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,

View File

@@ -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}$');

View File

@@ -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;
}

View File

@@ -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);
});
});
});

View File

@@ -95,6 +95,7 @@ describe('normalize', () => {
solution: null,
githubLink: null,
isManuallyApproved: null,
examResults: null,
files: [
{
contents: 'test',

View File

@@ -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;
};
/**