test: edit API

This commit is contained in:
Ravinou 2025-03-24 22:02:28 +01:00
commit 1ae96c8f9a
No known key found for this signature in database
GPG key ID: EEEE670C40F6A4D7
5 changed files with 223 additions and 27 deletions

View file

@ -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<Repository> },
@ -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');
}
};

View file

@ -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', () => ({

View file

@ -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(),
};
});

View file

@ -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' });
});
});

View file

@ -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', () => {