UNPKG

@zimic/interceptor

Version:

Next-gen TypeScript-first HTTP intercepting and mocking

212 lines (169 loc) 6.98 kB
import { HttpResponse, HttpMethod, HttpSchema } from '@zimic/http'; import excludeURLParams from '@zimic/utils/url/excludeURLParams'; import validateURLPathParams from '@zimic/utils/url/validateURLPathParams'; import { HttpHandlerCommit, InterceptorServerWebSocketSchema } from '@/server/types/schema'; import { importCrypto } from '@/utils/crypto'; import { isClientSide, isServerSide } from '@/utils/environment'; import { deserializeRequest, serializeResponse } from '@/utils/fetch'; import { WebSocketEventMessage } from '@/webSocket/types'; import WebSocketClient from '@/webSocket/WebSocketClient'; import NotRunningHttpInterceptorError from '../interceptor/errors/NotRunningHttpInterceptorError'; import UnknownHttpInterceptorPlatformError from '../interceptor/errors/UnknownHttpInterceptorPlatformError'; import HttpInterceptorClient, { AnyHttpInterceptorClient } from '../interceptor/HttpInterceptorClient'; import { HttpInterceptorPlatform } from '../interceptor/types/options'; import HttpInterceptorWorker from './HttpInterceptorWorker'; import { MSWHttpResponseFactory, MSWHttpResponseFactoryContext } from './types/msw'; import { RemoteHttpInterceptorWorkerOptions } from './types/options'; interface HttpHandler { id: string; url: { base: string; full: string }; method: HttpMethod; interceptor: AnyHttpInterceptorClient; createResponse: (context: MSWHttpResponseFactoryContext) => Promise<HttpResponse | null>; } class RemoteHttpInterceptorWorker extends HttpInterceptorWorker { private httpHandlers = new Map<HttpHandler['id'], HttpHandler>(); webSocketClient: WebSocketClient<InterceptorServerWebSocketSchema>; private auth?: RemoteHttpInterceptorWorkerOptions['auth']; constructor(options: RemoteHttpInterceptorWorkerOptions) { super(); this.webSocketClient = new WebSocketClient({ url: this.getWebSocketServerURL(options.serverURL).toString(), }); this.auth = options.auth; } get type() { return 'remote' as const; } private getWebSocketServerURL(serverURL: URL) { const webSocketServerURL = new URL(serverURL); webSocketServerURL.protocol = serverURL.protocol.replace(/^http(s)?:$/, 'ws$1:'); return webSocketServerURL; } async start() { await super.sharedStart(async () => { await this.webSocketClient.start({ parameters: this.auth ? { token: this.auth.token } : undefined, waitForAuthentication: true, }); this.webSocketClient.onEvent('interceptors/responses/create', this.createResponse); this.webSocketClient.onEvent('interceptors/responses/unhandled', this.handleUnhandledServerRequest); this.platform = this.readPlatform(); this.isRunning = true; }); } private createResponse = async ( message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/create'>, ) => { const { handlerId, request: serializedRequest } = message.data; const handler = this.httpHandlers.get(handlerId); const request = deserializeRequest(serializedRequest); try { const rawResponse = (await handler?.createResponse({ request })) ?? null; const response = rawResponse && request.method === 'HEAD' ? new Response(null, rawResponse) : rawResponse; if (response) { return { response: await serializeResponse(response) }; } } catch (error) { console.error(error); } const strategy = await super.getUnhandledRequestStrategy(request, 'remote'); await super.logUnhandledRequestIfNecessary(request, strategy); return { response: null }; }; private handleUnhandledServerRequest = async ( message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/unhandled'>, ) => { const { request: serializedRequest } = message.data; const request = deserializeRequest(serializedRequest); const strategy = await super.getUnhandledRequestStrategy(request, 'remote'); const { wasLogged } = await super.logUnhandledRequestIfNecessary(request, strategy); return { wasLogged }; }; private readPlatform(): HttpInterceptorPlatform { if (isServerSide()) { return 'node'; } /* istanbul ignore else -- @preserve */ if (isClientSide()) { return 'browser'; } /* istanbul ignore next -- @preserve * Ignoring because checking unknown platforms is not configured in our test setup. */ throw new UnknownHttpInterceptorPlatformError(); } async stop() { await super.sharedStop(async () => { await this.clearHandlers(); this.webSocketClient.offEvent('interceptors/responses/create', this.createResponse); this.webSocketClient.offEvent('interceptors/responses/unhandled', this.handleUnhandledServerRequest); await this.webSocketClient.stop(); this.isRunning = false; }); } async use<Schema extends HttpSchema>( interceptor: HttpInterceptorClient<Schema>, method: HttpMethod, rawURL: string | URL, createResponse: MSWHttpResponseFactory, ) { if (!this.isRunning) { throw new NotRunningHttpInterceptorError(); } const crypto = await importCrypto(); const url = new URL(rawURL); excludeURLParams(url); validateURLPathParams(url); const handler: HttpHandler = { id: crypto.randomUUID(), url: { base: interceptor.baseURLAsString, full: url.toString(), }, method, interceptor, async createResponse(context) { const response = await createResponse(context); return response; }, }; this.httpHandlers.set(handler.id, handler); await this.webSocketClient.request('interceptors/workers/commit', { id: handler.id, url: handler.url, method, }); } async clearHandlers() { if (!this.isRunning) { throw new NotRunningHttpInterceptorError(); } this.httpHandlers.clear(); if (this.webSocketClient.isRunning) { await this.webSocketClient.request('interceptors/workers/reset', undefined); } } async clearInterceptorHandlers<Schema extends HttpSchema>(interceptor: HttpInterceptorClient<Schema>) { if (!this.isRunning) { throw new NotRunningHttpInterceptorError(); } for (const handler of this.httpHandlers.values()) { if (handler.interceptor === interceptor) { this.httpHandlers.delete(handler.id); } } if (this.webSocketClient.isRunning) { const groupsToRecommit = Array.from<HttpHandler, HttpHandlerCommit>(this.httpHandlers.values(), (handler) => ({ id: handler.id, url: handler.url, method: handler.method, })); await this.webSocketClient.request('interceptors/workers/reset', groupsToRecommit); } } get interceptorsWithHandlers() { const interceptors = Array.from(this.httpHandlers.values(), (handler) => handler.interceptor); return interceptors; } } export default RemoteHttpInterceptorWorker;