UNPKG

@zimic/interceptor

Version:

Next-gen TypeScript-first HTTP intercepting and mocking

180 lines (149 loc) 5.88 kB
import { HttpSchema, HttpSchemaMethod, HttpSchemaPath, HttpStatusCode } from '@zimic/http'; import { Default, PossiblePromise } from '@zimic/utils/types'; import HttpInterceptorClient from '../interceptor/HttpInterceptorClient'; import HttpRequestHandlerClient from './HttpRequestHandlerClient'; import { InternalHttpRequestHandler, SyncedRemoteHttpRequestHandler as PublicSyncedRemoteHttpRequestHandler, } from './types/public'; import { HttpInterceptorRequest, HttpInterceptorResponse, HttpRequestHandlerResponseDeclaration, HttpRequestHandlerResponseDeclarationFactory, InterceptedHttpInterceptorRequest, } from './types/requests'; import { HttpRequestHandlerRestriction } from './types/restrictions'; const PENDING_PROPERTIES = new Set<string | symbol>(['then'] satisfies (keyof Promise<unknown>)[]); class RemoteHttpRequestHandler< Schema extends HttpSchema, Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath<Schema, Method>, StatusCode extends HttpStatusCode = never, > implements InternalHttpRequestHandler<Schema, Method, Path, StatusCode> { readonly type = 'remote'; client: HttpRequestHandlerClient<Schema, Method, Path, StatusCode>; private syncPromises: Promise<unknown>[] = []; private unsynced: this; private synced: this; constructor(interceptor: HttpInterceptorClient<Schema, typeof RemoteHttpRequestHandler>, method: Method, path: Path) { this.client = new HttpRequestHandlerClient(interceptor, method, path, this); this.unsynced = this; this.synced = this.createSyncedProxy(); } private createSyncedProxy() { return new Proxy(this, { has: (target, property) => { if (this.isHiddenPropertyWhenSynced(property)) { return false; } return Reflect.has(target, property); }, get: (target, property) => { if (this.isHiddenPropertyWhenSynced(property)) { return undefined; } return Reflect.get(target, property); }, }); } private isHiddenPropertyWhenSynced(property: string | symbol) { return PENDING_PROPERTIES.has(property); } get method() { return this.client.method; } get path() { return this.client.path; } with(restriction: HttpRequestHandlerRestriction<Schema, Method, Path>): this { this.client.with(restriction); return this.unsynced; } respond<NewStatusCode extends HttpStatusCode>( declaration: | HttpRequestHandlerResponseDeclaration<Default<Schema[Path][Method]>, NewStatusCode> | HttpRequestHandlerResponseDeclarationFactory<Path, Default<Schema[Path][Method]>, NewStatusCode>, ): RemoteHttpRequestHandler<Schema, Method, Path, NewStatusCode> { const newUnsyncedThis = this.unsynced as unknown as RemoteHttpRequestHandler<Schema, Method, Path, NewStatusCode>; newUnsyncedThis.client.respond(declaration); return newUnsyncedThis; } times(minNumberOfRequests: number, maxNumberOfRequests?: number): this { this.client.times(minNumberOfRequests, maxNumberOfRequests); return this; } async checkTimes() { return new Promise<void>((resolve, reject) => { try { this.client.checkTimes(); resolve(); } catch (error) { reject(error); } }); } clear(): this { this.client.clear(); return this.unsynced; } get requests(): readonly InterceptedHttpInterceptorRequest<Path, Default<Schema[Path][Method]>, StatusCode>[] { return this.client.requests; } matchesRequest(request: HttpInterceptorRequest<Path, Default<Schema[Path][Method]>>): Promise<boolean> { return this.client.matchesRequest(request); } async applyResponseDeclaration( request: HttpInterceptorRequest<Path, Default<Schema[Path][Method]>>, ): Promise<HttpRequestHandlerResponseDeclaration<Default<Schema[Path][Method]>, StatusCode>> { return this.client.applyResponseDeclaration(request); } saveInterceptedRequest( request: HttpInterceptorRequest<Path, Default<Schema[Path][Method]>>, response: HttpInterceptorResponse<Default<Schema[Path][Method]>, StatusCode>, ) { this.client.saveInterceptedRequest(request, response); } registerSyncPromise(promise: Promise<unknown>) { this.syncPromises.push(promise); } get isSynced() { return this.syncPromises.length === 0; } then< FulfilledResult = PublicSyncedRemoteHttpRequestHandler<Schema, Method, Path, StatusCode>, RejectedResult = never, >( onFulfilled?: | (( handler: PublicSyncedRemoteHttpRequestHandler<Schema, Method, Path, StatusCode>, ) => PossiblePromise<FulfilledResult>) | null, onRejected?: ((reason: unknown) => PossiblePromise<RejectedResult>) | null, ): Promise<FulfilledResult | RejectedResult> { const promisesToWait = new Set(this.syncPromises); return Promise.all(promisesToWait) .then(() => { this.syncPromises = this.syncPromises.filter((promise) => !promisesToWait.has(promise)); return this.isSynced ? this.synced : this.unsynced; }) .then(onFulfilled, onRejected); } catch<RejectedResult = never>( onRejected?: ((reason: unknown) => PossiblePromise<RejectedResult>) | null, ): Promise<PublicSyncedRemoteHttpRequestHandler<Schema, Method, Path, StatusCode> | RejectedResult> { return this.then().catch(onRejected); } finally( onFinally?: (() => void) | null, ): Promise<PublicSyncedRemoteHttpRequestHandler<Schema, Method, Path, StatusCode>> { return this.then().finally(onFinally); } } export type AnyRemoteHttpRequestHandler = // eslint-disable-next-line @typescript-eslint/no-explicit-any | RemoteHttpRequestHandler<any, any, any> // eslint-disable-next-line @typescript-eslint/no-explicit-any | RemoteHttpRequestHandler<any, any, any, any>; export default RemoteHttpRequestHandler;