mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
- Prevent spy leak on double mockFetch() call - Differentiate body types (URLSearchParams, ArrayBuffer, Blob, ReadableStream) - Fix route path parsing to preserve spaces - Escape regex special chars before wildcard replacement - Remove unused MockResponse.error and MockRoute interface
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
/**
|
|
* Mock HTTP Server
|
|
*
|
|
* Simulates HTTP server responses without actual network operations.
|
|
* No port binding - instant responses for fast testing.
|
|
*/
|
|
|
|
import type {
|
|
HttpMethod,
|
|
MockHttpServer,
|
|
MockHttpServerConfig,
|
|
MockResponse,
|
|
CapturedRequest,
|
|
RouteKey,
|
|
} from './types';
|
|
|
|
/** Default 404 response for unmatched routes */
|
|
const DEFAULT_NOT_FOUND: MockResponse = {
|
|
status: 404,
|
|
body: { error: 'Not Found' },
|
|
};
|
|
|
|
/**
|
|
* Create a mock HTTP server that handles requests without network
|
|
*
|
|
* @example
|
|
* const server = createMockHttpServer({
|
|
* routes: {
|
|
* 'GET /health': { status: 200, body: { healthy: true } },
|
|
* 'POST /upload': { status: 201, body: { id: '123' } },
|
|
* }
|
|
* });
|
|
* const response = server.handle('GET', '/health');
|
|
*/
|
|
export function createMockHttpServer(config: MockHttpServerConfig): MockHttpServer {
|
|
const capturedRequests: CapturedRequest[] = [];
|
|
const { routes, defaultResponse = DEFAULT_NOT_FOUND } = config;
|
|
|
|
return {
|
|
handle(method: HttpMethod, path: string, body?: unknown): MockResponse {
|
|
// Capture request for assertions
|
|
capturedRequests.push({
|
|
url: path,
|
|
method,
|
|
headers: {},
|
|
body: body ? JSON.stringify(body) : null,
|
|
});
|
|
|
|
// Find matching route
|
|
const routeKey = `${method} ${path}` as RouteKey;
|
|
const exactMatch = routes[routeKey];
|
|
if (exactMatch) {
|
|
return exactMatch;
|
|
}
|
|
|
|
// Try pattern matching for routes with wildcards
|
|
for (const [key, response] of Object.entries(routes)) {
|
|
// Split on first space only to preserve spaces in path
|
|
const spaceIndex = key.indexOf(' ');
|
|
if (spaceIndex === -1) continue;
|
|
const routeMethod = key.slice(0, spaceIndex);
|
|
const routePath = key.slice(spaceIndex + 1);
|
|
if (routeMethod !== method) continue;
|
|
|
|
// Check if route path is a pattern (contains *)
|
|
if (routePath.includes('*')) {
|
|
// Escape regex special chars, then replace * with .*
|
|
const escaped = routePath.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
const pattern = escaped.replace(/\*/g, '.*');
|
|
const regex = new RegExp(`^${pattern}$`);
|
|
if (regex.test(path)) {
|
|
return response;
|
|
}
|
|
}
|
|
}
|
|
|
|
return defaultResponse;
|
|
},
|
|
|
|
getCapturedRequests(): CapturedRequest[] {
|
|
return [...capturedRequests];
|
|
},
|
|
|
|
clearCapturedRequests(): void {
|
|
capturedRequests.length = 0;
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a mock Response object (Web API compatible)
|
|
*
|
|
* @example
|
|
* const response = createMockResponse({ status: 200, body: { ok: true } });
|
|
*/
|
|
export function createMockResponse(config: MockResponse): Response {
|
|
const { status = 200, body, headers = {} } = config;
|
|
|
|
let responseBody: string | null = null;
|
|
const responseHeaders = new Headers(headers);
|
|
|
|
if (body !== null && body !== undefined) {
|
|
if (typeof body === 'object' && !(body instanceof Buffer)) {
|
|
responseBody = JSON.stringify(body);
|
|
if (!responseHeaders.has('Content-Type')) {
|
|
responseHeaders.set('Content-Type', 'application/json');
|
|
}
|
|
} else if (body instanceof Buffer) {
|
|
responseBody = body.toString();
|
|
} else {
|
|
responseBody = body;
|
|
}
|
|
}
|
|
|
|
return new Response(responseBody, {
|
|
status,
|
|
headers: responseHeaders,
|
|
});
|
|
}
|