From 93000d44061ece9dba429196cb7e7e82c853f6bd Mon Sep 17 00:00:00 2001 From: Ravinou Date: Sun, 23 Mar 2025 16:12:03 +0100 Subject: [PATCH] =?UTF-8?q?test:=20=E2=9C=85=20delete=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/functions/tokenController.ts | 6 +- pages/api/repo/id/[slug]/delete.ts | 8 +- tests/supertest/delete.test.ts | 196 +++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 tests/supertest/delete.test.ts diff --git a/helpers/functions/tokenController.ts b/helpers/functions/tokenController.ts index 7c09fc0..da2c151 100644 --- a/helpers/functions/tokenController.ts +++ b/helpers/functions/tokenController.ts @@ -3,9 +3,9 @@ import { Optional } from '~/types'; import { TokenPermissionsType } from '~/types/api/integration.types'; import { getUsersList } from './fileHelpers'; -export default async function tokenController( +export const tokenController = async ( headers: IncomingHttpHeaders -): Promise> { +): Promise> => { const API_KEY = headers.authorization?.split(' ')[1]; const FROM_IP = headers['x-forwarded-for'] || 'unknown'; @@ -35,4 +35,4 @@ export default async function tokenController( } catch (error) { throw new Error('Error with tokenController'); } -} +}; diff --git a/pages/api/repo/id/[slug]/delete.ts b/pages/api/repo/id/[slug]/delete.ts index 6fbddac..526ad7c 100644 --- a/pages/api/repo/id/[slug]/delete.ts +++ b/pages/api/repo/id/[slug]/delete.ts @@ -1,10 +1,10 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getServerSession } from 'next-auth/next'; -import { getRepoList, updateRepoList } from '~/helpers/functions'; -import ApiResponse from '~/helpers/functions/apiResponse'; +import { getRepoList, updateRepoList, tokenController } from '~/helpers/functions'; import { deleteRepo } from '~/helpers/functions/shell.utils'; + +import ApiResponse from '~/helpers/functions/apiResponse'; import { BorgWarehouseApiResponse } from '~/types/api/error.types'; -import tokenController from '~/helpers/functions/tokenController'; import { authOptions } from '../../../auth/[...nextauth]'; export default async function handler( @@ -59,7 +59,7 @@ export default async function handler( return ApiResponse.serverError(res); } - const updatedRepoList = repoList.splice(indexToDelete, 1); + const updatedRepoList = repoList.filter((repo) => repo.id !== parseInt(slug, 10)); await updateRepoList(updatedRepoList, true); return ApiResponse.success(res, `Repository ${repoList[indexToDelete].repositoryName} deleted`); diff --git a/tests/supertest/delete.test.ts b/tests/supertest/delete.test.ts new file mode 100644 index 0000000..8493019 --- /dev/null +++ b/tests/supertest/delete.test.ts @@ -0,0 +1,196 @@ +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'; +import { deleteRepo } from '~/helpers/functions/shell.utils'; +import { getRepoList, tokenController, updateRepoList } 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(), +})); + +jest.mock('~/helpers/functions/shell.utils', () => { + return { + deleteRepo: jest.fn(), + }; +}); + +describe('DELETE /api/repo/id/[slug]/delete', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it('should return 405 if method is not DELETE', 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: 'DELETE' }); + await handler(req, res); + expect(res._getStatusCode()).toBe(401); + }); + + it('should return 403 if deletion is disabled via environment variable', async () => { + process.env.DISABLE_DELETE_REPO = 'true'; + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'USER' }, + }); + const { req, res } = createMocks({ method: 'DELETE' }); + await handler(req, res); + expect(res._getStatusCode()).toBe(403); + expect(res._getJSONData()).toEqual({ + status: 403, + message: 'Deletion is disabled on this server', + }); + delete process.env.DISABLE_DELETE_REPO; + }); + + it('should return 400 if slug is missing or malformed', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'USER' }, + }); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: undefined }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(400); + expect(res._getJSONData()).toEqual({ + status: 400, + message: 'Missing slug or slug is malformed', + }); + }); + + it('should return 404 if repository is not found', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'USER' }, + }); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: '123' }, + }); + (getRepoList as jest.Mock).mockResolvedValue([]); + await handler(req, res); + expect(res._getStatusCode()).toBe(404); + expect(res._getJSONData()).toEqual({ + status: 404, + message: 'Repository not found', + }); + }); + + it('should return 500 if deleteRepo fails', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'USER' }, + }); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: '123' }, + }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]); + (deleteRepo as jest.Mock).mockResolvedValue({ stderr: 'Error' }); + await handler(req, res); + expect(res._getStatusCode()).toBe(500); + expect(res._getJSONData()).toEqual({ + status: 500, + message: 'API error, contact the administrator.', + }); + expect(updateRepoList).not.toHaveBeenCalled(); + }); + + it('should delete the repository and return 200 on success with a session', async () => { + (getServerSession as jest.Mock).mockResolvedValue({ + user: { name: 'USER' }, + }); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: '1234' }, + }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 1234, repositoryName: 'test-repo' }]); + (deleteRepo as jest.Mock).mockResolvedValue({ stderr: null }); + (updateRepoList as jest.Mock).mockResolvedValue(true); + await handler(req, res); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual({ + status: 200, + message: 'Repository test-repo deleted', + }); + expect(updateRepoList).toHaveBeenCalledWith([], true); + }); + + it('should delete the repository and return 200 on success with an API key', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + (tokenController as jest.Mock).mockResolvedValue({ delete: true }); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: '12345' }, + headers: { + authorization: 'Bearer API_KEY', + }, + }); + (getRepoList as jest.Mock).mockResolvedValue([{ id: 12345, repositoryName: 'test-repo2' }]); + (deleteRepo as jest.Mock).mockResolvedValue({ stderr: null }); + (updateRepoList as jest.Mock).mockResolvedValue(true); + await handler(req, res); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual({ + status: 200, + message: 'Repository test-repo2 deleted', + }); + expect(updateRepoList).toHaveBeenCalledWith([], true); + }); + + it('should return 401 if the API key is invalid', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + (tokenController as jest.Mock).mockResolvedValue(null); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: '12345' }, + headers: { + authorization: 'Bearer API_KEY', + }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(401); + expect(res._getJSONData()).toEqual({ + status: 401, + message: 'Invalid API key', + }); + }); + + it('should return 403 if the API key does not have delete permissions', async () => { + (getServerSession as jest.Mock).mockResolvedValue(null); + (tokenController as jest.Mock).mockResolvedValue({ delete: false }); + const { req, res } = createMocks({ + method: 'DELETE', + query: { slug: '12345' }, + headers: { + authorization: 'Bearer API_KEY', + }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(403); + expect(res._getJSONData()).toEqual({ + status: 403, + message: 'Insufficient permissions', + }); + }); +});