mirror of
https://github.com/meienberger/runtipi.git
synced 2024-08-17 08:00:26 +03:00
feat: add APP_HOST variable and refactor fs mocks with memfs
This commit is contained in:
parent
db4923b9e7
commit
8453eebcd1
@ -41,10 +41,12 @@ module.exports = {
|
||||
'**/*.spec.{ts,tsx}',
|
||||
'**/*.factory.{ts,tsx}',
|
||||
'**/mocks/**',
|
||||
'**/__mocks__/**',
|
||||
'tests/**',
|
||||
'**/*.d.ts',
|
||||
'**/*.workspace.ts',
|
||||
'**/*.setup.{ts,js}',
|
||||
'**/*.config.{ts,js}',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -1,179 +1,35 @@
|
||||
import path from 'path';
|
||||
import { fs, vol } from 'memfs';
|
||||
|
||||
class FsMock {
|
||||
private static instance: FsMock;
|
||||
|
||||
private mockFiles = Object.create(null);
|
||||
|
||||
// private constructor() {}
|
||||
|
||||
static getInstance(): FsMock {
|
||||
if (!FsMock.instance) {
|
||||
FsMock.instance = new FsMock();
|
||||
}
|
||||
return FsMock.instance;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
__applyMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
export default {
|
||||
...fs,
|
||||
copySync: (src: string, dest: string) => {
|
||||
copyFolderRecursiveSync(src, dest);
|
||||
},
|
||||
__resetAllMocks: () => {
|
||||
vol.reset();
|
||||
},
|
||||
__applyMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
if (!this.mockFiles[dir]) {
|
||||
this.mockFiles[dir] = [];
|
||||
}
|
||||
|
||||
this.mockFiles[dir].push(path.basename(file));
|
||||
this.mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
__createMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
this.mockFiles = Object.create(null);
|
||||
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__createMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
vol.reset();
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
if (!this.mockFiles[dir]) {
|
||||
this.mockFiles[dir] = [];
|
||||
}
|
||||
|
||||
this.mockFiles[dir].push(path.basename(file));
|
||||
this.mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
__resetAllMocks = () => {
|
||||
this.mockFiles = Object.create(null);
|
||||
};
|
||||
|
||||
readFileSync = (p: string) => this.mockFiles[p];
|
||||
|
||||
existsSync = (p: string) => this.mockFiles[p] !== undefined;
|
||||
|
||||
writeFileSync = (p: string, data: string | string[]) => {
|
||||
this.mockFiles[p] = data;
|
||||
};
|
||||
|
||||
mkdirSync = (p: string) => {
|
||||
if (!this.mockFiles[p]) {
|
||||
this.mockFiles[p] = [];
|
||||
}
|
||||
};
|
||||
|
||||
rmSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
readdirSync = (p: string) => {
|
||||
const files: string[] = [];
|
||||
|
||||
const depth = p.split('/').length;
|
||||
|
||||
Object.keys(this.mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
copyFileSync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
};
|
||||
|
||||
copySync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
|
||||
if (this.mockFiles[source] instanceof Array) {
|
||||
this.mockFiles[source].forEach((file: string) => {
|
||||
this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createFileSync = (p: string) => {
|
||||
this.mockFiles[p] = '';
|
||||
};
|
||||
|
||||
unlinkSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
getMockFiles = () => this.mockFiles;
|
||||
|
||||
promises = {
|
||||
unlink: async (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete this.mockFiles[p];
|
||||
},
|
||||
writeFile: async (p: string, data: string | string[]) => {
|
||||
this.mockFiles[p] = data;
|
||||
|
||||
const dir = path.dirname(p);
|
||||
if (!this.mockFiles[dir]) {
|
||||
this.mockFiles[dir] = [];
|
||||
}
|
||||
|
||||
this.mockFiles[dir].push(path.basename(p));
|
||||
},
|
||||
mkdir: async (p: string) => {
|
||||
if (!this.mockFiles[p]) {
|
||||
this.mockFiles[p] = [];
|
||||
}
|
||||
},
|
||||
readdir: async (p: string) => {
|
||||
const files: string[] = [];
|
||||
|
||||
const depth = p.split('/').length;
|
||||
|
||||
Object.keys(this.mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
},
|
||||
lstat: async (p: string) => {
|
||||
return {
|
||||
isDirectory: () => {
|
||||
return this.mockFiles[p] instanceof Array;
|
||||
},
|
||||
};
|
||||
},
|
||||
readFile: async (p: string) => {
|
||||
return this.mockFiles[p];
|
||||
},
|
||||
copyFile: async (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default FsMock.getInstance();
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__printVol: () => console.log(vol.toTree()),
|
||||
};
|
||||
|
@ -22,6 +22,7 @@ export default async () => {
|
||||
const serverConfig = await createJestConfig(customServerConfig)();
|
||||
|
||||
return {
|
||||
randomize: true,
|
||||
verbose: true,
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/server/**/*.{ts,tsx}', 'src/client/**/*.{ts,tsx}', '!src/**/mocks/**/*.{ts,tsx}', '!**/*.{spec,test}.{ts,tsx}', '!**/index.{ts,tsx}'],
|
||||
|
@ -137,6 +137,7 @@
|
||||
"eslint-plugin-testing-library": "^5.11.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"memfs": "^4.2.0",
|
||||
"msw": "^1.2.2",
|
||||
"next-router-mock": "^0.9.7",
|
||||
"nodemon": "^2.0.22",
|
||||
|
194
pnpm-lock.yaml
194
pnpm-lock.yaml
@ -1,9 +1,5 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.1.1
|
||||
@ -319,6 +315,9 @@ devDependencies:
|
||||
jest-environment-jsdom:
|
||||
specifier: ^29.5.0
|
||||
version: 29.5.0
|
||||
memfs:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(quill-delta@5.1.0)(rxjs@7.8.0)(tslib@2.5.3)
|
||||
msw:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(typescript@5.1.5)
|
||||
@ -1615,10 +1614,10 @@ packages:
|
||||
resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
chalk: 4.1.2
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-util: 29.5.0
|
||||
slash: 3.0.0
|
||||
dev: true
|
||||
@ -1636,7 +1635,7 @@ packages:
|
||||
'@jest/reporters': 29.5.0
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/transform': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
@ -1646,7 +1645,7 @@ packages:
|
||||
jest-changed-files: 29.5.0
|
||||
jest-config: 29.5.0(@types/node@20.3.2)(ts-node@10.9.1)
|
||||
jest-haste-map: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.5.0
|
||||
jest-resolve-dependencies: 29.5.0
|
||||
@ -1657,7 +1656,7 @@ packages:
|
||||
jest-validate: 29.5.0
|
||||
jest-watcher: 29.5.0
|
||||
micromatch: 4.0.5
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
@ -1670,7 +1669,7 @@ packages:
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/fake-timers': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
jest-mock: 29.5.0
|
||||
dev: true
|
||||
@ -1696,10 +1695,10 @@ packages:
|
||||
resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@sinonjs/fake-timers': 10.0.2
|
||||
'@types/node': 20.3.2
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-mock: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
dev: true
|
||||
@ -1710,7 +1709,7 @@ packages:
|
||||
dependencies:
|
||||
'@jest/environment': 29.5.0
|
||||
'@jest/expect': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
jest-mock: 29.5.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -1729,7 +1728,7 @@ packages:
|
||||
'@jest/console': 29.5.0
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/transform': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
'@types/node': 20.3.2
|
||||
chalk: 4.1.2
|
||||
@ -1742,7 +1741,7 @@ packages:
|
||||
istanbul-lib-report: 3.0.0
|
||||
istanbul-lib-source-maps: 4.0.1
|
||||
istanbul-reports: 3.1.5
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-util: 29.5.0
|
||||
jest-worker: 29.5.0
|
||||
slash: 3.0.0
|
||||
@ -1760,6 +1759,13 @@ packages:
|
||||
'@sinclair/typebox': 0.25.23
|
||||
dev: true
|
||||
|
||||
/@jest/schemas@29.6.0:
|
||||
resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
dev: true
|
||||
|
||||
/@jest/source-map@29.4.3:
|
||||
resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@ -1774,7 +1780,7 @@ packages:
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/console': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
collect-v8-coverage: 1.0.1
|
||||
dev: true
|
||||
@ -1794,7 +1800,7 @@ packages:
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@babel/core': 7.22.5
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
babel-plugin-istanbul: 6.1.1
|
||||
chalk: 4.1.2
|
||||
@ -1824,6 +1830,18 @@ packages:
|
||||
chalk: 4.1.2
|
||||
dev: true
|
||||
|
||||
/@jest/types@29.6.0:
|
||||
resolution: {integrity: sha512-8XCgL9JhqbJTFnMRjEAO+TuW251+MoMd5BSzLiE3vvzpQ8RlBxy8NoyNkDhs3K3OL3HeVinlOl9or5p7GTeOLg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.0
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
'@types/istanbul-reports': 3.0.1
|
||||
'@types/node': 20.3.2
|
||||
'@types/yargs': 17.0.22
|
||||
chalk: 4.1.2
|
||||
dev: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.1.1:
|
||||
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@ -2766,6 +2784,10 @@ packages:
|
||||
resolution: {integrity: sha512-VEB8ygeP42CFLWyAJhN5OklpxUliqdNEUcXb4xZ/CINqtYGTjL5ukluKdKzQ0iWdUxyQ7B0539PAUhHKrCNWSQ==}
|
||||
dev: true
|
||||
|
||||
/@sinclair/typebox@0.27.8:
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
dev: true
|
||||
|
||||
/@sinonjs/commons@2.0.0:
|
||||
resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==}
|
||||
dependencies:
|
||||
@ -3813,6 +3835,10 @@ packages:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
dev: true
|
||||
|
||||
/arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
dev: true
|
||||
|
||||
/argon2@0.30.3:
|
||||
resolution: {integrity: sha512-DoH/kv8c9127ueJSBxAVJXinW9+EuPA3EMUxoV2sAY1qDE5H9BjTyVF/aD2XyHqbqUWabgBkIfcP3ZZuGhbJdg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -5708,7 +5734,7 @@ packages:
|
||||
'@jest/expect-utils': 29.5.0
|
||||
jest-get-type: 29.4.3
|
||||
jest-matcher-utils: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-util: 29.5.0
|
||||
dev: true
|
||||
|
||||
@ -6322,6 +6348,11 @@ packages:
|
||||
engines: {node: '>=10.17.0'}
|
||||
dev: true
|
||||
|
||||
/hyperdyperid@1.2.0:
|
||||
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
|
||||
engines: {node: '>=10.18'}
|
||||
dev: true
|
||||
|
||||
/iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6753,7 +6784,7 @@ packages:
|
||||
'@jest/environment': 29.5.0
|
||||
'@jest/expect': 29.5.0
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
chalk: 4.1.2
|
||||
co: 4.6.0
|
||||
@ -6761,12 +6792,12 @@ packages:
|
||||
is-generator-fn: 2.1.0
|
||||
jest-each: 29.5.0
|
||||
jest-matcher-utils: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-runtime: 29.5.0
|
||||
jest-snapshot: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
p-limit: 3.1.0
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
pure-rand: 6.0.1
|
||||
slash: 3.0.0
|
||||
stack-utils: 2.0.6
|
||||
@ -6786,7 +6817,7 @@ packages:
|
||||
dependencies:
|
||||
'@jest/core': 29.5.0(ts-node@10.9.1)
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.10
|
||||
@ -6816,7 +6847,7 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.22.5
|
||||
'@jest/test-sequencer': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
babel-jest: 29.5.0(@babel/core@7.22.5)
|
||||
chalk: 4.1.2
|
||||
@ -6834,7 +6865,7 @@ packages:
|
||||
jest-validate: 29.5.0
|
||||
micromatch: 4.0.5
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
ts-node: 10.9.1(@types/node@20.3.2)(typescript@5.1.5)
|
||||
@ -6849,7 +6880,7 @@ packages:
|
||||
chalk: 4.1.2
|
||||
diff-sequences: 29.4.3
|
||||
jest-get-type: 29.4.3
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
dev: true
|
||||
|
||||
/jest-docblock@29.4.3:
|
||||
@ -6863,11 +6894,11 @@ packages:
|
||||
resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
chalk: 4.1.2
|
||||
jest-get-type: 29.4.3
|
||||
jest-util: 29.5.0
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
dev: true
|
||||
|
||||
/jest-environment-jsdom@29.5.0:
|
||||
@ -6899,7 +6930,7 @@ packages:
|
||||
dependencies:
|
||||
'@jest/environment': 29.5.0
|
||||
'@jest/fake-timers': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
jest-mock: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
@ -6914,7 +6945,7 @@ packages:
|
||||
resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/graceful-fs': 4.1.6
|
||||
'@types/node': 20.3.2
|
||||
anymatch: 3.1.3
|
||||
@ -6934,7 +6965,7 @@ packages:
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
jest-get-type: 29.4.3
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
dev: true
|
||||
|
||||
/jest-matcher-utils@29.5.0:
|
||||
@ -6944,20 +6975,20 @@ packages:
|
||||
chalk: 4.1.2
|
||||
jest-diff: 29.5.0
|
||||
jest-get-type: 29.4.3
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
dev: true
|
||||
|
||||
/jest-message-util@29.5.0:
|
||||
resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==}
|
||||
/jest-message-util@29.6.0:
|
||||
resolution: {integrity: sha512-mkCp56cETbpoNtsaeWVy6SKzk228mMi9FPHSObaRIhbR2Ujw9PqjW/yqVHD2tN1bHbC8ol6h3UEo7dOPmIYwIA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.5
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/stack-utils': 2.0.1
|
||||
chalk: 4.1.2
|
||||
graceful-fs: 4.2.10
|
||||
micromatch: 4.0.5
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
slash: 3.0.0
|
||||
stack-utils: 2.0.6
|
||||
dev: true
|
||||
@ -6966,7 +6997,7 @@ packages:
|
||||
resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
jest-util: 29.5.0
|
||||
dev: true
|
||||
@ -7021,7 +7052,7 @@ packages:
|
||||
'@jest/environment': 29.5.0
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/transform': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
@ -7030,7 +7061,7 @@ packages:
|
||||
jest-environment-node: 29.5.0
|
||||
jest-haste-map: 29.5.0
|
||||
jest-leak-detector: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-resolve: 29.5.0
|
||||
jest-runtime: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
@ -7052,7 +7083,7 @@ packages:
|
||||
'@jest/source-map': 29.4.3
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/transform': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
chalk: 4.1.2
|
||||
cjs-module-lexer: 1.2.2
|
||||
@ -7060,7 +7091,7 @@ packages:
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.10
|
||||
jest-haste-map: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-mock: 29.5.0
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.5.0
|
||||
@ -7084,7 +7115,7 @@ packages:
|
||||
'@babel/types': 7.22.5
|
||||
'@jest/expect-utils': 29.5.0
|
||||
'@jest/transform': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/babel__traverse': 7.18.3
|
||||
'@types/prettier': 2.7.2
|
||||
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5)
|
||||
@ -7094,10 +7125,10 @@ packages:
|
||||
jest-diff: 29.5.0
|
||||
jest-get-type: 29.4.3
|
||||
jest-matcher-utils: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-message-util: 29.6.0
|
||||
jest-util: 29.5.0
|
||||
natural-compare: 1.4.0
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
semver: 7.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -7107,7 +7138,7 @@ packages:
|
||||
resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
@ -7119,12 +7150,12 @@ packages:
|
||||
resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
camelcase: 6.3.0
|
||||
chalk: 4.1.2
|
||||
jest-get-type: 29.4.3
|
||||
leven: 3.1.0
|
||||
pretty-format: 29.5.0
|
||||
pretty-format: 29.6.0
|
||||
dev: true
|
||||
|
||||
/jest-watcher@29.5.0:
|
||||
@ -7132,7 +7163,7 @@ packages:
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@jest/types': 29.6.0
|
||||
'@types/node': 20.3.2
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
@ -7259,6 +7290,22 @@ packages:
|
||||
dreamopt: 0.8.0
|
||||
dev: true
|
||||
|
||||
/json-joy@9.3.0(quill-delta@5.1.0)(rxjs@7.8.0)(tslib@2.5.3):
|
||||
resolution: {integrity: sha512-ZQiyMcbcfqki5Bsk0kWfne/Ixl4Q6cLBzCd3VE/TSp7jhns/WDBrIMTuyzDfwmLxuFtQdojiLSLX8MxTyK23QA==}
|
||||
engines: {node: '>=10.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
quill-delta: ^5
|
||||
rxjs: '7'
|
||||
tslib: '2'
|
||||
dependencies:
|
||||
arg: 5.0.2
|
||||
hyperdyperid: 1.2.0
|
||||
quill-delta: 5.1.0
|
||||
rxjs: 7.8.0
|
||||
tslib: 2.5.3
|
||||
dev: true
|
||||
|
||||
/json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
|
||||
@ -7384,6 +7431,14 @@ packages:
|
||||
p-locate: 5.0.0
|
||||
dev: true
|
||||
|
||||
/lodash.clonedeep@4.5.0:
|
||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||
dev: true
|
||||
|
||||
/lodash.isequal@4.5.0:
|
||||
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||
dev: true
|
||||
|
||||
/lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
dev: true
|
||||
@ -7633,6 +7688,20 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/memfs@4.2.0(quill-delta@5.1.0)(rxjs@7.8.0)(tslib@2.5.3):
|
||||
resolution: {integrity: sha512-V5/xE+zl6+soWxlBjiVTQSkfXybTwhEBj2I8sK9LaS5lcZsTuhRftakrcRpDY7Ycac2NTK/VzEtpKMp+gpymrQ==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
peerDependencies:
|
||||
tslib: '2'
|
||||
dependencies:
|
||||
json-joy: 9.3.0(quill-delta@5.1.0)(rxjs@7.8.0)(tslib@2.5.3)
|
||||
thingies: 1.12.0(tslib@2.5.3)
|
||||
tslib: 2.5.3
|
||||
transitivePeerDependencies:
|
||||
- quill-delta
|
||||
- rxjs
|
||||
dev: true
|
||||
|
||||
/memoize-one@6.0.0:
|
||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||
dev: false
|
||||
@ -8761,6 +8830,15 @@ packages:
|
||||
react-is: 18.2.0
|
||||
dev: true
|
||||
|
||||
/pretty-format@29.6.0:
|
||||
resolution: {integrity: sha512-XH+D4n7Ey0iSR6PdAnBs99cWMZdGsdKrR33iUHQNr79w1szKTCIZDVdXuccAsHVwDBp0XeWPfNEoaxP9EZgRmQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.0
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.2.0
|
||||
dev: true
|
||||
|
||||
/prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -8834,6 +8912,15 @@ packages:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
|
||||
/quill-delta@5.1.0:
|
||||
resolution: {integrity: sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
lodash.isequal: 4.5.0
|
||||
dev: true
|
||||
|
||||
/random-bytes@1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -9783,6 +9870,15 @@ packages:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
|
||||
/thingies@1.12.0(tslib@2.5.3):
|
||||
resolution: {integrity: sha512-AiGqfYC1jLmJagbzQGuoZRM48JPsr9yB734a7K6wzr34NMhjUPrWSQrkF7ZBybf3yCerCL2Gcr02kMv4NmaZfA==}
|
||||
engines: {node: '>=10.18'}
|
||||
peerDependencies:
|
||||
tslib: ^2
|
||||
dependencies:
|
||||
tslib: 2.5.3
|
||||
dev: true
|
||||
|
||||
/thirty-two@1.0.2:
|
||||
resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==}
|
||||
engines: {node: '>=0.2.6'}
|
||||
@ -10768,3 +10864,7 @@ packages:
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: false
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
@ -16,6 +16,11 @@ jest.mock('next/router', () => {
|
||||
|
||||
describe('Test: StatusProvider', () => {
|
||||
it("should render it's children when system is RUNNING", async () => {
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('RUNNING');
|
||||
});
|
||||
|
||||
render(
|
||||
<StatusProvider>
|
||||
<div>system running</div>
|
||||
@ -25,10 +30,12 @@ describe('Test: StatusProvider', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('system running')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render StatusScreen when system is RESTARTING', async () => {
|
||||
const { result } = renderHook(() => useSystemStore());
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('RESTARTING');
|
||||
});
|
||||
@ -41,10 +48,12 @@ describe('Test: StatusProvider', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Your system is restarting...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render StatusScreen when system is UPDATING', async () => {
|
||||
const { result } = renderHook(() => useSystemStore());
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('UPDATING');
|
||||
});
|
||||
@ -58,10 +67,12 @@ describe('Test: StatusProvider', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Your system is updating...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reload the page when system is RUNNING after being something else than RUNNING', async () => {
|
||||
const { result } = renderHook(() => useSystemStore());
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('UPDATING');
|
||||
});
|
||||
@ -82,5 +93,6 @@ describe('Test: StatusProvider', () => {
|
||||
await waitFor(() => {
|
||||
expect(reloadFn).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
@ -8,10 +8,13 @@ jest.mock('fs-extra');
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await fs.promises.mkdir('/runtipi/state', { recursive: true });
|
||||
await fs.promises.mkdir('/app/logs', { recursive: true });
|
||||
await fs.promises.writeFile(WATCH_FILE, '');
|
||||
await fs.promises.writeFile('/app/logs/123.log', 'test');
|
||||
|
||||
EventDispatcher.clear();
|
||||
fs.writeFileSync(WATCH_FILE, '');
|
||||
fs.writeFileSync('/app/logs/123.log', 'test');
|
||||
});
|
||||
|
||||
describe('EventDispatcher - dispatchEvent', () => {
|
||||
|
@ -3,11 +3,7 @@ import fs from 'fs-extra';
|
||||
import { getConfig, setConfig, getSettings, setSettings, TipiConfig } from '.';
|
||||
import { readJsonFile } from '../../common/fs.helpers';
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-expect-error - We are mocking fs
|
||||
fs.__resetAllMocks();
|
||||
jest.mock('fs-extra');
|
||||
});
|
||||
jest.mock('fs-extra');
|
||||
|
||||
jest.mock('next/config', () =>
|
||||
jest.fn(() => ({
|
||||
@ -124,9 +120,9 @@ describe('Test: setConfig', () => {
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should write config to json file', () => {
|
||||
it('Should write config to json file', async () => {
|
||||
const randomWord = faker.internet.url();
|
||||
setConfig('appsRepoUrl', randomWord, true);
|
||||
await setConfig('appsRepoUrl', randomWord, true);
|
||||
const config = getConfig();
|
||||
|
||||
expect(config).toBeDefined();
|
||||
@ -175,14 +171,14 @@ describe('Test: getSettings', () => {
|
||||
});
|
||||
|
||||
describe('Test: setSettings', () => {
|
||||
it('should write settings to json file', () => {
|
||||
it('should write settings to json file', async () => {
|
||||
// arrange
|
||||
const fakeSettings = {
|
||||
appsRepoUrl: faker.internet.url(),
|
||||
};
|
||||
|
||||
// act
|
||||
setSettings(fakeSettings);
|
||||
await setSettings(fakeSettings);
|
||||
const settingsJson = readJsonFile('/runtipi/state/settings.json') as { [key: string]: string };
|
||||
|
||||
// assert
|
||||
|
@ -2,21 +2,20 @@ import fs from 'fs-extra';
|
||||
import { fromAny, fromPartial } from '@total-typescript/shoehorn';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { getAppEnvMap } from '@/server/utils/env-generation';
|
||||
import { setConfig } from '../../core/TipiConfig';
|
||||
import { appInfoSchema, checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from './apps.helpers';
|
||||
import { appInfoSchema, checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getUpdateInfo } from './apps.helpers';
|
||||
import { createAppConfig, insertApp } from '../../tests/apps.factory';
|
||||
|
||||
let db: TestDatabase;
|
||||
const TEST_SUITE = 'appshelpers';
|
||||
jest.mock('fs-extra');
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await createDatabase(TEST_SUITE);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.mock('fs-extra');
|
||||
// @ts-expect-error - fs-extra mock is not typed
|
||||
fs.__resetAllMocks();
|
||||
await clearDatabase(db);
|
||||
});
|
||||
|
||||
@ -50,20 +49,6 @@ describe('Test: checkAppRequirements()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: getEnvMap()', () => {
|
||||
it('should return a map of env vars', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }] });
|
||||
insertApp({ config: { TEST_FIELD: 'test' } }, appConfig, db);
|
||||
|
||||
// act
|
||||
const envMap = getEnvMap(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(envMap.get('TEST_FIELD')).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: checkEnvFile()', () => {
|
||||
it('Should not throw if all required fields are present', async () => {
|
||||
// arrange
|
||||
@ -71,7 +56,7 @@ describe('Test: checkEnvFile()', () => {
|
||||
const app = await insertApp({}, appConfig, db);
|
||||
|
||||
// act
|
||||
checkEnvFile(app.id);
|
||||
await checkEnvFile(app.id);
|
||||
});
|
||||
|
||||
it('Should throw if a required field is missing', async () => {
|
||||
@ -83,7 +68,7 @@ describe('Test: checkEnvFile()', () => {
|
||||
fs.writeFileSync(`/app/storage/app-data/${app.id}/app.env`, newAppEnv);
|
||||
|
||||
// act & assert
|
||||
expect(() => checkEnvFile(app.id)).toThrowError('New info needed. App config needs to be updated');
|
||||
await expect(checkEnvFile(app.id)).rejects.toThrowError('New info needed. App config needs to be updated');
|
||||
});
|
||||
|
||||
it('Should throw if config.json is incorrect', async () => {
|
||||
@ -93,7 +78,7 @@ describe('Test: checkEnvFile()', () => {
|
||||
fs.writeFileSync(`/runtipi/apps/${app.id}/config.json`, 'invalid json');
|
||||
|
||||
// act & assert
|
||||
expect(() => checkEnvFile(app.id)).toThrowError(`App ${app.id} has invalid config.json file`);
|
||||
await expect(checkEnvFile(app.id)).rejects.toThrowError(`App ${app.id} has invalid config.json file`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -101,7 +86,8 @@ describe('Test: appInfoSchema', () => {
|
||||
it('should default form_field type to text if it is wrong', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig(fromAny({ form_fields: [{ env_variable: 'test', type: 'wrong', label: 'yo', required: true }] }));
|
||||
fs.writeFileSync(`/app/storage/app-data/${appConfig.id}/config.json`, JSON.stringify(appConfig));
|
||||
await fs.promises.mkdir(`/app/storage/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(`/app/storage/app-data/${appConfig.id}/config.json`, JSON.stringify(appConfig));
|
||||
|
||||
// act
|
||||
const appInfo = appInfoSchema.safeParse(appConfig);
|
||||
@ -118,7 +104,8 @@ describe('Test: appInfoSchema', () => {
|
||||
it('should default categories to ["utilities"] if it is wrong', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig(fromAny({ categories: 'wrong' }));
|
||||
fs.writeFileSync(`/app/storage/app-data/${appConfig.id}/config.json`, JSON.stringify(appConfig));
|
||||
await fs.promises.mkdir(`/app/storage/app-data/${appConfig.id}`, { recursive: true });
|
||||
await fs.promises.writeFile(`/app/storage/app-data/${appConfig.id}/config.json`, JSON.stringify(appConfig));
|
||||
|
||||
// act
|
||||
const appInfo = appInfoSchema.safeParse(appConfig);
|
||||
@ -141,8 +128,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const fakevalue = faker.string.alphanumeric(10);
|
||||
|
||||
// act
|
||||
generateEnvFile(Object.assign(app, { config: { TEST_FIELD: fakevalue } }));
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(Object.assign(app, { config: { TEST_FIELD: fakevalue } }));
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('TEST_FIELD')).toBe(fakevalue);
|
||||
@ -154,8 +141,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({}, appConfig, db);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('RANDOM_FIELD')).toBeDefined();
|
||||
@ -170,8 +157,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
fs.writeFileSync(`/app/storage/app-data/${app.id}/app.env`, `RANDOM_FIELD=${randomField}`);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('RANDOM_FIELD')).toBe(randomField);
|
||||
@ -183,12 +170,12 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({}, appConfig, db);
|
||||
|
||||
// act & assert
|
||||
expect(() => generateEnvFile(Object.assign(app, { config: { TEST_FIELD: undefined } }))).toThrowError('Variable test is required');
|
||||
await expect(generateEnvFile(Object.assign(app, { config: { TEST_FIELD: undefined } }))).rejects.toThrowError('Variable test is required');
|
||||
});
|
||||
|
||||
it('Should throw an error if app does not exist', async () => {
|
||||
// act & assert
|
||||
expect(() => generateEnvFile(fromPartial({ id: 'not-existing-app' }))).toThrowError('App not-existing-app has invalid config.json file');
|
||||
await expect(generateEnvFile(fromPartial({ id: 'not-existing-app' }))).rejects.toThrowError('App not-existing-app has invalid config.json file');
|
||||
});
|
||||
|
||||
it('Should add APP_EXPOSED to env file if domain is provided and app is exposed', async () => {
|
||||
@ -198,8 +185,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({ domain, exposed: true }, appConfig, db);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('APP_EXPOSED')).toBe('true');
|
||||
@ -212,8 +199,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({ exposed: true }, appConfig, db);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('APP_EXPOSED')).toBeUndefined();
|
||||
@ -225,8 +212,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({ exposed: false, domain: faker.internet.domainName() }, appConfig, db);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('APP_EXPOSED')).toBeUndefined();
|
||||
@ -240,7 +227,7 @@ describe('Test: generateEnvFile()', () => {
|
||||
fs.rmSync(`/app/storage/app-data/${app.id}`, { recursive: true });
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
await generateEnvFile(app);
|
||||
|
||||
// assert
|
||||
expect(fs.existsSync(`/app/storage/app-data/${app.id}`)).toBe(true);
|
||||
@ -252,8 +239,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({}, appConfig, db);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('VAPID_PRIVATE_KEY')).toBeDefined();
|
||||
@ -266,8 +253,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
const app = await insertApp({}, appConfig, db);
|
||||
|
||||
// act
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('VAPID_PRIVATE_KEY')).toBeUndefined();
|
||||
@ -284,8 +271,8 @@ describe('Test: generateEnvFile()', () => {
|
||||
|
||||
// act
|
||||
fs.writeFileSync(`/app/storage/app-data/${app.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`);
|
||||
generateEnvFile(app);
|
||||
const envmap = getEnvMap(app.id);
|
||||
await generateEnvFile(app);
|
||||
const envmap = await getAppEnvMap(app.id);
|
||||
|
||||
// assert
|
||||
expect(envmap.get('VAPID_PRIVATE_KEY')).toBe(vapidPrivateKey);
|
||||
@ -428,15 +415,10 @@ describe('Test: getUpdateInfo()', () => {
|
||||
});
|
||||
|
||||
describe('Test: ensureAppFolder()', () => {
|
||||
beforeEach(() => {
|
||||
const mockFiles = {
|
||||
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
|
||||
};
|
||||
// @ts-expect-error - Mocking fs
|
||||
fs.__createMockFiles(mockFiles);
|
||||
});
|
||||
|
||||
it('should copy the folder from repo', () => {
|
||||
it('should copy the folder from repo', async () => {
|
||||
// arrange
|
||||
await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile('/runtipi/repos/repo-id/apps/test/test.yml', 'test');
|
||||
// act
|
||||
ensureAppFolder('test');
|
||||
|
||||
@ -445,15 +427,12 @@ describe('Test: ensureAppFolder()', () => {
|
||||
expect(files).toEqual(['test.yml']);
|
||||
});
|
||||
|
||||
it('should not copy the folder if it already exists', () => {
|
||||
it('should not copy the folder if it already exists', async () => {
|
||||
// arrange
|
||||
const mockFiles = {
|
||||
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
|
||||
'/runtipi/apps/test': ['docker-compose.yml'],
|
||||
'/runtipi/apps/test/docker-compose.yml': 'test',
|
||||
};
|
||||
// @ts-expect-error - Mocking fs
|
||||
fs.__createMockFiles(mockFiles);
|
||||
await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile('/runtipi/repos/repo-id/apps/test/test.yml', 'test');
|
||||
await fs.promises.mkdir('/runtipi/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile('/runtipi/apps/test/docker-compose.yml', 'test');
|
||||
|
||||
// act
|
||||
ensureAppFolder('test');
|
||||
@ -463,15 +442,12 @@ describe('Test: ensureAppFolder()', () => {
|
||||
expect(files).toEqual(['docker-compose.yml']);
|
||||
});
|
||||
|
||||
it('Should overwrite the folder if clean up is true', () => {
|
||||
it('Should overwrite the folder if clean up is true', async () => {
|
||||
// arrange
|
||||
const mockFiles = {
|
||||
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
|
||||
'/runtipi/apps/test': ['docker-compose.yml'],
|
||||
'/runtipi/apps/test/docker-compose.yml': 'test',
|
||||
};
|
||||
// @ts-expect-error - Mocking fs
|
||||
fs.__createMockFiles(mockFiles);
|
||||
await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile('/runtipi/repos/repo-id/apps/test/test.yml', 'test');
|
||||
await fs.promises.mkdir('/runtipi/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile('/runtipi/apps/test/docker-compose.yml', 'test');
|
||||
|
||||
// act
|
||||
ensureAppFolder('test', true);
|
||||
@ -481,15 +457,13 @@ describe('Test: ensureAppFolder()', () => {
|
||||
expect(files).toEqual(['test.yml']);
|
||||
});
|
||||
|
||||
it('Should delete folder if it exists but has no docker-compose.yml file', () => {
|
||||
it('Should delete folder if it exists but has no docker-compose.yml file', async () => {
|
||||
// arrange
|
||||
const randomFileName = `${faker.lorem.word()}.yml`;
|
||||
const mockFiles = {
|
||||
[`/runtipi/repos/repo-id/apps/test`]: [randomFileName],
|
||||
'/runtipi/apps/test': ['test.yml'],
|
||||
};
|
||||
// @ts-expect-error - Mocking fs
|
||||
fs.__createMockFiles(mockFiles);
|
||||
await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile(`/runtipi/repos/repo-id/apps/test/${randomFileName}`, 'test');
|
||||
await fs.promises.mkdir('/runtipi/apps/test', { recursive: true });
|
||||
await fs.promises.writeFile('/runtipi/apps/test/test.yml', 'test');
|
||||
|
||||
// act
|
||||
ensureAppFolder('test');
|
||||
|
@ -2,8 +2,8 @@ import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import { z } from 'zod';
|
||||
import { App } from '@/server/db/schema';
|
||||
import { generateVapidKeys } from '@/server/utils/env-generation';
|
||||
import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../../common/fs.helpers';
|
||||
import { envMapToString, envStringToMap, generateVapidKeys, getAppEnvMap } from '@/server/utils/env-generation';
|
||||
import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
|
||||
import { APP_CATEGORIES, FIELD_TYPES } from './apps.types';
|
||||
import { getConfig } from '../../core/TipiConfig';
|
||||
import { Logger } from '../../core/Logger';
|
||||
@ -82,25 +82,6 @@ export const checkAppRequirements = (appName: string) => {
|
||||
return parsedConfig.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function reads the env file for the app with the provided name and returns a Map containing the key-value pairs of the environment variables.
|
||||
* It reads the file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value.
|
||||
*
|
||||
* @param {string} appName - The name of the app.
|
||||
*/
|
||||
export const getEnvMap = (appName: string) => {
|
||||
const envFile = readFile(`/app/storage/app-data/${appName}/app.env`).toString();
|
||||
const envVars = envFile.split('\n');
|
||||
const envVarsMap = new Map<string, string>();
|
||||
|
||||
envVars.forEach((envVar) => {
|
||||
const [key, value] = envVar.split('=');
|
||||
if (key && value) envVarsMap.set(key, value);
|
||||
});
|
||||
|
||||
return envVarsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function checks if the env file for the app with the provided name is valid.
|
||||
* It reads the config.json file for the app, parses it,
|
||||
@ -111,15 +92,23 @@ export const getEnvMap = (appName: string) => {
|
||||
* @param {string} appName - The name of the app.
|
||||
* @throws Will throw an error if the app has an invalid config.json file or if a required variable is missing in the env file.
|
||||
*/
|
||||
export const checkEnvFile = (appName: string) => {
|
||||
const configFile = readJsonFile(`/runtipi/apps/${appName}/config.json`);
|
||||
const parsedConfig = appInfoSchema.safeParse(configFile);
|
||||
export const checkEnvFile = async (appName: string) => {
|
||||
const configFile = await fs.promises.readFile(`/runtipi/apps/${appName}/config.json`);
|
||||
|
||||
let jsonConfig: unknown;
|
||||
try {
|
||||
jsonConfig = JSON.parse(configFile.toString());
|
||||
} catch (e) {
|
||||
throw new Error(`App ${appName} has invalid config.json file`);
|
||||
}
|
||||
|
||||
const parsedConfig = appInfoSchema.safeParse(jsonConfig);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
throw new Error(`App ${appName} has invalid config.json file`);
|
||||
}
|
||||
|
||||
const envMap = getEnvMap(appName);
|
||||
const envMap = await getAppEnvMap(appName);
|
||||
|
||||
parsedConfig.data.form_fields.forEach((field) => {
|
||||
const envVar = field.env_variable;
|
||||
@ -170,7 +159,7 @@ const castAppConfig = (json: unknown): Record<string, unknown> => {
|
||||
* @param {App} app - The app for which the env file is generated.
|
||||
* @throws Will throw an error if the app has an invalid config.json file or if a required variable is missing.
|
||||
*/
|
||||
export const generateEnvFile = (app: App) => {
|
||||
export const generateEnvFile = async (app: App) => {
|
||||
const configFile = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
|
||||
const parsedConfig = appInfoSchema.safeParse(configFile);
|
||||
|
||||
@ -179,17 +168,22 @@ export const generateEnvFile = (app: App) => {
|
||||
}
|
||||
|
||||
const baseEnvFile = readFile('/runtipi/.env').toString();
|
||||
let envFile = `${baseEnvFile}\nAPP_PORT=${parsedConfig.data.port}\nAPP_ID=${app.id}\n`;
|
||||
const envMap = getEnvMap(app.id);
|
||||
const envMap = envStringToMap(baseEnvFile);
|
||||
|
||||
// Default always present env variables
|
||||
envMap.set('APP_PORT', String(parsedConfig.data.port));
|
||||
envMap.set('APP_ID', app.id);
|
||||
|
||||
const existingEnvMap = await getAppEnvMap(app.id);
|
||||
|
||||
if (parsedConfig.data.generate_vapid_keys) {
|
||||
if (envMap.has('VAPID_PUBLIC_KEY') && envMap.has('VAPID_PRIVATE_KEY')) {
|
||||
envFile += `VAPID_PUBLIC_KEY=${envMap.get('VAPID_PUBLIC_KEY')}\n`;
|
||||
envFile += `VAPID_PRIVATE_KEY=${envMap.get('VAPID_PRIVATE_KEY')}\n`;
|
||||
if (existingEnvMap.has('VAPID_PUBLIC_KEY') && existingEnvMap.has('VAPID_PRIVATE_KEY')) {
|
||||
envMap.set('VAPID_PUBLIC_KEY', existingEnvMap.get('VAPID_PUBLIC_KEY') as string);
|
||||
envMap.set('VAPID_PRIVATE_KEY', existingEnvMap.get('VAPID_PRIVATE_KEY') as string);
|
||||
} else {
|
||||
const vapidKeys = generateVapidKeys();
|
||||
envFile += `VAPID_PUBLIC_KEY=${vapidKeys.publicKey}\n`;
|
||||
envFile += `VAPID_PRIVATE_KEY=${vapidKeys.privateKey}\n`;
|
||||
envMap.set('VAPID_PUBLIC_KEY', vapidKeys.publicKey);
|
||||
envMap.set('VAPID_PRIVATE_KEY', vapidKeys.privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,15 +192,15 @@ export const generateEnvFile = (app: App) => {
|
||||
const envVar = field.env_variable;
|
||||
|
||||
if (formValue || typeof formValue === 'boolean') {
|
||||
envFile += `${envVar}=${String(formValue)}\n`;
|
||||
envMap.set(envVar, String(formValue));
|
||||
} else if (field.type === 'random') {
|
||||
if (envMap.has(envVar)) {
|
||||
envFile += `${envVar}=${envMap.get(envVar)}\n`;
|
||||
if (existingEnvMap.has(envVar)) {
|
||||
envMap.set(envVar, existingEnvMap.get(envVar) as string);
|
||||
} else {
|
||||
const length = field.min || 32;
|
||||
const randomString = getEntropy(field.env_variable, length);
|
||||
|
||||
envFile += `${envVar}=${randomString}\n`;
|
||||
envMap.set(envVar, randomString);
|
||||
}
|
||||
} else if (field.required) {
|
||||
throw new Error(`Variable ${field.label || field.env_variable} is required`);
|
||||
@ -214,19 +208,22 @@ export const generateEnvFile = (app: App) => {
|
||||
});
|
||||
|
||||
if (app.exposed && app.domain) {
|
||||
envFile += 'APP_EXPOSED=true\n';
|
||||
envFile += `APP_DOMAIN=${app.domain}\n`;
|
||||
envFile += 'APP_PROTOCOL=https\n';
|
||||
envMap.set('APP_EXPOSED', 'true');
|
||||
envMap.set('APP_DOMAIN', app.domain);
|
||||
envMap.set('APP_PROTOCOL', 'https');
|
||||
envMap.set('APP_HOST', app.domain);
|
||||
} else {
|
||||
envFile += `APP_DOMAIN=${getConfig().internalIp}:${parsedConfig.data.port}\n`;
|
||||
envMap.set('APP_DOMAIN', `${getConfig().internalIp}:${parsedConfig.data.port}`);
|
||||
envMap.set('APP_HOST', getConfig().internalIp);
|
||||
}
|
||||
|
||||
// Create app-data folder if it doesn't exist
|
||||
if (!fs.existsSync(`/app/storage/app-data/${app.id}`)) {
|
||||
fs.mkdirSync(`/app/storage/app-data/${app.id}`, { recursive: true });
|
||||
const appDataDirectoryExists = await fs.promises.stat(`/app/storage/app-data/${app.id}`).catch(() => false);
|
||||
if (!appDataDirectoryExists) {
|
||||
await fs.promises.mkdir(`/app/storage/app-data/${app.id}`, { recursive: true });
|
||||
}
|
||||
|
||||
writeFile(`/app/storage/app-data/${app.id}/app.env`, envFile);
|
||||
await fs.promises.writeFile(`/app/storage/app-data/${app.id}/app.env`, envMapToString(envMap));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -253,7 +250,7 @@ const renderTemplate = (template: string, envMap: Map<string, string>) => {
|
||||
* @param {string} id - The id of the app.
|
||||
*/
|
||||
export const copyDataDir = async (id: string) => {
|
||||
const envMap = getEnvMap(id);
|
||||
const envMap = await getAppEnvMap(id);
|
||||
|
||||
const appDataDirExists = (await fs.promises.lstat(`/runtipi/apps/${id}/data`).catch(() => false)) as fs.Stats;
|
||||
if (!appDataDirExists || !appDataDirExists.isDirectory()) {
|
||||
|
@ -2,9 +2,9 @@ import fs from 'fs-extra';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { getAppEnvMap } from '@/server/utils/env-generation';
|
||||
import { AppServiceClass } from './apps.service';
|
||||
import { EventDispatcher, EVENT_TYPES } from '../../core/EventDispatcher';
|
||||
import { getEnvMap } from './apps.helpers';
|
||||
import { getAllApps, getAppById, updateApp, createAppConfig, insertApp } from '../../tests/apps.factory';
|
||||
import { setConfig } from '../../core/TipiConfig';
|
||||
|
||||
@ -18,11 +18,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.mock('fs-extra');
|
||||
await clearDatabase(db);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - we are mocking fs
|
||||
fs.__resetAllMocks();
|
||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
@ -37,10 +33,13 @@ describe('Install app', () => {
|
||||
|
||||
// act
|
||||
await AppsService.installApp(appConfig.id, { TEST_FIELD: 'test' });
|
||||
const envFile = fs.readFileSync(`/app/storage/app-data/${appConfig.id}/app.env`).toString();
|
||||
const envMap = await getAppEnvMap(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nAPP_ID=${appConfig.id}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${appConfig.port}`);
|
||||
expect(envMap.get('TEST_FIELD')).toBe('test');
|
||||
expect(envMap.get('APP_PORT')).toBe(appConfig.port.toString());
|
||||
expect(envMap.get('APP_ID')).toBe(appConfig.id);
|
||||
expect(envMap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`);
|
||||
});
|
||||
|
||||
it('Should add app in database', async () => {
|
||||
@ -102,7 +101,7 @@ describe('Install app', () => {
|
||||
|
||||
// act
|
||||
await AppsService.installApp(appConfig.id, {});
|
||||
const envMap = getEnvMap(appConfig.id);
|
||||
const envMap = await getAppEnvMap(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(envMap.get('RANDOM_FIELD')).toBeDefined();
|
||||
@ -243,8 +242,9 @@ describe('Install app', () => {
|
||||
it('should replace env variables in .templates files in data folder', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST', type: 'text', label: 'test', required: true }] });
|
||||
await fs.promises.writeFile(`/runtipi/apps/${appConfig.id}/data/test.txt.template`, 'test {{TEST}}');
|
||||
await fs.promises.writeFile(`/runtipi/apps/${appConfig.id}/data/test2.txt`, 'test {{TEST}}');
|
||||
await fs.promises.mkdir(`/runtipi/repos/repo-id/apps/${appConfig.id}/data`, { recursive: true });
|
||||
await fs.promises.writeFile(`/runtipi/repos/repo-id/apps/${appConfig.id}/data/test.txt.template`, 'test {{TEST}}');
|
||||
await fs.promises.writeFile(`/runtipi/repos/repo-id/apps/${appConfig.id}/data/test2.txt`, 'test {{TEST}}');
|
||||
|
||||
// act
|
||||
await AppsService.installApp(appConfig.id, { TEST: 'test' });
|
||||
@ -259,10 +259,10 @@ describe('Install app', () => {
|
||||
it('should copy and replace env variables in deeply nested .templates files in data folder', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST', type: 'text', label: 'test', required: true }] });
|
||||
await fs.promises.writeFile(`/runtipi/apps/${appConfig.id}/data/test.txt.template`, 'test {{TEST}}');
|
||||
await fs.promises.mkdir(`/runtipi/apps/${appConfig.id}/data/test`);
|
||||
await fs.promises.mkdir(`/runtipi/apps/${appConfig.id}/data/test/test`);
|
||||
await fs.promises.writeFile(`/runtipi/apps/${appConfig.id}/data/test/test/test.txt.template`, 'test {{TEST}}');
|
||||
await fs.promises.mkdir(`/runtipi/repos/repo-id/apps/${appConfig.id}/data`, { recursive: true });
|
||||
await fs.promises.writeFile(`/runtipi/repos/repo-id/apps/${appConfig.id}/data/test.txt.template`, 'test {{TEST}}');
|
||||
await fs.promises.mkdir(`/runtipi/repos/repo-id/apps/${appConfig.id}/data/test/test`, { recursive: true });
|
||||
await fs.promises.writeFile(`/runtipi/repos/repo-id/apps/${appConfig.id}/data/test/test/test.txt.template`, 'test {{TEST}}');
|
||||
|
||||
// act
|
||||
await AppsService.installApp(appConfig.id, { TEST: 'test' });
|
||||
@ -365,10 +365,14 @@ describe('Start app', () => {
|
||||
|
||||
// act
|
||||
await AppsService.startApp(appConfig.id);
|
||||
const envFile = fs.readFileSync(`/app/storage/app-data/${appConfig.id}/app.env`).toString();
|
||||
const envMap = await getAppEnvMap(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nAPP_ID=${appConfig.id}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${appConfig.port}`);
|
||||
expect(envMap.get('TEST_FIELD')).toBe('test');
|
||||
expect(envMap.get('APP_PORT')).toBe(appConfig.port.toString());
|
||||
expect(envMap.get('APP_ID')).toBe(appConfig.id);
|
||||
expect(envMap.get('TEST')).toBe('test');
|
||||
expect(envMap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`);
|
||||
});
|
||||
|
||||
it('Should throw if start script fails', async () => {
|
||||
@ -382,6 +386,18 @@ describe('Start app', () => {
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
expect(app?.status).toBe('stopped');
|
||||
});
|
||||
|
||||
it('Should throw if app has invalid config.json', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'stopped' }, appConfig, db);
|
||||
await fs.promises.writeFile(`/runtipi/apps/${appConfig.id}/config.json`, 'test');
|
||||
|
||||
// act & assert
|
||||
await expect(AppsService.startApp(appConfig.id)).rejects.toThrow(`App ${appConfig.id} has invalid config.json`);
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
expect(app?.status).toBe('stopped');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stop app', () => {
|
||||
@ -424,10 +440,13 @@ describe('Update app config', () => {
|
||||
|
||||
// act
|
||||
await AppsService.updateAppConfig(appConfig.id, { TEST_FIELD: word });
|
||||
const envFile = fs.readFileSync(`/app/storage/app-data/${appConfig.id}/app.env`).toString();
|
||||
const envMap = await getAppEnvMap(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nAPP_ID=${appConfig.id}\nTEST_FIELD=${word}\nAPP_DOMAIN=localhost:${appConfig.port}`);
|
||||
expect(envMap.get('TEST_FIELD')).toBe(word);
|
||||
expect(envMap.get('APP_PORT')).toBe(appConfig.port.toString());
|
||||
expect(envMap.get('APP_ID')).toBe(appConfig.id);
|
||||
expect(envMap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`);
|
||||
});
|
||||
|
||||
it('Should throw if required field is missing', async () => {
|
||||
@ -454,7 +473,7 @@ describe('Update app config', () => {
|
||||
|
||||
// act
|
||||
await AppsService.updateAppConfig(appConfig.id, { TEST_FIELD: 'test' });
|
||||
const envMap = getEnvMap(appConfig.id);
|
||||
const envMap = await getAppEnvMap(appConfig.id);
|
||||
|
||||
// assert
|
||||
expect(envMap.get(field)).toBe('test');
|
||||
@ -478,15 +497,6 @@ describe('Update app config', () => {
|
||||
expect(AppsService.updateAppConfig(appConfig.id, {}, true, 'test')).rejects.toThrowError('server-messages.errors.domain-not-valid');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and config does not allow it', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({ exposable: false });
|
||||
await insertApp({}, appConfig, db);
|
||||
|
||||
// act & assert
|
||||
expect(AppsService.updateAppConfig(appConfig.id, {}, true, 'test.com')).rejects.toThrowError('server-messages.errors.app-not-exposable');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and domain is already used', async () => {
|
||||
// arrange
|
||||
const domain = faker.internet.domainName();
|
||||
@ -518,6 +528,15 @@ describe('Update app config', () => {
|
||||
// act & assert
|
||||
await expect(AppsService.updateAppConfig(appConfig.id, {})).rejects.toThrowError('server-messages.errors.app-force-exposed');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and config does not allow it', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({ exposable: false });
|
||||
await insertApp({}, appConfig, db);
|
||||
|
||||
// act & assert
|
||||
await expect(AppsService.updateAppConfig(appConfig.id, {}, true, 'test.com')).rejects.toThrowError('server-messages.errors.app-not-exposable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get app config', () => {
|
||||
|
@ -29,6 +29,12 @@ export class AppServiceClass {
|
||||
this.queries = new AppQueries(p);
|
||||
}
|
||||
|
||||
async regenerateEnvFile(app: App) {
|
||||
ensureAppFolder(app.id);
|
||||
await generateEnvFile(app);
|
||||
await checkEnvFile(app.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function starts all apps that are in the 'running' status.
|
||||
* It finds all the running apps and starts them by regenerating the env file, checking the env file and dispatching the start event.
|
||||
@ -43,11 +49,9 @@ export class AppServiceClass {
|
||||
|
||||
await Promise.all(
|
||||
apps.map(async (app) => {
|
||||
// Regenerate env file
|
||||
try {
|
||||
ensureAppFolder(app.id);
|
||||
generateEnvFile(app);
|
||||
checkEnvFile(app.id);
|
||||
// Regenerate env file
|
||||
await this.regenerateEnvFile(app);
|
||||
|
||||
await this.queries.updateApp(app.id, { status: 'starting' });
|
||||
|
||||
@ -79,10 +83,8 @@ export class AppServiceClass {
|
||||
throw new TranslatedError('server-messages.errors.app-not-found', { id: appName });
|
||||
}
|
||||
|
||||
ensureAppFolder(appName);
|
||||
// Regenerate env file
|
||||
generateEnvFile(app);
|
||||
checkEnvFile(appName);
|
||||
await this.regenerateEnvFile(app);
|
||||
|
||||
await this.queries.updateApp(appName, { status: 'starting' });
|
||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['start', app.id]);
|
||||
@ -153,7 +155,7 @@ export class AppServiceClass {
|
||||
|
||||
if (newApp) {
|
||||
// Create env file
|
||||
generateEnvFile(newApp);
|
||||
await generateEnvFile(newApp);
|
||||
await copyDataDir(id);
|
||||
}
|
||||
|
||||
@ -229,7 +231,7 @@ export class AppServiceClass {
|
||||
const updatedApp = await this.queries.updateApp(id, { exposed: exposed || false, domain: domain || null, config: form });
|
||||
|
||||
if (updatedApp) {
|
||||
generateEnvFile(updatedApp);
|
||||
await generateEnvFile(updatedApp);
|
||||
}
|
||||
|
||||
return updatedApp;
|
||||
@ -248,8 +250,7 @@ export class AppServiceClass {
|
||||
throw new TranslatedError('server-messages.errors.app-not-found', { id });
|
||||
}
|
||||
|
||||
ensureAppFolder(id);
|
||||
generateEnvFile(app);
|
||||
await this.regenerateEnvFile(app);
|
||||
|
||||
// Run script
|
||||
await this.queries.updateApp(id, { status: 'stopping' });
|
||||
@ -284,8 +285,7 @@ export class AppServiceClass {
|
||||
await this.stopApp(id);
|
||||
}
|
||||
|
||||
ensureAppFolder(id);
|
||||
generateEnvFile(app);
|
||||
await this.regenerateEnvFile(app);
|
||||
|
||||
await this.queries.updateApp(id, { status: 'uninstalling' });
|
||||
|
||||
@ -336,8 +336,7 @@ export class AppServiceClass {
|
||||
throw new TranslatedError('server-messages.errors.app-not-found', { id });
|
||||
}
|
||||
|
||||
ensureAppFolder(id);
|
||||
generateEnvFile(app);
|
||||
await this.regenerateEnvFile(app);
|
||||
|
||||
await this.queries.updateApp(id, { status: 'updating' });
|
||||
|
||||
|
@ -15,6 +15,8 @@ const SystemService = new SystemServiceClass();
|
||||
const server = setupServer();
|
||||
|
||||
beforeEach(async () => {
|
||||
await setConfig('demoMode', false);
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import fs from 'fs-extra';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import fs from 'fs-extra';
|
||||
import { Architecture } from '../core/TipiConfig/TipiConfig';
|
||||
import { AppInfo, appInfoSchema } from '../services/apps/apps.helpers';
|
||||
import { APP_CATEGORIES } from '../services/apps/apps.types';
|
||||
@ -21,8 +21,6 @@ interface IProps {
|
||||
}
|
||||
|
||||
const createAppConfig = (props?: Partial<AppInfo>) => {
|
||||
const mockFiles: Record<string, string | string[]> = {};
|
||||
|
||||
const appInfo = appInfoSchema.parse({
|
||||
id: faker.string.alphanumeric(32),
|
||||
available: true,
|
||||
@ -37,13 +35,13 @@ const createAppConfig = (props?: Partial<AppInfo>) => {
|
||||
...props,
|
||||
});
|
||||
|
||||
const mockFiles: Record<string, string | string[]> = {};
|
||||
mockFiles['/runtipi/.env'] = 'TEST=test';
|
||||
mockFiles['/runtipi/repos/repo-id'] = '';
|
||||
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
|
||||
// @ts-expect-error - fs-extra mock is not typed
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__applyMockFiles(mockFiles);
|
||||
|
||||
return appInfo;
|
||||
@ -103,12 +101,11 @@ const createApp = async (props: IProps, database: TestDatabase) => {
|
||||
});
|
||||
}
|
||||
|
||||
const MockFiles: Record<string, string | string[]> = {};
|
||||
MockFiles['/runtipi/.env'] = 'TEST=test';
|
||||
MockFiles['/runtipi/repos/repo-id'] = '';
|
||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
const mockFiles: Record<string, string | string[]> = {};
|
||||
mockFiles['/runtipi/.env'] = 'TEST=test';
|
||||
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
|
||||
let appEntity: App = {} as App;
|
||||
if (installed) {
|
||||
@ -126,14 +123,15 @@ const createApp = async (props: IProps, database: TestDatabase) => {
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
appEntity = insertedApp[0] as App;
|
||||
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
|
||||
MockFiles[`/runtipi/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
|
||||
MockFiles[`/runtipi/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
mockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
|
||||
mockFiles[`/runtipi/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
|
||||
mockFiles[`/runtipi/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
}
|
||||
|
||||
return { appInfo, MockFiles, appEntity };
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__applyMockFiles(mockFiles);
|
||||
|
||||
return { appInfo, MockFiles: mockFiles, appEntity };
|
||||
};
|
||||
|
||||
const insertApp = async (data: Partial<NewApp>, appInfo: AppInfo, database: TestDatabase) => {
|
||||
@ -149,15 +147,15 @@ const insertApp = async (data: Partial<NewApp>, appInfo: AppInfo, database: Test
|
||||
|
||||
const mockFiles: Record<string, string | string[]> = {};
|
||||
if (data.status !== 'missing') {
|
||||
mockFiles[`/app/storage/app-data/${values.id}`] = '';
|
||||
mockFiles[`/app/storage/app-data/${values.id}/app.env`] = `TEST=test\nAPP_PORT=3000\n${Object.entries(data.config || {})
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n')}`;
|
||||
mockFiles[`/runtipi/apps/${values.id}/config.json`] = JSON.stringify(appInfo);
|
||||
mockFiles[`/runtipi/apps/${values.id}/metadata/description.md`] = 'md desc';
|
||||
mockFiles[`/runtipi/apps/${values.id}/docker-compose.yml`] = 'compose';
|
||||
}
|
||||
|
||||
// @ts-expect-error - fs-extra mock is not typed
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__applyMockFiles(mockFiles);
|
||||
|
||||
const insertedApp = await database.db.insert(appTable).values(values).returning();
|
||||
|
@ -1,4 +1,58 @@
|
||||
import webpush from 'web-push';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/**
|
||||
* Convert a string of environment variables to a Map
|
||||
*
|
||||
* @param {string} envString - String of environment variables
|
||||
*/
|
||||
export const envStringToMap = (envString: string) => {
|
||||
const envMap = new Map<string, string>();
|
||||
const envArray = envString.split('\n');
|
||||
|
||||
envArray.forEach((env) => {
|
||||
const [key, value] = env.split('=');
|
||||
if (key && value) {
|
||||
envMap.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return envMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Map of environment variables to a valid string of environment variables
|
||||
* that can be used in a .env file
|
||||
*
|
||||
* @param {Map<string, string>} envMap - Map of environment variables
|
||||
*/
|
||||
export const envMapToString = (envMap: Map<string, string>) => {
|
||||
const envArray = Array.from(envMap).map(([key, value]) => `${key}=${value}`);
|
||||
return envArray.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
|
||||
* It reads the app.env file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value.
|
||||
*
|
||||
* @param {string} id - App ID
|
||||
*/
|
||||
export const getAppEnvMap = async (id: string) => {
|
||||
try {
|
||||
const envFile = await fs.promises.readFile(`/app/storage/app-data/${id}/app.env`);
|
||||
const envVars = envFile.toString().split('\n');
|
||||
const envVarsMap = new Map<string, string>();
|
||||
|
||||
envVars.forEach((envVar) => {
|
||||
const [key, value] = envVar.split('=');
|
||||
if (key && value) envVarsMap.set(key, value);
|
||||
});
|
||||
|
||||
return envVarsMap;
|
||||
} catch (e) {
|
||||
return new Map<string, string>();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate VAPID keys
|
||||
|
@ -1,3 +1,4 @@
|
||||
import fs from 'fs-extra';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { EventDispatcher } from '../../src/server/core/EventDispatcher';
|
||||
|
||||
@ -14,6 +15,14 @@ jest.mock('vitest', () => ({
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
// Mock Logger
|
||||
jest.mock('../../src/server/core/Logger', () => ({
|
||||
Logger: {
|
||||
|
Loading…
Reference in New Issue
Block a user