fix(api): handle Date values for completedChallenge.completedDate (#60400)

This commit is contained in:
Oliver Eyton-Williams
2025-05-16 13:59:06 +02:00
committed by GitHub
parent c4a40f7a07
commit f5b0071a68
7 changed files with 89 additions and 24 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ type File {
type CompletedChallenge {
challengeType Int? @db.Int // Null | Undefined
completedDate Float // TODO(Post-MVP): Change to DateTime
completedDate Json // DateTime | Float, but not, as far as we know, Null
files File[]
githubLink String? // Undefined
id String
+10 -4
View File
@@ -1,7 +1,9 @@
import { Prisma } from '@prisma/client';
import {
certSlugTypeMap,
certIds
} from '../../../../shared/config/certification-settings';
import { normalizeDate } from '../../utils/normalize';
const {
legacyInfosecQaId,
@@ -41,12 +43,16 @@ export function isKnownCertSlug(
* @returns The latest certification date or the completed date if no certification is found.
*/
export function getFallbackFullStackDate(
completedChallenges: { id: string; completedDate: number }[],
completedDate: number
) {
completedChallenges: { id: string; completedDate: Prisma.JsonValue }[],
completedDate: Prisma.JsonValue
): number {
const latestCertDate = completedChallenges
.filter(chal => fullStackCertificateIds.includes(chal.id))
.map(chal => ({
...chal,
completedDate: normalizeDate(chal.completedDate)
}))
.sort((a, b) => b.completedDate - a.completedDate)[0]?.completedDate;
return latestCertDate ?? completedDate;
return latestCertDate ?? normalizeDate(completedDate);
}
+13 -10
View File
@@ -35,6 +35,7 @@ import {
verifyTrophyWithMicrosoft
} from '../helpers/challenge-helpers';
import { UpdateReqType } from '../../utils';
import { normalizeDate } from '../../utils/normalize';
interface JwtPayload {
userToken: string;
@@ -263,13 +264,12 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
'User tried to submit a codeRoad cert project before completing the required challenges'
);
void reply.code(403);
return {
return reply.send({
type: 'error',
message:
'You have to complete the project before you can submit a URL.'
} as const;
});
}
const challenge = {
challengeType,
solution,
@@ -287,13 +287,13 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
challenge
);
return {
reply.send({
alreadyCompleted,
// TODO(Post-MVP): audit the client and remove this if the client does
// not use it.
completedDate,
completedDate: normalizeDate(completedDate),
points: alreadyCompleted ? points : points + 1
};
});
}
);
@@ -685,11 +685,11 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
completedChallenge
);
return {
reply.send({
alreadyCompleted,
points: getPoints(progressTimestamps) + (alreadyCompleted ? 0 : 1),
completedDate
};
completedDate: normalizeDate(completedDate)
});
} catch (error) {
logger.error(error, 'Error submitting Microsoft trophy challenge');
fastify.Sentry.captureException(error);
@@ -808,7 +808,10 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
}
const newCompletedChallenges: CompletedChallenge[] =
completedChallenges;
completedChallenges.map(c => {
const { completedDate, ...rest } = c;
return { completedDate: normalizeDate(completedDate), ...rest };
});
const newCompletedExams: CompletedExam[] = completedExams;
const newProgressTimeStamps = progressTimestamps as ProgressTimestamp[];
const completedDate = Date.now();
+3 -2
View File
@@ -13,6 +13,7 @@ import {
getFallbackFullStackDate,
isKnownCertSlug
} from '../helpers/certificate-utils';
import { normalizeDate } from '../../utils/normalize';
/**
* Plugin for the unprotected certificate endpoints.
@@ -228,7 +229,7 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = (
certSlug,
certTitle,
username,
date: completedDate,
date: normalizeDate(completedDate),
completionTime
});
}
@@ -239,7 +240,7 @@ export const unprotectedCertificateRoutes: FastifyPluginCallbackTypebox = (
certTitle,
username,
name,
date: completedDate,
date: normalizeDate(completedDate),
completionTime
});
}
+9 -3
View File
@@ -3,6 +3,7 @@ import { FastifyInstance } from 'fastify';
import { omit, pick } from 'lodash';
import { challengeTypes } from '../../../shared/config/challenge-types';
import { getChallenges } from './get-challenges';
import { normalizeDate } from './normalize';
export const jsCertProjectIds = [
'aaa48de84e1ecc7c742e1124',
@@ -149,7 +150,8 @@ export async function updateUserChallengeData(
'path',
'ext'
]) as CompletedChallengeFile
)
),
completedDate: normalizeDate(_completedChallenge.completedDate)
};
} else {
completedChallenge = omit(_completedChallenge, ['files']);
@@ -171,7 +173,7 @@ export async function updateUserChallengeData(
const finalChallenge = alreadyCompleted
? {
...completedChallenge,
completedDate: oldChallenge.completedDate
completedDate: normalizeDate(oldChallenge.completedDate)
}
: completedChallenge;
@@ -180,7 +182,11 @@ export async function updateUserChallengeData(
// check and update some property of the user record such that the same update
// can't be applied twice.
const userCompletedChallenges = alreadyCompleted
? completedChallenges.map(x => (x.id === challengeId ? finalChallenge : x))
? completedChallenges.map(x =>
x.id === challengeId
? finalChallenge
: { ...x, completedDate: normalizeDate(x.completedDate) }
)
: { push: finalChallenge };
// We can't use push, because progressTimestamps is a JSON blob and, until
+20 -1
View File
@@ -2,7 +2,8 @@ import {
normalizeTwitter,
normalizeProfileUI,
normalizeChallenges,
normalizeFlags
normalizeFlags,
normalizeDate
} from './normalize';
describe('normalize', () => {
@@ -156,4 +157,22 @@ describe('normalize', () => {
});
});
});
describe('validateDate', () => {
it('should return the date as a number', () => {
expect(normalizeDate(1)).toEqual(1);
expect(normalizeDate({ $date: '2023-10-01T00:00:00Z' })).toEqual(
1696118400000
);
});
it('should throw an error if the date is not in the expected shape', () => {
expect(() => normalizeDate('2023-10-01T00:00:00Z')).toThrow(
'Unexpected date value: "2023-10-01T00:00:00Z"'
);
expect(() => normalizeDate({ date: '123' })).toThrow(
'Unexpected date value: {"date":"123"}'
);
});
});
});
+33 -3
View File
@@ -1,10 +1,11 @@
/* This module's job is to parse the database output and prepare it for
serialization */
import {
import type {
ProfileUI,
CompletedChallenge,
ExamResults,
type Survey
Survey,
Prisma
} from '@prisma/client';
import _ from 'lodash';
@@ -39,6 +40,27 @@ export const normalizeTwitter = (
return url ?? handleOrUrl;
};
/**
* Normalizes a date value to a timestamp number.
*
* @param date An object with a $date string or a number.
* @returns The date as a timestamp number.
*/
export const normalizeDate = (date?: Prisma.JsonValue): number => {
if (typeof date === 'number') {
return date;
} else if (
date &&
typeof date === 'object' &&
'$date' in date &&
typeof date.$date === 'string'
) {
return new Date(date.$date).getTime();
} else {
throw Error('Unexpected date value: ' + JSON.stringify(date));
}
};
/**
* Ensure that the user's profile UI settings are valid.
*
@@ -114,7 +136,15 @@ export const normalizeChallenges = (
return { ...rest, files: noNullFiles };
});
return noNullPath;
const withNumberDates = noNullPath.map(challenge => {
const { completedDate, ...rest } = challenge;
return {
...rest,
completedDate: normalizeDate(completedDate)
};
});
return withNumberDates;
};
type NormalizedSurvey = {