Files
cms-client/scripts/mock-server.ts
2026-04-18 12:14:30 +08:00

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}`);
});