forked from wangziqi/gongxue-base
90 lines
3.5 KiB
JavaScript
90 lines
3.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
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));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function startApi(nodeEnv) {
|
|
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: nodeEnv,
|
|
PORT: String(port),
|
|
CORS_ORIGIN: '*',
|
|
CORS_TENANT_DOMAINS_ENABLED: 'false',
|
|
AUTH_CODE_PEPPER: 'runtime-test-auth-code-pepper-0123456789',
|
|
AUTH_SESSION_SECRET: 'runtime-test-auth-session-secret-0123456789',
|
|
AUTH_JWT_SECRET: 'runtime-test-auth-jwt-secret-0123456789',
|
|
PLATFORM_ADMIN_API_KEY: 'runtime-test-platform-key-0123456789',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let output = '';
|
|
child.stdout.on('data', chunk => { output += chunk.toString(); });
|
|
child.stderr.on('data', chunk => { output += chunk.toString(); });
|
|
const startedAt = Date.now();
|
|
while (!output.includes('"event":"server_listening"')) {
|
|
if (child.exitCode !== null) throw new Error(`API exited during startup: ${output}`);
|
|
if (Date.now() - startedAt > 10_000) throw new Error(`API startup timed out: ${output}`);
|
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
}
|
|
return { child, port, output: () => output };
|
|
}
|
|
|
|
async function stopApi(api) {
|
|
api.child.kill('SIGTERM');
|
|
await new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error(`API shutdown timed out: ${api.output()}`)), 8_000);
|
|
api.child.once('exit', code => {
|
|
clearTimeout(timer);
|
|
code === 0 ? resolve() : reject(new Error(`API shutdown failed code=${code}: ${api.output()}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
const development = await startApi('development');
|
|
try {
|
|
const docs = await fetch(`http://127.0.0.1:${development.port}/docs`);
|
|
assert.equal(docs.status, 200);
|
|
assert.match(await docs.text(), /scalar/i);
|
|
|
|
const openapiResponse = await fetch(`http://127.0.0.1:${development.port}/openapi.json`);
|
|
assert.equal(openapiResponse.status, 200);
|
|
const openapi = await openapiResponse.json();
|
|
assert.ok(openapi.paths['/api/auth/sms/send']?.post, 'native auth route must be documented');
|
|
assert.equal(openapi.paths['/api/catalog/regions']?.get?.['x-migration-status'], 'legacy');
|
|
|
|
const invalidNative = await fetch(`http://127.0.0.1:${development.port}/api/auth/sms/send`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'x-tenant-id': 'not-a-uuid' },
|
|
body: JSON.stringify({ phone: 123 }),
|
|
});
|
|
assert.equal(invalidNative.status, 400);
|
|
assert.equal((await invalidNative.json()).code, 'VALIDATION_ERROR');
|
|
|
|
const legacyPost = await fetch(`http://127.0.0.1:${development.port}/api/commerce/orders`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
});
|
|
assert.notEqual(legacyPost.status, 404, 'legacy POST route must remain registered');
|
|
const legacyBody = await legacyPost.json();
|
|
assert.ok(legacyBody.meta?.requestId, 'legacy response must include requestId metadata');
|
|
} finally {
|
|
await stopApi(development);
|
|
}
|
|
|
|
console.log('[PASS] NestJS API docs, DTO validation and legacy bridge runtime');
|