Adapt tests
This commit is contained in:
parent
82fb49a684
commit
37662b574b
23 changed files with 371 additions and 504 deletions
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
@ -10,6 +10,19 @@ env:
|
|||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
# set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
@ -61,4 +74,9 @@ jobs:
|
|||
run: pnpm -r lint
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm -r test
|
||||
run: pnpm -r test
|
||||
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/system-api/coverage/clover.xml,./packages/dashboard/coverage/clover.xml
|
5
.github/workflows/release-candidate.yml
vendored
5
.github/workflows/release-candidate.yml
vendored
|
@ -40,7 +40,4 @@ jobs:
|
|||
push: true
|
||||
tags: meienberger/runtipi:rc-${{ steps.meta.outputs.TAG }}
|
||||
cache-from: type=registry,ref=meienberger/runtipi:buildcache
|
||||
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
|
||||
|
||||
|
||||
|
||||
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
|
|
@ -24,7 +24,7 @@ FROM alpine:3.16.0 as app
|
|||
WORKDIR /
|
||||
|
||||
# Install docker
|
||||
RUN apk --no-cache --virtual build-dependencies add docker docker-compose curl nodejs npm bash g++ make
|
||||
RUN apk --no-cache add docker docker-compose curl nodejs npm bash g++ make
|
||||
|
||||
RUN npm install node-gyp -g
|
||||
|
||||
|
@ -42,6 +42,4 @@ COPY ./packages/system-api /api
|
|||
COPY --from=build /dashboard/.next /dashboard/.next
|
||||
COPY ./packages/dashboard /dashboard
|
||||
|
||||
RUN apk del build-dependencies
|
||||
|
||||
WORKDIR /
|
||||
|
|
|
@ -14,6 +14,11 @@ services:
|
|||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DB: ${POSTGRES_DBNAME}
|
||||
healthcheck:
|
||||
test: /usr/bin/pg_isready
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
|
@ -23,7 +28,8 @@ services:
|
|||
dockerfile: Dockerfile.dev
|
||||
command: /bin/sh -c "cd /api && npm run dev"
|
||||
depends_on:
|
||||
- tipi-db
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
container_name: api
|
||||
ports:
|
||||
- 3001:3001
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"start:rc": "docker-compose -f docker-compose.rc.yml --env-file .env up --build",
|
||||
"start:prod": "docker-compose --env-file .env up --build",
|
||||
"build:common": "cd packages/common && npm run build",
|
||||
"start:pg": "docker run --name test-db -p 5432:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres",
|
||||
"version": "echo $npm_package_version"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -4,9 +4,8 @@ module.exports = {
|
|||
verbose: true,
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
setupFiles: ['<rootDir>/tests/dotenv-config.ts'],
|
||||
setupFiles: ['<rootDir>/src/test/dotenv-config.ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
coverageProvider: 'v8',
|
||||
passWithNoTests: true,
|
||||
};
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"winston": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.3.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cors": "^2.8.12",
|
||||
|
@ -67,6 +68,7 @@
|
|||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/passport": "^1.0.7",
|
||||
"@types/passport-http-bearer": "^1.0.37",
|
||||
"@types/pg": "^8.6.5",
|
||||
"@types/session-file-store": "^1.2.2",
|
||||
"@types/tcp-port-used": "^1.0.1",
|
||||
"@types/validator": "^13.7.2",
|
||||
|
|
|
@ -50,7 +50,7 @@ const config: IConfig = {
|
|||
password: POSTGRES_PASSWORD,
|
||||
port: 5432,
|
||||
logging: !__prod__,
|
||||
synchronize: true,
|
||||
synchronize: !__prod__,
|
||||
entities: [App, User],
|
||||
},
|
||||
NODE_ENV,
|
||||
|
|
4
packages/system-api/src/declarations.d.ts
vendored
4
packages/system-api/src/declarations.d.ts
vendored
|
@ -1,4 +0,0 @@
|
|||
declare module 'su-exec' {
|
||||
export function execFile(path: string, args: string[], options: {}, callback?: any): void;
|
||||
export function init(): void;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.types';
|
||||
import config from '../../../config';
|
||||
import App from '../app.entity';
|
||||
|
||||
const createApp = async (installed = false) => {
|
||||
const categories = Object.values(AppCategoriesEnum);
|
||||
|
||||
const appInfo: AppInfo = {
|
||||
id: faker.random.word().toLowerCase().trim(),
|
||||
port: faker.datatype.number({ min: 3000, max: 5000 }),
|
||||
available: true,
|
||||
form_fields: [
|
||||
{
|
||||
type: FieldTypes.text,
|
||||
label: faker.random.word(),
|
||||
required: true,
|
||||
env_variable: 'TEST_FIELD',
|
||||
},
|
||||
],
|
||||
name: faker.random.word(),
|
||||
description: faker.random.words(),
|
||||
image: faker.internet.url(),
|
||||
short_desc: faker.random.words(),
|
||||
author: faker.name.firstName(),
|
||||
source: faker.internet.url(),
|
||||
categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
|
||||
};
|
||||
|
||||
let MockFiles: any = {};
|
||||
MockFiles[`${config.ROOT_FOLDER}/.env`] = 'TEST=test';
|
||||
MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
|
||||
MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
|
||||
if (installed) {
|
||||
await App.create({
|
||||
id: appInfo.id,
|
||||
config: { TEST_FIELD: 'test' },
|
||||
status: AppStatusEnum.RUNNING,
|
||||
}).save();
|
||||
|
||||
MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}`] = '';
|
||||
MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
|
||||
}
|
||||
|
||||
return { appInfo, MockFiles };
|
||||
};
|
||||
|
||||
export { createApp };
|
|
@ -2,266 +2,287 @@ import AppsService from '../apps.service';
|
|||
import fs from 'fs';
|
||||
import config from '../../../config';
|
||||
import childProcess from 'child_process';
|
||||
import { AppConfig, FieldTypes } from '../apps.types';
|
||||
import { AppInfo, AppStatusEnum } from '../apps.types';
|
||||
import App from '../app.entity';
|
||||
import { createApp } from './apps.factory';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('child_process');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
let db: DataSource | null = null;
|
||||
const TEST_SUITE = 'appsservice';
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await setupConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
const testApp: Partial<AppConfig> = {
|
||||
id: 'test-app',
|
||||
port: 3000,
|
||||
available: true,
|
||||
form_fields: [
|
||||
{
|
||||
type: FieldTypes.text,
|
||||
label: 'Test field',
|
||||
required: true,
|
||||
env_variable: 'TEST_FIELD',
|
||||
},
|
||||
{
|
||||
type: FieldTypes.text,
|
||||
label: 'Test field 2',
|
||||
required: false,
|
||||
env_variable: 'TEST_FIELD_2',
|
||||
},
|
||||
],
|
||||
};
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
await App.clear();
|
||||
});
|
||||
|
||||
const testApp2: Partial<AppConfig> = {
|
||||
available: true,
|
||||
id: 'test-app2',
|
||||
};
|
||||
|
||||
const testApp3: Partial<AppConfig> = {
|
||||
id: 'test-app3',
|
||||
};
|
||||
|
||||
const MOCK_FILE_EMPTY = {
|
||||
[`${config.ROOT_FOLDER}/apps/test-app/config.json`]: JSON.stringify(testApp),
|
||||
[`${config.ROOT_FOLDER}/apps/test-app/metadata/description.md`]: 'md desc',
|
||||
[`${config.ROOT_FOLDER}/.env`]: 'TEST=test',
|
||||
[`${config.ROOT_FOLDER}/state/apps.json`]: '{"installed": ""}',
|
||||
};
|
||||
|
||||
const MOCK_FILE_INSTALLED = {
|
||||
[`${config.ROOT_FOLDER}/apps/test-app/config.json`]: JSON.stringify(testApp),
|
||||
[`${config.ROOT_FOLDER}/apps/test-app/metadata/description.md`]: 'md desc',
|
||||
[`${config.ROOT_FOLDER}/apps/test-app2/config.json`]: JSON.stringify(testApp2),
|
||||
[`${config.ROOT_FOLDER}/apps/test-app2/metadata/description.md`]: 'md desc',
|
||||
[`${config.ROOT_FOLDER}/apps/test-app3/config.json`]: JSON.stringify(testApp3),
|
||||
[`${config.ROOT_FOLDER}/apps/test-app3/metadata/description.md`]: 'md desc',
|
||||
[`${config.ROOT_FOLDER}/.env`]: 'TEST=test',
|
||||
[`${config.ROOT_FOLDER}/state/apps.json`]: '{"installed": "test-app"}',
|
||||
[`${config.ROOT_FOLDER}/app-data/test-app`]: '',
|
||||
[`${config.ROOT_FOLDER}/app-data/test-app/app.env`]: 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test',
|
||||
};
|
||||
afterAll(async () => {
|
||||
await db?.destroy();
|
||||
await teardownConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
describe('Install app', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { MockFiles, appInfo } = await createApp();
|
||||
app1 = appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_EMPTY);
|
||||
fs.__createMockFiles(MockFiles);
|
||||
});
|
||||
|
||||
it('Should correctly generate env file for app', async () => {
|
||||
await AppsService.installApp('test-app', { test: 'test' });
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
|
||||
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/test-app/app.env`).toString();
|
||||
|
||||
expect(envFile.trim()).toBe('TEST=test\nAPP_PORT=3000\nTEST_FIELD=test');
|
||||
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test`);
|
||||
});
|
||||
|
||||
it('Should add app to state file', async () => {
|
||||
await AppsService.installApp('test-app', { test: 'test' });
|
||||
it('Should add app in database', async () => {
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
const stateFile = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString());
|
||||
const app = await App.findOne({ where: { id: app1.id } });
|
||||
|
||||
expect(stateFile.installed).toBe('test-app');
|
||||
expect(app).toBeDefined();
|
||||
expect(app!.id).toBe(app1.id);
|
||||
expect(app!.config).toStrictEqual({ TEST_FIELD: 'test' });
|
||||
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
||||
});
|
||||
|
||||
it('Should correctly run app script', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
await AppsService.installApp('test-app', { test: 'test' });
|
||||
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', 'test-app', '/tipi'], {}, expect.any(Function)]);
|
||||
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should start app if already installed', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
|
||||
await AppsService.installApp('test-app', { test: 'test' });
|
||||
await AppsService.installApp('test-app', { test: 'test' });
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', 'test-app', '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', 'test-app', '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should throw if required form fields are missing', async () => {
|
||||
await expect(AppsService.installApp('test-app', {})).rejects.toThrowError('Variable test is required');
|
||||
await expect(AppsService.installApp(app1.id, {})).rejects.toThrowError('Variable TEST_FIELD is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall app', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_INSTALLED);
|
||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||
});
|
||||
|
||||
it('Should correctly remove app from state file', async () => {
|
||||
await AppsService.uninstallApp('test-app');
|
||||
it('App should be installed by default', async () => {
|
||||
const app = await App.findOne({ where: { id: app1.id } });
|
||||
expect(app).toBeDefined();
|
||||
expect(app!.id).toBe(app1.id);
|
||||
expect(app!.status).toBe(AppStatusEnum.RUNNING);
|
||||
});
|
||||
|
||||
const stateFile = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString());
|
||||
it('Should correctly remove app from database', async () => {
|
||||
await AppsService.uninstallApp(app1.id);
|
||||
|
||||
expect(stateFile.installed).toBe('');
|
||||
const app = await App.findOne({ where: { id: app1.id } });
|
||||
|
||||
expect(app).toBeNull();
|
||||
});
|
||||
|
||||
it('Should correctly run app script', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
|
||||
await AppsService.uninstallApp('test-app');
|
||||
await AppsService.uninstallApp(app1.id);
|
||||
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', 'test-app', '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should stop app if it is running', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
|
||||
await AppsService.uninstallApp(app1.id);
|
||||
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
await expect(AppsService.uninstallApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
|
||||
await expect(AppsService.uninstallApp('any')).rejects.toThrowError('App any not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Start app', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_INSTALLED);
|
||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||
});
|
||||
|
||||
it('Should correctly run app script', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
|
||||
await AppsService.startApp('test-app');
|
||||
await AppsService.startApp(app1.id);
|
||||
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', 'test-app', '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
await expect(AppsService.startApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
|
||||
await expect(AppsService.startApp('any')).rejects.toThrowError('App any not found');
|
||||
});
|
||||
|
||||
it('Should restart if app is already running', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
|
||||
await AppsService.startApp('test-app');
|
||||
await AppsService.startApp(app1.id);
|
||||
expect(spy.mock.calls.length).toBe(1);
|
||||
await AppsService.startApp('test-app');
|
||||
await AppsService.startApp(app1.id);
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
await expect(AppsService.startApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
|
||||
});
|
||||
|
||||
it('Regenerate env file', async () => {
|
||||
fs.writeFile(`${config.ROOT_FOLDER}/app-data/test-app/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
|
||||
fs.writeFile(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
|
||||
|
||||
await AppsService.startApp('test-app');
|
||||
await AppsService.startApp(app1.id);
|
||||
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/test-app/app.env`).toString();
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
|
||||
|
||||
expect(envFile.trim()).toBe('TEST=test\nAPP_PORT=3000\nTEST_FIELD=test');
|
||||
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stop app', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_INSTALLED);
|
||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||
});
|
||||
|
||||
it('Should correctly run app script', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
|
||||
await AppsService.stopApp('test-app');
|
||||
await AppsService.stopApp(app1.id);
|
||||
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', 'test-app', '/tipi'], {}, expect.any(Function)]);
|
||||
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', app1.id, '/tipi'], {}, expect.any(Function)]);
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
await expect(AppsService.stopApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
|
||||
await expect(AppsService.stopApp('any')).rejects.toThrowError('App any not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update app config', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_INSTALLED);
|
||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||
});
|
||||
|
||||
it('Should correctly update app config', async () => {
|
||||
await AppsService.updateAppConfig('test-app', { test: 'test', test2: 'test2' });
|
||||
await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/test-app/app.env`).toString();
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
|
||||
|
||||
expect(envFile.trim()).toBe('TEST=test\nAPP_PORT=3000\nTEST_FIELD=test\nTEST_FIELD_2=test2');
|
||||
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test`);
|
||||
});
|
||||
|
||||
it('Should throw if required field is missing', async () => {
|
||||
await expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: '' })).rejects.toThrowError('Variable TEST_FIELD is required');
|
||||
});
|
||||
|
||||
it('Should throw if app is not installed', async () => {
|
||||
await expect(AppsService.updateAppConfig('test-app-2', { test: 'test' })).rejects.toThrowError('App test-app-2 not installed');
|
||||
});
|
||||
|
||||
it('Should throw if required form fields are missing', async () => {
|
||||
await expect(AppsService.updateAppConfig('test-app', {})).rejects.toThrowError('Variable test is required');
|
||||
await expect(AppsService.updateAppConfig('test-app-2', { test: 'test' })).rejects.toThrowError('App test-app-2 not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get app config', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
app1 = app1create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_INSTALLED);
|
||||
fs.__createMockFiles(Object.assign(app1create.MockFiles));
|
||||
});
|
||||
|
||||
it('Should correctly get app config', async () => {
|
||||
const appconfig = await AppsService.getAppInfo('test-app');
|
||||
const app = await AppsService.getApp(app1.id);
|
||||
|
||||
expect(appconfig).toEqual({ ...testApp, installed: true, status: 'stopped', description: 'md desc' });
|
||||
expect(app).toBeDefined();
|
||||
expect(app.config).toStrictEqual({ TEST_FIELD: 'test' });
|
||||
expect(app.id).toBe(app1.id);
|
||||
expect(app.status).toBe(AppStatusEnum.RUNNING);
|
||||
});
|
||||
|
||||
it('Should have installed false if app is not installed', async () => {
|
||||
const appconfig = await AppsService.getAppInfo('test-app2');
|
||||
it('Should return default values if app is not installed', async () => {
|
||||
const appconfig = await AppsService.getApp('test-app2');
|
||||
|
||||
expect(appconfig).toEqual({ ...testApp2, installed: false, status: 'stopped', description: 'md desc' });
|
||||
expect(appconfig).toBeDefined();
|
||||
expect(appconfig.id).toBe('test-app2');
|
||||
expect(appconfig.config).toStrictEqual({});
|
||||
expect(appconfig.status).toBe(AppStatusEnum.MISSING);
|
||||
});
|
||||
});
|
||||
|
||||
describe('List apps', () => {
|
||||
beforeEach(() => {
|
||||
let app1: AppInfo;
|
||||
let app2: AppInfo;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app1create = await createApp(true);
|
||||
const app2create = await createApp();
|
||||
app1 = app1create.appInfo;
|
||||
app2 = app2create.appInfo;
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_FILE_INSTALLED);
|
||||
fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
|
||||
});
|
||||
|
||||
it('Should correctly list apps', async () => {
|
||||
const apps = await AppsService.listApps();
|
||||
it('Should correctly list apps sorted by name', async () => {
|
||||
const { apps } = await AppsService.listApps();
|
||||
|
||||
expect(apps).toEqual([
|
||||
{ ...testApp, installed: true, status: 'stopped', description: 'md desc' },
|
||||
{ ...testApp2, installed: false, status: 'stopped', description: 'md desc' },
|
||||
]);
|
||||
const sortedApps = [app1, app2].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
expect(apps).toBeDefined();
|
||||
expect(apps.length).toBe(2);
|
||||
expect(apps[0].id).toBe('test-app');
|
||||
expect(apps[1].id).toBe('test-app2');
|
||||
expect(apps.length).toBe(2);
|
||||
expect(apps[0].id).toBe(sortedApps[0].id);
|
||||
expect(apps[1].id).toBe(sortedApps[1].id);
|
||||
expect(apps[0].description).toBe('md desc');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,6 @@ import InternalIp from 'internal-ip';
|
|||
import config from '../../config';
|
||||
import { AppInfo } from './apps.types';
|
||||
|
||||
type AppsState = { installed: string };
|
||||
|
||||
export const checkAppRequirements = async (appName: string) => {
|
||||
let valid = true;
|
||||
const configFile: AppInfo = readJsonFile(`/apps/${appName}/config.json`);
|
||||
|
@ -106,10 +104,6 @@ export const generateEnvFile = (appName: string, form: Record<string, string>) =
|
|||
writeFile(`/app-data/${appName}/app.env`, envFile);
|
||||
};
|
||||
|
||||
export const getStateFile = (): AppsState => {
|
||||
return readJsonFile('/state/apps.json');
|
||||
};
|
||||
|
||||
export const getAvailableApps = (): string[] => {
|
||||
const apps: string[] = [];
|
||||
|
||||
|
@ -131,10 +125,6 @@ export const getAvailableApps = (): string[] => {
|
|||
export const getAppInfo = (id: string): AppInfo => {
|
||||
try {
|
||||
const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
|
||||
|
||||
const state = getStateFile();
|
||||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
configFile.installed = installed.includes(id);
|
||||
configFile.description = readFile(`/apps/${id}/metadata/description.md`);
|
||||
|
||||
return configFile;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, getStateFile, runAppScript } from './apps.helpers';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, runAppScript } from './apps.helpers';
|
||||
import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
|
||||
import App from './app.entity';
|
||||
|
||||
|
@ -10,11 +10,11 @@ const startApp = async (appName: string): Promise<App> => {
|
|||
throw new Error(`App ${appName} not found`);
|
||||
}
|
||||
|
||||
checkEnvFile(appName);
|
||||
|
||||
// Regenerate env file
|
||||
generateEnvFile(appName, app.config);
|
||||
|
||||
checkEnvFile(appName);
|
||||
|
||||
await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
|
||||
// Run script
|
||||
await runAppScript(['start', appName]);
|
||||
|
@ -65,15 +65,11 @@ const listApps = async (): Promise<ListAppsResonse> => {
|
|||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const state = getStateFile();
|
||||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
|
||||
apps.forEach((app) => {
|
||||
app.installed = installed.includes(app.id);
|
||||
app.description = readFile(`/apps/${app.id}/metadata/description.md`);
|
||||
});
|
||||
|
||||
return { apps, total: apps.length };
|
||||
return { apps: apps.sort((a, b) => a.name.localeCompare(b.name)), total: apps.length };
|
||||
};
|
||||
|
||||
const updateAppConfig = async (id: string, form: Record<string, string>): Promise<App> => {
|
||||
|
@ -93,6 +89,11 @@ const updateAppConfig = async (id: string, form: Record<string, string>): Promis
|
|||
|
||||
const stopApp = async (id: string): Promise<App> => {
|
||||
let app = await App.findOne({ where: { id } });
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
}
|
||||
|
||||
// Run script
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPING });
|
||||
await runAppScript(['stop', id]);
|
||||
|
|
|
@ -106,9 +106,6 @@ class AppInfo {
|
|||
@Field(() => String)
|
||||
source!: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
installed!: boolean;
|
||||
|
||||
@Field(() => [AppCategoriesEnum])
|
||||
categories!: AppCategoriesEnum[];
|
||||
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
import { Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import * as argon2 from 'argon2';
|
||||
import config from '../../../config';
|
||||
import AuthController from '../auth.controller';
|
||||
|
||||
let user: any;
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
const MOCK_USER_REGISTERED = () => ({
|
||||
[`${config.ROOT_FOLDER}/state/users.json`]: `[${user}]`,
|
||||
});
|
||||
|
||||
const MOCK_NO_USER = {
|
||||
[`${config.ROOT_FOLDER}/state/users.json`]: '[]',
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const hash = await argon2.hash('password');
|
||||
user = JSON.stringify({
|
||||
email: 'username',
|
||||
password: hash,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Login', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
});
|
||||
|
||||
it('Should put cookie in response after login', async () => {
|
||||
const json = jest.fn();
|
||||
const res = { cookie: jest.fn(), status: jest.fn(() => ({ json })), json: jest.fn() } as unknown as Response;
|
||||
const req = { body: { email: 'username', password: 'password' } } as Request;
|
||||
|
||||
await AuthController.login(req, res, next);
|
||||
|
||||
expect(res.cookie).toHaveBeenCalledWith('tipi_token', expect.any(String), expect.any(Object));
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(json).toHaveBeenCalledWith({ token: expect.any(String) });
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should throw if username is not provided in request', async () => {
|
||||
const res = { cookie: jest.fn(), status: jest.fn(), json: jest.fn() } as unknown as Response;
|
||||
const req = { body: { password: 'password' } } as Request;
|
||||
|
||||
await AuthController.login(req, res, next);
|
||||
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
});
|
||||
|
||||
it('Should throw if password is not provided in request', async () => {
|
||||
const res = { cookie: jest.fn(), status: jest.fn(), json: jest.fn() } as unknown as Response;
|
||||
const req = { body: { email: 'username' } } as Request;
|
||||
|
||||
await AuthController.login(req, res, next);
|
||||
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Register', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_NO_USER);
|
||||
});
|
||||
|
||||
it('Should put cookie in response after register', async () => {
|
||||
const json = jest.fn();
|
||||
const res = { cookie: jest.fn(), status: jest.fn(() => ({ json })), json: jest.fn() } as unknown as Response;
|
||||
const req = { body: { email: 'username', password: 'password', name: 'name' } } as Request;
|
||||
|
||||
await AuthController.register(req, res, next);
|
||||
|
||||
expect(res.cookie).toHaveBeenCalledWith('tipi_token', expect.any(String), expect.any(Object));
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(json).toHaveBeenCalledWith({ token: expect.any(String) });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Me', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
});
|
||||
|
||||
it('Should return user if present in request', async () => {
|
||||
const json = jest.fn();
|
||||
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
|
||||
const req = { user } as unknown as Request;
|
||||
|
||||
await AuthController.me(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(json).toHaveBeenCalledWith({ user });
|
||||
});
|
||||
|
||||
it('Should return null if user is not present in request', async () => {
|
||||
const json = jest.fn();
|
||||
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
|
||||
const req = {} as Request;
|
||||
|
||||
await AuthController.me(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(json).toHaveBeenCalledWith({ user: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConfigured', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_NO_USER);
|
||||
});
|
||||
|
||||
it('Should return false if no user is registered', async () => {
|
||||
const json = jest.fn();
|
||||
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
|
||||
const req = {} as Request;
|
||||
|
||||
await AuthController.isConfigured(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(json).toHaveBeenCalledWith({ configured: false });
|
||||
});
|
||||
|
||||
it('Should return true if user is registered', async () => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
|
||||
const json = jest.fn();
|
||||
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
|
||||
const req = { user } as unknown as Request;
|
||||
|
||||
await AuthController.isConfigured(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(json).toHaveBeenCalledWith({ configured: true });
|
||||
});
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
import * as argon2 from 'argon2';
|
||||
import fs from 'fs';
|
||||
import config from '../../../config';
|
||||
import { IUser } from '../../../config/types';
|
||||
import AuthHelpers from '../auth.helpers';
|
||||
|
||||
let user: IUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
const hash = await argon2.hash('password');
|
||||
user = { email: 'username', password: hash, name: 'name' };
|
||||
});
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const MOCK_USER_REGISTERED = () => ({
|
||||
[`${config.ROOT_FOLDER}/state/users.json`]: `[${JSON.stringify(user)}]`,
|
||||
});
|
||||
|
||||
describe('TradeTokenForUser', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
});
|
||||
|
||||
it('Should return null if token is invalid', () => {
|
||||
const result = AuthHelpers.tradeTokenForUser('invalid token');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Should return user if token is valid', async () => {
|
||||
const token = await AuthHelpers.getJwtToken(user, 'password');
|
||||
const result = AuthHelpers.tradeTokenForUser(token);
|
||||
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetJwtToken', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
});
|
||||
|
||||
it('Should return token if user and password are valid', async () => {
|
||||
const token = await AuthHelpers.getJwtToken(user, 'password');
|
||||
expect(token).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should throw if password is invalid', async () => {
|
||||
await expect(AuthHelpers.getJwtToken(user, 'invalid password')).rejects.toThrow('Wrong password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUser', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
});
|
||||
|
||||
it('Should return null if user is not found', () => {
|
||||
const result = AuthHelpers.getUser('invalid token');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should return user if token is valid', async () => {
|
||||
const result = AuthHelpers.getUser('username');
|
||||
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
});
|
|
@ -1,102 +1,88 @@
|
|||
import fs from 'fs';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import * as argon2 from 'argon2';
|
||||
import config from '../../../config';
|
||||
import AuthService from '../auth.service';
|
||||
import { IUser } from '../../../config/types';
|
||||
import { createUser } from './user.factory';
|
||||
import User from '../user.entity';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
let user: any;
|
||||
|
||||
const MOCK_USER_REGISTERED = () => ({
|
||||
[`${config.ROOT_FOLDER}/state/users.json`]: `[${user}]`,
|
||||
});
|
||||
|
||||
const MOCK_NO_USER = {
|
||||
[`${config.ROOT_FOLDER}/state/users.json`]: '[]',
|
||||
};
|
||||
let db: DataSource | null = null;
|
||||
const TEST_SUITE = 'authservice';
|
||||
|
||||
beforeAll(async () => {
|
||||
const hash = await argon2.hash('password');
|
||||
user = JSON.stringify({
|
||||
email: 'username',
|
||||
password: hash,
|
||||
});
|
||||
db = await setupConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await User.clear();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db?.destroy();
|
||||
await teardownConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
describe('Login', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_USER_REGISTERED());
|
||||
});
|
||||
it('Should return user after login', async () => {
|
||||
const email = faker.internet.email();
|
||||
await createUser(email);
|
||||
|
||||
it('Should return token after login', async () => {
|
||||
const token = await AuthService.login('username', 'password');
|
||||
const { user } = await AuthService.login({ username: email, password: 'password' });
|
||||
|
||||
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
|
||||
|
||||
expect(token).toBeDefined();
|
||||
expect(email).toBe('username');
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toBe(1);
|
||||
});
|
||||
|
||||
it('Should throw if user does not exist', async () => {
|
||||
await expect(AuthService.login('username1', 'password')).rejects.toThrowError('User not found');
|
||||
await expect(AuthService.login({ username: 'test', password: 'test' })).rejects.toThrowError('User not found');
|
||||
});
|
||||
|
||||
it('Should throw if password is incorrect', async () => {
|
||||
await expect(AuthService.login('username', 'password1')).rejects.toThrowError('Wrong password');
|
||||
const email = faker.internet.email();
|
||||
await createUser(email);
|
||||
await expect(AuthService.login({ username: email, password: 'wrong' })).rejects.toThrowError('Wrong password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Register', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MOCK_NO_USER);
|
||||
it('Should return new user after register', async () => {
|
||||
const email = faker.internet.email();
|
||||
const { user } = await AuthService.register({ username: email, password: 'test' });
|
||||
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should return token after register', async () => {
|
||||
const token = await AuthService.register('username', 'password', 'name');
|
||||
it('Should correctly trim and lowercase email', async () => {
|
||||
const email = faker.internet.email();
|
||||
await AuthService.register({ username: email, password: 'test' });
|
||||
|
||||
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
|
||||
const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
|
||||
|
||||
expect(token).toBeDefined();
|
||||
expect(email).toBe('username');
|
||||
});
|
||||
|
||||
it('Should correctly write user to file', async () => {
|
||||
await AuthService.register('username', 'password', 'name');
|
||||
|
||||
const users: IUser[] = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/users.json`, 'utf8'));
|
||||
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].email).toBe('username');
|
||||
expect(users[0].name).toBe('name');
|
||||
|
||||
const valid = await argon2.verify(users[0].password, 'password');
|
||||
|
||||
expect(valid).toBeTruthy();
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.username).toBe(email.toLowerCase().trim());
|
||||
});
|
||||
|
||||
it('Should throw if user already exists', async () => {
|
||||
await AuthService.register('username', 'password', 'name');
|
||||
const email = faker.internet.email();
|
||||
|
||||
await expect(AuthService.register('username', 'password', 'name')).rejects.toThrowError('There is already an admin user');
|
||||
await createUser(email);
|
||||
await expect(AuthService.register({ username: email, password: 'test' })).rejects.toThrowError('User already exists');
|
||||
});
|
||||
|
||||
it('Should throw if email is not provided', async () => {
|
||||
await expect(AuthService.register('', 'password', 'name')).rejects.toThrowError('Missing email or password');
|
||||
await expect(AuthService.register({ username: '', password: 'test' })).rejects.toThrowError('Missing email or password');
|
||||
});
|
||||
|
||||
it('Should throw if password is not provided', async () => {
|
||||
await expect(AuthService.register('username', '', 'name')).rejects.toThrowError('Missing email or password');
|
||||
await expect(AuthService.register({ username: faker.internet.email(), password: '' })).rejects.toThrowError('Missing email or password');
|
||||
});
|
||||
|
||||
it('Does not throw if name is not provided', async () => {
|
||||
await AuthService.register('username', 'password', '');
|
||||
it('Password is correctly hashed', async () => {
|
||||
const email = faker.internet.email();
|
||||
const { user } = await AuthService.register({ username: email, password: 'test' });
|
||||
|
||||
const users: IUser[] = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/users.json`, 'utf8'));
|
||||
const isPasswordValid = await argon2.verify(user?.password || '', 'test');
|
||||
|
||||
expect(users.length).toBe(1);
|
||||
expect(isPasswordValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import User from '../user.entity';
|
||||
import * as argon2 from 'argon2';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const createUser = async (email?: string) => {
|
||||
const hash = await argon2.hash('password');
|
||||
|
||||
const user = await User.create({
|
||||
username: email?.toLowerCase().trim() || faker.internet.email().toLowerCase().trim(),
|
||||
password: hash,
|
||||
}).save();
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export { createUser };
|
|
@ -1,46 +0,0 @@
|
|||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import * as argon2 from 'argon2';
|
||||
import { IUser, Maybe } from '../../config/types';
|
||||
import { readJsonFile } from '../fs/fs.helpers';
|
||||
import config from '../../config';
|
||||
import User from './user.entity';
|
||||
|
||||
const getUser = (email: string): Maybe<IUser> => {
|
||||
const savedUser: IUser[] = readJsonFile('/state/users.json');
|
||||
|
||||
return savedUser.find((u) => u.email === email);
|
||||
};
|
||||
|
||||
const compareHashPassword = (password: string, hash = ''): Promise<boolean> => {
|
||||
return argon2.verify(hash, password);
|
||||
};
|
||||
|
||||
const getJwtToken = async (user: User, password: string) => {
|
||||
const validPassword = await compareHashPassword(password, user.password);
|
||||
|
||||
if (validPassword) {
|
||||
if (config.JWT_SECRET) {
|
||||
return jsonwebtoken.sign({ email: user.username }, config.JWT_SECRET, {
|
||||
expiresIn: '7d',
|
||||
});
|
||||
} else {
|
||||
throw new Error('JWT_SECRET is not set');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Wrong password');
|
||||
};
|
||||
|
||||
const tradeTokenForUser = (token: string): Maybe<IUser> => {
|
||||
try {
|
||||
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
|
||||
|
||||
const users: IUser[] = readJsonFile('/state/users.json');
|
||||
|
||||
return users.find((user) => user.email === email);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default { tradeTokenForUser, getJwtToken, getUser };
|
|
@ -9,7 +9,13 @@ export const readJsonFile = (path: string): any => {
|
|||
return JSON.parse(rawFile);
|
||||
};
|
||||
|
||||
export const readFile = (path: string): string => fs.readFileSync(getAbsolutePath(path)).toString();
|
||||
export const readFile = (path: string): string => {
|
||||
try {
|
||||
return fs.readFileSync(getAbsolutePath(path)).toString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const readdirSync = (path: string): string[] => fs.readdirSync(getAbsolutePath(path));
|
||||
|
||||
|
|
40
packages/system-api/src/test/connection.ts
Executable file
40
packages/system-api/src/test/connection.ts
Executable file
|
@ -0,0 +1,40 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import App from '../modules/apps/app.entity';
|
||||
import User from '../modules/auth/user.entity';
|
||||
import pg from 'pg';
|
||||
|
||||
const pgClient = new pg.Client({
|
||||
user: 'postgres',
|
||||
host: 'localhost',
|
||||
database: 'postgres',
|
||||
password: 'postgres',
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
export const setupConnection = async (testsuite: string): Promise<DataSource> => {
|
||||
await pgClient.connect();
|
||||
|
||||
await pgClient.query(`DROP DATABASE IF EXISTS ${testsuite}`);
|
||||
await pgClient.query(`CREATE DATABASE ${testsuite}`);
|
||||
|
||||
const AppDataSource = new DataSource({
|
||||
name: 'default',
|
||||
type: 'postgres',
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: testsuite,
|
||||
dropSchema: true,
|
||||
logging: false,
|
||||
synchronize: true,
|
||||
entities: [App, User],
|
||||
});
|
||||
|
||||
return AppDataSource.initialize();
|
||||
};
|
||||
|
||||
export const teardownConnection = async (testsuite: string): Promise<void> => {
|
||||
await pgClient.query(`DROP DATABASE IF EXISTS ${testsuite}`);
|
||||
await pgClient.end();
|
||||
};
|
|
@ -141,7 +141,7 @@ importers:
|
|||
|
||||
packages/system-api:
|
||||
specifiers:
|
||||
'@runtipi/common': file:../common
|
||||
'@faker-js/faker': ^7.3.0
|
||||
'@types/compression': ^1.7.2
|
||||
'@types/cookie-parser': ^1.4.3
|
||||
'@types/cors': ^2.8.12
|
||||
|
@ -152,6 +152,7 @@ importers:
|
|||
'@types/mock-fs': ^4.13.1
|
||||
'@types/passport': ^1.0.7
|
||||
'@types/passport-http-bearer': ^1.0.37
|
||||
'@types/pg': ^8.6.5
|
||||
'@types/session-file-store': ^1.2.2
|
||||
'@types/tcp-port-used': ^1.0.1
|
||||
'@types/validator': ^13.7.2
|
||||
|
@ -204,7 +205,6 @@ importers:
|
|||
typescript: 4.6.4
|
||||
winston: ^3.7.2
|
||||
dependencies:
|
||||
'@runtipi/common': file:packages/common
|
||||
apollo-server-core: 3.9.0_graphql@15.8.0
|
||||
apollo-server-express: 3.9.0_jfj6k5cqxqbusbdzwqjdzioxzm
|
||||
argon2: 0.28.5
|
||||
|
@ -239,6 +239,7 @@ importers:
|
|||
typeorm: 0.3.6_pg@8.7.3
|
||||
winston: 3.7.2
|
||||
devDependencies:
|
||||
'@faker-js/faker': 7.3.0
|
||||
'@types/compression': 1.7.2
|
||||
'@types/cookie-parser': 1.4.3
|
||||
'@types/cors': 2.8.12
|
||||
|
@ -249,6 +250,7 @@ importers:
|
|||
'@types/mock-fs': 4.13.1
|
||||
'@types/passport': 1.0.7
|
||||
'@types/passport-http-bearer': 1.0.37
|
||||
'@types/pg': 8.6.5
|
||||
'@types/session-file-store': 1.2.2
|
||||
'@types/tcp-port-used': 1.0.1
|
||||
'@types/validator': 13.7.2
|
||||
|
@ -2154,6 +2156,11 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@faker-js/faker/7.3.0:
|
||||
resolution: {integrity: sha512-1W0PZezq2rxlAssoWemi9gFRD8IQxvf0FPL5Km3TOmGHFG7ib0TbFBJ0yC7D/1NsxunjNTK6WjUXV8ao/mKZ5w==}
|
||||
engines: {node: '>=14.0.0', npm: '>=6.0.0'}
|
||||
dev: true
|
||||
|
||||
/@fontsource/open-sans/4.5.8:
|
||||
resolution: {integrity: sha512-3b94XDdRLqL7OlE7OjWg/4pgG825Juw8PLVEDm6h5pio0gMU89ICxfatGxHsBxMGfqad+wnvdmUweZWlELDFpQ==}
|
||||
dev: false
|
||||
|
@ -3513,6 +3520,14 @@ packages:
|
|||
'@types/express': 4.17.13
|
||||
dev: true
|
||||
|
||||
/@types/pg/8.6.5:
|
||||
resolution: {integrity: sha512-tOkGtAqRVkHa/PVZicq67zuujI4Oorfglsr2IbKofDwBSysnaqSx7W1mDqFqdkGE6Fbgh+PZAl0r/BWON/mozw==}
|
||||
dependencies:
|
||||
'@types/node': 17.0.31
|
||||
pg-protocol: 1.5.0
|
||||
pg-types: 2.2.0
|
||||
dev: true
|
||||
|
||||
/@types/prettier/2.6.0:
|
||||
resolution: {integrity: sha512-G/AdOadiZhnJp0jXCaBQU449W2h716OW/EoXeYkCytxKL06X1WCXB4DZpp8TpZ8eyIJVS1cw4lrlkkSYU21cDw==}
|
||||
dev: true
|
||||
|
@ -5058,7 +5073,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/console-control-strings/1.1.0:
|
||||
resolution: {integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=}
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
dev: false
|
||||
|
||||
/constant-case/3.0.4:
|
||||
|
@ -5376,7 +5391,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/delegates/1.0.0:
|
||||
resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=}
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
dev: false
|
||||
|
||||
/depd/2.0.0:
|
||||
|
@ -6034,7 +6049,7 @@ packages:
|
|||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.22.0_uhoeudlwl7kc47h4kncsfowede
|
||||
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
|
||||
debug: 3.2.7
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
find-up: 2.1.0
|
||||
|
@ -7110,7 +7125,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/has-unicode/2.0.1:
|
||||
resolution: {integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=}
|
||||
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
|
||||
dev: false
|
||||
|
||||
/has-yarn/2.1.0:
|
||||
|
@ -7671,7 +7686,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/isexe/2.0.0:
|
||||
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=}
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
/isomorphic-fetch/3.0.0:
|
||||
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
|
||||
|
@ -10003,7 +10018,6 @@ packages:
|
|||
/pg-int8/1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/pg-pool/3.5.1_pg@8.7.3:
|
||||
resolution: {integrity: sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==}
|
||||
|
@ -10015,7 +10029,6 @@ packages:
|
|||
|
||||
/pg-protocol/1.5.0:
|
||||
resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==}
|
||||
dev: false
|
||||
|
||||
/pg-types/2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
|
@ -10026,7 +10039,6 @@ packages:
|
|||
postgres-bytea: 1.0.0
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
dev: false
|
||||
|
||||
/pg/8.7.3:
|
||||
resolution: {integrity: sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==}
|
||||
|
@ -10151,24 +10163,20 @@ packages:
|
|||
/postgres-array/2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/postgres-bytea/1.0.0:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/postgres-date/1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/postgres-interval/1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
dev: false
|
||||
|
||||
/prelude-ls/1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
|
|
Loading…
Reference in a new issue