apps.service.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. import AppsService from '../apps.service';
  2. import fs from 'fs-extra';
  3. import { AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum } from '../apps.types';
  4. import App from '../app.entity';
  5. import { createApp } from './apps.factory';
  6. import { setupConnection, teardownConnection } from '../../../test/connection';
  7. import { DataSource } from 'typeorm';
  8. import { getEnvMap } from '../apps.helpers';
  9. import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
  10. import { setConfig } from '../../../core/config/TipiConfig';
  11. jest.mock('fs-extra');
  12. jest.mock('child_process');
  13. let db: DataSource | null = null;
  14. const TEST_SUITE = 'appsservice';
  15. beforeAll(async () => {
  16. db = await setupConnection(TEST_SUITE);
  17. });
  18. beforeEach(async () => {
  19. jest.resetModules();
  20. jest.resetAllMocks();
  21. jest.restoreAllMocks();
  22. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
  23. await App.clear();
  24. });
  25. afterAll(async () => {
  26. await db?.destroy();
  27. await teardownConnection(TEST_SUITE);
  28. });
  29. describe('Install app', () => {
  30. let app1: AppInfo;
  31. beforeEach(async () => {
  32. const { MockFiles, appInfo } = await createApp({});
  33. app1 = appInfo;
  34. // @ts-ignore
  35. fs.__createMockFiles(MockFiles);
  36. });
  37. it('Should correctly generate env file for app', async () => {
  38. // EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
  39. await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
  40. const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
  41. expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
  42. });
  43. it('Should add app in database', async () => {
  44. await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
  45. const app = await App.findOne({ where: { id: app1.id } });
  46. expect(app).toBeDefined();
  47. expect(app!.id).toBe(app1.id);
  48. expect(app!.config).toStrictEqual({ TEST_FIELD: 'test' });
  49. expect(app!.status).toBe(AppStatusEnum.RUNNING);
  50. });
  51. it('Should start app if already installed', async () => {
  52. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  53. await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
  54. await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
  55. expect(spy.mock.calls.length).toBe(2);
  56. expect(spy.mock.calls[0]).toEqual([EventTypes.APP, ['install', app1.id]]);
  57. expect(spy.mock.calls[1]).toEqual([EventTypes.APP, ['start', app1.id]]);
  58. spy.mockRestore();
  59. });
  60. it('Should delete app if install script fails', async () => {
  61. // Arrange
  62. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
  63. await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' })).rejects.toThrow(`App ${app1.id} failed to install\nstdout: error`);
  64. const app = await App.findOne({ where: { id: app1.id } });
  65. expect(app).toBeNull();
  66. });
  67. it('Should throw if required form fields are missing', async () => {
  68. await expect(AppsService.installApp(app1.id, {})).rejects.toThrowError('Variable TEST_FIELD is required');
  69. });
  70. it('Correctly generates a random value if the field has a "random" type', async () => {
  71. const { appInfo, MockFiles } = await createApp({ randomField: true });
  72. // @ts-ignore
  73. fs.__createMockFiles(MockFiles);
  74. await AppsService.installApp(appInfo.id, { TEST_FIELD: 'yolo' });
  75. const envMap = getEnvMap(appInfo.id);
  76. expect(envMap.get('RANDOM_FIELD')).toBeDefined();
  77. expect(envMap.get('RANDOM_FIELD')).toHaveLength(32);
  78. });
  79. it('Should correctly copy app from repos to apps folder', async () => {
  80. await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
  81. const appFolder = fs.readdirSync(`/runtipi/apps/${app1.id}`);
  82. expect(appFolder).toBeDefined();
  83. expect(appFolder.indexOf('docker-compose.yml')).toBeGreaterThanOrEqual(0);
  84. });
  85. it('Should cleanup any app folder existing before install', async () => {
  86. const { MockFiles, appInfo } = await createApp({});
  87. app1 = appInfo;
  88. MockFiles[`/runtipi/apps/${appInfo.id}/docker-compose.yml`] = 'test';
  89. MockFiles[`/runtipi/apps/${appInfo.id}/test.yml`] = 'test';
  90. MockFiles[`/runtipi/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
  91. // @ts-ignore
  92. fs.__createMockFiles(MockFiles);
  93. expect(fs.existsSync(`/runtipi/apps/${app1.id}/test.yml`)).toBe(true);
  94. await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
  95. expect(fs.existsSync(`/runtipi/apps/${app1.id}/test.yml`)).toBe(false);
  96. expect(fs.existsSync(`/runtipi/apps/${app1.id}/docker-compose.yml`)).toBe(true);
  97. });
  98. it('Should throw if app is exposed and domain is not provided', async () => {
  99. await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required if app is exposed');
  100. });
  101. it('Should throw if app is exposed and config does not allow it', async () => {
  102. await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
  103. });
  104. it('Should throw if app is exposed and domain is not valid', async () => {
  105. const { MockFiles, appInfo } = await createApp({ exposable: true });
  106. // @ts-ignore
  107. fs.__createMockFiles(MockFiles);
  108. await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
  109. });
  110. it('Should throw if app is exposed and domain is already used', async () => {
  111. const app2 = await createApp({ exposable: true });
  112. const app3 = await createApp({ exposable: true });
  113. // @ts-ignore
  114. fs.__createMockFiles(Object.assign({}, app2.MockFiles, app3.MockFiles));
  115. await AppsService.installApp(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
  116. await expect(AppsService.installApp(app3.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`Domain test.com already in use by app ${app2.appInfo.id}`);
  117. });
  118. it('Should throw if architecure is not supported', async () => {
  119. const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
  120. // @ts-ignore
  121. fs.__createMockFiles(MockFiles);
  122. await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} is not supported on this architecture`);
  123. });
  124. it('Can install if architecture is supported', async () => {
  125. setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
  126. const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM, AppSupportedArchitecturesEnum.ARM64] });
  127. // @ts-ignore
  128. fs.__createMockFiles(MockFiles);
  129. await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
  130. const app = await App.findOne({ where: { id: appInfo.id } });
  131. expect(app).toBeDefined();
  132. });
  133. it('Can install if no architecture is specified', async () => {
  134. setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
  135. const { MockFiles, appInfo } = await createApp({ supportedArchitectures: undefined });
  136. // @ts-ignore
  137. fs.__createMockFiles(MockFiles);
  138. await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
  139. const app = await App.findOne({ where: { id: appInfo.id } });
  140. expect(app).toBeDefined();
  141. });
  142. });
  143. describe('Uninstall app', () => {
  144. let app1: AppInfo;
  145. beforeEach(async () => {
  146. const app1create = await createApp({ installed: true });
  147. app1 = app1create.appInfo;
  148. // @ts-ignore
  149. fs.__createMockFiles(Object.assign(app1create.MockFiles));
  150. });
  151. it('App should be installed by default', async () => {
  152. // Act
  153. const app = await App.findOne({ where: { id: app1.id } });
  154. // Assert
  155. expect(app).toBeDefined();
  156. expect(app!.id).toBe(app1.id);
  157. expect(app!.status).toBe(AppStatusEnum.RUNNING);
  158. });
  159. it('Should correctly remove app from database', async () => {
  160. // Act
  161. await AppsService.uninstallApp(app1.id);
  162. const app = await App.findOne({ where: { id: app1.id } });
  163. // Assert
  164. expect(app).toBeNull();
  165. });
  166. it('Should stop app if it is running', async () => {
  167. // Arrange
  168. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  169. // Act
  170. await AppsService.uninstallApp(app1.id);
  171. // Assert
  172. expect(spy.mock.calls.length).toBe(2);
  173. expect(spy.mock.calls[0]).toEqual([EventTypes.APP, ['stop', app1.id]]);
  174. expect(spy.mock.calls[1]).toEqual([EventTypes.APP, ['uninstall', app1.id]]);
  175. spy.mockRestore();
  176. });
  177. it('Should throw if app is not installed', async () => {
  178. // Act & Assert
  179. await expect(AppsService.uninstallApp('any')).rejects.toThrowError('App any not found');
  180. });
  181. it('Should throw if uninstall script fails', async () => {
  182. // Arrange
  183. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
  184. await App.update({ id: app1.id }, { status: AppStatusEnum.UPDATING });
  185. // Act & Assert
  186. await expect(AppsService.uninstallApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to uninstall\nstdout: test`);
  187. const app = await App.findOne({ where: { id: app1.id } });
  188. expect(app!.status).toBe(AppStatusEnum.STOPPED);
  189. });
  190. });
  191. describe('Start app', () => {
  192. let app1: AppInfo;
  193. beforeEach(async () => {
  194. const app1create = await createApp({ installed: true });
  195. app1 = app1create.appInfo;
  196. // @ts-ignore
  197. fs.__createMockFiles(Object.assign(app1create.MockFiles));
  198. });
  199. it('Should correctly dispatch event', async () => {
  200. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  201. await AppsService.startApp(app1.id);
  202. expect(spy.mock.lastCall).toEqual([EventTypes.APP, ['start', app1.id]]);
  203. spy.mockRestore();
  204. });
  205. it('Should throw if app is not installed', async () => {
  206. await expect(AppsService.startApp('any')).rejects.toThrowError('App any not found');
  207. });
  208. it('Should restart if app is already running', async () => {
  209. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  210. await AppsService.startApp(app1.id);
  211. expect(spy.mock.calls.length).toBe(1);
  212. await AppsService.startApp(app1.id);
  213. expect(spy.mock.calls.length).toBe(2);
  214. spy.mockRestore();
  215. });
  216. it('Regenerate env file', async () => {
  217. fs.writeFile(`/app/storage/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
  218. await AppsService.startApp(app1.id);
  219. const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
  220. expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
  221. });
  222. it('Should throw if start script fails', async () => {
  223. // Arrange
  224. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
  225. // Act & Assert
  226. await expect(AppsService.startApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to start\nstdout: test`);
  227. const app = await App.findOne({ where: { id: app1.id } });
  228. expect(app!.status).toBe(AppStatusEnum.STOPPED);
  229. });
  230. });
  231. describe('Stop app', () => {
  232. let app1: AppInfo;
  233. beforeEach(async () => {
  234. const app1create = await createApp({ installed: true });
  235. app1 = app1create.appInfo;
  236. // @ts-ignore
  237. fs.__createMockFiles(Object.assign(app1create.MockFiles));
  238. });
  239. it('Should correctly dispatch stop event', async () => {
  240. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  241. await AppsService.stopApp(app1.id);
  242. expect(spy.mock.lastCall).toEqual([EventTypes.APP, ['stop', app1.id]]);
  243. });
  244. it('Should throw if app is not installed', async () => {
  245. await expect(AppsService.stopApp('any')).rejects.toThrowError('App any not found');
  246. });
  247. it('Should throw if stop script fails', async () => {
  248. // Arrange
  249. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
  250. // Act & Assert
  251. await expect(AppsService.stopApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to stop\nstdout: test`);
  252. const app = await App.findOne({ where: { id: app1.id } });
  253. expect(app!.status).toBe(AppStatusEnum.RUNNING);
  254. });
  255. });
  256. describe('Update app config', () => {
  257. let app1: AppInfo;
  258. beforeEach(async () => {
  259. const app1create = await createApp({ installed: true });
  260. app1 = app1create.appInfo;
  261. // @ts-ignore
  262. fs.__createMockFiles(Object.assign(app1create.MockFiles));
  263. });
  264. it('Should correctly update app config', async () => {
  265. await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
  266. const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
  267. expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
  268. });
  269. it('Should throw if required field is missing', async () => {
  270. await expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: '' })).rejects.toThrowError('Variable TEST_FIELD is required');
  271. });
  272. it('Should throw if app is not installed', async () => {
  273. await expect(AppsService.updateAppConfig('test-app-2', { test: 'test' })).rejects.toThrowError('App test-app-2 not found');
  274. });
  275. it('Should not recreate random field if already present in .env', async () => {
  276. const { appInfo, MockFiles } = await createApp({ randomField: true, installed: true });
  277. // @ts-ignore
  278. fs.__createMockFiles(MockFiles);
  279. const envFile = fs.readFileSync(`/app/storage/app-data/${appInfo.id}/app.env`).toString();
  280. fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
  281. await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
  282. const envMap = getEnvMap(appInfo.id);
  283. expect(envMap.get('RANDOM_FIELD')).toBe('test');
  284. });
  285. it('Should throw if app is exposed and domain is not provided', () => {
  286. return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required');
  287. });
  288. it('Should throw if app is exposed and domain is not valid', () => {
  289. return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
  290. });
  291. it('Should throw if app is exposed and config does not allow it', () => {
  292. return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
  293. });
  294. it('Should throw if app is exposed and domain is already used', async () => {
  295. const app2 = await createApp({ exposable: true, installed: true });
  296. const app3 = await createApp({ exposable: true, installed: true });
  297. // @ts-ignore
  298. fs.__createMockFiles(Object.assign(app2.MockFiles, app3.MockFiles));
  299. await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
  300. await expect(AppsService.updateAppConfig(app3.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`Domain test.com already in use by app ${app2.appInfo.id}`);
  301. });
  302. it('Should not throw if updating with same domain', async () => {
  303. const app2 = await createApp({ exposable: true, installed: true });
  304. // @ts-ignore
  305. fs.__createMockFiles(Object.assign(app2.MockFiles));
  306. await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
  307. await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
  308. });
  309. });
  310. describe('Get app config', () => {
  311. let app1: AppInfo;
  312. beforeEach(async () => {
  313. const app1create = await createApp({ installed: true });
  314. app1 = app1create.appInfo;
  315. // @ts-ignore
  316. fs.__createMockFiles(Object.assign(app1create.MockFiles));
  317. });
  318. it('Should correctly get app config', async () => {
  319. const app = await AppsService.getApp(app1.id);
  320. expect(app).toBeDefined();
  321. expect(app.config).toStrictEqual({ TEST_FIELD: 'test' });
  322. expect(app.id).toBe(app1.id);
  323. expect(app.status).toBe(AppStatusEnum.RUNNING);
  324. });
  325. it('Should return default values if app is not installed', async () => {
  326. const appconfig = await AppsService.getApp('test-app2');
  327. expect(appconfig).toBeDefined();
  328. expect(appconfig.id).toBe('test-app2');
  329. expect(appconfig.config).toStrictEqual({});
  330. expect(appconfig.status).toBe(AppStatusEnum.MISSING);
  331. });
  332. });
  333. describe('List apps', () => {
  334. let app1: AppInfo;
  335. let app2: AppInfo;
  336. beforeEach(async () => {
  337. const app1create = await createApp({ installed: true });
  338. const app2create = await createApp({});
  339. app1 = app1create.appInfo;
  340. app2 = app2create.appInfo;
  341. // @ts-ignore
  342. fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
  343. });
  344. it('Should correctly list apps sorted by name', async () => {
  345. const { apps } = await AppsService.listApps();
  346. const sortedApps = [app1, app2].sort((a, b) => a.name.localeCompare(b.name));
  347. expect(apps).toBeDefined();
  348. expect(apps.length).toBe(2);
  349. expect(apps.length).toBe(2);
  350. expect(apps[0].id).toBe(sortedApps[0].id);
  351. expect(apps[1].id).toBe(sortedApps[1].id);
  352. expect(apps[0].description).toBe('md desc');
  353. });
  354. it('Should not list apps that have supportedArchitectures and are not supported', async () => {
  355. // Arrange
  356. setConfig('architecture', AppSupportedArchitecturesEnum.ARM64);
  357. const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
  358. // @ts-ignore
  359. fs.__createMockFiles(Object.assign(app3.MockFiles));
  360. // Act
  361. const { apps } = await AppsService.listApps();
  362. // Assert
  363. expect(apps).toBeDefined();
  364. expect(apps.length).toBe(0);
  365. });
  366. it('Should list apps that have supportedArchitectures and are supported', async () => {
  367. // Arrange
  368. setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
  369. const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
  370. // @ts-ignore
  371. fs.__createMockFiles(Object.assign(app3.MockFiles));
  372. // Act
  373. const { apps } = await AppsService.listApps();
  374. // Assert
  375. expect(apps).toBeDefined();
  376. expect(apps.length).toBe(1);
  377. });
  378. it('Should list apps that have no supportedArchitectures specified', async () => {
  379. // Arrange
  380. setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
  381. const app3 = await createApp({ supportedArchitectures: undefined });
  382. // @ts-ignore
  383. fs.__createMockFiles(Object.assign(app3.MockFiles));
  384. // Act
  385. const { apps } = await AppsService.listApps();
  386. // Assert
  387. expect(apps).toBeDefined();
  388. expect(apps.length).toBe(1);
  389. });
  390. });
  391. describe('Start all apps', () => {
  392. let app1: AppInfo;
  393. let app2: AppInfo;
  394. beforeEach(async () => {
  395. const app1create = await createApp({ installed: true });
  396. const app2create = await createApp({ installed: true });
  397. app1 = app1create.appInfo;
  398. app2 = app2create.appInfo;
  399. // @ts-ignore
  400. fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
  401. });
  402. it('Should correctly start all apps', async () => {
  403. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  404. await AppsService.startAllApps();
  405. expect(spy.mock.calls.length).toBe(2);
  406. expect(spy.mock.calls).toEqual([
  407. [EventTypes.APP, ['start', app1.id]],
  408. [EventTypes.APP, ['start', app2.id]],
  409. ]);
  410. });
  411. it('Should not start app which has not status RUNNING', async () => {
  412. const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
  413. await createApp({ installed: true, status: AppStatusEnum.STOPPED });
  414. await AppsService.startAllApps();
  415. const apps = await App.find();
  416. expect(spy.mock.calls.length).toBe(2);
  417. expect(apps.length).toBe(3);
  418. });
  419. it('Should put app status to STOPPED if start script fails', async () => {
  420. // Arrange
  421. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
  422. // Act
  423. await AppsService.startAllApps();
  424. const apps = await App.find();
  425. // Assert
  426. expect(apps.length).toBe(2);
  427. expect(apps[0].status).toBe(AppStatusEnum.STOPPED);
  428. expect(apps[1].status).toBe(AppStatusEnum.STOPPED);
  429. });
  430. });
  431. describe('Update app', () => {
  432. let app1: AppInfo;
  433. beforeEach(async () => {
  434. const app1create = await createApp({ installed: true });
  435. app1 = app1create.appInfo;
  436. // @ts-ignore
  437. fs.__createMockFiles(Object.assign(app1create.MockFiles));
  438. });
  439. it('Should correctly update app', async () => {
  440. await App.update({ id: app1.id }, { version: 0 });
  441. const app = await AppsService.updateApp(app1.id);
  442. expect(app).toBeDefined();
  443. expect(app.config).toStrictEqual({ TEST_FIELD: 'test' });
  444. expect(app.version).toBe(app1.tipi_version);
  445. expect(app.status).toBe(AppStatusEnum.STOPPED);
  446. });
  447. it("Should throw if app doesn't exist", async () => {
  448. await expect(AppsService.updateApp('test-app2')).rejects.toThrow('App test-app2 not found');
  449. });
  450. it('Should throw if update script fails', async () => {
  451. // Arrange
  452. EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
  453. await expect(AppsService.updateApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to update\nstdout: error`);
  454. const app = await App.findOne({ where: { id: app1.id } });
  455. expect(app!.status).toBe(AppStatusEnum.STOPPED);
  456. });
  457. });