test: migration from Jest to Vitest

This commit is contained in:
Ravinou 2025-04-06 15:35:37 +02:00
commit 52d8bca2ad
No known key found for this signature in database
GPG key ID: EEEE670C40F6A4D7
39 changed files with 4238 additions and 6479 deletions

6
.eslintrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View file

@ -0,0 +1,12 @@
const ApiResponse = {
success: vi.fn(),
badRequest: vi.fn(),
unauthorized: vi.fn(),
forbidden: vi.fn(),
notFound: vi.fn(),
methodNotAllowed: vi.fn(),
validationError: vi.fn(),
serverError: vi.fn(),
};
export default ApiResponse;

View file

@ -1,13 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': ['ts-jest', {}],
},
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/$1',
},
};
export default config;

5430
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,9 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "jest",
"test:watch": "jest --watch",
"lint": "next lint",
"test": "vitest",
"setup": "npm install && npm run setup:hooks",
"setup:hooks": "npx husky install",
"format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
@ -37,19 +36,15 @@
"@commitlint/cli": "^19.7.1",
"@commitlint/config-conventional": "^19.7.1",
"@types/bcryptjs": "^2.4.6",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.2",
"@types/nodemailer": "^6.4.17",
"@types/react": "^18.3.18",
"@types/supertest": "^6.0.2",
"eslint-config-next": "^15.1.6",
"husky": "^9.1.7",
"jest": "^29.7.0",
"node-mocks-http": "^1.16.2",
"prettier": "^3.5.1",
"supertest": "^7.0.0",
"ts-jest": "^29.3.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
"typescript": "^5.7.2",
"vitest": "^3.1.1"
}
}

View file

@ -3,15 +3,15 @@ import { createMocks } from 'node-mocks-http';
import { getUsersList } from '~/services';
import handler from '~/pages/api/account/getAppriseAlert';
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
getUsersList: jest.fn(),
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
}));
describe('Get Apprise Alert API', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
@ -21,7 +21,7 @@ describe('Get Apprise Alert API', () => {
});
it('should return 401 if the user is not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
@ -29,11 +29,20 @@ describe('Get Apprise Alert API', () => {
});
it('should return 400 if the user does not exist', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'nonexistent' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', appriseAlert: true }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseAlert: true,
},
]);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
@ -45,11 +54,20 @@ describe('Get Apprise Alert API', () => {
});
it('should return appriseAlert value if the user exists', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', appriseAlert: true }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseAlert: true,
},
]);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
@ -58,11 +76,11 @@ describe('Get Apprise Alert API', () => {
});
it('should return 500 if there is an error reading the file', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockImplementation(() => {
vi.mocked(getUsersList).mockImplementation(() => {
throw new Error();
});

View file

@ -2,16 +2,17 @@ import { getServerSession } from 'next-auth/next';
import { createMocks } from 'node-mocks-http';
import { getUsersList } from '~/services';
import handler from '~/pages/api/account/getAppriseMode';
import { AppriseModeEnum } from '~/types/domain/config.types';
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
getUsersList: jest.fn(),
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
}));
describe('Get Apprise Mode API', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
@ -21,7 +22,7 @@ describe('Get Apprise Mode API', () => {
});
it('should return 401 if the user is not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
@ -29,14 +30,18 @@ describe('Get Apprise Mode API', () => {
});
it('should return 400 if the user does not exist', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'nonexistent' },
});
(getUsersList as jest.Mock).mockResolvedValue([
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
appriseMode: 'stateless',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseMode: AppriseModeEnum.STATELESS,
appriseStatelessURL: 'https://example.com',
},
]);
@ -51,14 +56,18 @@ describe('Get Apprise Mode API', () => {
});
it('should return appriseMode and appriseStatelessURL if the user exists', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockResolvedValue([
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
appriseMode: 'stateless',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseMode: AppriseModeEnum.STATELESS,
appriseStatelessURL: 'https://example.com',
},
]);
@ -74,11 +83,11 @@ describe('Get Apprise Mode API', () => {
});
it('should return 500 if there is an error reading the file', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockImplementation(() => {
vi.mocked(getUsersList).mockImplementation(() => {
throw new Error();
});
const { req, res } = createMocks({ method: 'GET' });

View file

@ -3,15 +3,15 @@ import { createMocks } from 'node-mocks-http';
import { getUsersList } from '~/services';
import handler from '~/pages/api/account/getAppriseServices';
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
getUsersList: jest.fn(),
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
}));
describe('Get Apprise Services API', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
@ -21,7 +21,7 @@ describe('Get Apprise Services API', () => {
});
it('should return 401 if the user is not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
@ -29,12 +29,19 @@ describe('Get Apprise Services API', () => {
});
it('should return 400 if the user does not exist', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'nonexistent' },
});
(getUsersList as jest.Mock).mockResolvedValue([
{ username: 'testuser', appriseServices: ['service1', 'service2'] },
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseServices: ['service1', 'service2'],
},
]);
const { req, res } = createMocks({ method: 'GET' });
@ -47,12 +54,19 @@ describe('Get Apprise Services API', () => {
});
it('should return appriseServices if the user exists', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockResolvedValue([
{ username: 'testuser', appriseServices: ['service1', 'service2'] },
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseServices: ['service1', 'service2'],
},
]);
const { req, res } = createMocks({ method: 'GET' });
@ -65,11 +79,11 @@ describe('Get Apprise Services API', () => {
});
it('should return 500 if there is an error reading the file', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockImplementation(() => {
vi.mocked(getUsersList).mockImplementation(() => {
throw new Error();
});

View file

@ -1,19 +1,17 @@
import { getServerSession } from 'next-auth/next';
import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/account/getEmailAlert';
import { getServerSession } from 'next-auth/next';
import { promises as fs } from 'fs';
import path from 'path';
import { getUsersList } from '~/services';
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
getUsersList: jest.fn(),
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
}));
describe('Get Email Alert API', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not GET', async () => {
@ -23,7 +21,7 @@ describe('Get Email Alert API', () => {
});
it('should return 401 if the user is not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
@ -31,11 +29,20 @@ describe('Get Email Alert API', () => {
});
it('should return 400 if the user does not exist', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'nonexistent' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', emailAlert: true }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
email: 'testuser@example.com',
password: 'hashedpassword',
roles: ['user'],
emailAlert: true,
},
]);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
@ -47,11 +54,20 @@ describe('Get Email Alert API', () => {
});
it('should return emailAlert if the user exists', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', emailAlert: true }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
email: 'testuser@example.com',
password: 'hashedpassword',
roles: ['user'],
emailAlert: true,
},
]);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
@ -63,11 +79,11 @@ describe('Get Email Alert API', () => {
});
it('should return 500 if there is an error reading the file', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockImplementation(() => {
vi.mocked(getUsersList).mockImplementation(() => {
throw new Error();
});

View file

@ -2,7 +2,7 @@ import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/account/getWizardEnv';
import { getServerSession } from 'next-auth/next';
jest.mock('next-auth/next');
vi.mock('next-auth/next');
describe('Get Wizard Env API', () => {
it('should return 405 if the method is not GET', async () => {
@ -12,14 +12,14 @@ describe('Get Wizard Env API', () => {
});
it('should return 401 if the user is not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
});
it('should return 200 with wizardEnv if the user is authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testuser' } });
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testuser' } });
process.env.UNIX_USER = 'borgwarehouse';
process.env.FQDN = 'localhost';

View file

@ -2,34 +2,26 @@ import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/account/sendTestEmail';
import { getServerSession } from 'next-auth/next';
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/nodemailerSMTP', () => ({
vi.mock('next-auth/next', () => ({
__esModule: true,
default: jest.fn(() => ({
sendMail: jest.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
getServerSession: vi.fn(),
}));
vi.mock('~/helpers/functions/nodemailerSMTP', () => ({
__esModule: true,
default: vi.fn(() => ({
sendMail: vi.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
})),
}));
describe('Email API', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 401 if not authenticated', async () => {
// Mock unauthenticated session
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
// Simulate a POST request
const { req, res } = createMocks({ method: 'POST' });
@ -41,7 +33,7 @@ describe('Email API', () => {
it('should send an email if authenticated', async () => {
// Mock unauthenticated session
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { email: 'ada-lovelace@example.com', name: 'Lovelace' },
});

View file

@ -0,0 +1,208 @@
import handler from '~/pages/api/account/tokenManager';
import { createMocks } from 'node-mocks-http';
import { getServerSession } from 'next-auth/next';
import { getUsersList, updateUsersList } from '~/services';
import ApiResponse from '~/helpers/functions/apiResponse';
vi.mock('next-auth/next', () => ({
__esModule: true,
getServerSession: vi.fn(),
}));
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
updateUsersList: vi.fn(),
}));
vi.mock('~/helpers/functions/apiResponse');
describe('Token Manager API', () => {
afterEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return unauthorized if session is not found', async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
},
});
await handler(req, res);
expect(ApiResponse.unauthorized).toHaveBeenCalledWith(res);
});
it('should create a new token if valid data is provided', async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testUser',
password: 'hashedPassword',
email: 'testUser@example.com',
roles: ['user'],
tokens: [],
},
]);
vi.mocked(updateUsersList).mockResolvedValue();
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const responseData = JSON.parse(res._getData());
expect(responseData).toHaveProperty('token');
expect(updateUsersList).toHaveBeenCalled();
});
it('should return bad request if token name already exists', async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testUser',
password: 'hashedPassword',
email: 'testUser@example.com',
roles: ['user'],
tokens: [
{
token: 'sampleToken123',
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
creation: 123,
},
],
},
]);
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
},
});
await handler(req, res);
expect(ApiResponse.badRequest).toHaveBeenCalledWith(res, 'Token name already exists');
});
it('should return token list for GET request', async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testUser',
password: 'hashedPassword',
email: 'testUser@example.com',
roles: ['user'],
tokens: [
{
token: 'sampleToken1',
name: 'token1',
permissions: { create: false, read: false, update: false, delete: false },
creation: 123,
},
{
token: 'sampleToken2',
name: 'token2',
permissions: { create: false, read: false, update: false, delete: false },
creation: 456,
},
],
},
]);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const responseData = JSON.parse(res._getData());
expect(responseData).toEqual([
{
name: 'token1',
permissions: { create: false, read: false, update: false, delete: false },
creation: 123,
},
{
name: 'token2',
permissions: { create: false, read: false, update: false, delete: false },
creation: 456,
},
]);
});
it('should delete a token for DELETE request', async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testUser',
password: 'hashedPassword',
email: 'testUser@example.com',
roles: ['user'],
tokens: [
{
token: 'sampleToken1',
name: 'token1',
permissions: { create: false, read: false, update: false, delete: false },
creation: 123,
},
{
token: 'sampleToken2',
name: 'token2',
permissions: { create: false, read: false, update: false, delete: false },
creation: 456,
},
],
},
]);
vi.mocked(updateUsersList).mockResolvedValue();
const { req, res } = createMocks({
method: 'DELETE',
body: { name: 'token1' },
});
await handler(req, res);
expect(ApiResponse.success).toHaveBeenCalledWith(res, 'Token deleted');
expect(updateUsersList).toHaveBeenCalled();
});
it('should return bad request if token name is missing in DELETE request', async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
const { req, res } = createMocks({
method: 'DELETE',
body: {},
});
await handler(req, res);
expect(ApiResponse.badRequest).toHaveBeenCalledWith(res, 'Missing token name');
});
it('should return method not allowed for unsupported HTTP methods', async () => {
const { req, res } = createMocks({ method: 'PUT' });
await handler(req, res);
expect(ApiResponse.methodNotAllowed).toHaveBeenCalledWith(res);
});
});

View file

@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth/next';
import { NextApiRequest, NextApiResponse } from 'next';
import { IntegrationTokenType, TokenPermissionsType } from '~/types/api/integration.types';
import ApiResponse from '~/helpers/functions/apiResponse';
import { getUsersList, updateUsersList } from '~/helpers/functions';
import { getUsersList, updateUsersList } from '~/services';
import { getUnixTime } from 'date-fns';
import { v4 as uuidv4 } from 'uuid';
import { BorgWarehouseApiResponse } from '~/types/api/error.types';

View file

@ -3,19 +3,19 @@ import handler from '~/pages/api/account/updateAppriseAlert';
import { getServerSession } from 'next-auth/next';
import { getUsersList, updateUsersList } from '~/services';
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
__esModule: true,
getUsersList: jest.fn(),
updateUsersList: jest.fn(),
getUsersList: vi.fn(),
updateUsersList: vi.fn(),
}));
describe('Notifications API', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.resetAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if the method is not PUT', async () => {
@ -25,7 +25,7 @@ describe('Notifications API', () => {
});
it('should return 401 if the user is not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
await handler(req, res);
@ -33,7 +33,7 @@ describe('Notifications API', () => {
});
it('should return 422 if the request body is invalid', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
@ -44,11 +44,20 @@ describe('Notifications API', () => {
});
it('should return 400 if the user does not exist', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'nonexistent' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', appriseAlert: false }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseAlert: false,
},
]);
const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
await handler(req, res);
@ -59,26 +68,44 @@ describe('Notifications API', () => {
});
it('should update appriseAlert and return 200 if everything is correct', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testuser', appriseAlert: false }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseAlert: false,
},
]);
const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
await handler(req, res);
expect(updateUsersList).toHaveBeenCalledWith([{ username: 'testuser', appriseAlert: true }]);
expect(updateUsersList).toHaveBeenCalledWith([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseAlert: true,
},
]);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
});
it('should return 500 if there is an error reading users file', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'testuser' },
});
(getUsersList as jest.Mock).mockRejectedValue({ code: 'ENOENT' });
vi.mocked(getUsersList).mockRejectedValue({ code: 'ENOENT' });
const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
await handler(req, res);

View file

@ -2,11 +2,12 @@ import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/account/updateAppriseMode';
import { getServerSession } from 'next-auth/next';
import { getUsersList, updateUsersList } from '~/services';
import { AppriseModeEnum } from '~/types/domain/config.types';
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
getUsersList: jest.fn(),
updateUsersList: jest.fn(),
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
updateUsersList: vi.fn(),
}));
describe('Apprise Mode API', () => {
@ -17,7 +18,7 @@ describe('Apprise Mode API', () => {
});
it('should return 401 if not authenticated', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'PUT' });
await handler(req, res);
@ -26,7 +27,7 @@ describe('Apprise Mode API', () => {
});
it('should return 422 if invalid data is provided', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testuser' } });
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testuser' } });
const { req, res } = createMocks({
method: 'PUT',
@ -38,9 +39,16 @@ describe('Apprise Mode API', () => {
});
it('should return 400 if user does not exist', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'unknownuser' } });
(getUsersList as jest.Mock).mockResolvedValue([
{ username: 'testuser', appriseMode: 'package' },
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'unknownuser' } });
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseMode: AppriseModeEnum.PACKAGE,
},
]);
const { req, res } = createMocks({
@ -53,11 +61,18 @@ describe('Apprise Mode API', () => {
});
it('should update user settings and return 200', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testuser' } });
(getUsersList as jest.Mock).mockResolvedValue([
{ username: 'testuser', appriseMode: 'package' },
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testuser' } });
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'testuser',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseMode: AppriseModeEnum.PACKAGE,
},
]);
(updateUsersList as jest.Mock).mockResolvedValue(null);
vi.mocked(updateUsersList).mockResolvedValue();
const { req, res } = createMocks({
method: 'PUT',
@ -68,8 +83,12 @@ describe('Apprise Mode API', () => {
expect(updateUsersList).toHaveBeenCalledWith([
{
id: 1,
username: 'testuser',
appriseMode: 'stateless',
password: 'hashedpassword',
roles: ['user'],
email: 'testuser@example.com',
appriseMode: AppriseModeEnum.STATELESS,
appriseStatelessURL: 'https://example.com',
},
]);

View file

@ -3,24 +3,23 @@ import handler from '~/pages/api/account/updateAppriseServices';
import { getServerSession } from 'next-auth/next';
import { getUsersList, updateUsersList } from '~/services';
// Mock imports
jest.mock('next-auth/next');
jest.mock('~/services', () => ({
getUsersList: jest.fn(),
updateUsersList: jest.fn(),
vi.mock('next-auth/next');
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
updateUsersList: vi.fn(),
}));
describe('PUT /api/account/updateAppriseURLs', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.resetAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 401 if not authenticated', async () => {
// Mock unauthenticated session
(getServerSession as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
const { req, res } = createMocks({ method: 'PUT' });
await handler(req, res);
@ -37,11 +36,19 @@ describe('PUT /api/account/updateAppriseURLs', () => {
it('should return 400 if user is not found in the users list', async () => {
// Mock authenticated session
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'Lovelace' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'Ada' }]);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'Ada',
password: 'securepassword',
roles: ['user'],
email: 'ada@example.com',
},
]);
const { req, res } = createMocks({
method: 'PUT',
@ -56,12 +63,21 @@ describe('PUT /api/account/updateAppriseURLs', () => {
});
it('should return 200 and successfully update the appriseURLs', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'Lovelace' },
});
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'Lovelace', appriseServices: [] }]);
(updateUsersList as jest.Mock).mockResolvedValue(null);
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
username: 'Lovelace',
password: 'securepassword',
roles: ['user'],
email: 'lovelace@example.com',
appriseServices: [],
},
]);
vi.mocked(updateUsersList).mockResolvedValue();
const { req, res } = createMocks({
method: 'PUT',
@ -74,11 +90,11 @@ describe('PUT /api/account/updateAppriseURLs', () => {
});
it('should return 500 if there is an error reading the users file', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'Lovelace' },
});
(getUsersList as jest.Mock).mockRejectedValue({ code: 'ENOENT' });
vi.mocked(getUsersList).mockRejectedValue({ code: 'ENOENT' });
const { req, res } = createMocks({
method: 'PUT',
@ -94,11 +110,11 @@ describe('PUT /api/account/updateAppriseURLs', () => {
});
it('should return 500 if there is an API error', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'Lovelace' },
});
(getUsersList as jest.Mock).mockRejectedValue({ code: 'UNKNOWN_ERROR' });
vi.mocked(getUsersList).mockRejectedValue({ code: 'UNKNOWN_ERROR' });
const { req, res } = createMocks({
method: 'PUT',

View file

@ -0,0 +1,137 @@
import { describe, it, expect, vi } from 'vitest';
import handler from './updateEmail';
import { getUsersList, updateUsersList } from '~/services';
import { getServerSession } from 'next-auth/next';
import { NextApiRequest, NextApiResponse } from 'next';
vi.mock('~/services', () => ({
getUsersList: vi.fn(),
updateUsersList: vi.fn(),
}));
vi.mock('next-auth/next', () => ({
getServerSession: vi.fn(),
}));
describe('updateEmail API handler', () => {
const mockReq = {
method: 'PUT',
body: { email: 'newemail@example.com' },
} as unknown as NextApiRequest;
const mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as NextApiResponse;
it('should return 405 if method is not PUT', async () => {
mockReq.method = 'GET';
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(405);
});
it('should return 401 if session is not found', async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(401);
});
it('should return 422 if email is not provided', async () => {
mockReq.body = {};
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { name: 'testuser' } });
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(422);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unexpected data' });
});
it('should return 400 if user is not found in users list', async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { name: 'testuser' } });
vi.mocked(getUsersList).mockResolvedValueOnce([]);
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({
message: 'User is incorrect. Please, logout to update your session.',
});
});
it('should return 400 if email already exists', async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { name: 'testuser' } });
vi.mocked(getUsersList).mockResolvedValueOnce([
{
username: 'testuser',
email: 'oldemail@example.com',
id: 1,
password: 'hashedpassword',
roles: ['user'],
},
{
username: 'otheruser',
email: 'newemail@example.com',
id: 2,
password: 'hashedpassword',
roles: ['user'],
},
]);
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Email already exists' });
});
it('should update email and return 200 on success', async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { name: 'testuser' } });
vi.mocked(getUsersList).mockResolvedValueOnce([
{
username: 'testuser',
email: 'oldemail@example.com',
id: 1,
password: 'hashedpassword',
roles: ['user'],
},
{
username: 'otheruser',
email: 'otheremail@example.com',
id: 2,
password: 'hashedpassword',
roles: ['user'],
},
]);
vi.mocked(updateUsersList).mockResolvedValueOnce(undefined);
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Successful API send' });
expect(updateUsersList).toHaveBeenCalledWith([
{ username: 'testuser', email: 'newemail@example.com' },
{ username: 'otheruser', email: 'otheremail@example.com' },
]);
});
it('should return 500 if an unexpected error occurs', async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { name: 'testuser' } });
vi.mocked(getUsersList).mockRejectedValueOnce(new Error('Unexpected error'));
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(500);
expect(mockRes.json).toHaveBeenCalledWith({
status: 500,
message: 'API error, contact the administrator',
});
});
it('should return 500 with specific message if ENOENT error occurs', async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { name: 'testuser' } });
const enoentError = new Error('No such file or directory');
(enoentError as any).code = 'ENOENT';
vi.mocked(getUsersList).mockRejectedValueOnce(enoentError);
await handler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(500);
expect(mockRes.json).toHaveBeenCalledWith({
status: 500,
message: 'No such file or directory',
});
});
});

View file

@ -1,5 +1,6 @@
import { getUsersList, updateUsersList, hashPassword, verifyPassword } from '~/helpers/functions';
import { hashPassword, verifyPassword } from '~/helpers/functions';
import { authOptions } from '../auth/[...nextauth]';
import { getUsersList, updateUsersList } from '~/services';
import { getServerSession } from 'next-auth/next';
import { NextApiRequest, NextApiResponse } from 'next';
import { ErrorResponse } from '~/types/api/error.types';

View file

View file

@ -1,4 +1,4 @@
import { getUsersList, updateUsersList } from '~/helpers/functions';
import { getUsersList, updateUsersList } from '~/services';
import { authOptions } from '../auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import { UsernameSettingDTO } from '~/types/api/setting.types';

View file

@ -3,33 +3,33 @@ import handler from '~/pages/api/cronjob/checkStatus';
import { getRepoList, getUsersList, updateRepoList } from '~/services';
import { getLastSaveListShell } from '~/helpers/functions/shell.utils';
import nodemailerSMTP from '~/helpers/functions/nodemailerSMTP';
import { AppriseModeEnum } from '~/types/domain/config.types';
jest.mock('~/services', () => ({
getRepoList: jest.fn(),
getUsersList: jest.fn(),
updateRepoList: jest.fn(),
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
getUsersList: vi.fn(),
updateRepoList: vi.fn(),
}));
jest.mock('~/helpers/functions/shell.utils', () => ({
getLastSaveListShell: jest.fn(),
vi.mock('~/helpers/functions/shell.utils', () => ({
getLastSaveListShell: vi.fn(),
}));
jest.mock('~/helpers/functions/nodemailerSMTP', () => ({
__esModule: true,
default: jest.fn(() => ({
sendMail: jest.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
vi.mock('~/helpers/functions/nodemailerSMTP', () => ({
default: vi.fn(() => ({
sendMail: vi.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
})),
}));
jest.mock('~/helpers/templates/emailAlertStatus', () =>
jest.fn(() => ({
vi.mock('~/helpers/templates/emailAlertStatus', () => ({
default: vi.fn(() => ({
subject: 'Alert',
text: 'Alert text',
}))
);
})),
}));
jest.mock('node:child_process', () => ({
exec: jest.fn(
vi.mock('node:child_process', () => ({
exec: vi.fn(
(
command: string,
callback: (err: Error | null, result: { stdout: string; stderr: string }) => void
@ -42,9 +42,9 @@ jest.mock('node:child_process', () => ({
describe('Cronjob API Handler', () => {
beforeEach(() => {
process.env.CRONJOB_KEY = 'test-key';
jest.clearAllMocks();
jest.resetModules();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 401 if no authorization header', async () => {
@ -72,10 +72,8 @@ describe('Cronjob API Handler', () => {
});
it('should return 200 with message if no repository to check (empty repoList)', async () => {
(getRepoList as jest.Mock).mockResolvedValue([]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: 123 },
]);
vi.mocked(getRepoList).mockResolvedValue([]);
vi.mocked(getLastSaveListShell).mockResolvedValue([{ repositoryName: 'repo1', lastSave: 123 }]);
const { req, res } = createMocks({
method: 'POST',
@ -91,10 +89,21 @@ describe('Cronjob API Handler', () => {
});
it('should return 200 with message if no repository to check (empty lastSaveList)', async () => {
(getRepoList as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', alert: 100, alias: 'Repo1' },
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alert: 100,
alias: 'Repo1',
id: 1,
status: true,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([]);
vi.mocked(getLastSaveListShell).mockResolvedValue([]);
const { req, res } = createMocks({
method: 'POST',
@ -111,14 +120,25 @@ describe('Cronjob API Handler', () => {
it('should execute successfully without alerts if all repositories are OK', async () => {
const currentTime = Math.floor(Date.now() / 1000);
(getRepoList as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', alert: 1000, alias: 'Repo1', status: true },
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alert: 1000,
alias: 'Repo1',
status: true,
id: 1,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime },
]);
(updateRepoList as jest.Mock).mockResolvedValue(undefined);
(getUsersList as jest.Mock).mockResolvedValue([]);
vi.mocked(updateRepoList).mockResolvedValue(undefined);
vi.mocked(getUsersList).mockResolvedValue([]);
const { req, res } = createMocks({
method: 'POST',
@ -135,7 +155,7 @@ describe('Cronjob API Handler', () => {
});
it('should return 500 if an error occurs', async () => {
(getRepoList as jest.Mock).mockRejectedValue(new Error('Test error'));
vi.mocked(getRepoList).mockRejectedValue(new Error('Test error'));
const { req, res } = createMocks({
method: 'POST',
@ -153,19 +173,33 @@ describe('Cronjob API Handler', () => {
it('should not send email alert if emailAlert is false', async () => {
const currentTime = Math.floor(Date.now() / 1000);
(getRepoList as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', alert: 100, alias: 'Repo1' },
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alert: 100,
alias: 'Repo1',
id: 1,
status: true,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
]);
// User has disabled email alert but enabled Apprise alert
(getUsersList as jest.Mock).mockResolvedValue([
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
password: 'hashed-password',
roles: ['user'],
emailAlert: false,
appriseAlert: true,
appriseServices: ['http://example.com'],
appriseMode: 'package',
appriseMode: AppriseModeEnum.PACKAGE,
appriseStatelessURL: 'http://example.com',
email: 'test@example.com',
username: 'testuser',
@ -183,19 +217,33 @@ describe('Cronjob API Handler', () => {
it('should not send apprise alert if appriseAlert is false', async () => {
const currentTime = Math.floor(Date.now() / 1000);
(getRepoList as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', alert: 100, alias: 'Repo1' },
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alert: 100,
alias: 'Repo1',
id: 1,
status: true,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
]);
// User has disabled Apprise alert but enabled email alert
(getUsersList as jest.Mock).mockResolvedValue([
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
password: 'hashed-password',
roles: ['user'],
emailAlert: true,
appriseAlert: false,
appriseServices: ['http://example.com'],
appriseMode: 'package',
appriseMode: AppriseModeEnum.PACKAGE,
appriseStatelessURL: 'http://example.com',
email: 'test@example.com',
username: 'testuser',
@ -203,7 +251,7 @@ describe('Cronjob API Handler', () => {
]);
// Spy on exec to check if it is called
const execSpy = jest.spyOn(require('node:child_process'), 'exec');
const execSpy = vi.spyOn(require('node:child_process'), 'exec');
const { req, res } = createMocks({
method: 'POST',
headers: { authorization: 'Bearer test-key' },
@ -216,46 +264,68 @@ describe('Cronjob API Handler', () => {
it('should not send alert if alert is disabled on repo (repo.alert === 0)', async () => {
const currentTime = Math.floor(Date.now() / 1000);
(getRepoList as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', alert: 0, alias: 'Repo1' },
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alert: 0,
alias: 'Repo1',
id: 1,
status: false,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime - 1000 },
]);
(getUsersList as jest.Mock).mockResolvedValue([
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
password: 'hashed-password',
roles: ['user'],
emailAlert: true,
appriseAlert: true,
appriseServices: ['http://example.com'],
appriseMode: 'package',
appriseMode: AppriseModeEnum.PACKAGE,
appriseStatelessURL: 'http://example.com',
email: 'test@example.com',
username: 'testuser',
},
]);
// Spy on exec to check if it is called
const nodemailerSpy = jest.spyOn(require('~/helpers/functions/nodemailerSMTP'), 'default');
const execSpy = jest.spyOn(require('node:child_process'), 'exec');
const { req, res } = createMocks({
method: 'POST',
headers: { authorization: 'Bearer test-key' },
});
await handler(req, res);
expect(nodemailerSpy).not.toHaveBeenCalled();
expect(execSpy).not.toHaveBeenCalled();
expect(nodemailerSMTP).not.toHaveBeenCalled();
nodemailerSpy.mockRestore();
execSpy.mockRestore();
const childProcess = await import('node:child_process');
expect(childProcess.exec).not.toHaveBeenCalled();
});
it('should not update lastStatusAlertSend or add to repoListToSendAlert if repo status is OK', async () => {
(getRepoList as jest.Mock).mockResolvedValue([
{ repositoryName: 'repo1', status: true, alert: 100 },
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
status: true,
alert: 100,
id: 1,
alias: 'Repo1',
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
lastStatusAlertSend: 1000,
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(updateRepoList).mockResolvedValue(undefined);
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: Math.floor(Date.now() / 1000) },
]);
@ -271,7 +341,14 @@ describe('Cronjob API Handler', () => {
repositoryName: 'repo1',
status: true,
alert: 100,
id: 1,
alias: 'Repo1',
lastSave: expect.any(Number),
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
lastStatusAlertSend: 1000,
},
]);
expect(res._getStatusCode()).toBe(200);
@ -279,19 +356,32 @@ describe('Cronjob API Handler', () => {
it('should update lastStatusAlertSend if repo is down and alert is enabled', async () => {
const currentTime = 1741535661;
(getRepoList as jest.Mock).mockResolvedValue([
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alias: 'Repo1',
status: false,
alert: 100,
id: 1,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
]);
(getUsersList as jest.Mock).mockResolvedValue([
{ emailAlert: true, email: 'test@example.com', username: 'TestUser' },
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
password: 'hashed-password',
roles: ['user'],
emailAlert: true,
email: 'test@example.com',
username: 'TestUser',
},
]);
const { req, res } = createMocks({
@ -307,8 +397,13 @@ describe('Cronjob API Handler', () => {
alias: 'Repo1',
status: false,
alert: 100,
id: 1,
lastSave: currentTime - 200,
lastStatusAlertSend: expect.any(Number),
lastSave: expect.any(Number),
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
expect(res._getStatusCode()).toBe(200);
@ -316,16 +411,22 @@ describe('Cronjob API Handler', () => {
it('should not update lastStatusAlertSend or send alerts if alert is disabled', async () => {
const currentTime = Math.floor(Date.now() / 1000);
(getRepoList as jest.Mock).mockResolvedValue([
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alias: 'Repo1',
status: false,
alert: 0,
lastStatusAlertSend: null,
lastStatusAlertSend: undefined,
id: 1,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
]);
@ -342,8 +443,13 @@ describe('Cronjob API Handler', () => {
alias: 'Repo1',
status: false,
alert: 0,
lastStatusAlertSend: null,
lastStatusAlertSend: undefined,
id: 1,
lastSave: currentTime - 200,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
expect(nodemailerSMTP).not.toHaveBeenCalled();
@ -352,20 +458,33 @@ describe('Cronjob API Handler', () => {
it('should update lastStatusAlertSend only if the last alert was sent more than 90000 seconds ago', async () => {
const currentTime = Math.floor(Date.now() / 1000);
(getRepoList as jest.Mock).mockResolvedValue([
vi.mocked(getRepoList).mockResolvedValue([
{
repositoryName: 'repo1',
alias: 'Repo1',
status: false,
alert: 100,
lastStatusAlertSend: currentTime - 80000,
id: 1,
lastSave: 0,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
(getLastSaveListShell as jest.Mock).mockResolvedValue([
vi.mocked(getLastSaveListShell).mockResolvedValue([
{ repositoryName: 'repo1', lastSave: currentTime - 200 },
]);
(getUsersList as jest.Mock).mockResolvedValue([
{ emailAlert: true, email: 'test@example.com', username: 'TestUser' },
vi.mocked(getUsersList).mockResolvedValue([
{
id: 1,
password: 'hashed-password',
roles: ['user'],
emailAlert: true,
email: 'test@example.com',
username: 'TestUser',
},
]);
const { req, res } = createMocks({
@ -382,7 +501,12 @@ describe('Cronjob API Handler', () => {
status: false,
alert: 100,
lastStatusAlertSend: expect.any(Number),
id: 1,
lastSave: currentTime - 200,
storageSize: 0,
storageUsed: 0,
sshPublicKey: '',
comment: '',
},
]);
expect(res._getStatusCode()).toBe(200);

View file

@ -1,23 +1,21 @@
import handler from '~/pages/api/cronjob/getStorageUsed';
import { createMocks } from 'node-mocks-http';
import { getRepoList, updateRepoList } from '~/helpers/functions';
import { getRepoList, updateRepoList } from '~/services';
import { getStorageUsedShell } from '~/helpers/functions/shell.utils';
jest.mock('~/helpers/functions', () => ({
getRepoList: jest.fn(),
updateRepoList: jest.fn(),
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
updateRepoList: vi.fn(),
}));
jest.mock('~/helpers/functions/shell.utils', () => ({
getStorageUsedShell: jest.fn(),
vi.mock('~/helpers/functions/shell.utils', () => ({
getStorageUsedShell: vi.fn(),
}));
describe('GET /api/cronjob/getStorageUsed', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
const CRONJOB_KEY = 'test-cronjob-key';
@ -47,7 +45,7 @@ describe('GET /api/cronjob/getStorageUsed', () => {
});
it('should return success if no repositories are found', async () => {
(getRepoList as jest.Mock).mockResolvedValue([]);
vi.mocked(getRepoList).mockResolvedValue([]);
const { req, res } = createMocks({
method: 'POST',
@ -72,9 +70,9 @@ describe('GET /api/cronjob/getStorageUsed', () => {
{ name: 'repo2', size: 200 },
];
(getRepoList as jest.Mock).mockResolvedValue(mockRepoList);
(getStorageUsedShell as jest.Mock).mockResolvedValue(mockStorageUsed);
(updateRepoList as jest.Mock).mockResolvedValue(undefined);
vi.mocked(getRepoList).mockResolvedValue(mockRepoList);
vi.mocked(getStorageUsedShell).mockResolvedValue(mockStorageUsed);
vi.mocked(updateRepoList).mockResolvedValue(undefined);
const { req, res } = createMocks({
method: 'POST',
@ -94,7 +92,7 @@ describe('GET /api/cronjob/getStorageUsed', () => {
});
it('should return server error if an exception occurs', async () => {
(getRepoList as jest.Mock).mockRejectedValue(new Error('Test error'));
vi.mocked(getRepoList).mockRejectedValue(new Error('Test error'));
const { req, res } = createMocks({
method: 'POST',
@ -115,9 +113,9 @@ describe('GET /api/cronjob/getStorageUsed', () => {
];
const mockStorageUsed = [{ name: 'repo1', size: 100 }];
(getRepoList as jest.Mock).mockResolvedValue(mockRepoList);
(getStorageUsedShell as jest.Mock).mockResolvedValue(mockStorageUsed);
(updateRepoList as jest.Mock).mockResolvedValue(undefined);
vi.mocked(getRepoList).mockResolvedValue(mockRepoList);
vi.mocked(getStorageUsedShell).mockResolvedValue(mockStorageUsed);
vi.mocked(updateRepoList).mockResolvedValue(undefined);
const { req, res } = createMocks({
method: 'POST',

View file

@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getRepoList, updateRepoList } from '~/helpers/functions';
import { getRepoList, updateRepoList } from '~/services';
import ApiResponse from '~/helpers/functions/apiResponse';
import { getStorageUsedShell } from '~/helpers/functions/shell.utils';
import { BorgWarehouseApiResponse } from '~/types/api/error.types';

View file

@ -1,45 +1,34 @@
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 { tokenController, isSshPubKeyDuplicate } from '~/helpers/functions';
import { getRepoList, updateRepoList } from '~/services';
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(),
vi.mock('next-auth/next', () => ({
getServerSession: vi.fn(),
}));
jest.mock('~/helpers/functions', () => ({
getRepoList: jest.fn(),
updateRepoList: jest.fn(),
tokenController: jest.fn(),
isSshPubKeyDuplicate: jest.fn(),
vi.mock('~/helpers/functions', () => ({
tokenController: vi.fn(),
isSshPubKeyDuplicate: vi.fn(),
}));
jest.mock('~/helpers/functions/shell.utils', () => ({
createRepoShell: jest.fn(),
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
updateRepoList: vi.fn(),
}));
vi.mock('~/helpers/functions/shell.utils', () => ({
createRepoShell: vi.fn(),
}));
describe('POST /api/repo/add', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.resetAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if method is not POST', async () => {
@ -55,8 +44,8 @@ describe('POST /api/repo/add', () => {
});
it('should return 401 if API key is invalid', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'POST',
headers: { authorization: 'Bearer INVALID_API_KEY' },
@ -66,8 +55,8 @@ describe('POST /api/repo/add', () => {
});
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 });
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ create: false });
const { req, res } = createMocks({
method: 'POST',
headers: { authorization: 'Bearer API_KEY' },
@ -77,9 +66,9 @@ describe('POST /api/repo/add', () => {
});
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);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([{ id: 1, sshPublicKey: 'duplicate-key' }]);
(isSshPubKeyDuplicate as vi.Mock).mockReturnValue(true);
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'duplicate-key', storageSize: 10 },
@ -89,9 +78,9 @@ describe('POST /api/repo/add', () => {
});
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' });
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([]);
(createRepoShell as vi.Mock).mockResolvedValue({ stderr: 'Error' });
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
@ -101,10 +90,10 @@ describe('POST /api/repo/add', () => {
});
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);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([]);
(createRepoShell as vi.Mock).mockResolvedValue({ stdout: 'new-repo' });
vi.mocked(updateRepoList).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',
body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
@ -115,10 +104,10 @@ describe('POST /api/repo/add', () => {
});
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);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([]);
(createRepoShell as vi.Mock).mockResolvedValue({ stdout: 'new-repo' });
vi.mocked(updateRepoList).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',
@ -153,16 +142,16 @@ describe('POST /api/repo/add', () => {
});
it('should assign the correct ID based on existing repositories', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue([
vi.mocked(getRepoList).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);
(createRepoShell as vi.Mock).mockResolvedValue({ stdout: 'new-repo' });
vi.mocked(updateRepoList).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',

View file

@ -1,16 +1,12 @@
import { authOptions } from '../auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import {
getRepoList,
updateRepoList,
tokenController,
isSshPubKeyDuplicate,
} from '~/helpers/functions';
import { tokenController, isSshPubKeyDuplicate } from '~/helpers/functions';
import { NextApiRequest, NextApiResponse } from 'next';
import ApiResponse from '~/helpers/functions/apiResponse';
import { Repository } from '~/types/domain/config.types';
import { createRepoShell } from '~/helpers/functions/shell.utils';
import { getUnixTime } from 'date-fns';
import { getRepoList, updateRepoList } from '~/services';
export default async function handler(
req: NextApiRequest & { body: Partial<Repository> },

View file

@ -2,39 +2,33 @@ import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/repo/id/[slug]/delete';
import { getServerSession } from 'next-auth/next';
import { deleteRepoShell } from '~/helpers/functions/shell.utils';
import { getRepoList, tokenController, updateRepoList } from '~/helpers/functions';
import { getRepoList, updateRepoList } from '~/services';
import { tokenController } 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(),
vi.mock('next-auth/next', () => ({
getServerSession: vi.fn(),
}));
jest.mock('~/helpers/functions', () => ({
getRepoList: jest.fn(),
updateRepoList: jest.fn(),
tokenController: jest.fn(),
vi.mock('~/helpers/functions', () => ({
tokenController: vi.fn(),
}));
jest.mock('~/helpers/functions/shell.utils', () => {
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
updateRepoList: vi.fn(),
}));
vi.mock('~/helpers/functions/shell.utils', () => {
return {
deleteRepoShell: jest.fn(),
deleteRepoShell: vi.fn(),
};
});
describe('DELETE /api/repo/id/[slug]/delete', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if method is not DELETE', async () => {
@ -51,7 +45,7 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
it('should return 403 if deletion is disabled via environment variable', async () => {
process.env.DISABLE_DELETE_REPO = 'true';
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'USER' },
});
const { req, res } = createMocks({ method: 'DELETE' });
@ -65,7 +59,7 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
it('should return 400 if slug is missing or malformed', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'USER' },
});
const { req, res } = createMocks({
@ -81,14 +75,14 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
it('should return 404 if repository is not found', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'USER' },
});
const { req, res } = createMocks({
method: 'DELETE',
query: { slug: '123' },
});
(getRepoList as jest.Mock).mockResolvedValue([]);
vi.mocked(getRepoList).mockResolvedValue([]);
await handler(req, res);
expect(res._getStatusCode()).toBe(404);
expect(res._getJSONData()).toEqual({
@ -98,15 +92,15 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
it('should return 500 if deleteRepo fails', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'USER' },
});
const { req, res } = createMocks({
method: 'DELETE',
query: { slug: '123' },
});
(getRepoList as jest.Mock).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]);
(deleteRepoShell as jest.Mock).mockResolvedValue({ stderr: 'Error' });
vi.mocked(getRepoList).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]);
vi.mocked(deleteRepoShell).mockResolvedValue({ stderr: 'Error' });
await handler(req, res);
expect(res._getStatusCode()).toBe(500);
expect(res._getJSONData()).toEqual({
@ -117,16 +111,16 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
it('should delete the repository and return 200 on success with a session', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
vi.mocked(getServerSession).mockResolvedValue({
user: { name: 'USER' },
});
const { req, res } = createMocks({
method: 'DELETE',
query: { slug: '1234' },
});
(getRepoList as jest.Mock).mockResolvedValue([{ id: 1234, repositoryName: 'test-repo' }]);
(deleteRepoShell as jest.Mock).mockResolvedValue({ stderr: null });
(updateRepoList as jest.Mock).mockResolvedValue(true);
vi.mocked(getRepoList).mockResolvedValue([{ id: 1234, repositoryName: 'test-repo' }]);
vi.mocked(deleteRepoShell).mockResolvedValue({ stderr: null });
vi.mocked(updateRepoList).mockResolvedValue(true);
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({
@ -137,8 +131,8 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
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 });
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ delete: true });
const { req, res } = createMocks({
method: 'DELETE',
query: { slug: '12345' },
@ -146,9 +140,9 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
authorization: 'Bearer API_KEY',
},
});
(getRepoList as jest.Mock).mockResolvedValue([{ id: 12345, repositoryName: 'test-repo2' }]);
(deleteRepoShell as jest.Mock).mockResolvedValue({ stderr: null });
(updateRepoList as jest.Mock).mockResolvedValue(true);
vi.mocked(getRepoList).mockResolvedValue([{ id: 12345, repositoryName: 'test-repo2' }]);
vi.mocked(deleteRepoShell).mockResolvedValue({ stderr: null });
vi.mocked(updateRepoList).mockResolvedValue(true);
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({
@ -159,8 +153,8 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
it('should return 401 if the API key is invalid', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'DELETE',
query: { slug: '12345' },
@ -177,8 +171,8 @@ describe('DELETE /api/repo/id/[slug]/delete', () => {
});
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 });
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ delete: false });
const { req, res } = createMocks({
method: 'DELETE',
query: { slug: '12345' },

View file

@ -1,7 +1,8 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth/next';
import { getRepoList, updateRepoList, tokenController } from '~/helpers/functions';
import { tokenController } from '~/helpers/functions';
import { deleteRepoShell } from '~/helpers/functions/shell.utils';
import { getRepoList, updateRepoList } from '~/services';
import ApiResponse from '~/helpers/functions/apiResponse';
import { BorgWarehouseApiResponse } from '~/types/api/error.types';

View file

@ -2,44 +2,34 @@ 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';
import { tokenController, isSshPubKeyDuplicate } from '~/helpers/functions';
import { getRepoList, updateRepoList } from '~/services';
jest.mock('next-auth', () => {
return jest.fn(() => {
return {
auth: { session: {} },
GET: jest.fn(),
POST: jest.fn(),
};
});
});
jest.mock('next-auth/next', () => ({
getServerSession: jest.fn(),
vi.mock('next-auth/next', () => ({
__esModule: true,
getServerSession: vi.fn(),
}));
jest.mock('~/helpers/functions', () => ({
getRepoList: jest.fn(),
updateRepoList: jest.fn(),
tokenController: jest.fn(),
isSshPubKeyDuplicate: jest.fn(),
vi.mock('~/helpers/functions', () => ({
tokenController: vi.fn(),
isSshPubKeyDuplicate: vi.fn(),
}));
jest.mock('~/helpers/functions/shell.utils', () => ({
updateRepoShell: jest.fn(),
vi.mock('~/helpers/functions/shell.utils', () => ({
updateRepoShell: vi.fn(),
}));
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
updateRepoList: vi.fn(),
}));
describe('PATCH /api/repo/id/[slug]/edit', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.resetAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if method is not PATCH', async () => {
@ -55,8 +45,8 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
it('should return 401 if API key is invalid', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'PATCH',
headers: { authorization: 'Bearer INVALID_API_KEY' },
@ -66,8 +56,8 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
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 });
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ update: false });
const { req, res } = createMocks({
method: 'PATCH',
headers: { authorization: 'Bearer API_KEY' },
@ -77,24 +67,24 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
it('should return 400 if slug is missing or malformed', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getServerSession).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([]);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).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);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]);
(isSshPubKeyDuplicate as vi.Mock).mockReturnValue(true);
const { req, res } = createMocks({
method: 'PATCH',
query: { slug: '123' },
@ -105,9 +95,9 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
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' });
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]);
(updateRepoShell as vi.Mock).mockResolvedValue({ stderr: 'Error' });
const { req, res } = createMocks({
method: 'PATCH',
query: { slug: '123' },
@ -118,10 +108,10 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
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);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([{ id: 123, repositoryName: 'test-repo' }]);
(updateRepoShell as vi.Mock).mockResolvedValue({ stderr: null });
vi.mocked(updateRepoList).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'PATCH',
query: { slug: '123' },
@ -133,11 +123,11 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
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);
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ update: true });
vi.mocked(getRepoList).mockResolvedValue([{ id: 456, repositoryName: 'repo-key' }]);
(updateRepoShell as vi.Mock).mockResolvedValue({ stderr: null });
vi.mocked(updateRepoList).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'PATCH',
query: { slug: '456' },
@ -150,8 +140,8 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
});
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([
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([
{
id: 123,
repositoryName: 'test-repo',
@ -161,8 +151,8 @@ describe('PATCH /api/repo/id/[slug]/edit', () => {
lanCommand: false,
},
]);
(updateRepoShell as jest.Mock).mockResolvedValue({ stderr: null });
(updateRepoList as jest.Mock).mockResolvedValue(true);
(updateRepoShell as vi.Mock).mockResolvedValue({ stderr: null });
vi.mocked(updateRepoList).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'PATCH',
query: { slug: '123' },

View file

@ -1,15 +1,11 @@
import { authOptions } from '../../../auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import {
getRepoList,
updateRepoList,
tokenController,
isSshPubKeyDuplicate,
} from '~/helpers/functions';
import { tokenController, isSshPubKeyDuplicate } from '~/helpers/functions';
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';
import { getRepoList, updateRepoList } from '~/services';
export default async function handler(
req: NextApiRequest & { body: Partial<Repository> },

View file

@ -3,8 +3,9 @@ import { getServerSession } from 'next-auth/next';
import { NextApiRequest, NextApiResponse } from 'next';
import { BorgWarehouseApiResponse } from '~/types/api/error.types';
import ApiResponse from '~/helpers/functions/apiResponse';
import { getRepoList, tokenController } from '~/helpers/functions';
import { tokenController } from '~/helpers/functions';
import { Repository } from '~/types/domain/config.types';
import { getRepoList } from '~/services';
export default async function handler(
req: NextApiRequest,

View file

@ -1,26 +1,20 @@
import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/repo/id/[slug]';
import { getServerSession } from 'next-auth/next';
import { getRepoList, tokenController } from '~/helpers/functions';
import { tokenController } from '~/helpers/functions';
import { getRepoList } from '~/services';
import { Repository } from '~/types/domain/config.types';
jest.mock('next-auth', () => {
return jest.fn(() => {
return {
auth: { session: {} },
GET: jest.fn(),
POST: jest.fn(),
};
});
});
jest.mock('next-auth/next', () => ({
getServerSession: jest.fn(),
vi.mock('next-auth/next', () => ({
getServerSession: vi.fn(),
}));
jest.mock('~/helpers/functions', () => ({
getRepoList: jest.fn(),
tokenController: jest.fn(),
vi.mock('~/helpers/functions', () => ({
tokenController: vi.fn(),
}));
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
}));
const mockRepoList: Repository[] = [
@ -58,9 +52,9 @@ const mockRepoList: Repository[] = [
describe('GET /api/repo/id/[slug]', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if method is not GET', async () => {
@ -76,8 +70,8 @@ describe('GET /api/repo/id/[slug]', () => {
});
it('should return 401 if API key is invalid', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'GET',
headers: { authorization: 'Bearer INVALID_API_KEY' },
@ -87,8 +81,8 @@ describe('GET /api/repo/id/[slug]', () => {
});
it('should return 403 if API key does not have read permissions', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue({ read: false });
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ read: false });
const { req, res } = createMocks({
method: 'GET',
headers: { authorization: 'Bearer API_KEY' },
@ -98,23 +92,23 @@ describe('GET /api/repo/id/[slug]', () => {
});
it('should return 400 if slug is missing or malformed', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
const { req, res } = createMocks({ method: 'GET', 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([]);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue([]);
const { req, res } = createMocks({ method: 'GET', query: { slug: '3' } });
await handler(req, res);
expect(res._getStatusCode()).toBe(404);
});
it('should return 200 and the repository data if found', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue(mockRepoList);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue(mockRepoList);
const { req, res } = createMocks({ method: 'GET', query: { slug: '1' } });
await handler(req, res);
expect(res._getStatusCode()).toBe(200);

View file

@ -3,7 +3,8 @@ import { getServerSession } from 'next-auth/next';
import { NextApiRequest, NextApiResponse } from 'next';
import { BorgWarehouseApiResponse } from '~/types/api/error.types';
import ApiResponse from '~/helpers/functions/apiResponse';
import { getRepoList, tokenController } from '~/helpers/functions';
import { tokenController } from '~/helpers/functions';
import { getRepoList } from '~/services';
import { Repository } from '~/types/domain/config.types';
export default async function handler(

View file

@ -1,26 +1,20 @@
import { createMocks } from 'node-mocks-http';
import handler from '~/pages/api/repo';
import { getServerSession } from 'next-auth/next';
import { getRepoList, tokenController } from '~/helpers/functions';
import { tokenController } from '~/helpers/functions';
import { getRepoList } from '~/services';
import { Repository } from '~/types/domain/config.types';
jest.mock('next-auth', () => {
return jest.fn(() => {
return {
auth: { session: {} },
GET: jest.fn(),
POST: jest.fn(),
};
});
});
jest.mock('next-auth/next', () => ({
getServerSession: jest.fn(),
vi.mock('next-auth/next', () => ({
getServerSession: vi.fn(),
}));
jest.mock('~/helpers/functions', () => ({
getRepoList: jest.fn(),
tokenController: jest.fn(),
vi.mock('~/helpers/functions', () => ({
tokenController: vi.fn(),
}));
vi.mock('~/services', () => ({
getRepoList: vi.fn(),
}));
const mockRepoList: Repository[] = [
@ -58,9 +52,9 @@ const mockRepoList: Repository[] = [
describe('GET /api/repo/id/[slug]', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
jest.spyOn(console, 'log').mockImplementation(() => {});
vi.clearAllMocks();
vi.resetModules();
vi.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return 405 if method is not GET', async () => {
@ -76,8 +70,8 @@ describe('GET /api/repo/id/[slug]', () => {
});
it('should return 401 if API key is invalid', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue(null);
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'GET',
headers: { authorization: 'Bearer INVALID_API_KEY' },
@ -87,8 +81,8 @@ describe('GET /api/repo/id/[slug]', () => {
});
it('should return 403 if API key does not have read permissions', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
(tokenController as jest.Mock).mockResolvedValue({ read: false });
vi.mocked(getServerSession).mockResolvedValue(null);
vi.mocked(tokenController).mockResolvedValue({ read: false });
const { req, res } = createMocks({
method: 'GET',
headers: { authorization: 'Bearer API_KEY' },
@ -98,8 +92,8 @@ describe('GET /api/repo/id/[slug]', () => {
});
it('should return 200 and the repoList data if found', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'USER' } });
(getRepoList as jest.Mock).mockResolvedValue(mockRepoList);
vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
vi.mocked(getRepoList).mockResolvedValue(mockRepoList);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(200);

View file

@ -1,7 +1,3 @@
## BATS tests against bash scripts
From `tests/bats`, launch `docker compose up --build`
## supertest (Jest) is used to test API endpoints
Launch `npm test`

View file

@ -1,164 +0,0 @@
import handler from '~/pages/api/account/tokenManager';
import { createMocks } from 'node-mocks-http';
import { getServerSession } from 'next-auth/next';
import { getUsersList, updateUsersList } from '~/helpers/functions';
import ApiResponse from '~/helpers/functions/apiResponse';
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', () => ({
getUsersList: jest.fn(),
updateUsersList: jest.fn(),
}));
jest.mock('~/helpers/functions/apiResponse', () => ({
unauthorized: jest.fn(),
badRequest: jest.fn(),
serverError: jest.fn(),
methodNotAllowed: jest.fn(),
success: jest.fn(),
}));
describe('Token Manager API', () => {
afterEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it('should return unauthorized if session is not found', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
},
});
await handler(req, res);
expect(ApiResponse.unauthorized).toHaveBeenCalledWith(res);
});
it('should create a new token if valid data is provided', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testUser' } });
(getUsersList as jest.Mock).mockResolvedValue([{ username: 'testUser', tokens: [] }]);
(updateUsersList as jest.Mock).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const responseData = JSON.parse(res._getData());
expect(responseData).toHaveProperty('token');
expect(updateUsersList).toHaveBeenCalled();
});
it('should return bad request if token name already exists', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testUser' } });
(getUsersList as jest.Mock).mockResolvedValue([
{ username: 'testUser', tokens: [{ name: 'testToken', permissions: {}, creation: 123 }] },
]);
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'testToken',
permissions: { create: true, read: true, update: true, delete: true },
},
});
await handler(req, res);
expect(ApiResponse.badRequest).toHaveBeenCalledWith(res, 'Token name already exists');
});
it('should return token list for GET request', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testUser' } });
(getUsersList as jest.Mock).mockResolvedValue([
{
username: 'testUser',
tokens: [
{ name: 'token1', permissions: {}, creation: 123 },
{ name: 'token2', permissions: {}, creation: 456 },
],
},
]);
const { req, res } = createMocks({ method: 'GET' });
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const responseData = JSON.parse(res._getData());
expect(responseData).toEqual([
{ name: 'token1', permissions: {}, creation: 123 },
{ name: 'token2', permissions: {}, creation: 456 },
]);
});
it('should delete a token for DELETE request', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testUser' } });
(getUsersList as jest.Mock).mockResolvedValue([
{
username: 'testUser',
tokens: [
{ name: 'token1', permissions: {}, creation: 123 },
{ name: 'token2', permissions: {}, creation: 456 },
],
},
]);
(updateUsersList as jest.Mock).mockResolvedValue(true);
const { req, res } = createMocks({
method: 'DELETE',
body: { name: 'token1' },
});
await handler(req, res);
expect(ApiResponse.success).toHaveBeenCalledWith(res, 'Token deleted');
expect(updateUsersList).toHaveBeenCalled();
});
it('should return bad request if token name is missing in DELETE request', async () => {
(getServerSession as jest.Mock).mockResolvedValue({ user: { name: 'testUser' } });
const { req, res } = createMocks({
method: 'DELETE',
body: {},
});
await handler(req, res);
expect(ApiResponse.badRequest).toHaveBeenCalledWith(res, 'Missing token name');
});
it('should return method not allowed for unsupported HTTP methods', async () => {
const { req, res } = createMocks({ method: 'PUT' });
await handler(req, res);
expect(ApiResponse.methodNotAllowed).toHaveBeenCalledWith(res);
});
});

View file

@ -16,7 +16,8 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
"jsx": "preserve",
"types": ["vitest/globals"] // Auto import vitest types
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]

19
vitest.config.ts Normal file
View file

@ -0,0 +1,19 @@
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
globals: true,
},
resolve: {
alias: {
'~': path.resolve(__dirname, './'),
},
},
});