浏览代码

chore: update tests to cover invalid config.json

Nicolas Meienberger 2 年之前
父节点
当前提交
a76a8100ee

+ 0 - 7
packages/dashboard/next.config.js

@@ -1,13 +1,6 @@
 /** @type {import('next').NextConfig} */
 /** @type {import('next').NextConfig} */
 const nextConfig = {
 const nextConfig = {
   output: 'standalone',
   output: 'standalone',
-  webpackDevMiddleware: (config) => {
-    config.watchOptions = {
-      poll: 1000,
-      aggregateTimeout: 300,
-    };
-    return config;
-  },
   reactStrictMode: true,
   reactStrictMode: true,
   basePath: '/dashboard',
   basePath: '/dashboard',
 };
 };

+ 1 - 1
packages/dashboard/src/components/Form/validators.ts

@@ -62,7 +62,7 @@ const validateField = (field: FormField, value: string | undefined | boolean): s
 
 
 const validateDomain = (domain?: string): string | undefined => {
 const validateDomain = (domain?: string): string | undefined => {
   if (!validator.isFQDN(domain || '')) {
   if (!validator.isFQDN(domain || '')) {
-    return `${domain} must be a valid domain`;
+    return 'Must be a valid domain';
   }
   }
 };
 };
 
 

+ 232 - 283
packages/dashboard/src/generated/graphql.tsx

@@ -65,7 +65,6 @@ export type AppInfo = {
   name: Scalars['String'];
   name: Scalars['String'];
   no_gui?: Maybe<Scalars['Boolean']>;
   no_gui?: Maybe<Scalars['Boolean']>;
   port: Scalars['Float'];
   port: Scalars['Float'];
-  requirements?: Maybe<Scalars['JSONObject']>;
   short_desc: Scalars['String'];
   short_desc: Scalars['String'];
   source: Scalars['String'];
   source: Scalars['String'];
   supported_architectures?: Maybe<Array<AppSupportedArchitecturesEnum>>;
   supported_architectures?: Maybe<Array<AppSupportedArchitecturesEnum>>;
@@ -155,34 +154,42 @@ export type Mutation = {
   updateAppConfig: App;
   updateAppConfig: App;
 };
 };
 
 
+
 export type MutationInstallAppArgs = {
 export type MutationInstallAppArgs = {
   input: AppInputType;
   input: AppInputType;
 };
 };
 
 
+
 export type MutationLoginArgs = {
 export type MutationLoginArgs = {
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 };
 };
 
 
+
 export type MutationRegisterArgs = {
 export type MutationRegisterArgs = {
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 };
 };
 
 
+
 export type MutationStartAppArgs = {
 export type MutationStartAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationStopAppArgs = {
 export type MutationStopAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationUninstallAppArgs = {
 export type MutationUninstallAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationUpdateAppArgs = {
 export type MutationUpdateAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationUpdateAppConfigArgs = {
 export type MutationUpdateAppConfigArgs = {
   input: AppInputType;
   input: AppInputType;
 };
 };
@@ -199,6 +206,7 @@ export type Query = {
   version: VersionResponse;
   version: VersionResponse;
 };
 };
 
 
+
 export type QueryGetAppArgs = {
 export type QueryGetAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
@@ -245,184 +253,125 @@ export type InstallAppMutationVariables = Exact<{
   input: AppInputType;
   input: AppInputType;
 }>;
 }>;
 
 
-export type InstallAppMutation = { __typename?: 'Mutation'; installApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
+
+export type InstallAppMutation = { __typename?: 'Mutation', installApp: { __typename: 'App', id: string, status: AppStatusEnum } };
 
 
 export type LoginMutationVariables = Exact<{
 export type LoginMutationVariables = Exact<{
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 }>;
 }>;
 
 
-export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
 
 
-export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
+export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'TokenResponse', token: string } };
 
 
-export type LogoutMutation = { __typename?: 'Mutation'; logout: boolean };
+export type LogoutMutationVariables = Exact<{ [key: string]: never; }>;
+
+
+export type LogoutMutation = { __typename?: 'Mutation', logout: boolean };
 
 
 export type RegisterMutationVariables = Exact<{
 export type RegisterMutationVariables = Exact<{
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 }>;
 }>;
 
 
-export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
 
 
-export type RestartMutationVariables = Exact<{ [key: string]: never }>;
+export type RegisterMutation = { __typename?: 'Mutation', register: { __typename?: 'TokenResponse', token: string } };
+
+export type RestartMutationVariables = Exact<{ [key: string]: never; }>;
 
 
-export type RestartMutation = { __typename?: 'Mutation'; restart: boolean };
+
+export type RestartMutation = { __typename?: 'Mutation', restart: boolean };
 
 
 export type StartAppMutationVariables = Exact<{
 export type StartAppMutationVariables = Exact<{
   id: Scalars['String'];
   id: Scalars['String'];
 }>;
 }>;
 
 
-export type StartAppMutation = { __typename?: 'Mutation'; startApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
+
+export type StartAppMutation = { __typename?: 'Mutation', startApp: { __typename: 'App', id: string, status: AppStatusEnum } };
 
 
 export type StopAppMutationVariables = Exact<{
 export type StopAppMutationVariables = Exact<{
   id: Scalars['String'];
   id: Scalars['String'];
 }>;
 }>;
 
 
-export type StopAppMutation = { __typename?: 'Mutation'; stopApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
+
+export type StopAppMutation = { __typename?: 'Mutation', stopApp: { __typename: 'App', id: string, status: AppStatusEnum } };
 
 
 export type UninstallAppMutationVariables = Exact<{
 export type UninstallAppMutationVariables = Exact<{
   id: Scalars['String'];
   id: Scalars['String'];
 }>;
 }>;
 
 
-export type UninstallAppMutation = { __typename?: 'Mutation'; uninstallApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
 
 
-export type UpdateMutationVariables = Exact<{ [key: string]: never }>;
+export type UninstallAppMutation = { __typename?: 'Mutation', uninstallApp: { __typename: 'App', id: string, status: AppStatusEnum } };
+
+export type UpdateMutationVariables = Exact<{ [key: string]: never; }>;
 
 
-export type UpdateMutation = { __typename?: 'Mutation'; update: boolean };
+
+export type UpdateMutation = { __typename?: 'Mutation', update: boolean };
 
 
 export type UpdateAppMutationVariables = Exact<{
 export type UpdateAppMutationVariables = Exact<{
   id: Scalars['String'];
   id: Scalars['String'];
 }>;
 }>;
 
 
-export type UpdateAppMutation = { __typename?: 'Mutation'; updateApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
+
+export type UpdateAppMutation = { __typename?: 'Mutation', updateApp: { __typename: 'App', id: string, status: AppStatusEnum } };
 
 
 export type UpdateAppConfigMutationVariables = Exact<{
 export type UpdateAppConfigMutationVariables = Exact<{
   input: AppInputType;
   input: AppInputType;
 }>;
 }>;
 
 
-export type UpdateAppConfigMutation = { __typename?: 'Mutation'; updateAppConfig: { __typename: 'App'; id: string; status: AppStatusEnum } };
+
+export type UpdateAppConfigMutation = { __typename?: 'Mutation', updateAppConfig: { __typename: 'App', id: string, status: AppStatusEnum } };
 
 
 export type GetAppQueryVariables = Exact<{
 export type GetAppQueryVariables = Exact<{
   appId: Scalars['String'];
   appId: Scalars['String'];
 }>;
 }>;
 
 
-export type GetAppQuery = {
-  __typename?: 'Query';
-  getApp: {
-    __typename?: 'App';
-    id: string;
-    status: AppStatusEnum;
-    config: any;
-    version?: number | null;
-    exposed: boolean;
-    domain?: string | null;
-    updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
-    info?: {
-      __typename?: 'AppInfo';
-      id: string;
-      port: number;
-      name: string;
-      description: string;
-      available: boolean;
-      version?: string | null;
-      tipi_version: number;
-      short_desc: string;
-      author: string;
-      source: string;
-      categories: Array<AppCategoriesEnum>;
-      url_suffix?: string | null;
-      https?: boolean | null;
-      exposable?: boolean | null;
-      no_gui?: boolean | null;
-      form_fields: Array<{
-        __typename?: 'FormField';
-        type: FieldTypesEnum;
-        label: string;
-        max?: number | null;
-        min?: number | null;
-        hint?: string | null;
-        placeholder?: string | null;
-        required?: boolean | null;
-        env_variable: string;
-      }>;
-    } | null;
-  };
-};
 
 
-export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>;
+export type GetAppQuery = { __typename?: 'Query', getApp: { __typename?: 'App', id: string, status: AppStatusEnum, config: any, version?: number | null, exposed: boolean, domain?: string | null, updateInfo?: { __typename?: 'UpdateInfo', current: number, latest: number, dockerVersion?: string | null } | null, info?: { __typename?: 'AppInfo', id: string, port: number, name: string, description: string, available: boolean, version?: string | null, tipi_version: number, short_desc: string, author: string, source: string, categories: Array<AppCategoriesEnum>, url_suffix?: string | null, https?: boolean | null, exposable?: boolean | null, no_gui?: boolean | null, form_fields: Array<{ __typename?: 'FormField', type: FieldTypesEnum, label: string, max?: number | null, min?: number | null, hint?: string | null, placeholder?: string | null, required?: boolean | null, env_variable: string }> } | null } };
 
 
-export type InstalledAppsQuery = {
-  __typename?: 'Query';
-  installedApps: Array<{
-    __typename?: 'App';
-    id: string;
-    status: AppStatusEnum;
-    config: any;
-    version?: number | null;
-    updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
-    info?: { __typename?: 'AppInfo'; id: string; name: string; description: string; tipi_version: number; short_desc: string; https?: boolean | null } | null;
-  }>;
-};
+export type InstalledAppsQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
 
 
-export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
+export type InstalledAppsQuery = { __typename?: 'Query', installedApps: Array<{ __typename?: 'App', id: string, status: AppStatusEnum, config: any, version?: number | null, updateInfo?: { __typename?: 'UpdateInfo', current: number, latest: number, dockerVersion?: string | null } | null, info?: { __typename?: 'AppInfo', id: string, name: string, description: string, tipi_version: number, short_desc: string, https?: boolean | null } | null }> };
 
 
-export type ListAppsQueryVariables = Exact<{ [key: string]: never }>;
+export type ConfiguredQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type ListAppsQuery = {
-  __typename?: 'Query';
-  listAppsInfo: {
-    __typename?: 'ListAppsResonse';
-    total: number;
-    apps: Array<{
-      __typename?: 'AppInfo';
-      id: string;
-      available: boolean;
-      tipi_version: number;
-      port: number;
-      name: string;
-      version?: string | null;
-      short_desc: string;
-      author: string;
-      categories: Array<AppCategoriesEnum>;
-      https?: boolean | null;
-    }>;
-  };
-};
 
 
-export type MeQueryVariables = Exact<{ [key: string]: never }>;
+export type ConfiguredQuery = { __typename?: 'Query', isConfigured: boolean };
 
 
-export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
+export type ListAppsQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
 
 
-export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
+export type ListAppsQuery = { __typename?: 'Query', listAppsInfo: { __typename?: 'ListAppsResonse', total: number, apps: Array<{ __typename?: 'AppInfo', id: string, available: boolean, tipi_version: number, port: number, name: string, version?: string | null, short_desc: string, author: string, categories: Array<AppCategoriesEnum>, https?: boolean | null }> } };
 
 
-export type SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
+export type MeQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type SystemInfoQuery = {
-  __typename?: 'Query';
-  systemInfo?: {
-    __typename?: 'SystemInfoResponse';
-    cpu: { __typename?: 'Cpu'; load: number };
-    disk: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
-    memory: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
-  } | null;
-};
 
 
-export type VersionQueryVariables = Exact<{ [key: string]: never }>;
+export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null };
+
+export type RefreshTokenQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type RefreshTokenQuery = { __typename?: 'Query', refreshToken?: { __typename?: 'TokenResponse', token: string } | null };
+
+export type SystemInfoQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type SystemInfoQuery = { __typename?: 'Query', systemInfo?: { __typename?: 'SystemInfoResponse', cpu: { __typename?: 'Cpu', load: number }, disk: { __typename?: 'DiskMemory', available: number, used: number, total: number }, memory: { __typename?: 'DiskMemory', available: number, used: number, total: number } } | null };
+
+export type VersionQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type VersionQuery = { __typename?: 'Query', version: { __typename?: 'VersionResponse', current: string, latest?: string | null } };
 
 
-export type VersionQuery = { __typename?: 'Query'; version: { __typename?: 'VersionResponse'; current: string; latest?: string | null } };
 
 
 export const InstallAppDocument = gql`
 export const InstallAppDocument = gql`
-  mutation InstallApp($input: AppInputType!) {
-    installApp(input: $input) {
-      id
-      status
-      __typename
-    }
+    mutation InstallApp($input: AppInputType!) {
+  installApp(input: $input) {
+    id
+    status
+    __typename
   }
   }
-`;
+}
+    `;
 export type InstallAppMutationFn = Apollo.MutationFunction<InstallAppMutation, InstallAppMutationVariables>;
 export type InstallAppMutationFn = Apollo.MutationFunction<InstallAppMutation, InstallAppMutationVariables>;
 
 
 /**
 /**
@@ -450,12 +399,12 @@ export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutati
 export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
 export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
 export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
 export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
 export const LoginDocument = gql`
 export const LoginDocument = gql`
-  mutation Login($input: UsernamePasswordInput!) {
-    login(input: $input) {
-      token
-    }
+    mutation Login($input: UsernamePasswordInput!) {
+  login(input: $input) {
+    token
   }
   }
-`;
+}
+    `;
 export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
 export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
 
 
 /**
 /**
@@ -483,10 +432,10 @@ export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
 export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
 export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
 export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
 export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
 export const LogoutDocument = gql`
 export const LogoutDocument = gql`
-  mutation Logout {
-    logout
-  }
-`;
+    mutation Logout {
+  logout
+}
+    `;
 export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
 export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
 
 
 /**
 /**
@@ -513,12 +462,12 @@ export type LogoutMutationHookResult = ReturnType<typeof useLogoutMutation>;
 export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
 export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
 export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
 export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
 export const RegisterDocument = gql`
 export const RegisterDocument = gql`
-  mutation Register($input: UsernamePasswordInput!) {
-    register(input: $input) {
-      token
-    }
+    mutation Register($input: UsernamePasswordInput!) {
+  register(input: $input) {
+    token
   }
   }
-`;
+}
+    `;
 export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
 export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
 
 
 /**
 /**
@@ -546,10 +495,10 @@ export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
 export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
 export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
 export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
 export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
 export const RestartDocument = gql`
 export const RestartDocument = gql`
-  mutation Restart {
-    restart
-  }
-`;
+    mutation Restart {
+  restart
+}
+    `;
 export type RestartMutationFn = Apollo.MutationFunction<RestartMutation, RestartMutationVariables>;
 export type RestartMutationFn = Apollo.MutationFunction<RestartMutation, RestartMutationVariables>;
 
 
 /**
 /**
@@ -576,14 +525,14 @@ export type RestartMutationHookResult = ReturnType<typeof useRestartMutation>;
 export type RestartMutationResult = Apollo.MutationResult<RestartMutation>;
 export type RestartMutationResult = Apollo.MutationResult<RestartMutation>;
 export type RestartMutationOptions = Apollo.BaseMutationOptions<RestartMutation, RestartMutationVariables>;
 export type RestartMutationOptions = Apollo.BaseMutationOptions<RestartMutation, RestartMutationVariables>;
 export const StartAppDocument = gql`
 export const StartAppDocument = gql`
-  mutation StartApp($id: String!) {
-    startApp(id: $id) {
-      id
-      status
-      __typename
-    }
+    mutation StartApp($id: String!) {
+  startApp(id: $id) {
+    id
+    status
+    __typename
   }
   }
-`;
+}
+    `;
 export type StartAppMutationFn = Apollo.MutationFunction<StartAppMutation, StartAppMutationVariables>;
 export type StartAppMutationFn = Apollo.MutationFunction<StartAppMutation, StartAppMutationVariables>;
 
 
 /**
 /**
@@ -611,14 +560,14 @@ export type StartAppMutationHookResult = ReturnType<typeof useStartAppMutation>;
 export type StartAppMutationResult = Apollo.MutationResult<StartAppMutation>;
 export type StartAppMutationResult = Apollo.MutationResult<StartAppMutation>;
 export type StartAppMutationOptions = Apollo.BaseMutationOptions<StartAppMutation, StartAppMutationVariables>;
 export type StartAppMutationOptions = Apollo.BaseMutationOptions<StartAppMutation, StartAppMutationVariables>;
 export const StopAppDocument = gql`
 export const StopAppDocument = gql`
-  mutation StopApp($id: String!) {
-    stopApp(id: $id) {
-      id
-      status
-      __typename
-    }
+    mutation StopApp($id: String!) {
+  stopApp(id: $id) {
+    id
+    status
+    __typename
   }
   }
-`;
+}
+    `;
 export type StopAppMutationFn = Apollo.MutationFunction<StopAppMutation, StopAppMutationVariables>;
 export type StopAppMutationFn = Apollo.MutationFunction<StopAppMutation, StopAppMutationVariables>;
 
 
 /**
 /**
@@ -646,14 +595,14 @@ export type StopAppMutationHookResult = ReturnType<typeof useStopAppMutation>;
 export type StopAppMutationResult = Apollo.MutationResult<StopAppMutation>;
 export type StopAppMutationResult = Apollo.MutationResult<StopAppMutation>;
 export type StopAppMutationOptions = Apollo.BaseMutationOptions<StopAppMutation, StopAppMutationVariables>;
 export type StopAppMutationOptions = Apollo.BaseMutationOptions<StopAppMutation, StopAppMutationVariables>;
 export const UninstallAppDocument = gql`
 export const UninstallAppDocument = gql`
-  mutation UninstallApp($id: String!) {
-    uninstallApp(id: $id) {
-      id
-      status
-      __typename
-    }
+    mutation UninstallApp($id: String!) {
+  uninstallApp(id: $id) {
+    id
+    status
+    __typename
   }
   }
-`;
+}
+    `;
 export type UninstallAppMutationFn = Apollo.MutationFunction<UninstallAppMutation, UninstallAppMutationVariables>;
 export type UninstallAppMutationFn = Apollo.MutationFunction<UninstallAppMutation, UninstallAppMutationVariables>;
 
 
 /**
 /**
@@ -681,10 +630,10 @@ export type UninstallAppMutationHookResult = ReturnType<typeof useUninstallAppMu
 export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
 export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
 export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
 export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
 export const UpdateDocument = gql`
 export const UpdateDocument = gql`
-  mutation Update {
-    update
-  }
-`;
+    mutation Update {
+  update
+}
+    `;
 export type UpdateMutationFn = Apollo.MutationFunction<UpdateMutation, UpdateMutationVariables>;
 export type UpdateMutationFn = Apollo.MutationFunction<UpdateMutation, UpdateMutationVariables>;
 
 
 /**
 /**
@@ -711,14 +660,14 @@ export type UpdateMutationHookResult = ReturnType<typeof useUpdateMutation>;
 export type UpdateMutationResult = Apollo.MutationResult<UpdateMutation>;
 export type UpdateMutationResult = Apollo.MutationResult<UpdateMutation>;
 export type UpdateMutationOptions = Apollo.BaseMutationOptions<UpdateMutation, UpdateMutationVariables>;
 export type UpdateMutationOptions = Apollo.BaseMutationOptions<UpdateMutation, UpdateMutationVariables>;
 export const UpdateAppDocument = gql`
 export const UpdateAppDocument = gql`
-  mutation UpdateApp($id: String!) {
-    updateApp(id: $id) {
-      id
-      status
-      __typename
-    }
+    mutation UpdateApp($id: String!) {
+  updateApp(id: $id) {
+    id
+    status
+    __typename
   }
   }
-`;
+}
+    `;
 export type UpdateAppMutationFn = Apollo.MutationFunction<UpdateAppMutation, UpdateAppMutationVariables>;
 export type UpdateAppMutationFn = Apollo.MutationFunction<UpdateAppMutation, UpdateAppMutationVariables>;
 
 
 /**
 /**
@@ -746,14 +695,14 @@ export type UpdateAppMutationHookResult = ReturnType<typeof useUpdateAppMutation
 export type UpdateAppMutationResult = Apollo.MutationResult<UpdateAppMutation>;
 export type UpdateAppMutationResult = Apollo.MutationResult<UpdateAppMutation>;
 export type UpdateAppMutationOptions = Apollo.BaseMutationOptions<UpdateAppMutation, UpdateAppMutationVariables>;
 export type UpdateAppMutationOptions = Apollo.BaseMutationOptions<UpdateAppMutation, UpdateAppMutationVariables>;
 export const UpdateAppConfigDocument = gql`
 export const UpdateAppConfigDocument = gql`
-  mutation UpdateAppConfig($input: AppInputType!) {
-    updateAppConfig(input: $input) {
-      id
-      status
-      __typename
-    }
+    mutation UpdateAppConfig($input: AppInputType!) {
+  updateAppConfig(input: $input) {
+    id
+    status
+    __typename
   }
   }
-`;
+}
+    `;
 export type UpdateAppConfigMutationFn = Apollo.MutationFunction<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
 export type UpdateAppConfigMutationFn = Apollo.MutationFunction<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
 
 
 /**
 /**
@@ -781,49 +730,49 @@ export type UpdateAppConfigMutationHookResult = ReturnType<typeof useUpdateAppCo
 export type UpdateAppConfigMutationResult = Apollo.MutationResult<UpdateAppConfigMutation>;
 export type UpdateAppConfigMutationResult = Apollo.MutationResult<UpdateAppConfigMutation>;
 export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
 export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
 export const GetAppDocument = gql`
 export const GetAppDocument = gql`
-  query GetApp($appId: String!) {
-    getApp(id: $appId) {
+    query GetApp($appId: String!) {
+  getApp(id: $appId) {
+    id
+    status
+    config
+    version
+    exposed
+    domain
+    updateInfo {
+      current
+      latest
+      dockerVersion
+    }
+    info {
       id
       id
-      status
-      config
+      port
+      name
+      description
+      available
       version
       version
-      exposed
-      domain
-      updateInfo {
-        current
-        latest
-        dockerVersion
-      }
-      info {
-        id
-        port
-        name
-        description
-        available
-        version
-        tipi_version
-        short_desc
-        author
-        source
-        categories
-        url_suffix
-        https
-        exposable
-        no_gui
-        form_fields {
-          type
-          label
-          max
-          min
-          hint
-          placeholder
-          required
-          env_variable
-        }
+      tipi_version
+      short_desc
+      author
+      source
+      categories
+      url_suffix
+      https
+      exposable
+      no_gui
+      form_fields {
+        type
+        label
+        max
+        min
+        hint
+        placeholder
+        required
+        env_variable
       }
       }
     }
     }
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useGetAppQuery__
  * __useGetAppQuery__
@@ -853,28 +802,28 @@ export type GetAppQueryHookResult = ReturnType<typeof useGetAppQuery>;
 export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
 export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
 export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
 export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
 export const InstalledAppsDocument = gql`
 export const InstalledAppsDocument = gql`
-  query InstalledApps {
-    installedApps {
+    query InstalledApps {
+  installedApps {
+    id
+    status
+    config
+    version
+    updateInfo {
+      current
+      latest
+      dockerVersion
+    }
+    info {
       id
       id
-      status
-      config
-      version
-      updateInfo {
-        current
-        latest
-        dockerVersion
-      }
-      info {
-        id
-        name
-        description
-        tipi_version
-        short_desc
-        https
-      }
+      name
+      description
+      tipi_version
+      short_desc
+      https
     }
     }
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useInstalledAppsQuery__
  * __useInstalledAppsQuery__
@@ -903,10 +852,10 @@ export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQue
 export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
 export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
 export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
 export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
 export const ConfiguredDocument = gql`
 export const ConfiguredDocument = gql`
-  query Configured {
-    isConfigured
-  }
-`;
+    query Configured {
+  isConfigured
+}
+    `;
 
 
 /**
 /**
  * __useConfiguredQuery__
  * __useConfiguredQuery__
@@ -935,24 +884,24 @@ export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
 export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
 export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
 export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
 export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
 export const ListAppsDocument = gql`
 export const ListAppsDocument = gql`
-  query ListApps {
-    listAppsInfo {
-      apps {
-        id
-        available
-        tipi_version
-        port
-        name
-        version
-        short_desc
-        author
-        categories
-        https
-      }
-      total
+    query ListApps {
+  listAppsInfo {
+    apps {
+      id
+      available
+      tipi_version
+      port
+      name
+      version
+      short_desc
+      author
+      categories
+      https
     }
     }
+    total
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useListAppsQuery__
  * __useListAppsQuery__
@@ -981,12 +930,12 @@ export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
 export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
 export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
 export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
 export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
 export const MeDocument = gql`
 export const MeDocument = gql`
-  query Me {
-    me {
-      id
-    }
+    query Me {
+  me {
+    id
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useMeQuery__
  * __useMeQuery__
@@ -1015,12 +964,12 @@ export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
 export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
 export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
 export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
 export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
 export const RefreshTokenDocument = gql`
 export const RefreshTokenDocument = gql`
-  query RefreshToken {
-    refreshToken {
-      token
-    }
+    query RefreshToken {
+  refreshToken {
+    token
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useRefreshTokenQuery__
  * __useRefreshTokenQuery__
@@ -1049,24 +998,24 @@ export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery
 export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
 export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
 export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
 export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
 export const SystemInfoDocument = gql`
 export const SystemInfoDocument = gql`
-  query SystemInfo {
-    systemInfo {
-      cpu {
-        load
-      }
-      disk {
-        available
-        used
-        total
-      }
-      memory {
-        available
-        used
-        total
-      }
+    query SystemInfo {
+  systemInfo {
+    cpu {
+      load
+    }
+    disk {
+      available
+      used
+      total
+    }
+    memory {
+      available
+      used
+      total
     }
     }
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useSystemInfoQuery__
  * __useSystemInfoQuery__
@@ -1095,13 +1044,13 @@ export type SystemInfoQueryHookResult = ReturnType<typeof useSystemInfoQuery>;
 export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
 export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
 export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
 export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
 export const VersionDocument = gql`
 export const VersionDocument = gql`
-  query Version {
-    version {
-      current
-      latest
-    }
+    query Version {
+  version {
+    current
+    latest
   }
   }
-`;
+}
+    `;
 
 
 /**
 /**
  * __useVersionQuery__
  * __useVersionQuery__
@@ -1128,4 +1077,4 @@ export function useVersionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ve
 }
 }
 export type VersionQueryHookResult = ReturnType<typeof useVersionQuery>;
 export type VersionQueryHookResult = ReturnType<typeof useVersionQuery>;
 export type VersionLazyQueryHookResult = ReturnType<typeof useVersionLazyQuery>;
 export type VersionLazyQueryHookResult = ReturnType<typeof useVersionLazyQuery>;
-export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;
+export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;

+ 54 - 4
packages/system-api/src/core/updates/__tests__/v040.test.ts

@@ -1,9 +1,11 @@
+import { faker } from '@faker-js/faker';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import { DataSource } from 'typeorm';
 import { DataSource } from 'typeorm';
 import logger from '../../../config/logger/logger';
 import logger from '../../../config/logger/logger';
 import App from '../../../modules/apps/app.entity';
 import App from '../../../modules/apps/app.entity';
 import { AppInfo, AppStatusEnum } from '../../../modules/apps/apps.types';
 import { AppInfo, AppStatusEnum } from '../../../modules/apps/apps.types';
 import { createApp } from '../../../modules/apps/__tests__/apps.factory';
 import { createApp } from '../../../modules/apps/__tests__/apps.factory';
+import User from '../../../modules/auth/user.entity';
 import Update, { UpdateStatusEnum } from '../../../modules/system/update.entity';
 import Update, { UpdateStatusEnum } from '../../../modules/system/update.entity';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { getConfig } from '../../config/TipiConfig';
 import { getConfig } from '../../config/TipiConfig';
@@ -30,7 +32,8 @@ afterAll(async () => {
   await teardownConnection(TEST_SUITE);
   await teardownConnection(TEST_SUITE);
 });
 });
 
 
-const createState = (apps: string[]) => JSON.stringify({ installed: apps.join(' ') });
+const createAppState = (apps: string[]) => JSON.stringify({ installed: apps.join(' ') });
+const createUserState = (users: { email: string; password: string }[]) => JSON.stringify(users);
 
 
 describe('No state/apps.json', () => {
 describe('No state/apps.json', () => {
   it('Should do nothing and create the update with status SUCCES', async () => {
   it('Should do nothing and create the update with status SUCCES', async () => {
@@ -60,7 +63,7 @@ describe('No state/apps.json', () => {
 describe('State/apps.json exists with no installed app', () => {
 describe('State/apps.json exists with no installed app', () => {
   beforeEach(async () => {
   beforeEach(async () => {
     const { MockFiles } = await createApp({});
     const { MockFiles } = await createApp({});
-    MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([]);
+    MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createAppState([]);
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
   });
   });
@@ -87,7 +90,7 @@ describe('State/apps.json exists with one installed app', () => {
   beforeEach(async () => {
   beforeEach(async () => {
     const { MockFiles, appInfo } = await createApp({});
     const { MockFiles, appInfo } = await createApp({});
     app1 = appInfo;
     app1 = appInfo;
-    MockFiles['/runtipi/state/apps.json'] = createState([appInfo.id]);
+    MockFiles['/runtipi/state/apps.json'] = createAppState([appInfo.id]);
     MockFiles[`/app/storage/app-data/${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';
     MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
     // @ts-ignore
     // @ts-ignore
@@ -116,7 +119,7 @@ describe('State/apps.json exists with one installed app', () => {
   it('Should not try to migrate app if it already exists', async () => {
   it('Should not try to migrate app if it already exists', async () => {
     const { MockFiles, appInfo } = await createApp({ installed: true });
     const { MockFiles, appInfo } = await createApp({ installed: true });
     app1 = appInfo;
     app1 = appInfo;
-    MockFiles['/runtipi/state/apps.json'] = createState([appInfo.id]);
+    MockFiles['/runtipi/state/apps.json'] = createAppState([appInfo.id]);
     MockFiles[`/app/storage/app-data/${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';
     MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
     // @ts-ignore
     // @ts-ignore
@@ -129,3 +132,50 @@ describe('State/apps.json exists with one installed app', () => {
     expect(spy).toHaveBeenCalledWith('App already migrated');
     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');
+  });
+});

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

@@ -74,7 +74,7 @@ export const updateV040 = async (): Promise<void> => {
     }
     }
 
 
     // Migrate users
     // Migrate users
-    if (fileExists('/state/users.json')) {
+    if (fileExists('/runtipi/state/users.json')) {
       const state = readJsonFile('/runtipi/state/users.json');
       const state = readJsonFile('/runtipi/state/users.json');
       const parsedState = userStateSchema.safeParse(state);
       const parsedState = userStateSchema.safeParse(state);
 
 

+ 16 - 20
packages/system-api/src/modules/apps/__tests__/apps.factory.ts

@@ -1,6 +1,7 @@
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from '../apps.types';
 import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from '../apps.types';
 import App from '../app.entity';
 import App from '../app.entity';
+import { appInfoSchema } from '../apps.helpers';
 
 
 interface IProps {
 interface IProps {
   installed?: boolean;
   installed?: boolean;
@@ -17,21 +18,22 @@ type CreateConfigParams = {
   id?: string;
   id?: string;
 };
 };
 
 
-const createAppConfig = (props?: CreateConfigParams): AppInfo => ({
-  id: props?.id || faker.random.alphaNumeric(32),
-  available: true,
-  port: faker.datatype.number(),
-  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 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 createApp = async (props: IProps) => {
-  const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false, supportedArchitectures } = props;
+  const { installed = false, status = AppStatusEnum.RUNNING, randomField = false, exposed = false, domain = '', exposable = false, supportedArchitectures } = props;
 
 
   const categories = Object.values(AppCategoriesEnum);
   const categories = Object.values(AppCategoriesEnum);
 
 
@@ -66,16 +68,10 @@ const createApp = async (props: IProps) => {
     });
     });
   }
   }
 
 
-  if (requiredPort) {
-    appInfo.requirements = {
-      ports: [requiredPort],
-    };
-  }
-
   const MockFiles: Record<string, string | string[]> = {};
   const MockFiles: Record<string, string | string[]> = {};
   MockFiles['/runtipi/.env'] = 'TEST=test';
   MockFiles['/runtipi/.env'] = 'TEST=test';
   MockFiles['/runtipi/repos/repo-id'] = '';
   MockFiles['/runtipi/repos/repo-id'] = '';
-  MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
+  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}/docker-compose.yml`] = 'compose';
   MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
   MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
 
 

+ 85 - 6
packages/system-api/src/modules/apps/__tests__/apps.helpers.test.ts

@@ -2,10 +2,11 @@ import { faker } from '@faker-js/faker';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import { DataSource } from 'typeorm';
 import { DataSource } from 'typeorm';
 import logger from '../../../config/logger/logger';
 import logger from '../../../config/logger/logger';
+import { setConfig } from '../../../core/config/TipiConfig';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import App from '../app.entity';
 import App from '../app.entity';
 import { checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
 import { checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
-import { AppCategoriesEnum, AppInfo } from '../apps.types';
+import { AppInfo, AppSupportedArchitecturesEnum } from '../apps.types';
 import { createApp, createAppConfig } from './apps.factory';
 import { createApp, createAppConfig } from './apps.factory';
 
 
 jest.mock('fs-extra');
 jest.mock('fs-extra');
@@ -23,6 +24,13 @@ afterAll(async () => {
   await teardownConnection(TEST_SUITE);
   await teardownConnection(TEST_SUITE);
 });
 });
 
 
+beforeEach(async () => {
+  jest.resetModules();
+  jest.resetAllMocks();
+  jest.restoreAllMocks();
+  await App.clear();
+});
+
 describe('checkAppRequirements', () => {
 describe('checkAppRequirements', () => {
   let app1: AppInfo;
   let app1: AppInfo;
 
 
@@ -33,13 +41,34 @@ describe('checkAppRequirements', () => {
     fs.__createMockFiles(app1create.MockFiles);
     fs.__createMockFiles(app1create.MockFiles);
   });
   });
 
 
-  it('should return true if there are no particular requirement', async () => {
-    const ivValid = await checkAppRequirements(app1.id);
-    expect(ivValid).toBe(true);
+  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 () => {
   it('Should throw an error if app does not exist', async () => {
-    await expect(checkAppRequirements('not-existing-app')).rejects.toThrow('App not-existing-app has invalid config.json file');
+    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`);
+    }
   });
   });
 });
 });
 
 
@@ -60,7 +89,7 @@ describe('getEnvMap', () => {
   });
   });
 });
 });
 
 
-describe('checkEnvFile', () => {
+describe('Test: checkEnvFile', () => {
   let app1: AppInfo;
   let app1: AppInfo;
 
 
   beforeEach(async () => {
   beforeEach(async () => {
@@ -90,6 +119,25 @@ describe('checkEnvFile', () => {
       }
       }
     }
     }
   });
   });
+
+  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', () => {
 describe('Test: generateEnvFile', () => {
@@ -235,6 +283,18 @@ describe('getAvailableApps', () => {
 
 
     expect(availableApps.length).toBe(2);
     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', () => {
 describe('Test: getAppInfo', () => {
@@ -358,6 +418,25 @@ describe('getUpdateInfo', () => {
 
 
     expect(updateInfo).toBeNull();
     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 version 0 if version is not provided', async () => {
+    // @ts-ignore
+    const updateInfo = await getUpdateInfo(app1.id);
+
+    expect(updateInfo?.latest).toBe(app1.tipi_version);
+    expect(updateInfo?.current).toBe(0);
+  });
 });
 });
 
 
 describe('Test: ensureAppFolder', () => {
 describe('Test: ensureAppFolder', () => {

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

@@ -155,6 +155,8 @@ describe('Install app', () => {
   });
   });
 
 
   it('Should throw if architecure is not supported', async () => {
   it('Should throw if architecure is not supported', async () => {
+    // arrange
+    setConfig('architecture', AppSupportedArchitecturesEnum.AMD64);
     const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
     const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
@@ -185,6 +187,29 @@ describe('Install app', () => {
 
 
     expect(app).toBeDefined();
     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', () => {
 describe('Uninstall app', () => {
@@ -404,6 +429,17 @@ describe('Update app config', () => {
     await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
     await AppsService.updateAppConfig(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
     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', () => {
 describe('Get app config', () => {
@@ -504,6 +540,23 @@ describe('List apps', () => {
     expect(apps).toBeDefined();
     expect(apps).toBeDefined();
     expect(apps.length).toBe(1);
     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', () => {
 describe('Start all apps', () => {

+ 20 - 12
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -6,6 +6,7 @@ import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnu
 import logger from '../../config/logger/logger';
 import logger from '../../config/logger/logger';
 import { getConfig } from '../../core/config/TipiConfig';
 import { getConfig } from '../../core/config/TipiConfig';
 import { AppEntityType } from './app.types';
 import { AppEntityType } from './app.types';
+import { notEmpty } from '../../helpers/helpers';
 
 
 const formFieldSchema = z.object({
 const formFieldSchema = z.object({
   type: z.nativeEnum(FieldTypes),
   type: z.nativeEnum(FieldTypes),
@@ -39,7 +40,7 @@ export const appInfoSchema = z.object({
   supported_architectures: z.nativeEnum(AppSupportedArchitecturesEnum).array().optional(),
   supported_architectures: z.nativeEnum(AppSupportedArchitecturesEnum).array().optional(),
 });
 });
 
 
-export const checkAppRequirements = async (appName: string) => {
+export const checkAppRequirements = (appName: string) => {
   const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
   const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
   const parsedConfig = appInfoSchema.safeParse(configFile);
   const parsedConfig = appInfoSchema.safeParse(configFile);
 
 
@@ -51,7 +52,7 @@ export const checkAppRequirements = async (appName: string) => {
     throw new Error(`App ${appName} is not supported on this architecture`);
     throw new Error(`App ${appName} is not supported on this architecture`);
   }
   }
 
 
-  return true;
+  return parsedConfig.data;
 };
 };
 
 
 export const getEnvMap = (appName: string): Map<string, string> => {
 export const getEnvMap = (appName: string): Map<string, string> => {
@@ -77,7 +78,7 @@ export const checkEnvFile = (appName: string) => {
 
 
   const envMap = getEnvMap(appName);
   const envMap = getEnvMap(appName);
 
 
-  parsedConfig.data.form_fields?.forEach((field) => {
+  parsedConfig.data.form_fields.forEach((field) => {
     const envVar = field.env_variable;
     const envVar = field.env_variable;
     const envVarValue = envMap.get(envVar);
     const envVarValue = envMap.get(envVar);
 
 
@@ -141,21 +142,28 @@ export const generateEnvFile = (app: AppEntityType) => {
   writeFile(`/app/storage/app-data/${app.id}/app.env`, envFile);
   writeFile(`/app/storage/app-data/${app.id}/app.env`, envFile);
 };
 };
 
 
-export const getAvailableApps = async (): Promise<string[]> => {
-  const apps: string[] = [];
-
+export const getAvailableApps = async (): Promise<AppInfo[]> => {
   const appsDir = readdirSync(`/runtipi/repos/${getConfig().appsRepoId}/apps`);
   const appsDir = readdirSync(`/runtipi/repos/${getConfig().appsRepoId}/apps`);
 
 
-  appsDir.forEach((app) => {
-    if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)) {
+  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 configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
       const parsedConfig = appInfoSchema.safeParse(configFile);
       const parsedConfig = appInfoSchema.safeParse(configFile);
 
 
-      if (parsedConfig.success && parsedConfig.data.available) {
-        apps.push(app);
+      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;
   return apps;
 };
 };

+ 5 - 26
packages/system-api/src/modules/apps/apps.service.ts

@@ -1,13 +1,12 @@
 import validator from 'validator';
 import validator from 'validator';
 import { Not } from 'typeorm';
 import { Not } from 'typeorm';
-import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
+import { createFolder, readJsonFile } from '../fs/fs.helpers';
 import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, appInfoSchema } from './apps.helpers';
 import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, appInfoSchema } from './apps.helpers';
 import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
 import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
 import App from './app.entity';
 import App from './app.entity';
 import logger from '../../config/logger/logger';
 import logger from '../../config/logger/logger';
 import { getConfig } from '../../core/config/TipiConfig';
 import { getConfig } from '../../core/config/TipiConfig';
 import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
 import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
-import { notEmpty } from '../../helpers/helpers';
 
 
 const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
 const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
 const filterApp = (app: AppInfo): boolean => {
 const filterApp = (app: AppInfo): boolean => {
@@ -107,11 +106,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
     }
     }
 
 
     ensureAppFolder(id, true);
     ensureAppFolder(id, true);
-    const appIsValid = await checkAppRequirements(id);
-
-    if (!appIsValid) {
-      throw new Error(`App ${id} requirements not met`);
-    }
+    checkAppRequirements(id);
 
 
     // Create app folder
     // Create app folder
     createFolder(`/app/storage/app-data/${id}`);
     createFolder(`/app/storage/app-data/${id}`);
@@ -120,7 +115,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
     const parsedAppInfo = appInfoSchema.safeParse(appInfo);
     const parsedAppInfo = appInfoSchema.safeParse(appInfo);
 
 
     if (!parsedAppInfo.success) {
     if (!parsedAppInfo.success) {
-      throw new Error(`App ${id} config.json is not valid`);
+      throw new Error(`App ${id} has invalid config.json file`);
     }
     }
 
 
     if (!parsedAppInfo.data.exposable && exposed) {
     if (!parsedAppInfo.data.exposable && exposed) {
@@ -159,25 +154,9 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
  * @returns - list of all apps available
  * @returns - list of all apps available
  */
  */
 const listApps = async (): Promise<ListAppsResonse> => {
 const listApps = async (): Promise<ListAppsResonse> => {
-  const folders: string[] = await getAvailableApps();
-
-  const apps = folders.map((app) => readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)).filter(Boolean);
-
-  const parsedApps = apps
-    .map((app) => {
-      const result = appInfoSchema.safeParse(app);
-
-      if (!result.success) {
-        logger.error(`App ${JSON.stringify(app)} has invalid config.json`);
-        return null;
-      }
-
-      const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${result.data.id}/metadata/description.md`);
-      return { ...result.data, description };
-    })
-    .filter(notEmpty);
+  const apps = await getAvailableApps();
 
 
-  const filteredApps = filterApps(parsedApps);
+  const filteredApps = filterApps(apps);
 
 
   return { apps: filteredApps, total: apps.length };
   return { apps: filteredApps, total: apps.length };
 };
 };

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

@@ -86,12 +86,6 @@ class FormField {
   env_variable!: string;
   env_variable!: string;
 }
 }
 
 
-@ObjectType()
-class Requirements {
-  @Field(() => [Number], { nullable: true })
-  ports?: number[];
-}
-
 @ObjectType()
 @ObjectType()
 class AppInfo {
 class AppInfo {
   @Field(() => String)
   @Field(() => String)
@@ -133,9 +127,6 @@ class AppInfo {
   @Field(() => [FormField])
   @Field(() => [FormField])
   form_fields?: FormField[];
   form_fields?: FormField[];
 
 
-  @Field(() => GraphQLJSONObject, { nullable: true })
-  requirements?: Requirements;
-
   @Field(() => Boolean, { nullable: true })
   @Field(() => Boolean, { nullable: true })
   https?: boolean;
   https?: boolean;
 
 

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

@@ -20,7 +20,6 @@ mutation InstallApp($input: AppInputType!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
   }
   }
 }
 }

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

@@ -21,7 +21,6 @@ mutation StartApp($id: String!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     updateInfo {
     updateInfo {
       current
       current

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

@@ -20,7 +20,6 @@ mutation StopApp($id: String!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     updateInfo {
     updateInfo {
       current
       current

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

@@ -20,7 +20,6 @@ mutation UninstallApp($id: String!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     updateInfo {
     updateInfo {
       current
       current

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

@@ -20,7 +20,6 @@ mutation UpdateApp($id: String!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     updateInfo {
     updateInfo {
       current
       current

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

@@ -20,7 +20,6 @@ mutation UpdateAppConfig($input: AppInputType!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     updateInfo {
     updateInfo {
       current
       current

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

@@ -19,7 +19,6 @@ query GetApp($id: String!) {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     updateInfo {
     updateInfo {
       current
       current

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

@@ -24,7 +24,6 @@ query {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
   }
   }
 }
 }

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

@@ -17,7 +17,6 @@ query {
         required
         required
         env_variable
         env_variable
       }
       }
-      requirements
     }
     }
     total
     total
   }
   }