Skip to main content
Glama
http-fetch-mock.ts11.7 kB
/** * HTTP/Fetch Mocking System for Test Infrastructure * Provides comprehensive mocking for all HTTP/fetch operations in tests * Integrates with universal cleanup system for proper test isolation */ import { vi } from 'vitest'; import logger from '../../../../logger.js'; /** * Mock response configuration */ export interface MockHttpResponse { status: number; statusText?: string; headers?: Record<string, string>; body?: unknown; delay?: number; shouldFail?: boolean; failureReason?: string; } /** * Mock request matcher */ export interface MockRequestMatcher { url?: string | RegExp; method?: string; headers?: Record<string, string>; body?: unknown; } /** * Mock configuration entry */ interface MockEntry { matcher: MockRequestMatcher; response: MockHttpResponse; callCount: number; maxCalls?: number; id: string; } /** * Global mock state */ interface MockState { isInitialized: boolean; mockEntries: MockEntry[]; requestHistory: Array<{ url: string; method: string; headers: Record<string, string>; body: unknown; timestamp: number; }>; originalFetch?: typeof fetch; } let mockState: MockState = { isInitialized: false, mockEntries: [], requestHistory: [] }; /** * Initialize HTTP/fetch mocking system */ export function initializeHttpMocking(): void { if (mockState.isInitialized) { logger.debug('HTTP mocking already initialized'); return; } // Store original fetch if available if (typeof global.fetch !== 'undefined') { mockState.originalFetch = global.fetch; } // Mock global fetch global.fetch = vi.fn().mockImplementation(async (url: string | URL, options: RequestInit = {}) => { const urlString = url.toString(); const method = options.method || 'GET'; const headers = (options.headers as Record<string, string>) || {}; const body = options.body; // Record request in history mockState.requestHistory.push({ url: urlString, method, headers, body, timestamp: Date.now() }); logger.debug({ url: urlString, method, headers }, 'Mock fetch intercepted request'); // Find matching mock entry const matchingEntry = findMatchingMockEntry(urlString, method, headers, body); if (!matchingEntry) { logger.warn({ url: urlString, method }, 'No mock configured for HTTP request'); throw new Error(`No mock configured for ${method} ${urlString}`); } // Increment call count matchingEntry.callCount++; // Check if max calls exceeded if (matchingEntry.maxCalls && matchingEntry.callCount > matchingEntry.maxCalls) { throw new Error(`Mock for ${method} ${urlString} exceeded max calls (${matchingEntry.maxCalls})`); } // Apply delay if configured if (matchingEntry.response.delay) { await new Promise(resolve => setTimeout(resolve, matchingEntry.response.delay)); } // Handle failure scenarios if (matchingEntry.response.shouldFail) { throw new Error(matchingEntry.response.failureReason || 'Mock HTTP request failed'); } // Create mock response const mockResponse = createMockResponse(matchingEntry.response); logger.debug({ url: urlString, method, status: mockResponse.status, callCount: matchingEntry.callCount }, 'Mock fetch returning response'); return mockResponse; }); mockState.isInitialized = true; logger.debug('HTTP/fetch mocking system initialized'); } /** * Find matching mock entry for a request */ function findMatchingMockEntry( url: string, method: string, headers: Record<string, string>, body: unknown ): MockEntry | undefined { return mockState.mockEntries.find(entry => { const matcher = entry.matcher; // Check URL match if (matcher.url) { if (typeof matcher.url === 'string') { if (url !== matcher.url) return false; } else if (matcher.url instanceof RegExp) { if (!matcher.url.test(url)) return false; } } // Check method match if (matcher.method && method.toLowerCase() !== matcher.method.toLowerCase()) { return false; } // Check headers match if (matcher.headers) { for (const [key, value] of Object.entries(matcher.headers)) { if (headers[key] !== value) return false; } } // Check body match (basic comparison) if (matcher.body && JSON.stringify(body) !== JSON.stringify(matcher.body)) { return false; } return true; }); } /** * Create mock Response object */ function createMockResponse(config: MockHttpResponse): Response { const headers = new Headers(config.headers || {}); const responseBody = config.body !== undefined ? (typeof config.body === 'string' ? config.body : JSON.stringify(config.body)) : ''; return new Response(responseBody, { status: config.status, statusText: config.statusText || getDefaultStatusText(config.status), headers }); } /** * Get default status text for HTTP status codes */ function getDefaultStatusText(status: number): string { const statusTexts: Record<number, string> = { 200: 'OK', 201: 'Created', 204: 'No Content', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' }; return statusTexts[status] || 'Unknown'; } /** * Add a mock for HTTP requests */ export function mockHttpRequest( matcher: MockRequestMatcher, response: MockHttpResponse, options: { maxCalls?: number; id?: string } = {} ): string { const id = options.id || `mock-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; const mockEntry: MockEntry = { matcher, response, callCount: 0, maxCalls: options.maxCalls, id }; mockState.mockEntries.push(mockEntry); logger.debug({ id, matcher, response }, 'HTTP mock registered'); return id; } /** * Mock successful HTTP response */ export function mockHttpSuccess( matcher: MockRequestMatcher, body?: unknown, status: number = 200, options: { maxCalls?: number; id?: string } = {} ): string { return mockHttpRequest(matcher, { status, body, headers: { 'Content-Type': 'application/json' } }, options); } /** * Mock HTTP error response */ export function mockHttpError( matcher: MockRequestMatcher, status: number = 500, errorMessage?: string, options: { maxCalls?: number; id?: string } = {} ): string { return mockHttpRequest(matcher, { status, body: { error: errorMessage || 'Internal Server Error' }, headers: { 'Content-Type': 'application/json' } }, options); } /** * Mock HTTP request failure (network error) */ export function mockHttpFailure( matcher: MockRequestMatcher, failureReason?: string, options: { maxCalls?: number; id?: string } = {} ): string { return mockHttpRequest(matcher, { status: 0, shouldFail: true, failureReason: failureReason || 'Network error' }, options); } /** * Mock agent HTTP endpoint */ export function mockAgentEndpoint( agentId: string, endpoint: string, response: { success?: boolean; result?: unknown; error?: string } = { success: true }, options: { maxCalls?: number } = {} ): string { return mockHttpSuccess( { url: endpoint, method: 'POST', headers: { 'Content-Type': 'application/json' } }, response, 200, { ...options, id: `agent-${agentId}` } ); } /** * Remove a specific mock by ID */ export function removeMock(id: string): boolean { const initialLength = mockState.mockEntries.length; mockState.mockEntries = mockState.mockEntries.filter(entry => entry.id !== id); const removed = mockState.mockEntries.length < initialLength; if (removed) { logger.debug({ id }, 'HTTP mock removed'); } return removed; } /** * Clear all HTTP mocks */ export function clearAllHttpMocks(): void { const mockCount = mockState.mockEntries.length; mockState.mockEntries = []; mockState.requestHistory = []; if (mockCount > 0) { logger.debug({ mockCount }, 'All HTTP mocks cleared'); } } /** * Get request history */ export function getRequestHistory(): typeof mockState.requestHistory { return [...mockState.requestHistory]; } /** * Get mock statistics */ export function getMockStats(): { totalMocks: number; totalRequests: number; mockUsage: Array<{ id: string; matcher: MockRequestMatcher; callCount: number; maxCalls?: number; }>; } { return { totalMocks: mockState.mockEntries.length, totalRequests: mockState.requestHistory.length, mockUsage: mockState.mockEntries.map(entry => ({ id: entry.id, matcher: entry.matcher, callCount: entry.callCount, maxCalls: entry.maxCalls })) }; } /** * Verify that a mock was called */ export function verifyMockCalled(id: string, expectedCalls?: number): boolean { const entry = mockState.mockEntries.find(e => e.id === id); if (!entry) { throw new Error(`Mock with ID ${id} not found`); } if (expectedCalls !== undefined) { return entry.callCount === expectedCalls; } return entry.callCount > 0; } /** * Verify that a request was made */ export function verifyRequestMade( url: string | RegExp, method?: string, expectedCount?: number ): boolean { const matchingRequests = mockState.requestHistory.filter(req => { const urlMatches = typeof url === 'string' ? req.url === url : url.test(req.url); const methodMatches = !method || req.method.toLowerCase() === method.toLowerCase(); return urlMatches && methodMatches; }); if (expectedCount !== undefined) { return matchingRequests.length === expectedCount; } return matchingRequests.length > 0; } /** * Reset HTTP mocking system */ export function resetHttpMocking(): void { // Restore original fetch if it was stored if (mockState.originalFetch) { global.fetch = mockState.originalFetch; } else { delete (global as Record<string, unknown>).fetch; } // Reset state mockState = { isInitialized: false, mockEntries: [], requestHistory: [] }; logger.debug('HTTP mocking system reset'); } /** * Setup HTTP mocking for tests (call in beforeEach) */ export function setupHttpMocking(): void { initializeHttpMocking(); clearAllHttpMocks(); } /** * Cleanup HTTP mocking for tests (call in afterEach) */ export function cleanupHttpMocking(): void { clearAllHttpMocks(); } /** * Common mock configurations for agent communication */ export const AgentMocks = { /** * Mock successful agent task assignment */ successfulTaskAssignment: (agentId: string, endpoint: string) => mockAgentEndpoint(agentId, endpoint, { success: true, result: 'Task assigned' }), /** * Mock failed agent task assignment */ failedTaskAssignment: (agentId: string, endpoint: string, error: string = 'Agent unavailable') => mockAgentEndpoint(agentId, endpoint, { success: false, error }), /** * Mock agent timeout */ agentTimeout: (agentId: string, endpoint: string) => mockHttpRequest( { url: endpoint, method: 'POST' }, { status: 200, body: { success: true }, delay: 10000 }, // 10 second delay { id: `agent-timeout-${agentId}` } ), /** * Mock agent network error */ agentNetworkError: (agentId: string, endpoint: string) => mockHttpFailure( { url: endpoint, method: 'POST' }, 'Network connection failed', { id: `agent-network-error-${agentId}` } ) };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server