mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-19 03:31:35 +08:00
Reject invalid extra_schedules with HTTP 400 before create or update reaches the task service. Preserve omitted, null and empty extras, supported cron syntax, and additional metadata. Localize missing rules, invalid string and array types, and non-object extra entries through the existing per-request translation middleware. Cover POST/PUT rejection without mutations, dynamic English/Chinese switching, and stable error paths. Validation: 9 related regression tests passed on current develop; backend build passed. The API validation fix also passed full regression and isolated develop/debian-dev acceptance. Refs #3099
270 lines
8.3 KiB
JavaScript
270 lines
8.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { Joi, CelebrateError } = require('celebrate');
|
|
|
|
const storePath = require.resolve('../../back/shared/store');
|
|
require.cache[storePath] = {
|
|
id: storePath,
|
|
filename: storePath,
|
|
loaded: true,
|
|
exports: { shareStore: { setLang: async () => {} } },
|
|
};
|
|
const { setLang, t } = require('../../back/shared/i18n');
|
|
const validationErrors =
|
|
require('../../back/middlewares/validationErrors').default;
|
|
const {
|
|
scheduleSchema,
|
|
commonCronSchema,
|
|
} = require('../../back/validation/schedule');
|
|
const upgrade = require('../../cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
|
|
|
function responseFor(schema, value, fullResponse = false) {
|
|
const validation = schema.validate(value).error;
|
|
assert.ok(validation);
|
|
const error = new CelebrateError('Validation failed', { celebrated: true });
|
|
error.details.set('body', validation);
|
|
let response;
|
|
const res = {
|
|
status(code) {
|
|
assert.equal(code, 400);
|
|
return this;
|
|
},
|
|
send(body) {
|
|
response = body;
|
|
return this;
|
|
},
|
|
};
|
|
validationErrors(error, {}, res, (err) => {
|
|
throw err;
|
|
});
|
|
return fullResponse ? response : response.validation.body.message;
|
|
}
|
|
|
|
test('compiled validation schemas follow live language changes without altering field paths', () => {
|
|
const schema = Joi.object({
|
|
schedule: scheduleSchema,
|
|
log_name: commonCronSchema.log_name,
|
|
});
|
|
for (const [lang, schedule, empty, logName] of [
|
|
[
|
|
'en',
|
|
'Invalid schedule rule',
|
|
'Schedule rule cannot be empty',
|
|
'Log name cannot exceed 100 characters',
|
|
],
|
|
['zh', '无效的定时规则', '定时规则不能为空', '日志名称不能超过100个字符'],
|
|
[
|
|
'en',
|
|
'Invalid schedule rule',
|
|
'Schedule rule cannot be empty',
|
|
'Log name cannot exceed 100 characters',
|
|
],
|
|
]) {
|
|
setLang(lang);
|
|
assert.equal(responseFor(schema, { schedule: 'invalid' }), schedule);
|
|
assert.equal(responseFor(schema, { schedule: '' }), empty);
|
|
assert.equal(
|
|
responseFor(schema, { schedule: '* * * * *', log_name: 'x'.repeat(101) }),
|
|
logName,
|
|
);
|
|
}
|
|
setLang('zh');
|
|
});
|
|
|
|
test('cron structure errors switch languages while retaining status and field paths', (t) => {
|
|
const schema = Joi.object(commonCronSchema);
|
|
const empty = ['定时规则不能为空', 'Schedule rule cannot be empty'];
|
|
const string = ['定时规则必须是字符串', 'Schedule rule must be a string'];
|
|
const object = ['额外定时规则必须是对象', 'Extra schedule must be an object'];
|
|
const array = ['额外定时规则必须是数组', 'Extra schedules must be an array'];
|
|
const invalid = ['无效的定时规则', 'Invalid schedule rule'];
|
|
const cases = [
|
|
[{ schedule: undefined }, 'schedule', empty],
|
|
[{ schedule: '' }, 'schedule', empty],
|
|
[{ schedule: 12 }, 'schedule', string],
|
|
[{ schedule: null }, 'schedule', string],
|
|
[{ extra_schedules: [{}] }, 'extra_schedules.0.schedule', empty],
|
|
[
|
|
{ extra_schedules: [{ schedule: '' }] },
|
|
'extra_schedules.0.schedule',
|
|
empty,
|
|
],
|
|
[
|
|
{ extra_schedules: [{ schedule: 12 }] },
|
|
'extra_schedules.0.schedule',
|
|
string,
|
|
],
|
|
[
|
|
{ extra_schedules: [{ schedule: null }] },
|
|
'extra_schedules.0.schedule',
|
|
string,
|
|
],
|
|
[
|
|
{ extra_schedules: [{ schedule: 'invalid' }] },
|
|
'extra_schedules.0.schedule',
|
|
invalid,
|
|
],
|
|
[
|
|
{ extra_schedules: [{ schedule: '0 * * * *' }, {}] },
|
|
'extra_schedules.1.schedule',
|
|
empty,
|
|
],
|
|
...[null, '0 * * * *', 12, []].map((item) => [
|
|
{ extra_schedules: [item] },
|
|
'extra_schedules.0',
|
|
object,
|
|
]),
|
|
...['0 * * * *', {}, 12].map((value) => [
|
|
{ extra_schedules: value },
|
|
'extra_schedules',
|
|
array,
|
|
]),
|
|
];
|
|
t.after(() => setLang('zh', false));
|
|
for (const lang of ['en', 'zh', 'en']) {
|
|
setLang(lang, false);
|
|
for (const [value, field, messages] of cases) {
|
|
const response = responseFor(
|
|
schema,
|
|
{
|
|
command: 'task valid.js',
|
|
schedule: '0 * * * *',
|
|
...value,
|
|
},
|
|
true,
|
|
);
|
|
assert.equal(response.statusCode, 400);
|
|
assert.equal(
|
|
response.validation.body.message,
|
|
messages[lang === 'zh' ? 0 : 1],
|
|
`${lang}: ${field}`,
|
|
);
|
|
assert.deepEqual(response.validation.body.keys, [field]);
|
|
}
|
|
const multiple = responseFor(
|
|
schema.prefs({ abortEarly: false }),
|
|
{
|
|
command: 'task valid.js',
|
|
schedule: '0 * * * *',
|
|
extra_schedules: [{}, { schedule: 12 }, null],
|
|
},
|
|
true,
|
|
);
|
|
assert.equal(multiple.statusCode, 400);
|
|
assert.equal(
|
|
multiple.validation.body.message,
|
|
[empty, string, object]
|
|
.map((messages) => messages[lang === 'zh' ? 0 : 1])
|
|
.join('. '),
|
|
);
|
|
assert.deepEqual(multiple.validation.body.keys, [
|
|
'extra_schedules.0.schedule',
|
|
'extra_schedules.1.schedule',
|
|
'extra_schedules.2',
|
|
]);
|
|
}
|
|
});
|
|
|
|
test('history, upload, initialization and proxy messages resolve dynamically and preserve user text', () => {
|
|
const keys = [
|
|
'请先初始化',
|
|
'请先关闭两步验证',
|
|
'仅支持 PNG、JPEG、GIF、WebP、AVIF 图片',
|
|
'历史版本容量不足,无法安全保存',
|
|
'文件已变化,请刷新历史版本后重试',
|
|
'trust proxy 配置格式无效',
|
|
];
|
|
setLang('en');
|
|
for (const key of keys) assert.doesNotMatch(t(key), /[\u4e00-\u9fff]/);
|
|
assert.equal(t('用户脚本自己的提示'), '用户脚本自己的提示');
|
|
setLang('zh');
|
|
for (const key of keys) assert.equal(t(key), key);
|
|
});
|
|
|
|
test('upgrade verification uses the supplied language and retains artifact details', async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-locale-upgrade-'));
|
|
try {
|
|
await fs.writeFile(
|
|
path.join(root, 'build-info.json'),
|
|
JSON.stringify({ version: 1, dirty: true }),
|
|
);
|
|
await assert.rejects(
|
|
upgrade.verifyUpgrade(root, root, { QL_LANG: 'en' }),
|
|
/Invalid or dirty build manifest/,
|
|
);
|
|
await assert.rejects(
|
|
upgrade.verifyUpgrade(root, root, { QL_LANG: 'zh' }),
|
|
/构建清单无效/,
|
|
);
|
|
await fs.writeFile(
|
|
path.join(root, 'build-info.json'),
|
|
JSON.stringify({
|
|
version: 1,
|
|
dirty: false,
|
|
sourceCommit: 'a'.repeat(40),
|
|
lockfileSha256: 'b'.repeat(64),
|
|
files: {},
|
|
}),
|
|
);
|
|
await assert.rejects(
|
|
upgrade.verifyUpgrade(root, root, { QL_LANG: 'en' }),
|
|
/Missing build output: build\/app.js/,
|
|
);
|
|
assert.equal(
|
|
upgrade.upgradeMessage('更新失败: %s', { QL_LANG: 'en' }, '文件名.txt'),
|
|
'Update failed: 文件名.txt',
|
|
);
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('Shell help switches language and preserves percent characters in script paths', async () => {
|
|
const bash = fs.access('/opt/homebrew/bin/bash').then(
|
|
() => '/opt/homebrew/bin/bash',
|
|
() => 'bash',
|
|
);
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-help-%-'));
|
|
try {
|
|
const source = await fs.readFile(
|
|
path.resolve(__dirname, '../../shell/otask.sh'),
|
|
'utf8',
|
|
);
|
|
const functions = path.join(root, 'functions.sh');
|
|
await fs.writeFile(
|
|
functions,
|
|
source.slice(
|
|
0,
|
|
source.indexOf('\nhandle_task_start "${task_shell_params[@]}"'),
|
|
),
|
|
);
|
|
for (const lang of ['en', 'zh']) {
|
|
const result = spawnSync(
|
|
await bash,
|
|
[
|
|
'-c',
|
|
'. shell/share.sh; . "shell/lang/$QL_LANG.sh"; . "$2"; dir_scripts="$1"; cmd_task=task; array_scripts=(); array_scripts_name=(); usage',
|
|
'--',
|
|
root,
|
|
functions,
|
|
],
|
|
{
|
|
cwd: path.resolve(__dirname, '../..'),
|
|
env: { ...process.env, QL_LANG: lang },
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.ok(result.stdout.includes(root));
|
|
if (lang === 'en') assert.doesNotMatch(result.stdout, /[\u4e00-\u9fff]/);
|
|
else assert.match(result.stdout, /暂无脚本可以执行/);
|
|
}
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|