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');