test(wip): app resolvers

This commit is contained in:
Nicolas Meienberger 2022-08-01 18:21:02 +02:00
parent c37a0eb6d5
commit 3036bedf81
15 changed files with 258 additions and 26 deletions

View file

@ -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: '^_' }],
},
};

View 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;

View 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;

View file

@ -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',

View file

@ -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;
};

View file

@ -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 };

View file

@ -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(),

View file

@ -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();
});
});

View file

@ -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();

View file

@ -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);
};

View 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);

View 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
}
}
}

View file

@ -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);

View 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
}
}
}

View file

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