From 9bc4936049199ff9743eb11d78b38a1efdf47176 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 9 May 2024 18:31:56 +0200 Subject: [PATCH] feat: implement responses for locked profile --- api/src/routes/user.test.ts | 52 ++++++++++++++++--- api/src/routes/user.ts | 22 ++++++++ .../schemas/api/users/get-public-profile.ts | 21 +++++++- api/src/schemas/types.ts | 13 +++++ api/src/schemas/user/get-session-user.ts | 17 +----- 5 files changed, 102 insertions(+), 23 deletions(-) diff --git a/api/src/routes/user.test.ts b/api/src/routes/user.test.ts index 70446a7b7f09a9..acdf3ad1e0f53b 100644 --- a/api/src/routes/user.test.ts +++ b/api/src/routes/user.test.ts @@ -1096,11 +1096,31 @@ Thanks and regards, }); describe('/api/users/get-public-profile', () => { + const profilelessUser = 'profileless-user'; + const lockedUser = 'locked-user'; + const lockedUserProfileUI = { + isLocked: true, + showAbout: true, + showPortfolio: false + }; + const users = [profilelessUser, lockedUser]; beforeAll(async () => { + await fastifyTestInstance.prisma.user.create({ + data: { ...minimalUserData, username: profilelessUser } + }); await fastifyTestInstance.prisma.user.create({ data: { ...minimalUserData, - username: 'testuser' + username: lockedUser, + profileUI: lockedUserProfileUI + } + }); + }); + + afterAll(async () => { + await fastifyTestInstance.prisma.user.deleteMany({ + where: { + OR: users.map(username => ({ username })) } }); }); @@ -1131,23 +1151,43 @@ Thanks and regards, test('returns 200 status code with a locked profile if the profile is private', async () => { const response = await superGet( - '/api/users/get-public-profile?username=testuser' + `/api/users/get-public-profile?username=${lockedUser}` ); + expect(response.body).toStrictEqual({ entities: { user: { - 'private-user': { + [lockedUser]: { isLocked: true, - profileUI: lockedProfileUI, - username: 'private-user' + profileUI: lockedUserProfileUI, + username: lockedUser } } }, - result: 'private-user' + result: lockedUser }); expect(response.statusCode).toBe(200); }); + test('returns 200 status code locked profile if the profile is missing', async () => { + const response = await superGet( + `/api/users/get-public-profile?username=${profilelessUser}` + ); + + expect(response.body).toStrictEqual({ + entities: { + user: { + [profilelessUser]: { + isLocked: true, + profileUI: lockedProfileUI, + username: profilelessUser + } + } + }, + result: profilelessUser + }); + expect(response.statusCode).toBe(200); + }); test.todo('returns 200 status code with public user object'); }); }); diff --git a/api/src/routes/user.ts b/api/src/routes/user.ts index eaad94baa3e837..d16ec16630988d 100644 --- a/api/src/routes/user.ts +++ b/api/src/routes/user.ts @@ -610,6 +610,28 @@ export const userPublicGetRoutes: FastifyPluginCallbackTypebox = ( void reply.code(404); return reply.send({}); } + + // The old API would crash if it tried to handle users without profileUI, + // instead of crashing we can handle it gracefully. + const profileUI = normalizeProfileUI(user.profileUI); + + if (profileUI.isLocked) { + void reply.code(200); + return reply.send({ + // TODO(Post-MVP): just return isLocked and an empty profileUI. No + // need for entities or result. + entities: { + user: { + [user.username]: { + isLocked: true, + profileUI, + username: user.username + } + } + }, + result: user.username + }); + } } ); diff --git a/api/src/schemas/api/users/get-public-profile.ts b/api/src/schemas/api/users/get-public-profile.ts index 4e3f94ec852d0f..2a3ada2bf53835 100644 --- a/api/src/schemas/api/users/get-public-profile.ts +++ b/api/src/schemas/api/users/get-public-profile.ts @@ -1,11 +1,28 @@ import { Type } from '@fastify/type-provider-typebox'; +import { profileUI } from '../../types'; export const getPublicProfile = { querystring: Type.Object({ username: Type.String({ minLength: 1 }) }), response: { - 400: Type.Object({}), - 404: Type.Object({}) + 200: Type.Object({ + entities: Type.Object({ + user: Type.Record( + Type.String(), + Type.Object({ + isLocked: Type.Boolean(), + profileUI, + username: Type.String() + }) + ) + }), + result: Type.String() + }), + // We can't simply have Type.Object({}), even though that's correct, because + // TypeScript will then accept all responses (since every object can be + // assigned to {}) + 400: Type.Object({ entities: Type.Optional(Type.Never()) }), + 404: Type.Object({ entities: Type.Optional(Type.Never()) }) } }; diff --git a/api/src/schemas/types.ts b/api/src/schemas/types.ts index 10889e8129f90b..5b4c952dc05c03 100644 --- a/api/src/schemas/types.ts +++ b/api/src/schemas/types.ts @@ -57,3 +57,16 @@ export const examResults = Type.Object({ export const surveyTitles = Type.Union([ Type.Literal('Foundational C# with Microsoft Survey') ]); + +export const profileUI = Type.Object({ + isLocked: Type.Optional(Type.Boolean()), + showAbout: Type.Optional(Type.Boolean()), + showCerts: Type.Optional(Type.Boolean()), + showDonation: Type.Optional(Type.Boolean()), + showHeatMap: Type.Optional(Type.Boolean()), + showLocation: Type.Optional(Type.Boolean()), + showName: Type.Optional(Type.Boolean()), + showPoints: Type.Optional(Type.Boolean()), + showPortfolio: Type.Optional(Type.Boolean()), + showTimeLine: Type.Optional(Type.Boolean()) +}); diff --git a/api/src/schemas/user/get-session-user.ts b/api/src/schemas/user/get-session-user.ts index 9e22ca39483fdb..5f57b6b7b5e32d 100644 --- a/api/src/schemas/user/get-session-user.ts +++ b/api/src/schemas/user/get-session-user.ts @@ -1,5 +1,5 @@ import { Type } from '@fastify/type-provider-typebox'; -import { examResults, saveChallengeBody } from '../types'; +import { examResults, saveChallengeBody, profileUI } from '../types'; export const getSessionUser = { response: { @@ -89,20 +89,7 @@ export const getSessionUser = { url: Type.String() }) ), - profileUI: Type.Optional( - Type.Object({ - isLocked: Type.Optional(Type.Boolean()), - showAbout: Type.Optional(Type.Boolean()), - showCerts: Type.Optional(Type.Boolean()), - showDonation: Type.Optional(Type.Boolean()), - showHeatMap: Type.Optional(Type.Boolean()), - showLocation: Type.Optional(Type.Boolean()), - showName: Type.Optional(Type.Boolean()), - showPoints: Type.Optional(Type.Boolean()), - showPortfolio: Type.Optional(Type.Boolean()), - showTimeLine: Type.Optional(Type.Boolean()) - }) - ), + profileUI: Type.Optional(profileUI), sendQuincyEmail: Type.Boolean(), theme: Type.Optional(Type.String()), twitter: Type.Optional(Type.String()),