mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:03:29 +08:00
Synchronize scheduler concurrency across API and gRPC processes and restore persisted limits during recovery. Serialize subscription mutations and preserve shared storage while cleaning Shell and CLI namespaces, historical references, and filesystem-specific naming candidates. Handle CLI text responses correctly and include script history in backups. Add regression coverage for concurrency, cleanup ownership, long URLs, Unicode paths, and history exports.
608 lines
21 KiB
JavaScript
608 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, 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, 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, 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 || 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],
|
|
],
|
|
);
|
|
}
|
|
});
|