mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
398 lines
11 KiB
JavaScript
398 lines
11 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
require('reflect-metadata');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const http = require('node:http');
|
|
const { after, before, test } = require('node:test');
|
|
const express = require('express');
|
|
const { errors } = require('celebrate');
|
|
const { Container } = require('typedi');
|
|
|
|
const CronService = require('../../back/services/cron').default;
|
|
const SubscriptionService = require('../../back/services/subscription').default;
|
|
const registerCronRoutes = require('../../back/api/cron').default;
|
|
const registerSubscriptionRoutes =
|
|
require('../../back/api/subscription').default;
|
|
|
|
const calls = [];
|
|
let origin;
|
|
let server;
|
|
let serverListening = false;
|
|
|
|
function record(domain, operation, args, result) {
|
|
calls.push({ domain, operation, args });
|
|
return Promise.resolve(result);
|
|
}
|
|
|
|
const cronService = {
|
|
crontabs: (...args) =>
|
|
record('cron', 'list', args, {
|
|
data: [{ id: 11, name: 'legacy-task', status: 0 }],
|
|
total: 1,
|
|
}),
|
|
create: (...args) =>
|
|
record('cron', 'create', args, {
|
|
id: 11,
|
|
...args[0],
|
|
status: 0,
|
|
}),
|
|
update: (...args) =>
|
|
record('cron', 'update', args, {
|
|
...args[0],
|
|
status: 0,
|
|
}),
|
|
disabled: (...args) => record('cron', 'disable', args, undefined),
|
|
enabled: (...args) => record('cron', 'enable', args, undefined),
|
|
run: (...args) => record('cron', 'run', args, undefined),
|
|
stop: (...args) => record('cron', 'stop', args, undefined),
|
|
log: (...args) =>
|
|
record('cron', 'log', args, {
|
|
content: 'legacy log\n',
|
|
status: 'running',
|
|
}),
|
|
logs: (...args) => record('cron', 'logs', args, ['one.log', 'two.log']),
|
|
stopInstance: (...args) =>
|
|
record('cron', 'stop-instance', args, {
|
|
code: 200,
|
|
message: '实例已停止',
|
|
}),
|
|
};
|
|
|
|
const subscriptionService = {
|
|
list: (...args) =>
|
|
record('subscription', 'list', args, [
|
|
{ id: 21, name: 'legacy-subscription', status: 0 },
|
|
]),
|
|
create: (...args) =>
|
|
record('subscription', 'create', args, {
|
|
id: 21,
|
|
...args[0],
|
|
status: 0,
|
|
}),
|
|
update: (...args) =>
|
|
record('subscription', 'update', args, {
|
|
...args[0],
|
|
status: 0,
|
|
}),
|
|
disabled: (...args) => record('subscription', 'disable', args, undefined),
|
|
enabled: (...args) => record('subscription', 'enable', args, undefined),
|
|
run: (...args) => record('subscription', 'run', args, undefined),
|
|
stop: (...args) => record('subscription', 'stop', args, undefined),
|
|
log: (...args) => record('subscription', 'log', args, 'subscription log\n'),
|
|
};
|
|
|
|
const logger = {
|
|
debug() {},
|
|
error() {},
|
|
info() {},
|
|
warn() {},
|
|
};
|
|
|
|
async function request(method, pathname, body) {
|
|
const response = await fetch(`${origin}${pathname}`, {
|
|
method,
|
|
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
const text = await response.text();
|
|
return {
|
|
status: response.status,
|
|
body: text.length === 0 ? undefined : JSON.parse(text),
|
|
};
|
|
}
|
|
|
|
function lastCall() {
|
|
return calls.at(-1);
|
|
}
|
|
|
|
before(async () => {
|
|
Container.set('logger', logger);
|
|
Container.set(CronService, cronService);
|
|
Container.set(SubscriptionService, subscriptionService);
|
|
|
|
const app = express();
|
|
app.set('case sensitive routing', true);
|
|
app.set('strict routing', true);
|
|
app.use(express.json());
|
|
|
|
const api = express.Router();
|
|
registerCronRoutes(api);
|
|
registerSubscriptionRoutes(api);
|
|
app.use('/api', api);
|
|
app.use(errors());
|
|
app.use((error, _request, response, _next) => {
|
|
response.status(error.status || 500).json({
|
|
code: error.status || 500,
|
|
message: error.message,
|
|
});
|
|
});
|
|
|
|
server = http.createServer(app);
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', resolve);
|
|
});
|
|
serverListening = true;
|
|
const address = server.address();
|
|
origin = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
after(async () => {
|
|
Container.remove(CronService);
|
|
Container.remove(SubscriptionService);
|
|
Container.remove('logger');
|
|
if (serverListening) {
|
|
await new Promise((resolve, reject) =>
|
|
server.close((error) => (error ? reject(error) : resolve())),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('preserves the 2.x Cron API envelope across the QingLong 3.0 runtime boundary', async (t) => {
|
|
await t.test(
|
|
'lists tasks with the existing query and response shape',
|
|
async () => {
|
|
const response = await request(
|
|
'GET',
|
|
'/api/crons?searchValue=legacy&page=1&size=20',
|
|
);
|
|
assert.deepEqual(response, {
|
|
status: 200,
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
data: [{ id: 11, name: 'legacy-task', status: 0 }],
|
|
total: 1,
|
|
},
|
|
},
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation: 'list',
|
|
args: [
|
|
{
|
|
searchValue: 'legacy',
|
|
page: '1',
|
|
size: '20',
|
|
},
|
|
],
|
|
});
|
|
},
|
|
);
|
|
|
|
const createBody = {
|
|
name: 'legacy-task',
|
|
command: 'echo compatible',
|
|
schedule: '0 0 * * *',
|
|
};
|
|
|
|
await t.test(
|
|
'creates and updates tasks without adding a v3-only envelope',
|
|
async () => {
|
|
const created = await request('POST', '/api/crons', createBody);
|
|
assert.equal(created.status, 200);
|
|
assert.deepEqual(created.body, {
|
|
code: 200,
|
|
data: { id: 11, ...createBody, status: 0 },
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation: 'create',
|
|
args: [createBody],
|
|
});
|
|
|
|
const updateBody = { id: 11, ...createBody, command: 'echo updated' };
|
|
const updated = await request('PUT', '/api/crons', updateBody);
|
|
assert.equal(updated.status, 200);
|
|
assert.deepEqual(updated.body, {
|
|
code: 200,
|
|
data: { ...updateBody, status: 0 },
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation: 'update',
|
|
args: [updateBody],
|
|
});
|
|
},
|
|
);
|
|
|
|
for (const operation of ['disable', 'enable', 'run', 'stop']) {
|
|
await t.test(
|
|
`${operation} keeps the code-only success response`,
|
|
async () => {
|
|
const response = await request('PUT', `/api/crons/${operation}`, [11]);
|
|
assert.deepEqual(response, { status: 200, body: { code: 200 } });
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation,
|
|
args: [[11]],
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
await t.test(
|
|
'keeps task log and instance-stop response contracts',
|
|
async () => {
|
|
const log = await request('GET', '/api/crons/11/log');
|
|
assert.deepEqual(log, {
|
|
status: 200,
|
|
body: {
|
|
code: 200,
|
|
data: 'legacy log\n',
|
|
logStatus: 'running',
|
|
},
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation: 'log',
|
|
args: [11],
|
|
});
|
|
|
|
const logs = await request('GET', '/api/crons/11/logs');
|
|
assert.deepEqual(logs, {
|
|
status: 200,
|
|
body: { code: 200, data: ['one.log', 'two.log'] },
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation: 'logs',
|
|
args: [11],
|
|
});
|
|
|
|
const stopped = await request('POST', '/api/crons/11/instances/101/stop');
|
|
assert.deepEqual(stopped, {
|
|
status: 200,
|
|
body: { code: 200, message: '实例已停止' },
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'cron',
|
|
operation: 'stop-instance',
|
|
args: [101],
|
|
});
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
'rejects an invalid execution request before service dispatch',
|
|
async () => {
|
|
const callCount = calls.length;
|
|
const response = await request('PUT', '/api/crons/run', ['x']);
|
|
assert.equal(response.status, 400);
|
|
assert.equal(calls.length, callCount);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('preserves the 2.x Subscription API envelope across the QingLong 3.0 runtime boundary', async (t) => {
|
|
await t.test('lists subscriptions with both legacy filters', async () => {
|
|
const response = await request(
|
|
'GET',
|
|
'/api/subscriptions?searchValue=legacy&ids=%5B21%5D',
|
|
);
|
|
assert.deepEqual(response, {
|
|
status: 200,
|
|
body: {
|
|
code: 200,
|
|
data: [{ id: 21, name: 'legacy-subscription', status: 0 }],
|
|
},
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'subscription',
|
|
operation: 'list',
|
|
args: ['legacy', '[21]'],
|
|
});
|
|
});
|
|
|
|
const createBody = {
|
|
type: 'public-repo',
|
|
url: 'https://example.invalid/repository.git',
|
|
schedule_type: 'cron',
|
|
alias: 'legacy-subscription',
|
|
};
|
|
|
|
await t.test(
|
|
'creates and updates subscriptions with the legacy envelope',
|
|
async () => {
|
|
const created = await request('POST', '/api/subscriptions', createBody);
|
|
assert.deepEqual(created, {
|
|
status: 200,
|
|
body: {
|
|
code: 200,
|
|
data: { id: 21, ...createBody, status: 0 },
|
|
},
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'subscription',
|
|
operation: 'create',
|
|
args: [createBody],
|
|
});
|
|
|
|
const updateBody = {
|
|
id: 21,
|
|
type: 'public-repo',
|
|
url: 'https://example.invalid/updated.git',
|
|
alias: 'legacy-subscription',
|
|
};
|
|
const updated = await request('PUT', '/api/subscriptions', updateBody);
|
|
assert.deepEqual(updated, {
|
|
status: 200,
|
|
body: {
|
|
code: 200,
|
|
data: { ...updateBody, status: 0 },
|
|
},
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'subscription',
|
|
operation: 'update',
|
|
args: [updateBody],
|
|
});
|
|
},
|
|
);
|
|
|
|
for (const operation of ['disable', 'enable', 'run', 'stop']) {
|
|
await t.test(
|
|
`${operation} keeps the code-only success response`,
|
|
async () => {
|
|
const response = await request(
|
|
'PUT',
|
|
`/api/subscriptions/${operation}`,
|
|
[21],
|
|
);
|
|
assert.deepEqual(response, { status: 200, body: { code: 200 } });
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'subscription',
|
|
operation,
|
|
args: [[21]],
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
await t.test('keeps the subscription log response contract', async () => {
|
|
const response = await request('GET', '/api/subscriptions/21/log');
|
|
assert.deepEqual(response, {
|
|
status: 200,
|
|
body: { code: 200, data: 'subscription log\n' },
|
|
});
|
|
assert.deepEqual(lastCall(), {
|
|
domain: 'subscription',
|
|
operation: 'log',
|
|
args: [21],
|
|
});
|
|
});
|
|
|
|
await t.test(
|
|
'rejects an invalid stop request before service dispatch',
|
|
async () => {
|
|
const callCount = calls.length;
|
|
const response = await request('PUT', '/api/subscriptions/stop', [
|
|
21,
|
|
'x',
|
|
]);
|
|
assert.equal(response.status, 400);
|
|
assert.equal(calls.length, callCount);
|
|
},
|
|
);
|
|
});
|