fix(api): use challenge helper to update completed challenges (#55046)

This commit is contained in:
Oliver Eyton-Williams
2024-06-20 10:44:55 +02:00
committed by GitHub
parent ee26d4333c
commit 19b5134732
4 changed files with 81 additions and 156 deletions
+1 -2
View File
@@ -550,7 +550,7 @@ describe('challengeRoutes', () => {
// If a challenge has already been completed, it should return the
// original completedDate
expect(resUpdate.body.completedDate).not.toBe(
expect(resUpdate.body.completedDate).toBe(
resOriginal.body.completedDate
);
expect(resUpdate.statusCode).toBe(200);
@@ -1201,7 +1201,6 @@ describe('challengeRoutes', () => {
completedDate
});
// TODO: use a custom matcher for thisu
expect(completedDate).toBeGreaterThan(now);
expect(completedDate).toBeLessThan(now + 1000);
expect(res.statusCode).toBe(200);
+43 -54
View File
@@ -31,8 +31,6 @@ import {
import { generateRandomExam, createExamResults } from '../utils/exam';
import {
canSubmitCodeRoadCertProject,
createProject,
updateProject,
verifyTrophyWithMicrosoft
} from './helpers/challenge-helpers';
@@ -40,6 +38,17 @@ interface JwtPayload {
userToken: string;
}
// TODO(Post-MVP): This could be narrowed down to only the fields needed by
// specific endpoints, but that means complicating the update helper.
const userChallengeSelect = {
id: true,
completedChallenges: true,
partiallyCompletedChallenges: true,
progressTimestamps: true,
needsModeration: true,
savedChallenges: true
};
/**
* Plugin for the challenge submission endpoints.
*
@@ -218,11 +227,7 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
const user = await fastify.prisma.user.findUniqueOrThrow({
where: { id: userId },
select: {
completedChallenges: true,
partiallyCompletedChallenges: true,
progressTimestamps: true
}
select: userChallengeSelect
});
if (
@@ -237,33 +242,22 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
} as const;
}
const completedDate = Date.now();
const oldChallenge = user.completedChallenges?.find(
({ id }) => id === projectId
);
const updatedChallenge = {
const challenge = {
challengeType,
solution,
githubLink
};
const newChallenge = {
...updatedChallenge,
githubLink,
id: projectId,
completedDate
completedDate: Date.now()
};
const alreadyCompleted = !!oldChallenge;
const progressTimestamps = user.progressTimestamps as ProgressTimestamp[];
const points = getPoints(progressTimestamps);
const data = alreadyCompleted
? updateProject(projectId, updatedChallenge)
: createProject(projectId, newChallenge, progressTimestamps);
await fastify.prisma.user.update({
where: { id: userId },
data
});
const { alreadyCompleted, completedDate } = await updateUserChallengeData(
fastify,
user,
projectId,
challenge
);
return {
alreadyCompleted,
@@ -290,7 +284,9 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
},
async req => {
const user = await fastify.prisma.user.findUniqueOrThrow({
where: { id: req.user?.id }
where: { id: req.user?.id },
select: userChallengeSelect
});
const progressTimestamps = user.progressTimestamps as
| ProgressTimestamp[]
@@ -334,7 +330,8 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
const { id, files, challengeType } = req.body;
const user = await fastify.prisma.user.findUniqueOrThrow({
where: { id: req.user?.id }
where: { id: req.user?.id },
select: userChallengeSelect
});
const RawProgressTimestamp = user.progressTimestamps as
| ProgressTimestamp[]
@@ -549,36 +546,25 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
const user = await fastify.prisma.user.findUniqueOrThrow({
where: { id: req.user?.id },
select: { completedChallenges: true, progressTimestamps: true }
select: userChallengeSelect
});
const { completedChallenges } = user;
const progressTimestamps =
user.progressTimestamps as ProgressTimestamp[];
const oldChallenge = completedChallenges.find(
({ id }) => id === challengeId
);
const alreadyCompleted = !!oldChallenge;
const completedDate = alreadyCompleted
? oldChallenge.completedDate
: Date.now();
if (!alreadyCompleted) {
const newChallenge = {
id: challengeId,
completedDate,
solution: msTrophyStatus.msUserAchievementsApiUrl
};
await fastify.prisma.user.update({
where: { id: req.user?.id },
data: {
completedChallenges: {
push: newChallenge
},
progressTimestamps: [...progressTimestamps, completedDate]
}
});
}
const completedChallenge = {
id: challengeId,
solution: msTrophyStatus.msUserAchievementsApiUrl,
completedDate: Date.now()
};
const { alreadyCompleted, completedDate } =
await updateUserChallengeData(
fastify,
user,
challengeId,
completedChallenge
);
return {
alreadyCompleted,
@@ -730,6 +716,9 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
newCompletedChallenges[alreadyCompletedIndex] = updatedChallege;
// TODO(Post-MVP): Try to DRY the updates.
// updateUserChallengeData, for all its faults, handles the
// update/insert logic well.
await fastify.prisma.user.update({
where: { id: userId },
data: {
@@ -1,7 +1,3 @@
import type { Prisma } from '@prisma/client';
import type { ProgressTimestamp } from '../../utils/progress';
/**
* Confirm that a user can submit a CodeRoad project.
*
@@ -26,39 +22,6 @@ export const canSubmitCodeRoadCertProject = (
return false;
};
/**
* Create the Prisma query to update a project.
* @param id The id of the project.
* @param newChallenge The challenge corresponding to the project.
* @returns A Prisma query to update the project.
*/
export const updateProject = (
id: string,
newChallenge: Prisma.CompletedChallengeUpdateInput
) => ({
completedChallenges: {
updateMany: { where: { id }, data: newChallenge }
},
partiallyCompletedChallenges: { deleteMany: { where: { id } } }
});
/**
* Create the Prisma query to create a project.
* @param id The id of the project.
* @param newChallenge The challenge corresponding to the project.
* @param progressTimestamps The user's current progress timestamps.
* @returns A Prisma query to update the project.
*/
export const createProject = (
id: string,
newChallenge: Prisma.CompletedChallengeCreateInput,
progressTimestamps: ProgressTimestamp[]
) => ({
completedChallenges: { push: newChallenge },
partiallyCompletedChallenges: { deleteMany: { where: { id } } },
progressTimestamps: [...progressTimestamps, newChallenge.completedDate]
});
type MSProfileError = {
type: 'error';
message: 'flash.ms.profile.err';
+37 -63
View File
@@ -107,21 +107,26 @@ export function saveUserChallengeData(
/**
* Helper function to update a user's challenge data. Used in challenge
* submission endpoints.
*
* @deprecated Create specific functions for each submission endpoint.
* TODO: Keep refactoring. This function does too much.
* @param fastify The Fastify instance.
* @param user The existing user record.
* @param challengeId The id of the submitted challenge.
* @param _completedChallenge The challenge submission.
* @param timezone The user's timezone.
* @returns Information about the update.
*/
export async function updateUserChallengeData(
fastify: FastifyInstance,
user: user,
user: Pick<
user,
| 'id'
| 'completedChallenges'
| 'needsModeration'
| 'savedChallenges'
| 'progressTimestamps'
| 'partiallyCompletedChallenges'
>,
challengeId: string,
_completedChallenge: CompletedChallenge,
timezone?: string // TODO: is this required as its not given as a arg anywhere
_completedChallenge: CompletedChallenge
) {
const { files, completedDate: newProgressTimeStamp = Date.now() } =
_completedChallenge;
@@ -149,11 +154,8 @@ export async function updateUserChallengeData(
} else {
completedChallenge = omit(_completedChallenge, ['files']);
}
let finalChallenge = {} as CompletedChallenge;
// Since these values are destuctured for easier updating, collectively update before returning
const {
timezone: userTimezone,
completedChallenges = [],
needsModeration = false,
savedChallenges = [],
@@ -161,36 +163,26 @@ export async function updateUserChallengeData(
partiallyCompletedChallenges = []
} = user;
const userCompletedChallenges: CompletedChallenge[] = completedChallenges;
const userSavedChallenges: SavedChallenge[] = savedChallenges;
const userProgressTimestamps = progressTimestamps;
const userPartiallyCompletedChallenges = partiallyCompletedChallenges;
let userSavedChallenges = savedChallenges;
const oldIndex = userCompletedChallenges.findIndex(
({ id }) => challengeId === id
);
const oldChallenge = completedChallenges.find(({ id }) => challengeId === id);
const alreadyCompleted = !!oldChallenge;
const alreadyCompleted = oldIndex !== -1;
const oldChallenge = alreadyCompleted
? userCompletedChallenges[oldIndex]
: null;
const finalChallenge = alreadyCompleted
? {
...completedChallenge,
completedDate: oldChallenge.completedDate
}
: completedChallenge;
if (alreadyCompleted && oldChallenge) {
finalChallenge = {
...completedChallenge,
completedDate: oldChallenge.completedDate
};
const userCompletedChallenges = alreadyCompleted
? completedChallenges.map(x => (x.id === challengeId ? finalChallenge : x))
: [...completedChallenges, finalChallenge];
userCompletedChallenges[oldIndex] = finalChallenge;
} else {
finalChallenge = {
...completedChallenge
};
if (userProgressTimestamps && Array.isArray(userProgressTimestamps)) {
userProgressTimestamps.push(newProgressTimeStamp);
}
userCompletedChallenges.push(finalChallenge);
}
const userProgressTimestamps =
!alreadyCompleted && progressTimestamps && Array.isArray(progressTimestamps)
? [...progressTimestamps, newProgressTimeStamp]
: progressTimestamps;
if (
multifileCertProjectIds.includes(challengeId) ||
@@ -204,37 +196,19 @@ export async function updateUserChallengeData(
) as SavedChallengeFile[]
};
const savedIndex = userSavedChallenges.findIndex(
({ id }) => challengeId === id
);
const isSaved = userSavedChallenges.some(({ id }) => challengeId === id);
if (savedIndex >= 0) {
userSavedChallenges[savedIndex] = challengeToSave;
} else {
userSavedChallenges.push(challengeToSave);
}
userSavedChallenges = isSaved
? userSavedChallenges.map(x =>
x.id === challengeId ? challengeToSave : x
)
: [...userSavedChallenges, challengeToSave];
}
// remove from partiallyCompleted on submit
const updatedPartiallyCompletedChallenges =
userPartiallyCompletedChallenges.filter(
challenge => challenge.id !== challengeId
);
if (
timezone &&
timezone !== 'UTC' &&
!userTimezone &&
userTimezone === 'UTC'
) {
timezone = userTimezone;
await fastify.prisma.user.update({
where: { id: user.id },
data: {
timezone
}
});
}
const userPartiallyCompletedChallenges = partiallyCompletedChallenges.filter(
challenge => challenge.id !== challengeId
);
if (needsModeration) {
await fastify.prisma.user.update({
@@ -252,7 +226,7 @@ export async function updateUserChallengeData(
needsModeration,
savedChallenges: userSavedChallenges,
progressTimestamps: userProgressTimestamps,
partiallyCompletedChallenges: updatedPartiallyCompletedChallenges
partiallyCompletedChallenges: userPartiallyCompletedChallenges
}
});