forked from wangziqi/gongxue-base
142 lines
5.6 KiB
JavaScript
142 lines
5.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import http from 'node:http';
|
|
import net from 'node:net';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
function freePort() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
server.close(error => error ? reject(error) : resolve(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function waitFor(predicate, timeoutMs = 10_000) {
|
|
return new Promise((resolve, reject) => {
|
|
const startedAt = Date.now();
|
|
const timer = setInterval(() => {
|
|
const value = predicate();
|
|
if (value) {
|
|
clearInterval(timer);
|
|
resolve(value);
|
|
} else if (Date.now() - startedAt > timeoutMs) {
|
|
clearInterval(timer);
|
|
reject(new Error('timed out waiting for API server operation'));
|
|
}
|
|
}, 25);
|
|
});
|
|
}
|
|
|
|
function rawRequest(port, { method = 'GET', path = '/', headers = {} } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = http.request({ hostname: '127.0.0.1', port, method, path, headers }, response => {
|
|
let body = '';
|
|
response.setEncoding('utf8');
|
|
response.on('data', chunk => { body += chunk; });
|
|
response.on('end', () => resolve({ statusCode: response.statusCode, headers: response.headers, body }));
|
|
});
|
|
request.once('error', reject);
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
const port = await freePort();
|
|
const child = spawn(process.execPath, ['apps/api/dist/apps/api/src/server.js'], {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
NODE_ENV: 'development',
|
|
PORT: String(port),
|
|
API_SHUTDOWN_GRACE_PERIOD_MS: '2000',
|
|
CORS_ORIGIN: 'https://platform.example.test',
|
|
CORS_TENANT_DOMAINS_ENABLED: 'false',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let output = '';
|
|
child.stdout.on('data', chunk => { output += chunk.toString(); });
|
|
child.stderr.on('data', chunk => { output += chunk.toString(); });
|
|
|
|
try {
|
|
await waitFor(() => output.includes('"event":"server_listening"'));
|
|
const providedRequestId = 'operations-contract-123';
|
|
const response = await fetch(`http://127.0.0.1:${port}/not-found?secret=query-value`, {
|
|
headers: { 'x-request-id': providedRequestId },
|
|
});
|
|
const body = await response.json();
|
|
assert.equal(response.status, 404);
|
|
assert.equal(response.headers.get('x-request-id'), providedRequestId);
|
|
assert.equal(body.requestId, providedRequestId);
|
|
assert.equal(body.meta.requestId, providedRequestId);
|
|
await waitFor(() => output.includes('"event":"http_request"'));
|
|
assert.match(output, /"requestId":"operations-contract-123"/);
|
|
assert.match(output, /"path":"\/not-found"/);
|
|
assert.ok(!output.includes('query-value'), 'structured access logs must not record query strings');
|
|
|
|
const allowedPreflight = await rawRequest(port, {
|
|
method: 'OPTIONS',
|
|
path: '/api/platform-admin/tenants',
|
|
headers: { origin: 'https://platform.example.test', 'access-control-request-method': 'GET' },
|
|
});
|
|
assert.equal(allowedPreflight.statusCode, 204);
|
|
assert.equal(allowedPreflight.headers['access-control-allow-origin'], 'https://platform.example.test');
|
|
assert.match(String(allowedPreflight.headers.vary || ''), /origin/i);
|
|
|
|
const deniedPreflight = await rawRequest(port, {
|
|
method: 'OPTIONS',
|
|
path: '/health',
|
|
headers: {
|
|
origin: 'https://unknown.example.test',
|
|
host: 'platform.example.test',
|
|
'x-forwarded-host': 'platform.example.test',
|
|
'x-tenant-code': 'master',
|
|
'access-control-request-method': 'GET',
|
|
},
|
|
});
|
|
assert.equal(deniedPreflight.statusCode, 403, 'unknown Origin preflight must be explicitly rejected');
|
|
assert.equal(deniedPreflight.headers['access-control-allow-origin'], undefined);
|
|
assert.equal(JSON.parse(deniedPreflight.body).code, 'CORS_ORIGIN_DENIED');
|
|
|
|
const deniedRequest = await rawRequest(port, {
|
|
path: '/not-found',
|
|
headers: {
|
|
origin: 'https://unknown.example.test',
|
|
host: 'platform.example.test',
|
|
'x-forwarded-host': 'platform.example.test',
|
|
},
|
|
});
|
|
assert.equal(deniedRequest.statusCode, 403, 'spoofed Host headers must not bypass Origin validation');
|
|
assert.equal(JSON.parse(deniedRequest.body).code, 'CORS_ORIGIN_DENIED');
|
|
|
|
const duplicateOriginRequest = await rawRequest(port, {
|
|
path: '/not-found',
|
|
headers: { origin: ['https://platform.example.test', 'https://unknown.example.test'] },
|
|
});
|
|
assert.equal(duplicateOriginRequest.statusCode, 403, 'duplicate Origin headers must be rejected');
|
|
assert.equal(JSON.parse(duplicateOriginRequest.body).code, 'CORS_ORIGIN_DENIED');
|
|
|
|
const originlessHealthPreflight = await rawRequest(port, { method: 'OPTIONS', path: '/health' });
|
|
assert.equal(originlessHealthPreflight.statusCode, 204, 'originless health checks must not be blocked by CORS');
|
|
assert.equal(originlessHealthPreflight.headers['access-control-allow-origin'], undefined);
|
|
|
|
child.kill('SIGTERM');
|
|
const exit = await new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error('API server did not exit after SIGTERM')), 7_000);
|
|
child.once('exit', (code, signal) => {
|
|
clearTimeout(timer);
|
|
resolve({ code, signal });
|
|
});
|
|
});
|
|
assert.equal(exit.code, 0, `API server should gracefully exit: ${output}`);
|
|
assert.match(output, /"event":"shutdown_started"/);
|
|
assert.match(output, /"event":"shutdown_complete"/);
|
|
} finally {
|
|
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
}
|
|
|
|
console.log('[PASS] API operations, fail-closed CORS and graceful shutdown contract');
|