mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
test(ql3): lock legacy core api contracts
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user