test: fix paths

This commit is contained in:
Nicolas Meienberger 2024-03-09 19:14:07 +01:00 committed by Nicolas Meienberger
parent 6c5381954b
commit 83c163fa72
22 changed files with 100 additions and 74 deletions

View File

@ -84,7 +84,7 @@ services:
# Static
- ./.env:/data/.env
- /var/run/docker.sock:/var/run/docker.sock:ro
- /:/mnt/host:ro
- /proc:/host/proc:ro
networks:
- tipi_main_network
ports:
@ -145,6 +145,7 @@ services:
traefik.http.routers.worker-local.service: worker
traefik.http.routers.worker-api-local.rule: Host(`${LOCAL_DOMAIN}`) && PathPrefix("/worker-api")
traefik.http.routers.worker-api-local.entrypoints: websecure
traefik.http.routers.worker-api-local.service: worker-api
traefik.http.routers.worker-api-local.tls: true
networks:

View File

@ -80,7 +80,7 @@ services:
# Static
- ./.env:/data/.env
- /var/run/docker.sock:/var/run/docker.sock:ro
- /:/mnt/host:ro
- /proc:/host/proc
environment:
NODE_ENV: production
TIPI_VERSION: development
@ -138,6 +138,7 @@ services:
traefik.http.routers.worker-api-local.rule: Host(`${LOCAL_DOMAIN}`) && PathPrefix("/worker-api")
traefik.http.routers.worker-api-local.entrypoints: websecure
traefik.http.routers.worker-api-local.tls: true
traefik.http.routers.worker-api-local.service: worker-api
networks:
tipi_main_network:

View File

@ -4,6 +4,7 @@ import { vi, it, describe, beforeEach, expect } from 'vitest';
import { faker } from '@faker-js/faker';
import fs from 'fs';
import { compose } from './docker-helpers';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
const execAsync = vi.fn().mockImplementation(() => Promise.resolve({ stdout: '', stderr: '' }));
@ -39,10 +40,10 @@ describe('docker helpers', async () => {
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--env-file ${APP_DATA_DIR}/${appId}/app.env`,
`--project-name ${appId}`,
`-f /app/apps/${appId}/docker-compose.yml`,
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
`-f ${DATA_DIR}/apps/${appId}/docker-compose.yml`,
`-f ${DATA_DIR}/repos/repo-id/apps/docker-compose.common.yml`,
command,
].join(' ');
@ -53,8 +54,8 @@ describe('docker helpers', 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.mkdir(`${DATA_DIR}/user-config/${appId}`, { recursive: true });
const userEnvFile = `${DATA_DIR}/user-config/${appId}/app.env`;
await fs.promises.writeFile(userEnvFile, 'test');
// act
@ -63,11 +64,11 @@ describe('docker helpers', async () => {
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--env-file ${APP_DATA_DIR}/${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',
`-f ${DATA_DIR}/apps/${appId}/docker-compose.yml`,
`-f ${DATA_DIR}/repos/repo-id/apps/docker-compose.common.yml`,
command,
].join(' ');
@ -78,8 +79,8 @@ describe('docker helpers', 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.mkdir(`${DATA_DIR}/user-config/${appId}`, { recursive: true });
const userComposeFile = `${DATA_DIR}/user-config/${appId}/docker-compose.yml`;
await fs.promises.writeFile(userComposeFile, 'test');
// act
@ -88,10 +89,10 @@ describe('docker helpers', async () => {
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--env-file ${APP_DATA_DIR}/${appId}/app.env`,
`--project-name ${appId}`,
`-f /app/apps/${appId}/docker-compose.yml`,
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
`-f ${DATA_DIR}/apps/${appId}/docker-compose.yml`,
`-f ${DATA_DIR}/repos/repo-id/apps/docker-compose.common.yml`,
`--file ${userComposeFile}`,
command,
].join(' ');
@ -107,8 +108,8 @@ describe('docker helpers', async () => {
});
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.mkdir(`${DATA_DIR}/apps/${appId}`, { recursive: true });
const arm64ComposeFile = `${DATA_DIR}/apps/${appId}/docker-compose.arm64.yml`;
await fs.promises.writeFile(arm64ComposeFile, 'test');
// act
@ -117,10 +118,10 @@ describe('docker helpers', async () => {
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--env-file ${APP_DATA_DIR}/${appId}/app.env`,
`--project-name ${appId}`,
`-f ${arm64ComposeFile}`,
`-f /app/repos/repo-id/apps/docker-compose.common.yml`,
`-f ${DATA_DIR}/repos/repo-id/apps/docker-compose.common.yml`,
command,
].join(' ');

View File

@ -23,7 +23,7 @@ type EnvKeys =
| 'NGINX_PORT'
| 'NGINX_PORT_SSL'
| 'DOMAIN'
| 'APP_DATA_DIR'
| 'STOTAGE_PATH'
| 'POSTGRES_PORT'
| 'POSTGRES_HOST'
| 'POSTGRES_DBNAME'

View File

@ -115,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(`${DATA_DIR}/app-data/${appConfig.id}`, { recursive: true });
await fs.promises.mkdir(`${APP_DATA_DIR}/${appConfig.id}`, { recursive: true });
// act
await generateEnvFile(appConfig.id, {});

View File

@ -123,7 +123,7 @@ export class AppExecutors {
this.logger.info(`Copying folder ${repoPath} to ${appDirPath}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
// Create folder app-data folder
// Create app-data folder
this.logger.info(`Creating folder ${appDataDirPath}`);
await fs.promises.mkdir(appDataDirPath, { recursive: true });

View File

@ -119,7 +119,8 @@ const renderTemplate = (template: string, envMap: Map<string, string>) => {
let renderedTemplate = template;
envMap.forEach((value, key) => {
renderedTemplate = renderedTemplate.replace(new RegExp(`{{${key}}}`, 'g'), value);
const safeKey = key.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
renderedTemplate = renderedTemplate.replace(new RegExp(`{{${safeKey}}}`, 'g'), value);
});
return renderedTemplate;

View File

@ -29,13 +29,13 @@ export class SystemExecutors {
const memResult = { total: 0, used: 0, available: 0 };
try {
const memInfo = await fs.promises.readFile('/mnt/host/proc/meminfo');
const memInfo = await fs.promises.readFile('/host/proc/meminfo');
memResult.total = Number(memInfo.toString().match(/MemTotal:\s+(\d+)/)?.[1] ?? 0) * 1024;
memResult.available = Number(memInfo.toString().match(/MemAvailable:\s+(\d+)/)?.[1] ?? 0) * 1024;
memResult.used = memResult.total - memResult.available;
} catch (e) {
this.logger.error(`Unable to read /mnt/host/proc/meminfo: ${e}`);
this.logger.error(`Unable to read /host/proc/meminfo: ${e}`);
}
const [disk0] = await si.fsSize();

View File

@ -28,7 +28,7 @@ export const createAppConfig = (props?: Partial<AppInfo>, isInstalled = true) =>
mockFiles[`${DATA_DIR}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`${DATA_DIR}/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`${DATA_DIR}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
mockFiles[`${APP_DATA_DIR}/app-data/${appInfo.id}/data/test.txt`] = 'data';
mockFiles[`${APP_DATA_DIR}/${appInfo.id}/data/test.txt`] = 'data';
}
// @ts-expect-error - custom mock method

View File

@ -16,7 +16,7 @@ export type SettingsFormValues = {
internalIp?: string;
appsRepoUrl?: string;
domain?: string;
appDataDirPath?: string;
storagePath?: string;
localDomain?: string;
guestDashboard?: boolean;
allowAutoThemes?: boolean;
@ -246,7 +246,7 @@ export const SettingsForm = (props: IProps) => {
</div>
<div className="mb-3">
<Input
{...register('appDataDirPath')}
{...register('storagePath')}
label={
<>
{t('SETTINGS_GENERAL_STORAGE_PATH')}
@ -256,7 +256,7 @@ export const SettingsForm = (props: IProps) => {
<span className={clsx('ms-1 form-help storage-path-hint')}>?</span>
</>
}
error={errors.appDataDirPath?.message}
error={errors.storagePath?.message}
placeholder={t('SETTINGS_GENERAL_STORAGE_PATH')}
/>
</div>

View File

@ -1,14 +1,14 @@
import * as Sentry from '@sentry/nextjs';
import { getUserFromCookie } from '@/server/common/session.helpers';
import fs from 'fs-extra';
import { APP_DIR } from 'src/config';
import { DATA_DIR } from '../../../config/constants';
export async function GET() {
try {
const user = await getUserFromCookie();
if (user?.operator) {
const filePath = `${APP_DIR}/traefik/tls/cert.pem`;
const filePath = `${DATA_DIR}/traefik/tls/cert.pem`;
if (await fs.pathExists(filePath)) {
const file = await fs.promises.readFile(filePath);

View File

@ -1,7 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import { DATA_DIR } from 'src/config';
import { createLogger, format, transports } from 'winston';
import { DATA_DIR } from '../../../config/constants';
const { align, printf, timestamp, combine, colorize } = format;

View File

@ -1,5 +1,7 @@
import { faker } from '@faker-js/faker';
import fs from 'fs-extra';
import path from 'path';
import { DATA_DIR } from '@/config/constants';
import { TipiConfigClass } from './TipiConfig';
import { readJsonFile } from '../../common/fs.helpers';
@ -13,7 +15,6 @@ describe('Test: getConfig', () => {
// assert
expect(config).toBeDefined();
expect(config.NODE_ENV).toBe('test');
expect(config.rootFolder).toBe('/runtipi');
expect(config.internalIp).toBe('localhost');
});
@ -78,7 +79,7 @@ describe('Test: setConfig', () => {
expect(config).toBeDefined();
expect(config.appsRepoUrl).toBe(randomWord);
const settingsJson = readJsonFile('/data/state/settings.json') as { [key: string]: string };
const settingsJson = readJsonFile(path.join(DATA_DIR, 'state', 'settings.json')) as { [key: string]: string };
expect(settingsJson).toBeDefined();
expect(settingsJson.appsRepoUrl).toBe(randomWord);

View File

@ -3,6 +3,8 @@ import { fromAny } from '@total-typescript/shoehorn';
import { faker } from '@faker-js/faker';
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
import { appInfoSchema } from '@runtipi/shared';
import path from 'path';
import { DATA_DIR } from '@/config/constants';
import { TipiConfig } from '../../core/TipiConfig';
import { checkAppRequirements, getAppInfo, getAvailableApps, getUpdateInfo } from './apps.helpers';
import { createAppConfig, insertApp } from '../../tests/apps.factory';
@ -104,7 +106,7 @@ describe('Test: getAvailableApps()', () => {
// arrange
const appConfig = createAppConfig();
createAppConfig();
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appConfig.id}/config.json`, 'invalid json');
fs.writeFileSync(path.join(DATA_DIR, 'repos', 'repo-id', 'apps', appConfig.id, 'config.json'), 'invalid json');
// act
const availableApps = await getAvailableApps();
@ -131,7 +133,7 @@ describe('Test: getAppInfo()', () => {
const appConfig = createAppConfig();
const secondAppConfig = createAppConfig();
const app = await insertApp({}, appConfig, db);
fs.writeFileSync(`/runtipi/apps/${app.id}/config.json`, JSON.stringify(secondAppConfig));
fs.writeFileSync(path.join(DATA_DIR, 'apps', app.id, 'config.json'), JSON.stringify(secondAppConfig));
// act
const result = getAppInfo(app.id, app.status);
@ -145,7 +147,7 @@ describe('Test: getAppInfo()', () => {
const appConfig = createAppConfig();
const app = await insertApp({ status: 'missing' }, appConfig, db);
const secondAppConfig = createAppConfig();
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${app.id}/config.json`, JSON.stringify(secondAppConfig));
fs.writeFileSync(path.join(DATA_DIR, 'repos', 'repo-id', 'apps', app.id, 'config.json'), JSON.stringify(secondAppConfig));
// act
const result = getAppInfo(app.id, app.status);
@ -199,7 +201,7 @@ describe('Test: getUpdateInfo()', () => {
it('Should return default values if config.json is invalid', async () => {
// arrange
const appConfig = createAppConfig();
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appConfig.id}/config.json`, 'invalid json');
fs.writeFileSync(path.join(DATA_DIR, 'repos', 'repo-id', 'apps', appConfig.id, 'config.json'), 'invalid json');
// act
const updateInfo = getUpdateInfo(appConfig.id);

View File

@ -1,7 +1,8 @@
import { App } from '@/server/db/schema';
import { appInfoSchema } from '@runtipi/shared';
import { appInfoSchema, sanitizePath } from '@runtipi/shared';
import { pathExists } from '@runtipi/shared/node';
import { DATA_DIR } from '../../../config';
import { DATA_DIR } from '@/config/constants';
import path from 'path';
import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
import { TipiConfig } from '../../core/TipiConfig';
import { Logger } from '../../core/Logger';
@ -18,14 +19,15 @@ import { notEmpty } from '../../common/typescript.helpers';
* @throws Will throw an error if the app has an invalid config.json file or if the current system architecture is not supported by the app.
*/
export const checkAppRequirements = (appName: string) => {
const configFile = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${appName}/config.json`);
const { appsRepoId, architecture } = TipiConfig.getConfig();
const configFile = readJsonFile(path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps', sanitizePath(appName), 'config.json'));
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!parsedConfig.success) {
throw new Error(`App ${appName} has invalid config.json file`);
}
if (parsedConfig.data.supported_architectures && !parsedConfig.data.supported_architectures.includes(TipiConfig.getConfig().architecture)) {
if (parsedConfig.data.supported_architectures && !parsedConfig.data.supported_architectures.includes(architecture)) {
throw new Error(`App ${appName} is not supported on this architecture`);
}
@ -38,12 +40,13 @@ export const checkAppRequirements = (appName: string) => {
If the config.json file is invalid, it logs an error message.
*/
export const getAvailableApps = async () => {
if (!(await pathExists(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps`))) {
Logger.error(`Apps repo ${TipiConfig.getConfig().appsRepoId} not found. Make sure your repo is configured correctly.`);
const { appsRepoId } = TipiConfig.getConfig();
if (!(await pathExists(path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps')))) {
Logger.error(`Apps repo ${appsRepoId} not found. Make sure your repo is configured correctly.`);
return [];
}
const appsDir = readdirSync(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps`);
const appsDir = readdirSync(path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps'));
const skippedFiles = ['__tests__', 'docker-compose.common.yml', 'schema.json', '.DS_Store'];
@ -51,14 +54,15 @@ export const getAvailableApps = async () => {
.map((app) => {
if (skippedFiles.includes(app)) return null;
const configFile = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${app}/config.json`);
const repoPath = path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps', sanitizePath(app));
const configFile = readJsonFile(path.join(repoPath, 'config.json'));
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!parsedConfig.success) {
Logger.error(`App ${JSON.stringify(app)} has invalid config.json`);
Logger.error(JSON.stringify(parsedConfig.error, null, 2));
} else if (parsedConfig.data.available) {
const description = readFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${parsedConfig.data.id}/metadata/description.md`);
const description = readFile(path.join(repoPath, 'metadata', 'description.md'));
return { ...parsedConfig.data, description };
}
@ -78,7 +82,8 @@ export const getAvailableApps = async () => {
* @param {string} id - The app id.
*/
export const getUpdateInfo = (id: string) => {
const repoConfig = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`);
const { appsRepoId } = TipiConfig.getConfig();
const repoConfig = readJsonFile(path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps', sanitizePath(id), 'config.json'));
const parsedConfig = appInfoSchema.safeParse(repoConfig);
if (parsedConfig.success) {
@ -106,22 +111,27 @@ export const getAppInfo = (id: string, status?: App['status']) => {
// Check if app is installed
const installed = typeof status !== 'undefined' && status !== 'missing';
if (installed && fileExists(`${DATA_DIR}/apps/${id}/config.json`)) {
const configFile = readJsonFile(`${DATA_DIR}/apps/${id}/config.json`);
const appsFolder = path.join(DATA_DIR, 'apps', sanitizePath(id));
if (installed && fileExists(path.join(appsFolder, 'config.json'))) {
const configFile = readJsonFile(path.join(appsFolder, 'config.json'));
const parsedConfig = appInfoSchema.safeParse(configFile);
if (parsedConfig.success && parsedConfig.data.available) {
const description = readFile(`${DATA_DIR}/apps/${id}/metadata/description.md`);
const description = readFile(path.join(appsFolder, 'metadata', 'description.md'));
return { ...parsedConfig.data, description };
}
}
if (fileExists(`/data/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`);
const { appsRepoId } = TipiConfig.getConfig();
const repoFolder = path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps', sanitizePath(id));
if (fileExists(path.join(repoFolder, 'config.json'))) {
const configFile = readJsonFile(path.join(repoFolder, 'config.json'));
const parsedConfig = appInfoSchema.safeParse(configFile);
if (parsedConfig.success && parsedConfig.data.available) {
const description = readFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
const description = readFile(path.join(repoFolder, 'metadata', 'description.md'));
return { ...parsedConfig.data, description };
}
}

View File

@ -3,6 +3,8 @@ import waitForExpect from 'wait-for-expect';
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
import { faker } from '@faker-js/faker';
import { castAppConfig } from '@/lib/helpers/castAppConfig';
import path from 'path';
import { DATA_DIR } from '@/config/constants';
import { AppServiceClass } from './apps.service';
import { EventDispatcher } from '../../core/EventDispatcher';
import { getAllApps, getAppById, updateApp, createAppConfig, insertApp } from '../../tests/apps.factory';
@ -116,7 +118,7 @@ describe('Install app', () => {
it('Should throw if config.json is not valid', async () => {
// arrange
const appConfig = createAppConfig({});
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appConfig.id}/config.json`, 'test');
fs.writeFileSync(path.join(DATA_DIR, 'repos', 'repo-id', 'apps', appConfig.id, 'config.json'), 'test');
// act & assert
await expect(AppsService.installApp(appConfig.id, {})).rejects.toThrowError(`App ${appConfig.id} has invalid config.json file`);
@ -317,7 +319,7 @@ describe('List apps', () => {
// arrange
const appInfo = createAppConfig({});
createAppConfig({});
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, 'invalid json');
fs.writeFileSync(path.join(DATA_DIR, 'repos', 'repo-id', 'apps', appInfo.id, 'config.json'), 'invalid json');
// act
const { apps } = await AppsService.listApps();
@ -373,8 +375,8 @@ describe('installedApps', () => {
await insertApp({}, appConfig2, db);
await insertApp({}, appConfig3, db);
fs.writeFileSync(`/runtipi/apps/${appConfig3.id}/config.json`, 'invalid json');
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appConfig3.id}/config.json`, 'invalid json');
fs.writeFileSync(path.join(DATA_DIR, 'apps', appConfig3.id, 'config.json'), 'invalid json');
fs.writeFileSync(path.join(DATA_DIR, 'repos', 'repo-id', 'apps', appConfig3.id, 'config.json'), 'invalid json');
// act
const apps = await AppsService.installedApps();

View File

@ -8,6 +8,8 @@ import { mockInsert, mockQuery, mockSelect } from '@/tests/mocks/drizzle';
import { createDatabase, clearDatabase, closeDatabase, TestDatabase } from '@/server/tests/test-utils';
import { v4 } from 'uuid';
import { tipiCache } from '@/server/core/TipiCache';
import path from 'path';
import { DATA_DIR } from '@/config/constants';
import { encrypt } from '../../utils/encryption';
import { TipiConfig } from '../../core/TipiConfig';
import { createUser, getUserByEmail, getUserById } from '../../tests/user.factory';
@ -538,7 +540,7 @@ describe('Test: changeOperatorPassword', () => {
const user = await createUser({ email }, database);
const newPassword = faker.internet.password();
// @ts-expect-error - mocking fs
fs.__createMockFiles({ '/runtipi/state/password-change-request': '' });
fs.__createMockFiles({ [path.join(DATA_DIR, 'state', 'password-change-request')]: '' });
// Act
const result = await AuthService.changeOperatorPassword({ newPassword });
@ -567,7 +569,7 @@ describe('Test: changeOperatorPassword', () => {
await createUser({ email, operator: false }, database);
const newPassword = faker.internet.password();
// @ts-expect-error - mocking fs
fs.__createMockFiles({ '/runtipi/state/password-change-request': '' });
fs.__createMockFiles({ [path.join(DATA_DIR, 'state', 'password-change-request')]: '' });
// Act & Assert
await expect(AuthService.changeOperatorPassword({ newPassword })).rejects.toThrowError('AUTH_ERROR_OPERATOR_NOT_FOUND');
@ -579,7 +581,7 @@ describe('Test: changeOperatorPassword', () => {
const user = await createUser({ email, totpEnabled: true }, database);
const newPassword = faker.internet.password();
// @ts-expect-error - mocking fs
fs.__createMockFiles({ '/runtipi/state/password-change-request': '' });
fs.__createMockFiles({ [path.join(DATA_DIR, 'state', 'password-change-request')]: '' });
// Act
const result = await AuthService.changeOperatorPassword({ newPassword });
@ -597,7 +599,7 @@ describe('Test: checkPasswordChangeRequest', () => {
it('should return true if the password change request file exists', async () => {
// Arrange
// @ts-expect-error - mocking fs
fs.__createMockFiles({ '/runtipi/state/password-change-request': '' });
fs.__createMockFiles({ [path.join(DATA_DIR, 'state', 'password-change-request')]: '' });
// Act
const result = AuthServiceClass.checkPasswordChangeRequest();
@ -623,13 +625,13 @@ describe('Test: cancelPasswordChangeRequest', () => {
it('should delete the password change request file', async () => {
// Arrange
// @ts-expect-error - mocking fs
fs.__createMockFiles({ '/runtipi/state/password-change-request': '' });
fs.__createMockFiles({ [path.join(DATA_DIR, 'state', 'password-change-request')]: '' });
// Act
await AuthServiceClass.cancelPasswordChangeRequest();
// Assert
expect(fs.existsSync('/runtipi/state/password-change-request')).toBe(false);
expect(fs.existsSync(path.join(DATA_DIR, 'state', 'password-change-request'))).toBe(false);
});
});

View File

@ -8,7 +8,7 @@ import { Locales, getLocaleFromString } from '@/shared/internationalization/loca
import { generateSessionId, setSession } from '@/server/common/session.helpers';
import { Database } from '@/server/db';
import { tipiCache } from '@/server/core/TipiCache/TipiCache';
import { DATA_DIR } from '../../../config';
import { DATA_DIR } from '@/config/constants';
import { TipiConfig } from '../../core/TipiConfig';
import { fileExists, unlinkFile } from '../../common/fs.helpers';
import { decrypt, encrypt } from '../../utils/encryption';

View File

@ -1,7 +1,7 @@
import { promises } from 'fs';
import axios from 'redaxios';
import { tipiCache } from '@/server/core/TipiCache';
import { DATA_DIR } from '../../../config';
import { DATA_DIR } from '@/config/constants';
import { fileExists } from '../../common/fs.helpers';
import { Logger } from '../../core/Logger';
import { TipiConfig } from '../../core/TipiConfig';

View File

@ -35,10 +35,10 @@ const createAppConfig = (props?: Partial<AppInfo>) => {
});
const mockFiles: Record<string, string | string[]> = {};
mockFiles['/data/.env'] = 'TEST=test';
mockFiles[`/data/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`/data/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`/data/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
mockFiles[`${DATA_DIR}/.env`] = 'TEST=test';
mockFiles[`${DATA_DIR}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`${DATA_DIR}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`${DATA_DIR}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
// @ts-expect-error - custom mock method
fs.__applyMockFiles(mockFiles);
@ -147,7 +147,7 @@ const insertApp = async (data: Partial<NewApp>, appInfo: AppInfo, database: Test
const mockFiles: Record<string, string | string[]> = {};
if (data.status !== 'missing') {
mockFiles[`app-data/${values.id}/app.env`] = `TEST=test\nAPP_PORT=3000\n${Object.entries(data.config || {})
mockFiles[`${APP_DATA_DIR}/${values.id}/app.env`] = `TEST=test\nAPP_PORT=3000\n${Object.entries(data.config || {})
.map(([key, value]) => `${key}=${value}`)
.join('\n')}`;
mockFiles[`${DATA_DIR}/apps/${values.id}/config.json`] = JSON.stringify(appInfo);

View File

@ -2,6 +2,8 @@ import fs from 'fs-extra';
import { fromPartial } from '@total-typescript/shoehorn';
import { Job } from 'bullmq';
import { tipiCache } from '@/server/core/TipiCache';
import path from 'path';
import { DATA_DIR } from '@/config/constants';
global.fetch = jest.fn();
// Mock global location
@ -40,9 +42,9 @@ console.error = jest.fn();
beforeEach(async () => {
// @ts-expect-error - custom mock method
fs.__resetAllMocks();
await fs.promises.mkdir('/runtipi/state', { recursive: true });
await fs.promises.writeFile('/runtipi/state/settings.json', '{}');
await fs.promises.mkdir('/app/logs', { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'state'), { recursive: true });
await fs.promises.writeFile(path.join(DATA_DIR, 'state', 'settings.json'), '{}');
await fs.promises.mkdir(path.join(DATA_DIR, 'logs'), { recursive: true });
});
afterAll(async () => {

View File

@ -33,6 +33,9 @@
"@/api/*": [
"./src/app/api/*"
],
"@/config/*": [
"./src/config/*"
],
},
"lib": [
"dom",