import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { z } from 'zod'; type OverrideResponse = { status: number; body?: unknown; headers?: Record }; type RecordedRequest = { method: string; path: string; query: Record; body: unknown; headers: Record; }; const overrides = new Map(); const requestLog: RecordedRequest[] = []; const overrideKey = (method: string, path: string) => `${method.toUpperCase()} ${path}`; const overrideSchema = z.object({ method: z.enum(['GET', 'POST', 'PATCH', 'PUT', 'DELETE']), pathPattern: z.string().min(1), response: z.object({ status: z.number().int().min(100).max(599), body: z.unknown().optional(), headers: z.record(z.string()).optional() }) }); const app = new Hono(); app.onError((err, c) => { console.error(`[mock] handler error: ${err.message}`); return c.json({ status: 500, msg: `mock: handler error: ${err.message}` }, 500); }); app.get('/__test/health', (c) => c.json({ ok: true })); app.post('/__test/override', async (c) => { const parsed = overrideSchema.parse(await c.req.json()); overrides.set(overrideKey(parsed.method, parsed.pathPattern), parsed.response); console.error( `[mock] override registered: ${parsed.method} ${parsed.pathPattern} → ${parsed.response.status}` ); return c.json({ ok: true }); }); app.delete('/__test/override', (c) => { overrides.clear(); requestLog.length = 0; console.error(`[mock] overrides + request log cleared`); return c.json({ ok: true }); }); // Returns recorded requests newest-first, optionally filtered by method/path. app.get('/__test/requests', (c) => { const method = c.req.query('method')?.toUpperCase(); const path = c.req.query('path'); const filtered = requestLog .filter((r) => (method ? r.method === method : true)) .filter((r) => (path ? r.path === path : true)); // Newest first — log is appended in arrival order, so reverse for output. return c.json([...filtered].reverse()); }); // Catch-all: record then serve override or 500 unmatched. app.all('*', async (c) => { const method = c.req.method; const url = new URL(c.req.url); const path = url.pathname; // Don't log /__test/* control traffic (would noise up assertions). if (!path.startsWith('/__test/')) { const query: Record = {}; url.searchParams.forEach((v, k) => (query[k] = v)); const headers: Record = {}; c.req.raw.headers.forEach((v, k) => (headers[k] = v)); let body: unknown = undefined; if (method !== 'GET' && method !== 'HEAD') { const text = await c.req.text(); if (text) { try { body = JSON.parse(text); } catch { body = text; } } } requestLog.push({ method, path, query, body, headers }); } const override = overrides.get(overrideKey(method, path)); if (override) { console.error(`[mock] ${method} ${path} → ${override.status}`); const headers = override.headers ?? {}; return new Response(override.body === undefined ? null : JSON.stringify(override.body), { status: override.status, headers: { 'content-type': 'application/json', ...headers } }); } const msg = `mock: no override for ${method} ${path}; register one in your test`; console.error(`[mock] ${method} ${path} → 500 (unmatched)`); return c.json({ status: 500, msg }, 500); }); const port = 4010; serve({ fetch: app.fetch, port }, ({ port }) => { console.error(`[mock] listening on http://localhost:${port}`); });