mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +08:00
152 lines
6.6 KiB
JavaScript
152 lines
6.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { test } = require('node:test');
|
|
|
|
const apiFile = path.resolve(__dirname, '../../shell/api.sh');
|
|
|
|
const endpoints = {
|
|
status: {
|
|
method: 'PUT', url: /\/open\/crons\/status\?t=\d+$/,
|
|
body: { ids: ['37'], status: '1', pid: '4321', log_path: 'task/run.log',
|
|
last_execution_time: 1787004000, last_running_time: 90, exit_code: 0 },
|
|
success: '', failure: (message) => message,
|
|
},
|
|
statistics: {
|
|
method: 'POST', url: /\/open\/dashboard\/record$/,
|
|
body: { ref_id: 37, code: 0, elapsed: 90 },
|
|
success: '', failure: (message) => message,
|
|
},
|
|
add: {
|
|
method: 'POST', url: /\/open\/crons\?t=\d+$/,
|
|
body: { name: 'sample job', command: 'task job.py', schedule: '0 * * * *', sub_id: null },
|
|
success: 'sample job -> 添加成功', failure: (message) => `sample job -> 添加失败(${message})`,
|
|
},
|
|
update: {
|
|
method: 'PUT', url: /\/open\/crons\?t=\d+$/,
|
|
body: { name: 'sample job', command: 'task job.py', schedule: '0 * * * *', id: '37' },
|
|
success: 'sample job -> 更新成功', failure: (message) => `sample job -> 更新失败(${message})`,
|
|
},
|
|
command: {
|
|
method: 'PUT', url: /\/open\/crons\?t=\d+$/,
|
|
body: { command: 'task job.py', id: '37' },
|
|
success: 'task job.py -> 更新成功', failure: (message) => `task job.py -> 更新失败(${message})`,
|
|
},
|
|
delete: {
|
|
method: 'DELETE', url: /\/open\/crons\?t=\d+$/,
|
|
body: [37, 38], success: '成功', failure: (message) => `失败(${message})`,
|
|
},
|
|
notify: {
|
|
method: 'PUT', url: /\/open\/system\/notify\?t=\d+$/,
|
|
body: { title: 'title', content: 'body' },
|
|
success: '通知发送成功🎉', failure: (message) => `通知失败(${message})`,
|
|
},
|
|
find: {
|
|
method: 'GET', url: /\/open\/crons\/detail\?log_path=task\/run.log&t=\d+$/,
|
|
body: null, success: 'sample job', failure: () => '',
|
|
},
|
|
auth: {
|
|
method: 'PUT', url: /\/open\/system\/auth\/reset\?t=\d+$/,
|
|
body: { retries: 0 }, success: '重置登录错误次数成功🎉',
|
|
failure: (message) => `重置登录错误次数失败(${message})`,
|
|
},
|
|
};
|
|
|
|
function runCallback(endpoint, scenario) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-api-token-'));
|
|
try {
|
|
const tokenFile = path.join(directory, 'token.json');
|
|
fs.writeFileSync(tokenFile, JSON.stringify({
|
|
value: 'initial-token', expiration: Math.floor(Date.now() / 1000) + 3600,
|
|
}));
|
|
const child = spawnSync('bash', ['-c', `
|
|
. "$API_FILE"
|
|
create_token() {
|
|
printf 'refresh\\n' >> "$CAPTURE"
|
|
__ql_token__=fresh-token
|
|
printf '{"value":"fresh-token","expiration":4102444800}' > "$file_auth_token"
|
|
}
|
|
case "$SCENARIO" in
|
|
expired) printf '{"value":"initial-token","expiration":1}' > "$file_auth_token" ;;
|
|
replaced) printf '{"value":"fresh-token","expiration":4102444800}' > "$file_auth_token" ;;
|
|
esac
|
|
curl() {
|
|
local auth body url method arg
|
|
while [[ $# -gt 0 ]]; do
|
|
arg="$1"; shift
|
|
case "$arg" in
|
|
-H) [[ "$1" == Authorization:* ]] && auth="$1"; shift ;;
|
|
--data-raw) body="$1"; shift ;;
|
|
-X) method="$1"; shift ;;
|
|
http:*) url="$arg" ;;
|
|
esac
|
|
done
|
|
printf '%s\\n%s\\n%s\\n%s\\n' "$auth" "$method" "$url" "\${body:-null}" >> "$CAPTURE"
|
|
if [[ "$SCENARIO" == forbidden ]]; then
|
|
printf '{"code":403,"message":"forbidden"}'
|
|
elif [[ "$SCENARIO" == unavailable ]]; then
|
|
printf '{"code":500,"message":"unavailable"}'
|
|
elif [[ "$SCENARIO" == persistent || ( "$SCENARIO" != valid && "$auth" != 'Authorization: Bearer fresh-token' ) ]]; then
|
|
printf '{"code":401,"message":"Token 已失效"}'
|
|
else
|
|
printf '{"code":200,"data":{"name":"sample job"}}'
|
|
fi
|
|
}
|
|
t() { printf "$@"; }
|
|
case "$ENDPOINT" in
|
|
status) update_cron '"37"' 1 4321 'task/run.log' 1787004000 90 0 execution-37 ;;
|
|
statistics) record_cron_stat 37 0 90 ;;
|
|
add) add_cron_api '0 * * * *' 'task job.py' 'sample job' ;;
|
|
update) update_cron_api '0 * * * *' 'task job.py' 'sample job' 37 ;;
|
|
command) update_cron_command_api 'task job.py' 37 ;;
|
|
delete) del_cron_api '37,38' ;;
|
|
notify) notify_api title body ;;
|
|
find) find_cron_api 'log_path=task/run.log' ;;
|
|
auth) update_auth_config '"retries":0' '重置登录错误次数' ;;
|
|
esac
|
|
`], {
|
|
env: { ...process.env, PATH: `/usr/bin:/bin:${process.env.PATH}`, API_FILE: apiFile, file_auth_token: tokenFile,
|
|
CAPTURE: path.join(directory, 'requests'), SCENARIO: scenario,
|
|
ENDPOINT: endpoint, ql_port: '5700' },
|
|
encoding: 'utf8', timeout: 10_000,
|
|
});
|
|
assert.equal(child.status, 0, child.stderr);
|
|
assert.equal(child.stderr, '');
|
|
const lines = fs.readFileSync(path.join(directory, 'requests'), 'utf8').trim().split('\n');
|
|
const refreshes = lines.filter((line) => line === 'refresh').length;
|
|
const requests = lines.filter((line) => line !== 'refresh');
|
|
const captures = [];
|
|
for (let i = 0; i < requests.length; i += 4) {
|
|
captures.push({ auth: requests[i], method: requests[i + 1],
|
|
url: requests[i + 2], body: JSON.parse(requests[i + 3]) });
|
|
}
|
|
return { output: child.stdout, refreshes, captures };
|
|
} finally {
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
for (const [endpoint, expected] of Object.entries(endpoints)) {
|
|
for (const scenario of ['valid', 'expired', 'replaced', 'rejected', 'persistent', 'forbidden', 'unavailable']) {
|
|
test(`${endpoint} API handles ${scenario} token without unbounded retries`, () => {
|
|
const { output, refreshes, captures } = runCallback(endpoint, scenario);
|
|
const retries = ['rejected', 'persistent'].includes(scenario);
|
|
assert.equal(captures.length, retries ? 2 : 1);
|
|
assert.equal(refreshes, ['expired', 'rejected', 'persistent'].includes(scenario) ? 1 : 0);
|
|
for (const capture of captures) {
|
|
assert.deepEqual(capture.body, expected.body);
|
|
assert.equal(capture.method, expected.method);
|
|
assert.match(capture.url, expected.url);
|
|
}
|
|
if (['expired', 'replaced', 'rejected'].includes(scenario)) {
|
|
assert.equal(captures.at(-1).auth, 'Authorization: Bearer fresh-token');
|
|
}
|
|
const error = scenario === 'persistent' ? 'Token 已失效'
|
|
: scenario === 'forbidden' ? 'forbidden' : scenario === 'unavailable' ? 'unavailable' : '';
|
|
assert.equal(output.trim(), error ? expected.failure(error) : expected.success);
|
|
});
|
|
}
|
|
}
|