From 1ae96c8f9a537ff9a32366e302a517dfd2b51b73 Mon Sep 17 00:00:00 2001 From: Ravinou Date: Mon, 24 Mar 2025 22:02:28 +0100 Subject: [PATCH] =?UTF-8?q?test:=20=E2=9C=85=20edit=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/api/repo/id/[slug]/edit.ts | 47 +++--- tests/supertest/checkStatus.test.ts | 2 +- tests/supertest/delete.test.ts | 3 +- tests/supertest/edit.test.ts | 196 +++++++++++++++++++++++++ tests/supertest/getStorageUsed.test.ts | 2 +- 5 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 tests/supertest/edit.test.ts diff --git a/pages/api/repo/id/[slug]/edit.ts b/pages/api/repo/id/[slug]/edit.ts index 109f7f1..26da5cb 100644 --- a/pages/api/repo/id/[slug]/edit.ts +++ b/pages/api/repo/id/[slug]/edit.ts @@ -1,8 +1,5 @@ -import { promises as fs } from 'fs'; -import path from 'path'; import { authOptions } from '../../../auth/[...nextauth]'; import { getServerSession } from 'next-auth/next'; -import { alertOptions } from '~/types/domain/constants'; import { getRepoList, updateRepoList, @@ -13,8 +10,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; import ApiResponse from '~/helpers/functions/apiResponse'; import { Repository } from '~/types/domain/config.types'; import { updateRepoShell } from '~/helpers/functions/shell.utils'; -const util = require('node:util'); -const exec = util.promisify(require('node:child_process').exec); export default async function handler( req: NextApiRequest & { body: Partial }, @@ -45,7 +40,14 @@ export default async function handler( return ApiResponse.serverError(res); } - dataHandler(req, res); + try { + validateRequestBody(req); + } catch (error) { + if (error instanceof Error) { + return ApiResponse.badRequest(res, error.message); + } + return ApiResponse.badRequest(res, 'Invalid request data'); + } try { const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = @@ -97,31 +99,30 @@ export default async function handler( } } -const dataHandler = (req: NextApiRequest, res: NextApiResponse) => { +const validateRequestBody = (req: NextApiRequest) => { const slug = req.query.slug; if (!slug || Array.isArray(slug)) { - return ApiResponse.badRequest(res, 'Missing slug or slug is malformed'); + throw new Error('Missing slug or slug is malformed'); } - const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body; - if (alias !== undefined && typeof alias !== 'string') { - return ApiResponse.badRequest(res, 'Alias must be a string'); + if (req.body.alias !== undefined && typeof req.body.alias !== 'string') { + throw new Error('Alias must be a string'); } - if (sshPublicKey !== undefined && typeof sshPublicKey !== 'string') { - return ApiResponse.badRequest(res, 'SSH Public Key must be a string'); + if (req.body.sshPublicKey !== undefined && typeof req.body.sshPublicKey !== 'string') { + throw new Error('SSH Public Key must be a string'); } - if (storageSize !== undefined && typeof storageSize !== 'number') { - return ApiResponse.badRequest(res, 'Storage Size must be a number'); + if (req.body.storageSize !== undefined && typeof req.body.storageSize !== 'number') { + throw new Error('Storage Size must be a number'); } - if (comment !== undefined && typeof comment !== 'string') { - return ApiResponse.badRequest(res, 'Comment must be a string'); + if (req.body.comment !== undefined && typeof req.body.comment !== 'string') { + throw new Error('Comment must be a string'); } - if (alert !== undefined && typeof alert !== 'number') { - return ApiResponse.badRequest(res, 'Alert must be a number'); + if (req.body.alert !== undefined && typeof req.body.alert !== 'number') { + throw new Error('Alert must be a number'); } - if (lanCommand !== undefined && typeof lanCommand !== 'boolean') { - return ApiResponse.badRequest(res, 'Lan Command must be a boolean'); + if (req.body.lanCommand !== undefined && typeof req.body.lanCommand !== 'boolean') { + throw new Error('Lan Command must be a boolean'); } - if (appendOnlyMode !== undefined && typeof appendOnlyMode !== 'boolean') { - return ApiResponse.badRequest(res, 'Append Only Mode must be a boolean'); + if (req.body.appendOnlyMode !== undefined && typeof req.body.appendOnlyMode !== 'boolean') { + throw new Error('Append Only Mode must be a boolean'); } }; diff --git a/tests/supertest/checkStatus.test.ts b/tests/supertest/checkStatus.test.ts index c652dda..da6a798 100644 --- a/tests/supertest/checkStatus.test.ts +++ b/tests/supertest/checkStatus.test.ts @@ -11,7 +11,7 @@ jest.mock('~/helpers/functions/fileHelpers', () => ({ })); jest.mock('~/helpers/functions/shell.utils', () => ({ - getLastSaveList: jest.fn(), + getLastSaveListShell: jest.fn(), })); jest.mock('~/helpers/functions/nodemailerSMTP', () => ({ diff --git a/tests/supertest/delete.test.ts b/tests/supertest/delete.test.ts index 2f717ed..7bde772 100644 --- a/tests/supertest/delete.test.ts +++ b/tests/supertest/delete.test.ts @@ -1,4 +1,3 @@ -import request from 'supertest'; import { createMocks } from 'node-mocks-http'; import handler from '~/pages/api/repo/id/[slug]/delete'; import { getServerSession } from 'next-auth/next'; @@ -27,7 +26,7 @@ jest.mock('~/helpers/functions', () => ({ jest.mock('~/helpers/functions/shell.utils', () => { return { - deleteRepo: jest.fn(), + deleteRepoShell: jest.fn(), }; }); diff --git a/tests/supertest/edit.test.ts b/tests/supertest/edit.test.ts new file mode 100644 index 0000000..26153bb --- /dev/null +++ b/tests/supertest/edit.test.ts @@ -0,0 +1,196 @@ +import { createMocks } from 'node-mocks-http'; +import handler from '~/pages/api/repo/id/[slug]/edit'; +import { getServerSession } from 'next-auth/next'; +import { updateRepoShell } from '~/helpers/functions/shell.utils'; +import { + getRepoList, + updateRepoList, + tokenController, + isSshPubKeyDuplicate, +} from '~/helpers/functions'; + +jest.mock('next-auth', () => { + return jest.fn(() => { + return { + auth: { session: {} }, + GET: jest.fn(), + POST: jest.fn(), + }; + }); +}); + +jest.mock('next-auth/next', () => ({ + getServerSession: jest.fn(), +})); + +jest.mock('~/helpers/functions', () => ({ + getRepoList: jest.fn(), + updateRepoList: jest.fn(), + tokenController: jest.fn(), + isSshPubKeyDuplicate: jest.fn(), +})); + +jest.mock('~/helpers/functions/shell.utils', () => ({ + updateRepoShell: jest.fn(), +})); + +describe('PATCH /api/repo/id/[slug]/edit', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it('should return 405 if method is not PATCH', async () => { + const { req, res } = createMocks({ method: 'GET' }); + await handler(req, res); + expect(res._getStatusCode()).toBe(405); + }); + + it('should return 401 if no session or authorization header is provided', async () => { + const { req, res } = createMocks({ method: 'PATCH' }); + await handler(req, res); + expect(res._getStatusCode()).toBe(401); + }); + + it('should return 401 if API key is invalid', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + (tokenController as jest.Mock).mockResolvedValue(null); + const { req, res } = createMocks({ + method: 'PATCH', + headers: { authorization: 'Bearer INVALID_API_KEY' }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(401); + }); + + it('should return 403 if API key does not have update permissions', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + (tokenController as jest.Mock).mockResolvedValue({ update: false }); + const { req, res } = createMocks({ + method: 'PATCH', + headers: { authorization: 'Bearer API_KEY' }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(403); + }); + + it('should return 400 if slug is missing or malformed', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } }); + const { req, res } = createMocks({ method: 'PATCH', query: { slug: undefined } }); + await handler(req, res); + expect(res._getStatusCode()).toBe(400); + }); + + it('should return 404 if repository is not found', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } }); + (getRepoList as jest.Mock).mockResolvedValue([]); + const { req, res } = createMocks({ method: 'PATCH', query: { slug: '123' } }); + await handler(req, res); + expect(res._getStatusCode()).toBe(404); + }); + + it('should return 409 if SSH key is duplicated', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]); + (isSshPubKeyDuplicate as jest.Mock).mockReturnValue(true); + const { req, res } = createMocks({ + method: 'PATCH', + query: { slug: '123' }, + body: { sshPublicKey: 'duplicate-key' }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(409); + }); + + it('should return 500 if updateRepoShell fails', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]); + (updateRepoShell as jest.Mock).mockResolvedValue({ stderr: 'Error' }); + const { req, res } = createMocks({ + method: 'PATCH', + query: { slug: '123' }, + body: { alias: 'new-alias' }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(500); + }); + + it('should successfully update repository with a session', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]); + (updateRepoShell as jest.Mock).mockResolvedValue({ stderr: null }); + (updateRepoList as jest.Mock).mockResolvedValue(true); + const { req, res } = createMocks({ + method: 'PATCH', + query: { slug: '123' }, + body: { alias: 'new-alias' }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual({ message: 'Repository test-repo has been edited' }); + }); + + it('should successfully update repository with API key', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + (tokenController as jest.Mock).mockResolvedValue({ update: true }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 456, repositoryName: 'repo-key' }]); + (updateRepoShell as jest.Mock).mockResolvedValue({ stderr: null }); + (updateRepoList as jest.Mock).mockResolvedValue(true); + const { req, res } = createMocks({ + method: 'PATCH', + query: { slug: '456' }, + headers: { authorization: 'Bearer API_KEY' }, + body: { alias: 'updated-repo' }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual({ message: 'Repository repo-key has been edited' }); + }); + + it('should only update the provided fields, keep the rest unchanged and history the modification.', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } }); + (getRepoList as jest.Mock).mockResolvedValue([ + { + id: 123, + repositoryName: 'test-repo', + alias: 'old-alias', + sshPublicKey: 'old-key', + storageSize: 100, + lanCommand: false, + }, + ]); + (updateRepoShell as jest.Mock).mockResolvedValue({ stderr: null }); + (updateRepoList as jest.Mock).mockResolvedValue(true); + const { req, res } = createMocks({ + method: 'PATCH', + query: { slug: '123' }, + body: { + alias: 'new-alias', + sshPublicKey: 'new-key', + comment: 'new-comment', + alert: 0, + appendOnlyMode: true, + }, + }); + await handler(req, res); + expect(updateRepoList).toHaveBeenCalledWith( + [ + { + id: 123, + repositoryName: 'test-repo', + alias: 'new-alias', + sshPublicKey: 'new-key', + comment: 'new-comment', + alert: 0, + appendOnlyMode: true, + storageSize: 100, + lanCommand: false, + }, + ], + true + ); + expect(updateRepoShell).toHaveBeenCalledWith('test-repo', 'new-key', 100, true); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual({ message: 'Repository test-repo has been edited' }); + }); +}); diff --git a/tests/supertest/getStorageUsed.test.ts b/tests/supertest/getStorageUsed.test.ts index 911ed4e..22c5c6a 100644 --- a/tests/supertest/getStorageUsed.test.ts +++ b/tests/supertest/getStorageUsed.test.ts @@ -9,7 +9,7 @@ jest.mock('~/helpers/functions', () => ({ })); jest.mock('~/helpers/functions/shell.utils', () => ({ - getStorageUsed: jest.fn(), + getStorageUsedShell: jest.fn(), })); describe('GET /api/cronjob/getStorageUsed', () => {