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, get: () => 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 }, '../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, get: () => 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, get: () => doc }; const Subscription = load(path.resolve('back/services/subscription.ts'), { '../config': config, '../data/subscription': { SubscriptionModel: { findAll: async () => [doc], destroy: async () => destroyed++, }, }, '../data/cron': { CrontabModel: { findAll: async () => [] } }, '../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: { findAll: async ({ where }) => docs.filter((doc) => where.id.includes(doc.id)), destroy: async ({ where }) => events.push(['destroy', where.id]), update: async (value, { where }) => { 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: { findAll: async () => docs, destroy: async () => { docs = []; }, }, }, '../data/cron': { CrontabModel: { findAll: async () => [] } }, '../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], ], ); } });