test: add repo API

This commit is contained in:
Ravinou 2025-03-29 10:34:28 +01:00
commit e4dc585fe5
No known key found for this signature in database
GPG key ID: EEEE670C40F6A4D7
6 changed files with 230 additions and 11 deletions

View file

@ -64,18 +64,18 @@ export default async function handler(
const newRepo: Repository = {
id: repoList.length > 0 ? Math.max(...repoList.map((repo) => repo.id)) + 1 : 0,
alias: alias!,
alias: alias,
repositoryName: '',
status: false,
lastSave: 0,
lastStatusAlertSend: getUnixTime(new Date()),
alert: alert!,
storageSize: storageSize!,
alert: alert ?? 0,
storageSize: storageSize,
storageUsed: 0,
sshPublicKey: sshPublicKey!,
comment: comment!,
lanCommand: lanCommand!,
appendOnlyMode: appendOnlyMode!,
sshPublicKey: sshPublicKey,
comment: comment ?? '',
lanCommand: lanCommand ?? false,
appendOnlyMode: appendOnlyMode ?? false,
};
const { stdout, stderr } = await createRepoShell(
@ -101,6 +101,7 @@ export default async function handler(
const validateRequestBody = (req: NextApiRequest) => {
const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body;
// Required fields
if (!alias || typeof alias !== 'string') {
throw new Error('Alias must be a non-empty string');
}
@ -110,16 +111,17 @@ const validateRequestBody = (req: NextApiRequest) => {
if (typeof storageSize !== 'number' || storageSize <= 0 || !Number.isInteger(storageSize)) {
throw new Error('Storage Size must be a positive integer');
}
if (typeof comment !== 'string') {
// Optional fields
if (comment != undefined && typeof comment !== 'string') {
throw new Error('Comment must be a string');
}
if (typeof alert !== 'number') {
if (alert != undefined && typeof alert !== 'number') {
throw new Error('Alert must be a number');
}
if (typeof lanCommand !== 'boolean') {
if (lanCommand != undefined && typeof lanCommand !== 'boolean') {
throw new Error('Lan Command must be a boolean');
}
if (typeof appendOnlyMode !== 'boolean') {
if (appendOnlyMode != undefined && typeof appendOnlyMode !== 'boolean') {
throw new Error('Append Only Mode must be a boolean');
}
};

201
tests/supertest/add.test.ts Normal file
View file

@ -0,0 +1,201 @@
import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/repo/add';
import { getServerSession } from 'next-auth/next';
import {
getRepoList,
updateRepoList,
tokenController,
isSshPubKeyDuplicate,
} from '~/helpers/functions';
import { createRepoShell } from '~/helpers/functions/shell.utils';
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', () => ({
createRepoShell: jest.fn(),
}));
describe('POST /api/repo/add', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if method is not POST', 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: 'POST' });
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: 'POST',
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 create permissions', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue({ create: false });
const { req, res } = createMocks({
method: 'POST',
headers: { authorization: 'Bearer API_KEY' },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(403);
});
it('should return 409 if SSH key is duplicated', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue([{ id: 1, sshPublicKey: 'duplicate-key' }]);
(isSshPubKeyDuplicate as jest.Mock).mockReturnValue(true);
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'duplicate-key', storageSize: 10 },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(409);
});
it('should return 500 if createRepoShell fails', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue([]);
(createRepoShell as jest.Mock).mockResolvedValue({ stderr: 'Error' });
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(500);
});
it('should successfully create a repository with a session', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue([]);
(createRepoShell as jest.Mock).mockResolvedValue({ stdout: 'new-repo' });
(updateRepoList as jest.Mock).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({ id: 0, repositoryName: 'new-repo' });
});
it('should add missing optional properties with default values and update repo list correctly', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue([]);
(createRepoShell as jest.Mock).mockResolvedValue({ stdout: 'new-repo' });
(updateRepoList as jest.Mock).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({ id: 0, repositoryName: 'new-repo' });
expect(updateRepoList).toHaveBeenCalledWith(
[
{
id: 0,
alias: 'repo1',
repositoryName: 'new-repo',
status: false,
lastSave: 0,
lastStatusAlertSend: expect.any(Number),
alert: 0,
storageSize: 10,
storageUsed: 0,
sshPublicKey: 'valid-key',
comment: '',
lanCommand: false,
appendOnlyMode: false,
},
],
true
);
});
it('should assign the correct ID based on existing repositories', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue([
{ id: 0, alias: 'repo0', sshPublicKey: 'key0', storageSize: 10 },
{ id: 1, alias: 'repo1', sshPublicKey: 'key1', storageSize: 20 },
{ id: 3, alias: 'repo3', sshPublicKey: 'key3', storageSize: 30 },
]);
(createRepoShell as jest.Mock).mockResolvedValue({ stdout: 'new-repo' });
(updateRepoList as jest.Mock).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo-new', sshPublicKey: 'new-key', storageSize: 50 },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({ id: 4, repositoryName: 'new-repo' });
expect(updateRepoList).toHaveBeenCalledWith(
[
{ id: 0, alias: 'repo0', sshPublicKey: 'key0', storageSize: 10 },
{ id: 1, alias: 'repo1', sshPublicKey: 'key1', storageSize: 20 },
{ id: 3, alias: 'repo3', sshPublicKey: 'key3', storageSize: 30 },
{
id: 4,
alias: 'repo-new',
repositoryName: 'new-repo',
status: false,
lastSave: 0,
lastStatusAlertSend: expect.any(Number),
alert: 0,
storageSize: 50,
storageUsed: 0,
sshPublicKey: 'new-key',
comment: '',
lanCommand: false,
appendOnlyMode: false,
},
],
true
);
});
});

View file

@ -12,6 +12,10 @@ jest.mock('fs', () => ({
}));
describe('Get Apprise Alert API', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
const { req, res } = createMocks({ method: 'POST' });
await handler(req, res);

View file

@ -12,6 +12,10 @@ jest.mock('fs', () => ({
}));
describe('Get Apprise Mode API', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
const { req, res } = createMocks({ method: 'POST' });
await handler(req, res);

View file

@ -12,6 +12,10 @@ jest.mock('fs', () => ({
}));
describe('Get Apprise Services API', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
const { req, res } = createMocks({ method: 'POST' });
await handler(req, res);

View file

@ -12,6 +12,10 @@ jest.mock('fs', () => ({
}));
describe('Get Email Alert API', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
const { req, res } = createMocks({ method: 'POST' });
await handler(req, res);