From a980ac03e5003bcdb9c2c6dfc66c49df7368a8a3 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Tue, 18 Apr 2023 17:01:26 +0200 Subject: [PATCH] feat: introduce /update-my-profileui route in new API (#49827) * feat: add response codes * fix: update TypeBox imports * refactor: convert inject based tests to supertest * feat: require authentication to use route * test: confirm db is updated as expected * fix: respond appropriately on error Co-authored-by: Oliver Eyton-Williams Co-authored-by: Niraj Nandish Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> --- api/src/app.ts | 2 + api/src/routes/settings.test.ts | 117 ++++++++++++++++++++++++++++++++ api/src/routes/settings.ts | 77 +++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 api/src/routes/settings.test.ts create mode 100644 api/src/routes/settings.ts diff --git a/api/src/app.ts b/api/src/app.ts index 7e131be26b4..09da59b1a9a 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -20,6 +20,7 @@ import jwtAuthz from './plugins/fastify-jwt-authz'; import security from './plugins/security'; import sessionAuth from './plugins/session-auth'; import { testRoutes } from './routes/test'; +import { settingRoutes } from './routes/settings'; import { auth0Routes, devLoginCallback } from './routes/auth'; import { testValidatedRoutes } from './routes/validation-test'; import { testMiddleware } from './middleware'; @@ -117,6 +118,7 @@ export const build = async ( if (FCC_ENABLE_DEV_LOGIN_MODE) { void fastify.register(devLoginCallback, { prefix: '/auth' }); } + void fastify.register(settingRoutes); void fastify.register(testValidatedRoutes); return fastify; diff --git a/api/src/routes/settings.test.ts b/api/src/routes/settings.test.ts new file mode 100644 index 00000000000..e4c909edea5 --- /dev/null +++ b/api/src/routes/settings.test.ts @@ -0,0 +1,117 @@ +import request from 'supertest'; + +import { build } from '../app'; + +const baseProfileUI = { + isLocked: false, + showAbout: false, + showCerts: false, + showDonation: false, + showHeatMap: false, + showLocation: false, + showName: false, + showPoints: false, + showPortfolio: false, + showTimeLine: false +}; + +const profileUI = { + ...baseProfileUI, + isLocked: true, + showAbout: true, + showDonation: true, + showLocation: true, + showName: true, + showPortfolio: true +}; + +describe('settingRoutes', () => { + let fastify: undefined | Awaited>; + + beforeAll(async () => { + fastify = await build(); + await fastify.ready(); + }, 20000); + + afterAll(async () => { + await fastify?.close(); + }); + + test('PUT /update-my-profileui returns 401 status code for un-authenticated users', async () => { + const response = await request(fastify?.server).put('/update-my-profileui'); + + expect(response?.statusCode).toEqual(401); + }); + + describe('Authenticated user', () => { + let cookies: string[]; + + beforeAll(async () => { + await fastify?.prisma.user.updateMany({ + where: { email: 'foo@bar.com' }, + data: { profileUI: baseProfileUI } + }); + const res = await request(fastify?.server).get('/auth/dev-callback'); + cookies = res.get('Set-Cookie'); + }); + + test('PUT /update-my-profileui returns 200 status code with "success" message', async () => { + const response = await request(fastify?.server) + .put('/update-my-profileui') + .set('Cookie', cookies) + .send({ profileUI }); + + const user = await fastify?.prisma.user.findFirst({ + where: { email: 'foo@bar.com' } + }); + + expect(response?.statusCode).toEqual(200); + expect(response?.body).toEqual({ + message: 'flash.updated-preferences', + type: 'success' + }); + expect(user?.profileUI).toEqual(profileUI); + }); + + test('PUT /update-my-profileui ignores invalid keys', async () => { + const response = await request(fastify?.server) + .put('/update-my-profileui') + .set('Cookie', cookies) + .send({ + profileUI: { + ...profileUI, + invalidKey: 'invalidValue' + } + }); + + const user = await fastify?.prisma.user.findFirst({ + where: { email: 'foo@bar.com' } + }); + + expect(response?.statusCode).toEqual(200); + expect(user?.profileUI).toEqual(profileUI); + }); + + test('PUT /update-my-profileui returns 400 status code with missing keys', async () => { + const response = await request(fastify?.server) + .put('/update-my-profileui') + .set('Cookie', cookies) + .send({ + profileUI: { + isLocked: true, + showName: true, + showPoints: false, + showPortfolio: true, + showTimeLine: false + } + }); + + expect(response?.statusCode).toEqual(400); + expect(response?.body).toEqual({ + error: 'Bad Request', + message: `body/profileUI must have required property 'showAbout'`, + statusCode: 400 + }); + }); + }); +}); diff --git a/api/src/routes/settings.ts b/api/src/routes/settings.ts new file mode 100644 index 00000000000..cff143fb593 --- /dev/null +++ b/api/src/routes/settings.ts @@ -0,0 +1,77 @@ +import { + Type, + type FastifyPluginCallbackTypebox +} from '@fastify/type-provider-typebox'; + +export const settingRoutes: FastifyPluginCallbackTypebox = ( + fastify, + _options, + done +) => { + fastify.addHook('onRequest', fastify.authenticateSession); + + fastify.put( + '/update-my-profileui', + { + schema: { + body: Type.Object({ + profileUI: Type.Object({ + isLocked: Type.Boolean(), + showAbout: Type.Boolean(), + showCerts: Type.Boolean(), + showDonation: Type.Boolean(), + showHeatMap: Type.Boolean(), + showLocation: Type.Boolean(), + showName: Type.Boolean(), + showPoints: Type.Boolean(), + showPortfolio: Type.Boolean(), + showTimeLine: Type.Boolean() + }) + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.updated-preferences'), + type: Type.Literal('success') + }), + 500: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + } + }, + async (req, reply) => { + try { + await fastify.prisma.user.update({ + where: { id: req.session.user.id }, + data: { + profileUI: { + isLocked: req.body.profileUI.isLocked, + showAbout: req.body.profileUI.showAbout, + showCerts: req.body.profileUI.showCerts, + showDonation: req.body.profileUI.showDonation, + showHeatMap: req.body.profileUI.showHeatMap, + showLocation: req.body.profileUI.showLocation, + showName: req.body.profileUI.showName, + showPoints: req.body.profileUI.showPoints, + showPortfolio: req.body.profileUI.showPortfolio, + showTimeLine: req.body.profileUI.showTimeLine + } + } + }); + + return { + message: 'flash.updated-preferences', + type: 'success' + } as const; + } catch (err) { + // TODO: send to Sentry + fastify.log.error(err); + void reply.code(500); + return { message: 'flash.wrong-updating', type: 'danger' } as const; + } + } + ); + + done(); +};