UNPKG

@pollyjs/adapter

Version:

Extendable base adapter class used by @pollyjs

349 lines (283 loc) 8.79 kB
import { ACTIONS, MODES, EXPIRY_STRATEGIES, PollyError, Serializers, assert } from '@pollyjs/utils'; import isExpired from './utils/is-expired'; import stringifyRequest from './utils/stringify-request'; import normalizeRecordedResponse from './utils/normalize-recorded-response'; const REQUEST_HANDLER = Symbol(); export default class Adapter { constructor(polly) { this.polly = polly; this.isConnected = false; } static get type() { return 'adapter'; } /* eslint-disable-next-line getter-return */ static get id() { assert('Must override the static `id` getter.'); } get defaultOptions() { return {}; } get options() { return { ...(this.defaultOptions || {}), ...((this.polly.config.adapterOptions || {})[this.constructor.id] || {}) }; } get persister() { return this.polly.persister; } connect() { if (!this.isConnected) { this.onConnect(); this.isConnected = true; this.polly.logger.log.debug( `Connected to ${this.constructor.id} adapter.` ); } } onConnect() { this.assert('Must implement the `onConnect` hook.'); } disconnect() { if (this.isConnected) { this.onDisconnect(); this.isConnected = false; this.polly.logger.log.debug( `Disconnected from ${this.constructor.id} adapter.` ); } } onDisconnect() { this.assert('Must implement the `onDisconnect` hook.'); } timeout(pollyRequest, { time }) { const { timing } = pollyRequest.config; if (typeof timing === 'function') { return timing(time); } } async handleRequest(request) { const pollyRequest = this.polly.registerRequest(request); try { pollyRequest.on('identify', (...args) => this.onIdentifyRequest(...args)); await this.onRequest(pollyRequest); await pollyRequest.init(); await this[REQUEST_HANDLER](pollyRequest); if (pollyRequest.aborted) { throw new PollyError('Request aborted.'); } await this.onRequestFinished(pollyRequest); } catch (error) { await this.onRequestFailed(pollyRequest, error); } return pollyRequest; } async [REQUEST_HANDLER](pollyRequest) { const { mode } = this.polly; const { _interceptor: interceptor } = pollyRequest; if (pollyRequest.aborted) { return; } if (pollyRequest.shouldIntercept) { await this.intercept(pollyRequest, interceptor); if (interceptor.shouldIntercept) { return; } } if ( mode === MODES.PASSTHROUGH || pollyRequest.shouldPassthrough || interceptor.shouldPassthrough ) { return this.passthrough(pollyRequest); } this.assert( 'A persister must be configured in order to record and replay requests.', !!this.persister ); if (mode === MODES.RECORD) { return this.record(pollyRequest); } if (mode === MODES.REPLAY) { return this.replay(pollyRequest); } // This should never be reached. If it did, then something screwy happened. this.assert( 'Unhandled request: \n' + stringifyRequest(pollyRequest, null, 2) ); } async passthrough(pollyRequest) { pollyRequest.action = ACTIONS.PASSTHROUGH; return this.onPassthrough(pollyRequest); } /** * @param {PollyRequest} pollyRequest */ async onPassthrough(pollyRequest) { const response = await this.onFetchResponse(pollyRequest); await pollyRequest.respond(response); } async intercept(pollyRequest, interceptor) { pollyRequest.action = ACTIONS.INTERCEPT; await pollyRequest._intercept(interceptor); if (interceptor.shouldIntercept) { return this.onIntercept(pollyRequest, pollyRequest.response); } } /** * @param {PollyRequest} pollyRequest * @param {PollyResponse} pollyResponse */ async onIntercept(pollyRequest, pollyResponse) { await pollyRequest.respond(pollyResponse); } async record(pollyRequest) { pollyRequest.action = ACTIONS.RECORD; if ('navigator' in global && !navigator.onLine) { pollyRequest.log.warn( '[Polly] Recording may fail because the browser is offline.\n' + `${stringifyRequest(pollyRequest)}` ); } return this.onRecord(pollyRequest); } /** * @param {PollyRequest} pollyRequest */ async onRecord(pollyRequest) { await this.onPassthrough(pollyRequest); if (!pollyRequest.aborted) { await this.persister.recordRequest(pollyRequest); } } async replay(pollyRequest) { const { config } = pollyRequest; const recordingEntry = await this.persister.findEntry(pollyRequest); if (recordingEntry) { /* Clone the recording entry so any changes will not actually persist to the stored recording. Note: Using JSON.parse/stringify instead of lodash/cloneDeep since the recording entry is stored as json. */ const clonedRecordingEntry = JSON.parse(JSON.stringify(recordingEntry)); await pollyRequest._emit('beforeReplay', clonedRecordingEntry); if (isExpired(clonedRecordingEntry.startedDateTime, config.expiresIn)) { const message = 'Recording for the following request has expired.\n' + `${stringifyRequest(pollyRequest, null, 2)}`; switch (config.expiryStrategy) { // exit into the record flow if expiryStrategy is "record". case EXPIRY_STRATEGIES.RECORD: return this.record(pollyRequest); // throw an error and exit if expiryStrategy is "error". case EXPIRY_STRATEGIES.ERROR: this.assert(message); break; // log a warning and continue if expiryStrategy is "warn". case EXPIRY_STRATEGIES.WARN: pollyRequest.log.warn(`[Polly] ${message}`); break; // throw an error if we encounter an unsupported expiryStrategy. default: this.assert( `Invalid config option passed for "expiryStrategy": "${config.expiryStrategy}"` ); break; } } await this.timeout(pollyRequest, clonedRecordingEntry); pollyRequest.action = ACTIONS.REPLAY; return this.onReplay( pollyRequest, normalizeRecordedResponse(clonedRecordingEntry.response), clonedRecordingEntry ); } if (config.recordIfMissing) { return this.record(pollyRequest); } this.assert( 'Recording for the following request is not found and `recordIfMissing` is `false`.\n' + stringifyRequest(pollyRequest, null, 2) ); } /** * @param {PollyRequest} pollyRequest * @param {Object} normalizedResponse The normalized response generated from the recording entry * @param {Object} recordingEntry The entire recording entry */ async onReplay(pollyRequest, normalizedResponse) { await pollyRequest.respond(normalizedResponse); } assert(message, ...args) { assert( `[${this.constructor.type}:${this.constructor.id}] ${message}`, ...args ); } /** * @param {PollyRequest} pollyRequest */ onRequest() {} /** * @param {PollyRequest} pollyRequest */ async onIdentifyRequest(pollyRequest) { const { identifiers } = pollyRequest; // Serialize the request body so it can be properly hashed for (const type of ['blob', 'formData', 'buffer']) { identifiers.body = await Serializers[type](identifiers.body); } } /** * @param {PollyRequest} pollyRequest */ async onRequestFinished(pollyRequest) { await this.onRespond(pollyRequest); pollyRequest.promise.resolve(); } /** * @param {PollyRequest} pollyRequest * @param {Error} [error] */ async onRequestFailed(pollyRequest, error) { const { aborted } = pollyRequest; error = error || new PollyError('Request failed due to an unknown error.'); try { if (aborted) { await pollyRequest._emit('abort'); } else { await pollyRequest._emit('error', error); } await this.onRespond(pollyRequest, error); } finally { pollyRequest.promise.reject(error); } } /** * Make sure the response from a Polly request is delivered to the * user through the adapter interface. * * Calling `pollyjs.flush()` will await this method. * * @param {PollyRequest} pollyRequest * @param {Error} [error] */ async onRespond(/* pollyRequest, error */) {} /** * @param {PollyRequest} pollyRequest * @returns {Object({ statusCode: number, headers: Object, body: string })} */ async onFetchResponse(/* pollyRequest */) { this.assert('Must implement the `onFetchResponse` hook.'); } }