From 948b7c52c2a0b71926e9542b908b0a922f2af2a0 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Sat, 7 Oct 2023 12:50:30 +0200 Subject: [PATCH] fix(api): generate required properties for new users (#51822) --- api/jest.utils.ts | 7 ++- api/src/routes/auth.test.ts | 88 +++++++++++++++++++++++++++++++++-- api/src/routes/auth.ts | 4 +- api/src/routes/user.test.ts | 5 +- api/src/utils/create-user.ts | 76 ++++++++++++++++++++++++++++++ api/src/utils/default-user.ts | 55 ---------------------- 6 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 api/src/utils/create-user.ts delete mode 100644 api/src/utils/default-user.ts diff --git a/api/jest.utils.ts b/api/jest.utils.ts index 698922a058d..599cf62593a 100644 --- a/api/jest.utils.ts +++ b/api/jest.utils.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import { build } from './src/app'; -import { defaultUser } from './src/utils/default-user'; +import { createUserInput } from './src/utils/create-user'; type FastifyTestInstance = Awaited>; @@ -84,9 +84,8 @@ export async function devLogin(): Promise { await fastifyTestInstance.prisma.user.create({ data: { - ...defaultUser, - id: defaultUserId, - email: defaultUserEmail + ...createUserInput(defaultUserEmail), + id: defaultUserId } }); const res = await superRequest('/auth/dev-callback', { method: 'GET' }); diff --git a/api/src/routes/auth.test.ts b/api/src/routes/auth.test.ts index d8e5d59a57e..83f697fd0ae 100644 --- a/api/src/routes/auth.test.ts +++ b/api/src/routes/auth.test.ts @@ -1,15 +1,95 @@ -import { setupServer, superRequest } from '../../jest.utils'; +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { defaultUserEmail, setupServer, superRequest } from '../../jest.utils'; describe('dev login', () => { setupServer(); - it('should create an account if one does not exist', async () => { + beforeEach(async () => { await fastifyTestInstance.prisma.user.deleteMany({ - where: { email: 'foo@bar.com' } + where: { email: defaultUserEmail } + }); + }); + + afterAll(async () => { + await fastifyTestInstance.prisma.user.deleteMany({ + where: { email: defaultUserEmail } + }); + }); + + it('should create an account if one does not exist', async () => { + const res = await superRequest('/auth/dev-callback', { method: 'GET' }); + + const count = await fastifyTestInstance.prisma.user.count({ + where: { email: defaultUserEmail } }); - const res = await superRequest('/auth/dev-callback', { method: 'GET' }); + expect(count).toBe(1); expect(res.body).toStrictEqual({ statusCode: 200 }); expect(res.status).toBe(200); }); + + it('should populate the user with the correct data', async () => { + const uuidRe = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/; + const fccUuidRe = /^fcc-[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/; + + await superRequest('/auth/dev-callback', { method: 'GET' }); + const user = await fastifyTestInstance.prisma.user.findFirstOrThrow({ + where: { email: defaultUserEmail } + }); + + expect(user).toMatchObject({ + about: '', + acceptedPrivacyTerms: false, + completedChallenges: [], + currentChallengeId: '', + email: defaultUserEmail, + emailVerified: true, + externalId: expect.stringMatching(uuidRe), + is2018DataVisCert: false, + is2018FullStackCert: false, + isApisMicroservicesCert: false, + isBackEndCert: false, + isBanned: false, + isCheater: false, + isDataAnalysisPyCertV7: false, + isDataVisCert: false, + isDonating: false, + isFrontEndCert: false, + isFrontEndLibsCert: false, + isFullStackCert: false, + isHonest: false, + isInfosecCertV7: false, + isInfosecQaCert: false, + isJsAlgoDataStructCert: false, + isMachineLearningPyCertV7: false, + isQaCertV7: false, + isRelationalDatabaseCertV8: false, + isCollegeAlgebraPyCertV8: false, + isRespWebDesignCert: false, + isSciCompPyCertV7: false, + keyboardShortcuts: false, + location: '', + name: '', + unsubscribeId: '', + picture: '', + profileUI: { + isLocked: false, + showAbout: false, + showCerts: false, + showDonation: false, + showHeatMap: false, + showLocation: false, + showName: false, + showPoints: false, + showPortfolio: false, + showTimeLine: false + }, + progressTimestamps: [], + sendQuincyEmail: false, + theme: 'default', + username: expect.stringMatching(fccUuidRe), + usernameDisplay: expect.stringMatching(fccUuidRe) + }); + expect(user.username).toBe(user.usernameDisplay); + }); }); diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index 40ddece7492..06f8e94c925 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -4,7 +4,7 @@ import { FastifyRequest } from 'fastify'; -import { defaultUser } from '../utils/default-user'; +import { createUserInput } from '../utils/create-user'; import { AUTH0_DOMAIN, HOME_LOCATION } from '../utils/env'; declare module 'fastify' { @@ -41,7 +41,7 @@ const findOrCreateUser = async (fastify: FastifyInstance, email: string) => { return ( existingUser ?? (await fastify.prisma.user.create({ - data: { ...defaultUser, email }, + data: createUserInput(email), select: { id: true } })) ); diff --git a/api/src/routes/user.test.ts b/api/src/routes/user.test.ts index 6db961abcc4..fef9a51ea4f 100644 --- a/api/src/routes/user.test.ts +++ b/api/src/routes/user.test.ts @@ -6,7 +6,7 @@ import type { Prisma } from '@prisma/client'; import { ObjectId } from 'mongodb'; import _ from 'lodash'; -import { defaultUser } from '../utils/default-user'; +import { createUserInput } from '../utils/create-user'; import { defaultUserId, defaultUserEmail, @@ -18,8 +18,7 @@ import { JWT_SECRET } from '../utils/env'; // This is used to build a test user. const testUserData: Prisma.userCreateInput = { - ...defaultUser, - email: defaultUserEmail, + ...createUserInput(defaultUserEmail), username: 'foobar', usernameDisplay: 'Foo Bar', progressTimestamps: [1520002973119, 1520440323273], diff --git a/api/src/utils/create-user.ts b/api/src/utils/create-user.ts new file mode 100644 index 00000000000..0c24d9df4b2 --- /dev/null +++ b/api/src/utils/create-user.ts @@ -0,0 +1,76 @@ +import crypto from 'node:crypto'; + +import { type Prisma } from '@prisma/client'; + +/** + * Creates the necessary data to create a new user. + * @param email The email address of the new user. + * @returns Default data for a new user. + */ +export function createUserInput(email: string): Prisma.userCreateInput { + const username = 'fcc-' + crypto.randomUUID(); + const externalId = crypto.randomUUID(); + // This explicitly includes all array fields. This is not strictly necessary - + // Prisma will return an empty array even if the property is missing, but it's + // probably best to add them to the document, at least until we normalise the + // data. + return { + about: '', + acceptedPrivacyTerms: false, + completedChallenges: [], // 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, + emailVerified: true, // this should be true until a user changes their email address + // TODO(Post-MVP): remove externalId? + externalId, + is2018DataVisCert: false, + is2018FullStackCert: false, + isApisMicroservicesCert: false, + isBackEndCert: false, + isBanned: false, + isCheater: false, + isDataAnalysisPyCertV7: false, + isDataVisCert: false, + isDonating: false, + isFrontEndCert: false, + isFrontEndLibsCert: false, + isFullStackCert: false, + isHonest: false, + isInfosecCertV7: false, + isInfosecQaCert: false, + isJsAlgoDataStructCert: false, + isMachineLearningPyCertV7: false, + isQaCertV7: false, + isRelationalDatabaseCertV8: false, + isCollegeAlgebraPyCertV8: false, + isRespWebDesignCert: false, + isSciCompPyCertV7: false, + keyboardShortcuts: false, + location: '', + name: '', + unsubscribeId: '', + partiallyCompletedChallenges: [], // TODO(Post-MVP): Omit this from the document? (prisma will always return []) + picture: '', + portfolio: [], // TODO(Post-MVP): Omit this from the document? (prisma will always return []) + profileUI: { + isLocked: false, + showAbout: false, + showCerts: false, + showDonation: false, + showHeatMap: false, + showLocation: false, + showName: false, + showPoints: false, + showPortfolio: false, + showTimeLine: false + }, + progressTimestamps: [], // TODO(Post-MVP): This may need normalising before we can omit it. + savedChallenges: [], // TODO(Post-MVP): Omit this from the document? (prisma will always return []) + sendQuincyEmail: false, + theme: 'default', + username, + usernameDisplay: username, + yearsTopContributor: [] // TODO: Omit this from the document? (prisma will always return []) + }; +} diff --git a/api/src/utils/default-user.ts b/api/src/utils/default-user.ts deleted file mode 100644 index 65920fef981..00000000000 --- a/api/src/utils/default-user.ts +++ /dev/null @@ -1,55 +0,0 @@ -// TODO: audit this object to find out which properties need to be updated. -import { type Prisma } from '@prisma/client'; - -export const defaultUser: Omit = { - about: '', - acceptedPrivacyTerms: false, - completedChallenges: [], - currentChallengeId: '', - emailVerified: true, // this should be true until a user changes their email address - externalId: '', - is2018DataVisCert: false, - is2018FullStackCert: false, - isApisMicroservicesCert: false, - isBackEndCert: false, - isBanned: false, - isCheater: false, - isDataAnalysisPyCertV7: false, - isDataVisCert: false, - isDonating: false, - isFrontEndCert: false, - isFrontEndLibsCert: false, - isFullStackCert: false, - isHonest: false, - isInfosecCertV7: false, - isInfosecQaCert: false, - isJsAlgoDataStructCert: false, - isMachineLearningPyCertV7: false, - isQaCertV7: false, - isRelationalDatabaseCertV8: false, - isCollegeAlgebraPyCertV8: false, - isRespWebDesignCert: false, - isSciCompPyCertV7: false, - keyboardShortcuts: false, - location: '', - name: '', - unsubscribeId: '', - picture: '', - profileUI: { - isLocked: false, - showAbout: false, - showCerts: false, - showDonation: false, - showHeatMap: false, - showLocation: false, - showName: false, - showPoints: false, - showPortfolio: false, - showTimeLine: false - }, - progressTimestamps: [], - sendQuincyEmail: false, - theme: 'default', - // TODO: generate a UUID like in api-server - username: '' -};