mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-07 19:19:53 +08:00
Use MikroORM's official Node SQLite dialect with bounded database isolation, preserve legacy schema and data for rollback, and require Node 22.17 or newer. Update Debian runtimes and retain ARMv6 musl compatibility without a SQLite addon. Complete short archive writes, stop orphaned SQL workers after owner death, preserve buffered startup requests, and prestart the archived backend on every platform. Cover real package installation, migration, failure recovery, and legacy database compatibility. Keep authentication storage failures distinct from expired sessions with a pinned Keyv runtime, propagated cache errors, and retryable HTTP 503 responses. Preserve genuine 401 failures and verify legacy cache bytes, initialization safety, and session recovery.
181 lines
6.0 KiB
JavaScript
181 lines
6.0 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const express = require('express');
|
|
const load = require('../helpers/load-security-module.cjs');
|
|
function fixture(t, customData = false) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-access-scope-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const data = path.join(root, customData ? 'custom-data' : 'data');
|
|
const config = {
|
|
rootPath: root,
|
|
configPath: path.join(data, 'config'),
|
|
scriptPath: path.join(data, 'scripts'),
|
|
logPath: path.join(data, 'log'),
|
|
blackFileList: ['token.json', 'auth.json', 'grpc', 'env.js'],
|
|
};
|
|
for (const directory of [
|
|
config.configPath,
|
|
config.scriptPath,
|
|
config.logPath,
|
|
])
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
const secret = path.join(config.configPath, 'token.json');
|
|
fs.writeFileSync(secret, 'PANEL-SECRET');
|
|
for (const directory of [
|
|
config.configPath,
|
|
config.scriptPath,
|
|
config.logPath,
|
|
])
|
|
fs.symlinkSync(secret, path.join(directory, 'secret-link'));
|
|
const mocks = {
|
|
'../config': config,
|
|
typedi: { Service: () => (x) => x, Inject: () => () => {} },
|
|
'../config/util': {
|
|
getFileContentByName: (p) => fs.promises.readFile(p, 'utf8'),
|
|
rmPath: (p) => fs.promises.rm(p),
|
|
removeAnsi: (s) => s,
|
|
},
|
|
'../shared/utils': {
|
|
writeFileWithLock: (p, s) => fs.promises.writeFile(p, s),
|
|
},
|
|
'../shared/i18n': { t: (s) => s },
|
|
'../config/const': {},
|
|
'../data/runningInstance': {
|
|
InstanceStatus: { running: 'running' },
|
|
RunningInstanceModel: {
|
|
findOne: async (where, read = {}) => {
|
|
return null;
|
|
},
|
|
},
|
|
},
|
|
};
|
|
return { config, mocks, secret };
|
|
}
|
|
function api(name, service, mocks) {
|
|
mocks.typedi = { Container: { get: () => service } };
|
|
const app = express.Router();
|
|
load(path.resolve(`back/api/${name}.ts`), mocks).default(app);
|
|
const router = app.stack.find((layer) => layer.name === 'router').handle;
|
|
return async (method, url, values) => {
|
|
const handler = router.stack
|
|
.find((layer) => layer.route?.path === url && layer.route.methods[method])
|
|
.route.stack.at(-1).handle;
|
|
let result;
|
|
await handler(
|
|
{ query: values, body: values },
|
|
{
|
|
send: (value) => {
|
|
result = value;
|
|
},
|
|
download: (p) => {
|
|
result = { code: 200, data: fs.readFileSync(p, 'utf8') };
|
|
},
|
|
},
|
|
(error) => {
|
|
throw error;
|
|
},
|
|
);
|
|
return result;
|
|
};
|
|
}
|
|
for (const customData of [false, true]) {
|
|
test(`config APIs reject script compatibility paths (custom=${customData})`, async (t) => {
|
|
const { config, mocks, secret } = fixture(t, customData);
|
|
const Config = load(path.resolve('back/services/config.ts'), mocks).default;
|
|
mocks['../services/config'] = Config;
|
|
const invoke = api('config', new Config(), mocks);
|
|
const legacy = path.join(config.rootPath, 'data/scripts');
|
|
if (customData) {
|
|
fs.mkdirSync(legacy, { recursive: true });
|
|
fs.writeFileSync(path.join(legacy, 'notify.py'), 'legacy');
|
|
}
|
|
for (const filename of [
|
|
'token.json',
|
|
'auth.json',
|
|
'grpc/env.js',
|
|
'notify.py',
|
|
]) {
|
|
const file = path.join(config.scriptPath, filename);
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, 'initial');
|
|
const name = `data/scripts/${filename}`;
|
|
assert.equal((await invoke('get', '/detail', { path: name })).code, 403);
|
|
assert.equal(
|
|
(await invoke('post', '/save', { name, content: 'updated' })).code,
|
|
403,
|
|
);
|
|
assert.equal(fs.readFileSync(file, 'utf8'), 'initial');
|
|
}
|
|
if (customData)
|
|
assert.equal(
|
|
fs.readFileSync(path.join(legacy, 'notify.py'), 'utf8'),
|
|
'legacy',
|
|
);
|
|
for (const name of [
|
|
'token.json',
|
|
'grpc/client.key',
|
|
'secret-link',
|
|
'data/scripts/secret-link',
|
|
'data/scripts/../config/token.json',
|
|
secret,
|
|
]) {
|
|
assert.equal(
|
|
(await invoke('get', '/detail', { path: name })).code,
|
|
403,
|
|
name,
|
|
);
|
|
assert.equal(
|
|
(await invoke('post', '/save', { name, content: 'overwrite' })).code,
|
|
403,
|
|
name,
|
|
);
|
|
}
|
|
assert.equal(
|
|
(await invoke('post', '/save', { name: 'normal.txt', content: 'normal' }))
|
|
.code,
|
|
200,
|
|
);
|
|
assert.equal(
|
|
(await invoke('get', '/detail', { path: 'normal.txt' })).data,
|
|
'normal',
|
|
);
|
|
assert.equal(fs.readFileSync(secret, 'utf8'), 'PANEL-SECRET');
|
|
});
|
|
}
|
|
test('log APIs allow configuration-like names while retaining directory boundaries', async (t) => {
|
|
const { config, mocks, secret } = fixture(t);
|
|
const Log = load(path.resolve('back/services/log.ts'), mocks).default;
|
|
mocks['../services/log'] = Log;
|
|
const invoke = api('log', new Log(), mocks);
|
|
for (const filename of ['token.json', 'grpc/run.log', 'env.js/run.log']) {
|
|
const file = path.join(config.logPath, filename);
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, 'log content');
|
|
const values = {
|
|
path: path.dirname(filename) === '.' ? '' : path.dirname(filename),
|
|
file: path.basename(filename),
|
|
filename: path.basename(filename),
|
|
};
|
|
assert.equal((await invoke('get', '/detail', values)).data, 'log content');
|
|
assert.equal(
|
|
(await invoke('post', '/download', values)).data,
|
|
'log content',
|
|
);
|
|
assert.equal((await invoke('delete', '/', values)).code, 200);
|
|
assert.equal(fs.existsSync(file), false);
|
|
}
|
|
for (const filename of ['../config/token.json', 'secret-link', secret]) {
|
|
const values = { path: '', file: filename, filename };
|
|
for (const [method, route] of [
|
|
['get', '/detail'],
|
|
['post', '/download'],
|
|
['delete', '/'],
|
|
])
|
|
assert.equal((await invoke(method, route, values)).code, 403, filename);
|
|
}
|
|
assert.equal(fs.readFileSync(secret, 'utf8'), 'PANEL-SECRET');
|
|
});
|