From e8e9f40cc53864080941d26f3a09a0613f51d640 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Tue, 19 Mar 2024 14:42:28 +0100 Subject: [PATCH] feat(api): update my classroom mode (#54009) --- api/prisma/oldschema.prisma | 1 + api/prisma/schema.prisma | 3 +- api/src/routes/settings.test.ts | 99 +++++++++++++++++++++------------ api/src/routes/settings.ts | 36 ++++++++++++ api/src/schemas.ts | 15 +++++ 5 files changed, 118 insertions(+), 36 deletions(-) diff --git a/api/prisma/oldschema.prisma b/api/prisma/oldschema.prisma index b416050bcd7..3e59295e58c 100644 --- a/api/prisma/oldschema.prisma +++ b/api/prisma/oldschema.prisma @@ -198,6 +198,7 @@ model user { verificationToken String? // Undefined website String? // Undefined yearsTopContributor String[] @default([]) // Undefined | String[] + isClassroomAccount Boolean? // Undefined } // ----------------------------------- diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 1d1a8be1d9f..c93da9b1fff 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -114,7 +114,7 @@ model user { is2018DataVisCert Boolean? // Undefined is2018FullStackCert Boolean? // Undefined isCollegeAlgebraPyCertV8 Boolean? // Undefined - isUpcomingPythonCertV8 Boolean? // Undefined + isUpcomingPythonCertV8 Boolean? // Undefined keyboardShortcuts Boolean? // Undefined linkedin String? // Null | Undefined location String? // Null @@ -139,6 +139,7 @@ model user { verificationToken String? // Undefined website String? // Undefined yearsTopContributor String[] // Undefined | String[] + isClassroomAccount Boolean? // Undefined } // ----------------------------------- diff --git a/api/src/routes/settings.test.ts b/api/src/routes/settings.test.ts index f1f4fb216de..6a28800dbea 100644 --- a/api/src/routes/settings.test.ts +++ b/api/src/routes/settings.test.ts @@ -689,51 +689,80 @@ Please wait 5 minutes to resend an authentication link.` expect(response.statusCode).toEqual(400); }); }); - }); - describe('Unauthenticated User', () => { - let setCookies: string[]; + describe('/update-my-classroom-mode', () => { + test('PUT returns 200 status code with "success" message', async () => { + const response = await superPut('/update-my-classroom-mode').send({ + isClassroomAccount: true + }); - // Get the CSRF cookies from an unprotected route - beforeAll(async () => { - const res = await superRequest('/status/ping', { method: 'GET' }); - setCookies = res.get('Set-Cookie'); + expect(response.body).toEqual({ + message: 'flash.classroom-mode-updated', + type: 'success' + }); + + expect(response.statusCode).toEqual(200); + }); + + test('After updating the classroom mode, the user should have this property set', async () => { + await superPut('/update-my-classroom-mode').send({ + isClassroomAccount: false + }); + + const user = await fastifyTestInstance?.prisma.user.findFirst({ + where: { + email: developerUserEmail + } + }); + + expect(user?.isClassroomAccount).toEqual(false); + }); }); - const endpoints: { path: string; method: 'PUT' }[] = [ - { path: '/update-my-profileui', method: 'PUT' }, - { path: '/update-my-theme', method: 'PUT' }, - { path: '/update-my-username', method: 'PUT' }, - { path: '/update-my-keyboard-shortcuts', method: 'PUT' }, - { path: '/update-my-socials', method: 'PUT' }, - { path: '/update-my-quincy-email', method: 'PUT' }, - { path: '/update-my-about', method: 'PUT' }, - { path: '/update-my-honesty', method: 'PUT' }, - { path: '/update-privacy-terms', method: 'PUT' }, - { path: '/update-my-portfolio', method: 'PUT' } - ]; + describe('Unauthenticated User', () => { + let setCookies: string[]; - endpoints.forEach(({ path, method }) => { - test(`${method} ${path} returns 401 status code with error message`, async () => { - const response = await superRequest(path, { - method, - setCookies + // Get the CSRF cookies from an unprotected route + beforeAll(async () => { + const res = await superRequest('/status/ping', { method: 'GET' }); + setCookies = res.get('Set-Cookie'); + }); + + const endpoints: { path: string; method: 'PUT' }[] = [ + { path: '/update-my-profileui', method: 'PUT' }, + { path: '/update-my-theme', method: 'PUT' }, + { path: '/update-my-username', method: 'PUT' }, + { path: '/update-my-keyboard-shortcuts', method: 'PUT' }, + { path: '/update-my-socials', method: 'PUT' }, + { path: '/update-my-quincy-email', method: 'PUT' }, + { path: '/update-my-about', method: 'PUT' }, + { path: '/update-my-honesty', method: 'PUT' }, + { path: '/update-privacy-terms', method: 'PUT' }, + { path: '/update-my-portfolio', method: 'PUT' } + ]; + + endpoints.forEach(({ path, method }) => { + test(`${method} ${path} returns 401 status code with error message`, async () => { + const response = await superRequest(path, { + method, + setCookies + }); + expect(response.statusCode).toBe(401); }); - expect(response.statusCode).toBe(401); }); }); }); -}); -describe('isPictureWithProtocol', () => { - test('Valid protocol', () => { - expect(isPictureWithProtocol('https://www.example.com/')).toEqual(true); - expect(isPictureWithProtocol('http://www.example.com/')).toEqual(true); - }); + describe('isPictureWithProtocol', () => { + test('Valid protocol', () => { + expect(isPictureWithProtocol('https://www.example.com/')).toEqual(true); + expect(isPictureWithProtocol('http://www.example.com/')).toEqual(true); + }); - test('Invalid protocol', () => { - expect(isPictureWithProtocol('htps://www.example.com/')).toEqual(false); - expect(isPictureWithProtocol('tp://www.example.com/')).toEqual(false); - expect(isPictureWithProtocol('www.example.com/')).toEqual(false); + test('Invalid protocol', () => { + expect(isPictureWithProtocol('htps://www.example.com/')).toEqual(false); + expect(isPictureWithProtocol('tp://www.example.com/')).toEqual(false); + expect(isPictureWithProtocol('www.example.com/')).toEqual(false); + }); }); }); diff --git a/api/src/routes/settings.ts b/api/src/routes/settings.ts index e93f3ee394c..70c880b23c2 100644 --- a/api/src/routes/settings.ts +++ b/api/src/routes/settings.ts @@ -576,5 +576,41 @@ ${isLinkSentWithinLimitTTL}` } ); + fastify.put( + '/update-my-classroom-mode', + { + schema: schemas.updateMyClassroomMode, + errorHandler: (error, request, reply) => { + if (error.validation) { + void reply.code(403); + void reply.send({ message: 'flash.wrong-updating', type: 'danger' }); + } else { + fastify.errorHandler(error, request, reply); + } + } + }, + async (req, reply) => { + try { + const classroomMode = req.body.isClassroomAccount; + + await fastify.prisma.user.update({ + where: { id: req.session.user.id }, + data: { + isClassroomAccount: classroomMode + } + }); + + return { + message: 'flash.classroom-mode-updated', + type: 'success' + } as const; + } catch (err) { + fastify.log.error(err); + void reply.code(403); + return { message: 'flash.wrong-updating', type: 'danger' } as const; + } + } + ); + done(); }; diff --git a/api/src/schemas.ts b/api/src/schemas.ts index 827dbacc6e2..2b615003390 100644 --- a/api/src/schemas.ts +++ b/api/src/schemas.ts @@ -297,6 +297,21 @@ export const schemas = { }) } }, + updateMyClassroomMode: { + body: Type.Object({ + isClassroomAccount: Type.Boolean() + }), + response: { + 200: Type.Object({ + message: Type.Literal('flash.classroom-mode-updated'), + type: Type.Literal('success') + }), + 403: Type.Object({ + message: Type.Literal('flash.wrong-updating'), + type: Type.Literal('danger') + }) + } + }, // User: deleteMyAccount: { response: {