apps.helpers.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import { faker } from '@faker-js/faker';
  2. import fs from 'fs-extra';
  3. import { DataSource } from 'typeorm';
  4. import logger from '../../../config/logger/logger';
  5. import { setupConnection, teardownConnection } from '../../../test/connection';
  6. import App from '../app.entity';
  7. import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
  8. import { AppInfo } from '../apps.types';
  9. import { createApp } from './apps.factory';
  10. jest.mock('fs-extra');
  11. jest.mock('child_process');
  12. jest.mock('internal-ip');
  13. jest.mock('tcp-port-used', () => ({
  14. check: (port: number) => {
  15. if (port === 53) {
  16. return true;
  17. }
  18. return false;
  19. },
  20. }));
  21. let db: DataSource | null = null;
  22. const TEST_SUITE = 'appshelpers';
  23. beforeAll(async () => {
  24. db = await setupConnection(TEST_SUITE);
  25. });
  26. afterAll(async () => {
  27. await db?.destroy();
  28. await teardownConnection(TEST_SUITE);
  29. });
  30. describe('checkAppRequirements', () => {
  31. let app1: AppInfo;
  32. beforeEach(async () => {
  33. const app1create = await createApp({});
  34. app1 = app1create.appInfo;
  35. // @ts-ignore
  36. fs.__createMockFiles(app1create.MockFiles);
  37. });
  38. it('should return true if there are no particular requirement', async () => {
  39. const ivValid = await checkAppRequirements(app1.id);
  40. expect(ivValid).toBe(true);
  41. });
  42. it('Should throw an error if app does not exist', async () => {
  43. await expect(checkAppRequirements('not-existing-app')).rejects.toThrow('App not-existing-app not found');
  44. });
  45. it('Should return false if a required port is in use', async () => {
  46. const { appInfo, MockFiles } = await createApp({ requiredPort: 53 });
  47. // @ts-ignore
  48. fs.__createMockFiles(MockFiles);
  49. const ivValid = await checkAppRequirements(appInfo.id);
  50. expect(ivValid).toBe(false);
  51. });
  52. });
  53. describe('getEnvMap', () => {
  54. let app1: AppInfo;
  55. beforeEach(async () => {
  56. const app1create = await createApp({ installed: true });
  57. app1 = app1create.appInfo;
  58. // @ts-ignore
  59. fs.__createMockFiles(app1create.MockFiles);
  60. });
  61. it('should return a map of env vars', async () => {
  62. const envMap = await getEnvMap(app1.id);
  63. expect(envMap.get('TEST_FIELD')).toBe('test');
  64. });
  65. });
  66. describe('checkEnvFile', () => {
  67. let app1: AppInfo;
  68. beforeEach(async () => {
  69. const app1create = await createApp({ installed: true });
  70. app1 = app1create.appInfo;
  71. // @ts-ignore
  72. fs.__createMockFiles(app1create.MockFiles);
  73. });
  74. it('Should not throw if all required fields are present', async () => {
  75. await checkEnvFile(app1.id);
  76. });
  77. it('Should throw if a required field is missing', () => {
  78. const newAppEnv = 'APP_PORT=test\n';
  79. fs.writeFileSync(`/app/storage/app-data/${app1.id}/app.env`, newAppEnv);
  80. try {
  81. checkEnvFile(app1.id);
  82. expect(true).toBe(false);
  83. } catch (e: any) {
  84. expect(e).toBeDefined();
  85. expect(e.message).toBe('New info needed. App config needs to be updated');
  86. }
  87. });
  88. });
  89. describe('Test: generateEnvFile', () => {
  90. let app1: AppInfo;
  91. let appEntity1: App;
  92. beforeEach(async () => {
  93. const app1create = await createApp({ installed: true });
  94. app1 = app1create.appInfo;
  95. appEntity1 = app1create.appEntity;
  96. // @ts-ignore
  97. fs.__createMockFiles(app1create.MockFiles);
  98. });
  99. it('Should generate an env file', async () => {
  100. const fakevalue = faker.random.alphaNumeric(10);
  101. generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: fakevalue } }));
  102. const envmap = await getEnvMap(app1.id);
  103. expect(envmap.get('TEST_FIELD')).toBe(fakevalue);
  104. });
  105. it('Should automatically generate value for random field', async () => {
  106. const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
  107. // @ts-ignore
  108. fs.__createMockFiles(MockFiles);
  109. generateEnvFile(appEntity);
  110. const envmap = await getEnvMap(appInfo.id);
  111. expect(envmap.get('RANDOM_FIELD')).toBeDefined();
  112. expect(envmap.get('RANDOM_FIELD')).toHaveLength(32);
  113. });
  114. it('Should not re-generate random field if it already exists', async () => {
  115. const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
  116. // @ts-ignore
  117. fs.__createMockFiles(MockFiles);
  118. const randomField = faker.random.alphaNumeric(32);
  119. fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
  120. generateEnvFile(appEntity);
  121. const envmap = await getEnvMap(appInfo.id);
  122. expect(envmap.get('RANDOM_FIELD')).toBe(randomField);
  123. });
  124. it('Should throw an error if required field is not provided', async () => {
  125. try {
  126. generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: undefined } }));
  127. expect(true).toBe(false);
  128. } catch (e: any) {
  129. expect(e).toBeDefined();
  130. expect(e.message).toBe('Variable TEST_FIELD is required');
  131. }
  132. });
  133. it('Should throw an error if app does not exist', async () => {
  134. try {
  135. generateEnvFile(Object.assign(appEntity1, { id: 'not-existing-app' }));
  136. expect(true).toBe(false);
  137. } catch (e: any) {
  138. expect(e).toBeDefined();
  139. expect(e.message).toBe('App not-existing-app not found');
  140. }
  141. });
  142. it('Should add APP_EXPOSED to env file', async () => {
  143. const domain = faker.internet.domainName();
  144. const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, exposed: true, domain });
  145. // @ts-ignore
  146. fs.__createMockFiles(MockFiles);
  147. generateEnvFile(appEntity);
  148. const envmap = await getEnvMap(appInfo.id);
  149. expect(envmap.get('APP_EXPOSED')).toBe('true');
  150. expect(envmap.get('APP_DOMAIN')).toBe(domain);
  151. });
  152. it('Should not add APP_EXPOSED if domain is not provided', async () => {
  153. const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, exposed: true });
  154. // @ts-ignore
  155. fs.__createMockFiles(MockFiles);
  156. generateEnvFile(appEntity);
  157. const envmap = await getEnvMap(appInfo.id);
  158. expect(envmap.get('APP_EXPOSED')).toBeUndefined();
  159. });
  160. it('Should not add APP_EXPOSED if app is not exposed', async () => {
  161. const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, domain: faker.internet.domainName() });
  162. // @ts-ignore
  163. fs.__createMockFiles(MockFiles);
  164. generateEnvFile(appEntity);
  165. const envmap = await getEnvMap(appInfo.id);
  166. expect(envmap.get('APP_EXPOSED')).toBeUndefined();
  167. expect(envmap.get('APP_DOMAIN')).toBe(`192.168.1.10:${appInfo.port}`);
  168. });
  169. it('Should create app folder if it does not exist', async () => {
  170. const { appEntity, appInfo, MockFiles } = await createApp({ installed: true });
  171. // @ts-ignore
  172. fs.__createMockFiles(MockFiles);
  173. fs.rmSync(`/app/storage/app-data/${appInfo.id}`, { recursive: true });
  174. generateEnvFile(appEntity);
  175. expect(fs.existsSync(`/app/storage/app-data/${appInfo.id}`)).toBe(true);
  176. });
  177. });
  178. describe('getAvailableApps', () => {
  179. beforeEach(async () => {
  180. const app1create = await createApp({ installed: true });
  181. const app2create = await createApp({});
  182. // @ts-ignore
  183. fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
  184. });
  185. it('Should return all available apps', async () => {
  186. const availableApps = await getAvailableApps();
  187. expect(availableApps.length).toBe(2);
  188. });
  189. });
  190. describe('Test: getAppInfo', () => {
  191. let app1: AppInfo;
  192. beforeEach(async () => {
  193. const app1create = await createApp({ installed: false });
  194. app1 = app1create.appInfo;
  195. // @ts-ignore
  196. fs.__createMockFiles(app1create.MockFiles);
  197. });
  198. it('Should return app info', async () => {
  199. const appInfo = await getAppInfo(app1.id);
  200. expect(appInfo?.id).toBe(app1.id);
  201. });
  202. it('Should take config.json locally if app is installed', async () => {
  203. const { appInfo, MockFiles, appEntity } = await createApp({ installed: true });
  204. // @ts-ignore
  205. fs.__createMockFiles(MockFiles);
  206. const newConfig = {
  207. id: faker.random.alphaNumeric(32),
  208. };
  209. fs.writeFileSync(`/runtipi/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
  210. const app = await getAppInfo(appInfo.id, appEntity.status);
  211. expect(app?.id).toEqual(newConfig.id);
  212. });
  213. it('Should take config.json from repo if app is not installed', async () => {
  214. const { appInfo, MockFiles, appEntity } = await createApp({ installed: false });
  215. // @ts-ignore
  216. fs.__createMockFiles(MockFiles);
  217. const newConfig = {
  218. id: faker.random.alphaNumeric(32),
  219. available: true,
  220. };
  221. fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
  222. const app = await getAppInfo(appInfo.id, appEntity.status);
  223. expect(app?.id).toEqual(newConfig.id);
  224. });
  225. it('Should return null if app is not available', async () => {
  226. const { appInfo, MockFiles, appEntity } = await createApp({ installed: false });
  227. // @ts-ignore
  228. fs.__createMockFiles(MockFiles);
  229. const newConfig = {
  230. id: faker.random.alphaNumeric(32),
  231. available: false,
  232. };
  233. fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
  234. const app = await getAppInfo(appInfo.id, appEntity.status);
  235. expect(app).toBeNull();
  236. });
  237. it('Should throw if something goes wrong', async () => {
  238. const log = jest.spyOn(logger, 'error');
  239. const spy = jest.spyOn(fs, 'existsSync').mockImplementation(() => {
  240. throw new Error('Something went wrong');
  241. });
  242. const { appInfo, MockFiles, appEntity } = await createApp({ installed: false });
  243. // @ts-ignore
  244. fs.__createMockFiles(MockFiles);
  245. const newConfig = {
  246. id: faker.random.alphaNumeric(32),
  247. available: false,
  248. };
  249. fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
  250. try {
  251. await getAppInfo(appInfo.id, appEntity.status);
  252. expect(true).toBe(false);
  253. } catch (e: any) {
  254. expect(e.message).toBe(`Error loading app: ${appInfo.id}`);
  255. expect(log).toBeCalledWith(`Error loading app: ${appInfo.id}`);
  256. }
  257. spy.mockRestore();
  258. log.mockRestore();
  259. });
  260. it('Should return null if app does not exist', async () => {
  261. const app = await getAppInfo(faker.random.word());
  262. expect(app).toBeNull();
  263. });
  264. });
  265. describe('getUpdateInfo', () => {
  266. let app1: AppInfo;
  267. beforeEach(async () => {
  268. const app1create = await createApp({ installed: true });
  269. app1 = app1create.appInfo;
  270. // @ts-ignore
  271. fs.__createMockFiles(app1create.MockFiles);
  272. });
  273. it('Should return update info', async () => {
  274. const updateInfo = await getUpdateInfo(app1.id);
  275. expect(updateInfo?.latest).toBe(app1.tipi_version);
  276. expect(updateInfo?.current).toBe(1);
  277. });
  278. it('Should return null if app is not installed', async () => {
  279. const updateInfo = await getUpdateInfo(faker.random.word());
  280. expect(updateInfo).toBeNull();
  281. });
  282. });