From 8138f086aa8b2e00d272d87b0772621a8391243e Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Fri, 23 May 2025 14:56:18 +0200 Subject: [PATCH] fix(api): handle string challengeType (#60491) --- api/prisma/schema.prisma | 2 +- api/src/routes/protected/challenge.ts | 11 ++++-- api/src/utils/normalize.test.ts | 22 ++++++++++-- api/src/utils/normalize.ts | 51 ++++++++++++++++++++------- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index e748beef93c..fb4cf1b96ad 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -19,7 +19,7 @@ type File { } type CompletedChallenge { - challengeType Int? @db.Int // Null | Undefined + challengeType Json? // Null | Undefined | String | Int completedDate Json // DateTime | Float, but not, as far as we know, Null files File[] githubLink String? // Undefined diff --git a/api/src/routes/protected/challenge.ts b/api/src/routes/protected/challenge.ts index 30a95cbf376..0cde3eb3ffa 100644 --- a/api/src/routes/protected/challenge.ts +++ b/api/src/routes/protected/challenge.ts @@ -35,7 +35,7 @@ import { verifyTrophyWithMicrosoft } from '../helpers/challenge-helpers'; import { UpdateReqType } from '../../utils'; -import { normalizeDate } from '../../utils/normalize'; +import { normalizeChallengeType, normalizeDate } from '../../utils/normalize'; interface JwtPayload { userToken: string; @@ -670,8 +670,13 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = ( const newCompletedChallenges: CompletedChallenge[] = completedChallenges.map(c => { - const { completedDate, ...rest } = c; - return { completedDate: normalizeDate(completedDate), ...rest }; + const { completedDate, challengeType, ...rest } = c; + + return { + completedDate: normalizeDate(completedDate), + challengeType: normalizeChallengeType(challengeType), + ...rest + }; }); const newCompletedExams: CompletedExam[] = completedExams; const newProgressTimeStamps = progressTimestamps as ProgressTimestamp[]; diff --git a/api/src/utils/normalize.test.ts b/api/src/utils/normalize.test.ts index 94a38321f6e..65c9a8d1ba1 100644 --- a/api/src/utils/normalize.test.ts +++ b/api/src/utils/normalize.test.ts @@ -3,7 +3,8 @@ import { normalizeProfileUI, normalizeChallenges, normalizeFlags, - normalizeDate + normalizeDate, + normalizeChallengeType } from './normalize'; describe('normalize', () => { @@ -158,7 +159,7 @@ describe('normalize', () => { }); }); - describe('validateDate', () => { + describe('normalizeDate', () => { it('should return the date as a number', () => { expect(normalizeDate(1)).toEqual(1); expect(normalizeDate({ $date: '2023-10-01T00:00:00Z' })).toEqual( @@ -175,4 +176,21 @@ describe('normalize', () => { ); }); }); + + describe('normalizeChallengeType', () => { + it('should return the challenge type as a number or null', () => { + expect(normalizeChallengeType(10)).toEqual(10); + expect(normalizeChallengeType('10')).toEqual(10); + expect(normalizeChallengeType(null)).toEqual(null); + }); + + it('should throw an error if the challenge type is not in the expected shape', () => { + expect(() => normalizeChallengeType('invalid')).toThrow( + 'Unexpected challengeType value: "invalid"' + ); + expect(() => normalizeChallengeType({ type: '123' })).toThrow( + 'Unexpected challengeType value: {"type":"123"}' + ); + }); + }); }); diff --git a/api/src/utils/normalize.ts b/api/src/utils/normalize.ts index 4bd341dcddb..0c621c4eb09 100644 --- a/api/src/utils/normalize.ts +++ b/api/src/utils/normalize.ts @@ -61,6 +61,34 @@ export const normalizeDate = (date?: Prisma.JsonValue): number => { } }; +/** + * Normalizes a challenge type value to a number. + * + * @param challengeType A JSON value that can be a number, string, or null. + * @returns The challenge type as a number or null. + */ +export const normalizeChallengeType = ( + challengeType?: Prisma.JsonValue +): number | null => { + if (typeof challengeType === 'number') { + return challengeType; + } else if (typeof challengeType === 'string') { + const parsed = parseInt(challengeType, 10); + if (isNaN(parsed)) { + throw Error( + 'Unexpected challengeType value: ' + JSON.stringify(challengeType) + ); + } + return parsed; + } else if (challengeType === null) { + return null; + } else { + throw Error( + 'Unexpected challengeType value: ' + JSON.stringify(challengeType) + ); + } +}; + /** * Ensure that the user's profile UI settings are valid. * @@ -125,9 +153,16 @@ export type NormalizedChallenge = { export const normalizeChallenges = ( completedChallenges: CompletedChallenge[] ): NormalizedChallenge[] => { - const noNullProps = completedChallenges.map(challenge => - removeNulls(challenge) - ); + const fixedDateAndType = completedChallenges.map(challenge => { + const { completedDate, challengeType, ...rest } = challenge; + return { + ...rest, + completedDate: normalizeDate(completedDate), + challengeType: normalizeChallengeType(challengeType) + }; + }); + + const noNullProps = fixedDateAndType.map(challenge => removeNulls(challenge)); // files.path is optional const noNullPath = noNullProps.map(challenge => { const { files, ...rest } = challenge; @@ -136,15 +171,7 @@ export const normalizeChallenges = ( return { ...rest, files: noNullFiles }; }); - const withNumberDates = noNullPath.map(challenge => { - const { completedDate, ...rest } = challenge; - return { - ...rest, - completedDate: normalizeDate(completedDate) - }; - }); - - return withNumberDates; + return noNullPath; }; type NormalizedSurvey = {