From 8a0a69b7dc106849de54763d6ed9efe2946a30e0 Mon Sep 17 00:00:00 2001 From: Ravinou Date: Tue, 25 Feb 2025 20:34:06 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=9A=A1=20updateAppriseAlert=20AP?= =?UTF-8?q?I?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EmailAlertSettings/EmailAlertSettings.tsx | 6 +- helpers/functions/fileHelpers.ts | 16 ++++ pages/api/account/getEmailAlert.ts | 4 +- pages/api/account/updateAppriseAlert.js | 74 ----------------- pages/api/account/updateAppriseAlert.ts | 53 ++++++++++++ tests/supertest/updateAppriseAlert.test.ts | 82 +++++++++++++++++++ types/api/notifications.types.ts | 11 ++- 7 files changed, 166 insertions(+), 80 deletions(-) create mode 100644 helpers/functions/fileHelpers.ts delete mode 100644 pages/api/account/updateAppriseAlert.js create mode 100644 pages/api/account/updateAppriseAlert.ts create mode 100644 tests/supertest/updateAppriseAlert.test.ts diff --git a/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx b/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx index 267cd5e..8ec5010 100644 --- a/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx +++ b/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx @@ -12,7 +12,7 @@ import Error from '~/Components/UI/Error/Error'; import Switch from '~/Components/UI/Switch/Switch'; import { useFormStatus } from '~/hooks/useFormStatus'; import { Optional } from '~/types'; -import { EmailAlert } from '~/types/api/notifications.types'; +import { EmailAlertDTO } from '~/types/api/notifications.types'; export default function EmailAlertSettings() { //Var @@ -48,7 +48,7 @@ export default function EmailAlertSettings() { }, }); - const data: Optional = await response.json(); + const data: Optional = await response.json(); setIsAlertEnabled(data?.emailAlert ?? false); setIsSwitchDisabled(false); } catch (error) { @@ -62,7 +62,7 @@ export default function EmailAlertSettings() { ////Functions //Switch to enable/disable Email notifications - const onChangeSwitchHandler = async (data: EmailAlert) => { + const onChangeSwitchHandler = async (data: EmailAlertDTO) => { clearError(); setIsSwitchDisabled(true); await fetch('/api/account/updateEmailAlert', { diff --git a/helpers/functions/fileHelpers.ts b/helpers/functions/fileHelpers.ts new file mode 100644 index 0000000..6072453 --- /dev/null +++ b/helpers/functions/fileHelpers.ts @@ -0,0 +1,16 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import { BorgWarehouseUser } from '~/types/domain/config.types'; + +// Paths definition +const jsonDirectory = path.join(process.cwd(), '/config'); +const usersFilePath = path.join(jsonDirectory, 'users.json'); + +export const getUsersList = async (): Promise => { + const fileContent = await fs.readFile(usersFilePath, 'utf8'); + return JSON.parse(fileContent); +}; + +export const updateUsersList = async (usersList: BorgWarehouseUser[]): Promise => { + await fs.writeFile(usersFilePath, JSON.stringify(usersList, null, 2)); +}; diff --git a/pages/api/account/getEmailAlert.ts b/pages/api/account/getEmailAlert.ts index c184e88..b575f53 100644 --- a/pages/api/account/getEmailAlert.ts +++ b/pages/api/account/getEmailAlert.ts @@ -4,13 +4,13 @@ import path from 'path'; import { authOptions } from '../auth/[...nextauth]'; import { getServerSession } from 'next-auth/next'; import { NextApiRequest, NextApiResponse } from 'next'; -import { EmailAlert } from '~/types/api/notifications.types'; +import { EmailAlertDTO } from '~/types/api/notifications.types'; import { ErrorResponse } from '~/types/api/error.types'; import { BorgWarehouseUser } from '~/types/domain/config.types'; export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { if (req.method !== 'GET') { res.status(405).json({ message: 'Bad request on API' }); diff --git a/pages/api/account/updateAppriseAlert.js b/pages/api/account/updateAppriseAlert.js deleted file mode 100644 index add2a7e..0000000 --- a/pages/api/account/updateAppriseAlert.js +++ /dev/null @@ -1,74 +0,0 @@ -//Lib -import { promises as fs } from 'fs'; -import path from 'path'; -import { authOptions } from '../auth/[...nextauth]'; -import { getServerSession } from 'next-auth/next'; - -export default async function handler(req, res) { - if (req.method == 'PUT') { - //Verify that the user is logged in. - const session = await getServerSession(req, res, authOptions); - if (!session) { - res.status(401).json({ message: 'You must be logged in.' }); - return; - } - - //The data we expect to receive - let { appriseAlert } = req.body; - - //Read the users file - //Find the absolute path of the json directory - const jsonDirectory = path.join(process.cwd(), '/config'); - let usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8'); - //Parse the usersList - usersList = JSON.parse(usersList); - - //1 : control the data - if (typeof appriseAlert != 'boolean') { - res.status(422).json({ message: 'Unexpected data' }); - return; - } - - //2 : Verify that the user of the session exists - const userIndex = usersList.map((user) => user.username).indexOf(session.user.name); - if (userIndex === -1) { - res.status(400).json({ - message: 'User is incorrect. Please, logout to update your session.', - }); - return; - } - - //3 : Change the appriseAlert settings - try { - //Modify the appriseAlert bool for the user - let newUsersList = usersList.map((user) => - user.username == session.user.name ? { ...user, appriseAlert: appriseAlert } : user - ); - //Stringify the new users list - newUsersList = JSON.stringify(newUsersList); - //Write the new JSON - await fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => { - if (err) console.log(err); - }); - res.status(200).json({ message: 'Successful API send' }); - } catch (error) { - //Log for backend - console.log(error); - //Log for frontend - if (error.code == 'ENOENT') { - res.status(500).json({ - status: 500, - message: 'No such file or directory', - }); - } else { - res.status(500).json({ - status: 500, - message: 'API error, contact the administrator', - }); - } - return; - } - } else { - res.status(405).json({ message: 'Bad request on API' }); - } -} diff --git a/pages/api/account/updateAppriseAlert.ts b/pages/api/account/updateAppriseAlert.ts new file mode 100644 index 0000000..1fa6720 --- /dev/null +++ b/pages/api/account/updateAppriseAlert.ts @@ -0,0 +1,53 @@ +// Imports +import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers'; +import { authOptions } from '../auth/[...nextauth]'; +import { getServerSession } from 'next-auth/next'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { AppriseAlertDTO, AppriseAlertResponse } from '~/types/api/notifications.types'; +import { ErrorResponse } from '~/types/api/error.types'; + +export default async function handler( + req: NextApiRequest & { body: AppriseAlertDTO }, + res: NextApiResponse +) { + if (req.method !== 'PUT') { + return res.status(405); + } + + const session = await getServerSession(req, res, authOptions); + if (!session) { + return res.status(401); + } + + const { appriseAlert } = req.body; + if (typeof appriseAlert !== 'boolean') { + return res.status(422).json({ message: 'Unexpected data' }); + } + + try { + const usersList = await getUsersList(); + const userIndex = usersList.findIndex((u) => u.username === session.user?.name); + + if (userIndex === -1) { + return res + .status(400) + .json({ message: 'User is incorrect. Please, logout to update your session.' }); + } + + const updatedUsersList = usersList.map((user, index) => + index === userIndex ? { ...user, appriseAlert } : user + ); + + await updateUsersList(updatedUsersList); + return res.status(200).json({ message: 'Successful API send' }); + } catch (error: any) { + console.error(error); + return res.status(500).json({ + status: 500, + message: + error.code === 'ENOENT' + ? 'No such file or directory' + : 'API error, contact the administrator', + }); + } +} diff --git a/tests/supertest/updateAppriseAlert.test.ts b/tests/supertest/updateAppriseAlert.test.ts new file mode 100644 index 0000000..5ee13b5 --- /dev/null +++ b/tests/supertest/updateAppriseAlert.test.ts @@ -0,0 +1,82 @@ +import { createMocks } from 'node-mocks-http'; +import handler from '~/pages/api/account/updateAppriseAlert'; +import { getServerSession } from 'next-auth/next'; +import { getUsersList, updateUsersList } from '~/helpers/functions/fileHelpers'; + +jest.mock('next-auth/next'); +jest.mock('~/helpers/functions/fileHelpers', () => ({ + __esModule: true, + getUsersList: jest.fn(), + updateUsersList: jest.fn(), +})); + +describe('Notifications API', () => { + it('should return 405 if the method is not PUT', async () => { + const { req, res } = createMocks({ method: 'GET' }); + await handler(req, res); + expect(res._getStatusCode()).toBe(405); + }); + + it('should return 401 if the user is not authenticated', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + + const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } }); + await handler(req, res); + expect(res._getStatusCode()).toBe(401); + }); + + it('should return 422 if the request body is invalid', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'testuser' }, + }); + + const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: 'not-boolean' } }); + await handler(req, res); + expect(res._getStatusCode()).toBe(422); + expect(res._getJSONData()).toEqual({ message: 'Unexpected data' }); + }); + + it('should return 400 if the user does not exist', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'nonexistent' }, + }); + + (getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', appriseAlert: false }]); + + const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } }); + await handler(req, res); + expect(res._getStatusCode()).toBe(400); + expect(res._getJSONData()).toEqual({ + message: 'User is incorrect. Please, logout to update your session.', + }); + }); + + it('should update appriseAlert and return 200 if everything is correct', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'testuser' }, + }); + + (getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', appriseAlert: false }]); + + const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } }); + await handler(req, res); + + expect(updateUsersList).toHaveBeenCalledWith([{ username: 'testuser', appriseAlert: true }]); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual({ message: 'Successful API send' }); + }); + + it('should return 500 if there is an error reading users file', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'testuser' }, + }); + + (getUsersList as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); + + const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } }); + await handler(req, res); + + expect(res._getStatusCode()).toBe(500); + expect(res._getJSONData()).toEqual({ status: 500, message: 'No such file or directory' }); + }); +}); diff --git a/types/api/notifications.types.ts b/types/api/notifications.types.ts index d085aed..eb9c16c 100644 --- a/types/api/notifications.types.ts +++ b/types/api/notifications.types.ts @@ -13,6 +13,15 @@ export type AppriseAlertResponse = { appriseAlert?: boolean; }; -export type EmailAlert = { +export type EmailAlertDTO = { emailAlert?: boolean; }; + +export type AppriseAlertDTO = { + appriseAlert: boolean; +}; + +export type AppriseModeDTO = { + appriseMode: AppriseModeEnum; + appriseStatelessURL?: string; +};