Files
ccs/tests/mocks/mock-http-server.ts
kaitranntt 2b91c40e37 fix(tests): address edge cases in mock infrastructure
- 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
2026-01-23 23:41:49 -05:00

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