UNPKG

@zimic/interceptor

Version:

Next-gen TypeScript-first HTTP intercepting and mocking

1,422 lines (1,375 loc) 99.1 kB
'use strict'; var http = require('@zimic/http'); var color2 = require('picocolors'); var msw = require('msw'); var mswBrowser = require('msw/browser'); var mswNode = require('msw/node'); var ClientSocket = require('isomorphic-ws'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var color2__default = /*#__PURE__*/_interopDefault(color2); var mswBrowser__namespace = /*#__PURE__*/_interopNamespace(mswBrowser); var mswNode__namespace = /*#__PURE__*/_interopNamespace(mswNode); var ClientSocket__default = /*#__PURE__*/_interopDefault(ClientSocket); // src/http/index.ts // src/http/interceptor/errors/RunningHttpInterceptorError.ts var RunningHttpInterceptorError = class extends Error { constructor(additionalMessage) { super(`The interceptor is running. ${additionalMessage}`); this.name = "RunningHttpInterceptorError"; } }; var RunningHttpInterceptorError_default = RunningHttpInterceptorError; // src/http/interceptor/errors/NotRunningHttpInterceptorError.ts var NotRunningHttpInterceptorError = class extends Error { constructor() { super("Interceptor is not running. Did you forget to call `await interceptor.start()`?"); this.name = "NotRunningHttpInterceptorError"; } }; var NotRunningHttpInterceptorError_default = NotRunningHttpInterceptorError; // src/http/interceptor/errors/UnknownHttpInterceptorPlatformError.ts var UnknownHttpInterceptorPlatformError = class extends Error { /* istanbul ignore next -- @preserve * Ignoring because checking unknown platforms is currently not possible in our Vitest setup. */ constructor() { super("Unknown interceptor platform."); this.name = "UnknownHttpInterceptorPlatformError"; } }; var UnknownHttpInterceptorPlatformError_default = UnknownHttpInterceptorPlatformError; // src/http/interceptor/errors/UnknownHttpInterceptorTypeError.ts var UnknownHttpInterceptorTypeError = class extends TypeError { constructor(unknownType) { super( `Unknown HTTP interceptor type: ${unknownType}. The available options are '${"local"}' and '${"remote"}'.` ); this.name = "UnknownHttpInterceptorTypeError"; } }; var UnknownHttpInterceptorTypeError_default = UnknownHttpInterceptorTypeError; // src/http/interceptor/errors/RequestSavingSafeLimitExceededError.ts var RequestSavingSafeLimitExceededError = class extends TypeError { constructor(numberOfSavedRequests, safeLimit) { super( `The number of intercepted requests saved in memory (${numberOfSavedRequests}) exceeded the safe limit of ${safeLimit}. Did you forget to call \`interceptor.clear()\`? If you need to save requests, make sure to regularly call \`interceptor.clear()\`. Alternatively, you can hide this warning by increasing \`requestSaving.safeLimit\` in your interceptor. Note that saving too many requests in memory can lead to performance issues. If you do not need to save requests, consider setting \`requestSaving.enabled: false\` in your interceptor. Learn more: https://zimic.dev/docs/interceptor/api/create-http-interceptor` ); this.name = "RequestSavingSafeLimitExceededError"; } }; var RequestSavingSafeLimitExceededError_default = RequestSavingSafeLimitExceededError; // src/cli/browser/shared/constants.ts var SERVICE_WORKER_FILE_NAME = "mockServiceWorker.js"; // src/http/interceptorWorker/errors/UnregisteredBrowserServiceWorkerError.ts var UnregisteredBrowserServiceWorkerError = class extends Error { constructor() { super( `Failed to register the browser service worker: script '${window.location.origin}/${SERVICE_WORKER_FILE_NAME}' not found. Did you forget to run \`zimic-interceptor browser init <publicDirectory>\`? Learn more: https://zimic.dev/docs/interceptor/cli/browser#zimic-interceptor-browser-init` ); this.name = "UnregisteredBrowserServiceWorkerError"; } static matchesRawError(error) { return error instanceof Error && error.message.toLowerCase().includes("service worker script does not exist at the given path"); } }; var UnregisteredBrowserServiceWorkerError_default = UnregisteredBrowserServiceWorkerError; // src/http/requestHandler/errors/DisabledRequestSavingError.ts var DisabledRequestSavingError = class extends TypeError { constructor() { super( "Intercepted requests are not being saved. Did you forget to use `requestSaving.enabled: true` in your interceptor?\n\nLearn more: https://zimic.dev/docs/interceptor/api/create-http-interceptor" ); this.name = "DisabledRequestSavingError"; } }; var DisabledRequestSavingError_default = DisabledRequestSavingError; // ../zimic-utils/dist/chunk-5UH44FTS.mjs function isDefined(value) { return value !== void 0 && value !== null; } var isDefined_default = isDefined; // ../zimic-utils/dist/data/isNonEmpty.mjs function isNonEmpty(value) { return isDefined_default(value) && value !== ""; } var isNonEmpty_default = isNonEmpty; // ../zimic-utils/dist/import/createCachedDynamicImport.mjs function createCachedDynamicImport(importModuleDynamically) { let cachedImportResult; return async function importModuleDynamicallyWithCache() { cachedImportResult ??= await importModuleDynamically(); return cachedImportResult; }; } var createCachedDynamicImport_default = createCachedDynamicImport; // ../zimic-utils/dist/logging/Logger.mjs var Logger = class _Logger { prefix; raw; constructor(options = {}) { const { prefix } = options; this.prefix = prefix; this.raw = prefix ? new _Logger({ ...options, prefix: void 0 }) : this; } logWithLevel(level, ...messages) { if (this.prefix) { console[level](this.prefix, ...messages); } else { console[level](...messages); } } info(...messages) { this.logWithLevel("log", ...messages); } warn(...messages) { this.logWithLevel("warn", ...messages); } error(...messages) { this.logWithLevel("error", ...messages); } table(headers, rows) { const columnLengths = headers.map((header) => { let maxValueLength = header.title.length; for (const row of rows) { const value = row[header.property]; if (value.length > maxValueLength) { maxValueLength = value.length; } } return maxValueLength; }); const formattedRows = []; const horizontalLine = columnLengths.map((length) => "\u2500".repeat(length)); formattedRows.push(horizontalLine, []); for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) { const header = headers[headerIndex]; const columnLength = columnLengths[headerIndex]; const value = header.title; formattedRows.at(-1)?.push(value.padEnd(columnLength, " ")); } formattedRows.push(horizontalLine); for (const row of rows) { formattedRows.push([]); for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) { const header = headers[headerIndex]; const columnLength = columnLengths[headerIndex]; const value = row[header.property]; formattedRows.at(-1)?.push(value.padEnd(columnLength, " ")); } } formattedRows.push(horizontalLine); const formattedTable = formattedRows.map((row, index) => { const isFirstLine = index === 0; if (isFirstLine) { return `\u250C\u2500${row.join("\u2500\u252C\u2500")}\u2500\u2510`; } const isLineAfterHeaders = index === 2; if (isLineAfterHeaders) { return `\u251C\u2500${row.join("\u2500\u253C\u2500")}\u2500\u2524`; } const isLastLine = index === formattedRows.length - 1; if (isLastLine) { return `\u2514\u2500${row.join("\u2500\u2534\u2500")}\u2500\u2518`; } return `\u2502 ${row.join(" \u2502 ")} \u2502`; }).join("\n"); this.logWithLevel("log", formattedTable); } }; var Logger_default = Logger; // src/utils/environment.ts function isServerSide() { return typeof process !== "undefined" && typeof process.versions !== "undefined"; } function isClientSide() { return typeof window !== "undefined" && typeof document !== "undefined"; } function isGlobalFileAvailable() { return globalThis.File !== void 0; } // src/utils/logging.ts var logger = new Logger_default({ prefix: color2__default.default.cyan("[@zimic/interceptor]") }); function stringifyJSONToLog(value) { return JSON.stringify( value, (_key, value2) => { return stringifyValueToLog(value2, { fallback: (value3) => value3 }); }, 2 ).replace(/\n\s*/g, " ").replace(/"(File { name: '.*?', type: '.*?', size: \d*? })"/g, "$1").replace(/"(Blob { type: '.*?', size: \d*? })"/g, "$1"); } function stringifyValueToLog(value, options = {}) { const { fallback = stringifyJSONToLog, includeClassName } = options; if (value === null || value === void 0 || typeof value !== "object") { return String(value); } if (value instanceof http.HttpHeaders) { return stringifyValueToLog(value.toObject()); } if (value instanceof http.HttpHeaders || value instanceof http.HttpSearchParams) { const prefix = includeClassName?.searchParams ?? false ? "URLSearchParams " : ""; return `${prefix}${stringifyValueToLog(value.toObject())}`; } if (value instanceof http.HttpFormData) { return `FormData ${stringifyValueToLog(value.toObject())}`; } if (isGlobalFileAvailable() && value instanceof File) { return `File { name: '${value.name}', type: '${value.type}', size: ${value.size} }`; } if (value instanceof Blob) { return `Blob { type: '${value.type}', size: ${value.size} }`; } return fallback(value); } var importUtil = createCachedDynamicImport_default(() => import('util')); async function formatValueToLog(value, options = {}) { if (isClientSide()) { return value; } const { colors = true } = options; const util = await importUtil(); return util.inspect(value, { colors, compact: true, depth: Infinity, maxArrayLength: Infinity, maxStringLength: Infinity, breakLength: Infinity, sorted: true }); } // src/http/requestHandler/errors/TimesCheckError.ts function createMessageHeader({ requestLimits, hasRestrictions, numberOfMatchedRequests, unmatchedRequestGroups }) { const requestPrefix = hasRestrictions ? "matching " : ""; return [ "Expected ", requestLimits.min === requestLimits.max ? "exactly " : "at least ", requestLimits.min, requestLimits.min === requestLimits.max && (requestLimits.min === 1 ? ` ${requestPrefix}request` : ` ${requestPrefix}requests`), requestLimits.min !== requestLimits.max && Number.isFinite(requestLimits.max) && ` and at most ${requestLimits.max}`, requestLimits.min !== requestLimits.max && (requestLimits.max === 1 ? ` ${requestPrefix}request` : ` ${requestPrefix}requests`), ", but got ", numberOfMatchedRequests, ".", unmatchedRequestGroups.length > 0 && ` Requests evaluated by this handler: ${color2__default.default.green("- Expected")} ${color2__default.default.red("+ Received")}` ].filter((part) => part !== false).join(""); } function createMessageDiffs({ requestSaving, unmatchedRequestGroups }) { if (!requestSaving.enabled) { return "Tip: use `requestSaving.enabled: true` in your interceptor for more details about the unmatched requests."; } return unmatchedRequestGroups.map(({ request, diff }, index) => { const requestNumber = index + 1; const messageParts = [`${requestNumber}: ${request.method} ${request.url}`]; if (diff.computed) { messageParts.push("Computed restriction:"); const stringifiedExpected = stringifyValueToLog(diff.computed.expected); const stringifiedReceived = stringifyValueToLog(diff.computed.received); messageParts.push(` ${color2__default.default.green(`- return ${stringifiedExpected}`)}`); messageParts.push(` ${color2__default.default.red(`+ return ${stringifiedReceived}`)}`); } if (diff.headers) { messageParts.push("Headers:"); const stringifiedExpected = stringifyValueToLog(diff.headers.expected); const stringifiedReceived = stringifyValueToLog(diff.headers.received); messageParts.push(` ${color2__default.default.green(`- ${stringifiedExpected}`)}`); messageParts.push(` ${color2__default.default.red(`+ ${stringifiedReceived}`)}`); } if (diff.searchParams) { messageParts.push("Search params:"); const stringifiedExpected = stringifyValueToLog(diff.searchParams.expected); const stringifiedReceived = stringifyValueToLog(diff.searchParams.received); messageParts.push(` ${color2__default.default.green(`- ${stringifiedExpected}`)}`); messageParts.push(` ${color2__default.default.red(`+ ${stringifiedReceived}`)}`); } if (diff.body) { messageParts.push("Body:"); const stringifiedExpected = stringifyValueToLog(diff.body.expected, { includeClassName: { searchParams: true } }); const stringifiedReceived = stringifyValueToLog(diff.body.received, { includeClassName: { searchParams: true } }); messageParts.push(` ${color2__default.default.green(`- ${stringifiedExpected}`)}`); messageParts.push(` ${color2__default.default.red(`+ ${stringifiedReceived}`)}`); } return messageParts.join("\n "); }).join("\n\n"); } function createMessageFooter() { return "Learn more: https://zimic.dev/docs/interceptor/api/http-request-handler#handlertimes"; } function createMessage(options) { const messageHeader = createMessageHeader(options); const messageDiffs = createMessageDiffs(options); const messageFooter = createMessageFooter(); return [messageHeader, messageDiffs, messageFooter].filter(isNonEmpty_default).join("\n\n"); } var TimesCheckError = class extends TypeError { constructor(options) { const message = createMessage(options); super(message); this.name = "TimesCheckError"; this.cause = options.declarationPointer; } }; var TimesCheckError_default = TimesCheckError; // ../zimic-utils/dist/chunk-BSG74SVQ.mjs async function blobEquals(blob, otherBlob) { if (blob.type !== otherBlob.type || blob.size !== otherBlob.size) { return false; } const reader = blob.stream().getReader(); const otherReader = otherBlob.stream().getReader(); let buffer = new Uint8Array(0); let otherBuffer = new Uint8Array(0); try { while (true) { const bufferReadPromises = []; if (buffer.length === 0) { bufferReadPromises.push( reader.read().then((result) => { if (!result.done) { buffer = result.value; } }) ); } if (otherBuffer.length === 0) { bufferReadPromises.push( otherReader.read().then((result) => { if (!result.done) { otherBuffer = result.value; } }) ); } await Promise.all(bufferReadPromises); const haveStreamsEndedTogether = buffer.length === 0 && otherBuffer.length === 0; if (haveStreamsEndedTogether) { return true; } const hasOneStreamEndedBeforeTheOther = buffer.length === 0 && otherBuffer.length > 0 || buffer.length > 0 && otherBuffer.length === 0; if (hasOneStreamEndedBeforeTheOther) { return false; } const minimumByteLength = Math.min(buffer.length, otherBuffer.length); for (let byteIndex = 0; byteIndex < minimumByteLength; byteIndex++) { if (buffer[byteIndex] !== otherBuffer[byteIndex]) { return false; } } buffer = buffer.slice(minimumByteLength); otherBuffer = otherBuffer.slice(minimumByteLength); } } finally { reader.releaseLock(); otherReader.releaseLock(); } } var blobEquals_default = blobEquals; // ../zimic-utils/dist/chunk-LBZAJMTR.mjs function isPrimitiveJSONValue(value) { return typeof value !== "object" || value === null; } // ../zimic-utils/dist/data/jsonContains.mjs function jsonContains(value, otherValue) { if (isPrimitiveJSONValue(value)) { return value === otherValue; } if (isPrimitiveJSONValue(otherValue)) { return false; } if (Array.isArray(value)) { if (!Array.isArray(otherValue)) { return false; } if (value.length < otherValue.length) { return false; } let lastMatchedIndex = -1; return otherValue.every((otherItem) => { for (let index = lastMatchedIndex + 1; index < value.length; index++) { if (jsonContains(value[index], otherItem)) { lastMatchedIndex = index; return true; } } return false; }); } if (Array.isArray(otherValue)) { return false; } const valueKeys = Object.keys(value); const otherValueKeys = Object.keys(otherValue); if (valueKeys.length < otherValueKeys.length) { return false; } return otherValueKeys.every((key) => { const subValue = value[key]; const subOtherValue = otherValue[key]; return jsonContains(subValue, subOtherValue); }); } var jsonContains_default = jsonContains; // ../zimic-utils/dist/data/jsonEquals.mjs function jsonEquals(value, otherValue) { if (isPrimitiveJSONValue(value)) { return value === otherValue; } if (isPrimitiveJSONValue(otherValue)) { return false; } if (Array.isArray(value)) { if (!Array.isArray(otherValue)) { return false; } if (value.length !== otherValue.length) { return false; } return value.every((item, index) => jsonEquals(item, otherValue[index])); } if (Array.isArray(otherValue)) { return false; } const valueKeys = Object.keys(value); const otherValueKeys = Object.keys(otherValue); if (valueKeys.length !== otherValueKeys.length) { return false; } return valueKeys.every((key) => { const subValue = value[key]; const subOtherValue = otherValue[key]; return jsonEquals(subValue, subOtherValue); }); } var jsonEquals_default = jsonEquals; // ../zimic-utils/dist/chunk-QISSVK3C.mjs function waitForDelay(milliseconds) { return new Promise((resolve) => { setTimeout(resolve, milliseconds); }); } var waitForDelay_default = waitForDelay; // src/utils/data.ts async function convertReadableStreamToBlob(stream, options) { const chunks = []; const reader = stream.getReader(); while (true) { const result = await reader.read(); if (result.value) { chunks.push(result.value); } if (result.done) { break; } } return new Blob(chunks, options); } function convertArrayBufferToBlob(buffer, options) { return new Blob([buffer], options); } function convertArrayBufferToBase64(buffer) { if (isClientSide()) { const bufferBytes = new Uint8Array(buffer); const bufferAsStringArray = []; for (const byte of bufferBytes) { const byteCode = String.fromCharCode(byte); bufferAsStringArray.push(byteCode); } const bufferAsString = bufferAsStringArray.join(""); return btoa(bufferAsString); } else { return Buffer.from(buffer).toString("base64"); } } function convertBase64ToArrayBuffer(base64Value) { if (isClientSide()) { const bufferAsString = atob(base64Value); const array = Uint8Array.from(bufferAsString, (character) => character.charCodeAt(0)); return array.buffer; } else { return Buffer.from(base64Value, "base64"); } } // src/utils/numbers.ts function random(lowerLimit, upperLimit) { const range = Math.max(upperLimit - lowerLimit, 0); if (range === 0) { return lowerLimit; } return Math.random() * range + lowerLimit; } // src/http/requestHandler/errors/TimesDeclarationPointer.ts var TimesDeclarationPointer = class extends Error { constructor(minNumberOfRequests, maxNumberOfRequests) { super("declared at:"); this.name = `handler.times(${minNumberOfRequests}${maxNumberOfRequests === void 0 ? "" : `, ${maxNumberOfRequests}`})`; } }; var TimesDeclarationPointer_default = TimesDeclarationPointer; // src/http/requestHandler/HttpRequestHandlerClient.ts var DEFAULT_NUMBER_OF_REQUEST_LIMITS = Object.freeze({ min: 0, max: Infinity }); var HttpRequestHandlerClient = class { constructor(interceptor, method, path, handler) { this.interceptor = interceptor; this.method = method; this.path = path; this.handler = handler; } restrictions = []; limits = { numberOfRequests: DEFAULT_NUMBER_OF_REQUEST_LIMITS }; timesPointer; numberOfMatchedRequests = 0; unmatchedRequestGroups = []; _requests = []; createResponseDeclaration; createResponseDelay; with(restriction) { this.restrictions.push(restriction); return this; } delay(minMilliseconds, maxMilliseconds) { if (minMilliseconds === maxMilliseconds) { return this.delay(minMilliseconds); } if (typeof minMilliseconds === "number" && typeof maxMilliseconds === "number") { this.createResponseDelay = () => random(minMilliseconds, maxMilliseconds); return this; } if (typeof minMilliseconds === "number") { this.createResponseDelay = () => minMilliseconds; return this; } this.createResponseDelay = minMilliseconds; return this; } respond(declaration) { const newThis = this; newThis.createResponseDeclaration = this.isResponseDeclarationFactory(declaration) ? declaration : () => declaration; newThis.numberOfMatchedRequests = 0; newThis.unmatchedRequestGroups.length = 0; newThis.clearInterceptedRequests(); this.interceptor.registerRequestHandler(this.handler); return newThis; } isResponseDeclarationFactory(declaration) { return typeof declaration === "function"; } times(minNumberOfRequests, maxNumberOfRequests) { this.limits.numberOfRequests = { min: minNumberOfRequests, max: maxNumberOfRequests ?? minNumberOfRequests }; this.timesPointer = new TimesDeclarationPointer_default(minNumberOfRequests, maxNumberOfRequests); return this; } checkTimes() { const isWithinLimits = this.numberOfMatchedRequests >= this.limits.numberOfRequests.min && this.numberOfMatchedRequests <= this.limits.numberOfRequests.max; if (!isWithinLimits) { throw new TimesCheckError_default({ requestLimits: this.limits.numberOfRequests, numberOfMatchedRequests: this.numberOfMatchedRequests, declarationPointer: this.timesPointer, unmatchedRequestGroups: this.unmatchedRequestGroups, hasRestrictions: this.restrictions.length > 0, requestSaving: this.interceptor.requestSaving }); } } clear() { this.restrictions.length = 0; this.limits = { numberOfRequests: DEFAULT_NUMBER_OF_REQUEST_LIMITS }; this.timesPointer = void 0; this.numberOfMatchedRequests = 0; this.unmatchedRequestGroups.length = 0; this.clearInterceptedRequests(); this.createResponseDeclaration = void 0; this.createResponseDelay = void 0; return this; } async matchesRequest(request) { const restrictionsMatch = await this.matchesRestrictions(request); if (!restrictionsMatch.success) { return { success: false, cause: "unmatchedRestrictions", diff: restrictionsMatch.diff }; } const hasResponseDeclaration = this.createResponseDeclaration !== void 0; if (!hasResponseDeclaration) { return { success: false, cause: "missingResponseDeclaration" }; } const canAcceptMoreRequests = this.numberOfMatchedRequests < this.limits.numberOfRequests.max; if (!canAcceptMoreRequests) { return { success: false, cause: "exceededNumberOfRequests" }; } return { success: true }; } markRequestAsMatched(_request) { this.numberOfMatchedRequests++; } markRequestAsUnmatched(request, options) { const shouldSaveUnmatchedRequests = this.interceptor.requestSaving.enabled && this.restrictions.length > 0 && this.timesPointer !== void 0; if (shouldSaveUnmatchedRequests) { this.unmatchedRequestGroups.push({ request, diff: options.diff }); } } async matchesRestrictions(request) { for (const restriction of this.restrictions) { if (this.isComputedRequestRestriction(restriction)) { const matchesComputedRestriction = await restriction(request); if (!matchesComputedRestriction) { return { success: false, diff: { computed: { expected: true, received: false } } }; } continue; } const matchesHeadersRestrictions = this.matchesRequestHeadersRestrictions(request, restriction); const matchesSearchParamsRestrictions = this.matchesRequestSearchParamsRestrictions(request, restriction); const matchesBodyRestrictions = await this.matchesRequestBodyRestrictions(request, restriction); const matchesRestriction = matchesHeadersRestrictions.success && matchesSearchParamsRestrictions.success && matchesBodyRestrictions.success; if (!matchesRestriction) { return { success: false, diff: { headers: matchesHeadersRestrictions.diff, searchParams: matchesSearchParamsRestrictions.diff, body: matchesBodyRestrictions.diff } }; } } return { success: true }; } matchesRequestHeadersRestrictions(request, restriction) { if (restriction.headers === void 0) { return { success: true }; } const restrictedHeaders = new http.HttpHeaders( restriction.headers ); const matchesRestriction = restriction.exact ? request.headers.equals(restrictedHeaders) : request.headers.contains(restrictedHeaders); return matchesRestriction ? { success: true } : { success: false, diff: { expected: restrictedHeaders, received: request.headers } }; } matchesRequestSearchParamsRestrictions(request, restriction) { if (restriction.searchParams === void 0) { return { success: true }; } const restrictedSearchParams = new http.HttpSearchParams( restriction.searchParams ); const matchesRestriction = restriction.exact ? request.searchParams.equals(restrictedSearchParams) : request.searchParams.contains(restrictedSearchParams); return matchesRestriction ? { success: true } : { success: false, diff: { expected: restrictedSearchParams, received: request.searchParams } }; } async matchesRequestBodyRestrictions(request, restriction) { if (restriction.body === void 0) { return { success: true }; } const body = request.body; const restrictionBody = restriction.body; if (typeof body === "string" && typeof restrictionBody === "string") { const matchesRestriction2 = restriction.exact ? body === restrictionBody : body.includes(restrictionBody); return matchesRestriction2 ? { success: true } : { success: false, diff: { expected: restrictionBody, received: body } }; } if (restrictionBody instanceof http.HttpFormData) { if (!(body instanceof http.HttpFormData)) { return { success: false, diff: { expected: restrictionBody, received: body } }; } const matchesRestriction2 = restriction.exact ? await body.equals(restrictionBody) : await body.contains(restrictionBody); return matchesRestriction2 ? { success: true } : { success: false, diff: { expected: restrictionBody, received: body } }; } if (restrictionBody instanceof http.HttpSearchParams) { if (!(body instanceof http.HttpSearchParams)) { return { success: false, diff: { expected: restrictionBody, received: body } }; } const matchesRestriction2 = restriction.exact ? body.equals(restrictionBody) : body.contains(restrictionBody); return matchesRestriction2 ? { success: true } : { success: false, diff: { expected: restrictionBody, received: body } }; } if (restrictionBody instanceof Blob || restrictionBody instanceof ArrayBuffer || restrictionBody instanceof ReadableStream) { if (!(body instanceof Blob)) { return { success: false, diff: { expected: restrictionBody, received: body } }; } let restrictionBodyAsBlob; if (restrictionBody instanceof ArrayBuffer) { restrictionBodyAsBlob = convertArrayBufferToBlob(restrictionBody, { type: body.type }); } else if (restrictionBody instanceof ReadableStream) { restrictionBodyAsBlob = await convertReadableStreamToBlob( restrictionBody, { type: body.type } ); } else { restrictionBodyAsBlob = restrictionBody; } const matchesRestriction2 = await blobEquals_default(body, restrictionBodyAsBlob); return matchesRestriction2 ? { success: true } : { success: false, diff: { expected: restrictionBody, received: body } }; } const matchesRestriction = restriction.exact ? jsonEquals_default(request.body, restriction.body) : jsonContains_default(request.body, restriction.body); return matchesRestriction ? { success: true } : { success: false, diff: { expected: restrictionBody, received: body } }; } isComputedRequestRestriction(restriction) { return typeof restriction === "function"; } async applyResponseDeclaration(request) { if (this.createResponseDelay) { const delay = await this.createResponseDelay(request); if (delay > 0) { await waitForDelay_default(delay); } } const appliedDeclaration = await this.createResponseDeclaration?.(request); return appliedDeclaration; } saveInterceptedRequest(request, response) { const interceptedRequest = this.createInterceptedRequest(request, response); this._requests.push(interceptedRequest); this.interceptor.incrementNumberOfSavedRequests(1); } clearInterceptedRequests() { this.interceptor.incrementNumberOfSavedRequests(-this._requests.length); this._requests.length = 0; } createInterceptedRequest(request, response) { const interceptedRequest = request; Object.defineProperty(interceptedRequest, "response", { value: response, enumerable: true, configurable: false, writable: false }); return interceptedRequest; } get requests() { if (!this.interceptor.requestSaving.enabled) { throw new DisabledRequestSavingError_default(); } return this._requests; } }; var HttpRequestHandlerClient_default = HttpRequestHandlerClient; // src/http/requestHandler/LocalHttpRequestHandler.ts var LocalHttpRequestHandler = class { type = "local"; client; constructor(interceptor, method, path) { this.client = new HttpRequestHandlerClient_default(interceptor, method, path, this); } get method() { return this.client.method; } get path() { return this.client.path; } with(restriction) { this.client.with(restriction); return this; } delay(minMilliseconds, maxMilliseconds) { this.client.delay(minMilliseconds, maxMilliseconds); return this; } respond(declaration) { this.client.respond(declaration); const newThis = this; return newThis; } times(minNumberOfRequests, maxNumberOfRequests) { this.client.times(minNumberOfRequests, maxNumberOfRequests); return this; } checkTimes() { this.client.checkTimes(); } clear() { this.client.clear(); return this; } get requests() { return this.client.requests; } async matchesRequest(request) { const requestMatch = await this.client.matchesRequest(request); if (requestMatch.success) { this.client.markRequestAsMatched(request); } else if (requestMatch.cause === "unmatchedRestrictions") { this.client.markRequestAsUnmatched(request, { diff: requestMatch.diff }); } else { this.client.markRequestAsMatched(request); } return requestMatch; } async applyResponseDeclaration(request) { return this.client.applyResponseDeclaration(request); } saveInterceptedRequest(request, response) { this.client.saveInterceptedRequest(request, response); } }; var LocalHttpRequestHandler_default = LocalHttpRequestHandler; // ../zimic-utils/dist/chunk-VPHA4ZCK.mjs function createPathCharactersToEscapeRegex() { return /([.(){}+$])/g; } function preparePathForRegex(path) { const pathURLPrefix = `data:${path.startsWith("/") ? "" : "/"}`; const pathAsURL = new URL(`${pathURLPrefix}${path}`); const encodedPath = pathAsURL.href.replace(pathURLPrefix, ""); return encodedPath.replace(/^\/+/g, "").replace(/\/+$/g, "").replace(createPathCharactersToEscapeRegex(), "\\$1"); } function createPathParamRegex() { return /(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)(?!\\[*+?])/gu; } function createRepeatingPathParamRegex() { return /(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\\\+/gu; } function createOptionalPathParamRegex() { return /(?<leadingSlash>\/)?(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\?(?<trailingSlash>\/)?/gu; } function createOptionalRepeatingPathParamRegex() { return /(?<leadingSlash>\/)?(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\*(?<trailingSlash>\/)?/gu; } function createRegexFromPath(path) { const pathRegexContent = preparePathForRegex(path).replace( createOptionalRepeatingPathParamRegex(), (_match, leadingSlash, escape, identifier, trailingSlash) => { if (escape) { return `${leadingSlash ?? ""}:${identifier}\\*${trailingSlash ?? ""}`; } const hasSegmentBeforePrefix = leadingSlash === "/"; const prefixExpression = hasSegmentBeforePrefix ? "/?" : leadingSlash; const hasSegmentAfterSuffix = trailingSlash === "/"; const suffixExpression = hasSegmentAfterSuffix ? "/?" : trailingSlash; if (prefixExpression && suffixExpression) { return `(?:${prefixExpression}(?<${identifier}>.+?)?${suffixExpression})?`; } else if (prefixExpression) { return `(?:${prefixExpression}(?<${identifier}>.+?))?`; } else if (suffixExpression) { return `(?:(?<${identifier}>.+?)${suffixExpression})?`; } else { return `(?<${identifier}>.+?)?`; } } ).replace(createRepeatingPathParamRegex(), (_match, escape, identifier) => { return escape ? `:${identifier}\\+` : `(?<${identifier}>.+)`; }).replace( createOptionalPathParamRegex(), (_match, leadingSlash, escape, identifier, trailingSlash) => { if (escape) { return `${leadingSlash ?? ""}:${identifier}\\?${trailingSlash ?? ""}`; } const hasSegmentBeforePrefix = leadingSlash === "/"; const prefixExpression = hasSegmentBeforePrefix ? "/?" : leadingSlash; const hasSegmentAfterSuffix = trailingSlash === "/"; const suffixExpression = hasSegmentAfterSuffix ? "/?" : trailingSlash; if (prefixExpression && suffixExpression) { return `(?:${prefixExpression}(?<${identifier}>[^\\/]+?)?${suffixExpression})`; } else if (prefixExpression) { return `(?:${prefixExpression}(?<${identifier}>[^\\/]+?))?`; } else if (suffixExpression) { return `(?:(?<${identifier}>[^\\/]+?)${suffixExpression})?`; } else { return `(?<${identifier}>[^\\/]+?)?`; } } ).replace(createPathParamRegex(), (_match, escape, identifier) => { return escape ? `:${identifier}` : `(?<${identifier}>[^\\/]+?)`; }); return new RegExp(`^/?${pathRegexContent}/?$`); } var createRegexFromPath_default = createRegexFromPath; // ../zimic-utils/dist/url/excludeNonPathParams.mjs function excludeNonPathParams(url) { url.hash = ""; url.search = ""; url.username = ""; url.password = ""; return url; } var excludeNonPathParams_default = excludeNonPathParams; // ../zimic-utils/dist/url/validateURLProtocol.mjs var UnsupportedURLProtocolError = class extends TypeError { constructor(protocol, availableProtocols) { super( `Unsupported URL protocol: '${protocol}'. The available options are ${availableProtocols.map((protocol2) => `'${protocol2}'`).join(", ")}` ); this.name = "UnsupportedURLProtocolError"; } }; function validateURLProtocol(url, protocols) { const protocol = url.protocol.replace(/:$/, ""); if (!protocols.includes(protocol)) { throw new UnsupportedURLProtocolError(protocol, protocols); } } var validateURLProtocol_default = validateURLProtocol; // src/utils/arrays.ts function removeArrayIndex(array, index) { if (index >= 0 && index < array.length) { array.splice(index, 1); } return array; } function removeArrayElement(array, element) { const index = array.indexOf(element); return removeArrayIndex(array, index); } // src/utils/http.ts var HTTP_METHODS_WITH_RESPONSE_BODY = /* @__PURE__ */ new Set([ "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" ]); function methodCanHaveResponseBody(method) { return HTTP_METHODS_WITH_RESPONSE_BODY.has(method); } // src/http/requestHandler/types/requests.ts var HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES = Object.freeze( /* @__PURE__ */ new Set([ "bodyUsed", "arrayBuffer", "blob", "formData", "json", "text", "clone" ]) ); var HTTP_INTERCEPTOR_RESPONSE_HIDDEN_PROPERTIES = Object.freeze( new Set( HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES ) ); // src/http/interceptorWorker/constants.ts var DEFAULT_UNHANDLED_REQUEST_STRATEGY = Object.freeze({ local: Object.freeze({ action: "reject", log: true }), remote: Object.freeze({ action: "reject", log: true }) }); // src/http/interceptorWorker/HttpInterceptorWorker.ts var HttpInterceptorWorker = class _HttpInterceptorWorker { platform = null; isRunning = false; startingPromise; stoppingPromise; runningInterceptors = []; async sharedStart(internalStart) { if (this.isRunning) { return; } if (this.startingPromise) { return this.startingPromise; } try { this.startingPromise = internalStart(); await this.startingPromise; this.startingPromise = void 0; } catch (error) { if (!isClientSide()) { console.error(error); } await this.stop(); throw error; } } async sharedStop(internalStop) { if (!this.isRunning) { return; } if (this.stoppingPromise) { return this.stoppingPromise; } const stoppingResult = internalStop(); if (stoppingResult instanceof Promise) { this.stoppingPromise = stoppingResult; await this.stoppingPromise; } this.stoppingPromise = void 0; } async logUnhandledRequestIfNecessary(request, strategy) { if (strategy?.log) { await _HttpInterceptorWorker.logUnhandledRequestWarning(request, strategy.action); return { wasLogged: true }; } return { wasLogged: false }; } async getUnhandledRequestStrategy(request, interceptorType) { const candidates = await this.getUnhandledRequestStrategyCandidates(request, interceptorType); const strategy = this.reduceUnhandledRequestStrategyCandidates(candidates); return strategy; } reduceUnhandledRequestStrategyCandidates(candidateStrategies) { if (candidateStrategies.length === 0) { return null; } return candidateStrategies.reduce( (accumulatedStrategy, candidateStrategy) => ({ action: accumulatedStrategy.action, log: accumulatedStrategy.log ?? candidateStrategy.log }) ); } async getUnhandledRequestStrategyCandidates(request, interceptorType) { const globalDefaultStrategy = DEFAULT_UNHANDLED_REQUEST_STRATEGY[interceptorType]; try { const interceptor = this.findInterceptorByRequestBaseURL(request); if (!interceptor) { return []; } const requestClone = request.clone(); const interceptorStrategy = await this.getInterceptorUnhandledRequestStrategy(requestClone, interceptor); return [interceptorStrategy, globalDefaultStrategy].filter(isDefined_default); } catch (error) { console.error(error); return [globalDefaultStrategy]; } } registerRunningInterceptor(interceptor) { this.runningInterceptors.push(interceptor); } unregisterRunningInterceptor(interceptor) { removeArrayElement(this.runningInterceptors, interceptor); } findInterceptorByRequestBaseURL(request) { const interceptor = this.runningInterceptors.findLast((interceptor2) => { return request.url.startsWith(interceptor2.baseURLAsString); }); return interceptor; } async getInterceptorUnhandledRequestStrategy(request, interceptor) { if (typeof interceptor.onUnhandledRequest === "function") { const parsedRequest = await _HttpInterceptorWorker.parseRawUnhandledRequest(request); return interceptor.onUnhandledRequest(parsedRequest); } return interceptor.onUnhandledRequest; } static createResponseFromDeclaration(request, declaration) { const headers = new http.HttpHeaders(declaration.headers); const status = declaration.status; const canHaveBody = methodCanHaveResponseBody(request.method) && status !== 204; if (!canHaveBody) { return new Response(null, { headers, status }); } if (typeof declaration.body === "string" || declaration.body === null || declaration.body === void 0 || declaration.body instanceof FormData || declaration.body instanceof URLSearchParams || declaration.body instanceof Blob || declaration.body instanceof ArrayBuffer || declaration.body instanceof ReadableStream) { return new Response(declaration.body ?? null, { headers, status }); } return Response.json(declaration.body, { headers, status }); } static async parseRawUnhandledRequest(request) { return this.parseRawRequest( request ); } static async parseRawRequest(originalRawRequest, options) { const rawRequest = originalRawRequest.clone(); const rawRequestClone = rawRequest.clone(); const parsedBody = await http.parseHttpBody(rawRequest).catch((error) => { logger.error("Failed to parse request body:", error); return null; }); const headers = new http.HttpHeaders(rawRequest.headers); const pathParams = this.parseRawPathParams(rawRequest, options); const parsedURL = new URL(rawRequest.url); const searchParams = new http.HttpSearchParams(parsedURL.searchParams); const parsedRequest = new Proxy(rawRequest, { has(target, property) { if (_HttpInterceptorWorker.isHiddenRequestProperty(property)) { return false; } return Reflect.has(target, property); }, get(target, property) { if (_HttpInterceptorWorker.isHiddenRequestProperty(property)) { return void 0; } return Reflect.get(target, property, target); } }); Object.defineProperty(parsedRequest, "body", { value: parsedBody, enumerable: true, configurable: false, writable: false }); Object.defineProperty(parsedRequest, "headers", { value: headers, enumerable: true, configurable: false, writable: false }); Object.defineProperty(parsedRequest, "pathParams", { value: pathParams, enumerable: true, configurable: false, writable: false }); Object.defineProperty(parsedRequest, "searchParams", { value: searchParams, enumerable: true, configurable: false, writable: false }); Object.defineProperty(parsedRequest, "raw", { value: rawRequestClone, enumerable: true, configurable: false, writable: false }); return parsedRequest; } static isHiddenRequestProperty(property) { return HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES.has(property); } static async parseRawResponse(originalRawResponse) { const rawResponse = originalRawResponse.clone(); const rawResponseClone = rawResponse.clone(); const parsedBody = await http.parseHttpBody(rawResponse).catch((error) => { logger.error("Failed to parse response body:", error); return null; }); const headers = new http.HttpHeaders(rawResponse.headers); const parsedRequest = new Proxy(rawResponse, { has(target, property) { if (_HttpInterceptorWorker.isHiddenResponseProperty(property)) { return false; } return Reflect.has(target, property); }, get(target, property) { if (_HttpInterceptorWorker.isHiddenResponseProperty(property)) { return void 0; } return Reflect.get(target, property, target); } }); Object.defineProperty(parsedRequest, "body", { value: parsedBody, enumerable: true, configurable: false, writable: false }); Object.defineProperty(parsedRequest, "headers", { value: headers, enumerable: true, configurable: false, writable: false }); Object.defineProperty(parsedRequest, "raw", { value: rawResponseClone, enumerable: true, configurable: false, writable: false }); return parsedRequest; } static isHiddenResponseProperty(property) { return HTTP_INTERCEPTOR_RESPONSE_HIDDEN_PROPERTIES.has(property); } static parseRawPathParams(request, options) { const requestPath = request.url.replace(options?.baseURL ?? "", ""); const paramsMatch = options?.pathRegex.exec(requestPath); const params = {}; for (const [paramName, paramValue] of Object.entries(paramsMatch?.groups ?? {})) { params[paramName] = typeof paramValue === "string" ? decodeURIComponent(paramValue) : void 0; } return params; } static async logUnhandledRequestWarning(rawRequest, action) { const request = await this.parseRawRequest(rawRequest); const [formattedHeaders, formattedSearchParams, formattedBody] = await Promise.all([ formatValueToLog(request.headers.toObject()), formatValueToLog(request.searchParams.toObject()), formatValueToLog(request.body) ]); logger[action === "bypass" ? "warn" : "error"]( `${action === "bypass" ? "Warning:" : "Error:"} Request was not handled and was ${action === "bypass" ? color2__default.default.yellow("bypassed") : color2__default.default.red("rejected")}. `, `${request.method} ${request.url}`, "\n Headers:", formattedHeaders, "\n Search params:", formattedSearchParams, "\n Body:", formattedBody, "\n\nLearn more: https://zimic.dev/docs/interceptor/guides/http/unhandled-requests" ); } }; var HttpInterceptorWorker_default = HttpInterceptorWorker; // src/http/requestHandler/RemoteHttpRequestHandler.ts var PENDING_PROPERTIES = /* @__PURE__ */ new Set(["then"]); var RemoteHttpRequestHandler = class { type = "remote"; client; syncPromises = []; unsynced; synced; constructor(interceptor, method, path) { this.client = new HttpRequestHandlerClient_default(interceptor, method, path, this); this.unsynced = this; this.synced = this.createSyncedProxy(); } 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 void 0; } return Reflect.get(target, property); } }); } isHiddenPropertyWhenSynced(property) { return PENDING_PROPERTIES.has(property); } get method() { return this.client.method; } get path() { return this.client.path; } with(restriction) { this.client.with(restriction); return this.unsynced; } delay(minMilliseconds, maxMilliseconds) { this.client.delay(minMilliseconds, maxMilliseconds); return this.unsynced; } respond(declaration) { const newUnsyncedThis = this.unsynced; newUnsyncedThis.client.respond(declaration); return newUnsyncedThis; } times(minNumberOfRequests, maxNumberOfRequests) { this.client.times(minNumberOfRequests, maxNumberOfRequests); return this; } async checkTimes() { return new Promise((resolve, reject) => { try { this.client.checkTimes(); resolve(); } catch (error) { reject(error); } }); } clear() { this.client.clear(); return this.unsynced; } get requests() { return this.client.requests; } async matchesRequest(request) { const requestMatch = await this.client.matchesRequest(request); if (requestMatch.success) { this.client.markRequestAsMatched(request); } else if (requestMatch.cause === "unmatchedRestrictions") { this.client.markRequestAsUnmatched(request, { diff: requestMatch.diff }); } else { this.client.markRequestAsMatched(request); } return requestMatch; } async applyResponseDeclaration(request) { return this.client.applyResponseDeclaration(request); } saveInterceptedRequest(request, response) { this.client.saveInterceptedRequest(request, response); } registerSyncPromise(promise) { this.syncPromises.push(promise); } get isSynced() { return this.syncPromises.length === 0; } then(onFulfilled, onRejected) { const promisesToWait = new Set(this.syncPromises); return Promise.all(promisesToWait).then(() => { this.syncPromises = this.syncPromises.filter((promise) => !promisesToWait.has(promise)); r