mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
410 lines
12 KiB
JavaScript
410 lines
12 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { readFileSync } = require('node:fs');
|
|
const { request: httpsRequest } = require('node:https');
|
|
const { resolve } = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
ClusterPluginPackageIdentityKeysetUnavailableError,
|
|
} = require('@qinglong/cluster-admin/plugin-package-identity-keyset');
|
|
const {
|
|
ClusterPluginPackageManagementHttpConfigurationError,
|
|
startClusterPluginPackageManagementHttp,
|
|
} = require('@qinglong/cluster-admin/plugin-package-management-http');
|
|
const {
|
|
PluginPackageManagementQuotaExceededError,
|
|
} = require('@qinglong/runtime-core/plugin-package-management');
|
|
|
|
const SERVER_KEY = resolve(
|
|
__dirname,
|
|
'../../ql3-cluster-control/test/fixtures/mtls/server-key.pem',
|
|
);
|
|
const SERVER_CERT = resolve(
|
|
__dirname,
|
|
'../../ql3-cluster-control/test/fixtures/mtls/server-cert.pem',
|
|
);
|
|
|
|
function principal() {
|
|
return {
|
|
subject: { type: 'user', id: 'cluster-reviewer' },
|
|
authenticationId: 'ql3oidc.authentication-id',
|
|
authenticatedAtMs: 900,
|
|
expiresAtMs: 10_000,
|
|
assurance: 'multi_factor',
|
|
};
|
|
}
|
|
|
|
function command() {
|
|
return {
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.inspect',
|
|
request: {
|
|
actionRef: 'package:cluster-monitor:1',
|
|
approvalRequestId: 'approval-cluster-monitor-1',
|
|
inspectionId: 'inspection-cluster-monitor-1',
|
|
},
|
|
};
|
|
}
|
|
|
|
function identityProvider(overrides = {}) {
|
|
const calls = {
|
|
reload: 0,
|
|
bind: [],
|
|
authenticate: 0,
|
|
};
|
|
return {
|
|
calls,
|
|
provider: {
|
|
async reload() {
|
|
calls.reload += 1;
|
|
return {
|
|
schemaVersion: 1,
|
|
generation: 1,
|
|
digest: 'digest',
|
|
issuer: 'https://identity.example.test/',
|
|
audience: 'qinglong3-package-management',
|
|
activeKeyIds: ['key-1'],
|
|
revokedKeyIds: [],
|
|
};
|
|
},
|
|
bind(assertion) {
|
|
calls.bind.push(assertion);
|
|
return {
|
|
async authenticate() {
|
|
calls.authenticate += 1;
|
|
if (overrides.authenticate) {
|
|
return overrides.authenticate();
|
|
}
|
|
return principal();
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function transportFixture(overrides = {}) {
|
|
const calls = [];
|
|
return {
|
|
calls,
|
|
transport: {
|
|
async execute(value, authentication) {
|
|
calls.push({
|
|
command: value,
|
|
principal: await authentication.authenticate(),
|
|
});
|
|
if (overrides.execute) return overrides.execute(value);
|
|
return {
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.inspect',
|
|
proposal: null,
|
|
approval: null,
|
|
};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function startFixture(overrides = {}) {
|
|
const identities = overrides.identities ?? identityProvider();
|
|
const transport = overrides.transport ?? transportFixture();
|
|
const privateKey = Buffer.from(readFileSync(SERVER_KEY));
|
|
const application = await startClusterPluginPackageManagementHttp({
|
|
host: '127.0.0.1',
|
|
port: 0,
|
|
tls: {
|
|
privateKey,
|
|
certificate: Buffer.from(readFileSync(SERVER_CERT)),
|
|
},
|
|
identities: identities.provider,
|
|
transport: transport.transport,
|
|
limits: {
|
|
requestTimeoutMs: 2_000,
|
|
drainTimeoutMs: 500,
|
|
...(overrides.limits ?? {}),
|
|
},
|
|
now: overrides.now ?? (() => 1_000),
|
|
createRequestId: overrides.createRequestId ?? (() => 'request-1'),
|
|
onError: overrides.onError,
|
|
});
|
|
assert.equal(
|
|
privateKey.every((value) => value === 0),
|
|
true,
|
|
);
|
|
return { application, identities, transport };
|
|
}
|
|
|
|
async function request(application, options = {}) {
|
|
const body =
|
|
options.body === undefined
|
|
? Buffer.from(JSON.stringify(command()))
|
|
: Buffer.isBuffer(options.body)
|
|
? options.body
|
|
: Buffer.from(options.body);
|
|
return new Promise((resolvePromise, reject) => {
|
|
const outgoing = httpsRequest(
|
|
{
|
|
host: '127.0.0.1',
|
|
port: application.address.port,
|
|
path: options.path ?? '/api/v3/plugin-packages/management',
|
|
method: options.method ?? 'POST',
|
|
rejectUnauthorized: false,
|
|
agent: false,
|
|
headers: {
|
|
...(options.authorization === false
|
|
? {}
|
|
: { authorization: 'Bearer assertion-value' }),
|
|
...(options.contentType === false
|
|
? {}
|
|
: { 'content-type': 'application/json' }),
|
|
...(options.omitLength
|
|
? {}
|
|
: { 'content-length': String(body.length) }),
|
|
...(options.headers ?? {}),
|
|
},
|
|
},
|
|
(incoming) => {
|
|
const chunks = [];
|
|
incoming.on('data', (chunk) => chunks.push(chunk));
|
|
incoming.once('end', () => {
|
|
const bytes = Buffer.concat(chunks);
|
|
resolvePromise({
|
|
statusCode: incoming.statusCode,
|
|
headers: incoming.headers,
|
|
body:
|
|
bytes.length === 0 ? null : JSON.parse(bytes.toString('utf8')),
|
|
});
|
|
});
|
|
},
|
|
);
|
|
outgoing.once('error', reject);
|
|
if (body.length > 0) outgoing.write(body);
|
|
outgoing.end();
|
|
});
|
|
}
|
|
|
|
test('rejects request and connection ceilings above hard bounds', async () => {
|
|
for (const limits of [
|
|
{ maxBodyBytes: 256 * 1024 + 1 },
|
|
{ maxConnections: 513 },
|
|
]) {
|
|
const privateKey = Buffer.from(readFileSync(SERVER_KEY));
|
|
try {
|
|
await assert.rejects(
|
|
startClusterPluginPackageManagementHttp({
|
|
host: '127.0.0.1',
|
|
port: 0,
|
|
tls: {
|
|
privateKey,
|
|
certificate: Buffer.from(readFileSync(SERVER_CERT)),
|
|
},
|
|
identities: identityProvider().provider,
|
|
transport: transportFixture().transport,
|
|
limits,
|
|
}),
|
|
ClusterPluginPackageManagementHttpConfigurationError,
|
|
);
|
|
} finally {
|
|
privateKey.fill(0);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('serves health and authenticates before one exact management command', async () => {
|
|
const fixture = await startFixture();
|
|
try {
|
|
const live = await request(fixture.application, {
|
|
method: 'GET',
|
|
path: '/livez',
|
|
body: Buffer.alloc(0),
|
|
authorization: false,
|
|
contentType: false,
|
|
});
|
|
assert.equal(live.statusCode, 200);
|
|
assert.equal(live.headers['x-request-id'], 'request-1');
|
|
assert.equal(live.headers['cache-control'], 'no-store');
|
|
assert.deepEqual(live.body, { schemaVersion: 1, status: 'live' });
|
|
const response = await request(fixture.application);
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.result.operation, 'plugin-package.inspect');
|
|
assert.deepEqual(fixture.identities.calls.bind, ['assertion-value']);
|
|
assert.equal(fixture.identities.calls.authenticate, 1);
|
|
assert.equal(fixture.transport.calls.length, 1);
|
|
assert.deepEqual(fixture.transport.calls[0].command, command());
|
|
assert.deepEqual(fixture.transport.calls[0].principal.subject, {
|
|
type: 'user',
|
|
id: 'cluster-reviewer',
|
|
});
|
|
} finally {
|
|
await fixture.application.close();
|
|
}
|
|
});
|
|
|
|
test('maps durable quota exhaustion to 429 with a bounded Retry-After', async () => {
|
|
const fixture = await startFixture({
|
|
transport: transportFixture({
|
|
async execute() {
|
|
throw new PluginPackageManagementQuotaExceededError(1_250);
|
|
},
|
|
}),
|
|
});
|
|
try {
|
|
const response = await request(fixture.application);
|
|
assert.equal(response.statusCode, 429);
|
|
assert.equal(response.headers['retry-after'], '2');
|
|
assert.equal(response.body.error.code, 'quota_exceeded');
|
|
} finally {
|
|
await fixture.application.close();
|
|
}
|
|
});
|
|
|
|
test('rejects routes, missing authentication and media type before transport', async () => {
|
|
const fixture = await startFixture();
|
|
try {
|
|
assert.equal(
|
|
(
|
|
await request(fixture.application, {
|
|
path: '/api/v3/plugin-packages/unknown',
|
|
})
|
|
).statusCode,
|
|
404,
|
|
);
|
|
assert.equal(
|
|
(await request(fixture.application, { authorization: false })).statusCode,
|
|
401,
|
|
);
|
|
assert.equal(
|
|
(await request(fixture.application, { contentType: false })).statusCode,
|
|
415,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(fixture.application, {
|
|
headers: { expect: '100-continue' },
|
|
})
|
|
).statusCode,
|
|
417,
|
|
);
|
|
assert.equal(fixture.transport.calls.length, 0);
|
|
assert.equal(fixture.identities.calls.authenticate, 1);
|
|
} finally {
|
|
await fixture.application.close();
|
|
}
|
|
});
|
|
|
|
test('fails closed when the live keyset is unavailable', async () => {
|
|
const identities = identityProvider({
|
|
authenticate() {
|
|
throw new ClusterPluginPackageIdentityKeysetUnavailableError();
|
|
},
|
|
});
|
|
const fixture = await startFixture({ identities });
|
|
try {
|
|
const response = await request(fixture.application);
|
|
assert.equal(response.statusCode, 503);
|
|
assert.equal(response.body.error.code, 'unavailable');
|
|
assert.equal(fixture.transport.calls.length, 0);
|
|
} finally {
|
|
await fixture.application.close();
|
|
}
|
|
});
|
|
|
|
test('enforces peer limits before authentication with bounded retry evidence', async () => {
|
|
const fixture = await startFixture({
|
|
limits: {
|
|
rateWindowMs: 60_000,
|
|
peerRequestLimit: 1,
|
|
globalRequestLimit: 10,
|
|
maxRateLimitPeers: 2,
|
|
},
|
|
});
|
|
try {
|
|
assert.equal((await request(fixture.application)).statusCode, 200);
|
|
const limited = await request(fixture.application);
|
|
assert.equal(limited.statusCode, 429);
|
|
assert.equal(limited.body.error.code, 'rate_limited');
|
|
assert.equal(limited.headers['retry-after'], '60');
|
|
assert.equal(fixture.identities.calls.authenticate, 1);
|
|
assert.equal(fixture.transport.calls.length, 1);
|
|
} finally {
|
|
await fixture.application.close();
|
|
}
|
|
});
|
|
|
|
test('bounds request bodies and concurrent management work', async () => {
|
|
let release;
|
|
const gate = new Promise((resolvePromise) => {
|
|
release = resolvePromise;
|
|
});
|
|
const fixture = await startFixture({
|
|
limits: {
|
|
maxBodyBytes: 1_024,
|
|
maxConcurrentRequests: 1,
|
|
},
|
|
transport: transportFixture({
|
|
async execute() {
|
|
await gate;
|
|
return {
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.inspect',
|
|
proposal: null,
|
|
approval: null,
|
|
};
|
|
},
|
|
}),
|
|
});
|
|
try {
|
|
assert.equal(
|
|
(
|
|
await request(fixture.application, {
|
|
body: Buffer.alloc(1_025, 0x20),
|
|
})
|
|
).statusCode,
|
|
413,
|
|
);
|
|
const first = request(fixture.application);
|
|
await new Promise((resolvePromise) => setImmediate(resolvePromise));
|
|
const overloaded = await request(fixture.application);
|
|
assert.equal(overloaded.statusCode, 503);
|
|
assert.equal(overloaded.body.error.code, 'overloaded');
|
|
release();
|
|
assert.equal((await first).statusCode, 200);
|
|
} finally {
|
|
release();
|
|
await fixture.application.close();
|
|
}
|
|
});
|
|
|
|
test('withdraws readiness without killing liveness and closes idempotently', async () => {
|
|
const fixture = await startFixture();
|
|
fixture.application.withdraw(new Error('database unavailable'));
|
|
assert.equal(fixture.application.availabilityStatus(), 'unavailable');
|
|
assert.equal(
|
|
(
|
|
await request(fixture.application, {
|
|
method: 'GET',
|
|
path: '/readyz',
|
|
body: Buffer.alloc(0),
|
|
authorization: false,
|
|
contentType: false,
|
|
})
|
|
).statusCode,
|
|
503,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(fixture.application, {
|
|
method: 'GET',
|
|
path: '/livez',
|
|
body: Buffer.alloc(0),
|
|
authorization: false,
|
|
contentType: false,
|
|
})
|
|
).statusCode,
|
|
200,
|
|
);
|
|
assert.equal((await request(fixture.application)).statusCode, 503);
|
|
await Promise.all([fixture.application.close(), fixture.application.close()]);
|
|
assert.equal(fixture.application.availabilityStatus(), 'stopped');
|
|
});
|