Преглед изворни кода

chore: cleanup system-api from now un-used files

Nicolas Meienberger пре 2 година
родитељ
комит
36a6483ff7
40 измењених фајлова са 2 додато и 3535 уклоњено
  1. 0 26
      packages/system-api/src/config/datasource.ts
  2. 0 4
      packages/system-api/src/core/config/TipiConfig.ts
  3. 0 181
      packages/system-api/src/core/updates/__tests__/v040.test.ts
  4. 0 48
      packages/system-api/src/core/updates/recover-migrations.ts
  5. 0 6
      packages/system-api/src/core/updates/run.ts
  6. 0 92
      packages/system-api/src/core/updates/v040.ts
  7. 0 98
      packages/system-api/src/modules/apps/__tests__/apps.factory.ts
  8. 0 513
      packages/system-api/src/modules/apps/__tests__/apps.helpers.test.ts
  9. 0 513
      packages/system-api/src/modules/apps/__tests__/apps.resolver.test.ts
  10. 0 646
      packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
  11. 0 77
      packages/system-api/src/modules/apps/app.entity.ts
  12. 0 6
      packages/system-api/src/modules/apps/app.types.ts
  13. 0 236
      packages/system-api/src/modules/apps/apps.helpers.ts
  14. 0 63
      packages/system-api/src/modules/apps/apps.resolver.ts
  15. 0 328
      packages/system-api/src/modules/apps/apps.service.ts
  16. 0 167
      packages/system-api/src/modules/apps/apps.types.ts
  17. 0 16
      packages/system-api/src/modules/auth/__tests__/user.factory.ts
  18. 0 28
      packages/system-api/src/modules/auth/user.entity.ts
  19. 0 24
      packages/system-api/src/modules/system/update.entity.ts
  20. 0 13
      packages/system-api/src/schema.ts
  21. 2 47
      packages/system-api/src/server.ts
  22. 0 48
      packages/system-api/src/test/connection.ts
  23. 0 27
      packages/system-api/src/test/gcall.ts
  24. 0 21
      packages/system-api/src/test/mutations/index.ts
  25. 0 25
      packages/system-api/src/test/mutations/installApp.graphql
  26. 0 6
      packages/system-api/src/test/mutations/login.graphql
  27. 0 6
      packages/system-api/src/test/mutations/register.graphql
  28. 0 31
      packages/system-api/src/test/mutations/startApp.graphql
  29. 0 30
      packages/system-api/src/test/mutations/stopApp.graphql
  30. 0 30
      packages/system-api/src/test/mutations/uninstallApp.graphql
  31. 0 30
      packages/system-api/src/test/mutations/updateApp.graphql
  32. 0 30
      packages/system-api/src/test/mutations/updateAppConfig.graphql
  33. 0 29
      packages/system-api/src/test/queries/getApp.graphql
  34. 0 17
      packages/system-api/src/test/queries/index.ts
  35. 0 29
      packages/system-api/src/test/queries/installedApps.graphql
  36. 0 3
      packages/system-api/src/test/queries/isConfigured.graphql
  37. 0 23
      packages/system-api/src/test/queries/listAppInfos.graphql
  38. 0 6
      packages/system-api/src/test/queries/me.graphql
  39. 0 6
      packages/system-api/src/test/queries/refreshToken.graphql
  40. 0 6
      packages/system-api/src/test/queries/version.graphql

+ 0 - 26
packages/system-api/src/config/datasource.ts

@@ -1,26 +0,0 @@
-import * as dotenv from 'dotenv';
-import { DataSource } from 'typeorm';
-import App from '../modules/apps/app.entity';
-import User from '../modules/auth/user.entity';
-import Update from '../modules/system/update.entity';
-import { __prod__ } from './constants/constants';
-
-if (process.env.NODE_ENV !== 'production') {
-  dotenv.config({ path: '.env.dev' });
-} else {
-  dotenv.config({ path: '.env' });
-}
-const { POSTGRES_DBNAME = '', POSTGRES_HOST = '', POSTGRES_USERNAME = '', POSTGRES_PASSWORD = '' } = process.env;
-
-export default new DataSource({
-  type: 'postgres',
-  host: POSTGRES_HOST,
-  database: POSTGRES_DBNAME,
-  username: POSTGRES_USERNAME,
-  password: POSTGRES_PASSWORD,
-  port: 5432,
-  logging: !__prod__,
-  synchronize: false,
-  entities: [App, User, Update],
-  migrations: [`${process.cwd()}/dist/config/migrations/*.js`],
-});

+ 0 - 4
packages/system-api/src/core/config/TipiConfig.ts

@@ -2,7 +2,6 @@ import { z } from 'zod';
 import * as dotenv from 'dotenv';
 import fs from 'fs-extra';
 import { readJsonFile } from '../../modules/fs/fs.helpers';
-import { AppSupportedArchitecturesEnum } from '../../modules/apps/apps.types';
 
 if (process.env.NODE_ENV !== 'production') {
   dotenv.config({ path: '.env.dev' });
@@ -23,14 +22,12 @@ const {
   DOMAIN = '',
   STORAGE_PATH = '/runtipi',
   REDIS_HOST = 'tipi-redis',
-  ARCHITECTURE = 'amd64',
 } = process.env;
 
 const configSchema = z.object({
   NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
   REDIS_HOST: z.string(),
   status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
-  architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
   logs: z.object({
     LOGS_FOLDER: z.string(),
     LOGS_APP: z.string(),
@@ -62,7 +59,6 @@ class Config {
       },
       REDIS_HOST,
       NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
-      architecture: ARCHITECTURE as z.infer<typeof configSchema>['architecture'],
       rootFolder: '/runtipi',
       internalIp: INTERNAL_IP,
       version: TIPI_VERSION,

+ 0 - 181
packages/system-api/src/core/updates/__tests__/v040.test.ts

@@ -1,181 +0,0 @@
-import { faker } from '@faker-js/faker';
-import fs from 'fs-extra';
-import { DataSource } from 'typeorm';
-import logger from '../../../config/logger/logger';
-import App from '../../../modules/apps/app.entity';
-import { AppInfo, AppStatusEnum } from '../../../modules/apps/apps.types';
-import { createApp } from '../../../modules/apps/__tests__/apps.factory';
-import User from '../../../modules/auth/user.entity';
-import Update, { UpdateStatusEnum } from '../../../modules/system/update.entity';
-import { setupConnection, teardownConnection } from '../../../test/connection';
-import { getConfig } from '../../config/TipiConfig';
-import { updateV040 } from '../v040';
-
-jest.mock('fs');
-
-let db: DataSource | null = null;
-const TEST_SUITE = 'updatev040';
-
-beforeAll(async () => {
-  db = await setupConnection(TEST_SUITE);
-});
-
-beforeEach(async () => {
-  jest.resetModules();
-  jest.resetAllMocks();
-  await App.clear();
-  await Update.clear();
-});
-
-afterAll(async () => {
-  await db?.destroy();
-  await teardownConnection(TEST_SUITE);
-});
-
-const createAppState = (apps: string[]) => JSON.stringify({ installed: apps.join(' ') });
-const createUserState = (users: { email: string; password: string }[]) => JSON.stringify(users);
-
-describe('No state/apps.json', () => {
-  it('Should do nothing and create the update with status SUCCES', async () => {
-    await updateV040();
-
-    const update = await Update.findOne({ where: { name: 'v040' } });
-
-    expect(update).toBeDefined();
-    expect(update?.status).toBe(UpdateStatusEnum.SUCCESS);
-
-    const apps = await App.find();
-
-    expect(apps).toHaveLength(0);
-  });
-
-  it('Should not run the update if already done', async () => {
-    const spy = jest.spyOn(logger, 'info');
-
-    await updateV040();
-    await updateV040();
-
-    expect(spy).toHaveBeenCalledTimes(1);
-    expect(spy).toHaveBeenCalledWith('Update v040 already applied');
-  });
-});
-
-describe('State/apps.json exists with no installed app', () => {
-  beforeEach(async () => {
-    const { MockFiles } = await createApp({});
-    MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createAppState([]);
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-  });
-
-  it('Should do nothing and create the update with status SUCCES', async () => {
-    await updateV040();
-    const update = await Update.findOne({ where: { name: 'v040' } });
-
-    expect(update).toBeDefined();
-    expect(update?.status).toBe(UpdateStatusEnum.SUCCESS);
-
-    const apps = await App.find();
-    expect(apps).toHaveLength(0);
-  });
-
-  it('Should delete state file after update', async () => {
-    await updateV040();
-    expect(fs.existsSync('/runtipi/state/apps.json')).toBe(false);
-  });
-});
-
-describe('State/apps.json exists with one installed app', () => {
-  let app1: AppInfo | null = null;
-  beforeEach(async () => {
-    const { MockFiles, appInfo } = await createApp({});
-    app1 = appInfo;
-    MockFiles['/runtipi/state/apps.json'] = createAppState([appInfo.id]);
-    MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
-    MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-  });
-
-  it('Should create a new app and update', async () => {
-    await updateV040();
-
-    const app = await App.findOne({ where: { id: app1?.id } });
-    const update = await Update.findOne({ where: { name: 'v040' } });
-
-    expect(app).toBeDefined();
-    expect(app?.status).toBe(AppStatusEnum.STOPPED);
-    expect(update).toBeDefined();
-    expect(update?.status).toBe('SUCCESS');
-  });
-
-  it("Should correctly pick up app's variables from existing .env file", async () => {
-    await updateV040();
-    const app = await App.findOne({ where: { id: app1?.id } });
-
-    expect(app?.config).toStrictEqual({ TEST_FIELD: 'test' });
-  });
-
-  it('Should not try to migrate app if it already exists', async () => {
-    const { MockFiles, appInfo } = await createApp({ installed: true });
-    app1 = appInfo;
-    MockFiles['/runtipi/state/apps.json'] = createAppState([appInfo.id]);
-    MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
-    MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    await updateV040();
-    const spy = jest.spyOn(logger, 'info');
-
-    expect(spy).toHaveBeenCalledTimes(1);
-    expect(spy).toHaveBeenCalledWith('App already migrated');
-  });
-});
-
-describe('State/users.json exists with no user', () => {
-  beforeEach(async () => {
-    const { MockFiles } = await createApp({});
-    MockFiles[`${getConfig().rootFolder}/state/users.json`] = createUserState([]);
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-  });
-
-  it('Should do nothing and create the update with status SUCCES', async () => {
-    await updateV040();
-    const update = await Update.findOne({ where: { name: 'v040' } });
-
-    expect(update).toBeDefined();
-    expect(update?.status).toBe(UpdateStatusEnum.SUCCESS);
-
-    const apps = await App.find();
-    expect(apps).toHaveLength(0);
-  });
-
-  it('Should delete state file after update', async () => {
-    await updateV040();
-    expect(fs.existsSync('/runtipi/state/apps.json')).toBe(false);
-  });
-});
-
-describe('State/users.json exists with one user', () => {
-  const email = faker.internet.email();
-
-  beforeEach(async () => {
-    const MockFiles: Record<string, string> = {};
-    MockFiles[`/runtipi/state/users.json`] = createUserState([{ email, password: faker.internet.password() }]);
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-  });
-
-  it('Should create a new user and update', async () => {
-    await updateV040();
-
-    const user = await User.findOne({ where: { username: email } });
-    const update = await Update.findOne({ where: { name: 'v040' } });
-
-    expect(user).toBeDefined();
-    expect(update).toBeDefined();
-    expect(update?.status).toBe('SUCCESS');
-  });
-});

+ 0 - 48
packages/system-api/src/core/updates/recover-migrations.ts

@@ -1,48 +0,0 @@
-import { BaseEntity, DataSource, DeepPartial } from 'typeorm';
-import logger from '../../config/logger/logger';
-import App from '../../modules/apps/app.entity';
-import User from '../../modules/auth/user.entity';
-import Update from '../../modules/system/update.entity';
-
-const createUser = async (user: DeepPartial<BaseEntity>): Promise<void> => {
-  await User.create(user).save();
-};
-
-const createApp = async (app: DeepPartial<BaseEntity>): Promise<void> => {
-  await App.create(app).save();
-};
-
-const createUpdate = async (update: DeepPartial<BaseEntity>): Promise<void> => {
-  await Update.create(update).save();
-};
-
-const recover = async (datasource: DataSource) => {
-  logger.info('Recovering broken database');
-
-  const queryRunner = datasource.createQueryRunner();
-  const apps = await queryRunner.query('SELECT * FROM app');
-  const users = await queryRunner.query('SELECT * FROM "user"');
-  const updates = await queryRunner.query('SELECT * FROM update');
-
-  // drop database
-  await datasource.dropDatabase();
-
-  logger.info('running migrations');
-  await datasource.runMigrations();
-
-  // recreate users
-  await Promise.all(users.map(createUser));
-
-  // create apps
-  await Promise.all(apps.map(createApp));
-
-  // create updates
-  await Promise.all(updates.map(createUpdate));
-
-  logger.info(`Users recovered ${users.length}`);
-  logger.info(`Apps recovered ${apps.length}`);
-  logger.info(`Updates recovered ${updates.length}`);
-  logger.info('Database fully recovered');
-};
-
-export default recover;

+ 0 - 6
packages/system-api/src/core/updates/run.ts

@@ -1,6 +0,0 @@
-import { updateV040 } from './v040';
-
-export const runUpdates = async (): Promise<void> => {
-  // v040: Update to 0.4.0
-  await updateV040();
-};

+ 0 - 92
packages/system-api/src/core/updates/v040.ts

@@ -1,92 +0,0 @@
-import { z } from 'zod';
-import logger from '../../config/logger/logger';
-import App from '../../modules/apps/app.entity';
-import { appInfoSchema } from '../../modules/apps/apps.helpers';
-import { AppStatusEnum } from '../../modules/apps/apps.types';
-import User from '../../modules/auth/user.entity';
-import { deleteFolder, fileExists, readFile, readJsonFile } from '../../modules/fs/fs.helpers';
-import Update, { UpdateStatusEnum } from '../../modules/system/update.entity';
-import { getConfig } from '../config/TipiConfig';
-
-const appStateSchema = z.object({ installed: z.string().optional().default('') });
-const userStateSchema = z.object({ email: z.string(), password: z.string() }).array();
-
-const UPDATE_NAME = 'v040';
-
-const migrateApp = async (appId: string): Promise<void> => {
-  const app = await App.findOne({ where: { id: appId } });
-
-  if (!app) {
-    const envFile = readFile(`/app/storage/app-data/${appId}/app.env`).toString();
-    const envVars = envFile.split('\n');
-    const envVarsMap = new Map<string, string>();
-
-    envVars.forEach((envVar) => {
-      const [key, value] = envVar.split('=');
-      envVarsMap.set(key, value);
-    });
-
-    const form: Record<string, string> = {};
-
-    const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
-    const parsedConfig = appInfoSchema.safeParse(configFile);
-
-    if (parsedConfig.success) {
-      parsedConfig.data.form_fields.forEach((field) => {
-        const envVar = field.env_variable;
-        const envVarValue = envVarsMap.get(envVar);
-
-        if (envVarValue) {
-          form[field.env_variable] = envVarValue;
-        }
-      });
-
-      await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
-    }
-  } else {
-    logger.info('App already migrated');
-  }
-};
-
-const migrateUser = async (user: { email: string; password: string }): Promise<void> => {
-  await User.create({ username: user.email.trim().toLowerCase(), password: user.password }).save();
-};
-
-export const updateV040 = async (): Promise<void> => {
-  try {
-    const update = await Update.findOne({ where: { name: UPDATE_NAME } });
-
-    if (update) {
-      logger.info(`Update ${UPDATE_NAME} already applied`);
-      return;
-    }
-
-    // Migrate apps
-    if (fileExists('/runtipi/state/apps.json')) {
-      const state = readJsonFile('/runtipi/state/apps.json');
-      const parsedState = appStateSchema.safeParse(state);
-
-      if (parsedState.success) {
-        const installed: string[] = parsedState.data.installed.split(' ').filter(Boolean);
-        await Promise.all(installed.map((appId) => migrateApp(appId)));
-        deleteFolder('/runtipi/state/apps.json');
-      }
-    }
-
-    // Migrate users
-    if (fileExists('/runtipi/state/users.json')) {
-      const state = readJsonFile('/runtipi/state/users.json');
-      const parsedState = userStateSchema.safeParse(state);
-
-      if (parsedState.success) {
-        await Promise.all(parsedState.data.map((user) => migrateUser(user)));
-        deleteFolder('/runtipi/state/users.json');
-      }
-    }
-
-    await Update.create({ name: UPDATE_NAME, status: UpdateStatusEnum.SUCCESS }).save();
-  } catch (error) {
-    logger.error(error);
-    await Update.create({ name: UPDATE_NAME, status: UpdateStatusEnum.FAILED }).save();
-  }
-};

+ 0 - 98
packages/system-api/src/modules/apps/__tests__/apps.factory.ts

@@ -1,98 +0,0 @@
-import { faker } from '@faker-js/faker';
-import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from '../apps.types';
-import App from '../app.entity';
-import { appInfoSchema } from '../apps.helpers';
-
-interface IProps {
-  installed?: boolean;
-  status?: AppStatusEnum;
-  requiredPort?: number;
-  randomField?: boolean;
-  exposed?: boolean;
-  domain?: string;
-  exposable?: boolean;
-  supportedArchitectures?: AppSupportedArchitecturesEnum[];
-}
-
-type CreateConfigParams = {
-  id?: string;
-};
-
-const createAppConfig = (props?: CreateConfigParams): AppInfo =>
-  appInfoSchema.parse({
-    id: props?.id || faker.random.alphaNumeric(32),
-    available: true,
-    port: faker.datatype.number({ min: 30, max: 65535 }),
-    name: faker.random.alphaNumeric(32),
-    description: faker.random.alphaNumeric(32),
-    tipi_version: 1,
-    short_desc: faker.random.alphaNumeric(32),
-    author: faker.random.alphaNumeric(32),
-    source: faker.internet.url(),
-    categories: [AppCategoriesEnum.AUTOMATION],
-  });
-
-const createApp = async (props: IProps) => {
-  const { installed = false, status = AppStatusEnum.RUNNING, randomField = false, exposed = false, domain = '', exposable = false, supportedArchitectures } = props;
-
-  const categories = Object.values(AppCategoriesEnum);
-
-  const appInfo: AppInfo = {
-    id: faker.random.word().toLowerCase().trim(),
-    port: faker.datatype.number({ min: 3000, max: 5000 }),
-    available: true,
-    form_fields: [
-      {
-        type: FieldTypes.text,
-        label: faker.random.word(),
-        required: true,
-        env_variable: 'TEST_FIELD',
-      },
-    ],
-    name: faker.random.word(),
-    description: faker.random.words(),
-    tipi_version: faker.datatype.number({ min: 1, max: 10 }),
-    short_desc: faker.random.words(),
-    author: faker.name.firstName(),
-    source: faker.internet.url(),
-    categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
-    exposable,
-    supported_architectures: supportedArchitectures,
-  };
-
-  if (randomField) {
-    appInfo.form_fields?.push({
-      type: FieldTypes.random,
-      label: faker.random.word(),
-      env_variable: 'RANDOM_FIELD',
-    });
-  }
-
-  const MockFiles: Record<string, string | string[]> = {};
-  MockFiles['/runtipi/.env'] = 'TEST=test';
-  MockFiles['/runtipi/repos/repo-id'] = '';
-  MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
-  MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
-  MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
-
-  let appEntity = new App();
-  if (installed) {
-    appEntity = await App.create({
-      id: appInfo.id,
-      config: { TEST_FIELD: 'test' },
-      status,
-      exposed,
-      domain,
-      version: 1,
-    }).save();
-
-    MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
-    MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
-    MockFiles[`/runtipi/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
-    MockFiles[`/runtipi/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
-  }
-
-  return { appInfo, MockFiles, appEntity };
-};
-
-export { createApp, createAppConfig };

+ 0 - 513
packages/system-api/src/modules/apps/__tests__/apps.helpers.test.ts

@@ -1,513 +0,0 @@
-import { faker } from '@faker-js/faker';
-import fs from 'fs-extra';
-import { DataSource } from 'typeorm';
-import logger from '../../../config/logger/logger';
-import { setConfig } from '../../../core/config/TipiConfig';
-import { setupConnection, teardownConnection } from '../../../test/connection';
-import App from '../app.entity';
-import { checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
-import { AppInfo, AppSupportedArchitecturesEnum } from '../apps.types';
-import { createApp, createAppConfig } from './apps.factory';
-
-jest.mock('fs-extra');
-jest.mock('child_process');
-
-let db: DataSource | null = null;
-const TEST_SUITE = 'appshelperslegacy';
-
-beforeAll(async () => {
-  db = await setupConnection(TEST_SUITE);
-});
-
-afterAll(async () => {
-  await db?.destroy();
-  await teardownConnection(TEST_SUITE);
-});
-
-beforeEach(async () => {
-  jest.resetModules();
-  jest.resetAllMocks();
-  jest.restoreAllMocks();
-  await App.clear();
-});
-
-describe('checkAppRequirements', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({});
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('should return appInfo if there are no particular requirement', async () => {
-    const result = checkAppRequirements(app1.id);
-    expect(result.id).toEqual(app1.id);
-  });
-
-  it('Should throw an error if app does not exist', async () => {
-    try {
-      checkAppRequirements('notexisting');
-      expect(true).toBe(false);
-    } catch (e) {
-      // @ts-ignore
-      expect(e.message).toEqual('App notexisting has invalid config.json file');
-    }
-  });
-
-  it('Should throw if architecture is not supported', async () => {
-    setConfig('architecture', AppSupportedArchitecturesEnum.ARM64);
-    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    try {
-      checkAppRequirements(appInfo.id);
-      expect(true).toBe(false);
-    } catch (e) {
-      // @ts-ignore
-      expect(e.message).toEqual(`App ${appInfo.id} is not supported on this architecture`);
-    }
-  });
-});
-
-describe('getEnvMap', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('should return a map of env vars', async () => {
-    const envMap = await getEnvMap(app1.id);
-
-    expect(envMap.get('TEST_FIELD')).toBe('test');
-  });
-});
-
-describe('Test: checkEnvFile', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should not throw if all required fields are present', async () => {
-    await checkEnvFile(app1.id);
-  });
-
-  it('Should throw if a required field is missing', () => {
-    const newAppEnv = 'APP_PORT=test\n';
-    fs.writeFileSync(`/app/storage/app-data/${app1.id}/app.env`, newAppEnv);
-
-    try {
-      checkEnvFile(app1.id);
-      expect(true).toBe(false);
-    } catch (e: unknown) {
-      if (e instanceof Error) {
-        expect(e).toBeDefined();
-        expect(e.message).toBe('New info needed. App config needs to be updated');
-      } else {
-        fail('Should throw an error');
-      }
-    }
-  });
-
-  it('Should throw if config.json is incorrect', async () => {
-    // arrange
-    fs.writeFileSync(`/app/storage/app-data/${app1.id}/config.json`, 'invalid json');
-    const { appInfo } = await createApp({});
-
-    // act
-    try {
-      await checkEnvFile(appInfo.id);
-      expect(true).toBe(false);
-    } catch (e: unknown) {
-      if (e instanceof Error) {
-        expect(e).toBeDefined();
-        expect(e.message).toBe(`App ${appInfo.id} has invalid config.json file`);
-      } else {
-        fail('Should throw an error');
-      }
-    }
-  });
-});
-
-describe('Test: generateEnvFile', () => {
-  let app1: AppInfo;
-  let appEntity1: App;
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    appEntity1 = app1create.appEntity;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should generate an env file', async () => {
-    const fakevalue = faker.random.alphaNumeric(10);
-
-    generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: fakevalue } }));
-
-    const envmap = await getEnvMap(app1.id);
-
-    expect(envmap.get('TEST_FIELD')).toBe(fakevalue);
-  });
-
-  it('Should automatically generate value for random field', async () => {
-    const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    generateEnvFile(appEntity);
-
-    const envmap = await getEnvMap(appInfo.id);
-
-    expect(envmap.get('RANDOM_FIELD')).toBeDefined();
-    expect(envmap.get('RANDOM_FIELD')).toHaveLength(32);
-  });
-
-  it('Should not re-generate random field if it already exists', async () => {
-    const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, randomField: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const randomField = faker.random.alphaNumeric(32);
-
-    fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
-
-    generateEnvFile(appEntity);
-
-    const envmap = await getEnvMap(appInfo.id);
-
-    expect(envmap.get('RANDOM_FIELD')).toBe(randomField);
-  });
-
-  it('Should throw an error if required field is not provided', async () => {
-    try {
-      generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: undefined } }));
-      expect(true).toBe(false);
-    } catch (e: unknown) {
-      if (e instanceof Error) {
-        expect(e).toBeDefined();
-        expect(e.message).toBe('Variable TEST_FIELD is required');
-      } else {
-        fail('Should throw an error');
-      }
-    }
-  });
-
-  it('Should throw an error if app does not exist', async () => {
-    try {
-      generateEnvFile(Object.assign(appEntity1, { id: 'not-existing-app' }));
-      expect(true).toBe(false);
-    } catch (e: unknown) {
-      if (e instanceof Error) {
-        expect(e).toBeDefined();
-        expect(e.message).toBe('App not-existing-app has invalid config.json file');
-      } else {
-        fail('Should throw an error');
-      }
-    }
-  });
-
-  it('Should add APP_EXPOSED to env file', async () => {
-    const domain = faker.internet.domainName();
-    const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, exposed: true, domain });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    generateEnvFile(appEntity);
-
-    const envmap = await getEnvMap(appInfo.id);
-
-    expect(envmap.get('APP_EXPOSED')).toBe('true');
-    expect(envmap.get('APP_DOMAIN')).toBe(domain);
-  });
-
-  it('Should not add APP_EXPOSED if domain is not provided', async () => {
-    const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, exposed: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    generateEnvFile(appEntity);
-
-    const envmap = await getEnvMap(appInfo.id);
-
-    expect(envmap.get('APP_EXPOSED')).toBeUndefined();
-  });
-
-  it('Should not add APP_EXPOSED if app is not exposed', async () => {
-    const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, domain: faker.internet.domainName() });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    generateEnvFile(appEntity);
-
-    const envmap = await getEnvMap(appInfo.id);
-
-    expect(envmap.get('APP_EXPOSED')).toBeUndefined();
-    expect(envmap.get('APP_DOMAIN')).toBe(`localhost:${appInfo.port}`);
-  });
-
-  it('Should create app folder if it does not exist', async () => {
-    const { appEntity, appInfo, MockFiles } = await createApp({ installed: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    fs.rmSync(`/app/storage/app-data/${appInfo.id}`, { recursive: true });
-
-    generateEnvFile(appEntity);
-
-    expect(fs.existsSync(`/app/storage/app-data/${appInfo.id}`)).toBe(true);
-  });
-});
-
-describe('getAvailableApps', () => {
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    const app2create = await createApp({});
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
-  });
-
-  it('Should return all available apps', async () => {
-    const availableApps = await getAvailableApps();
-
-    expect(availableApps.length).toBe(2);
-  });
-
-  it('Should not return apps with invalid config.json', async () => {
-    const { appInfo: app1, MockFiles: MockFiles1 } = await createApp({ installed: true });
-    const { MockFiles: MockFiles2 } = await createApp({});
-    MockFiles1[`/runtipi/repos/repo-id/apps/${app1.id}/config.json`] = 'invalid json';
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(MockFiles1, MockFiles2));
-
-    const availableApps = await getAvailableApps();
-
-    expect(availableApps.length).toBe(1);
-  });
-});
-
-describe('Test: getAppInfo', () => {
-  let app1: AppInfo;
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: false });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should return app info', async () => {
-    const appInfo = await getAppInfo(app1.id);
-
-    expect(appInfo?.id).toBe(app1.id);
-  });
-
-  it('Should take config.json locally if app is installed', async () => {
-    const { appInfo, MockFiles, appEntity } = await createApp({ installed: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const newConfig = createAppConfig();
-
-    fs.writeFileSync(`/runtipi/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
-
-    const app = await getAppInfo(appInfo.id, appEntity.status);
-
-    expect(app?.id).toEqual(newConfig.id);
-  });
-
-  it('Should take config.json from repo if app is not installed', async () => {
-    const { appInfo, MockFiles, appEntity } = await createApp({ installed: false });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const newConfig = createAppConfig();
-
-    fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
-
-    const app = await getAppInfo(appInfo.id, appEntity.status);
-
-    expect(app?.id).toEqual(newConfig.id);
-  });
-
-  it('Should return null if app is not available', async () => {
-    const { appInfo, MockFiles, appEntity } = await createApp({ installed: false });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const newConfig = {
-      id: faker.random.alphaNumeric(32),
-      available: false,
-    };
-
-    fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
-
-    const app = await getAppInfo(appInfo.id, appEntity.status);
-
-    expect(app).toBeNull();
-  });
-
-  it('Should throw if something goes wrong', async () => {
-    const log = jest.spyOn(logger, 'error');
-    const spy = jest.spyOn(fs, 'existsSync').mockImplementation(() => {
-      throw new Error('Something went wrong');
-    });
-
-    const { appInfo, MockFiles, appEntity } = await createApp({ installed: false });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const newConfig = {
-      id: faker.random.alphaNumeric(32),
-      available: false,
-    };
-
-    fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
-
-    try {
-      await getAppInfo(appInfo.id, appEntity.status);
-      expect(true).toBe(false);
-    } catch (e: unknown) {
-      if (e instanceof Error) {
-        expect(e.message).toBe(`Error loading app: ${appInfo.id}`);
-        expect(log).toBeCalledWith(`Error loading app: ${appInfo.id}`);
-      } else {
-        expect(true).toBe(false);
-      }
-    }
-
-    spy.mockRestore();
-    log.mockRestore();
-  });
-
-  it('Should return null if app does not exist', async () => {
-    const app = await getAppInfo(faker.random.word());
-
-    expect(app).toBeNull();
-  });
-});
-
-describe('getUpdateInfo', () => {
-  let app1: AppInfo;
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should return update info', async () => {
-    const updateInfo = await getUpdateInfo(app1.id, 1);
-
-    expect(updateInfo?.latest).toBe(app1.tipi_version);
-    expect(updateInfo?.current).toBe(1);
-  });
-
-  it('Should return null if app is not installed', async () => {
-    const updateInfo = await getUpdateInfo(faker.random.word(), 1);
-
-    expect(updateInfo).toBeNull();
-  });
-
-  it('Should return null if config.json is invalid', async () => {
-    const { appInfo, MockFiles } = await createApp({ installed: true });
-    MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = 'invalid json';
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const updateInfo = await getUpdateInfo(appInfo.id, 1);
-
-    expect(updateInfo).toBeNull();
-  });
-
-  it('should return null if version is not provided', async () => {
-    // @ts-ignore
-    const updateInfo = await getUpdateInfo(app1.id);
-
-    expect(updateInfo).toBe(null);
-  });
-});
-
-describe('Test: ensureAppFolder', () => {
-  beforeEach(() => {
-    const mockFiles = {
-      [`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
-    };
-    // @ts-ignore
-    fs.__createMockFiles(mockFiles);
-  });
-
-  it('should copy the folder from repo', () => {
-    // Act
-    ensureAppFolder('test');
-
-    // Assert
-    const files = fs.readdirSync('/runtipi/apps/test');
-    expect(files).toEqual(['test.yml']);
-  });
-
-  it('should not copy the folder if it already exists', () => {
-    const mockFiles = {
-      [`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
-      '/runtipi/apps/test': ['docker-compose.yml'],
-      '/runtipi/apps/test/docker-compose.yml': 'test',
-    };
-
-    // @ts-ignore
-    fs.__createMockFiles(mockFiles);
-
-    // Act
-    ensureAppFolder('test');
-
-    // Assert
-    const files = fs.readdirSync('/runtipi/apps/test');
-    expect(files).toEqual(['docker-compose.yml']);
-  });
-
-  it('Should overwrite the folder if clean up is true', () => {
-    const mockFiles = {
-      [`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
-      '/runtipi/apps/test': ['docker-compose.yml'],
-      '/runtipi/apps/test/docker-compose.yml': 'test',
-    };
-
-    // @ts-ignore
-    fs.__createMockFiles(mockFiles);
-
-    // Act
-    ensureAppFolder('test', true);
-
-    // Assert
-    const files = fs.readdirSync('/runtipi/apps/test');
-    expect(files).toEqual(['test.yml']);
-  });
-
-  it('Should delete folder if it exists but has no docker-compose.yml file', () => {
-    // Arrange
-    const randomFileName = `${faker.random.word()}.yml`;
-    const mockFiles = {
-      [`/runtipi/repos/repo-id/apps/test`]: [randomFileName],
-      '/runtipi/apps/test': ['test.yml'],
-    };
-
-    // @ts-ignore
-    fs.__createMockFiles(mockFiles);
-
-    // Act
-    ensureAppFolder('test');
-
-    // Assert
-    const files = fs.readdirSync('/runtipi/apps/test');
-    expect(files).toEqual([randomFileName]);
-  });
-});

+ 0 - 513
packages/system-api/src/modules/apps/__tests__/apps.resolver.test.ts

@@ -1,513 +0,0 @@
-import { DataSource } from 'typeorm';
-import fs from 'fs-extra';
-import { faker } from '@faker-js/faker';
-import { setupConnection, teardownConnection } from '../../../test/connection';
-import { gcall } from '../../../test/gcall';
-import App from '../app.entity';
-import { getAppQuery, InstalledAppsQuery, listAppInfosQuery } from '../../../test/queries';
-import { createApp } from './apps.factory';
-import { AppInfo, AppStatusEnum, ListAppsResonse } from '../apps.types';
-import { createUser } from '../../auth/__tests__/user.factory';
-import User from '../../auth/user.entity';
-import { installAppMutation, startAppMutation, stopAppMutation, uninstallAppMutation, updateAppConfigMutation, updateAppMutation } from '../../../test/mutations';
-import EventDispatcher from '../../../core/config/EventDispatcher';
-
-jest.mock('fs');
-jest.mock('child_process');
-
-type TApp = App & {
-  info: AppInfo;
-};
-
-let db: DataSource | null = null;
-const TEST_SUITE = 'appsresolver';
-
-beforeAll(async () => {
-  db = await setupConnection(TEST_SUITE);
-});
-
-afterAll(async () => {
-  await db?.destroy();
-  await teardownConnection(TEST_SUITE);
-});
-
-beforeEach(async () => {
-  jest.resetModules();
-  jest.resetAllMocks();
-  jest.restoreAllMocks();
-  EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
-  await App.clear();
-  await User.clear();
-});
-
-describe('ListAppsInfos', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const { MockFiles, appInfo } = await createApp({});
-    app1 = appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-  });
-
-  it('Can list apps', async () => {
-    const { data } = await gcall<{ listAppsInfo: ListAppsResonse }>({ source: listAppInfosQuery });
-
-    expect(data?.listAppsInfo.apps.length).toBe(1);
-    expect(data?.listAppsInfo.total).toBe(1);
-
-    const app = data?.listAppsInfo.apps[0];
-
-    expect(app?.id).toBe(app1.id);
-    expect(app?.author).toBe(app1.author);
-    expect(app?.name).toBe(app1.name);
-    expect(app?.available).toBe(app1.available);
-  });
-});
-
-describe('GetApp', () => {
-  let app1: AppInfo;
-  let app2: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({});
-    const app2create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    app2 = app2create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
-  });
-
-  it('Can get app', async () => {
-    const { data } = await gcall<{ getApp: TApp }>({
-      source: getAppQuery,
-      variableValues: { id: app1.id },
-    });
-
-    expect(data?.getApp.info.id).toBe(app1.id);
-    expect(data?.getApp.status).toBe(AppStatusEnum.MISSING.toUpperCase());
-
-    const { data: data2 } = await gcall<{ getApp: TApp }>({
-      source: getAppQuery,
-      variableValues: { id: app2.id },
-    });
-
-    expect(data2?.getApp.info.id).toBe(app2.id);
-  });
-
-  it("Should return null info if app doesn't exist", async () => {
-    const { data } = await gcall<{ getApp: TApp }>({
-      source: getAppQuery,
-      variableValues: { id: 'not-existing' },
-    });
-
-    expect(data?.getApp.info).toBeNull();
-    expect(data?.getApp.status).toBe(AppStatusEnum.MISSING.toUpperCase());
-  });
-});
-
-describe('InstalledApps', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Can list installed apps', async () => {
-    const user = await createUser();
-
-    const { data } = await gcall<{ installedApps: TApp[] }>({ source: InstalledAppsQuery, userId: user.id });
-
-    expect(data?.installedApps.length).toBe(1);
-
-    const app = data?.installedApps[0];
-
-    expect(app?.id).toBe(app1.id);
-    expect(app?.info.author).toBe(app1.author);
-    expect(app?.info.name).toBe(app1.name);
-  });
-
-  it("Should return an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ installedApps: TApp[] }>({
-      source: InstalledAppsQuery,
-      userId: 1,
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.installedApps).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ installedApps: TApp[] }>({
-      source: InstalledAppsQuery,
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.installedApps).toBeUndefined();
-  });
-});
-
-describe('InstallApp', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({});
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Can install app', async () => {
-    const user = await createUser();
-
-    const { data } = await gcall<{ installApp: TApp }>({
-      source: installAppMutation,
-      userId: user.id,
-      variableValues: { input: { id: app1.id, form: { TEST_FIELD: 'hello' }, exposed: false, domain: '' } },
-    });
-
-    expect(data?.installApp.info.id).toBe(app1.id);
-    expect(data?.installApp.status).toBe(AppStatusEnum.RUNNING.toUpperCase());
-  });
-
-  it("Should return an error if app doesn't exist", async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ installApp: TApp }>({
-      source: installAppMutation,
-      userId: user.id,
-      variableValues: { input: { id: 'not-existing', form: { TEST_FIELD: 'hello' }, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe('App not-existing has invalid config.json file');
-    expect(data?.installApp).toBeUndefined();
-  });
-
-  it("Should throw an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ installApp: TApp }>({
-      source: installAppMutation,
-      variableValues: { input: { id: app1.id, form: { TEST_FIELD: 'hello' }, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.installApp).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ installApp: TApp }>({
-      source: installAppMutation,
-      variableValues: { input: { id: app1.id, form: { TEST_FIELD: 'hello' }, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.installApp).toBeUndefined();
-  });
-
-  it('Should throw an error if a required field is missing in form', async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ installApp: TApp }>({
-      source: installAppMutation,
-      userId: user.id,
-      variableValues: { input: { id: app1.id, form: {}, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe(`Variable ${app1.form_fields?.[0].env_variable} is required`);
-    expect(data?.installApp).toBeUndefined();
-  });
-});
-
-describe('StartApp', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ status: AppStatusEnum.STOPPED, installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Can start app', async () => {
-    const user = await createUser();
-
-    const { data } = await gcall<{ startApp: TApp }>({
-      source: startAppMutation,
-      userId: user.id,
-      variableValues: { id: app1.id },
-    });
-
-    expect(data?.startApp.info.id).toBe(app1.id);
-    expect(data?.startApp.status).toBe(AppStatusEnum.RUNNING.toUpperCase());
-  });
-
-  it("Should return an error if app doesn't exist", async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ startApp: TApp }>({
-      source: startAppMutation,
-      userId: user.id,
-      variableValues: { id: 'not-existing' },
-    });
-
-    expect(errors?.[0].message).toBe('App not-existing not found');
-    expect(data?.startApp).toBeUndefined();
-  });
-
-  it("Should throw an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ startApp: TApp }>({
-      source: startAppMutation,
-      userId: 0,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.startApp).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ startApp: TApp }>({
-      source: startAppMutation,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.startApp).toBeUndefined();
-  });
-});
-
-describe('StopApp', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ status: AppStatusEnum.RUNNING, installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Can stop app', async () => {
-    const user = await createUser();
-
-    const { data } = await gcall<{ stopApp: TApp }>({
-      source: stopAppMutation,
-      userId: user.id,
-      variableValues: { id: app1.id },
-    });
-
-    expect(data?.stopApp.info.id).toBe(app1.id);
-    expect(data?.stopApp.status).toBe(AppStatusEnum.STOPPED.toUpperCase());
-  });
-
-  it("Should return an error if app doesn't exist", async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ stopApp: TApp }>({
-      source: stopAppMutation,
-      userId: user.id,
-      variableValues: { id: 'not-existing' },
-    });
-
-    expect(errors?.[0].message).toBe('App not-existing not found');
-    expect(data?.stopApp).toBeUndefined();
-  });
-
-  it("Should throw an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ stopApp: TApp }>({
-      source: stopAppMutation,
-      userId: 0,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.stopApp).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ stopApp: TApp }>({
-      source: stopAppMutation,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.stopApp).toBeUndefined();
-  });
-});
-
-describe('UninstallApp', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ status: AppStatusEnum.STOPPED, installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should uninstall app', async () => {
-    const user = await createUser();
-
-    const { data } = await gcall<{ uninstallApp: TApp }>({
-      source: uninstallAppMutation,
-      userId: user.id,
-      variableValues: { id: app1.id },
-    });
-
-    expect(data?.uninstallApp.info.id).toBe(app1.id);
-    expect(data?.uninstallApp.status).toBe(AppStatusEnum.MISSING.toUpperCase());
-  });
-
-  it("Should return an error if app doesn't exist", async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ uninstallApp: TApp }>({
-      source: uninstallAppMutation,
-      userId: user.id,
-      variableValues: { id: 'not-existing' },
-    });
-
-    expect(errors?.[0].message).toBe('App not-existing not found');
-    expect(data?.uninstallApp).toBeUndefined();
-  });
-
-  it("Should throw an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ uninstallApp: TApp }>({
-      source: uninstallAppMutation,
-      userId: 0,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.uninstallApp).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ uninstallApp: TApp }>({
-      source: uninstallAppMutation,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.uninstallApp).toBeUndefined();
-  });
-});
-
-describe('UpdateAppConfig', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ status: AppStatusEnum.STOPPED, installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should update app config', async () => {
-    const user = await createUser();
-
-    const word = faker.random.word();
-
-    const { data } = await gcall<{ updateAppConfig: TApp }>({
-      source: updateAppConfigMutation,
-      userId: user.id,
-      variableValues: { input: { id: app1.id, form: { TEST_FIELD: word }, exposed: false, domain: '' } },
-    });
-
-    expect(data?.updateAppConfig.info.id).toBe(app1.id);
-    expect(data?.updateAppConfig.config.TEST_FIELD).toBe(word);
-  });
-
-  it("Should return an error if app doesn't exist", async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ updateAppConfig: TApp }>({
-      source: updateAppConfigMutation,
-      userId: user.id,
-      variableValues: { input: { id: 'not-existing', form: { TEST_FIELD: faker.random.word() }, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe('App not-existing not found');
-    expect(data?.updateAppConfig).toBeUndefined();
-  });
-
-  it("Should throw an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ updateAppConfig: TApp }>({
-      source: updateAppConfigMutation,
-      userId: 0,
-      variableValues: { input: { id: app1.id, form: { TEST_FIELD: faker.random.word() }, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.updateAppConfig).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ updateAppConfig: TApp }>({
-      source: updateAppConfigMutation,
-      variableValues: { input: { id: app1.id, form: { TEST_FIELD: faker.random.word() }, exposed: false, domain: '' } },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.updateAppConfig).toBeUndefined();
-  });
-});
-
-describe('UpdateApp', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ status: AppStatusEnum.STOPPED, installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(app1create.MockFiles);
-  });
-
-  it('Should update app', async () => {
-    const user = await createUser();
-
-    const { data } = await gcall<{ updateApp: TApp }>({
-      source: updateAppMutation,
-      userId: user.id,
-      variableValues: { id: app1.id },
-    });
-
-    expect(data?.updateApp.info.id).toBe(app1.id);
-    expect(data?.updateApp.info.name).toBe(data?.updateApp.info.name);
-  });
-
-  it("Should return an error if app doesn't exist", async () => {
-    const user = await createUser();
-
-    const { data, errors } = await gcall<{ updateApp: TApp }>({
-      source: updateAppMutation,
-      userId: user.id,
-      variableValues: { id: 'not-existing' },
-    });
-
-    expect(errors?.[0].message).toBe('App not-existing not found');
-    expect(data?.updateApp).toBeUndefined();
-  });
-
-  it("Should throw an error if user doesn't exist", async () => {
-    const { data, errors } = await gcall<{ updateApp: TApp }>({
-      source: updateAppMutation,
-      userId: 0,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.updateApp).toBeUndefined();
-  });
-
-  it('Should throw an error if no userId is provided', async () => {
-    const { data, errors } = await gcall<{ updateApp: TApp }>({
-      source: updateAppMutation,
-      variableValues: { id: app1.id },
-    });
-
-    expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
-    expect(data?.updateApp).toBeUndefined();
-  });
-});

+ 0 - 646
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -1,646 +0,0 @@
-import fs from 'fs-extra';
-import { DataSource } from 'typeorm';
-import AppsService from '../apps.service';
-import { AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum } from '../apps.types';
-import App from '../app.entity';
-import { createApp } from './apps.factory';
-import { setupConnection, teardownConnection } from '../../../test/connection';
-import { getEnvMap } from '../apps.helpers';
-import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
-import { setConfig } from '../../../core/config/TipiConfig';
-
-jest.mock('fs-extra');
-jest.mock('child_process');
-
-let db: DataSource | null = null;
-const TEST_SUITE = 'appsservicelegacy';
-
-beforeAll(async () => {
-  db = await setupConnection(TEST_SUITE);
-});
-
-beforeEach(async () => {
-  jest.resetModules();
-  jest.resetAllMocks();
-  jest.restoreAllMocks();
-  EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
-  await App.clear();
-});
-
-afterAll(async () => {
-  await db?.destroy();
-  await teardownConnection(TEST_SUITE);
-});
-
-describe('Install app', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const { MockFiles, appInfo } = await createApp({});
-    app1 = appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-  });
-
-  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' });
-    const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
-
-    expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${app1.port}`);
-  });
-
-  it('Should add app in database', async () => {
-    await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
-
-    const app = await App.findOne({ where: { id: app1.id } });
-
-    expect(app).toBeDefined();
-    expect(app?.id).toBe(app1.id);
-    expect(app?.config).toStrictEqual({ TEST_FIELD: 'test' });
-    expect(app?.status).toBe(AppStatusEnum.RUNNING);
-  });
-
-  it('Should start app if already installed', async () => {
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-
-    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[0]).toEqual([EventTypes.APP, ['install', app1.id]]);
-    expect(spy.mock.calls[1]).toEqual([EventTypes.APP, ['start', app1.id]]);
-
-    spy.mockRestore();
-  });
-
-  it('Should delete app if install script fails', async () => {
-    // Arrange
-    EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: '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 } });
-
-    expect(app).toBeNull();
-  });
-
-  it('Should throw if required form fields are missing', async () => {
-    await expect(AppsService.installApp(app1.id, {})).rejects.toThrowError('Variable TEST_FIELD is required');
-  });
-
-  it('Correctly generates a random value if the field has a "random" type', async () => {
-    const { appInfo, MockFiles } = await createApp({ randomField: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    await AppsService.installApp(appInfo.id, { TEST_FIELD: 'yolo' });
-    const envMap = getEnvMap(appInfo.id);
-
-    expect(envMap.get('RANDOM_FIELD')).toBeDefined();
-    expect(envMap.get('RANDOM_FIELD')).toHaveLength(32);
-  });
-
-  it('Should correctly copy app from repos to apps folder', async () => {
-    await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
-    const appFolder = fs.readdirSync(`/runtipi/apps/${app1.id}`);
-
-    expect(appFolder).toBeDefined();
-    expect(appFolder.indexOf('docker-compose.yml')).toBeGreaterThanOrEqual(0);
-  });
-
-  it('Should cleanup any app folder existing before install', async () => {
-    const { MockFiles, appInfo } = await createApp({});
-    app1 = appInfo;
-    MockFiles[`/runtipi/apps/${appInfo.id}/docker-compose.yml`] = 'test';
-    MockFiles[`/runtipi/apps/${appInfo.id}/test.yml`] = 'test';
-    MockFiles[`/runtipi/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
-
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    expect(fs.existsSync(`/runtipi/apps/${app1.id}/test.yml`)).toBe(true);
-
-    await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
-
-    expect(fs.existsSync(`/runtipi/apps/${app1.id}/test.yml`)).toBe(false);
-    expect(fs.existsSync(`/runtipi/apps/${app1.id}/docker-compose.yml`)).toBe(true);
-  });
-
-  it('Should throw if app is exposed and domain is not provided', async () => {
-    await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required if app is exposed');
-  });
-
-  it('Should throw if app is exposed and config does not allow it', async () => {
-    await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
-  });
-
-  it('Should throw if app is exposed and domain is not valid', async () => {
-    const { MockFiles, appInfo } = await createApp({ exposable: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
-  });
-
-  it('Should throw if app is exposed and domain is already used', async () => {
-    const app2 = await createApp({ exposable: true });
-    const app3 = await createApp({ exposable: true });
-    // @ts-ignore
-    fs.__createMockFiles({ ...app2.MockFiles, ...app3.MockFiles });
-
-    await AppsService.installApp(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
-
-    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}`);
-  });
-
-  it('Should throw if architecure is not supported', async () => {
-    // arrange
-    setConfig('architecture', AppSupportedArchitecturesEnum.AMD64);
-    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} is not supported on this architecture`);
-  });
-
-  it('Can install if architecture is supported', async () => {
-    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
-    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM, AppSupportedArchitecturesEnum.ARM64] });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
-    const app = await App.findOne({ where: { id: appInfo.id } });
-
-    expect(app).toBeDefined();
-  });
-
-  it('Can install if no architecture is specified', async () => {
-    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
-    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: undefined });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
-    const app = await App.findOne({ where: { id: appInfo.id } });
-
-    expect(app).toBeDefined();
-  });
-
-  it('Should throw if config.json is not valid', async () => {
-    // arrange
-    const { MockFiles, appInfo } = await createApp({});
-    MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = 'test';
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    // act & assert
-    await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} has invalid config.json file`);
-  });
-
-  it('Should throw if config.json is not valid after folder copy', async () => {
-    // arrange
-    jest.spyOn(fs, 'copySync').mockImplementationOnce(() => {});
-    const { MockFiles, appInfo } = await createApp({});
-    MockFiles[`/runtipi/apps/${appInfo.id}/config.json`] = 'test';
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    // act & assert
-    await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} has invalid config.json file`);
-  });
-});
-
-describe('Uninstall app', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles));
-  });
-
-  it('App should be installed by default', async () => {
-    // Act
-    const app = await App.findOne({ where: { id: app1.id } });
-
-    // Assert
-    expect(app).toBeDefined();
-    expect(app?.id).toBe(app1.id);
-    expect(app?.status).toBe(AppStatusEnum.RUNNING);
-  });
-
-  it('Should correctly remove app from database', async () => {
-    // Act
-    await AppsService.uninstallApp(app1.id);
-    const app = await App.findOne({ where: { id: app1.id } });
-
-    // Assert
-    expect(app).toBeNull();
-  });
-
-  it('Should stop app if it is running', async () => {
-    // Arrange
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-
-    // Act
-    await AppsService.uninstallApp(app1.id);
-
-    // Assert
-    expect(spy.mock.calls.length).toBe(2);
-    expect(spy.mock.calls[0]).toEqual([EventTypes.APP, ['stop', app1.id]]);
-    expect(spy.mock.calls[1]).toEqual([EventTypes.APP, ['uninstall', app1.id]]);
-
-    spy.mockRestore();
-  });
-
-  it('Should throw if app is not installed', async () => {
-    // Act & Assert
-    await expect(AppsService.uninstallApp('any')).rejects.toThrowError('App any not found');
-  });
-
-  it('Should throw if uninstall script fails', async () => {
-    // Arrange
-    EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
-    await App.update({ id: app1.id }, { status: AppStatusEnum.UPDATING });
-
-    // Act & Assert
-    await expect(AppsService.uninstallApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to uninstall\nstdout: test`);
-    const app = await App.findOne({ where: { id: app1.id } });
-    expect(app?.status).toBe(AppStatusEnum.STOPPED);
-  });
-});
-
-describe('Start app', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles));
-  });
-
-  it('Should correctly dispatch event', async () => {
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-
-    await AppsService.startApp(app1.id);
-
-    expect(spy.mock.lastCall).toEqual([EventTypes.APP, ['start', app1.id]]);
-
-    spy.mockRestore();
-  });
-
-  it('Should throw if app is not installed', async () => {
-    await expect(AppsService.startApp('any')).rejects.toThrowError('App any not found');
-  });
-
-  it('Should restart if app is already running', async () => {
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-
-    await AppsService.startApp(app1.id);
-    expect(spy.mock.calls.length).toBe(1);
-    await AppsService.startApp(app1.id);
-    expect(spy.mock.calls.length).toBe(2);
-
-    spy.mockRestore();
-  });
-
-  it('Regenerate env file', async () => {
-    fs.writeFileSync(`/app/storage/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000');
-
-    await AppsService.startApp(app1.id);
-
-    const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
-
-    expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${app1.port}`);
-  });
-
-  it('Should throw if start script fails', async () => {
-    // Arrange
-    EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
-
-    // 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 } });
-    expect(app?.status).toBe(AppStatusEnum.STOPPED);
-  });
-});
-
-describe('Stop app', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles));
-  });
-
-  it('Should correctly dispatch stop event', async () => {
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-
-    await AppsService.stopApp(app1.id);
-
-    expect(spy.mock.lastCall).toEqual([EventTypes.APP, ['stop', app1.id]]);
-  });
-
-  it('Should throw if app is not installed', async () => {
-    await expect(AppsService.stopApp('any')).rejects.toThrowError('App any not found');
-  });
-
-  it('Should throw if stop script fails', async () => {
-    // Arrange
-    EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'test' });
-
-    // 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 } });
-    expect(app?.status).toBe(AppStatusEnum.RUNNING);
-  });
-});
-
-describe('Update app config', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles));
-  });
-
-  it('Should correctly update app config', async () => {
-    await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
-
-    const envFile = fs.readFileSync(`/app/storage/app-data/${app1.id}/app.env`).toString();
-
-    expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${app1.port}`);
-  });
-
-  it('Should throw if required field is missing', async () => {
-    await expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: '' })).rejects.toThrowError('Variable TEST_FIELD is required');
-  });
-
-  it('Should throw if app is not installed', async () => {
-    await expect(AppsService.updateAppConfig('test-app-2', { test: 'test' })).rejects.toThrowError('App test-app-2 not found');
-  });
-
-  it('Should not recreate random field if already present in .env', async () => {
-    const { appInfo, MockFiles } = await createApp({ randomField: true, installed: true });
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    const envFile = fs.readFileSync(`/app/storage/app-data/${appInfo.id}/app.env`).toString();
-    fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
-
-    await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
-
-    const envMap = getEnvMap(appInfo.id);
-
-    expect(envMap.get('RANDOM_FIELD')).toBe('test');
-  });
-
-  it('Should throw if app is exposed and domain is not provided', () => expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required'));
-
-  it('Should throw if app is exposed and domain is not valid', () =>
-    expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid'));
-
-  it('Should throw if app is exposed and config does not allow it', () =>
-    expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`));
-
-  it('Should throw if app is exposed and domain is already used', async () => {
-    const app2 = await createApp({ exposable: true, installed: true });
-    const app3 = await createApp({ exposable: true, installed: true });
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app2.MockFiles, app3.MockFiles));
-
-    await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
-    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}`);
-  });
-
-  it('Should not throw if updating with same domain', async () => {
-    const app2 = await createApp({ exposable: true, installed: true });
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app2.MockFiles));
-
-    await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
-    await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
-  });
-
-  it('Should throw if app has invalid config.json', async () => {
-    const { appInfo, MockFiles } = await createApp({ installed: true });
-    MockFiles[`/runtipi/apps/${appInfo.id}/config.json`] = 'invalid json';
-
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(MockFiles));
-    fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/config.json`, 'test');
-
-    await expect(AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} has invalid config.json`);
-  });
-});
-
-describe('Get app config', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles));
-  });
-
-  it('Should correctly get app config', async () => {
-    const app = await AppsService.getApp(app1.id);
-
-    expect(app).toBeDefined();
-    expect(app.config).toStrictEqual({ TEST_FIELD: 'test' });
-    expect(app.id).toBe(app1.id);
-    expect(app.status).toBe(AppStatusEnum.RUNNING);
-  });
-
-  it('Should return default values if app is not installed', async () => {
-    const appconfig = await AppsService.getApp('test-app2');
-
-    expect(appconfig).toBeDefined();
-    expect(appconfig.id).toBe('test-app2');
-    expect(appconfig.config).toStrictEqual({});
-    expect(appconfig.status).toBe(AppStatusEnum.MISSING);
-  });
-});
-
-describe('List apps', () => {
-  let app1: AppInfo;
-  let app2: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    const app2create = await createApp({});
-    app1 = app1create.appInfo;
-    app2 = app2create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
-  });
-
-  it('Should correctly list apps sorted by name', async () => {
-    const { apps } = await AppsService.listApps();
-
-    const sortedApps = [app1, app2].sort((a, b) => a.name.localeCompare(b.name));
-
-    expect(apps).toBeDefined();
-    expect(apps.length).toBe(2);
-    expect(apps.length).toBe(2);
-    expect(apps[0].id).toBe(sortedApps[0].id);
-    expect(apps[1].id).toBe(sortedApps[1].id);
-    expect(apps[0].description).toBe('md desc');
-  });
-
-  it('Should not list apps that have supportedArchitectures and are not supported', async () => {
-    // Arrange
-    setConfig('architecture', AppSupportedArchitecturesEnum.ARM64);
-    const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app3.MockFiles));
-
-    // Act
-    const { apps } = await AppsService.listApps();
-
-    // Assert
-    expect(apps).toBeDefined();
-    expect(apps.length).toBe(0);
-  });
-
-  it('Should list apps that have supportedArchitectures and are supported', async () => {
-    // Arrange
-    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
-    const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app3.MockFiles));
-    // Act
-    const { apps } = await AppsService.listApps();
-
-    // Assert
-    expect(apps).toBeDefined();
-    expect(apps.length).toBe(1);
-  });
-
-  it('Should list apps that have no supportedArchitectures specified', async () => {
-    // Arrange
-    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
-    const app3 = await createApp({ supportedArchitectures: undefined });
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app3.MockFiles));
-
-    // Act
-    const { apps } = await AppsService.listApps();
-
-    // Assert
-    expect(apps).toBeDefined();
-    expect(apps.length).toBe(1);
-  });
-
-  it('Should not list app with invalid config.json', async () => {
-    // Arrange
-    const { MockFiles: mockApp1, appInfo } = await createApp({});
-    const { MockFiles: mockApp2 } = await createApp({});
-    const MockFiles = Object.assign(mockApp1, mockApp2);
-    MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = 'invalid json';
-    // @ts-ignore
-    fs.__createMockFiles(MockFiles);
-
-    // Act
-    const { apps } = await AppsService.listApps();
-
-    // Assert
-    expect(apps).toBeDefined();
-    expect(apps.length).toBe(1);
-  });
-});
-
-describe('Start all apps', () => {
-  let app1: AppInfo;
-  let app2: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    const app2create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    app2 = app2create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
-  });
-
-  it('Should correctly start all apps', async () => {
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-
-    await AppsService.startAllApps();
-
-    expect(spy.mock.calls.length).toBe(2);
-    expect(spy.mock.calls).toEqual([
-      [EventTypes.APP, ['start', app1.id]],
-      [EventTypes.APP, ['start', app2.id]],
-    ]);
-  });
-
-  it('Should not start app which has not status RUNNING', async () => {
-    const spy = jest.spyOn(eventDispatcher, 'dispatchEventAsync');
-    await createApp({ installed: true, status: AppStatusEnum.STOPPED });
-
-    await AppsService.startAllApps();
-    const apps = await App.find();
-
-    expect(spy.mock.calls.length).toBe(2);
-    expect(apps.length).toBe(3);
-  });
-
-  it('Should put app status to STOPPED if start script fails', async () => {
-    // Arrange
-    EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
-
-    // Act
-    await AppsService.startAllApps();
-    const apps = await App.find();
-
-    // Assert
-    expect(apps.length).toBe(2);
-    expect(apps[0].status).toBe(AppStatusEnum.STOPPED);
-    expect(apps[1].status).toBe(AppStatusEnum.STOPPED);
-  });
-});
-
-describe('Update app', () => {
-  let app1: AppInfo;
-
-  beforeEach(async () => {
-    const app1create = await createApp({ installed: true });
-    app1 = app1create.appInfo;
-    // @ts-ignore
-    fs.__createMockFiles(Object.assign(app1create.MockFiles));
-  });
-
-  it('Should correctly update app', async () => {
-    await App.update({ id: app1.id }, { version: 0 });
-
-    const app = await AppsService.updateApp(app1.id);
-
-    expect(app).toBeDefined();
-    expect(app.config).toStrictEqual({ TEST_FIELD: 'test' });
-    expect(app.version).toBe(app1.tipi_version);
-    expect(app.status).toBe(AppStatusEnum.STOPPED);
-  });
-
-  it("Should throw if app doesn't exist", async () => {
-    await expect(AppsService.updateApp('test-app2')).rejects.toThrow('App test-app2 not found');
-  });
-
-  it('Should throw if update script fails', async () => {
-    // Arrange
-    EventDispatcher.prototype.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: '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 } });
-    expect(app?.status).toBe(AppStatusEnum.STOPPED);
-  });
-});

+ 0 - 77
packages/system-api/src/modules/apps/app.entity.ts

@@ -1,77 +0,0 @@
-import { GraphQLJSONObject } from 'graphql-type-json';
-import { Field, ObjectType, registerEnumType } from 'type-graphql';
-import { BaseEntity, Column, CreateDateColumn, Entity, UpdateDateColumn } from 'typeorm';
-import { getAppInfo, getUpdateInfo } from './apps.helpers';
-import { AppInfo, AppStatusEnum } from './apps.types';
-
-registerEnumType(AppStatusEnum, {
-  name: 'AppStatusEnum',
-});
-
-@ObjectType()
-class UpdateInfo {
-  @Field(() => Number)
-  current!: number;
-
-  @Field(() => Number)
-  latest!: number;
-
-  @Field(() => String, { nullable: true })
-  dockerVersion?: string;
-}
-
-@ObjectType()
-@Entity()
-class App extends BaseEntity {
-  @Field(() => String)
-  @Column({ type: 'varchar', primary: true, unique: true })
-  id!: string;
-
-  @Field(() => AppStatusEnum)
-  @Column({ type: 'enum', enum: AppStatusEnum, default: AppStatusEnum.STOPPED, nullable: false })
-  status!: AppStatusEnum;
-
-  @Field(() => Date)
-  @Column({ type: 'timestamptz', nullable: true, default: () => 'CURRENT_TIMESTAMP' })
-  lastOpened!: Date;
-
-  @Field(() => Number)
-  @Column({ type: 'integer', default: 0, nullable: false })
-  numOpened!: number;
-
-  @Field(() => GraphQLJSONObject)
-  @Column({ type: 'jsonb', nullable: false })
-  config!: Record<string, string>;
-
-  @Field(() => Number, { nullable: true })
-  @Column({ type: 'integer', default: 1, nullable: false })
-  version!: number;
-
-  @Field(() => Date)
-  @CreateDateColumn()
-  createdAt!: Date;
-
-  @Field(() => Date)
-  @UpdateDateColumn()
-  updatedAt!: Date;
-
-  @Field(() => Boolean)
-  @Column({ type: 'boolean', default: false })
-  exposed!: boolean;
-
-  @Field(() => String, { nullable: true })
-  @Column({ type: 'varchar', nullable: true })
-  domain?: string;
-
-  @Field(() => AppInfo, { nullable: true })
-  info(): AppInfo | null {
-    return getAppInfo(this.id, this.status);
-  }
-
-  @Field(() => UpdateInfo, { nullable: true })
-  updateInfo(): Promise<UpdateInfo | null> {
-    return getUpdateInfo(this.id, this.version);
-  }
-}
-
-export default App;

+ 0 - 6
packages/system-api/src/modules/apps/app.types.ts

@@ -1,6 +0,0 @@
-export interface AppEntityType {
-  id: string;
-  config: Record<string, string>;
-  exposed: boolean;
-  domain?: string;
-}

+ 0 - 236
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -1,236 +0,0 @@
-import crypto from 'crypto';
-import fs from 'fs-extra';
-import { z } from 'zod';
-import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../fs/fs.helpers';
-import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from './apps.types';
-import logger from '../../config/logger/logger';
-import { getConfig } from '../../core/config/TipiConfig';
-import { AppEntityType } from './app.types';
-import { notEmpty } from '../../helpers/helpers';
-
-const formFieldSchema = z.object({
-  type: z.nativeEnum(FieldTypes),
-  label: z.string(),
-  placeholder: z.string().optional(),
-  max: z.number().optional(),
-  min: z.number().optional(),
-  hint: z.string().optional(),
-  required: z.boolean().optional().default(false),
-  env_variable: z.string(),
-});
-
-export const appInfoSchema = z.object({
-  id: z.string(),
-  available: z.boolean(),
-  port: z.number().min(1).max(65535),
-  name: z.string(),
-  description: z.string().optional().default(''),
-  version: z.string().optional().default('latest'),
-  tipi_version: z.number(),
-  short_desc: z.string(),
-  author: z.string(),
-  source: z.string(),
-  website: z.string().optional(),
-  categories: z.nativeEnum(AppCategoriesEnum).array(),
-  url_suffix: z.string().optional(),
-  form_fields: z.array(formFieldSchema).optional().default([]),
-  https: z.boolean().optional().default(false),
-  exposable: z.boolean().optional().default(false),
-  no_gui: z.boolean().optional().default(false),
-  supported_architectures: z.nativeEnum(AppSupportedArchitecturesEnum).array().optional(),
-});
-
-export const checkAppRequirements = (appName: string) => {
-  const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
-  const parsedConfig = appInfoSchema.safeParse(configFile);
-
-  if (!parsedConfig.success) {
-    throw new Error(`App ${appName} has invalid config.json file`);
-  }
-
-  if (parsedConfig.data.supported_architectures && !parsedConfig.data.supported_architectures.includes(getConfig().architecture)) {
-    throw new Error(`App ${appName} is not supported on this architecture`);
-  }
-
-  return parsedConfig.data;
-};
-
-export const getEnvMap = (appName: string): Map<string, string> => {
-  const envFile = readFile(`/app/storage/app-data/${appName}/app.env`).toString();
-  const envVars = envFile.split('\n');
-  const envVarsMap = new Map<string, string>();
-
-  envVars.forEach((envVar) => {
-    const [key, value] = envVar.split('=');
-    envVarsMap.set(key, value);
-  });
-
-  return envVarsMap;
-};
-
-export const checkEnvFile = (appName: string) => {
-  const configFile = readJsonFile(`/runtipi/apps/${appName}/config.json`);
-  const parsedConfig = appInfoSchema.safeParse(configFile);
-
-  if (!parsedConfig.success) {
-    throw new Error(`App ${appName} has invalid config.json file`);
-  }
-
-  const envMap = getEnvMap(appName);
-
-  parsedConfig.data.form_fields.forEach((field) => {
-    const envVar = field.env_variable;
-    const envVarValue = envMap.get(envVar);
-
-    if (!envVarValue && field.required) {
-      throw new Error('New info needed. App config needs to be updated');
-    }
-  });
-};
-
-const getEntropy = (name: string, length: number) => {
-  const hash = crypto.createHash('sha256');
-  hash.update(name + getSeed());
-  return hash.digest('hex').substring(0, length);
-};
-
-export const generateEnvFile = (app: AppEntityType) => {
-  const configFile = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
-  const parsedConfig = appInfoSchema.safeParse(configFile);
-
-  if (!parsedConfig.success) {
-    throw new Error(`App ${app.id} has invalid config.json file`);
-  }
-
-  const baseEnvFile = readFile('/runtipi/.env').toString();
-  let envFile = `${baseEnvFile}\nAPP_PORT=${parsedConfig.data.port}\n`;
-  const envMap = getEnvMap(app.id);
-
-  parsedConfig.data.form_fields.forEach((field) => {
-    const formValue = app.config[field.env_variable];
-    const envVar = field.env_variable;
-
-    if (formValue) {
-      envFile += `${envVar}=${formValue}\n`;
-    } else if (field.type === 'random') {
-      if (envMap.has(envVar)) {
-        envFile += `${envVar}=${envMap.get(envVar)}\n`;
-      } else {
-        const length = field.min || 32;
-        const randomString = getEntropy(field.env_variable, length);
-
-        envFile += `${envVar}=${randomString}\n`;
-      }
-    } else if (field.required) {
-      throw new Error(`Variable ${field.env_variable} is required`);
-    }
-  });
-
-  if (app.exposed && app.domain) {
-    envFile += 'APP_EXPOSED=true\n';
-    envFile += `APP_DOMAIN=${app.domain}\n`;
-    envFile += 'APP_PROTOCOL=https\n';
-  } else {
-    envFile += `APP_DOMAIN=${getConfig().internalIp}:${parsedConfig.data.port}\n`;
-  }
-
-  // Create app-data folder if it doesn't exist
-  if (!fs.existsSync(`/app/storage/app-data/${app.id}`)) {
-    fs.mkdirSync(`/app/storage/app-data/${app.id}`, { recursive: true });
-  }
-
-  writeFile(`/app/storage/app-data/${app.id}/app.env`, envFile);
-};
-
-export const getAvailableApps = async (): Promise<AppInfo[]> => {
-  const appsDir = readdirSync(`/runtipi/repos/${getConfig().appsRepoId}/apps`);
-
-  const skippedFiles = ['__tests__', 'docker-compose.common.yml', 'schema.json'];
-
-  const apps = appsDir
-    .map((app) => {
-      if (skippedFiles.includes(app)) return null;
-
-      const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
-      const parsedConfig = appInfoSchema.safeParse(configFile);
-
-      if (!parsedConfig.success) {
-        logger.error(`App ${JSON.stringify(app)} has invalid config.json`);
-      } else if (parsedConfig.data.available) {
-        const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${parsedConfig.data.id}/metadata/description.md`);
-        return { ...parsedConfig.data, description };
-      }
-
-      return null;
-    })
-    .filter(notEmpty);
-
-  return apps;
-};
-
-export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null => {
-  try {
-    // Check if app is installed
-    const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
-
-    if (installed && fileExists(`/runtipi/apps/${id}/config.json`)) {
-      const configFile = readJsonFile(`/runtipi/apps/${id}/config.json`);
-      const parsedConfig = appInfoSchema.safeParse(configFile);
-
-      if (parsedConfig.success && parsedConfig.data.available) {
-        const description = readFile(`/runtipi/apps/${id}/metadata/description.md`);
-        return { ...parsedConfig.data, description };
-      }
-    }
-
-    if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
-      const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
-      const parsedConfig = appInfoSchema.safeParse(configFile);
-
-      if (parsedConfig.success && parsedConfig.data.available) {
-        const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
-        return { ...parsedConfig.data, description };
-      }
-    }
-
-    return null;
-  } catch (e) {
-    logger.error(`Error loading app: ${id}`);
-    throw new Error(`Error loading app: ${id}`);
-  }
-};
-
-export const getUpdateInfo = async (id: string, version?: number) => {
-  const doesFileExist = fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}`);
-
-  if (!doesFileExist || !version) {
-    return null;
-  }
-
-  const repoConfig = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
-  const parsedConfig = appInfoSchema.safeParse(repoConfig);
-
-  if (parsedConfig.success) {
-    return {
-      current: version || 0,
-      latest: parsedConfig.data.tipi_version,
-      dockerVersion: parsedConfig.data.version,
-    };
-  }
-
-  return null;
-};
-
-export const ensureAppFolder = (appName: string, cleanup = false) => {
-  if (cleanup && fileExists(`/runtipi/apps/${appName}`)) {
-    deleteFolder(`/runtipi/apps/${appName}`);
-  }
-
-  if (!fileExists(`/runtipi/apps/${appName}/docker-compose.yml`)) {
-    if (fileExists(`/runtipi/apps/${appName}`)) {
-      deleteFolder(`/runtipi/apps/${appName}`);
-    }
-    // Copy from apps repo
-    fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/runtipi/apps/${appName}`);
-  }
-};

+ 0 - 63
packages/system-api/src/modules/apps/apps.resolver.ts

@@ -1,63 +0,0 @@
-import { Arg, Authorized, Mutation, Query, Resolver } from 'type-graphql';
-import AppsService from './apps.service';
-import { AppInputType, ListAppsResonse } from './apps.types';
-import App from './app.entity';
-
-@Resolver()
-export default class AppsResolver {
-  @Query(() => ListAppsResonse)
-  listAppsInfo(): Promise<ListAppsResonse> {
-    return AppsService.listApps();
-  }
-
-  @Query(() => App)
-  getApp(@Arg('id', () => String) id: string): Promise<App> {
-    return AppsService.getApp(id);
-  }
-
-  @Authorized()
-  @Query(() => [App])
-  async installedApps(): Promise<App[]> {
-    return App.find();
-  }
-
-  @Authorized()
-  @Mutation(() => App)
-  async installApp(@Arg('input', () => AppInputType) input: AppInputType): Promise<App> {
-    const { id, form, exposed, domain } = input;
-
-    return AppsService.installApp(id, form, exposed, domain);
-  }
-
-  @Authorized()
-  @Mutation(() => App)
-  async startApp(@Arg('id', () => String) id: string): Promise<App> {
-    return AppsService.startApp(id);
-  }
-
-  @Authorized()
-  @Mutation(() => App)
-  async stopApp(@Arg('id', () => String) id: string): Promise<App> {
-    return AppsService.stopApp(id);
-  }
-
-  @Authorized()
-  @Mutation(() => App)
-  async uninstallApp(@Arg('id', () => String) id: string): Promise<App> {
-    return AppsService.uninstallApp(id);
-  }
-
-  @Authorized()
-  @Mutation(() => App)
-  async updateAppConfig(@Arg('input', () => AppInputType) input: AppInputType): Promise<App> {
-    const { id, form, exposed, domain } = input;
-
-    return AppsService.updateAppConfig(id, form, exposed, domain);
-  }
-
-  @Authorized()
-  @Mutation(() => App)
-  async updateApp(@Arg('id', () => String) id: string): Promise<App> {
-    return AppsService.updateApp(id);
-  }
-}

+ 0 - 328
packages/system-api/src/modules/apps/apps.service.ts

@@ -1,328 +0,0 @@
-import validator from 'validator';
-import { Not } from 'typeorm';
-import { createFolder, readJsonFile } from '../fs/fs.helpers';
-import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, appInfoSchema } from './apps.helpers';
-import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
-import App from './app.entity';
-import logger from '../../config/logger/logger';
-import { getConfig } from '../../core/config/TipiConfig';
-import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
-
-const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
-const filterApp = (app: AppInfo): boolean => {
-  if (!app.supported_architectures) {
-    return true;
-  }
-
-  const arch = getConfig().architecture;
-  return app.supported_architectures.includes(arch);
-};
-
-const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(filterApp);
-
-/**
- * Start all apps which had the status RUNNING in the database
- */
-const startAllApps = async (): Promise<void> => {
-  const apps = await App.find({ where: { status: AppStatusEnum.RUNNING } });
-
-  await Promise.all(
-    apps.map(async (app) => {
-      // Regenerate env file
-      try {
-        ensureAppFolder(app.id);
-        generateEnvFile(app);
-        checkEnvFile(app.id);
-
-        await App.update({ id: app.id }, { status: AppStatusEnum.STARTING });
-
-        eventDispatcher.dispatchEventAsync(EventTypes.APP, ['start', app.id]).then(({ success }) => {
-          if (success) {
-            App.update({ id: app.id }, { status: AppStatusEnum.RUNNING });
-          } else {
-            App.update({ id: app.id }, { status: AppStatusEnum.STOPPED });
-          }
-        });
-      } catch (e) {
-        await App.update({ id: app.id }, { status: AppStatusEnum.STOPPED });
-        logger.error(e);
-      }
-    }),
-  );
-};
-
-/**
- * Start an app
- * @param appName - id of the app to start
- * @returns - the app entity
- */
-const startApp = async (appName: string): Promise<App> => {
-  let app = await App.findOne({ where: { id: appName } });
-
-  if (!app) {
-    throw new Error(`App ${appName} not found`);
-  }
-
-  ensureAppFolder(appName);
-  // Regenerate env file
-  generateEnvFile(app);
-  checkEnvFile(appName);
-
-  await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
-  const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['start', app.id]);
-
-  if (success) {
-    await App.update({ id: appName }, { status: AppStatusEnum.RUNNING });
-  } else {
-    await App.update({ id: appName }, { status: AppStatusEnum.STOPPED });
-    throw new Error(`App ${appName} failed to start\nstdout: ${stdout}`);
-  }
-
-  app = (await App.findOne({ where: { id: appName } })) as App;
-
-  return app;
-};
-
-/**
- * Given parameters, create a new app and start it
- * @param id - id of the app to stop
- * @param form - form data
- * @param exposed - if the app should be exposed
- * @param domain - domain to expose the app on
- * @returns - the app entity
- */
-const installApp = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string): Promise<App> => {
-  let app = await App.findOne({ where: { id } });
-
-  if (app) {
-    await startApp(id);
-  } else {
-    if (exposed && !domain) {
-      throw new Error('Domain is required if app is exposed');
-    }
-
-    if (domain && !validator.isFQDN(domain)) {
-      throw new Error(`Domain ${domain} is not valid`);
-    }
-
-    ensureAppFolder(id, true);
-    checkAppRequirements(id);
-
-    // Create app folder
-    createFolder(`/app/storage/app-data/${id}`);
-
-    const appInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
-    const parsedAppInfo = appInfoSchema.safeParse(appInfo);
-
-    if (!parsedAppInfo.success) {
-      throw new Error(`App ${id} has invalid config.json file`);
-    }
-
-    if (!parsedAppInfo.data.exposable && exposed) {
-      throw new Error(`App ${id} is not exposable`);
-    }
-
-    if (exposed) {
-      const appsWithSameDomain = await App.find({ where: { domain, exposed: true } });
-      if (appsWithSameDomain.length > 0) {
-        throw new Error(`Domain ${domain} already in use by app ${appsWithSameDomain[0].id}`);
-      }
-    }
-
-    app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form, version: parsedAppInfo.data.tipi_version, exposed: exposed || false, domain }).save();
-
-    // Create env file
-    generateEnvFile(app);
-
-    // Run script
-    const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['install', id]);
-
-    if (!success) {
-      await App.delete({ id });
-      throw new Error(`App ${id} failed to install\nstdout: ${stdout}`);
-    }
-  }
-
-  await App.update({ id }, { status: AppStatusEnum.RUNNING });
-  app = (await App.findOne({ where: { id } })) as App;
-
-  return app;
-};
-
-/**
- * List all apps available for installation
- * @returns - list of all apps available
- */
-const listApps = async (): Promise<ListAppsResonse> => {
-  const apps = await getAvailableApps();
-
-  const filteredApps = filterApps(apps);
-
-  return { apps: filteredApps, total: apps.length };
-};
-
-/**
- * Given parameters, updates an app config and regenerates the env file
- * @param id - id of the app to stop
- * @param form - form data
- * @param exposed - if the app should be exposed
- * @param domain - domain to expose the app on
- * @returns - the app entity
- */
-const updateAppConfig = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string): Promise<App> => {
-  if (exposed && !domain) {
-    throw new Error('Domain is required if app is exposed');
-  }
-
-  if (domain && !validator.isFQDN(domain)) {
-    throw new Error(`Domain ${domain} is not valid`);
-  }
-
-  let app = await App.findOne({ where: { id } });
-
-  if (!app) {
-    throw new Error(`App ${id} not found`);
-  }
-
-  const appInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
-  const parsedAppInfo = appInfoSchema.safeParse(appInfo);
-
-  if (!parsedAppInfo.success) {
-    throw new Error(`App ${id} has invalid config.json`);
-  }
-
-  if (!parsedAppInfo.data.exposable && exposed) {
-    throw new Error(`App ${id} is not exposable`);
-  }
-
-  if (exposed) {
-    const appsWithSameDomain = await App.find({ where: { domain, exposed: true, id: Not(id) } });
-    if (appsWithSameDomain.length > 0) {
-      throw new Error(`Domain ${domain} already in use by app ${appsWithSameDomain[0].id}`);
-    }
-  }
-
-  await App.update({ id }, { config: form, exposed: exposed || false, domain });
-  app = (await App.findOne({ where: { id } })) as App;
-
-  generateEnvFile(app);
-  app = (await App.findOne({ where: { id } })) as App;
-
-  return app;
-};
-
-/**
- * Stops an app
- * @param id - id of the app to stop
- * @returns - the app entity
- */
-const stopApp = async (id: string): Promise<App> => {
-  let app = await App.findOne({ where: { id } });
-
-  if (!app) {
-    throw new Error(`App ${id} not found`);
-  }
-
-  ensureAppFolder(id);
-  generateEnvFile(app);
-
-  // Run script
-  await App.update({ id }, { status: AppStatusEnum.STOPPING });
-
-  const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['stop', id]);
-
-  if (success) {
-    await App.update({ id }, { status: AppStatusEnum.STOPPED });
-  } else {
-    await App.update({ id }, { status: AppStatusEnum.RUNNING });
-    throw new Error(`App ${id} failed to stop\nstdout: ${stdout}`);
-  }
-
-  app = (await App.findOne({ where: { id } })) as App;
-
-  return app;
-};
-
-/**
- * Uninstalls an app
- * @param id - id of the app to uninstall
- * @returns - the app entity
- */
-const uninstallApp = async (id: string): Promise<App> => {
-  const app = await App.findOne({ where: { id } });
-
-  if (!app) {
-    throw new Error(`App ${id} not found`);
-  }
-  if (app.status === AppStatusEnum.RUNNING) {
-    await stopApp(id);
-  }
-
-  ensureAppFolder(id);
-  generateEnvFile(app);
-
-  await App.update({ id }, { status: AppStatusEnum.UNINSTALLING });
-
-  const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['uninstall', id]);
-
-  if (!success) {
-    await App.update({ id }, { status: AppStatusEnum.STOPPED });
-    throw new Error(`App ${id} failed to uninstall\nstdout: ${stdout}`);
-  }
-
-  await App.delete({ id });
-
-  return { id, status: AppStatusEnum.MISSING, config: {} } as App;
-};
-
-/**
- * Get an app entity
- * @param id - id of the app
- * @returns - the app entity
- */
-const getApp = async (id: string): Promise<App> => {
-  let app = await App.findOne({ where: { id } });
-
-  if (!app) {
-    app = { id, status: AppStatusEnum.MISSING, config: {}, exposed: false, domain: '' } as App;
-  }
-
-  return app;
-};
-
-/**
- * Updates an app to the latest version from repository
- * @param id - id of the app
- * @returns - the app entity
- */
-const updateApp = async (id: string) => {
-  let app = await App.findOne({ where: { id } });
-
-  if (!app) {
-    throw new Error(`App ${id} not found`);
-  }
-
-  ensureAppFolder(id);
-  generateEnvFile(app);
-
-  await App.update({ id }, { status: AppStatusEnum.UPDATING });
-
-  const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['update', id]);
-
-  if (success) {
-    const appInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
-    const parsedAppInfo = appInfoSchema.parse(appInfo);
-
-    await App.update({ id }, { status: AppStatusEnum.RUNNING, version: parsedAppInfo.tipi_version });
-  } else {
-    await App.update({ id }, { status: AppStatusEnum.STOPPED });
-    throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
-  }
-
-  await App.update({ id }, { status: AppStatusEnum.STOPPED });
-  app = (await App.findOne({ where: { id } })) as App;
-
-  return app;
-};
-
-export default { installApp, startApp, updateApp, listApps, getApp, updateAppConfig, stopApp, uninstallApp, startAllApps };

+ 0 - 167
packages/system-api/src/modules/apps/apps.types.ts

@@ -1,167 +0,0 @@
-import { Field, InputType, ObjectType, registerEnumType } from 'type-graphql';
-import { GraphQLJSONObject } from 'graphql-type-json';
-
-export enum AppCategoriesEnum {
-  NETWORK = 'network',
-  MEDIA = 'media',
-  DEVELOPMENT = 'development',
-  AUTOMATION = 'automation',
-  SOCIAL = 'social',
-  UTILITIES = 'utilities',
-  PHOTOGRAPHY = 'photography',
-  SECURITY = 'security',
-  FEATURED = 'featured',
-  BOOKS = 'books',
-  DATA = 'data',
-  MUSIC = 'music',
-  FINANCE = 'finance',
-  GAMING = 'gaming',
-}
-
-export enum FieldTypes {
-  text = 'text',
-  password = 'password',
-  email = 'email',
-  number = 'number',
-  fqdn = 'fqdn',
-  ip = 'ip',
-  fqdnip = 'fqdnip',
-  url = 'url',
-  random = 'random',
-}
-
-export enum AppStatusEnum {
-  RUNNING = 'running',
-  STOPPED = 'stopped',
-  INSTALLING = 'installing',
-  UNINSTALLING = 'uninstalling',
-  STOPPING = 'stopping',
-  STARTING = 'starting',
-  MISSING = 'missing',
-  UPDATING = 'updating',
-}
-
-export enum AppSupportedArchitecturesEnum {
-  ARM = 'arm',
-  ARM64 = 'arm64',
-  AMD64 = 'amd64',
-}
-
-registerEnumType(AppCategoriesEnum, {
-  name: 'AppCategoriesEnum',
-});
-
-registerEnumType(FieldTypes, {
-  name: 'FieldTypesEnum',
-});
-
-registerEnumType(AppSupportedArchitecturesEnum, {
-  name: 'AppSupportedArchitecturesEnum',
-});
-
-@ObjectType()
-class FormField {
-  @Field(() => FieldTypes)
-  type!: FieldTypes;
-
-  @Field(() => String)
-  label!: string;
-
-  @Field(() => Number, { nullable: true })
-  max?: number;
-
-  @Field(() => Number, { nullable: true })
-  min?: number;
-
-  @Field(() => String, { nullable: true })
-  hint?: string;
-
-  @Field(() => String, { nullable: true })
-  placeholder?: string;
-
-  @Field(() => Boolean, { nullable: true })
-  required?: boolean;
-
-  @Field(() => String)
-  env_variable!: string;
-}
-
-@ObjectType()
-class AppInfo {
-  @Field(() => String)
-  id!: string;
-
-  @Field(() => Boolean)
-  available!: boolean;
-
-  @Field(() => Number)
-  port!: number;
-
-  @Field(() => String)
-  name!: string;
-
-  @Field(() => String)
-  description!: string;
-
-  @Field(() => String, { nullable: true })
-  version?: string;
-
-  @Field(() => Number, { nullable: false })
-  tipi_version!: number;
-
-  @Field(() => String)
-  short_desc!: string;
-
-  @Field(() => String)
-  author!: string;
-
-  @Field(() => String)
-  source!: string;
-
-  @Field(() => [AppCategoriesEnum])
-  categories!: AppCategoriesEnum[];
-
-  @Field(() => String, { nullable: true })
-  url_suffix?: string;
-
-  @Field(() => [FormField])
-  form_fields?: FormField[];
-
-  @Field(() => Boolean, { nullable: true })
-  https?: boolean;
-
-  @Field(() => Boolean, { nullable: true })
-  exposable?: boolean;
-
-  @Field(() => Boolean, { nullable: true })
-  no_gui?: boolean;
-
-  @Field(() => [AppSupportedArchitecturesEnum], { nullable: true })
-  supported_architectures?: AppSupportedArchitecturesEnum[];
-}
-
-@ObjectType()
-class ListAppsResonse {
-  @Field(() => [AppInfo])
-  apps!: AppInfo[];
-
-  @Field(() => Number)
-  total!: number;
-}
-
-@InputType()
-class AppInputType {
-  @Field(() => String)
-  id!: string;
-
-  @Field(() => GraphQLJSONObject)
-  form!: Record<string, string>;
-
-  @Field(() => Boolean)
-  exposed!: boolean;
-
-  @Field(() => String)
-  domain!: string;
-}
-
-export { ListAppsResonse, AppInfo, AppInputType };

+ 0 - 16
packages/system-api/src/modules/auth/__tests__/user.factory.ts

@@ -1,16 +0,0 @@
-import * as argon2 from 'argon2';
-import { faker } from '@faker-js/faker';
-import User from '../user.entity';
-
-const createUser = async (email?: string) => {
-  const hash = await argon2.hash('password');
-
-  const user = await User.create({
-    username: email?.toLowerCase().trim() || faker.internet.email().toLowerCase().trim(),
-    password: hash,
-  }).save();
-
-  return user;
-};
-
-export { createUser };

+ 0 - 28
packages/system-api/src/modules/auth/user.entity.ts

@@ -1,28 +0,0 @@
-/* eslint-disable import/no-cycle */
-import { Field, ID, ObjectType } from 'type-graphql';
-import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
-import { IsEmail } from 'class-validator';
-
-@ObjectType()
-@Entity()
-export default class User extends BaseEntity {
-  @Field(() => ID)
-  @PrimaryGeneratedColumn()
-  id!: number;
-
-  @Field(() => String)
-  @IsEmail()
-  @Column({ type: 'varchar', unique: true })
-  username!: string;
-
-  @Column({ type: 'varchar', nullable: false })
-  password!: string;
-
-  @Field(() => Date)
-  @CreateDateColumn()
-  createdAt!: Date;
-
-  @Field(() => Date)
-  @UpdateDateColumn()
-  updatedAt!: Date;
-}

+ 0 - 24
packages/system-api/src/modules/system/update.entity.ts

@@ -1,24 +0,0 @@
-import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
-
-export enum UpdateStatusEnum {
-  FAILED = 'FAILED',
-  SUCCESS = 'SUCCESS',
-}
-
-@Entity()
-export default class Update extends BaseEntity {
-  @PrimaryGeneratedColumn()
-  id!: number;
-
-  @Column({ type: 'varchar', unique: true, nullable: false })
-  name!: string;
-
-  @Column({ type: 'enum', enum: UpdateStatusEnum, nullable: false })
-  status!: UpdateStatusEnum;
-
-  @CreateDateColumn()
-  createdAt!: Date;
-
-  @UpdateDateColumn()
-  updatedAt!: Date;
-}

+ 0 - 13
packages/system-api/src/schema.ts

@@ -1,13 +0,0 @@
-import { GraphQLSchema } from 'graphql';
-import { buildSchema } from 'type-graphql';
-import { customAuthChecker } from './core/middlewares/authChecker';
-import AppsResolver from './modules/apps/apps.resolver';
-
-const createSchema = (): Promise<GraphQLSchema> =>
-  buildSchema({
-    resolvers: [AppsResolver],
-    validate: true,
-    authChecker: customAuthChecker,
-  });
-
-export { createSchema };

+ 2 - 47
packages/system-api/src/server.ts

@@ -1,35 +1,16 @@
 import 'reflect-metadata';
 import express from 'express';
 import { ApolloServerPluginLandingPageGraphQLPlayground as Playground } from 'apollo-server-core';
-import { ApolloServer } from 'apollo-server-express';
 import { createServer } from 'http';
 import { ZodError } from 'zod';
 import cors, { CorsOptions } from 'cors';
-import { createSchema } from './schema';
 import { ApolloLogs } from './config/logger/apollo.logger';
 import logger from './config/logger/logger';
 import getSessionMiddleware from './core/middlewares/sessionMiddleware';
-import { MyContext } from './types';
 import { __prod__ } from './config/constants/constants';
-import datasource from './config/datasource';
-import appsService from './modules/apps/apps.service';
-import { runUpdates } from './core/updates/run';
 import startJobs from './core/jobs/jobs';
-import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig';
-import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
-
-const applyCustomConfig = () => {
-  try {
-    applyJsonConfig();
-  } catch (e) {
-    logger.error('Error applying settings.json config');
-    if (e instanceof ZodError) {
-      Object.keys(e.flatten().fieldErrors).forEach((key) => {
-        logger.error(`Error in field ${key}`);
-      });
-    }
-  }
-};
+import { applyJsonConfig, getConfig } from './core/config/TipiConfig';
+import { eventDispatcher } from './core/config/EventDispatcher';
 
 const corsOptions: CorsOptions = {
   credentials: false,
@@ -41,7 +22,6 @@ const corsOptions: CorsOptions = {
 const main = async () => {
   try {
     eventDispatcher.clear();
-    applyCustomConfig();
 
     const app = express();
     const port = 3001;
@@ -50,37 +30,12 @@ const main = async () => {
     app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
     app.use(getSessionMiddleware);
 
-    await datasource.initialize();
-
-    const schema = await createSchema();
     const httpServer = createServer(app);
-    const plugins = [ApolloLogs];
-
-    if (!__prod__) {
-      plugins.push(Playground());
-    }
-
-    const apolloServer = new ApolloServer({
-      schema,
-      context: ({ req, res }): MyContext => ({ req, res }),
-      plugins,
-      cache: 'bounded',
-    });
-
-    await apolloServer.start();
-    apolloServer.applyMiddleware({ app });
-
-    await runUpdates();
 
     httpServer.listen(port, async () => {
-      await eventDispatcher.dispatchEventAsync(EventTypes.CLONE_REPO, [getConfig().appsRepoUrl]);
-      await eventDispatcher.dispatchEventAsync(EventTypes.UPDATE_REPO, [getConfig().appsRepoUrl]);
-
       startJobs();
-      setConfig('status', 'RUNNING');
 
       // Start apps
-      appsService.startAllApps();
       logger.info(`Server running on port ${port} 🚀 Production => ${__prod__}`);
       logger.info(`Config: ${JSON.stringify(getConfig(), null, 2)}`);
     });

+ 0 - 48
packages/system-api/src/test/connection.ts

@@ -1,48 +0,0 @@
-import { DataSource } from 'typeorm';
-import pg from 'pg';
-import App from '../modules/apps/app.entity';
-import User from '../modules/auth/user.entity';
-import Update from '../modules/system/update.entity';
-
-const HOST = 'localhost';
-const USER = 'postgres';
-const DATABASE = 'postgres';
-const PASSWORD = 'postgres';
-const PORT = 5433;
-
-const pgClient = new pg.Client({
-  user: USER,
-  host: HOST,
-  database: DATABASE,
-  password: PASSWORD,
-  port: PORT,
-});
-
-export const setupConnection = async (testsuite: string): Promise<DataSource> => {
-  await pgClient.connect();
-
-  await pgClient.query(`DROP DATABASE IF EXISTS ${testsuite}`);
-  await pgClient.query(`CREATE DATABASE ${testsuite}`);
-
-  const AppDataSource = new DataSource({
-    name: 'default',
-    type: 'postgres',
-    host: HOST,
-    port: PORT,
-    username: USER,
-    password: PASSWORD,
-    database: testsuite,
-    dropSchema: true,
-    logging: false,
-    synchronize: true,
-    entities: [App, User, Update],
-  });
-
-  await AppDataSource.initialize();
-  return AppDataSource;
-};
-
-export const teardownConnection = async (testsuite: string): Promise<void> => {
-  await pgClient.query(`DROP DATABASE IF EXISTS ${testsuite}`);
-  await pgClient.end();
-};

+ 0 - 27
packages/system-api/src/test/gcall.ts

@@ -1,27 +0,0 @@
-import { ExecutionResult, graphql, GraphQLSchema } from 'graphql';
-import { Maybe } from 'type-graphql';
-import { createSchema } from '../schema';
-
-interface Options {
-  source: string;
-  variableValues?: Maybe<{
-    [key: string]: unknown;
-  }>;
-  userId?: number;
-  session?: string;
-}
-
-let schema: GraphQLSchema | null = null;
-
-export const gcall = async <T>({ source, variableValues, userId, session }: Options): Promise<ExecutionResult<T, { [key: string]: unknown }>> => {
-  if (!schema) {
-    schema = await createSchema();
-  }
-
-  return graphql({
-    schema,
-    source,
-    variableValues,
-    contextValue: { req: { session: { userId, id: session } } },
-  }) as unknown as ExecutionResult<T, { [key: string]: unknown }>;
-};

+ 0 - 21
packages/system-api/src/test/mutations/index.ts

@@ -1,21 +0,0 @@
-/* eslint-disable import/no-extraneous-dependencies */
-import 'graphql-import-node';
-import { print } from 'graphql/language/printer';
-
-import * as installApp from './installApp.graphql';
-import * as startApp from './startApp.graphql';
-import * as stopApp from './stopApp.graphql';
-import * as uninstallApp from './uninstallApp.graphql';
-import * as updateAppConfig from './updateAppConfig.graphql';
-import * as updateApp from './updateApp.graphql';
-import * as register from './register.graphql';
-import * as login from './login.graphql';
-
-export const installAppMutation = print(installApp);
-export const startAppMutation = print(startApp);
-export const stopAppMutation = print(stopApp);
-export const uninstallAppMutation = print(uninstallApp);
-export const updateAppConfigMutation = print(updateAppConfig);
-export const updateAppMutation = print(updateApp);
-export const registerMutation = print(register);
-export const loginMutation = print(login);

+ 0 - 25
packages/system-api/src/test/mutations/installApp.graphql

@@ -1,25 +0,0 @@
-mutation InstallApp($input: AppInputType!) {
-  installApp(input: $input) {
-    id
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-  }
-}

+ 0 - 6
packages/system-api/src/test/mutations/login.graphql

@@ -1,6 +0,0 @@
-# Write your query or mutation here
-mutation Login($input: UsernamePasswordInput!) {
-  login(input: $input) {
-    token
-  }
-}

+ 0 - 6
packages/system-api/src/test/mutations/register.graphql

@@ -1,6 +0,0 @@
-# Write your query or mutation here
-mutation Register($input: UsernamePasswordInput!) {
-  register(input: $input) {
-    token
-  }
-}

+ 0 - 31
packages/system-api/src/test/mutations/startApp.graphql

@@ -1,31 +0,0 @@
-# Write your query or mutation here
-mutation StartApp($id: String!) {
-  startApp(id: $id) {
-    id
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-  }
-}

+ 0 - 30
packages/system-api/src/test/mutations/stopApp.graphql

@@ -1,30 +0,0 @@
-mutation StopApp($id: String!) {
-  stopApp(id: $id) {
-    id
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-  }
-}

+ 0 - 30
packages/system-api/src/test/mutations/uninstallApp.graphql

@@ -1,30 +0,0 @@
-mutation UninstallApp($id: String!) {
-  uninstallApp(id: $id) {
-    id
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-  }
-}

+ 0 - 30
packages/system-api/src/test/mutations/updateApp.graphql

@@ -1,30 +0,0 @@
-mutation UpdateApp($id: String!) {
-  updateApp(id: $id) {
-    id
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-  }
-}

+ 0 - 30
packages/system-api/src/test/mutations/updateAppConfig.graphql

@@ -1,30 +0,0 @@
-mutation UpdateAppConfig($input: AppInputType!) {
-  updateAppConfig(input: $input) {
-    id
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-  }
-}

+ 0 - 29
packages/system-api/src/test/queries/getApp.graphql

@@ -1,29 +0,0 @@
-query GetApp($id: String!) {
-  getApp(id: $id) {
-    status
-    config
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-  }
-}

+ 0 - 17
packages/system-api/src/test/queries/index.ts

@@ -1,17 +0,0 @@
-/* eslint-disable import/no-extraneous-dependencies */
-import 'graphql-import-node';
-import { print } from 'graphql/language/printer';
-
-import * as listAppInfos from './listAppInfos.graphql';
-import * as getApp from './getApp.graphql';
-import * as InstalledApps from './installedApps.graphql';
-import * as Me from './me.graphql';
-import * as isConfigured from './isConfigured.graphql';
-import * as refreshToken from './refreshToken.graphql';
-
-export const listAppInfosQuery = print(listAppInfos);
-export const getAppQuery = print(getApp);
-export const InstalledAppsQuery = print(InstalledApps);
-export const MeQuery = print(Me);
-export const isConfiguredQuery = print(isConfigured);
-export const refreshTokenQuery = print(refreshToken);

+ 0 - 29
packages/system-api/src/test/queries/installedApps.graphql

@@ -1,29 +0,0 @@
-query {
-  installedApps {
-    id
-    status
-    lastOpened
-    numOpened
-    config
-    createdAt
-    updatedAt
-    info {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-  }
-}

+ 0 - 3
packages/system-api/src/test/queries/isConfigured.graphql

@@ -1,3 +0,0 @@
-query IsConfigured {
-  isConfigured
-}

+ 0 - 23
packages/system-api/src/test/queries/listAppInfos.graphql

@@ -1,23 +0,0 @@
-query {
-  listAppsInfo {
-    apps {
-      id
-      available
-      port
-      name
-      description
-      version
-      author
-      source
-      categories
-      url_suffix
-      form_fields {
-        max
-        min
-        required
-        env_variable
-      }
-    }
-    total
-  }
-}

+ 0 - 6
packages/system-api/src/test/queries/me.graphql

@@ -1,6 +0,0 @@
-{
-  me {
-    id
-    username
-  }
-}

+ 0 - 6
packages/system-api/src/test/queries/refreshToken.graphql

@@ -1,6 +0,0 @@
-# Write your query or mutation here
-query RefreshToken {
-  refreshToken {
-    token
-  }
-}

+ 0 - 6
packages/system-api/src/test/queries/version.graphql

@@ -1,6 +0,0 @@
-query {
-  version {
-    current
-    latest
-  }
-}