109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { serve } from '@hono/node-server';
|
|
import { Hono } from 'hono';
|
|
import { z } from 'zod';
|
|
|
|
type OverrideResponse = { status: number; body?: unknown; headers?: Record<string, string> };
|
|
type RecordedRequest = {
|
|
method: string;
|
|
path: string;
|
|
query: Record<string, string>;
|
|
body: unknown;
|
|
headers: Record<string, string>;
|
|
};
|
|
|
|
const overrides = new Map<string, OverrideResponse>();
|
|
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<string, string> = {};
|
|
url.searchParams.forEach((v, k) => (query[k] = v));
|
|
const headers: Record<string, string> = {};
|
|
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}`);
|
|
});
|