From 35ad73fd23847460087a080edd119497a0f892a1 Mon Sep 17 00:00:00 2001 From: Ravinou Date: Fri, 21 Mar 2025 22:16:25 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=9A=A1=20storage=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/functions/shell.utils.ts | 8 +- pages/api/cronjob/getStorageUsed.js | 86 ---------------- pages/api/cronjob/getStorageUsed.ts | 47 +++++++++ tests/supertest/getStorageUsed.test.ts | 130 +++++++++++++++++++++++++ types/api/shell.types.ts | 5 + 5 files changed, 189 insertions(+), 87 deletions(-) delete mode 100644 pages/api/cronjob/getStorageUsed.js create mode 100644 pages/api/cronjob/getStorageUsed.ts create mode 100644 tests/supertest/getStorageUsed.test.ts diff --git a/helpers/functions/shell.utils.ts b/helpers/functions/shell.utils.ts index 75b55c6..ec00d0d 100644 --- a/helpers/functions/shell.utils.ts +++ b/helpers/functions/shell.utils.ts @@ -1,7 +1,7 @@ import path from 'path'; import { promisify } from 'util'; import { exec as execCallback } from 'node:child_process'; -import { LastSaveDTO } from '~/types/api/shell.types'; +import { LastSaveDTO, StorageUsedDTO } from '~/types/api/shell.types'; const exec = promisify(execCallback); @@ -10,3 +10,9 @@ export const getLastSaveList = async (): Promise => { const { stdout } = await exec(`${shellsDirectory}/shells/getLastSave.sh`); return JSON.parse(stdout || '[]'); }; + +export const getStorageUsed = async (): Promise => { + const shellsDirectory = path.join(process.cwd(), '/helpers'); + const { stdout } = await exec(`${shellsDirectory}/shells/getStorageUsed.sh`); + return JSON.parse(stdout || '[]'); +}; diff --git a/pages/api/cronjob/getStorageUsed.js b/pages/api/cronjob/getStorageUsed.js deleted file mode 100644 index 8bc9e07..0000000 --- a/pages/api/cronjob/getStorageUsed.js +++ /dev/null @@ -1,86 +0,0 @@ -// This API is design to be used by a cron (of your choice). Call it with curl for example -//(e.g : curl --request POST --url 'http://localhost:3000/api/cronjob/getStorageUsed' --header 'Authorization: Bearer 5173f388c0f4a0df92d1412c3036ddc897c22e4448') - -//Lib -import { promises as fs } from 'fs'; -import path from 'path'; -const util = require('node:util'); -const exec = util.promisify(require('node:child_process').exec); - -export default async function handler(req, res) { - if (req.headers.authorization == null) { - res.status(401).json({ - status: 401, - message: 'Unauthorized', - }); - return; - } - - const CRONJOB_KEY = process.env.CRONJOB_KEY; - const ACTION_KEY = req.headers.authorization.split(' ')[1]; - - try { - if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) { - //Check the repoList - const jsonDirectory = path.join(process.cwd(), '/config'); - let repoList = await fs.readFile(jsonDirectory + '/repo.json', 'utf8'); - //Parse the repoList - repoList = JSON.parse(repoList); - //If repoList is empty we stop here. - if (repoList.length === 0) { - res.status(200).json({ - success: 'No repositories to analyse yet.', - }); - return; - } - - ////Call the shell : getStorageUsed.sh - //Find the absolute path of the shells directory - const shellsDirectory = path.join(process.cwd(), '/helpers'); - //Exec the shell - const { stdout, stderr } = await exec(`${shellsDirectory}/shells/getStorageUsed.sh`); - if (stderr) { - res.status(500).json({ - status: 500, - message: 'Error on getting storage, contact the administrator.', - }); - return; - } - //Parse the JSON output of getStorageUsed.sh to use it - const storageUsed = JSON.parse(stdout); - - //Rebuild a newRepoList with the storageUsed value updated - let newRepoList = repoList; - for (let index in newRepoList) { - const repoFiltered = storageUsed.filter( - (x) => x.name === newRepoList[index].repositoryName - ); - if (repoFiltered.length === 1) { - newRepoList[index].storageUsed = repoFiltered[0].size; - } - } - - //Stringify the repoList to write it into the json file. - newRepoList = JSON.stringify(newRepoList); - //Write the new json - await fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => { - if (err) console.log(err); - }); - - res.status(200).json({ - success: 'Storage cron has been executed.', - }); - } else { - res.status(401).json({ - status: 401, - message: 'Unauthorized', - }); - } - } catch (err) { - console.log(err); - res.status(500).json({ - status: 500, - message: 'API error, contact the administrator.', - }); - } -} diff --git a/pages/api/cronjob/getStorageUsed.ts b/pages/api/cronjob/getStorageUsed.ts new file mode 100644 index 0000000..148daa7 --- /dev/null +++ b/pages/api/cronjob/getStorageUsed.ts @@ -0,0 +1,47 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { getRepoList, updateRepoList } from '~/helpers/functions'; +import ApiResponse from '~/helpers/functions/apiResponse'; +import { getStorageUsed } from '~/helpers/functions/shell.utils'; +import { BorgWarehouseApiResponse } from '~/types/api/error.types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (!req.headers.authorization) { + return ApiResponse.unauthorized(res); + } + + const CRONJOB_KEY = process.env.CRONJOB_KEY; + const ACTION_KEY = req.headers.authorization.split(' ')[1]; + + if (req.method !== 'POST' || ACTION_KEY !== CRONJOB_KEY) { + return ApiResponse.unauthorized(res); + } + + try { + //Check the repoList + const repoList = await getRepoList(); + if (repoList.length === 0) { + return ApiResponse.success(res, 'Storage cron executed. No repository to check.'); + } + + const storageUsed = await getStorageUsed(); + + //Update the storageUsed value of each repository + const updatedRepoList = repoList.map((repo) => { + const repoFiltered = storageUsed.find((x) => x.name === repo.repositoryName); + if (!repoFiltered) return repo; + return { + ...repo, + storageUsed: repoFiltered.size, + }; + }); + + await updateRepoList(updatedRepoList); + return ApiResponse.success(res, 'Storage cron has been executed.'); + } catch (err) { + console.error(err); + return ApiResponse.serverError(res); + } +} diff --git a/tests/supertest/getStorageUsed.test.ts b/tests/supertest/getStorageUsed.test.ts new file mode 100644 index 0000000..4cbcff5 --- /dev/null +++ b/tests/supertest/getStorageUsed.test.ts @@ -0,0 +1,130 @@ +import handler from '~/pages/api/cronjob/getStorageUsed'; +import { createMocks } from 'node-mocks-http'; +import { getRepoList, updateRepoList } from '~/helpers/functions'; +import { getStorageUsed } from '~/helpers/functions/shell.utils'; + +jest.mock('~/helpers/functions', () => ({ + getRepoList: jest.fn(), + updateRepoList: jest.fn(), +})); + +jest.mock('~/helpers/functions/shell.utils', () => ({ + getStorageUsed: jest.fn(), +})); + +describe('GET /api/cronjob/getStorageUsed', () => { + const CRONJOB_KEY = 'test-cronjob-key'; + process.env.CRONJOB_KEY = CRONJOB_KEY; + + it('should return unauthorized if no authorization header is provided', async () => { + const { req, res } = createMocks({ + method: 'POST', + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(401); + }); + + it('should return unauthorized if the authorization key is invalid', async () => { + const { req, res } = createMocks({ + method: 'POST', + headers: { + authorization: 'Bearer invalid-key', + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(401); + }); + + it('should return success if no repositories are found', async () => { + (getRepoList as jest.Mock).mockResolvedValue([]); + + const { req, res } = createMocks({ + method: 'POST', + headers: { + authorization: `Bearer ${CRONJOB_KEY}`, + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(res._getData()).toContain('No repository to check'); + }); + + it('should update repositories with storage used and return success', async () => { + const mockRepoList = [ + { repositoryName: 'repo1', storageUsed: 0 }, + { repositoryName: 'repo2', storageUsed: 0 }, + ]; + const mockStorageUsed = [ + { name: 'repo1', size: 100 }, + { name: 'repo2', size: 200 }, + ]; + + (getRepoList as jest.Mock).mockResolvedValue(mockRepoList); + (getStorageUsed as jest.Mock).mockResolvedValue(mockStorageUsed); + (updateRepoList as jest.Mock).mockResolvedValue(undefined); + + const { req, res } = createMocks({ + method: 'POST', + headers: { + authorization: `Bearer ${CRONJOB_KEY}`, + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(res._getData()).toContain('Storage cron has been executed'); + expect(updateRepoList).toHaveBeenCalledWith([ + { repositoryName: 'repo1', storageUsed: 100 }, + { repositoryName: 'repo2', storageUsed: 200 }, + ]); + }); + + it('should return server error if an exception occurs', async () => { + (getRepoList as jest.Mock).mockRejectedValue(new Error('Test error')); + + const { req, res } = createMocks({ + method: 'POST', + headers: { + authorization: `Bearer ${CRONJOB_KEY}`, + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(500); + }); + + it('should not touch to a repository if it is not found in the storage used list', async () => { + const mockRepoList = [ + { repositoryName: 'repo1', storageUsed: 0 }, + { repositoryName: 'repo2', storageUsed: 0 }, + ]; + const mockStorageUsed = [{ name: 'repo1', size: 100 }]; + + (getRepoList as jest.Mock).mockResolvedValue(mockRepoList); + (getStorageUsed as jest.Mock).mockResolvedValue(mockStorageUsed); + (updateRepoList as jest.Mock).mockResolvedValue(undefined); + + const { req, res } = createMocks({ + method: 'POST', + headers: { + authorization: `Bearer ${CRONJOB_KEY}`, + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(updateRepoList).toHaveBeenCalledWith([ + { repositoryName: 'repo1', storageUsed: 100 }, + { repositoryName: 'repo2', storageUsed: 0 }, + ]); + }); +}); diff --git a/types/api/shell.types.ts b/types/api/shell.types.ts index ca82056..ff1a85d 100644 --- a/types/api/shell.types.ts +++ b/types/api/shell.types.ts @@ -2,3 +2,8 @@ export type LastSaveDTO = { repositoryName: string; lastSave: number; }; + +export type StorageUsedDTO = { + size: number; + name: string; +};