mirror of
https://github.com/meienberger/runtipi.git
synced 2024-08-17 08:00:26 +03:00
refactor: remove usages of singletons and optimize redis connections count
This commit is contained in:
parent
fd7f9d810a
commit
779f7951d9
@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc', 'jsx-a11y', 'testing-library', 'jest-dom'],
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsx-a11y', 'testing-library', 'jest-dom'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'next/core-web-vitals',
|
||||
@ -10,7 +10,6 @@ module.exports = {
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
@ -53,8 +52,6 @@ module.exports = {
|
||||
'no-underscore-dangle': 0,
|
||||
'arrow-body-style': 0,
|
||||
'class-methods-use-this': 0,
|
||||
'jsdoc/require-returns': 0,
|
||||
'jsdoc/tag-lines': 0,
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
|
@ -126,7 +126,6 @@
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-jest-dom": "^5.0.1",
|
||||
"eslint-plugin-jsdoc": "^46.3.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
|
@ -108,7 +108,7 @@ export const startWorker = async () => {
|
||||
|
||||
return { success, stdout: message };
|
||||
},
|
||||
{ connection: { host: '127.0.0.1', port: 6379, password: getEnv().redisPassword, connectTimeout: 60000 } },
|
||||
{ connection: { host: '127.0.0.1', port: 6379, password: getEnv().redisPassword, connectTimeout: 60000 }, removeOnComplete: { count: 200 }, removeOnFail: { count: 500 } },
|
||||
);
|
||||
|
||||
worker.on('ready', () => {
|
||||
@ -124,6 +124,6 @@ export const startWorker = async () => {
|
||||
});
|
||||
|
||||
worker.on('error', async (e) => {
|
||||
fileLogger.error(`Worker error: ${e}`);
|
||||
fileLogger.debug(`Worker error: ${e}`);
|
||||
});
|
||||
};
|
||||
|
@ -8,12 +8,9 @@ import { fileLogger } from '../logger/file-logger';
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const composeUp = async (args: string[]) => {
|
||||
fileLogger.info(`Running docker compose with args ${args.join(' ')}`);
|
||||
const { stdout, stderr } = await execAsync(`docker compose ${args.join(' ')}`);
|
||||
|
||||
if (stderr) {
|
||||
fileLogger.error(stderr);
|
||||
}
|
||||
|
||||
return { stdout, stderr };
|
||||
};
|
||||
|
||||
|
@ -34,6 +34,10 @@ export const newLogger = (id: string, logsFolder: string) => {
|
||||
);
|
||||
exceptionHandlers = [new transports.File({ filename: path.join(logsFolder, 'error.log') })];
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
tr.push(new transports.Console({ level: 'debug' }));
|
||||
}
|
||||
|
||||
return createLogger({
|
||||
level: 'debug',
|
||||
format: combine(
|
||||
|
@ -281,9 +281,6 @@ importers:
|
||||
eslint-plugin-jest-dom:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(@testing-library/dom@9.3.1)(eslint@8.43.0)
|
||||
eslint-plugin-jsdoc:
|
||||
specifier: ^46.3.0
|
||||
version: 46.3.0(eslint@8.43.0)
|
||||
eslint-plugin-jsx-a11y:
|
||||
specifier: ^6.6.1
|
||||
version: 6.7.1(eslint@8.43.0)
|
||||
@ -968,15 +965,6 @@ packages:
|
||||
unescape-js: 1.1.4
|
||||
dev: true
|
||||
|
||||
/@es-joy/jsdoccomment@0.39.4:
|
||||
resolution: {integrity: sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==}
|
||||
engines: {node: '>=16'}
|
||||
dependencies:
|
||||
comment-parser: 1.3.1
|
||||
esquery: 1.5.0
|
||||
jsdoc-type-pratt-parser: 4.0.0
|
||||
dev: true
|
||||
|
||||
/@esbuild-kit/cjs-loader@2.4.2:
|
||||
resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==}
|
||||
dependencies:
|
||||
@ -4103,11 +4091,6 @@ packages:
|
||||
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
|
||||
dev: false
|
||||
|
||||
/are-docs-informative@0.0.2:
|
||||
resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
|
||||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/are-we-there-yet@2.0.0:
|
||||
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4472,11 +4455,6 @@ packages:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
/builtin-modules@3.3.0:
|
||||
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/bullmq@4.5.0:
|
||||
resolution: {integrity: sha512-pG0teKyP45jtyXIH0MBglxDfy1NCsdXf95YGzJEZTycl9eE1oXHJlr8/RlhVfIA+XCYEnEQD36u0OINGrxaqeQ==}
|
||||
dependencies:
|
||||
@ -4792,11 +4770,6 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/comment-parser@1.3.1:
|
||||
resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
dev: true
|
||||
|
||||
/component-emitter@1.3.0:
|
||||
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
|
||||
dev: true
|
||||
@ -5734,26 +5707,6 @@ packages:
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-jsdoc@46.3.0(eslint@8.43.0):
|
||||
resolution: {integrity: sha512-nfSvsR8YJRZyKrWwcXPSQyQC8jllfdEjcRhTXFr7RxfB5Wyl7AxrfjCUz72WwalkXMF4u+R6F/oDoW46ah69HQ==}
|
||||
engines: {node: '>=16'}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
'@es-joy/jsdoccomment': 0.39.4
|
||||
are-docs-informative: 0.0.2
|
||||
comment-parser: 1.3.1
|
||||
debug: 4.3.4
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint: 8.43.0
|
||||
esquery: 1.5.0
|
||||
is-builtin-module: 3.2.1
|
||||
semver: 7.5.3
|
||||
spdx-expression-parse: 3.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-jsx-a11y@6.7.1(eslint@8.43.0):
|
||||
resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==}
|
||||
engines: {node: '>=4.0'}
|
||||
@ -6816,13 +6769,6 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/is-builtin-module@3.2.1:
|
||||
resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
builtin-modules: 3.3.0
|
||||
dev: true
|
||||
|
||||
/is-callable@1.2.7:
|
||||
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -7604,11 +7550,6 @@ packages:
|
||||
argparse: 2.0.1
|
||||
dev: true
|
||||
|
||||
/jsdoc-type-pratt-parser@4.0.0:
|
||||
resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: true
|
||||
|
||||
/jsdom@20.0.3:
|
||||
resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==}
|
||||
engines: {node: '>=14'}
|
||||
@ -10139,21 +10080,6 @@ packages:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
dev: false
|
||||
|
||||
/spdx-exceptions@2.3.0:
|
||||
resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
|
||||
dev: true
|
||||
|
||||
/spdx-expression-parse@3.0.1:
|
||||
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
|
||||
dependencies:
|
||||
spdx-exceptions: 2.3.0
|
||||
spdx-license-ids: 3.0.12
|
||||
dev: true
|
||||
|
||||
/spdx-license-ids@3.0.12:
|
||||
resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==}
|
||||
dev: true
|
||||
|
||||
/split2@4.1.0:
|
||||
resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
@ -6,7 +6,7 @@ import { getAuthedPageProps, getMessagesPageProps } from '../page-helpers';
|
||||
import englishMessages from '../../messages/en.json';
|
||||
import frenchMessages from '../../messages/fr-FR.json';
|
||||
|
||||
const cache = new TipiCache();
|
||||
const cache = new TipiCache('page-helpers.test.ts');
|
||||
|
||||
afterAll(async () => {
|
||||
await cache.close();
|
||||
|
@ -5,7 +5,7 @@ import { getCookie } from 'cookies-next';
|
||||
import { TipiCache } from '@/server/core/TipiCache';
|
||||
|
||||
export const getAuthedPageProps: GetServerSideProps = async (ctx) => {
|
||||
const cache = new TipiCache();
|
||||
const cache = new TipiCache('getAuthedPageProps');
|
||||
const sessionId = ctx.req.headers['x-session-id'];
|
||||
const userId = await cache.get(`session:${sessionId}`);
|
||||
await cache.close();
|
||||
|
@ -13,15 +13,13 @@ import fs from 'fs-extra';
|
||||
* @param {NextApiResponse} res - The response
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const cache = new TipiCache();
|
||||
|
||||
const authService = new AuthQueries(db);
|
||||
|
||||
const sessionId = req.headers['x-session-id'];
|
||||
const cache = new TipiCache('certificate');
|
||||
const userId = await cache.get(`session:${sessionId}`);
|
||||
const user = await authService.getUserById(Number(userId));
|
||||
|
||||
await cache.close();
|
||||
const user = await authService.getUserById(Number(userId));
|
||||
|
||||
if (user?.operator) {
|
||||
const filePath = `${getConfig().rootFolder}/traefik/tls/cert.pem`;
|
||||
|
@ -11,7 +11,7 @@ export const generateSessionId = (prefix: string) => {
|
||||
};
|
||||
|
||||
export const setSession = async (sessionId: string, userId: string, req: NextApiRequest, res: NextApiResponse) => {
|
||||
const cache = new TipiCache();
|
||||
const cache = new TipiCache('setSession');
|
||||
|
||||
setCookie(COOKIE_NAME, sessionId, { req, res, maxAge: COOKIE_MAX_AGE, httpOnly: true, secure: false, sameSite: false });
|
||||
|
||||
|
@ -27,13 +27,12 @@ const createContextInner = async (opts: CreateContextOptions) => ({
|
||||
* @param {CreateNextContextOptions} opts - options
|
||||
*/
|
||||
export const createContext = async (opts: CreateNextContextOptions) => {
|
||||
const cache = new TipiCache();
|
||||
const { req, res } = opts;
|
||||
|
||||
const sessionId = req.headers['x-session-id'] as string;
|
||||
|
||||
const cache = new TipiCache('createContext');
|
||||
const userId = await cache.get(`session:${sessionId}`);
|
||||
|
||||
await cache.close();
|
||||
|
||||
return createContextInner({
|
||||
|
@ -1,25 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
import { EventDispatcher } from '.';
|
||||
|
||||
const WATCH_FILE = '/runtipi/state/events';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.promises.mkdir('/runtipi/state', { recursive: true });
|
||||
await fs.promises.mkdir('/app/logs', { recursive: true });
|
||||
await fs.promises.writeFile(WATCH_FILE, '');
|
||||
await fs.promises.writeFile('/app/logs/123.log', 'test');
|
||||
|
||||
EventDispatcher.clear();
|
||||
});
|
||||
|
||||
describe('EventDispatcher - dispatchEvent', () => {
|
||||
it('should dispatch an event in the queue', () => {});
|
||||
});
|
||||
|
||||
describe('EventDispatcher - dispatchEventAsync', () => {
|
||||
it('Should dispatch an event and wait for it to finish', async () => {});
|
||||
|
||||
it('Should dispatch an event and wait for it to finish with error', async () => {});
|
||||
});
|
@ -1,24 +1,25 @@
|
||||
/* eslint-disable vars-on-top */
|
||||
import { Queue, QueueEvents } from 'bullmq';
|
||||
import { eventResultSchema, eventSchema, SystemEvent } from '@runtipi/shared';
|
||||
import { getConfig } from '@/server/core/TipiConfig';
|
||||
import { Logger } from '../Logger';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var EventDispatcher: EventDispatcher | undefined;
|
||||
}
|
||||
|
||||
class EventDispatcher {
|
||||
private static instance: EventDispatcher | null;
|
||||
|
||||
export class EventDispatcher {
|
||||
private queue;
|
||||
|
||||
private queueEvents;
|
||||
|
||||
constructor() {
|
||||
private instanceId: string;
|
||||
|
||||
private timeout: NodeJS.Timeout;
|
||||
|
||||
constructor(reference: string) {
|
||||
this.queue = new Queue('events', { connection: { host: getConfig().REDIS_HOST, port: 6379, password: getConfig().redisPassword } });
|
||||
this.queueEvents = new QueueEvents('events', { connection: { host: getConfig().REDIS_HOST, port: 6379, password: getConfig().redisPassword } });
|
||||
this.instanceId = `${getConfig().version}-${Date.now()}`;
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
Logger.debug(`Redis connection is running for more than 30 seconds. Consider closing it. reference: ${reference}`);
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
public async cleanRepeatableJobs() {
|
||||
@ -33,13 +34,6 @@ class EventDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): EventDispatcher {
|
||||
if (!EventDispatcher.instance) {
|
||||
EventDispatcher.instance = new EventDispatcher();
|
||||
}
|
||||
return EventDispatcher.instance;
|
||||
}
|
||||
|
||||
private generateJobId(event: SystemEvent) {
|
||||
return [event.type, Date.now()].join('_');
|
||||
}
|
||||
@ -62,13 +56,11 @@ class EventDispatcher {
|
||||
* @returns {Promise<{ success: boolean; stdout?: string }>} - Promise that resolves when the event is done
|
||||
*/
|
||||
public async dispatchEventAsync(event: SystemEvent): Promise<{ success: boolean; stdout?: string }> {
|
||||
Logger.info(`Dispatching event ${JSON.stringify(event)}`);
|
||||
Logger.info(`Dispatching event ${JSON.stringify(event)}. Instance: ${this.instanceId}`);
|
||||
try {
|
||||
const job = await this.dispatchEvent(event);
|
||||
const result = await job.waitUntilFinished(this.queueEvents, 1000 * 60 * 5);
|
||||
|
||||
// const isFailed = await job.isFailed();
|
||||
|
||||
return eventResultSchema.parse(result);
|
||||
} catch (e) {
|
||||
Logger.error(`Event failed: ${e}`);
|
||||
@ -91,8 +83,10 @@ class EventDispatcher {
|
||||
|
||||
this.queue.add(jobid, eventSchema.parse(event), { repeat: { pattern: cronExpression } });
|
||||
}
|
||||
|
||||
public async close() {
|
||||
await this.queue.close();
|
||||
await this.queueEvents.close();
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export const EventDispatcherInstance = global.EventDispatcher || EventDispatcher.getInstance();
|
||||
|
||||
global.EventDispatcher = EventDispatcherInstance;
|
||||
|
@ -1 +1 @@
|
||||
export { EventDispatcherInstance as EventDispatcher } from './EventDispatcher';
|
||||
export { EventDispatcher } from './EventDispatcher';
|
||||
|
@ -5,11 +5,11 @@ import { getConfig } from '../TipiConfig';
|
||||
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
|
||||
|
||||
export class TipiCache {
|
||||
private static instance: TipiCache;
|
||||
|
||||
private client: RedisClientType;
|
||||
|
||||
constructor() {
|
||||
private timeout: NodeJS.Timeout;
|
||||
|
||||
constructor(reference: string) {
|
||||
const client = createClient({
|
||||
url: `redis://${getConfig().REDIS_HOST}:6379`,
|
||||
password: getConfig().redisPassword,
|
||||
@ -20,14 +20,10 @@ export class TipiCache {
|
||||
});
|
||||
|
||||
this.client = client as RedisClientType;
|
||||
}
|
||||
|
||||
public static getInstance(): TipiCache {
|
||||
if (!TipiCache.instance) {
|
||||
TipiCache.instance = new TipiCache();
|
||||
}
|
||||
|
||||
return TipiCache.instance;
|
||||
this.timeout = setTimeout(() => {
|
||||
Logger.debug(`Redis connection is running for more than 30 seconds. Consider closing it. reference: ${reference}`);
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
private async getClient(): Promise<RedisClientType> {
|
||||
@ -70,6 +66,7 @@ export class TipiCache {
|
||||
}
|
||||
|
||||
public async close() {
|
||||
clearTimeout(this.timeout);
|
||||
return this.client.quit();
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import waitForExpect from 'wait-for-expect';
|
||||
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { castAppConfig } from '@/client/modules/Apps/helpers/castAppConfig';
|
||||
import { waitUntilFinishedMock } from '@/tests/server/jest.setup';
|
||||
import { AppServiceClass } from './apps.service';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
import { getAllApps, getAppById, updateApp, createAppConfig, insertApp } from '../../tests/apps.factory';
|
||||
@ -11,6 +12,7 @@ import { setConfig } from '../../core/TipiConfig';
|
||||
let db: TestDatabase;
|
||||
let AppsService: AppServiceClass;
|
||||
const TEST_SUITE = 'appsservice';
|
||||
const dispatcher = new EventDispatcher(TEST_SUITE);
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await createDatabase(TEST_SUITE);
|
||||
@ -19,11 +21,12 @@ beforeAll(async () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await clearDatabase(db);
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
|
||||
dispatcher.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeDatabase(db);
|
||||
await dispatcher.close();
|
||||
});
|
||||
|
||||
describe('Install app', () => {
|
||||
@ -45,26 +48,22 @@ describe('Install app', () => {
|
||||
it('Should start app if already installed', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig();
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
|
||||
// act
|
||||
await AppsService.installApp(appConfig.id, {});
|
||||
await AppsService.installApp(appConfig.id, {});
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
|
||||
// assert
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
expect(spy.mock.calls[0]).toEqual([{ appid: appConfig.id, command: 'install', form: {}, type: 'app' }]);
|
||||
expect(spy.mock.calls[1]).toEqual([{ appid: appConfig.id, command: 'start', form: {}, type: 'app' }]);
|
||||
|
||||
spy.mockRestore();
|
||||
expect(app?.status).toBe('running');
|
||||
});
|
||||
|
||||
it('Should delete app if install script fails', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig();
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||
|
||||
// act
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
await expect(AppsService.installApp(appConfig.id, {})).rejects.toThrow('server-messages.errors.app-failed-to-install');
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
|
||||
@ -177,16 +176,15 @@ describe('Uninstall app', () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'running' }, appConfig, db);
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
|
||||
// act
|
||||
await AppsService.uninstallApp(appConfig.id);
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: true, stdout: 'test' });
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
await expect(AppsService.uninstallApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-uninstall');
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
|
||||
// assert
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
expect(spy.mock.calls[0]).toEqual([{ appid: appConfig.id, command: 'stop', form: {}, type: 'app' }]);
|
||||
expect(spy.mock.calls[1]).toEqual([{ appid: appConfig.id, command: 'uninstall', form: {}, type: 'app' }]);
|
||||
spy.mockRestore();
|
||||
expect(app?.status).toBe('stopped');
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
@ -198,7 +196,7 @@ describe('Uninstall app', () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'running' }, appConfig, db);
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
await updateApp(appConfig.id, { status: 'updating' }, db);
|
||||
|
||||
// act & assert
|
||||
@ -209,18 +207,17 @@ describe('Uninstall app', () => {
|
||||
});
|
||||
|
||||
describe('Start app', () => {
|
||||
it('Should correctly dispatch start event', async () => {
|
||||
it('Should correctly start app', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({}, appConfig, db);
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
await insertApp({ status: 'stopped' }, appConfig, db);
|
||||
|
||||
// act
|
||||
await AppsService.startApp(appConfig.id);
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
|
||||
// assert
|
||||
expect(spy.mock.lastCall).toEqual([{ appid: appConfig.id, command: 'start', form: {}, type: 'app' }]);
|
||||
spy.mockRestore();
|
||||
expect(app?.status).toBe('running');
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
@ -231,23 +228,21 @@ describe('Start app', () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'running' }, appConfig, db);
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
|
||||
// act
|
||||
await AppsService.startApp(appConfig.id);
|
||||
expect(spy.mock.calls.length).toBe(1);
|
||||
await AppsService.startApp(appConfig.id);
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
|
||||
// assert
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
spy.mockRestore();
|
||||
expect(app?.status).toBe('running');
|
||||
});
|
||||
|
||||
it('Should throw if start script fails', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'stopped' }, appConfig, db);
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
|
||||
// act & assert
|
||||
await expect(AppsService.startApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-start');
|
||||
@ -257,17 +252,17 @@ describe('Start app', () => {
|
||||
});
|
||||
|
||||
describe('Stop app', () => {
|
||||
it('Should correctly dispatch stop event', async () => {
|
||||
it('Should correctly stop app', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'running' }, appConfig, db);
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
|
||||
// act
|
||||
await AppsService.stopApp(appConfig.id);
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
|
||||
// assert
|
||||
expect(spy.mock.lastCall).toEqual([{ appid: appConfig.id, command: 'stop', form: {}, type: 'app' }]);
|
||||
expect(app?.status).toBe('stopped');
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
@ -278,7 +273,7 @@ describe('Stop app', () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'running' }, appConfig, db);
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||
|
||||
// act & assert
|
||||
await expect(AppsService.stopApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-stop');
|
||||
@ -488,7 +483,7 @@ describe('Update app', () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({}, appConfig, db);
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||
waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||
|
||||
// act & assert
|
||||
await expect(AppsService.updateApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-update');
|
||||
@ -537,29 +532,11 @@ describe('installedApps', () => {
|
||||
});
|
||||
|
||||
describe('startAllApps', () => {
|
||||
it('should start all apps with status RUNNING', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
const appConfig2 = createAppConfig({});
|
||||
const appConfig3 = createAppConfig({});
|
||||
await insertApp({ status: 'running' }, appConfig, db);
|
||||
await insertApp({ status: 'running' }, appConfig2, db);
|
||||
await insertApp({ status: 'stopped' }, appConfig3, db);
|
||||
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
|
||||
// act
|
||||
await AppsService.startAllApps();
|
||||
|
||||
// assert
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should put status to STOPPED if start script fails', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'stopped' }, appConfig, db);
|
||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||
const spy = jest.spyOn(dispatcher, 'dispatchEventAsync');
|
||||
spy.mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||
|
||||
// act
|
||||
|
@ -5,9 +5,9 @@ import { TranslatedError } from '@/server/utils/errors';
|
||||
import { Database } from '@/server/db';
|
||||
import { castAppConfig } from '@/client/modules/Apps/helpers/castAppConfig';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher';
|
||||
import { checkAppRequirements, getAvailableApps, getAppInfo, getUpdateInfo } from './apps.helpers';
|
||||
import { getConfig } from '../../core/TipiConfig';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
import { Logger } from '../../core/Logger';
|
||||
import { notEmpty } from '../../common/typescript.helpers';
|
||||
|
||||
@ -42,12 +42,14 @@ export class AppServiceClass {
|
||||
// Update all apps with status different than running or stopped to stopped
|
||||
await this.queries.updateAppsByStatusNotIn(['running', 'stopped', 'missing'], { status: 'stopped' });
|
||||
|
||||
const eventDispatcher = new EventDispatcher('startAllApps');
|
||||
|
||||
await Promise.all(
|
||||
apps.map(async (app) => {
|
||||
try {
|
||||
await this.queries.updateApp(app.id, { status: 'starting' });
|
||||
|
||||
EventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) }).then(({ success }) => {
|
||||
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) }).then(({ success }) => {
|
||||
if (success) {
|
||||
this.queries.updateApp(app.id, { status: 'running' });
|
||||
} else {
|
||||
@ -60,6 +62,8 @@ export class AppServiceClass {
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await eventDispatcher.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,7 +80,9 @@ export class AppServiceClass {
|
||||
}
|
||||
|
||||
await this.queries.updateApp(appName, { status: 'starting' });
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: appName, form: castAppConfig(app.config) });
|
||||
const eventDispatcher = new EventDispatcher('startApp');
|
||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: appName, form: castAppConfig(app.config) });
|
||||
await eventDispatcher.close();
|
||||
|
||||
if (success) {
|
||||
await this.queries.updateApp(appName, { status: 'running' });
|
||||
@ -145,7 +151,9 @@ export class AppServiceClass {
|
||||
await this.queries.createApp({ id, status: 'installing', config: form, version: appInfo.tipi_version, exposed: exposed || false, domain: domain || null });
|
||||
|
||||
// Run script
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form });
|
||||
const eventDispatcher = new EventDispatcher('installApp');
|
||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form });
|
||||
await eventDispatcher.close();
|
||||
|
||||
if (!success) {
|
||||
await this.queries.deleteApp(id);
|
||||
@ -213,7 +221,9 @@ export class AppServiceClass {
|
||||
}
|
||||
}
|
||||
|
||||
const { success } = await EventDispatcher.dispatchEventAsync({ type: 'app', command: 'generate_env', appid: id, form });
|
||||
const eventDispatcher = new EventDispatcher('updateAppConfig');
|
||||
const { success } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'generate_env', appid: id, form });
|
||||
await eventDispatcher.close();
|
||||
|
||||
if (success) {
|
||||
const updatedApp = await this.queries.updateApp(id, { exposed: exposed || false, domain: domain || null, config: form });
|
||||
@ -239,7 +249,9 @@ export class AppServiceClass {
|
||||
// Run script
|
||||
await this.queries.updateApp(id, { status: 'stopping' });
|
||||
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) });
|
||||
const eventDispatcher = new EventDispatcher('stopApp');
|
||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) });
|
||||
await eventDispatcher.close();
|
||||
|
||||
if (success) {
|
||||
await this.queries.updateApp(id, { status: 'stopped' });
|
||||
@ -271,7 +283,9 @@ export class AppServiceClass {
|
||||
|
||||
await this.queries.updateApp(id, { status: 'uninstalling' });
|
||||
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) });
|
||||
const eventDispatcher = new EventDispatcher('uninstallApp');
|
||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) });
|
||||
await eventDispatcher.close();
|
||||
|
||||
if (!success) {
|
||||
await this.queries.updateApp(id, { status: 'stopped' });
|
||||
@ -320,7 +334,9 @@ export class AppServiceClass {
|
||||
|
||||
await this.queries.updateApp(id, { status: 'updating' });
|
||||
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync({ type: 'app', command: 'update', appid: id, form: castAppConfig(app.config) });
|
||||
const eventDispatcher = new EventDispatcher('updateApp');
|
||||
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'update', appid: id, form: castAppConfig(app.config) });
|
||||
await eventDispatcher.close();
|
||||
|
||||
if (success) {
|
||||
const appInfo = getAppInfo(app.id, app.status);
|
||||
|
@ -17,7 +17,7 @@ let AuthService: AuthServiceClass;
|
||||
let database: TestDatabase;
|
||||
const TEST_SUITE = 'authservice';
|
||||
|
||||
const cache = new TipiCache();
|
||||
const cache = new TipiCache('auth.service.test.ts');
|
||||
|
||||
beforeAll(async () => {
|
||||
setConfig('jwtSecret', 'test');
|
||||
|
@ -22,11 +22,8 @@ type UsernamePasswordInput = {
|
||||
export class AuthServiceClass {
|
||||
private queries;
|
||||
|
||||
private cache;
|
||||
|
||||
constructor(p: Database) {
|
||||
this.queries = new AuthQueries(p);
|
||||
this.cache = new TipiCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,8 +48,10 @@ export class AuthServiceClass {
|
||||
}
|
||||
|
||||
if (user.totpEnabled) {
|
||||
const cache = new TipiCache('login');
|
||||
const totpSessionId = generateSessionId('otp');
|
||||
await this.cache.set(totpSessionId, user.id.toString());
|
||||
await cache.set(totpSessionId, user.id.toString());
|
||||
await cache.close();
|
||||
return { totpSessionId };
|
||||
}
|
||||
|
||||
@ -73,7 +72,9 @@ export class AuthServiceClass {
|
||||
*/
|
||||
public verifyTotp = async (params: { totpSessionId: string; totpCode: string }, req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { totpSessionId, totpCode } = params;
|
||||
const userId = await this.cache.get(totpSessionId);
|
||||
const cache = new TipiCache('verifyTotp');
|
||||
const userId = await cache.get(totpSessionId);
|
||||
await cache.close();
|
||||
|
||||
if (!userId) {
|
||||
throw new TranslatedError('server-messages.errors.totp-session-not-found');
|
||||
@ -265,7 +266,9 @@ export class AuthServiceClass {
|
||||
* @returns {Promise<boolean>} - Returns true if the session token is removed successfully
|
||||
*/
|
||||
public logout = async (sessionId: string): Promise<boolean> => {
|
||||
await this.cache.del(`session:${sessionId}`);
|
||||
const cache = new TipiCache('logout');
|
||||
await cache.del(`session:${sessionId}`);
|
||||
await cache.close();
|
||||
|
||||
return true;
|
||||
};
|
||||
@ -343,13 +346,13 @@ export class AuthServiceClass {
|
||||
*
|
||||
* @param {number} userId - The user ID
|
||||
*/
|
||||
private destroyAllSessionsByUserId = async (userId: number) => {
|
||||
const sessions = await this.cache.getByPrefix(`session:${userId}:`);
|
||||
private destroyAllSessionsByUserId = async (userId: number, cache: TipiCache) => {
|
||||
const sessions = await cache.getByPrefix(`session:${userId}:`);
|
||||
|
||||
await Promise.all(
|
||||
sessions.map(async (session) => {
|
||||
await this.cache.del(session.key);
|
||||
if (session.val) await this.cache.del(session.val);
|
||||
await cache.del(session.key);
|
||||
if (session.val) await cache.del(session.val);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@ -379,7 +382,9 @@ export class AuthServiceClass {
|
||||
|
||||
const hash = await argon2.hash(newPassword);
|
||||
await this.queries.updateUser(user.id, { password: hash });
|
||||
await this.destroyAllSessionsByUserId(user.id);
|
||||
const cache = new TipiCache('changePassword');
|
||||
await this.destroyAllSessionsByUserId(user.id, cache);
|
||||
await cache.close();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -3,25 +3,29 @@ import { setupServer } from 'msw/node';
|
||||
import fs from 'fs-extra';
|
||||
import semver from 'semver';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
import { setConfig } from '../../core/TipiConfig';
|
||||
import { TipiCache } from '../../core/TipiCache';
|
||||
import { SystemServiceClass } from '.';
|
||||
|
||||
jest.mock('redis');
|
||||
|
||||
const SystemService = new SystemServiceClass();
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
const cache = new TipiCache();
|
||||
const cache = new TipiCache('system.service.test');
|
||||
|
||||
afterAll(async () => {
|
||||
server.close();
|
||||
await cache.close();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await setConfig('demoMode', false);
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
await cache.del('latestVersion');
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Test: systemInfo', () => {
|
||||
@ -69,21 +73,6 @@ describe('Test: systemInfo', () => {
|
||||
});
|
||||
|
||||
describe('Test: getVersion', () => {
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
server.resetHandlers();
|
||||
await cache.del('latestVersion');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
server.close();
|
||||
jest.restoreAllMocks();
|
||||
await cache.close();
|
||||
});
|
||||
|
||||
it('It should return version with body', async () => {
|
||||
// Arrange
|
||||
const body = faker.lorem.words(10);
|
||||
@ -142,9 +131,6 @@ describe('Test: getVersion', () => {
|
||||
|
||||
describe('Test: restart', () => {
|
||||
it('Should return true', async () => {
|
||||
// Arrange
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||
|
||||
// Act
|
||||
const restart = await SystemService.restart();
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import axios from 'redaxios';
|
||||
import { TranslatedError } from '@/server/utils/errors';
|
||||
import { EventDispatcher } from '@/server/core/EventDispatcher';
|
||||
import { TipiCache } from '@/server/core/TipiCache';
|
||||
import { readJsonFile } from '../../common/fs.helpers';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
import { Logger } from '../../core/Logger';
|
||||
import { TipiCache } from '../../core/TipiCache';
|
||||
import * as TipiConfig from '../../core/TipiConfig';
|
||||
|
||||
const SYSTEM_STATUS = ['UPDATING', 'RESTARTING', 'RUNNING'] as const;
|
||||
@ -27,21 +27,13 @@ const systemInfoSchema = z.object({
|
||||
});
|
||||
|
||||
export class SystemServiceClass {
|
||||
private cache;
|
||||
|
||||
private dispatcher;
|
||||
|
||||
constructor() {
|
||||
this.cache = new TipiCache();
|
||||
this.dispatcher = EventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current and latest version of Tipi
|
||||
*
|
||||
* @returns {Promise<{ current: string; latest: string }>} The current and latest version
|
||||
*/
|
||||
public getVersion = async () => {
|
||||
const cache = new TipiCache('getVersion');
|
||||
try {
|
||||
const { seePreReleaseVersions } = TipiConfig.getConfig();
|
||||
|
||||
@ -51,8 +43,8 @@ export class SystemServiceClass {
|
||||
return { current: TipiConfig.getConfig().version, latest: data[0]?.tag_name, body: data[0]?.body };
|
||||
}
|
||||
|
||||
let version = await this.cache.get('latestVersion');
|
||||
let body = await this.cache.get('latestVersionBody');
|
||||
let version = await cache.get('latestVersion');
|
||||
let body = await cache.get('latestVersionBody');
|
||||
|
||||
if (!version) {
|
||||
const { data } = await axios.get<{ tag_name: string; body: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest');
|
||||
@ -60,14 +52,16 @@ export class SystemServiceClass {
|
||||
version = data.tag_name;
|
||||
body = data.body;
|
||||
|
||||
await this.cache.set('latestVersion', version || '', 60 * 60);
|
||||
await this.cache.set('latestVersionBody', body || '', 60 * 60);
|
||||
await cache.set('latestVersion', version || '', 60 * 60);
|
||||
await cache.set('latestVersionBody', body || '', 60 * 60);
|
||||
}
|
||||
|
||||
return { current: TipiConfig.getConfig().version, latest: version, body };
|
||||
} catch (e) {
|
||||
Logger.error(e);
|
||||
return { current: TipiConfig.getConfig().version, latest: undefined };
|
||||
} finally {
|
||||
await cache.close();
|
||||
}
|
||||
};
|
||||
|
||||
@ -93,7 +87,9 @@ export class SystemServiceClass {
|
||||
}
|
||||
|
||||
TipiConfig.setConfig('status', 'RESTARTING');
|
||||
this.dispatcher.dispatchEvent({ type: 'system', command: 'restart' });
|
||||
const dispatcher = new EventDispatcher('restart');
|
||||
dispatcher.dispatchEvent({ type: 'system', command: 'restart' });
|
||||
await dispatcher.close();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import fs from 'fs-extra';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { EventDispatcher } from '../../src/server/core/EventDispatcher';
|
||||
import { Job } from 'bullmq';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
// Mock global location
|
||||
@ -13,15 +13,24 @@ jest.mock('vitest', () => ({
|
||||
vi: jest,
|
||||
}));
|
||||
|
||||
export const waitUntilFinishedMock = jest.fn().mockResolvedValue({ success: true, stdout: '' });
|
||||
jest.mock('bullmq', () => ({
|
||||
Queue: jest.fn().mockImplementation(() => ({
|
||||
add: jest.fn(),
|
||||
add: jest.fn(() => {
|
||||
const job: Job = fromPartial({
|
||||
waitUntilFinished: waitUntilFinishedMock,
|
||||
});
|
||||
|
||||
return Promise.resolve(job);
|
||||
}),
|
||||
getRepeatableJobs: jest.fn().mockResolvedValue([]),
|
||||
removeRepeatableByKey: jest.fn(),
|
||||
obliterate: jest.fn(),
|
||||
close: jest.fn(),
|
||||
})),
|
||||
QueueEvents: jest.fn().mockImplementation(() => ({
|
||||
on: jest.fn(),
|
||||
close: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
@ -41,6 +50,7 @@ jest.mock('../../src/server/core/Logger', () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@ -49,7 +59,3 @@ jest.mock('next/config', () => () => ({
|
||||
...process.env,
|
||||
},
|
||||
}));
|
||||
|
||||
afterAll(() => {
|
||||
EventDispatcher.clear();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user