Files
qinglong/test/back/subscription-review-regression.test.cjs
whyour 15d54866a8 feat: migrate SQLite runtime and verify complete upgrade bundles
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.
2026-10-22 13:47:26 +08:00

636 lines
21 KiB
JavaScript

const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { execFileSync } = require('node:child_process');
const ts = require('typescript');
const load = require('../helpers/load-security-module.cjs');
const paths = load(path.resolve('back/shared/subscriptionPath.ts'));
const { formatUrl, formatCommand } = load(
path.resolve('back/config/subscription.ts'),
);
function temporary(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-sub-review-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return root;
}
function privateSubscription(alias, id = 1) {
return {
id,
type: 'private-repo',
pull_type: 'ssh-key',
alias,
url: 'git@example.com:owner/repo.git',
pull_option: { private_key: 'fixture-key' },
};
}
function sshService(root, warnings = []) {
const Ssh = load(path.resolve('back/services/sshKey.ts'), {
typedi: { Service: () => (x) => x, Inject: () => () => {} },
'../config': { sshdPath: root },
'../config/util': {
fileExist: async (p) => fs.existsSync(p),
rmPath: (p) => fs.promises.rm(p, { recursive: true, force: true }),
},
}).default;
return Object.assign(Object.create(Ssh.prototype), {
sshPath: root,
logger: { error() {}, warn: (...args) => warnings.push(args) },
});
}
test('aliases generated by the frontend for valid branches remain accepted', () => {
const text = fs.readFileSync(
path.resolve('src/pages/subscription/modal.tsx'),
'utf8',
);
const source = text.slice(
text.indexOf('const formatAlias ='),
text.indexOf('\n\n const IntervalSelect'),
);
const js = ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2020 },
}).outputText;
const formatAlias = new Function(
'repoUrlRegx',
'fileUrlRegx',
'type',
js + '\nreturn formatAlias;',
)(
/([^\/\:]+\/[^\/]+)(?=\.git)/,
/([^\/\:]+\/[^\/\.]+)\.[a-z]+/,
'public-repo',
);
for (const branch of [
'main',
'release+hotfix',
'feature/foo',
'feature/foo/bar',
'release@v2',
'release#v2',
]) {
const alias = formatAlias('https://example.com/owner/repo.git', branch);
assert.equal(alias, `owner_repo_${branch}`);
assert.doesNotThrow(() => paths.assertSubscriptionAlias(alias));
}
});
test('nested aliases stay inside the root and cannot claim internal SSH names', (t) => {
const root = temporary(t);
const scripts = path.join(root, 'scripts');
fs.mkdirSync(scripts);
assert.equal(
paths.resolveSubscriptionPath(scripts, 'owner_repo_feature/foo'),
path.join(scripts, 'owner_repo_feature/foo'),
);
fs.symlinkSync(root, path.join(scripts, 'escape'));
assert.throws(() => paths.resolveSubscriptionPath(scripts, 'escape/outside'));
for (const alias of [
'.',
'..',
'../outside',
'/outside',
'x/../outside',
'x/./a',
'x//a',
'x/',
'x\\a',
'x\0a',
'x\nHost *',
'x y',
'~global_secret',
'~sub_' + 'a'.repeat(64),
]) {
assert.throws(() => paths.assertSubscriptionAlias(alias), alias);
}
assert.throws(() => paths.assertSubscriptionAlias('a'.repeat(201)));
});
test('SSH URL, key and configuration use the same stable name for nested aliases', async (t) => {
const root = temporary(t);
const ssh = sshService(root);
const docs = [
privateSubscription('owner_repo_release+hotfix'),
privateSubscription('owner_repo_feature/foo', 2),
privateSubscription('owner_repo_feature_foo', 3),
privateSubscription('owner_repo.config', 4),
];
const snapshot = structuredClone(docs);
await ssh.setSshConfig(docs);
assert.deepEqual(docs, snapshot);
const mapped = paths.getSubscriptionSshAlias(docs[1].alias);
assert.match(mapped, /^~sub_[a-f0-9]{64}$/);
assert.notEqual(mapped, paths.getSubscriptionSshAlias(docs[2].alias));
assert.equal(paths.getSubscriptionSshAlias(docs[0].alias), docs[0].alias);
for (const doc of docs) {
const name = paths.getSubscriptionSshAlias(doc.alias);
const key = path.join(root, name);
const config = path.join(root, `${name}.config`);
assert.equal(fs.readFileSync(key, 'utf8').trim(), 'fixture-key');
assert.equal(fs.statSync(key).mode & 0o777, 0o400);
assert.equal(fs.statSync(config).mode & 0o777, 0o600);
assert.ok(fs.readFileSync(config, 'utf8').startsWith(`Host ${name}\n`));
assert.equal(formatUrl(doc).url, `git@${name}:owner/repo.git`);
const parsed = execFileSync('ssh', ['-G', '-F', config, name], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
assert.match(parsed, /^hostname example\.com$/m);
assert.ok(parsed.includes(`identityfile ${key}\n`));
}
await ssh.removeSSHKey(docs[1].alias, 'example.com');
assert.equal(fs.existsSync(path.join(root, mapped)), false);
assert.equal(fs.existsSync(path.join(root, `${mapped}.config`)), false);
await ssh.addSSHKey('replacement', docs[1].alias, 'example.com');
assert.equal(
fs.readFileSync(path.join(root, mapped), 'utf8').trim(),
'replacement',
);
});
test('startup retains historical aliases and isolates bad rows and asynchronous scheduling failures', async (t) => {
const root = temporary(t);
const warnings = [];
const ssh = sshService(root, warnings);
const docs = [
privateSubscription('owner_repo_release+hotfix', 1),
privateSubscription('owner_repo_feature/foo', 2),
privateSubscription('../outside', 3),
{
id: 4,
type: 'public-repo',
alias: 'failed-job',
url: 'https://example.com/repo.git',
},
{
id: 5,
type: 'public-repo',
alias: 'healthy-job',
url: 'https://example.com/repo.git',
},
];
const registered = [];
const types = Object.fromEntries(
['system', 'schedule', 'subscription', 'ssh'].map((key) => [key, class {}]),
);
const services = new Map([
[types.system, { getSystemConfig: async () => ({}) }],
[
types.schedule,
{ cancelIntervalTask: async () => {}, createIntervalTask: () => {} },
],
[types.ssh, ssh],
[
types.subscription,
{
setSshConfig: () => ssh.setSshConfig(docs),
list: async () => docs.map((doc) => ({ ...doc, ...doc })),
handleTask: async (doc) => {
if (doc.id === 4) throw new Error('unavailable scheduler');
registered.push(doc.id);
},
},
],
]);
const initTask = load(path.resolve('back/loaders/initTask.ts'), {
'../config': { rootPath: root },
'../config/util': { fileExist: async () => false },
'../shared/i18n': { t: (x) => x },
'./logger': { warn: (...args) => warnings.push(args) },
'../services/system': types.system,
'../services/schedule': types.schedule,
'../services/subscription': types.subscription,
'../services/sshKey': types.ssh,
typedi: { Container: { get: (key) => services.get(key) } },
}).default;
await initTask();
assert.deepEqual(registered, [1, 2, 5]);
assert.equal(warnings.length, 3);
assert.equal(
fs.existsSync(path.join(root, 'owner_repo_release+hotfix.config')),
true,
);
});
test('subscription create and update preserve branch aliases without rewriting records', async (t) => {
const root = temporary(t);
const Subscription = load(path.resolve('back/services/subscription.ts'), {
'../config': { logPath: root, scriptPath: root },
'../data/subscription': {
Subscription: class {
constructor(value) {
Object.assign(this, value);
}
},
SubscriptionModel: {},
},
'../data/cron': {},
'../config/util': {},
'../config/const': {},
'../shared/i18n': { t: (x) => x, tf: (x) => x },
'../shared/pLimit': {},
'./schedule': {},
'./sock': {},
'./sshKey': {},
'./cron': {},
}).default;
const service = new Subscription({}, {}, {}, {}, {});
const tasks = [];
service.insert = async (value) => ({ ...value, ...value });
service.getDb = async () => ({ id: 1, alias: 'owner_repo_release+hotfix' });
service.updateDb = async (value) => value;
service.handleTask = async (doc) => tasks.push(doc.alias);
service.setSshConfig = async () => {};
for (const alias of ['owner_repo_release+hotfix', 'owner_repo_feature/foo']) {
await service.create({ id: 1, alias });
await service.update({ id: 1, alias });
}
assert.deepEqual(tasks, [
'owner_repo_release+hotfix',
'owner_repo_release+hotfix',
'owner_repo_feature/foo',
'owner_repo_feature/foo',
]);
});
function shellFunction(name) {
const source = fs.readFileSync(path.resolve('shell/update.sh'), 'utf8');
const start = source.indexOf(`${name}() {`);
assert.ok(start >= 0);
return source.slice(start, source.indexOf('\n}', start) + 2);
}
function updateFixture(t) {
const root = temporary(t);
const template = path.join(root, 'template');
for (const dir of ['template', 'repo', 'scripts', 'lists'])
fs.mkdirSync(path.join(root, dir));
for (const file of ['keep.js', 'keep.py', 'omit.js', 'other.txt', 'dep.js'])
fs.writeFileSync(path.join(template, file), file);
for (const file of ['notify.js', 'notify.py', 'crontab'])
fs.writeFileSync(path.join(root, file), '');
const helpers = [
'update_repo',
'diff_scripts',
'gen_list_repo',
'get_uniq_path',
]
.map(shellFunction)
.join('\n');
return {
root,
run: (extensions, dependence = 'dep') => {
const doc = {
id: 1,
type: 'public-repo',
alias: 'owner_repo',
url: 'https://example.invalid/owner/repo.git',
whitelist: 'keep',
blacklist: 'omit',
dependences: dependence,
extensions,
autoAddCron: false,
autoDelCron: false,
};
const code = `dir_repo="$QA_ROOT/repo"; dir_scripts="$QA_ROOT/scripts"; dir_list_tmp="$QA_ROOT/lists"
file_notify_js="$QA_ROOT/notify.js"; file_notify_py="$QA_ROOT/notify.py"; list_crontab_user="$QA_ROOT/crontab"
cmd_task=task; file_extensions='js py'
${helpers}
t() { :; }; make_dir() { mkdir -p "$1"; }; diff_cron() { :; }
git_clone_scripts() { mkdir -p "$2"; cp "$QA_ROOT/template/"* "$2/"; exit_status=0; }
ql() { get_uniq_path "$2" "$6"; update_repo "${'${@:2}'}"; }
${formatCommand(doc)}`;
execFileSync('/bin/bash', ['-c', code], {
env: { ...process.env, QA_ROOT: root },
encoding: 'utf8',
timeout: 5000,
});
return path.join(root, 'scripts', 'owner_repo');
},
};
}
test('real repository update supports extension separators, filtering and dependency copies', (t) => {
for (const extension of ['js|py', 'js py', 'js\tpy', 'js\npy', '']) {
const f = updateFixture(t);
const scripts = f.run(extension);
assert.deepEqual(fs.readdirSync(scripts).sort(), [
'dep.js',
'keep.js',
'keep.py',
'notify.js',
'notify.py',
]);
assert.deepEqual(
fs
.readFileSync(
path.join(f.root, 'lists', 'owner_repo_scripts.list'),
'utf8',
)
.trim()
.split('\n')
.sort(),
['owner_repo/keep.js', 'owner_repo/keep.py'],
);
}
});
test('real repository update never evaluates extension input, including dependency discovery', (t) => {
const f = updateFixture(t);
const marker = path.join(f.root, 'executed');
const payloads = [
'js|$(touch${IFS}' + marker + ')',
'js|`touch${IFS}' + marker + '`',
'js|py";touch${IFS}' + marker + ';#',
'js|$(touch${IFS}' + marker + ')\npy',
];
for (const payload of payloads) {
const scripts = f.run(payload);
assert.equal(fs.existsSync(marker), false, payload);
assert.equal(
fs.readFileSync(path.join(scripts, 'keep.js'), 'utf8'),
'keep.js',
);
assert.equal(
fs.readFileSync(path.join(scripts, 'dep.js'), 'utf8'),
'dep.js',
);
}
});
test('force deletion supports historical nested aliases while preserving outside targets', async (t) => {
const root = temporary(t);
const config = {
scriptPath: path.join(root, 'scripts'),
repoPath: path.join(root, 'repo'),
};
const alias = 'owner_repo_feature/foo';
for (const dir of [config.scriptPath, config.repoPath]) {
fs.mkdirSync(path.join(dir, alias), { recursive: true });
fs.writeFileSync(path.join(dir, alias, 'file'), 'fixture');
}
const outside = path.join(root, 'outside');
fs.mkdirSync(outside);
fs.writeFileSync(path.join(outside, 'file'), 'keep');
let destroyed = 0;
const doc = { id: 1, alias };
const Subscription = load(path.resolve('back/services/subscription.ts'), {
'../config': config,
'../data/subscription': {
SubscriptionModel: {
find: async (where, read = {}) => {
return [doc];
},
nativeDelete: async (where, read = {}) => {
return destroyed++;
},
},
},
'../data/cron': {
CrontabModel: {
find: async (where, read = {}) => {
return [];
},
},
},
'../config/util': {
rmPath: (p) => fs.promises.rm(p, { recursive: true, force: true }),
},
'../config/const': {},
'../shared/i18n': { t: (x) => x, tf: (x) => x },
'../shared/pLimit': {},
'./schedule': {},
'./sock': {},
'./sshKey': {},
'./cron': {},
}).default;
const service = new Subscription({}, {}, {}, {}, {});
service.handleTask = async () => {};
service.setSshConfig = async () => {};
await service.remove([1], { force: true });
assert.equal(destroyed, 1);
for (const dir of [config.scriptPath, config.repoPath])
assert.equal(fs.existsSync(path.join(dir, alias)), false);
fs.symlinkSync(outside, path.join(config.scriptPath, 'escape'));
doc.alias = 'escape/file';
await assert.rejects(
service.remove([1], { force: true }),
/Invalid subscription path/,
);
assert.equal(destroyed, 1);
assert.equal(fs.readFileSync(path.join(outside, 'file'), 'utf8'), 'keep');
assert.equal(paths.getSubscriptionSshAlias('仓库-main'), '仓库-main');
});
function cancellationFixture(t, subscriptions) {
const root = temporary(t);
const events = [];
const docs = subscriptions.map((value) => {
const doc = { ...value };
doc.get = () => ({ ...doc });
return doc;
});
const Subscription = load(path.resolve('back/services/subscription.ts'), {
'../config': { scriptPath: root, repoPath: root },
'../data/subscription': {
SubscriptionModel: {
find: async (where, read = {}) => {
return docs.filter((doc) => !where.id || where.id.includes(doc.id));
},
nativeDelete: async (where, read = {}) => {
return events.push(['destroy', where.id]);
},
nativeUpdate: async (where, value) => {
for (const doc of docs) {
if (where.id.includes(doc.id)) Object.assign(doc, value);
}
},
},
},
'../data/cron': {},
'../config/util': { rmPath: async () => events.push(['removeFile']) },
'../config/const': {},
'../shared/i18n': { t: (x) => x, tf: (x) => x },
'../shared/pLimit': {},
'./schedule': {},
'./sock': {},
'./sshKey': {},
'./cron': {},
}).default;
const schedule = {
cancelCronTask: (doc) => events.push(['cancelCron', doc.id]),
cancelIntervalTask: (doc) => events.push(['cancelInterval', doc.id]),
createCronTask: async (doc, callbacks, immediately) =>
events.push(['createCron', doc.command, immediately]),
createIntervalTask: async (doc, interval, immediately) =>
events.push(['createInterval', doc.command, interval, immediately]),
};
const service = new Subscription(
{},
schedule,
{},
{ removeSSHKey: async () => {} },
{},
);
service.setSshConfig = async () => {};
return { service, docs, events };
}
async function sshLifecycleFixture(t, initial) {
const root = temporary(t);
const ssh = sshService(root);
let docs = [
{
...initial,
get() {
return { ...this };
},
},
];
const SubscriptionService = load(
path.resolve('back/services/subscription.ts'),
{
'../config': { scriptPath: root, repoPath: root },
'../data/subscription': {
Subscription: class {
constructor(value) {
Object.assign(this, value);
}
},
SubscriptionModel: {
find: async (where, read = {}) => {
return docs;
},
nativeDelete: async (where, read = {}) => {
docs = [];
},
},
},
'../data/cron': {
CrontabModel: {
find: async (where, read = {}) => {
return [];
},
},
},
'../config/util': {
rmPath: (p) => fs.promises.rm(p, { recursive: true, force: true }),
},
'../config/const': {},
'../shared/i18n': { t: (x) => x, tf: (x) => x },
'../shared/pLimit': {},
'./schedule': {},
'./sock': {},
'./sshKey': {},
'./cron': {},
},
).default;
const service = new SubscriptionService({}, {}, {}, ssh, {});
service.handleTask = async () => {};
service.getDb = async () => ({ ...docs[0] });
service.updateDb = async (value) => {
Object.assign(docs[0], value);
return { ...docs[0] };
};
await service.setSshConfig();
return { service, root };
}
test('editing an SSH subscription removes the old alias key and configuration', async (t) => {
const initial = privateSubscription('owner_repo_main');
const f = await sshLifecycleFixture(t, initial);
const alias = 'owner_repo_feature/中文';
await f.service.update({ ...initial, alias });
const mapped = paths.getSubscriptionSshAlias(alias);
assert.equal(fs.existsSync(path.join(f.root, initial.alias)), false);
assert.equal(
fs.existsSync(path.join(f.root, initial.alias + '.config')),
false,
);
assert.equal(
fs.readFileSync(path.join(f.root, mapped), 'utf8').trim(),
'fixture-key',
);
assert.ok(fs.existsSync(path.join(f.root, mapped + '.config')));
});
test('switching an SSH subscription to a public repository removes obsolete credentials', async (t) => {
const initial = privateSubscription('owner_repo.config');
const f = await sshLifecycleFixture(t, initial);
await f.service.update({ ...initial, type: 'public-repo' });
assert.deepEqual(fs.readdirSync(f.root), []);
});
test('ordinary and forced subscription deletion remove the current SSH key and configuration', async (t) => {
for (const force of [false, true]) {
const initial = privateSubscription('owner_repo_feature/中文');
const f = await sshLifecycleFixture(t, initial);
await f.service.remove([initial.id], { force });
assert.deepEqual(fs.readdirSync(f.root), []);
}
});
test('ordinary deletion cancels invalid historical SSH subscriptions by ID', async (t) => {
for (const schedule_type of ['crontab', 'interval']) {
const f = cancellationFixture(t, [
{
...privateSubscription('../outside'),
schedule_type,
interval_schedule: { type: 'hours', value: 1 },
},
]);
await f.service.remove([1], { force: false });
assert.deepEqual(f.events, [
[schedule_type === 'crontab' ? 'cancelCron' : 'cancelInterval', 1],
['destroy', [1]],
]);
}
});
test('invalid historical SSH rows do not block mixed batch deletion or disabling', async (t) => {
const docs = [
{ ...privateSubscription('healthy', 1), schedule_type: 'crontab' },
{ ...privateSubscription('../outside', 2), schedule_type: 'crontab' },
{
...privateSubscription('old alias', 3),
schedule_type: 'interval',
interval_schedule: { type: 'hours', value: 1 },
},
];
const cancellations = [
['cancelCron', 1],
['cancelCron', 2],
['cancelInterval', 3],
];
const removed = cancellationFixture(t, docs);
await removed.service.remove([1, 2, 3], {});
assert.deepEqual(removed.events, [...cancellations, ['destroy', [1, 2, 3]]]);
const disabled = cancellationFixture(t, docs);
await disabled.service.disabled([1, 2, 3]);
assert.deepEqual(disabled.events, cancellations);
assert.ok(disabled.docs.every((doc) => doc.is_disabled === 1));
});
test('cancellation bypass leaves alias validation for creation and forced deletion intact', async (t) => {
const doc = {
...privateSubscription('../outside'),
schedule_type: 'crontab',
};
const f = cancellationFixture(t, [doc]);
await assert.rejects(f.service.handleTask(doc), /Invalid subscription alias/);
await assert.rejects(f.service.create(doc), /Invalid subscription alias/);
await assert.rejects(f.service.enabled([1]), /Invalid subscription alias/);
await assert.rejects(
f.service.remove([1], { force: true }),
/Invalid subscription alias/,
);
assert.deepEqual(f.events, []);
});
test('valid subscription scheduling still creates quoted commands for both schedule types', async (t) => {
for (const schedule_type of ['crontab', 'interval']) {
const doc = {
...privateSubscription('owner_repo_feature/foo'),
schedule_type,
interval_schedule: { type: 'hours', value: 1 },
};
const f = cancellationFixture(t, [doc]);
await f.service.handleTask(doc, true, true);
const command = formatCommand(doc);
assert.deepEqual(
f.events,
schedule_type === 'crontab'
? [
['cancelCron', 1],
['createCron', command, true],
]
: [
['cancelInterval', 1],
['createInterval', command, { hours: 1 }, true],
],
);
}
});