mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 15:56:59 +08:00
fix(cli): support cross-platform config and prepare 0.1.4 (#3119)
This commit is contained in:
@@ -296,11 +296,13 @@ test('malformed successful responses are rejected, including expired authenticat
|
||||
failure(await f.invoke(['login', '--url', f.url, '--json']), 1);
|
||||
});
|
||||
|
||||
test('config permissions, malformed content and symlinks fail closed', async (t) => {
|
||||
test('config content and symlinks are checked independently of permission bits', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.login();
|
||||
fs.chmodSync(f.file, 0o644);
|
||||
failure(await f.invoke(['task', 'list', '--json']), 1);
|
||||
const sharedMode = await f.invoke(['task', 'list', '--json']);
|
||||
assert.equal(sharedMode.code, 0, sharedMode.err);
|
||||
assert.equal(JSON.parse(sharedMode.out).data.total, 1);
|
||||
fs.chmodSync(f.file, 0o600);
|
||||
fs.writeFileSync(f.file, '{broken');
|
||||
failure(await f.invoke(['task', 'list', '--json']), 1);
|
||||
@@ -312,12 +314,73 @@ test('config permissions, malformed content and symlinks fail closed', async (t)
|
||||
const target = path.join(f.dir, 'target');
|
||||
fs.renameSync(f.file, target);
|
||||
fs.symlinkSync(target, f.file);
|
||||
failure(await f.invoke(['task', 'list', '--json']), 1);
|
||||
const linked = await f.invoke(['task', 'list', '--json'], { QL_LANG: 'en' });
|
||||
failure(linked, 1);
|
||||
assert.match(JSON.parse(linked.err).message, /regular file, not a symbolic link/);
|
||||
await f.login();
|
||||
assert.equal(fs.lstatSync(f.file).isSymbolicLink(), false);
|
||||
assert.equal(JSON.parse(fs.readFileSync(target)).clientId, 123);
|
||||
});
|
||||
|
||||
test('Windows accepts a saved config despite synthetic 0666 mode bits', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.login();
|
||||
const preload = path.join(f.dir, 'windows-mode.cjs');
|
||||
fs.writeFileSync(
|
||||
preload,
|
||||
`Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
const fs = require('node:fs');
|
||||
const originalFstatSync = fs.fstatSync;
|
||||
const originalOpenSync = fs.openSync;
|
||||
fs.openSync = function (file, flags, ...args) {
|
||||
return originalOpenSync(file, typeof flags === 'number'
|
||||
? flags & ~fs.constants.O_NOFOLLOW : flags, ...args);
|
||||
};
|
||||
fs.fstatSync = function (...args) {
|
||||
const stat = originalFstatSync(...args);
|
||||
stat.mode = (stat.mode & ~0o777) | 0o666;
|
||||
return stat;
|
||||
};\n`,
|
||||
);
|
||||
const env = { NODE_OPTIONS: `--require=${preload}` };
|
||||
const status = await f.invoke(['auth', 'status', '--json'], env);
|
||||
assert.equal(status.code, 0, status.err);
|
||||
assert.equal(JSON.parse(status.out).data.scopeChecked, 'crons');
|
||||
const list = await f.invoke(['task', 'list', '--json'], env);
|
||||
assert.equal(list.code, 0, list.err);
|
||||
assert.equal(JSON.parse(list.out).data.total, 1);
|
||||
const target = path.join(f.dir, 'target');
|
||||
fs.renameSync(f.file, target);
|
||||
fs.symlinkSync(target, f.file);
|
||||
failure(await f.invoke(['task', 'list', '--json'], env), 1);
|
||||
});
|
||||
|
||||
test('Linux ARM accepts a saved config with synthetic mode and owner', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.login();
|
||||
const preload = path.join(f.dir, 'arm-mode.cjs');
|
||||
fs.writeFileSync(
|
||||
preload,
|
||||
`Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
Object.defineProperty(process, 'arch', { value: 'arm' });
|
||||
const fs = require('node:fs');
|
||||
const originalFstatSync = fs.fstatSync;
|
||||
fs.fstatSync = function (...args) {
|
||||
const stat = originalFstatSync(...args);
|
||||
stat.mode = (stat.mode & ~0o777) | 0o666;
|
||||
stat.uid = 999999;
|
||||
return stat;
|
||||
};\n`,
|
||||
);
|
||||
const env = { NODE_OPTIONS: `--require=${preload}` };
|
||||
const status = await f.invoke(['auth', 'status', '--json'], env);
|
||||
assert.equal(status.code, 0, status.err);
|
||||
assert.equal(JSON.parse(status.out).data.scopeChecked, 'crons');
|
||||
const list = await f.invoke(['task', 'list', '--json'], env);
|
||||
assert.equal(list.code, 0, list.err);
|
||||
assert.equal(JSON.parse(list.out).data.total, 1);
|
||||
});
|
||||
|
||||
test('standalone compiled artifact and symlink run without repo runtime dependencies', async (t) => {
|
||||
const f = await fixture(t);
|
||||
const dist = path.join(f.dir, 'standalone');
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const http = require('node:http');
|
||||
const { spawn } = require('node:child_process');
|
||||
|
||||
const entry = path.resolve(__dirname, '../../dist/npm/ql.js');
|
||||
|
||||
test('saved login works on the host filesystem', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-platform-smoke-'));
|
||||
const config = path.join(dir, 'cli.json');
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, 'http://127.0.0.1');
|
||||
res.setHeader('content-type', 'application/json');
|
||||
if (url.pathname === '/panel/open/auth/token') {
|
||||
assert.equal(url.searchParams.get('client_id'), 'smoke-id');
|
||||
assert.equal(url.searchParams.get('client_secret'), 'smoke-secret');
|
||||
res.end(JSON.stringify({
|
||||
code: 200,
|
||||
data: {
|
||||
token: 'smoke-token',
|
||||
expiration: Math.floor(Date.now() / 1000) + 3600,
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
assert.equal(req.headers.authorization, 'Bearer smoke-token');
|
||||
assert.equal(url.pathname, '/panel/open/crons');
|
||||
res.end(JSON.stringify({ code: 200, data: { data: [], total: 0 } }));
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
t.after(async () => {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
QL_CLI_CONFIG: config,
|
||||
QL_CLIENT_ID: 'smoke-id',
|
||||
QL_CLIENT_SECRET: 'smoke-secret',
|
||||
QL_URL: '',
|
||||
QL_ACCESS_TOKEN: '',
|
||||
};
|
||||
const invoke = (args) => new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [entry, ...args], {
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let out = '';
|
||||
let err = '';
|
||||
child.stdout.on('data', (data) => (out += data));
|
||||
child.stderr.on('data', (data) => (err += data));
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => resolve({ code, out, err }));
|
||||
});
|
||||
const url = `http://127.0.0.1:${server.address().port}/panel`;
|
||||
const login = await invoke(['login', '--url', url, '--json']);
|
||||
assert.equal(login.code, 0, login.err);
|
||||
assert.equal(JSON.parse(login.out).data.authenticated, true);
|
||||
assert.equal(fs.lstatSync(config).isFile(), true);
|
||||
const status = await invoke(['auth', 'status', '--json']);
|
||||
assert.equal(status.code, 0, status.err);
|
||||
assert.equal(JSON.parse(status.out).data.authenticated, true);
|
||||
const list = await invoke(['task', 'list', '--json']);
|
||||
assert.equal(list.code, 0, list.err);
|
||||
assert.equal(JSON.parse(list.out).data.total, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user