mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
185 lines
5.4 KiB
JavaScript
185 lines
5.4 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
CLUSTER_CONTROL_ROUTE_REGISTRY_LIMITS,
|
|
ClusterControlRouteRegistryConfigurationError,
|
|
ClusterControlRouteResolutionError,
|
|
createClusterControlRouteRegistry,
|
|
isClusterControlRouteRegistry,
|
|
} = require('@qinglong/cluster-control/routes');
|
|
|
|
function metadata(overrides = {}) {
|
|
return Object.freeze({
|
|
requestId: 'request-1',
|
|
method: 'POST',
|
|
path: '/api/v3/projects/prj_default/runs',
|
|
query: Object.freeze({}),
|
|
headers: Object.freeze({}),
|
|
signal: new AbortController().signal,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function definition(overrides = {}) {
|
|
return {
|
|
method: 'POST',
|
|
path: '/api/v3/projects/{projectId}/runs',
|
|
operationId: 'run.create',
|
|
permission: 'run.start',
|
|
projectParameter: 'projectId',
|
|
handle(request, parameters) {
|
|
return {
|
|
statusCode: 202,
|
|
body: {
|
|
projectId: request.projectId,
|
|
parameter: parameters.projectId,
|
|
},
|
|
};
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('compiles one immutable reviewed route and owns its Project scope', async () => {
|
|
const source = definition({ allowedQuery: ['dry_run'] });
|
|
const registry = createClusterControlRouteRegistry([source]);
|
|
source.operationId = 'forged.operation';
|
|
|
|
assert.equal(isClusterControlRouteRegistry(registry), true);
|
|
assert.equal(isClusterControlRouteRegistry({ ...registry }), false);
|
|
assert.equal(Object.isFrozen(registry), true);
|
|
assert.equal(registry.contractVersion, 1);
|
|
assert.equal(registry.size, 1);
|
|
|
|
const route = registry.resolve(
|
|
metadata({ query: Object.freeze({ dry_run: Object.freeze(['true']) }) }),
|
|
);
|
|
assert.equal(route.operationId, 'run.create');
|
|
assert.equal(route.permission, 'run.start');
|
|
assert.equal(route.projectId, 'prj_default');
|
|
assert.equal(Object.isFrozen(route), true);
|
|
assert.deepEqual(
|
|
await route.handle({
|
|
request: { ...metadata(), body: { taskId: 'task-1' } },
|
|
principal: {
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
authenticationId: 'session:1',
|
|
authenticatedAtMs: 1,
|
|
expiresAtMs: 2,
|
|
assurance: 'multi_factor',
|
|
},
|
|
operationId: route.operationId,
|
|
permission: route.permission,
|
|
projectId: route.projectId,
|
|
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
|
}),
|
|
{
|
|
statusCode: 202,
|
|
body: { projectId: 'prj_default', parameter: 'prj_default' },
|
|
},
|
|
);
|
|
});
|
|
|
|
test('rejects widened, ambiguous and unbounded route definitions at startup', () => {
|
|
const invalidSets = [
|
|
[definition({ extra: true })],
|
|
[definition({ path: '/api/v3/projects/{projectId}/' })],
|
|
[definition({ path: '/api/v3/projects/%7BprojectId%7D/runs' })],
|
|
[definition({ projectParameter: 'missing' })],
|
|
[definition(), definition({ operationId: 'run.create' })],
|
|
[
|
|
definition(),
|
|
definition({
|
|
path: '/api/v3/projects/fixed/runs',
|
|
operationId: 'run.create.fixed',
|
|
}),
|
|
],
|
|
];
|
|
for (const definitions of invalidSets) {
|
|
assert.throws(
|
|
() => createClusterControlRouteRegistry(definitions),
|
|
ClusterControlRouteRegistryConfigurationError,
|
|
);
|
|
}
|
|
|
|
const tooMany = Array.from(
|
|
{ length: CLUSTER_CONTROL_ROUTE_REGISTRY_LIMITS.maxRoutes + 1 },
|
|
(_, index) =>
|
|
definition({
|
|
path: `/api/v3/routes/route-${index}`,
|
|
operationId: `route.operation:${index}`,
|
|
projectParameter: null,
|
|
}),
|
|
);
|
|
assert.throws(
|
|
() => createClusterControlRouteRegistry(tooMany),
|
|
ClusterControlRouteRegistryConfigurationError,
|
|
);
|
|
});
|
|
|
|
test('rejects non-canonical paths and unknown query before returning a route', () => {
|
|
const registry = createClusterControlRouteRegistry([
|
|
definition({ allowedQuery: ['dry_run'] }),
|
|
]);
|
|
for (const path of [
|
|
'/api/v3/projects/prj_default/runs/',
|
|
'/api/v3/projects//runs',
|
|
'/api/v3/projects/%2e%2e/runs',
|
|
'/api/v3/projects/prj_default\\runs',
|
|
]) {
|
|
assert.throws(
|
|
() => registry.resolve(metadata({ path })),
|
|
(error) =>
|
|
error instanceof ClusterControlRouteResolutionError &&
|
|
error.statusCode === 400 &&
|
|
error.code === 'invalid_route_path',
|
|
);
|
|
}
|
|
assert.throws(
|
|
() =>
|
|
registry.resolve(
|
|
metadata({ query: Object.freeze({ debug: Object.freeze(['1']) }) }),
|
|
),
|
|
(error) =>
|
|
error instanceof ClusterControlRouteResolutionError &&
|
|
error.code === 'invalid_route_query',
|
|
);
|
|
assert.equal(
|
|
registry.resolve(metadata({ method: 'GET' })),
|
|
null,
|
|
'method is part of the reviewed route identity',
|
|
);
|
|
assert.equal(
|
|
registry.resolve(metadata({ path: '/api/v3/projects/prj_default/tasks' })),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('bounds repeated query values and rejects control characters', () => {
|
|
const registry = createClusterControlRouteRegistry([
|
|
definition({ allowedQuery: ['cursor'] }),
|
|
]);
|
|
for (const values of [
|
|
[],
|
|
Array.from(
|
|
{
|
|
length:
|
|
CLUSTER_CONTROL_ROUTE_REGISTRY_LIMITS.maxQueryValuesPerParameter + 1,
|
|
},
|
|
() => 'value',
|
|
),
|
|
['unsafe\u0000value'],
|
|
['x'.repeat(CLUSTER_CONTROL_ROUTE_REGISTRY_LIMITS.maxQueryValueBytes + 1)],
|
|
]) {
|
|
assert.throws(
|
|
() =>
|
|
registry.resolve(
|
|
metadata({ query: Object.freeze({ cursor: Object.freeze(values) }) }),
|
|
),
|
|
(error) =>
|
|
error instanceof ClusterControlRouteResolutionError &&
|
|
error.code === 'invalid_route_query',
|
|
);
|
|
}
|
|
});
|