test(worker): implement test suites with vitest
This commit is contained in:
parent
55e0cd155e
commit
af8509aacc
24 changed files with 2438 additions and 58 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -56,6 +56,7 @@ node_modules/
|
|||
/apps/
|
||||
traefik/shared
|
||||
traefik/tls
|
||||
./traefik/
|
||||
|
||||
# media folder
|
||||
media
|
||||
|
|
|
@ -55,6 +55,45 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./packages/worker/Dockerfile.dev
|
||||
container_name: tipi-worker
|
||||
user: root
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
start_period: 5s
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
volumes:
|
||||
# Dev mode
|
||||
- ${PWD}/packages/worker/src:/app/packages/worker/src
|
||||
# Production mode
|
||||
- /:/mnt/host:ro
|
||||
- /proc:/host/proc:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${PWD}/.env:/app/.env
|
||||
- ${PWD}/state:/app/state
|
||||
- ${PWD}/repos:/app/repos
|
||||
- ${PWD}/apps:/app/apps
|
||||
- ${STORAGE_PATH:-$PWD}/app-data:/storage/app-data
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/app/traefik
|
||||
- ${PWD}/user-config:/app/user-config
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-dashboard:
|
||||
build:
|
||||
context: .
|
||||
|
@ -65,6 +104,8 @@ services:
|
|||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
tipi-worker:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
@ -84,7 +125,7 @@ services:
|
|||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/runtipi/traefik
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${STORAGE_PATH:-$PWD}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
|
|
|
@ -55,6 +55,42 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./packages/worker/Dockerfile
|
||||
container_name: tipi-worker
|
||||
user: root
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
start_period: 5s
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
- /:/host/root:ro
|
||||
- /proc:/host/proc
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${PWD}/.env:/app/.env
|
||||
- ${PWD}/state:/app/state
|
||||
- ${PWD}/repos:/app/repos
|
||||
- ${PWD}/apps:/app/apps
|
||||
- ${STORAGE_PATH:-$PWD}/app-data:/storage/app-data
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/app/traefik
|
||||
- ${PWD}/user-config:/app/user-config
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-dashboard:
|
||||
build:
|
||||
context: .
|
||||
|
@ -65,6 +101,8 @@ services:
|
|||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
tipi-worker:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
|
@ -57,6 +57,7 @@ services:
|
|||
tipi-worker:
|
||||
container_name: tipi-worker
|
||||
image: ghcr.io/runtipi/runtipi-worker:${TIPI_VERSION}
|
||||
user: root
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
|
@ -83,6 +84,7 @@ services:
|
|||
- ${STORAGE_PATH:-$PWD}/app-data:/storage/app-data
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/app/traefik
|
||||
- ${PWD}/user-config:/app/user-config
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"main": "index.js",
|
||||
"bin": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "dotenv -e .env.test vitest -- --coverage --watch=false",
|
||||
"test": "dotenv -e .env.test vitest -- --coverage --watch=false --passWithNoTests",
|
||||
"test:watch": "dotenv -e .env.test vitest",
|
||||
"package": "npm run build && pkg package.json && chmod +x dist/bin/cli-x64 && chmod +x dist/bin/cli-arm64",
|
||||
"package:m1": "npm run build && pkg package.json -t node18-darwin-arm64",
|
||||
|
|
|
@ -33,7 +33,7 @@ export class AppExecutors {
|
|||
|
||||
const jobid = this.generateJobId({ appId, action: 'stop' });
|
||||
|
||||
const event = { type: 'app', command: 'stop', appid: appId, form: {} } satisfies SystemEvent;
|
||||
const event = { type: 'app', command: 'stop', appid: appId, form: {}, skipEnv: true } satisfies SystemEvent;
|
||||
const job = await this.queue.add(jobid, eventSchema.parse(event));
|
||||
const result = await job.waitUntilFinished(this.queueEvents, 1000 * 60 * 5);
|
||||
|
||||
|
@ -51,7 +51,7 @@ export class AppExecutors {
|
|||
|
||||
const jobid = this.generateJobId({ appId, action: 'start' });
|
||||
|
||||
const event = { type: 'app', command: 'start', appid: appId, form: {} } satisfies SystemEvent;
|
||||
const event = { type: 'app', command: 'start', appid: appId, form: {}, skipEnv: true } satisfies SystemEvent;
|
||||
const job = await this.queue.add(jobid, eventSchema.parse(event));
|
||||
const result = await job.waitUntilFinished(this.queueEvents, 1000 * 60 * 5);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ const appCommandSchema = z.object({
|
|||
type: z.literal(EVENT_TYPES.APP),
|
||||
command: z.union([z.literal('start'), z.literal('stop'), z.literal('install'), z.literal('uninstall'), z.literal('update'), z.literal('generate_env')]),
|
||||
appid: z.string(),
|
||||
skipEnv: z.boolean().optional().default(false),
|
||||
form: z.object({}).catchall(z.any()),
|
||||
});
|
||||
|
||||
|
@ -33,4 +34,4 @@ export const eventResultSchema = z.object({
|
|||
stdout: z.string(),
|
||||
});
|
||||
|
||||
export type SystemEvent = z.infer<typeof eventSchema>;
|
||||
export type SystemEvent = z.input<typeof eventSchema>;
|
||||
|
|
14
packages/worker/.env.test
Normal file
14
packages/worker/.env.test
Normal file
|
@ -0,0 +1,14 @@
|
|||
INTERNAL_IP=localhost
|
||||
ARCHITECTURE=arm64
|
||||
APPS_REPO_ID=repo-id
|
||||
APPS_REPO_URL=https://test.com/test
|
||||
ROOT_FOLDER_HOST=/runtipi
|
||||
STORAGE_PATH=/runtipi
|
||||
TIPI_VERSION=1
|
||||
REDIS_PASSWORD=redis
|
||||
REDIS_HOST=localhost
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_DBNAME=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PORT=5433
|
1
packages/worker/.gitignore
vendored
1
packages/worker/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
dist/
|
||||
coverage/
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "dotenv -e .env.test vitest -- --coverage --watch=false",
|
||||
"test:watch": "dotenv -e .env.test vitest",
|
||||
"build": "node build.js",
|
||||
"tsc": "tsc",
|
||||
"dev": "dotenv -e ../../.env nodemon",
|
||||
|
@ -14,13 +15,17 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@types/web-push": "^3.6.2",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"esbuild": "^0.19.4",
|
||||
"knip": "^2.39.0",
|
||||
"memfs": "^4.6.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"tsx": "^3.14.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.2.2",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
|
|
125
packages/worker/src/lib/docker/docker-helpers.test.ts
Normal file
125
packages/worker/src/lib/docker/docker-helpers.test.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
// const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: '', stderr: randomError }));
|
||||
|
||||
import { vi, it, describe, expect } from 'vitest';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import fs from 'fs';
|
||||
import { compose } from './docker-helpers';
|
||||
|
||||
const execAsync = vi.fn().mockImplementation(() => Promise.resolve({ stdout: '', stderr: '' }));
|
||||
|
||||
vi.mock('@runtipi/shared', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as object;
|
||||
|
||||
return {
|
||||
...mod,
|
||||
FileLogger: vi.fn().mockImplementation(() => ({
|
||||
flush: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
execAsync: (cmd: string) => execAsync(cmd),
|
||||
};
|
||||
});
|
||||
|
||||
describe('docker helpers', async () => {
|
||||
it('should call execAsync with correct args', async () => {
|
||||
// arrange
|
||||
const appId = faker.word.noun().toLowerCase();
|
||||
const command = faker.word.noun().toLowerCase();
|
||||
|
||||
// act
|
||||
await compose(appId, command);
|
||||
|
||||
// assert
|
||||
const expected = [
|
||||
'docker compose',
|
||||
`--env-file /storage/app-data/${appId}/app.env`,
|
||||
`--project-name ${appId}`,
|
||||
`-f /app/apps/${appId}/docker-compose.yml`,
|
||||
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
|
||||
command,
|
||||
].join(' ');
|
||||
|
||||
expect(execAsync).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should add user env file if exists', async () => {
|
||||
// arrange
|
||||
const appId = faker.word.noun().toLowerCase();
|
||||
const command = faker.word.noun().toLowerCase();
|
||||
await fs.promises.mkdir(`/app/user-config/${appId}`, { recursive: true });
|
||||
const userEnvFile = `/app/user-config/${appId}/app.env`;
|
||||
await fs.promises.writeFile(userEnvFile, 'test');
|
||||
|
||||
// act
|
||||
await compose(appId, command);
|
||||
|
||||
// assert
|
||||
const expected = [
|
||||
'docker compose',
|
||||
`--env-file /storage/app-data/${appId}/app.env`,
|
||||
`--env-file ${userEnvFile}`,
|
||||
`--project-name ${appId}`,
|
||||
`-f /app/apps/${appId}/docker-compose.yml`,
|
||||
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
|
||||
command,
|
||||
].join(' ');
|
||||
|
||||
expect(execAsync).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should add user compose file if exists', async () => {
|
||||
// arrange
|
||||
const appId = faker.word.noun().toLowerCase();
|
||||
const command = faker.word.noun().toLowerCase();
|
||||
await fs.promises.mkdir(`/app/user-config/${appId}`, { recursive: true });
|
||||
const userComposeFile = `/app/user-config/${appId}/docker-compose.yml`;
|
||||
await fs.promises.writeFile(userComposeFile, 'test');
|
||||
|
||||
// act
|
||||
await compose(appId, command);
|
||||
|
||||
// assert
|
||||
const expected = [
|
||||
'docker compose',
|
||||
`--env-file /storage/app-data/${appId}/app.env`,
|
||||
`--project-name ${appId}`,
|
||||
`-f /app/apps/${appId}/docker-compose.yml`,
|
||||
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
|
||||
`--file ${userComposeFile}`,
|
||||
command,
|
||||
].join(' ');
|
||||
|
||||
expect(execAsync).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should add arm64 compose file if exists and arch is arm64', async () => {
|
||||
// arrange
|
||||
vi.mock('@/lib/environment', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as object;
|
||||
return { ...mod, getEnv: () => ({ arch: 'arm64', appsRepoId: 'repo-id' }) };
|
||||
});
|
||||
const appId = faker.word.noun().toLowerCase();
|
||||
const command = faker.word.noun().toLowerCase();
|
||||
await fs.promises.mkdir(`/app/apps/${appId}`, { recursive: true });
|
||||
const arm64ComposeFile = `/app/apps/${appId}/docker-compose.arm64.yml`;
|
||||
await fs.promises.writeFile(arm64ComposeFile, 'test');
|
||||
|
||||
// act
|
||||
await compose(appId, command);
|
||||
|
||||
// assert
|
||||
const expected = [
|
||||
'docker compose',
|
||||
`--env-file /storage/app-data/${appId}/app.env`,
|
||||
`--project-name ${appId}`,
|
||||
`-f ${arm64ComposeFile}`,
|
||||
`-f /app/repos/repo-id/apps/docker-compose.common.yml`,
|
||||
command,
|
||||
].join(' ');
|
||||
|
||||
expect(execAsync).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
|
@ -1 +1 @@
|
|||
export { setEnvVariable, copySystemFiles, generateSystemEnvFile, ensureFilePermissions, generateTlsCertificates } from './system.helpers';
|
||||
export { copySystemFiles, generateSystemEnvFile, ensureFilePermissions, generateTlsCertificates } from './system.helpers';
|
||||
|
|
|
@ -2,13 +2,14 @@ import fs from 'fs';
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import path from 'path';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { pathExists } from '@runtipi/shared';
|
||||
import { AppExecutors } from '../app.executors';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import * as dockerHelpers from '@/utils/docker-helpers';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
import { pathExists } from '@/utils/fs-helpers';
|
||||
import * as dockerHelpers from '@/lib/docker';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
|
||||
|
||||
const { storagePath, rootFolderHost, appsRepoId } = getEnv();
|
||||
const { appsRepoId } = getEnv();
|
||||
|
||||
describe('test: app executors', () => {
|
||||
const appExecutors = new AppExecutors();
|
||||
|
@ -23,7 +24,7 @@ describe('test: app executors', () => {
|
|||
const { message, success } = await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const envExists = await pathExists(path.join(storagePath, 'app-data', config.id, 'app.env'));
|
||||
const envExists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, 'app.env'));
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(message).toBe(`App ${config.id} installed successfully`);
|
||||
|
@ -32,17 +33,32 @@ describe('test: app executors', () => {
|
|||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return error if compose script fails', async () => {
|
||||
// arrange
|
||||
const randomError = faker.system.fileName();
|
||||
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: '', stderr: randomError }));
|
||||
const config = createAppConfig({}, false);
|
||||
|
||||
// act
|
||||
const { message, success } = await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
expect(success).toBe(false);
|
||||
expect(message).toContain(randomError);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('should delete existing app folder', async () => {
|
||||
// arrange
|
||||
const config = createAppConfig();
|
||||
await fs.promises.mkdir(path.join(rootFolderHost, 'apps', config.id), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(rootFolderHost, 'apps', config.id, 'test.txt'), 'test');
|
||||
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'apps', config.id), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'apps', config.id, 'test.txt'), 'test');
|
||||
|
||||
// act
|
||||
await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(path.join(storagePath, 'apps', config.id, 'test.txt'));
|
||||
const exists = await pathExists(path.join(STORAGE_FOLDER, 'apps', config.id, 'test.txt'));
|
||||
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
|
@ -51,13 +67,13 @@ describe('test: app executors', () => {
|
|||
// arrange
|
||||
const config = createAppConfig();
|
||||
const filename = faker.system.fileName();
|
||||
await fs.promises.writeFile(path.join(storagePath, 'app-data', config.id, filename), 'test');
|
||||
await fs.promises.writeFile(path.join(STORAGE_FOLDER, 'app-data', config.id, filename), 'test');
|
||||
|
||||
// act
|
||||
await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(path.join(storagePath, 'app-data', config.id, filename));
|
||||
const exists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, filename));
|
||||
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
@ -66,15 +82,15 @@ describe('test: app executors', () => {
|
|||
// arrange
|
||||
const config = createAppConfig({}, false);
|
||||
const filename = faker.system.fileName();
|
||||
await fs.promises.mkdir(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'test');
|
||||
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'test');
|
||||
|
||||
// act
|
||||
await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(path.join(storagePath, 'app-data', config.id, 'data', filename));
|
||||
const data = await fs.promises.readFile(path.join(storagePath, 'app-data', config.id, 'data', filename), 'utf-8');
|
||||
const exists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename));
|
||||
const data = await fs.promises.readFile(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename), 'utf-8');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(data).toBe('test');
|
||||
|
@ -84,16 +100,16 @@ describe('test: app executors', () => {
|
|||
// arrange
|
||||
const config = createAppConfig();
|
||||
const filename = faker.system.fileName();
|
||||
await fs.promises.writeFile(path.join(storagePath, 'app-data', config.id, 'data', filename), 'test');
|
||||
await fs.promises.mkdir(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'yeah');
|
||||
await fs.promises.writeFile(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename), 'test');
|
||||
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'yeah');
|
||||
|
||||
// act
|
||||
await appExecutors.installApp(config.id, config);
|
||||
|
||||
// assert
|
||||
const exists = await pathExists(path.join(storagePath, 'app-data', config.id, 'data', filename));
|
||||
const data = await fs.promises.readFile(path.join(storagePath, 'app-data', config.id, 'data', filename), 'utf-8');
|
||||
const exists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename));
|
||||
const data = await fs.promises.readFile(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename), 'utf-8');
|
||||
|
||||
expect(exists).toBe(true);
|
||||
expect(data).toBe('test');
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
import fs from 'fs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { pathExists } from '@runtipi/shared';
|
||||
import { copyDataDir, generateEnvFile } from '../app.helpers';
|
||||
import { createAppConfig } from '@/tests/apps.factory';
|
||||
import { getAppEnvMap } from '../env.helpers';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
import { pathExists } from '@/utils/fs-helpers';
|
||||
|
||||
const { rootFolderHost, storagePath } = getEnv();
|
||||
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
|
||||
|
||||
describe('app helpers', () => {
|
||||
describe('Test: generateEnvFile()', () => {
|
||||
it('should throw an error if the app has an invalid config.json file', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig();
|
||||
await fs.promises.writeFile(`${rootFolderHost}/apps/${appConfig.id}/config.json`, '{}');
|
||||
await fs.promises.writeFile(`${ROOT_FOLDER}/apps/${appConfig.id}/config.json`, '{}');
|
||||
|
||||
// act & assert
|
||||
expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(`App ${appConfig.id} has invalid config.json file`);
|
||||
|
@ -50,8 +48,8 @@ describe('app helpers', () => {
|
|||
// arrange
|
||||
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'RANDOM_FIELD', type: 'random', label: 'test', min: 32, max: 32, required: true }] });
|
||||
const randomField = faker.string.alphanumeric(32);
|
||||
await fs.promises.mkdir(`${rootFolderHost}/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(`${rootFolderHost}/app-data/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
|
||||
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(`${STORAGE_FOLDER}/app-data/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
|
||||
|
||||
// act
|
||||
await generateEnvFile(appConfig.id, {});
|
||||
|
@ -117,7 +115,7 @@ describe('app helpers', () => {
|
|||
it('Should not re-create app-data folder if it already exists', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await fs.promises.mkdir(`${rootFolderHost}/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.mkdir(`${ROOT_FOLDER}/app-data/${appConfig.id}`, { recursive: true });
|
||||
|
||||
// act
|
||||
await generateEnvFile(appConfig.id, {});
|
||||
|
@ -161,8 +159,8 @@ describe('app helpers', () => {
|
|||
const vapidPublicKey = faker.string.alphanumeric(32);
|
||||
|
||||
// act
|
||||
await fs.promises.mkdir(`${rootFolderHost}/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(`${rootFolderHost}/app-data/${appConfig.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`);
|
||||
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(`${STORAGE_FOLDER}/app-data/${appConfig.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`);
|
||||
await generateEnvFile(appConfig.id, {});
|
||||
const envmap = await getAppEnvMap(appConfig.id);
|
||||
|
||||
|
@ -181,13 +179,13 @@ describe('app helpers', () => {
|
|||
await copyDataDir(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(await pathExists(`${rootFolderHost}/apps/${appConfig.id}/data`)).toBe(false);
|
||||
expect(await pathExists(`${ROOT_FOLDER}/apps/${appConfig.id}/data`)).toBe(false);
|
||||
});
|
||||
|
||||
it('should copy data dir to app-data folder', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
const dataDir = `${rootFolderHost}/apps/${appConfig.id}/data`;
|
||||
const dataDir = `${ROOT_FOLDER}/apps/${appConfig.id}/data`;
|
||||
|
||||
await fs.promises.mkdir(dataDir, { recursive: true });
|
||||
await fs.promises.writeFile(`${dataDir}/test.txt`, 'test');
|
||||
|
@ -196,14 +194,14 @@ describe('app helpers', () => {
|
|||
await copyDataDir(appConfig.id);
|
||||
|
||||
// assert
|
||||
const appDataDir = `${storagePath}/app-data/${appConfig.id}`;
|
||||
const appDataDir = `${STORAGE_FOLDER}/app-data/${appConfig.id}`;
|
||||
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
|
||||
});
|
||||
|
||||
it('should copy folders recursively', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
const dataDir = `${rootFolderHost}/apps/${appConfig.id}/data`;
|
||||
const dataDir = `${ROOT_FOLDER}/apps/${appConfig.id}/data`;
|
||||
|
||||
await fs.promises.mkdir(dataDir, { recursive: true });
|
||||
|
||||
|
@ -217,7 +215,7 @@ describe('app helpers', () => {
|
|||
await copyDataDir(appConfig.id);
|
||||
|
||||
// assert
|
||||
const appDataDir = `${storagePath}/app-data/${appConfig.id}`;
|
||||
const appDataDir = `${STORAGE_FOLDER}/app-data/${appConfig.id}`;
|
||||
expect(await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8')).toBe('test');
|
||||
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
|
||||
});
|
||||
|
@ -225,8 +223,8 @@ describe('app helpers', () => {
|
|||
it('should replace the content of .template files with the content of the app.env file', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
const dataDir = `${rootFolderHost}/apps/${appConfig.id}/data`;
|
||||
const appDataDir = `${storagePath}/app-data/${appConfig.id}`;
|
||||
const dataDir = `${ROOT_FOLDER}/apps/${appConfig.id}/data`;
|
||||
const appDataDir = `${STORAGE_FOLDER}/app-data/${appConfig.id}`;
|
||||
|
||||
await fs.promises.mkdir(dataDir, { recursive: true });
|
||||
await fs.promises.mkdir(appDataDir, { recursive: true });
|
||||
|
|
|
@ -130,7 +130,12 @@ export class AppExecutors {
|
|||
|
||||
// run docker-compose up
|
||||
this.logger.info(`Running docker-compose up for app ${appId}`);
|
||||
await compose(appId, 'up -d');
|
||||
const { stderr } = await compose(appId, 'up -d');
|
||||
|
||||
if (stderr) {
|
||||
this.logger.error(`Error running docker-compose up for app ${appId}: ${stderr}`);
|
||||
return { success: false, message: `Error running docker-compose up for app ${appId}: ${stderr}` };
|
||||
}
|
||||
|
||||
this.logger.info(`Docker-compose up for app ${appId} finished`);
|
||||
|
||||
|
@ -164,15 +169,18 @@ export class AppExecutors {
|
|||
}
|
||||
};
|
||||
|
||||
public startApp = async (appId: string, config: Record<string, unknown>) => {
|
||||
public startApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
|
||||
try {
|
||||
const { appDataDirPath } = this.getAppPaths(appId);
|
||||
|
||||
this.logger.info(`Starting app ${appId}`);
|
||||
|
||||
this.logger.info(`Regenerating app.env file for app ${appId}`);
|
||||
await this.ensureAppDir(appId);
|
||||
|
||||
if (!skipEnvGeneration) {
|
||||
this.logger.info(`Regenerating app.env file for app ${appId}`);
|
||||
await generateEnvFile(appId, config);
|
||||
}
|
||||
|
||||
await compose(appId, 'up --detach --force-recreate --remove-orphans --pull always');
|
||||
|
||||
|
|
|
@ -26,11 +26,11 @@ const runCommand = async (jobData: unknown) => {
|
|||
}
|
||||
|
||||
if (data.command === 'stop') {
|
||||
({ success, message } = await stopApp(data.appid, data.form));
|
||||
({ success, message } = await stopApp(data.appid, data.form, data.skipEnv));
|
||||
}
|
||||
|
||||
if (data.command === 'start') {
|
||||
({ success, message } = await startApp(data.appid, data.form));
|
||||
({ success, message } = await startApp(data.appid, data.form, data.skipEnv));
|
||||
}
|
||||
|
||||
if (data.command === 'uninstall') {
|
||||
|
|
38
packages/worker/tests/apps.factory.ts
Normal file
38
packages/worker/tests/apps.factory.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import fs from 'fs';
|
||||
import { APP_CATEGORIES, AppInfo, appInfoSchema } from '@runtipi/shared';
|
||||
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
|
||||
|
||||
export const createAppConfig = (props?: Partial<AppInfo>, isInstalled = true) => {
|
||||
const appInfo = appInfoSchema.parse({
|
||||
id: faker.string.alphanumeric(32),
|
||||
available: true,
|
||||
port: faker.number.int({ min: 30, max: 65535 }),
|
||||
name: faker.string.alphanumeric(32),
|
||||
description: faker.string.alphanumeric(32),
|
||||
tipi_version: 1,
|
||||
short_desc: faker.string.alphanumeric(32),
|
||||
author: faker.string.alphanumeric(32),
|
||||
source: faker.internet.url(),
|
||||
categories: [APP_CATEGORIES.AUTOMATION],
|
||||
...props,
|
||||
});
|
||||
|
||||
const mockFiles: Record<string, string | string[]> = {};
|
||||
mockFiles[`${ROOT_FOLDER}/.env`] = 'TEST=test';
|
||||
mockFiles[`${ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
mockFiles[`${ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
mockFiles[`${ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
|
||||
if (isInstalled) {
|
||||
mockFiles[`${ROOT_FOLDER}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
mockFiles[`${ROOT_FOLDER}/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
mockFiles[`${ROOT_FOLDER}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
mockFiles[`${STORAGE_FOLDER}/app-data/${appInfo.id}/data/test.txt`] = 'data';
|
||||
}
|
||||
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__applyMockFiles(mockFiles);
|
||||
|
||||
return appInfo;
|
||||
};
|
41
packages/worker/tests/mocks/fs.ts
Normal file
41
packages/worker/tests/mocks/fs.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { fs, vol } from 'memfs';
|
||||
|
||||
const copyFolderRecursiveSync = (src: string, dest: string) => {
|
||||
const exists = vol.existsSync(src);
|
||||
const stats = vol.statSync(src);
|
||||
const isDirectory = exists && stats.isDirectory();
|
||||
if (isDirectory) {
|
||||
vol.mkdirSync(dest, { recursive: true });
|
||||
vol.readdirSync(src).forEach((childItemName) => {
|
||||
copyFolderRecursiveSync(`${src}/${childItemName}`, `${dest}/${childItemName}`);
|
||||
});
|
||||
} else {
|
||||
vol.copyFileSync(src, dest);
|
||||
}
|
||||
};
|
||||
|
||||
export const fsMock = {
|
||||
default: {
|
||||
...fs,
|
||||
promises: {
|
||||
...fs.promises,
|
||||
cp: copyFolderRecursiveSync,
|
||||
},
|
||||
copySync: (src: string, dest: string) => {
|
||||
copyFolderRecursiveSync(src, dest);
|
||||
},
|
||||
__resetAllMocks: () => {
|
||||
vol.reset();
|
||||
},
|
||||
__applyMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
// Create folder tree
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__createMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
vol.reset();
|
||||
// Create folder tree
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__printVol: () => console.log(vol.toTree()),
|
||||
},
|
||||
};
|
42
packages/worker/tests/vite.setup.ts
Normal file
42
packages/worker/tests/vite.setup.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { vi, beforeEach } from 'vitest';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { ROOT_FOLDER } from '@/config/constants';
|
||||
|
||||
vi.mock('@runtipi/shared', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as object;
|
||||
|
||||
return {
|
||||
...mod,
|
||||
createLogger: vi.fn().mockReturnValue({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
FileLogger: vi.fn().mockImplementation(() => ({
|
||||
flush: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('fs', async () => {
|
||||
const { fsMock } = await import('@/tests/mocks/fs');
|
||||
return {
|
||||
...fsMock,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__resetAllMocks();
|
||||
|
||||
const { appsRepoId } = getEnv();
|
||||
|
||||
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'state'), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'state', 'seed'), 'seed');
|
||||
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps'), { recursive: true });
|
||||
});
|
|
@ -13,6 +13,9 @@
|
|||
"@/config/*": [
|
||||
"./src/config/*"
|
||||
],
|
||||
"@/tests/*": [
|
||||
"./tests/*"
|
||||
],
|
||||
},
|
||||
"lib": [
|
||||
"dom",
|
||||
|
|
10
packages/worker/vitest.config.ts
Normal file
10
packages/worker/vitest.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
setupFiles: ['./tests/vite.setup.ts'],
|
||||
coverage: { all: true, reporter: ['lcov', 'text-summary'] },
|
||||
},
|
||||
});
|
2004
pnpm-lock.yaml
2004
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -36,11 +36,11 @@ export const runPostgresMigrations = async (dbName?: string) => {
|
|||
|
||||
Logger.info('Running migrations');
|
||||
try {
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/cli/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/worker/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
} catch (e) {
|
||||
Logger.error('Error running migrations. Dropping table migrations and trying again');
|
||||
await client.query('DROP TABLE migrations');
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/cli/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
await migrate({ client }, path.join(__dirname, '../../packages/worker/assets/migrations'), { skipCreateMigrationTable: true });
|
||||
}
|
||||
|
||||
Logger.info('Migration complete');
|
||||
|
|
|
@ -89,10 +89,8 @@ export class SystemServiceClass {
|
|||
await cache.close();
|
||||
|
||||
const dispatcher = new EventDispatcher('restart');
|
||||
dispatcher.dispatchEvent({ type: 'system', command: 'restart' });
|
||||
await dispatcher.close();
|
||||
|
||||
return true;
|
||||
throw new Error('Implement restart');
|
||||
};
|
||||
|
||||
public static status = (): { status: SystemStatus } => ({
|
||||
|
|
Loading…
Reference in a new issue