refactor(api): more, smaller tests (#54671)

This commit is contained in:
Oliver Eyton-Williams
2024-05-20 20:18:14 +02:00
committed by GitHub
parent f897769f0b
commit d06bbab6f8
4 changed files with 197 additions and 154 deletions

View File

@@ -67,12 +67,25 @@ export const completedTrophyChallenges = [
{
id: '647f85d407d29547b3bee1bb',
solution: 'challenge-solution',
completedDate: 1695064765244
completedDate: 1695064765244,
files: []
}
];
export type ExamSubmission = {
userExamQuestions: {
id: string;
question: string;
answer: {
id: string;
answer: string;
};
}[];
examTimeInSeconds: number;
};
// failed: 0 correct
export const userExam1 = {
export const examWithZeroCorrect: ExamSubmission = {
userExamQuestions: [
{
id: '3bbl2mx2mq',
@@ -94,7 +107,7 @@ export const userExam1 = {
};
// passed: 1 correct
export const userExam2 = {
export const examWithOneCorrect: ExamSubmission = {
userExamQuestions: [
{
id: '3bbl2mx2mq',
@@ -116,7 +129,7 @@ export const userExam2 = {
};
// passed: 2 correct
export const userExam3 = {
export const examWithTwoCorrect: ExamSubmission = {
userExamQuestions: [
{
id: '3bbl2mx2mq',
@@ -138,7 +151,7 @@ export const userExam3 = {
};
// passed: 3 correct
export const userExam4 = {
export const examWithAllCorrect: ExamSubmission = {
userExamQuestions: [
{
id: '3bbl2mx2mq',
@@ -159,7 +172,7 @@ export const userExam4 = {
examTimeInSeconds: 20
};
export const mockResults1 = {
export const mockResultsZeroCorrect = {
numberOfCorrectAnswers: 0,
numberOfQuestionsInExam: 3,
percentCorrect: 0,
@@ -168,7 +181,7 @@ export const mockResults1 = {
examTimeInSeconds: 20
};
export const mockResults2 = {
export const mockResultsOneCorrect = {
numberOfCorrectAnswers: 1,
numberOfQuestionsInExam: 3,
percentCorrect: 33.3,
@@ -177,7 +190,7 @@ export const mockResults2 = {
examTimeInSeconds: 20
};
export const mockResults3 = {
export const mockResultsTwoCorrect = {
numberOfCorrectAnswers: 2,
numberOfQuestionsInExam: 3,
percentCorrect: 66.7,
@@ -186,7 +199,7 @@ export const mockResults3 = {
examTimeInSeconds: 20
};
export const mockResults4 = {
export const mockResultsAllCorrect = {
numberOfCorrectAnswers: 3,
numberOfQuestionsInExam: 3,
percentCorrect: 100,
@@ -197,25 +210,27 @@ export const mockResults4 = {
const completedExamChallenge = {
id: examChallengeId,
challengeType: 17
challengeType: 17,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
completedDate: expect.any(Number)
};
export const completedExamChallenge1 = {
export const completedExamChallengeZeroCorrect = {
...completedExamChallenge,
examResults: mockResults1
examResults: mockResultsZeroCorrect
};
export const completedExamChallenge2 = {
export const completedExamChallengeOneCorrect = {
...completedExamChallenge,
examResults: mockResults2
examResults: mockResultsOneCorrect
};
export const completedExamChallenge3 = {
export const completedExamChallengeTwoCorrect = {
...completedExamChallenge,
examResults: mockResults3
examResults: mockResultsTwoCorrect
};
export const completedExamChallenge4 = {
export const completedExamChallengeAllCorrect = {
...completedExamChallenge,
examResults: mockResults4
examResults: mockResultsAllCorrect
};

View File

@@ -175,6 +175,7 @@ If you are seeing this error, the root cause is likely an error thrown in the be
export const defaultUserId = '64c7810107dd4782d32baee7';
export const defaultUserEmail = 'foo@bar.com';
export const defaultUsername = 'fcc-test-user';
export async function devLogin(): Promise<string[]> {
await fastifyTestInstance.prisma.user.deleteMany({
@@ -184,7 +185,8 @@ export async function devLogin(): Promise<string[]> {
await fastifyTestInstance.prisma.user.create({
data: {
...createUserInput(defaultUserEmail),
id: defaultUserId
id: defaultUserId,
username: defaultUsername
}
});
const res = await superRequest('/signin', { method: 'GET' });

View File

@@ -4,6 +4,7 @@ const mockVerifyTrophyWithMicrosoft = jest.fn();
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { omit } from 'lodash';
import { Static } from '@fastify/type-provider-typebox';
import { challengeTypes } from '../../../shared/config/challenge-types';
import {
@@ -13,24 +14,26 @@ import {
superRequest,
seedExam,
defaultUserEmail,
createSuperRequest
createSuperRequest,
defaultUsername
} from '../../jest.utils';
import {
completedExamChallenge2,
completedExamChallenge3,
completedExamChallenge4,
completedExamChallengeOneCorrect,
completedExamChallengeTwoCorrect,
completedExamChallengeAllCorrect,
completedTrophyChallenges,
examChallengeId,
mockResults1,
mockResults2,
mockResults3,
mockResults4,
userExam1,
userExam2,
userExam3,
userExam4
mockResultsZeroCorrect,
mockResultsTwoCorrect,
mockResultsAllCorrect,
examWithZeroCorrect,
examWithOneCorrect,
examWithTwoCorrect,
examWithAllCorrect,
type ExamSubmission
} from '../../__mocks__/exam';
import { Answer } from '../utils/exam-types';
import type { getSessionUser } from '../schemas/user/get-session-user';
jest.mock('./helpers/challenge-helpers', () => {
const originalModule = jest.requireActual<
@@ -1493,173 +1496,184 @@ describe('challengeRoutes', () => {
});
});
test('POST handles submitting a failing exam', async () => {
const now = Date.now();
// Submit exam with 0 correct answers
const response = await superRequest('/exam-challenge-completed', {
const submitExam = async (exam: ExamSubmission) => {
return superRequest('/exam-challenge-completed', {
method: 'POST',
setCookies
}).send({
id: examChallengeId,
challengeType: 17,
userCompletedExam: userExam1
userCompletedExam: exam
});
};
const {
completedChallenges = [],
completedExams = [],
progressTimestamps = []
} = (await fastifyTestInstance.prisma.user.findFirst({
where: { email: 'foo@bar.com' }
})) || {};
test('POST handles submitting a failing exam', async () => {
const now = Date.now();
// Submit exam with 0 correct answers
const response = await submitExam(examWithZeroCorrect);
type GetSessionUserResponseBody = Static<
(typeof getSessionUser)['response']['200']
>['user'];
const res = (await superGet('/user/get-session-user')).body as {
user: GetSessionUserResponseBody;
};
const { completedChallenges, completedExams, calendar } =
res.user[defaultUsername]!;
// should have the 1 prerequisite challenge
expect(completedChallenges).toHaveLength(1);
expect(completedExams).toHaveLength(1);
expect(progressTimestamps).toHaveLength(0);
expect(completedChallenges).toMatchObject(completedTrophyChallenges);
expect(completedExams[0]).toMatchObject({
expect(calendar).toStrictEqual({});
expect(completedChallenges).toEqual(completedTrophyChallenges);
expect(completedExams[0]).toEqual({
id: '647e22d18acb466c97ccbef8',
challengeType: 17,
examResults: mockResults1
completedDate: expect.any(Number),
examResults: mockResultsZeroCorrect
});
expect(completedExams[0]?.completedDate).toBeGreaterThan(now);
expect(response.body).toMatchObject({
points: 0,
alreadyCompleted: false,
examResults: mockResults1
examResults: mockResultsZeroCorrect
});
expect(response.statusCode).toBe(200);
});
test('POST handles submitting multiple passing exams', async () => {
// Submit exam with 2/3 correct answers
const nowA = Date.now();
const responseA = await superRequest('/exam-challenge-completed', {
method: 'POST',
setCookies
}).send({
id: examChallengeId,
challengeType: 17,
userCompletedExam: userExam3
});
test("POST always adds to the user's completedExams", async () => {
let now = Date.now();
// The first exam should be stored in the user's completedExams
await submitExam(examWithAllCorrect);
const userA = await fastifyTestInstance.prisma.user.findFirst({
let { completedExams } =
await fastifyTestInstance.prisma.user.findFirstOrThrow({
where: { id: defaultUserId }
});
expect(completedExams).toHaveLength(1);
expect(completedExams[0]).toEqual(completedExamChallengeAllCorrect);
expect(completedExams[0]?.completedDate).toBeGreaterThan(now);
expect(completedExams[0]?.completedDate).toBeLessThan(Date.now());
now = Date.now();
// the second exam should be added to the exams, not replace the first
await submitExam(examWithOneCorrect);
completedExams = (
await fastifyTestInstance.prisma.user.findFirstOrThrow({
where: { id: defaultUserId }
})
).completedExams;
expect(completedExams).toHaveLength(2);
expect(completedExams).toEqual(
expect.arrayContaining([
completedExamChallengeAllCorrect,
completedExamChallengeOneCorrect
])
);
expect(completedExams[1]?.completedDate).toBeGreaterThan(now);
expect(completedExams[1]?.completedDate).toBeLessThan(Date.now());
});
test('POST updates user progress if they have not completed the exam before', async () => {
// Submit exam with 2/3 correct answers
const now = Date.now();
const res = await submitExam(examWithTwoCorrect);
const user = await fastifyTestInstance.prisma.user.findFirstOrThrow({
where: { id: defaultUserId }
});
const completedChallengesA = userA?.completedChallenges || [];
const completedExamsA = userA?.completedExams || [];
const progressTimestampsA = userA?.progressTimestamps || [];
// should add to completedChallenges
expect(completedChallengesA).toHaveLength(2);
expect(completedChallengesA).toMatchObject([
expect(user.completedChallenges).toHaveLength(2);
expect(user.completedChallenges).toMatchObject([
...completedTrophyChallenges,
completedExamChallenge3
completedExamChallengeTwoCorrect
]);
expect(completedChallengesA[1]?.completedDate).toBeGreaterThan(nowA);
// should add to completedExams
expect(completedExamsA).toHaveLength(1);
expect(completedExamsA[0]).toMatchObject(completedExamChallenge3);
expect(completedExamsA[0]?.completedDate).toBeGreaterThan(nowA);
expect(user.completedChallenges[1]?.completedDate).toBeGreaterThan(
now
);
// should add to progressTimestamps
expect(progressTimestampsA).toHaveLength(1);
expect(user.progressTimestamps).toHaveLength(1);
expect(responseA.body).toMatchObject({
expect(res.body).toMatchObject({
points: 1,
alreadyCompleted: false,
examResults: mockResults3
examResults: mockResultsTwoCorrect
});
expect(responseA.statusCode).toBe(200);
expect(res.statusCode).toBe(200);
});
// Submit exam with 1/3 correct answers (worse exam than already submitted)
const now2 = Date.now();
const response2 = await superRequest('/exam-challenge-completed', {
method: 'POST',
setCookies
}).send({
id: examChallengeId,
challengeType: 17,
userCompletedExam: userExam2
});
test('POST does not update user progress if new exam is not an improvement', async () => {
// Submit exam with 2/3 correct answers
await submitExam(examWithTwoCorrect);
const user2 = await fastifyTestInstance.prisma.user.findFirst({
const user1 = await fastifyTestInstance.prisma.user.findFirstOrThrow({
where: { id: defaultUserId }
});
const completedChallenges2 = user2?.completedChallenges || [];
const completedExams2 = user2?.completedExams || [];
const progressTimestamps2 = user2?.progressTimestamps || [];
// Submit exam with 2/3 correct answers (no improvement)
const res2 = await submitExam(examWithTwoCorrect);
// should not add to or update completedChallenges
expect(completedChallenges2).toHaveLength(2);
expect(completedChallenges2).toMatchObject([
...completedTrophyChallenges,
// should still have old completed challenge (should not update)
completedExamChallenge3
]);
expect(completedChallenges2[1]?.completedDate).toBeLessThan(now2);
const user2 = await fastifyTestInstance.prisma.user.findFirstOrThrow({
where: { id: defaultUserId }
});
// should add to completedExams
expect(completedExams2).toHaveLength(2);
expect(completedExams2[1]).toMatchObject(completedExamChallenge2);
expect(completedExams2[1]?.completedDate).toBeGreaterThan(nowA);
// should not update user progress
expect(user2.completedChallenges).toEqual(user1.completedChallenges);
expect(user2.progressTimestamps).toEqual(user1.progressTimestamps);
// should not add to progressTimestamps
expect(progressTimestamps2).toHaveLength(1);
expect(response2.body).toMatchObject({
expect(res2.body).toMatchObject({
points: 1,
alreadyCompleted: true,
examResults: mockResults2
examResults: mockResultsTwoCorrect
});
expect(response2.statusCode).toBe(200);
expect(res2.statusCode).toBe(200);
});
// Submit exam with 3/3 correct answers (better exam than already submitted)
const now3 = Date.now();
const response3 = await superRequest('/exam-challenge-completed', {
method: 'POST',
setCookies
}).send({
id: examChallengeId,
challengeType: 17,
userCompletedExam: userExam4
});
test('POST updates user progress if exam is an improvement', async () => {
// Submit exam with 2/3 correct answers
await submitExam(examWithTwoCorrect);
const user1 = await fastifyTestInstance.prisma.user.findUniqueOrThrow(
{
where: { id: defaultUserId }
}
);
const user3 = await fastifyTestInstance.prisma.user.findFirst({
// Submit improved exam
const res = await submitExam(examWithAllCorrect);
const user2 = await fastifyTestInstance.prisma.user.findFirstOrThrow({
where: { email: 'foo@bar.com' }
});
const completedChallenges3 = user3?.completedChallenges || [];
const completedExams3 = user3?.completedExams || [];
const progressTimestamps3 = user3?.progressTimestamps || [];
// should update existing completedChallenge
expect(completedChallenges3).toHaveLength(2);
expect(completedChallenges3).toMatchObject([
expect(user2.completedChallenges).toHaveLength(2);
expect(user2.completedChallenges).toMatchObject([
...completedTrophyChallenges,
completedExamChallenge4
completedExamChallengeAllCorrect
]);
expect(completedChallenges3[1]?.completedDate).toBeLessThan(now3);
expect(user2.completedChallenges[1]?.completedDate).toEqual(
user1.completedChallenges[1]?.completedDate
);
// should add to completedExams
expect(completedExams3).toHaveLength(3);
expect(completedExams3[2]).toMatchObject(completedExamChallenge4);
expect(completedExams3[2]?.completedDate).toBeGreaterThan(now3);
// they have not completed anything new, so progressTimestamps should
// remain the same
expect(user2.progressTimestamps).toEqual(user1.progressTimestamps);
expect(progressTimestamps3).toHaveLength(1);
expect(response3.body).toMatchObject({
expect(res.body).toMatchObject({
points: 1,
alreadyCompleted: true,
examResults: mockResults4
examResults: mockResultsAllCorrect
});
expect(response3.statusCode).toBe(200);
expect(res.statusCode).toBe(200);
});
});
});

View File

@@ -1,14 +1,14 @@
import { Exam, Question } from '@prisma/client';
import {
examJson,
userExam1,
userExam2,
userExam3,
userExam4,
mockResults1,
mockResults2,
mockResults3,
mockResults4
examWithZeroCorrect,
examWithOneCorrect,
examWithTwoCorrect,
examWithAllCorrect,
mockResultsZeroCorrect,
mockResultsOneCorrect,
mockResultsTwoCorrect,
mockResultsAllCorrect
} from '../../__mocks__/exam';
import { generateRandomExam, createExamResults } from './exam';
import { GeneratedExam } from './exam-types';
@@ -45,19 +45,31 @@ describe('Exam helpers', () => {
});
describe('createExamResults()', () => {
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);
const examResults1 = createExamResults(
examWithZeroCorrect,
examJson as Exam
);
const examResults2 = createExamResults(
examWithOneCorrect,
examJson as Exam
);
const examResults3 = createExamResults(
examWithTwoCorrect,
examJson as Exam
);
const examResults4 = createExamResults(
examWithAllCorrect,
examJson as Exam
);
it('failing exam should return correct results', () => {
expect(examResults1).toEqual(mockResults1);
expect(examResults1).toEqual(mockResultsZeroCorrect);
});
it('passing exam should return correct results', () => {
expect(examResults2).toEqual(mockResults2);
expect(examResults3).toEqual(mockResults3);
expect(examResults4).toEqual(mockResults4);
expect(examResults2).toEqual(mockResultsOneCorrect);
expect(examResults3).toEqual(mockResultsTwoCorrect);
expect(examResults4).toEqual(mockResultsAllCorrect);
});
});
});