refactor: make event dispatcher a singleton and update app accordingly
This commit is contained in:
parent
3b0fc56563
commit
a024b03508
17 changed files with 208 additions and 181 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,6 +1,8 @@
|
||||||
*.swo
|
*.swo
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
logs
|
logs
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
.env*
|
.env*
|
||||||
|
|
|
@ -19,4 +19,7 @@ module.exports = {
|
||||||
'no-unused-vars': [1, { argsIgnorePattern: '^_' }],
|
'no-unused-vars': [1, { argsIgnorePattern: '^_' }],
|
||||||
'@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_' }],
|
||||||
},
|
},
|
||||||
|
globals: {
|
||||||
|
NodeJS: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
2
packages/system-api/.gitignore
vendored
2
packages/system-api/.gitignore
vendored
|
@ -1,6 +1,8 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
coverage/
|
coverage/
|
||||||
logs/
|
logs/
|
||||||
|
|
|
@ -11,6 +11,7 @@ const fs: {
|
||||||
copyFileSync: typeof copyFileSync;
|
copyFileSync: typeof copyFileSync;
|
||||||
copySync: typeof copyFileSync;
|
copySync: typeof copyFileSync;
|
||||||
createFileSync: typeof createFileSync;
|
createFileSync: typeof createFileSync;
|
||||||
|
unlinkSync: typeof unlinkSync;
|
||||||
} = jest.genMockFromModule('fs-extra');
|
} = jest.genMockFromModule('fs-extra');
|
||||||
|
|
||||||
let mockFiles = Object.create(null);
|
let mockFiles = Object.create(null);
|
||||||
|
@ -97,6 +98,16 @@ const resetAllMocks = () => {
|
||||||
mockFiles = Object.create(null);
|
mockFiles = Object.create(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unlinkSync = (p: string) => {
|
||||||
|
if (mockFiles[p] instanceof Array) {
|
||||||
|
mockFiles[p].forEach((file: string) => {
|
||||||
|
delete mockFiles[path.join(p, file)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
delete mockFiles[p];
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.unlinkSync = unlinkSync;
|
||||||
fs.readdirSync = readdirSync;
|
fs.readdirSync = readdirSync;
|
||||||
fs.existsSync = existsSync;
|
fs.existsSync = existsSync;
|
||||||
fs.readFileSync = readFileSync;
|
fs.readFileSync = readFileSync;
|
||||||
|
|
|
@ -25,12 +25,24 @@ const WATCH_FILE = '/runtipi/state/events';
|
||||||
// File state example:
|
// File state example:
|
||||||
// restart 1631231231231 running "arg1 arg2"
|
// restart 1631231231231 running "arg1 arg2"
|
||||||
class EventDispatcher {
|
class EventDispatcher {
|
||||||
|
private static instance: EventDispatcher | null;
|
||||||
|
|
||||||
private queue: SystemEvent[] = [];
|
private queue: SystemEvent[] = [];
|
||||||
|
|
||||||
private lock: SystemEvent | null = null;
|
private lock: SystemEvent | null = null;
|
||||||
|
|
||||||
|
private interval: NodeJS.Timer;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.pollQueue();
|
const timer = this.pollQueue();
|
||||||
|
this.interval = timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): EventDispatcher {
|
||||||
|
if (!EventDispatcher.instance) {
|
||||||
|
EventDispatcher.instance = new EventDispatcher();
|
||||||
|
}
|
||||||
|
return EventDispatcher.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,7 +50,7 @@ class EventDispatcher {
|
||||||
* @returns - Random id
|
* @returns - Random id
|
||||||
*/
|
*/
|
||||||
private generateId() {
|
private generateId() {
|
||||||
return Math.random().toString(36).substr(2, 9);
|
return Math.random().toString(36).substring(2, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,7 +77,7 @@ class EventDispatcher {
|
||||||
*/
|
*/
|
||||||
private pollQueue() {
|
private pollQueue() {
|
||||||
logger.info('EventDispatcher: Polling queue...');
|
logger.info('EventDispatcher: Polling queue...');
|
||||||
setInterval(() => {
|
return setInterval(() => {
|
||||||
this.runEvent();
|
this.runEvent();
|
||||||
this.collectLockStatusAndClean();
|
this.collectLockStatusAndClean();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
@ -89,7 +101,6 @@ class EventDispatcher {
|
||||||
// Write event to state file
|
// Write event to state file
|
||||||
const args = event.args.join(' ');
|
const args = event.args.join(' ');
|
||||||
const line = `${event.type} ${event.id} waiting ${args}`;
|
const line = `${event.type} ${event.id} waiting ${args}`;
|
||||||
console.log('Writing line: ', line);
|
|
||||||
fs.writeFileSync(WATCH_FILE, `${line}`);
|
fs.writeFileSync(WATCH_FILE, `${line}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,8 +201,12 @@ class EventDispatcher {
|
||||||
public clear() {
|
public clear() {
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.lock = null;
|
this.lock = null;
|
||||||
|
clearInterval(this.interval);
|
||||||
|
EventDispatcher.instance = null;
|
||||||
fs.writeFileSync(WATCH_FILE, '');
|
fs.writeFileSync(WATCH_FILE, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new EventDispatcher();
|
export const eventDispatcher = EventDispatcher.getInstance();
|
||||||
|
|
||||||
|
export default EventDispatcher;
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import EventDispatcher from '../EventDispatcher';
|
|
||||||
|
|
||||||
describe('EventDispatcher', () => {});
|
|
|
@ -1,7 +1,7 @@
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import * as repoHelpers from '../../../helpers/repo-helpers';
|
|
||||||
import { getConfig } from '../../config/TipiConfig';
|
import { getConfig } from '../../config/TipiConfig';
|
||||||
import startJobs from '../jobs';
|
import startJobs from '../jobs';
|
||||||
|
import { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('node-cron');
|
jest.mock('node-cron');
|
||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
|
@ -17,16 +17,21 @@ describe('Test: startJobs', () => {
|
||||||
|
|
||||||
startJobs();
|
startJobs();
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(spy).toHaveBeenCalledWith('0 * * * *', expect.any(Function));
|
expect(spy).toHaveBeenCalledWith('*/30 * * * *', expect.any(Function));
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should update apps repo on cron trigger', () => {
|
it('Should update apps repo on cron trigger', () => {
|
||||||
const spy = jest.spyOn(repoHelpers, 'updateRepo');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEvent');
|
||||||
|
|
||||||
|
// Act
|
||||||
startJobs();
|
startJobs();
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(getConfig().appsRepoUrl);
|
// Assert
|
||||||
|
expect(spy.mock.calls.length).toBe(2);
|
||||||
|
expect(spy.mock.calls[0]).toEqual([EventTypes.UPDATE_REPO, [getConfig().appsRepoUrl]]);
|
||||||
|
expect(spy.mock.calls[1]).toEqual([EventTypes.SYSTEM_INFO, []]);
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import logger from '../../config/logger/logger';
|
import logger from '../../config/logger/logger';
|
||||||
import { getConfig } from '../../core/config/TipiConfig';
|
import { getConfig } from '../../core/config/TipiConfig';
|
||||||
import EventDispatcher, { EventTypes } from '../config/EventDispatcher';
|
import { eventDispatcher, EventTypes } from '../config/EventDispatcher';
|
||||||
|
|
||||||
const startJobs = () => {
|
const startJobs = () => {
|
||||||
logger.info('Starting cron jobs...');
|
logger.info('Starting cron jobs...');
|
||||||
|
|
||||||
// Every 30 minutes
|
// Every 30 minutes
|
||||||
cron.schedule('*/30 * * * *', async () => {
|
cron.schedule('*/30 * * * *', async () => {
|
||||||
EventDispatcher.dispatchEvent(EventTypes.UPDATE_REPO, [getConfig().appsRepoUrl]);
|
eventDispatcher.dispatchEvent(EventTypes.UPDATE_REPO, [getConfig().appsRepoUrl]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// every minute
|
// every minute
|
||||||
cron.schedule('* * * * *', () => {
|
cron.schedule('* * * * *', () => {
|
||||||
EventDispatcher.dispatchEvent(EventTypes.SYSTEM_INFO, []);
|
eventDispatcher.dispatchEvent(EventTypes.SYSTEM_INFO, []);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import childProcess from 'child_process';
|
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import logger from '../../../config/logger/logger';
|
import logger from '../../../config/logger/logger';
|
||||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||||
import App from '../app.entity';
|
import App from '../app.entity';
|
||||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo, runAppScript } from '../apps.helpers';
|
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
|
||||||
import { AppInfo } from '../apps.types';
|
import { AppInfo } from '../apps.types';
|
||||||
import { createApp } from './apps.factory';
|
import { createApp } from './apps.factory';
|
||||||
|
|
||||||
|
@ -108,48 +107,6 @@ describe('checkEnvFile', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test: runAppScript', () => {
|
|
||||||
let app1: AppInfo;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const app1create = await createApp({ installed: true });
|
|
||||||
app1 = app1create.appInfo;
|
|
||||||
// @ts-ignore
|
|
||||||
fs.__createMockFiles(app1create.MockFiles);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should run the app script', async () => {
|
|
||||||
const { MockFiles } = await createApp({ installed: true });
|
|
||||||
// @ts-ignore
|
|
||||||
fs.__createMockFiles(MockFiles);
|
|
||||||
|
|
||||||
await runAppScript(['install', app1.id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should log the error if the script fails', async () => {
|
|
||||||
const log = jest.spyOn(logger, 'error');
|
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
|
||||||
const randomWord = faker.random.word();
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
spy.mockImplementation((_path, _args, _, cb) => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (cb) cb(randomWord, null, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await runAppScript(['install', app1.id]);
|
|
||||||
expect(true).toBe(false);
|
|
||||||
} catch (e: any) {
|
|
||||||
expect(e).toBe(randomWord);
|
|
||||||
expect(log).toHaveBeenCalledWith(`Error running app script: ${randomWord}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.mockRestore();
|
|
||||||
spy.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Test: generateEnvFile', () => {
|
describe('Test: generateEnvFile', () => {
|
||||||
let app1: AppInfo;
|
let app1: AppInfo;
|
||||||
let appEntity1: App;
|
let appEntity1: App;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { createUser } from '../../auth/__tests__/user.factory';
|
||||||
import User from '../../auth/user.entity';
|
import User from '../../auth/user.entity';
|
||||||
import { installAppMutation, startAppMutation, stopAppMutation, uninstallAppMutation, updateAppConfigMutation, updateAppMutation } from '../../../test/mutations';
|
import { installAppMutation, startAppMutation, stopAppMutation, uninstallAppMutation, updateAppConfigMutation, updateAppMutation } from '../../../test/mutations';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import EventDispatcher from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('fs');
|
jest.mock('fs');
|
||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
|
@ -36,6 +37,7 @@ beforeEach(async () => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
|
||||||
await App.clear();
|
await App.clear();
|
||||||
await User.clear();
|
await User.clear();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import AppsService from '../apps.service';
|
import AppsService from '../apps.service';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import childProcess from 'child_process';
|
|
||||||
import { AppInfo, AppStatusEnum } from '../apps.types';
|
import { AppInfo, AppStatusEnum } from '../apps.types';
|
||||||
import App from '../app.entity';
|
import App from '../app.entity';
|
||||||
import { createApp } from './apps.factory';
|
import { createApp } from './apps.factory';
|
||||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { getEnvMap } from '../apps.helpers';
|
import { getEnvMap } from '../apps.helpers';
|
||||||
import { getConfig } from '../../../core/config/TipiConfig';
|
import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('fs-extra');
|
jest.mock('fs-extra');
|
||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
|
@ -23,6 +22,7 @@ beforeEach(async () => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
|
||||||
await App.clear();
|
await App.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ describe('Install app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly generate env file for app', async () => {
|
it('Should correctly generate env file for app', async () => {
|
||||||
|
// EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||||
const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
|
const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
|
||||||
|
|
||||||
|
@ -59,39 +60,28 @@ describe('Install app', () => {
|
||||||
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly run app script', async () => {
|
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
|
||||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
|
||||||
|
|
||||||
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
|
|
||||||
spy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should start app if already installed', async () => {
|
it('Should start app if already installed', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
|
|
||||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||||
|
|
||||||
expect(spy.mock.calls.length).toBe(2);
|
expect(spy.mock.calls.length).toBe(2);
|
||||||
expect(spy.mock.calls[0]).toEqual(['/runtipi/scripts/app.sh', ['install', app1.id], {}, expect.any(Function)]);
|
expect(spy.mock.calls[0]).toEqual([EventTypes.APP, ['install', app1.id]]);
|
||||||
expect(spy.mock.calls[1]).toEqual(['/runtipi/scripts/app.sh', ['start', app1.id], {}, expect.any(Function)]);
|
expect(spy.mock.calls[1]).toEqual([EventTypes.APP, ['start', app1.id]]);
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should delete app if install script fails', async () => {
|
it('Should delete app if install script fails', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Arrange
|
||||||
spy.mockImplementation(() => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||||
throw new Error('Test error');
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' })).rejects.toThrow('Test error');
|
await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' })).rejects.toThrow(`App ${app1.id} failed to install\nstdout: error`);
|
||||||
|
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
|
|
||||||
expect(app).toBeNull();
|
expect(app).toBeNull();
|
||||||
spy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if required form fields are missing', async () => {
|
it('Should throw if required form fields are missing', async () => {
|
||||||
|
@ -175,56 +165,51 @@ describe('Uninstall app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('App should be installed by default', async () => {
|
it('App should be installed by default', async () => {
|
||||||
|
// Act
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(app).toBeDefined();
|
expect(app).toBeDefined();
|
||||||
expect(app!.id).toBe(app1.id);
|
expect(app!.id).toBe(app1.id);
|
||||||
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly remove app from database', async () => {
|
it('Should correctly remove app from database', async () => {
|
||||||
|
// Act
|
||||||
await AppsService.uninstallApp(app1.id);
|
await AppsService.uninstallApp(app1.id);
|
||||||
|
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(app).toBeNull();
|
expect(app).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly run app script', async () => {
|
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
|
||||||
|
|
||||||
await AppsService.uninstallApp(app1.id);
|
|
||||||
|
|
||||||
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
|
|
||||||
|
|
||||||
spy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should stop app if it is running', async () => {
|
it('Should stop app if it is running', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Arrange
|
||||||
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
|
|
||||||
|
// Act
|
||||||
await AppsService.uninstallApp(app1.id);
|
await AppsService.uninstallApp(app1.id);
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(spy.mock.calls.length).toBe(2);
|
expect(spy.mock.calls.length).toBe(2);
|
||||||
expect(spy.mock.calls[0]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
|
expect(spy.mock.calls[0]).toEqual([EventTypes.APP, ['stop', app1.id]]);
|
||||||
expect(spy.mock.calls[1]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
|
expect(spy.mock.calls[1]).toEqual([EventTypes.APP, ['uninstall', app1.id]]);
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if app is not installed', async () => {
|
it('Should throw if app is not installed', async () => {
|
||||||
|
// Act & Assert
|
||||||
await expect(AppsService.uninstallApp('any')).rejects.toThrowError('App any not found');
|
await expect(AppsService.uninstallApp('any')).rejects.toThrowError('App any not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if uninstall script fails', async () => {
|
it('Should throw if uninstall script fails', async () => {
|
||||||
// Update app
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||||
await App.update({ id: app1.id }, { status: AppStatusEnum.UPDATING });
|
await App.update({ id: app1.id }, { status: AppStatusEnum.UPDATING });
|
||||||
|
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Act & Assert
|
||||||
spy.mockImplementation(() => {
|
await expect(AppsService.uninstallApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to uninstall\nstdout: test`);
|
||||||
throw new Error('Test error');
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(AppsService.uninstallApp(app1.id)).rejects.toThrow('Test error');
|
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
expect(app!.status).toBe(AppStatusEnum.STOPPED);
|
expect(app!.status).toBe(AppStatusEnum.STOPPED);
|
||||||
});
|
});
|
||||||
|
@ -240,12 +225,12 @@ describe('Start app', () => {
|
||||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly run app script', async () => {
|
it('Should correctly dispatch event', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
|
|
||||||
await AppsService.startApp(app1.id);
|
await AppsService.startApp(app1.id);
|
||||||
|
|
||||||
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
|
expect(spy.mock.lastCall).toEqual([EventTypes.APP, ['start', app1.id]]);
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
@ -255,7 +240,7 @@ describe('Start app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should restart if app is already running', async () => {
|
it('Should restart if app is already running', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
|
|
||||||
await AppsService.startApp(app1.id);
|
await AppsService.startApp(app1.id);
|
||||||
expect(spy.mock.calls.length).toBe(1);
|
expect(spy.mock.calls.length).toBe(1);
|
||||||
|
@ -276,12 +261,11 @@ describe('Start app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if start script fails', async () => {
|
it('Should throw if start script fails', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Arrange
|
||||||
spy.mockImplementation(() => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||||
throw new Error('Test error');
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(AppsService.startApp(app1.id)).rejects.toThrow('Test error');
|
// Act & Assert
|
||||||
|
await expect(AppsService.startApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to start\nstdout: test`);
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
expect(app!.status).toBe(AppStatusEnum.STOPPED);
|
expect(app!.status).toBe(AppStatusEnum.STOPPED);
|
||||||
});
|
});
|
||||||
|
@ -297,12 +281,12 @@ describe('Stop app', () => {
|
||||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly run app script', async () => {
|
it('Should correctly dispatch stop event', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
|
|
||||||
await AppsService.stopApp(app1.id);
|
await AppsService.stopApp(app1.id);
|
||||||
|
|
||||||
expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
|
expect(spy.mock.lastCall).toEqual([EventTypes.APP, ['stop', app1.id]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if app is not installed', async () => {
|
it('Should throw if app is not installed', async () => {
|
||||||
|
@ -310,12 +294,11 @@ describe('Stop app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if stop script fails', async () => {
|
it('Should throw if stop script fails', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Arrange
|
||||||
spy.mockImplementation(() => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
|
||||||
throw new Error('Test error');
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(AppsService.stopApp(app1.id)).rejects.toThrow('Test error');
|
// Act & Assert
|
||||||
|
await expect(AppsService.stopApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to stop\nstdout: test`);
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
||||||
});
|
});
|
||||||
|
@ -464,19 +447,19 @@ describe('Start all apps', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should correctly start all apps', async () => {
|
it('Should correctly start all apps', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
|
|
||||||
await AppsService.startAllApps();
|
await AppsService.startAllApps();
|
||||||
|
|
||||||
expect(spy.mock.calls.length).toBe(2);
|
expect(spy.mock.calls.length).toBe(2);
|
||||||
expect(spy.mock.calls).toEqual([
|
expect(spy.mock.calls).toEqual([
|
||||||
[`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)],
|
[EventTypes.APP, ['start', app1.id]],
|
||||||
[`${getConfig().rootFolder}/scripts/app.sh`, ['start', app2.id], {}, expect.any(Function)],
|
[EventTypes.APP, ['start', app2.id]],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not start app which has not status RUNNING', async () => {
|
it('Should not start app which has not status RUNNING', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
|
||||||
await createApp({ installed: true, status: AppStatusEnum.STOPPED });
|
await createApp({ installed: true, status: AppStatusEnum.STOPPED });
|
||||||
|
|
||||||
await AppsService.startAllApps();
|
await AppsService.startAllApps();
|
||||||
|
@ -487,16 +470,14 @@ describe('Start all apps', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should put app status to STOPPED if start script fails', async () => {
|
it('Should put app status to STOPPED if start script fails', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Arrange
|
||||||
spy.mockImplementation(() => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||||
throw new Error('Test error');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Act
|
||||||
await AppsService.startAllApps();
|
await AppsService.startAllApps();
|
||||||
|
|
||||||
const apps = await App.find();
|
const apps = await App.find();
|
||||||
|
|
||||||
expect(spy.mock.calls.length).toBe(2);
|
// Assert
|
||||||
expect(apps.length).toBe(2);
|
expect(apps.length).toBe(2);
|
||||||
expect(apps[0].status).toBe(AppStatusEnum.STOPPED);
|
expect(apps[0].status).toBe(AppStatusEnum.STOPPED);
|
||||||
expect(apps[1].status).toBe(AppStatusEnum.STOPPED);
|
expect(apps[1].status).toBe(AppStatusEnum.STOPPED);
|
||||||
|
@ -529,12 +510,10 @@ describe('Update app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if update script fails', async () => {
|
it('Should throw if update script fails', async () => {
|
||||||
const spy = jest.spyOn(childProcess, 'execFile');
|
// Arrange
|
||||||
spy.mockImplementation(() => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||||
throw new Error('Test error');
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(AppsService.updateApp(app1.id)).rejects.toThrow('Test error');
|
await expect(AppsService.updateApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to update\nstdout: error`);
|
||||||
const app = await App.findOne({ where: { id: app1.id } });
|
const app = await App.findOne({ where: { id: app1.id } });
|
||||||
expect(app!.status).toBe(AppStatusEnum.STOPPED);
|
expect(app!.status).toBe(AppStatusEnum.STOPPED);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import App from './app.entity';
|
||||||
import logger from '../../config/logger/logger';
|
import logger from '../../config/logger/logger';
|
||||||
import { Not } from 'typeorm';
|
import { Not } from 'typeorm';
|
||||||
import { getConfig } from '../../core/config/TipiConfig';
|
import { getConfig } from '../../core/config/TipiConfig';
|
||||||
import EventDispatcher, { EventTypes } from '../../core/config/EventDispatcher';
|
import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
|
||||||
|
|
||||||
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
|
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
|
||||||
|
|
||||||
|
@ -26,9 +26,13 @@ const startAllApps = async (): Promise<void> => {
|
||||||
|
|
||||||
await App.update({ id: app.id }, { status: AppStatusEnum.STARTING });
|
await App.update({ id: app.id }, { status: AppStatusEnum.STARTING });
|
||||||
|
|
||||||
EventDispatcher.dispatchEvent(EventTypes.APP, ['start', app.id]);
|
eventDispatcher.dispatchEventAsync(EventTypes.APP, ['start', app.id]).then(({ success }) => {
|
||||||
|
if (success) {
|
||||||
await App.update({ id: app.id }, { status: AppStatusEnum.RUNNING });
|
App.update({ id: app.id }, { status: AppStatusEnum.RUNNING });
|
||||||
|
} else {
|
||||||
|
App.update({ id: app.id }, { status: AppStatusEnum.STOPPED });
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await App.update({ id: app.id }, { status: AppStatusEnum.STOPPED });
|
await App.update({ id: app.id }, { status: AppStatusEnum.STOPPED });
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
|
@ -55,7 +59,7 @@ const startApp = async (appName: string): Promise<App> => {
|
||||||
checkEnvFile(appName);
|
checkEnvFile(appName);
|
||||||
|
|
||||||
await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
|
await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
|
||||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['start', app.id]);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['start', app.id]);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await App.update({ id: appName }, { status: AppStatusEnum.RUNNING });
|
await App.update({ id: appName }, { status: AppStatusEnum.RUNNING });
|
||||||
|
@ -120,7 +124,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
|
||||||
generateEnvFile(app);
|
generateEnvFile(app);
|
||||||
|
|
||||||
// Run script
|
// Run script
|
||||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['install', id]);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['install', id]);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await App.delete({ id });
|
await App.delete({ id });
|
||||||
|
@ -221,7 +225,7 @@ const stopApp = async (id: string): Promise<App> => {
|
||||||
// Run script
|
// Run script
|
||||||
await App.update({ id }, { status: AppStatusEnum.STOPPING });
|
await App.update({ id }, { status: AppStatusEnum.STOPPING });
|
||||||
|
|
||||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['stop', id]);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['stop', id]);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||||
|
@ -255,7 +259,7 @@ const uninstallApp = async (id: string): Promise<App> => {
|
||||||
|
|
||||||
await App.update({ id }, { status: AppStatusEnum.UNINSTALLING });
|
await App.update({ id }, { status: AppStatusEnum.UNINSTALLING });
|
||||||
|
|
||||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['uninstall', id]);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['uninstall', id]);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||||
|
@ -299,12 +303,13 @@ const updateApp = async (id: string) => {
|
||||||
|
|
||||||
await App.update({ id }, { status: AppStatusEnum.UPDATING });
|
await App.update({ id }, { status: AppStatusEnum.UPDATING });
|
||||||
|
|
||||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['update', id]);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['update', id]);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const appInfo: AppInfo | null = await readJsonFile(`/runtipi/apps/${id}/config.json`);
|
const appInfo: AppInfo | null = await readJsonFile(`/runtipi/apps/${id}/config.json`);
|
||||||
await App.update({ id }, { status: AppStatusEnum.RUNNING, version: Number(appInfo?.tipi_version) });
|
await App.update({ id }, { status: AppStatusEnum.RUNNING, version: Number(appInfo?.tipi_version) });
|
||||||
} else {
|
} else {
|
||||||
|
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||||
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
|
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { systemInfoQuery, versionQuery } from '../../../test/queries';
|
||||||
import User from '../../auth/user.entity';
|
import User from '../../auth/user.entity';
|
||||||
import { createUser } from '../../auth/__tests__/user.factory';
|
import { createUser } from '../../auth/__tests__/user.factory';
|
||||||
import { SystemInfoResponse } from '../system.types';
|
import { SystemInfoResponse } from '../system.types';
|
||||||
|
import EventDispatcher from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('fs-extra');
|
jest.mock('fs-extra');
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
@ -133,38 +134,50 @@ describe('Test: restart', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return true', async () => {
|
it('Should return true', async () => {
|
||||||
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
const user = await createUser();
|
const user = await createUser();
|
||||||
const { data } = await gcall<{ restart: boolean }>({ source: restartMutation, userId: user.id });
|
const { data } = await gcall<{ restart: boolean }>({ source: restartMutation, userId: user.id });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(data?.restart).toBeDefined();
|
expect(data?.restart).toBeDefined();
|
||||||
expect(data?.restart).toBe(true);
|
expect(data?.restart).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return an error if user doesn't exist", async () => {
|
it("Should return an error if user doesn't exist", async () => {
|
||||||
|
// Arrange
|
||||||
const { data, errors } = await gcall<{ restart: boolean }>({
|
const { data, errors } = await gcall<{ restart: boolean }>({
|
||||||
source: restartMutation,
|
source: restartMutation,
|
||||||
userId: 1,
|
userId: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||||
expect(data?.restart).toBeUndefined();
|
expect(data?.restart).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw an error if no userId is not provided', async () => {
|
it('Should throw an error if no userId is not provided', async () => {
|
||||||
|
// Arrange
|
||||||
const { data, errors } = await gcall<{ restart: boolean }>({ source: restartMutation });
|
const { data, errors } = await gcall<{ restart: boolean }>({ source: restartMutation });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||||
expect(data?.restart).toBeUndefined();
|
expect(data?.restart).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set app status to restarting', async () => {
|
it('Should set app status to restarting', async () => {
|
||||||
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
const spy = jest.spyOn(TipiConfig, 'setConfig');
|
const spy = jest.spyOn(TipiConfig, 'setConfig');
|
||||||
|
|
||||||
const user = await createUser();
|
const user = await createUser();
|
||||||
|
|
||||||
|
// Act
|
||||||
await gcall<{ restart: boolean }>({ source: restartMutation, userId: user.id });
|
await gcall<{ restart: boolean }>({ source: restartMutation, userId: user.id });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(1, 'status', 'RESTARTING');
|
expect(spy).toHaveBeenNthCalledWith(1, 'status', 'RESTARTING');
|
||||||
expect(spy).toHaveBeenNthCalledWith(2, 'status', 'RUNNING');
|
expect(spy).toHaveBeenNthCalledWith(2, 'status', 'RUNNING');
|
||||||
|
|
||||||
|
@ -180,35 +193,47 @@ describe('Test: update', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return true', async () => {
|
it('Should return true', async () => {
|
||||||
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
const user = await createUser();
|
const user = await createUser();
|
||||||
|
|
||||||
|
// Act
|
||||||
const { data } = await gcall<{ update: boolean }>({ source: updateMutation, userId: user.id });
|
const { data } = await gcall<{ update: boolean }>({ source: updateMutation, userId: user.id });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(data?.update).toBeDefined();
|
expect(data?.update).toBeDefined();
|
||||||
expect(data?.update).toBe(true);
|
expect(data?.update).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return an error if user doesn't exist", async () => {
|
it("Should return an error if user doesn't exist", async () => {
|
||||||
|
// Act
|
||||||
const { data, errors } = await gcall<{ update: boolean }>({ source: updateMutation, userId: 1 });
|
const { data, errors } = await gcall<{ update: boolean }>({ source: updateMutation, userId: 1 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||||
expect(data?.update).toBeUndefined();
|
expect(data?.update).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw an error if no userId is not provided', async () => {
|
it('Should throw an error if no userId is not provided', async () => {
|
||||||
|
// Act
|
||||||
const { data, errors } = await gcall<{ update: boolean }>({ source: updateMutation });
|
const { data, errors } = await gcall<{ update: boolean }>({ source: updateMutation });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||||
expect(data?.update).toBeUndefined();
|
expect(data?.update).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set app status to updating', async () => {
|
it('Should set app status to updating', async () => {
|
||||||
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
const spy = jest.spyOn(TipiConfig, 'setConfig');
|
const spy = jest.spyOn(TipiConfig, 'setConfig');
|
||||||
|
|
||||||
const user = await createUser();
|
const user = await createUser();
|
||||||
|
|
||||||
|
// Act
|
||||||
await gcall<{ update: boolean }>({ source: updateMutation, userId: user.id });
|
await gcall<{ update: boolean }>({ source: updateMutation, userId: user.id });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
expect(spy).toHaveBeenNthCalledWith(1, 'status', 'UPDATING');
|
expect(spy).toHaveBeenNthCalledWith(1, 'status', 'UPDATING');
|
||||||
expect(spy).toHaveBeenNthCalledWith(2, 'status', 'RUNNING');
|
expect(spy).toHaveBeenNthCalledWith(2, 'status', 'RUNNING');
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import childProcess from 'child_process';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import SystemService from '../system.service';
|
import SystemService from '../system.service';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import TipiCache from '../../../config/TipiCache';
|
import TipiCache from '../../../config/TipiCache';
|
||||||
import { setConfig } from '../../../core/config/TipiConfig';
|
import { setConfig } from '../../../core/config/TipiConfig';
|
||||||
import logger from '../../../config/logger/logger';
|
import logger from '../../../config/logger/logger';
|
||||||
|
import EventDispatcher from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('fs-extra');
|
jest.mock('fs-extra');
|
||||||
jest.mock('child_process');
|
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -21,14 +20,14 @@ describe('Test: systemInfo', () => {
|
||||||
it('Should throw if system-info.json does not exist', () => {
|
it('Should throw if system-info.json does not exist', () => {
|
||||||
try {
|
try {
|
||||||
SystemService.systemInfo();
|
SystemService.systemInfo();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
expect(e).toBeDefined();
|
expect(e).toBeDefined();
|
||||||
// @ts-ignore
|
|
||||||
expect(e.message).toBe('Error parsing system info');
|
expect(e.message).toBe('Error parsing system info');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('It should return system info', async () => {
|
it('It should return system info', async () => {
|
||||||
|
// Arrange
|
||||||
const info = {
|
const info = {
|
||||||
cpu: { load: 0.1 },
|
cpu: { load: 0.1 },
|
||||||
memory: { available: 1000, total: 2000, used: 1000 },
|
memory: { available: 1000, total: 2000, used: 1000 },
|
||||||
|
@ -42,8 +41,10 @@ describe('Test: systemInfo', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fs.__createMockFiles(MockFiles);
|
fs.__createMockFiles(MockFiles);
|
||||||
|
|
||||||
|
// Act
|
||||||
const systemInfo = SystemService.systemInfo();
|
const systemInfo = SystemService.systemInfo();
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(systemInfo).toBeDefined();
|
expect(systemInfo).toBeDefined();
|
||||||
expect(systemInfo.cpu).toBeDefined();
|
expect(systemInfo.cpu).toBeDefined();
|
||||||
expect(systemInfo.memory).toBeDefined();
|
expect(systemInfo.memory).toBeDefined();
|
||||||
|
@ -60,11 +61,15 @@ describe('Test: getVersion', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('It should return version', async () => {
|
it('It should return version', async () => {
|
||||||
|
// Arrange
|
||||||
const spy = jest.spyOn(axios, 'get').mockResolvedValue({
|
const spy = jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
data: { name: `v${faker.random.numeric(1)}.${faker.random.numeric(1)}.${faker.random.numeric()}` },
|
data: { name: `v${faker.random.numeric(1)}.${faker.random.numeric(1)}.${faker.random.numeric()}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
const version = await SystemService.getVersion();
|
const version = await SystemService.getVersion();
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(version).toBeDefined();
|
expect(version).toBeDefined();
|
||||||
expect(version.current).toBeDefined();
|
expect(version.current).toBeDefined();
|
||||||
expect(semver.valid(version.latest)).toBeTruthy();
|
expect(semver.valid(version.latest)).toBeTruthy();
|
||||||
|
@ -85,17 +90,20 @@ describe('Test: getVersion', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return cached version', async () => {
|
it('Should return cached version', async () => {
|
||||||
|
// Arrange
|
||||||
const spy = jest.spyOn(axios, 'get').mockResolvedValue({
|
const spy = jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
data: { name: `v${faker.random.numeric(1)}.${faker.random.numeric(1)}.${faker.random.numeric()}` },
|
data: { name: `v${faker.random.numeric(1)}.${faker.random.numeric(1)}.${faker.random.numeric()}` },
|
||||||
});
|
});
|
||||||
const version = await SystemService.getVersion();
|
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const version = await SystemService.getVersion();
|
||||||
|
const version2 = await SystemService.getVersion();
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(version).toBeDefined();
|
expect(version).toBeDefined();
|
||||||
expect(version.current).toBeDefined();
|
expect(version.current).toBeDefined();
|
||||||
expect(semver.valid(version.latest)).toBeTruthy();
|
expect(semver.valid(version.latest)).toBeTruthy();
|
||||||
|
|
||||||
const version2 = await SystemService.getVersion();
|
|
||||||
|
|
||||||
expect(version2.latest).toBe(version.latest);
|
expect(version2.latest).toBe(version.latest);
|
||||||
expect(version2.current).toBeDefined();
|
expect(version2.current).toBeDefined();
|
||||||
expect(semver.valid(version2.latest)).toBeTruthy();
|
expect(semver.valid(version2.latest)).toBeTruthy();
|
||||||
|
@ -108,88 +116,98 @@ describe('Test: getVersion', () => {
|
||||||
|
|
||||||
describe('Test: restart', () => {
|
describe('Test: restart', () => {
|
||||||
it('Should return true', async () => {
|
it('Should return true', async () => {
|
||||||
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
const restart = await SystemService.restart();
|
const restart = await SystemService.restart();
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(restart).toBeTruthy();
|
expect(restart).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should log error if fails', async () => {
|
it('Should log error if fails', async () => {
|
||||||
// @ts-ignore
|
// Arrange
|
||||||
const spy = jest.spyOn(childProcess, 'execFile').mockImplementation((_path, _args, _, cb) => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'fake' });
|
||||||
// @ts-ignore
|
|
||||||
if (cb) cb('error', null, null);
|
|
||||||
});
|
|
||||||
const log = jest.spyOn(logger, 'error');
|
const log = jest.spyOn(logger, 'error');
|
||||||
|
|
||||||
|
// Act
|
||||||
const restart = await SystemService.restart();
|
const restart = await SystemService.restart();
|
||||||
|
|
||||||
expect(restart).toBeTruthy();
|
// Assert
|
||||||
expect(log).toHaveBeenCalledWith('Error restarting: error');
|
expect(restart).toBeFalsy();
|
||||||
|
expect(log).toHaveBeenCalledWith('Error restarting system: fake');
|
||||||
spy.mockRestore();
|
log.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test: update', () => {
|
describe('Test: update', () => {
|
||||||
it('Should return true', async () => {
|
it('Should return true', async () => {
|
||||||
|
// Arrange
|
||||||
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
|
||||||
setConfig('version', '0.0.1');
|
setConfig('version', '0.0.1');
|
||||||
TipiCache.set('latestVersion', '0.0.2');
|
TipiCache.set('latestVersion', '0.0.2');
|
||||||
|
|
||||||
|
// Act
|
||||||
const update = await SystemService.update();
|
const update = await SystemService.update();
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(update).toBeTruthy();
|
expect(update).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw an error if latest version is not set', async () => {
|
it('Should throw an error if latest version is not set', async () => {
|
||||||
|
// Arrange
|
||||||
TipiCache.del('latestVersion');
|
TipiCache.del('latestVersion');
|
||||||
const spy = jest.spyOn(axios, 'get').mockResolvedValue({
|
const spy = jest.spyOn(axios, 'get').mockResolvedValue({
|
||||||
data: { name: null },
|
data: { name: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
setConfig('version', '0.0.1');
|
setConfig('version', '0.0.1');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
await expect(SystemService.update()).rejects.toThrow('Could not get latest version');
|
await expect(SystemService.update()).rejects.toThrow('Could not get latest version');
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if current version is higher than latest', async () => {
|
it('Should throw if current version is higher than latest', async () => {
|
||||||
|
// Arrange
|
||||||
setConfig('version', '0.0.2');
|
setConfig('version', '0.0.2');
|
||||||
TipiCache.set('latestVersion', '0.0.1');
|
TipiCache.set('latestVersion', '0.0.1');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
await expect(SystemService.update()).rejects.toThrow('Current version is newer than latest version');
|
await expect(SystemService.update()).rejects.toThrow('Current version is newer than latest version');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if current version is equal to latest', async () => {
|
it('Should throw if current version is equal to latest', async () => {
|
||||||
|
// Arrange
|
||||||
setConfig('version', '0.0.1');
|
setConfig('version', '0.0.1');
|
||||||
TipiCache.set('latestVersion', '0.0.1');
|
TipiCache.set('latestVersion', '0.0.1');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
await expect(SystemService.update()).rejects.toThrow('Current version is already up to date');
|
await expect(SystemService.update()).rejects.toThrow('Current version is already up to date');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw an error if there is a major version difference', async () => {
|
it('Should throw an error if there is a major version difference', async () => {
|
||||||
|
// Arrange
|
||||||
setConfig('version', '0.0.1');
|
setConfig('version', '0.0.1');
|
||||||
TipiCache.set('latestVersion', '1.0.0');
|
TipiCache.set('latestVersion', '1.0.0');
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
await expect(SystemService.update()).rejects.toThrow('The major version has changed. Please update manually');
|
await expect(SystemService.update()).rejects.toThrow('The major version has changed. Please update manually');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should log error if fails', async () => {
|
it('Should log error if fails', async () => {
|
||||||
// @ts-ignore
|
// Arrange
|
||||||
const spy = jest.spyOn(childProcess, 'execFile').mockImplementation((_path, _args, _, cb) => {
|
EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'fake2' });
|
||||||
// @ts-ignore
|
|
||||||
if (cb) cb('error', null, null);
|
|
||||||
});
|
|
||||||
const log = jest.spyOn(logger, 'error');
|
const log = jest.spyOn(logger, 'error');
|
||||||
|
|
||||||
|
// Act
|
||||||
setConfig('version', '0.0.1');
|
setConfig('version', '0.0.1');
|
||||||
TipiCache.set('latestVersion', '0.0.2');
|
TipiCache.set('latestVersion', '0.0.2');
|
||||||
|
|
||||||
const update = await SystemService.update();
|
const update = await SystemService.update();
|
||||||
|
|
||||||
expect(update).toBeTruthy();
|
// Assert
|
||||||
expect(log).toHaveBeenCalledWith('Error updating: error');
|
expect(update).toBeFalsy();
|
||||||
|
expect(log).toHaveBeenCalledWith('Error updating system: fake2');
|
||||||
spy.mockRestore();
|
log.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ import logger from '../../config/logger/logger';
|
||||||
import TipiCache from '../../config/TipiCache';
|
import TipiCache from '../../config/TipiCache';
|
||||||
import { getConfig, setConfig } from '../../core/config/TipiConfig';
|
import { getConfig, setConfig } from '../../core/config/TipiConfig';
|
||||||
import { readJsonFile } from '../fs/fs.helpers';
|
import { readJsonFile } from '../fs/fs.helpers';
|
||||||
import EventDispatcher, { EventTypes } from '../../core/config/EventDispatcher';
|
import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
|
||||||
|
|
||||||
const systemInfoSchema = z.object({
|
const systemInfoSchema = z.object({
|
||||||
cpu: z.object({
|
cpu: z.object({
|
||||||
|
@ -58,10 +58,10 @@ const getVersion = async (): Promise<{ current: string; latest?: string }> => {
|
||||||
const restart = async (): Promise<boolean> => {
|
const restart = async (): Promise<boolean> => {
|
||||||
setConfig('status', 'RESTARTING');
|
setConfig('status', 'RESTARTING');
|
||||||
|
|
||||||
const { success } = await EventDispatcher.dispatchEventAsync(EventTypes.RESTART);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.RESTART);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
logger.error('Error restarting system');
|
logger.error(`Error restarting system: ${stdout}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +91,10 @@ const update = async (): Promise<boolean> => {
|
||||||
|
|
||||||
setConfig('status', 'UPDATING');
|
setConfig('status', 'UPDATING');
|
||||||
|
|
||||||
const { success } = await EventDispatcher.dispatchEventAsync(EventTypes.UPDATE);
|
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.UPDATE);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
logger.error('Error updating system');
|
logger.error(`Error updating system: ${stdout}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import startJobs from './core/jobs/jobs';
|
||||||
import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig';
|
import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import systemController from './modules/system/system.controller';
|
import systemController from './modules/system/system.controller';
|
||||||
import EventDispatcher, { EventTypes } from './core/config/EventDispatcher';
|
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
|
||||||
|
|
||||||
let corsOptions = {
|
let corsOptions = {
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
@ -53,7 +53,7 @@ const applyCustomConfig = () => {
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
EventDispatcher.clear();
|
eventDispatcher.clear();
|
||||||
applyCustomConfig();
|
applyCustomConfig();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
@ -94,8 +94,8 @@ const main = async () => {
|
||||||
await runUpdates();
|
await runUpdates();
|
||||||
|
|
||||||
httpServer.listen(port, async () => {
|
httpServer.listen(port, async () => {
|
||||||
await EventDispatcher.dispatchEventAsync(EventTypes.CLONE_REPO, [getConfig().appsRepoUrl]);
|
await eventDispatcher.dispatchEventAsync(EventTypes.CLONE_REPO, [getConfig().appsRepoUrl]);
|
||||||
await EventDispatcher.dispatchEventAsync(EventTypes.UPDATE_REPO, [getConfig().appsRepoUrl]);
|
await eventDispatcher.dispatchEventAsync(EventTypes.UPDATE_REPO, [getConfig().appsRepoUrl]);
|
||||||
|
|
||||||
startJobs();
|
startJobs();
|
||||||
setConfig('status', 'RUNNING');
|
setConfig('status', 'RUNNING');
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
|
import { eventDispatcher } from '../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('../config/logger/logger', () => ({
|
jest.mock('../config/logger/logger', () => ({
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
warn: jest.fn(),
|
warn: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
eventDispatcher.clear();
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue