forked from wangziqi/gongxue-base
feat: establish production SaaS foundation
This commit is contained in:
141
scripts/api-server-operations-contract-test.js
Normal file
141
scripts/api-server-operations-contract-test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
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');
|
||||
Reference in New Issue
Block a user