// A small node:http stand-in for the anonymous menu API (/_menu/api/v1), // answering GET /menus/ from testing/fixtures/menus/.json and // 404 {"error":"Menu not found"} otherwise. It records every request so a // smoke run can assert header forwarding. // // Plain .mjs with JSDoc types: Node does not strip TypeScript under // node_modules, and consuming apps may import this file from there. import { createServer } from 'node:http' import { readFileSync } from 'node:fs' import { join } from 'node:path' const PREFIX = '/_menu/api/v1' /** * @typedef {{ method: string, url: string, xff: string | null }} FixtureRequest * @typedef {{ url: string, requests: FixtureRequest[], close: () => Promise }} FixtureApi */ /** * Starts the fixture API on 127.0.0.1. * @param {{ dir: string, port?: number }} opts * @returns {Promise} */ export function startFixtureApi({ dir, port = 0 }) { /** @type {FixtureRequest[]} */ const requests = [] /** @param {import('node:http').IncomingMessage} req */ const route = (req) => { const url = new URL(req.url ?? '/', 'http://fixture.local') if (req.method !== 'GET' && req.method !== 'HEAD') return { status: 405, body: { error: 'Method not allowed' } } const m = /^\/menus\/([A-Za-z0-9_-]+)$/.exec(url.pathname.slice(PREFIX.length)) if (!url.pathname.startsWith(PREFIX + '/') || !m) return { status: 404, body: { error: 'Menu not found' } } try { return { status: 200, body: JSON.parse(readFileSync(join(dir, 'menus', `${m[1]}.json`), 'utf8')) } } catch (err) { if (/** @type {any} */ (err).code === 'ENOENT') return { status: 404, body: { error: 'Menu not found' } } throw err } } const server = createServer((req, res) => { const xff = req.headers['x-forwarded-for'] requests.push({ method: req.method ?? 'GET', url: req.url ?? '/', xff: Array.isArray(xff) ? xff.join(', ') : (xff ?? null) }) let out try { out = route(req) } catch (err) { out = { status: 500, body: { error: String(err) } } } res.writeHead(out.status, { 'content-type': 'application/json' }) res.end(req.method === 'HEAD' ? undefined : JSON.stringify(out.body)) }) return new Promise((resolve, reject) => { server.once('error', reject) server.listen(port, '127.0.0.1', () => { const addr = /** @type {import('node:net').AddressInfo} */ (server.address()) resolve({ url: `http://127.0.0.1:${addr.port}`, requests, close: () => new Promise((done) => server.close(() => done())), }) }) }) }