chore: update tests to cover invalid config.json

This commit is contained in:
Nicolas Meienberger 2022-11-17 20:37:23 +01:00
parent 1e12261614
commit a76a8100ee
20 changed files with 467 additions and 378 deletions

View file

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

View file

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

View file

@ -65,7 +65,6 @@ export type AppInfo = {
name: Scalars['String'];
no_gui?: Maybe<Scalars['Boolean']>;
port: Scalars['Float'];
requirements?: Maybe<Scalars['JSONObject']>;
short_desc: Scalars['String'];
source: Scalars['String'];
supported_architectures?: Maybe<Array<AppSupportedArchitecturesEnum>>;
@ -155,34 +154,42 @@ export type Mutation = {
updateAppConfig: App;
};
export type MutationInstallAppArgs = {
input: AppInputType;
};
export type MutationLoginArgs = {
input: UsernamePasswordInput;
};
export type MutationRegisterArgs = {
input: UsernamePasswordInput;
};
export type MutationStartAppArgs = {
id: Scalars['String'];
};
export type MutationStopAppArgs = {
id: Scalars['String'];
};
export type MutationUninstallAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppConfigArgs = {
input: AppInputType;
};
@ -199,6 +206,7 @@ export type Query = {
version: VersionResponse;
};
export type QueryGetAppArgs = {
id: Scalars['String'];
};
@ -245,184 +253,125 @@ export type InstallAppMutationVariables = Exact<{
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<{
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<{
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 RestartMutation = { __typename?: 'Mutation'; restart: boolean };
export type RestartMutationVariables = Exact<{ [key: string]: never; }>;
export type RestartMutation = { __typename?: 'Mutation', restart: boolean };
export type StartAppMutationVariables = Exact<{
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<{
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<{
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 UpdateMutation = { __typename?: 'Mutation'; update: boolean };
export type UpdateMutationVariables = Exact<{ [key: string]: never; }>;
export type UpdateMutation = { __typename?: 'Mutation', update: boolean };
export type UpdateAppMutationVariables = Exact<{
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<{
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<{
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`
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>;
/**
@ -450,12 +399,12 @@ export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutati
export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
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>;
/**
@ -483,10 +432,10 @@ export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
export const LogoutDocument = gql`
mutation Logout {
logout
}
`;
mutation Logout {
logout
}
`;
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 LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
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>;
/**
@ -546,10 +495,10 @@ export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
export const RestartDocument = gql`
mutation Restart {
restart
}
`;
mutation Restart {
restart
}
`;
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 RestartMutationOptions = Apollo.BaseMutationOptions<RestartMutation, RestartMutationVariables>;
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>;
/**
@ -611,14 +560,14 @@ export type StartAppMutationHookResult = ReturnType<typeof useStartAppMutation>;
export type StartAppMutationResult = Apollo.MutationResult<StartAppMutation>;
export type StartAppMutationOptions = Apollo.BaseMutationOptions<StartAppMutation, StartAppMutationVariables>;
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>;
/**
@ -646,14 +595,14 @@ export type StopAppMutationHookResult = ReturnType<typeof useStopAppMutation>;
export type StopAppMutationResult = Apollo.MutationResult<StopAppMutation>;
export type StopAppMutationOptions = Apollo.BaseMutationOptions<StopAppMutation, StopAppMutationVariables>;
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>;
/**
@ -681,10 +630,10 @@ export type UninstallAppMutationHookResult = ReturnType<typeof useUninstallAppMu
export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
export const UpdateDocument = gql`
mutation Update {
update
}
`;
mutation Update {
update
}
`;
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 UpdateMutationOptions = Apollo.BaseMutationOptions<UpdateMutation, UpdateMutationVariables>;
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>;
/**
@ -746,14 +695,14 @@ export type UpdateAppMutationHookResult = ReturnType<typeof useUpdateAppMutation
export type UpdateAppMutationResult = Apollo.MutationResult<UpdateAppMutation>;
export type UpdateAppMutationOptions = Apollo.BaseMutationOptions<UpdateAppMutation, UpdateAppMutationVariables>;
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>;
/**
@ -781,49 +730,49 @@ export type UpdateAppConfigMutationHookResult = ReturnType<typeof useUpdateAppCo
export type UpdateAppConfigMutationResult = Apollo.MutationResult<UpdateAppConfigMutation>;
export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
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
status
config
port
name
description
available
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__
@ -853,28 +802,28 @@ export type GetAppQueryHookResult = ReturnType<typeof useGetAppQuery>;
export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
export const InstalledAppsDocument = gql`
query InstalledApps {
installedApps {
query InstalledApps {
installedApps {
id
status
config
version
updateInfo {
current
latest
dockerVersion
}
info {
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__
@ -903,10 +852,10 @@ export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQue
export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
export const ConfiguredDocument = gql`
query Configured {
isConfigured
}
`;
query Configured {
isConfigured
}
`;
/**
* __useConfiguredQuery__
@ -935,24 +884,24 @@ export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
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__
@ -981,12 +930,12 @@ export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
export const MeDocument = gql`
query Me {
me {
id
}
query Me {
me {
id
}
`;
}
`;
/**
* __useMeQuery__
@ -1015,12 +964,12 @@ export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
export const RefreshTokenDocument = gql`
query RefreshToken {
refreshToken {
token
}
query RefreshToken {
refreshToken {
token
}
`;
}
`;
/**
* __useRefreshTokenQuery__
@ -1049,24 +998,24 @@ export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery
export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
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__
@ -1095,13 +1044,13 @@ export type SystemInfoQueryHookResult = ReturnType<typeof useSystemInfoQuery>;
export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
export const VersionDocument = gql`
query Version {
version {
current
latest
}
query Version {
version {
current
latest
}
`;
}
`;
/**
* __useVersionQuery__
@ -1128,4 +1077,4 @@ export function useVersionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ve
}
export type VersionQueryHookResult = ReturnType<typeof useVersionQuery>;
export type VersionLazyQueryHookResult = ReturnType<typeof useVersionLazyQuery>;
export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;
export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;

View file

@ -1,9 +1,11 @@
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';
@ -30,7 +32,8 @@ afterAll(async () => {
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', () => {
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', () => {
beforeEach(async () => {
const { MockFiles } = await createApp({});
MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([]);
MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createAppState([]);
// @ts-ignore
fs.__createMockFiles(MockFiles);
});
@ -87,7 +90,7 @@ describe('State/apps.json exists with one installed app', () => {
beforeEach(async () => {
const { MockFiles, appInfo } = await createApp({});
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}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
// @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 () => {
const { MockFiles, appInfo } = await createApp({ installed: true });
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}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
// @ts-ignore
@ -129,3 +132,50 @@ describe('State/apps.json exists with one installed app', () => {
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');
});
});

View file

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

View file

@ -1,6 +1,7 @@
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;
@ -17,21 +18,22 @@ type CreateConfigParams = {
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 { 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);
@ -66,16 +68,10 @@ const createApp = async (props: IProps) => {
});
}
if (requiredPort) {
appInfo.requirements = {
ports: [requiredPort],
};
}
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(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}/metadata/description.md`] = 'md desc';

View file

@ -2,10 +2,11 @@ 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 { AppCategoriesEnum, AppInfo } from '../apps.types';
import { AppInfo, AppSupportedArchitecturesEnum } from '../apps.types';
import { createApp, createAppConfig } from './apps.factory';
jest.mock('fs-extra');
@ -23,6 +24,13 @@ afterAll(async () => {
await teardownConnection(TEST_SUITE);
});
beforeEach(async () => {
jest.resetModules();
jest.resetAllMocks();
jest.restoreAllMocks();
await App.clear();
});
describe('checkAppRequirements', () => {
let app1: AppInfo;
@ -33,13 +41,34 @@ describe('checkAppRequirements', () => {
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 () => {
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;
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', () => {
@ -235,6 +283,18 @@ describe('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', () => {
@ -358,6 +418,25 @@ describe('getUpdateInfo', () => {
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', () => {

View file

@ -155,6 +155,8 @@ describe('Install app', () => {
});
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);
@ -185,6 +187,29 @@ describe('Install app', () => {
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', () => {
@ -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');
});
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', () => {
@ -504,6 +540,23 @@ describe('List apps', () => {
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', () => {

View file

@ -6,6 +6,7 @@ import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnu
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),
@ -39,7 +40,7 @@ export const appInfoSchema = z.object({
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 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`);
}
return true;
return parsedConfig.data;
};
export const getEnvMap = (appName: string): Map<string, string> => {
@ -77,7 +78,7 @@ export const checkEnvFile = (appName: string) => {
const envMap = getEnvMap(appName);
parsedConfig.data.form_fields?.forEach((field) => {
parsedConfig.data.form_fields.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envMap.get(envVar);
@ -141,21 +142,28 @@ export const generateEnvFile = (app: AppEntityType) => {
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`);
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 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;
};

View file

@ -1,13 +1,12 @@
import validator from 'validator';
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 { 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';
import { notEmpty } from '../../helpers/helpers';
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
const filterApp = (app: AppInfo): boolean => {
@ -107,11 +106,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
}
ensureAppFolder(id, true);
const appIsValid = await checkAppRequirements(id);
if (!appIsValid) {
throw new Error(`App ${id} requirements not met`);
}
checkAppRequirements(id);
// Create app folder
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);
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) {
@ -159,25 +154,9 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
* @returns - list of all apps available
*/
const listApps = async (): Promise<ListAppsResonse> => {
const folders: string[] = await getAvailableApps();
const apps = 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 filteredApps = filterApps(parsedApps);
const filteredApps = filterApps(apps);
return { apps: filteredApps, total: apps.length };
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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