test(wip): app resolvers
This commit is contained in:
parent
c37a0eb6d5
commit
3036bedf81
15 changed files with 258 additions and 26 deletions
|
@ -16,5 +16,7 @@ module.exports = {
|
|||
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': 0,
|
||||
'no-unused-vars': [1, { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
};
|
||||
|
|
11
packages/system-api/__mocks__/internal-ip.ts
Normal file
11
packages/system-api/__mocks__/internal-ip.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const internalIp: { v4: typeof v4Mock } = jest.genMockFromModule('internal-ip');
|
||||
|
||||
const v4Mock = () => {
|
||||
return faker.internet.ipv4();
|
||||
};
|
||||
|
||||
internalIp.v4 = v4Mock;
|
||||
|
||||
module.exports = internalIp;
|
9
packages/system-api/__mocks__/tcp-port-used.ts
Normal file
9
packages/system-api/__mocks__/tcp-port-used.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import portUsed, { TcpPortUsedOptions } from 'tcp-port-used';
|
||||
|
||||
const internalIp: { check: typeof portUsed.check } = jest.genMockFromModule('tcp-port-used');
|
||||
|
||||
internalIp.check = async (_: number | TcpPortUsedOptions, __?: string | undefined) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = internalIp;
|
|
@ -6,7 +6,7 @@ module.exports = {
|
|||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
setupFiles: ['<rootDir>/src/test/dotenv-config.ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/migrations/**/*.{ts,tsx}', '!**/config/**/*.{ts,tsx}'],
|
||||
passWithNoTests: true,
|
||||
transform: {
|
||||
'^.+\\.graphql$': 'graphql-import-node/jest',
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { AuthChecker } from 'type-graphql';
|
||||
import User from '../../modules/auth/user.entity';
|
||||
import { MyContext } from '../../types';
|
||||
|
||||
export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => {
|
||||
export const customAuthChecker: AuthChecker<MyContext> = async ({ context }) => {
|
||||
// here we can read the user from context
|
||||
// and check his permission in the db against the `roles` argument
|
||||
// that comes from the `@Authorized` decorator, eg. ["ADMIN", "MODERATOR"]
|
||||
|
@ -9,5 +10,12 @@ export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => {
|
|||
return false;
|
||||
}
|
||||
|
||||
const { userId } = context.req.session;
|
||||
const user = await User.findOne({ where: { id: userId } });
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { ObjectType, Field } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
class FieldError {
|
||||
@Field()
|
||||
code!: number;
|
||||
|
||||
@Field()
|
||||
message!: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
field?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class ErrorResponse {
|
||||
@Field(() => [FieldError], { nullable: true })
|
||||
errors?: FieldError[];
|
||||
}
|
||||
|
||||
export { FieldError, ErrorResponse };
|
|
@ -3,7 +3,7 @@ import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.t
|
|||
import config from '../../../config';
|
||||
import App from '../app.entity';
|
||||
|
||||
const createApp = async (installed = false, status = AppStatusEnum.RUNNING) => {
|
||||
const createApp = async (installed = false, status = AppStatusEnum.RUNNING, requiredPort?: number) => {
|
||||
const categories = Object.values(AppCategoriesEnum);
|
||||
|
||||
const appInfo: AppInfo = {
|
||||
|
@ -18,6 +18,11 @@ const createApp = async (installed = false, status = AppStatusEnum.RUNNING) => {
|
|||
env_variable: 'TEST_FIELD',
|
||||
},
|
||||
],
|
||||
requirements: requiredPort
|
||||
? {
|
||||
ports: [requiredPort],
|
||||
}
|
||||
: undefined,
|
||||
name: faker.random.word(),
|
||||
description: faker.random.words(),
|
||||
image: faker.internet.url(),
|
||||
|
|
|
@ -3,11 +3,17 @@ import { setupConnection, teardownConnection } from '../../../test/connection';
|
|||
import fs from 'fs';
|
||||
import { gcall } from '../../../test/gcall';
|
||||
import App from '../app.entity';
|
||||
import { getAppQuery, listAppInfosQuery } from '../../../test/queries';
|
||||
import { getAppQuery, InstalledAppsQuery, listAppInfosQuery } from '../../../test/queries';
|
||||
import { createApp } from './apps.factory';
|
||||
import { AppInfo, AppStatusEnum, ListAppsResonse } from '../apps.types';
|
||||
import { createUser } from '../../auth/__tests__/user.factory';
|
||||
import User from '../../auth/user.entity';
|
||||
import { installAppMutation } from '../../../test/mutations';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('child_process');
|
||||
jest.mock('internal-ip');
|
||||
jest.mock('tcp-port-used');
|
||||
|
||||
type TApp = App & {
|
||||
info: AppInfo;
|
||||
|
@ -30,6 +36,7 @@ beforeEach(async () => {
|
|||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
await App.clear();
|
||||
await User.clear();
|
||||
});
|
||||
|
||||
describe('ListAppsInfos', () => {
|
||||
|
@ -97,3 +104,134 @@ describe('GetApp', () => {
|
|||
expect(data?.getApp).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstalledApps', () => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(app1create.MockFiles);
|
||||
});
|
||||
|
||||
it('Can list installed apps', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
const { data } = await gcall<{ installedApps: TApp[] }>({ source: InstalledAppsQuery, userId: user.id });
|
||||
|
||||
expect(data?.installedApps.length).toBe(1);
|
||||
|
||||
const app = data?.installedApps[0];
|
||||
|
||||
expect(app?.id).toBe(app1.id);
|
||||
expect(app?.info.author).toBe(app1.author);
|
||||
expect(app?.info.name).toBe(app1.name);
|
||||
});
|
||||
|
||||
it("Should return an error if user doesn't exist", async () => {
|
||||
const { data, errors } = await gcall<{ installedApps: TApp[] }>({
|
||||
source: InstalledAppsQuery,
|
||||
userId: 1,
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||
expect(data?.installedApps).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should throw an error if no userId is provided', async () => {
|
||||
const { data, errors } = await gcall<{ installedApps: TApp[] }>({
|
||||
source: InstalledAppsQuery,
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||
expect(data?.installedApps).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstallApp', () => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp();
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(app1create.MockFiles);
|
||||
});
|
||||
|
||||
it('Can install app', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
const { data } = await gcall<{ installApp: TApp }>({
|
||||
source: installAppMutation,
|
||||
userId: user.id,
|
||||
variableValues: { input: { id: app1.id, form: { TEST_FIELD: 'hello' } } },
|
||||
});
|
||||
|
||||
expect(data?.installApp.info.id).toBe(app1.id);
|
||||
expect(data?.installApp.status).toBe(AppStatusEnum.RUNNING.toUpperCase());
|
||||
});
|
||||
|
||||
it("Should return an error if app doesn't exist", async () => {
|
||||
const user = await createUser();
|
||||
|
||||
const { data, errors } = await gcall<{ installApp: TApp }>({
|
||||
source: installAppMutation,
|
||||
userId: user.id,
|
||||
variableValues: { input: { id: 'not-existing', form: { TEST_FIELD: 'hello' } } },
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe('App not-existing not found');
|
||||
expect(data?.installApp).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Should throw an error if user doesn't exist", async () => {
|
||||
const { data, errors } = await gcall<{ installApp: TApp }>({
|
||||
source: installAppMutation,
|
||||
variableValues: { input: { id: app1.id, form: { TEST_FIELD: 'hello' } } },
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||
expect(data?.installApp).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should throw an error if no userId is provided', async () => {
|
||||
const { data, errors } = await gcall<{ installApp: TApp }>({
|
||||
source: installAppMutation,
|
||||
variableValues: { input: { id: app1.id, form: { TEST_FIELD: 'hello' } } },
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe('Access denied! You need to be authorized to perform this action!');
|
||||
expect(data?.installApp).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should throw an error if a required field is missing in form', async () => {
|
||||
const user = await createUser();
|
||||
|
||||
const { data, errors } = await gcall<{ installApp: TApp }>({
|
||||
source: installAppMutation,
|
||||
userId: user.id,
|
||||
variableValues: { input: { id: app1.id, form: {} } },
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe(`Variable ${app1.form_fields?.[0].env_variable} is required`);
|
||||
expect(data?.installApp).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should throw an error if the requirements are not met', async () => {
|
||||
const { appInfo, MockFiles } = await createApp(false, undefined, 400);
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
const user = await createUser();
|
||||
|
||||
const { data, errors } = await gcall<{ installApp: TApp }>({
|
||||
source: installAppMutation,
|
||||
userId: user.id,
|
||||
variableValues: { input: { id: appInfo.id, form: { TEST_FIELD: 'hello' } } },
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toBe(`App ${appInfo.id} requirements not met`);
|
||||
expect(data?.installApp).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,10 @@ export const checkAppRequirements = async (appName: string) => {
|
|||
let valid = true;
|
||||
const configFile: AppInfo = readJsonFile(`/apps/${appName}/config.json`);
|
||||
|
||||
if (!configFile) {
|
||||
throw new Error(`App ${appName} not found`);
|
||||
}
|
||||
|
||||
if (configFile?.requirements?.ports) {
|
||||
for (const port of configFile.requirements.ports) {
|
||||
const ip = await InternalIp.v4();
|
||||
|
|
|
@ -5,7 +5,12 @@ import config from '../../config';
|
|||
export const getAbsolutePath = (path: string) => `${config.ROOT_FOLDER}${path}`;
|
||||
|
||||
export const readJsonFile = (path: string): any => {
|
||||
const rawFile = fs.readFileSync(getAbsolutePath(path)).toString();
|
||||
const rawFile = fs.readFileSync(getAbsolutePath(path))?.toString();
|
||||
|
||||
if (!rawFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(rawFile);
|
||||
};
|
||||
|
||||
|
|
7
packages/system-api/src/test/mutations/index.ts
Normal file
7
packages/system-api/src/test/mutations/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import 'graphql-import-node';
|
||||
import { print } from 'graphql/language/printer';
|
||||
|
||||
import * as installApp from './installApp.graphql';
|
||||
|
||||
export const installAppMutation = print(installApp);
|
26
packages/system-api/src/test/mutations/installApp.graphql
Normal file
26
packages/system-api/src/test/mutations/installApp.graphql
Normal file
|
@ -0,0 +1,26 @@
|
|||
mutation InstallApp($input: AppInputType!) {
|
||||
installApp(input: $input) {
|
||||
id
|
||||
status
|
||||
config
|
||||
info {
|
||||
id
|
||||
available
|
||||
port
|
||||
name
|
||||
description
|
||||
version
|
||||
author
|
||||
source
|
||||
categories
|
||||
url_suffix
|
||||
form_fields {
|
||||
max
|
||||
min
|
||||
required
|
||||
env_variable
|
||||
}
|
||||
requirements
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import { print } from 'graphql/language/printer';
|
|||
|
||||
import * as listAppInfos from './listAppInfos.graphql';
|
||||
import * as getApp from './getApp.graphql';
|
||||
import * as InstalledApps from './InstalledApps.graphql';
|
||||
|
||||
export const listAppInfosQuery = print(listAppInfos);
|
||||
export const getAppQuery = print(getApp);
|
||||
export const InstalledAppsQuery = print(InstalledApps);
|
||||
|
|
30
packages/system-api/src/test/queries/installedApps.graphql
Normal file
30
packages/system-api/src/test/queries/installedApps.graphql
Normal file
|
@ -0,0 +1,30 @@
|
|||
query {
|
||||
installedApps {
|
||||
id
|
||||
status
|
||||
lastOpened
|
||||
numOpened
|
||||
config
|
||||
createdAt
|
||||
updatedAt
|
||||
info {
|
||||
id
|
||||
available
|
||||
port
|
||||
name
|
||||
description
|
||||
version
|
||||
author
|
||||
source
|
||||
categories
|
||||
url_suffix
|
||||
form_fields {
|
||||
max
|
||||
min
|
||||
required
|
||||
env_variable
|
||||
}
|
||||
requirements
|
||||
}
|
||||
}
|
||||
}
|
6
packages/system-api/src/test/queries/me.graphql
Normal file
6
packages/system-api/src/test/queries/me.graphql
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
me {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue