| import type { Options, Definition } from './lib/scope.ts'; | ||
| import type { BackOptions, BackContext, BackMode, Back, InterceptorSurface } from './lib/back.ts'; | ||
| import type { RecorderOptions } from './lib/recorder.ts'; | ||
| import type { InterceptorMatchResult } from './lib/global_emitter.ts'; | ||
| import type { DataMatcher, DataMatcherArray, DataMatcherMap, RequestBodyMatcher, RequestHeaderMatcher, Body, ReplyBody, ReplyHeaderFunction, ReplyHeaderValue, ReplyHeaders, StatusCode, ReplyFnResult, Interceptor } from './lib/interceptor.ts'; | ||
| import type { Scope } from './lib/scope.ts'; | ||
| import { removeAll } from './lib/intercept.ts'; | ||
| declare function nock(basePath: string | RegExp | URL, options?: Options): Scope; | ||
| declare namespace nock { | ||
| var activate: typeof import("./lib/intercept.ts").activate; | ||
| var isActive: () => boolean; | ||
| var isDone: typeof import("./lib/intercept.ts").isDone; | ||
| var pendingMocks: typeof import("./lib/intercept.ts").pendingMocks; | ||
| var activeMocks: typeof import("./lib/intercept.ts").activeMocks; | ||
| var removeInterceptor: typeof import("./lib/intercept.ts").removeInterceptor; | ||
| var disableNetConnect: typeof import("./lib/intercept.ts").disableNetConnect; | ||
| var enableNetConnect: typeof import("./lib/intercept.ts").enableNetConnect; | ||
| var cleanAll: typeof removeAll; | ||
| var abortPendingRequests: typeof import("./lib/common.ts").removeAllTimers; | ||
| var load: typeof import("./lib/scope.ts").load; | ||
| var loadDefs: typeof import("./lib/scope.ts").loadDefs; | ||
| var define: typeof import("./lib/scope.ts").define; | ||
| var emitter: import("events")<{ | ||
| 'no match': [req: Request, interceptorResults?: InterceptorMatchResult[]]; | ||
| }>; | ||
| var recorder: { | ||
| rec: typeof import("./lib/recorder.ts").record; | ||
| clear: typeof import("./lib/recorder.ts").clear; | ||
| play: typeof import("./lib/recorder.ts").outputs; | ||
| }; | ||
| var restore: typeof import("./lib/recorder.ts").restore; | ||
| var back: typeof import("./lib/back.ts").default; | ||
| var getGetRequestBody: typeof import("./lib/utils/node/index.ts").getGetRequestBody; | ||
| } | ||
| export default nock; | ||
| declare namespace nock { | ||
| export type { Scope }; | ||
| export type { Interceptor }; | ||
| export type { Options }; | ||
| export type { Definition }; | ||
| export type { BackMode }; | ||
| export type { BackContext }; | ||
| export type { BackOptions }; | ||
| export type { RecorderOptions }; | ||
| export type { InterceptorSurface }; | ||
| export type { InterceptorMatchResult }; | ||
| export type { DataMatcher }; | ||
| export type { DataMatcherArray }; | ||
| export type { DataMatcherMap }; | ||
| export type { RequestBodyMatcher }; | ||
| export type { RequestHeaderMatcher }; | ||
| export type { Body }; | ||
| export type { ReplyBody }; | ||
| export type { ReplyHeaderFunction }; | ||
| export type { ReplyHeaderValue }; | ||
| export type { ReplyHeaders }; | ||
| export type { StatusCode }; | ||
| export type { ReplyFnResult }; | ||
| export type { Back }; | ||
| } |
+109
| import back from './lib/back.js' | ||
| import emitter from './lib/global_emitter.js' | ||
| import { | ||
| activate, | ||
| isActive, | ||
| isDone, | ||
| isOn, | ||
| pendingMocks, | ||
| activeMocks, | ||
| removeInterceptor, | ||
| disableNetConnect, | ||
| enableNetConnect, | ||
| removeAll, | ||
| abortPendingRequests, | ||
| } from './lib/intercept.js' | ||
| import * as recorder from './lib/recorder.js' | ||
| import { Scope as ScopeClass, load, loadDefs, define } from './lib/scope.js' | ||
| import { getGetRequestBody } from './lib/utils/node/index.js' | ||
| function nock(basePath , options ) { | ||
| return new ScopeClass(basePath, options) | ||
| } | ||
| nock.activate = activate | ||
| nock.isActive = isActive | ||
| nock.isDone = isDone | ||
| nock.pendingMocks = pendingMocks | ||
| nock.activeMocks = activeMocks | ||
| nock.removeInterceptor = removeInterceptor | ||
| nock.disableNetConnect = disableNetConnect | ||
| nock.enableNetConnect = enableNetConnect | ||
| nock.cleanAll = removeAll | ||
| nock.abortPendingRequests = abortPendingRequests | ||
| nock.load = load | ||
| nock.loadDefs = loadDefs | ||
| nock.define = define | ||
| nock.emitter = emitter | ||
| nock.recorder = { | ||
| rec: recorder.record, | ||
| clear: recorder.clear, | ||
| play: recorder.outputs, | ||
| } | ||
| nock.restore = recorder.restore | ||
| nock.back = back | ||
| nock.getGetRequestBody = getGetRequestBody | ||
| export default nock | ||
| // Re-export types into the nock namespace so consumers can use nock.Scope, nock.Options, etc. | ||
| // We always activate Nock on import, overriding the globals. | ||
| // Setting the Back mode "activates" Nock by overriding the global entries in the `http/s` modules. | ||
| // If Nock Back is configured, we need to honor that setting for backward compatibility, | ||
| // otherwise we rely on Nock Back's default initializing side effect. | ||
| if (isOn()) { | ||
| back.setMode(process.env.NOCK_BACK_MODE || 'dryrun') | ||
| } |
| import type { Scope } from './scope.ts'; | ||
| import type { Definition } from './scope.ts'; | ||
| import type { RecorderOptions } from './recorder.ts'; | ||
| export type BackMode = 'wild' | 'dryrun' | 'record' | 'update' | 'lockdown'; | ||
| export interface BackOptions { | ||
| before?: (def: Definition) => void; | ||
| after?: (scope: Scope) => void; | ||
| afterRecord?: (defs: Definition[]) => Definition[] | string; | ||
| recorder?: RecorderOptions; | ||
| } | ||
| declare namespace Back { | ||
| let fixtures: string | null; | ||
| let currentMode: string; | ||
| } | ||
| declare function Back(fixtureName: string, nockedFn: (nockDone: () => void) => void): void; | ||
| declare function Back(fixtureName: string, options: BackOptions, nockedFn: (nockDone: () => void) => void): void; | ||
| declare function Back(fixtureName: string, options?: BackOptions): Promise<{ | ||
| nockDone: () => void; | ||
| context: BackContext; | ||
| }>; | ||
| declare namespace Back { | ||
| var setMode: (mode: string) => void; | ||
| } | ||
| declare function load(fixture?: string, options?: BackOptions | Record<string, any>): { | ||
| isLoaded: boolean; | ||
| isRecording: boolean; | ||
| scopes: Scope[]; | ||
| assertScopesFinished: () => void; | ||
| query: () => { | ||
| method: string; | ||
| uri: string | RegExp | ((path: string) => boolean); | ||
| basePath: string | RegExp; | ||
| path: string | RegExp | ((path: string) => boolean); | ||
| queries: boolean | Record<string, any> | ((queryObject: Record<string, any>) => boolean) | null; | ||
| counter: number; | ||
| body: any; | ||
| statusCode: number | null | undefined; | ||
| optional: boolean; | ||
| }[]; | ||
| }; | ||
| export type BackContext = Omit<ReturnType<typeof load>, 'isRecording'>; | ||
| export type InterceptorSurface = ReturnType<BackContext['query']>[number]; | ||
| export type Back = typeof Back; | ||
| export default Back; |
+371
| import fs from 'node:fs' | ||
| import assert from 'node:assert' | ||
| import * as recorder from './recorder.js' | ||
| import { | ||
| activate, | ||
| disableNetConnect, | ||
| enableNetConnect, | ||
| removeAll as cleanAll, | ||
| } from './intercept.js' | ||
| import { loadDefs, define } from './scope.js' | ||
| import { back as debug } from './debug.js' | ||
| import { format } from 'node:util' | ||
| import path from 'node:path' | ||
| let _mode = null | ||
| function Back( | ||
| fixtureName , | ||
| options , | ||
| nockedFn , | ||
| ) { | ||
| if (!Back.fixtures) { | ||
| throw new Error( | ||
| 'Back requires nock.back.fixtures to be set\n' + | ||
| 'Ex:\n' + | ||
| "\trequire(nock).back.fixtures = '/path/to/fixtures/'", | ||
| ) | ||
| } | ||
| if (typeof fixtureName !== 'string') { | ||
| throw new Error('Parameter fixtureName must be a string') | ||
| } | ||
| if (arguments.length === 1) { | ||
| options = {} | ||
| } else if (arguments.length === 2) { | ||
| // If 2nd parameter is a function then `options` has been omitted | ||
| // otherwise `options` haven't been omitted but `nockedFn` was. | ||
| if (typeof options === 'function') { | ||
| nockedFn = options | ||
| options = {} | ||
| } | ||
| } | ||
| ;(_mode ).setup() | ||
| const fixture = path.join(Back.fixtures , fixtureName) | ||
| const context = (_mode ).start(fixture, options) | ||
| const nockDone = function () { | ||
| ;(_mode ).finish(fixture, options, context) | ||
| } | ||
| debug('context:', context) | ||
| // If nockedFn is a function then invoke it, otherwise return a promise resolving to nockDone. | ||
| if (typeof nockedFn === 'function') { | ||
| nockedFn.call(context, nockDone) | ||
| } else { | ||
| return Promise.resolve({ nockDone, context }) | ||
| } | ||
| } | ||
| /******************************************************************************* | ||
| * Modes * | ||
| *******************************************************************************/ | ||
| const wild = { | ||
| setup: function () { | ||
| cleanAll() | ||
| recorder.restore() | ||
| activate() | ||
| enableNetConnect() | ||
| }, | ||
| start: function () { | ||
| return load(undefined, undefined) // don't load anything but get correct context | ||
| }, | ||
| finish: function () { | ||
| // nothing to do | ||
| }, | ||
| } | ||
| const dryrun = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| cleanAll() | ||
| activate() | ||
| // We have to explicitly enable net connectivity as by default it's off. | ||
| enableNetConnect() | ||
| }, | ||
| start: function (fixture , options ) { | ||
| const contexts = load(fixture, options) | ||
| enableNetConnect() | ||
| return contexts | ||
| }, | ||
| finish: function () { | ||
| // nothing to do | ||
| }, | ||
| } | ||
| const record = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| recorder.clear() | ||
| cleanAll() | ||
| activate() | ||
| disableNetConnect() | ||
| }, | ||
| start: function (fixture , options ) { | ||
| if (!fs) { | ||
| throw new Error('no fs') | ||
| } | ||
| const context = load(fixture, options) | ||
| if (!context.isLoaded) { | ||
| recorder.record({ | ||
| dont_print: true, | ||
| output_objects: true, | ||
| ...options.recorder, | ||
| }) | ||
| context.isRecording = true | ||
| } | ||
| return context | ||
| }, | ||
| finish: function ( | ||
| fixture , | ||
| options , | ||
| context , | ||
| ) { | ||
| if (context.isRecording) { | ||
| let outputs = recorder.outputs() | ||
| if (typeof options.afterRecord === 'function') { | ||
| outputs = options.afterRecord(outputs) | ||
| } | ||
| const data = | ||
| typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4) | ||
| debug('recorder outputs:', data) | ||
| fs.mkdirSync(path.dirname(fixture), { recursive: true }) | ||
| fs.writeFileSync(fixture, data) | ||
| } | ||
| }, | ||
| } | ||
| const update = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| recorder.clear() | ||
| cleanAll() | ||
| activate() | ||
| disableNetConnect() | ||
| }, | ||
| start: function (fixture , options ) { | ||
| if (!fs) { | ||
| throw new Error('no fs') | ||
| } | ||
| const context = removeFixture(fixture) | ||
| recorder.record({ | ||
| dont_print: true, | ||
| output_objects: true, | ||
| ...options.recorder, | ||
| }) | ||
| context.isRecording = true | ||
| return context | ||
| }, | ||
| finish: function (fixture , options ) { | ||
| let outputs = recorder.outputs() | ||
| if (typeof options.afterRecord === 'function') { | ||
| outputs = options.afterRecord(outputs) | ||
| } | ||
| const data = | ||
| typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4) | ||
| debug('recorder outputs:', data) | ||
| fs.mkdirSync(path.dirname(fixture), { recursive: true }) | ||
| fs.writeFileSync(fixture, data) | ||
| }, | ||
| } | ||
| const lockdown = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| recorder.clear() | ||
| cleanAll() | ||
| activate() | ||
| disableNetConnect() | ||
| }, | ||
| start: function (fixture , options ) { | ||
| return load(fixture, options) | ||
| }, | ||
| finish: function () { | ||
| // nothing to do | ||
| }, | ||
| } | ||
| function load(fixture , options ) { | ||
| const context = { | ||
| isLoaded: false, | ||
| isRecording: false, | ||
| scopes: [] , | ||
| assertScopesFinished: function () { | ||
| assertScopes(this.scopes, fixture) | ||
| }, | ||
| query: function () { | ||
| return this.scopes.flatMap((scope ) => | ||
| scope.interceptors.map((interceptor ) => ({ | ||
| method: interceptor.method, | ||
| uri: interceptor.uri, | ||
| basePath: interceptor.basePath, | ||
| path: interceptor.path, | ||
| queries: interceptor.queries, | ||
| counter: interceptor.counter, | ||
| body: interceptor.body, | ||
| statusCode: interceptor.statusCode, | ||
| optional: interceptor.optional, | ||
| })), | ||
| ) | ||
| }, | ||
| } | ||
| if (fixture && fixtureExists(fixture)) { | ||
| let scopes = loadDefs(fixture) | ||
| applyHook(scopes, options?.before) | ||
| scopes = define(scopes) | ||
| applyHook(scopes, options?.after) | ||
| context.scopes = scopes | ||
| context.isLoaded = true | ||
| } | ||
| return context | ||
| } | ||
| function removeFixture(fixture ) { | ||
| const context = { | ||
| scopes: [] , | ||
| assertScopesFinished: () => {}, | ||
| isLoaded: false, | ||
| isRecording: false, | ||
| } | ||
| if (fixture && fixtureExists(fixture)) { | ||
| /* istanbul ignore next - fs.unlinkSync is for node 10 support */ | ||
| fs.rmSync ? fs.rmSync(fixture) : fs.unlinkSync(fixture) | ||
| } | ||
| return context | ||
| } | ||
| function applyHook(items , fn ) { | ||
| if (!fn) { | ||
| return | ||
| } | ||
| if (typeof fn !== 'function') { | ||
| throw new Error('processing hooks must be a function') | ||
| } | ||
| items.forEach(fn ) | ||
| } | ||
| function fixtureExists(fixture ) { | ||
| if (!fs) { | ||
| throw new Error('no fs') | ||
| } | ||
| return fs.existsSync(fixture) | ||
| } | ||
| function assertScopes(scopes , fixture ) { | ||
| const pending = scopes | ||
| .filter((scope ) => !scope.isDone()) | ||
| .map((scope ) => scope.pendingMocks()) | ||
| if (pending.length) { | ||
| assert.fail( | ||
| format( | ||
| '%j was not used, consider removing %s to rerecord fixture', | ||
| ([] ).concat(...pending), | ||
| fixture, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| const Modes = { | ||
| wild, // all requests go out to the internet, dont replay anything, doesnt record anything | ||
| dryrun, // use recorded nocks, allow http calls, doesnt record anything, useful for writing new tests (default) | ||
| record, // use recorded nocks, record new nocks | ||
| update, // allow http calls, record all nocks, don't use recorded nocks | ||
| lockdown, // use recorded nocks, disables all http calls even when not nocked, doesnt record | ||
| } | ||
| Back.setMode = function (mode ) { | ||
| if (!(mode in Modes)) { | ||
| throw new Error(`Unknown mode: ${mode}`) | ||
| } | ||
| Back.currentMode = mode | ||
| debug('New nock back mode:', Back.currentMode) | ||
| _mode = Modes[mode ] | ||
| _mode.setup() | ||
| } | ||
| Back.fixtures = null | ||
| Back.currentMode = '' | ||
| export default Back |
| declare function normalizeRequestOptions(options: Record<string, any>): Record<string, any>; | ||
| declare function isUtf8Representable(buffer: ArrayBuffer | Buffer): boolean; | ||
| declare function normalizeOrigin(url: URL): string; | ||
| declare function stringifyRequest(request: Request, body: string): string; | ||
| declare function isContentEncoded(headers: Record<string, any>): boolean; | ||
| declare function contentEncoding(headers: Headers, encoder: 'gzip' | 'deflate'): boolean; | ||
| declare function isJSONContent(headers: Headers): boolean; | ||
| declare function headersFieldNamesToLowerCase(headers: Record<string, any>, throwOnDuplicate?: boolean): Record<string, any>; | ||
| declare const headersFieldsArrayToLowerCase: (headers: string[]) => string[]; | ||
| declare function headersInputToRawArray(headers?: any[] | Map<string, any> | Record<string, any>): any[]; | ||
| declare function headersArrayToObject(rawHeaders: any[]): Record<string, any>; | ||
| declare function deleteHeadersField(headers: Record<string, any>, fieldNameToDelete: string): void; | ||
| declare function forEachHeader(rawHeaders: any[], callback: (value: any, fieldName: any, index: number) => void): void; | ||
| declare function percentDecode(str: string): string; | ||
| declare function percentEncode(str: string): string; | ||
| declare function matchStringOrRegexp(target: string | null | undefined, pattern: string | RegExp): boolean; | ||
| declare function formatQueryValue(key: string, value: any, stringFormattingFn?: (s: string) => string): [string, any]; | ||
| declare function isStream(obj: any): any; | ||
| declare const dataEqual: (expected: any, actual: any) => boolean; | ||
| declare const setTimeout: (callback: (...args: any[]) => any, ...timerArgs: any[]) => any; | ||
| declare const setImmediate: (callback: (...args: any[]) => any, ...timerArgs: any[]) => any; | ||
| declare function removeAllTimers(): void; | ||
| declare function isPlainObject(value: any): boolean; | ||
| declare const expand: (input: Record<string, any> | null | undefined) => Record<string, any> | null | undefined; | ||
| declare function decompressRequestBody(buffer: ArrayBuffer, contentEncoding: string): ArrayBuffer | NonSharedBuffer; | ||
| declare function convertHeadersToRaw(headers: Headers): string[]; | ||
| export { contentEncoding, dataEqual, deleteHeadersField, expand, forEachHeader, formatQueryValue, headersArrayToObject, headersFieldNamesToLowerCase, headersFieldsArrayToLowerCase, headersInputToRawArray, isContentEncoded, isJSONContent, isPlainObject, isStream, isUtf8Representable, matchStringOrRegexp, normalizeOrigin, normalizeRequestOptions, percentDecode, percentEncode, removeAllTimers, setImmediate, setTimeout, stringifyRequest, decompressRequestBody, convertHeadersToRaw, }; |
| import { common as debug } from './debug.js' | ||
| import timers from 'node:timers' | ||
| import util from 'node:util' | ||
| import zlib from 'node:zlib' | ||
| function normalizeRequestOptions(options ) { | ||
| options.proto = options.proto || 'http' | ||
| options.port = options.port || (options.proto === 'http' ? 80 : 443) | ||
| if (options.host) { | ||
| debug('options.host:', options.host) | ||
| if (!options.hostname) { | ||
| if (options.host.split(':').length === 2) { | ||
| options.hostname = options.host.split(':')[0] | ||
| } else { | ||
| options.hostname = options.host | ||
| } | ||
| } | ||
| } | ||
| debug('options.hostname in the end: %j', options.hostname) | ||
| options.host = `${options.hostname || 'localhost'}:${options.port}` | ||
| debug('options.host in the end: %j', options.host) | ||
| /// lowercase host names | ||
| ;['hostname', 'host'].forEach(function (attr) { | ||
| if (options[attr]) { | ||
| options[attr] = options[attr].toLowerCase() | ||
| } | ||
| }) | ||
| return options | ||
| } | ||
| function isUtf8Representable(buffer ) { | ||
| try { | ||
| new TextDecoder('utf8', { fatal: true }).decode(buffer) | ||
| return true | ||
| } catch { | ||
| return false | ||
| } | ||
| } | ||
| function normalizeOrigin(url ) { | ||
| // Remove brackets from hostname if IPV6 | ||
| const normalizedOrigin = url.hostname.startsWith('[') | ||
| ? `${url.protocol}//${url.hostname.slice(1, -1)}${url.port ? `:${url.port}` : ''}` | ||
| : url.origin | ||
| if (url.port) { | ||
| return normalizedOrigin | ||
| } else { | ||
| return normalizedOrigin + (url.protocol === 'http:' ? ':80' : ':443') | ||
| } | ||
| } | ||
| function stringifyRequest(request , body ) { | ||
| const url = new URL(request.url) | ||
| const log = { | ||
| method: request.method, | ||
| url: `${url.origin}${url.pathname}`, | ||
| headers: Object.fromEntries(request.headers.entries()), | ||
| } | ||
| if (body) { | ||
| log.body = body | ||
| } | ||
| return JSON.stringify(log, null, 2) | ||
| } | ||
| function isContentEncoded(headers ) { | ||
| const contentEncoding = headers['content-encoding'] | ||
| return typeof contentEncoding === 'string' && contentEncoding !== '' | ||
| } | ||
| function contentEncoding(headers , encoder ) { | ||
| const contentEncoding = headers.get('content-encoding') | ||
| return contentEncoding?.toString() === encoder | ||
| } | ||
| function isJSONContent(headers ) { | ||
| // https://tools.ietf.org/html/rfc8259 | ||
| const contentType = String(headers.get('content-type') || '').toLowerCase() | ||
| return contentType.startsWith('application/json') | ||
| } | ||
| function headersFieldNamesToLowerCase( | ||
| headers , | ||
| throwOnDuplicate , | ||
| ) { | ||
| if (!isPlainObject(headers)) { | ||
| throw Error('Headers must be provided as an object') | ||
| } | ||
| const lowerCaseHeaders = {} | ||
| Object.entries(headers).forEach(([fieldName, fieldValue]) => { | ||
| const key = fieldName.toLowerCase() | ||
| if (lowerCaseHeaders[key] !== undefined) { | ||
| if (throwOnDuplicate) { | ||
| throw Error( | ||
| `Failed to convert header keys to lower case due to field name conflict: ${key}`, | ||
| ) | ||
| } else { | ||
| debug( | ||
| `Duplicate header provided in request: ${key}. Only the last value can be matched.`, | ||
| ) | ||
| } | ||
| } | ||
| lowerCaseHeaders[key] = fieldValue | ||
| }) | ||
| return lowerCaseHeaders | ||
| } | ||
| const headersFieldsArrayToLowerCase = (headers ) => [ | ||
| ...new Set(headers.map(fieldName => fieldName.toLowerCase())), | ||
| ] | ||
| function headersInputToRawArray( | ||
| headers , | ||
| ) { | ||
| if (headers === undefined) { | ||
| return [] | ||
| } | ||
| if (Array.isArray(headers)) { | ||
| // If the input is an array, assume it's already in the raw format and simply return a copy | ||
| // but throw an error if there aren't an even number of items in the array | ||
| if (headers.length % 2) { | ||
| throw new Error( | ||
| `Raw headers must be provided as an array with an even number of items. [fieldName, value, ...]`, | ||
| ) | ||
| } | ||
| return [...headers] | ||
| } | ||
| // [].concat(...) is used instead of Array.flat until v11 is the minimum Node version | ||
| if (util.types.isMap(headers)) { | ||
| return ([] ).concat( | ||
| ...Array.from(headers , ([k, v]) => [k.toString(), v]), | ||
| ) | ||
| } | ||
| if (isPlainObject(headers)) { | ||
| return ([] ).concat(...Object.entries(headers)) | ||
| } | ||
| throw new Error( | ||
| `Headers must be provided as an array of raw values, a Map, or a plain Object. ${headers}`, | ||
| ) | ||
| } | ||
| function headersArrayToObject(rawHeaders ) { | ||
| if (!Array.isArray(rawHeaders)) { | ||
| throw Error('Expected a header array') | ||
| } | ||
| const accumulator = {} | ||
| forEachHeader(rawHeaders, (value, fieldName) => { | ||
| addHeaderLine(accumulator, fieldName, value) | ||
| }) | ||
| return accumulator | ||
| } | ||
| const noDuplicatesHeaders = new Set([ | ||
| 'age', | ||
| 'authorization', | ||
| 'content-length', | ||
| 'content-type', | ||
| 'etag', | ||
| 'expires', | ||
| 'from', | ||
| 'host', | ||
| 'if-modified-since', | ||
| 'if-unmodified-since', | ||
| 'last-modified', | ||
| 'location', | ||
| 'max-forwards', | ||
| 'proxy-authorization', | ||
| 'referer', | ||
| 'retry-after', | ||
| 'user-agent', | ||
| ]) | ||
| function addHeaderLine( | ||
| headers , | ||
| name , | ||
| value , | ||
| ) { | ||
| let values // code below expects `values` to be an array of strings | ||
| if (typeof value === 'function') { | ||
| // Function values are evaluated towards the end of the response, before that we use a placeholder | ||
| // string just to designate that the header exists. Useful when `Content-Type` is set with a function. | ||
| values = [value.name] | ||
| } else if (Array.isArray(value)) { | ||
| values = value.map(String) | ||
| } else { | ||
| values = [String(value)] | ||
| } | ||
| const key = name.toLowerCase() | ||
| if (key === 'set-cookie') { | ||
| // Array header -- only Set-Cookie at the moment | ||
| if (headers['set-cookie'] === undefined) { | ||
| headers['set-cookie'] = values | ||
| } else { | ||
| headers['set-cookie'].push(...values) | ||
| } | ||
| } else if (noDuplicatesHeaders.has(key)) { | ||
| if (headers[key] === undefined) { | ||
| // Drop duplicates | ||
| headers[key] = values[0] | ||
| } | ||
| } else { | ||
| if (headers[key] !== undefined) { | ||
| values = [headers[key], ...values] | ||
| } | ||
| const separator = key === 'cookie' ? '; ' : ', ' | ||
| headers[key] = values.join(separator) | ||
| } | ||
| } | ||
| function deleteHeadersField( | ||
| headers , | ||
| fieldNameToDelete , | ||
| ) { | ||
| if (!isPlainObject(headers)) { | ||
| throw Error('headers must be an object') | ||
| } | ||
| if (typeof fieldNameToDelete !== 'string') { | ||
| throw Error('field name must be a string') | ||
| } | ||
| const lowerCaseFieldNameToDelete = fieldNameToDelete.toLowerCase() | ||
| // Search through the headers and delete all values whose field name matches the given field name. | ||
| Object.keys(headers) | ||
| .filter(fieldName => fieldName.toLowerCase() === lowerCaseFieldNameToDelete) | ||
| .forEach(fieldName => delete headers[fieldName]) | ||
| } | ||
| function forEachHeader( | ||
| rawHeaders , | ||
| callback , | ||
| ) { | ||
| for (let i = 0; i < rawHeaders.length; i += 2) { | ||
| callback(rawHeaders[i + 1], rawHeaders[i], i) | ||
| } | ||
| } | ||
| function percentDecode(str ) { | ||
| try { | ||
| return decodeURIComponent(str.replace(/\+/g, ' ')) | ||
| } catch { | ||
| return str | ||
| } | ||
| } | ||
| function percentEncode(str ) { | ||
| return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { | ||
| return `%${c.charCodeAt(0).toString(16).toUpperCase()}` | ||
| }) | ||
| } | ||
| function matchStringOrRegexp( | ||
| target , | ||
| pattern , | ||
| ) { | ||
| const targetStr = | ||
| target === undefined || target === null ? '' : String(target) | ||
| if (pattern instanceof RegExp) { | ||
| // if the regexp happens to have a global flag, we want to ensure we test the entire target | ||
| pattern.lastIndex = 0 | ||
| return pattern.test(targetStr) | ||
| } | ||
| return targetStr === String(pattern) | ||
| } | ||
| function formatQueryValue( | ||
| key , | ||
| value , | ||
| stringFormattingFn , | ||
| ) { | ||
| // TODO: Probably refactor code to replace `switch(true)` with `if`/`else`. | ||
| switch (true) { | ||
| case typeof value === 'number': // fall-through | ||
| case typeof value === 'boolean': | ||
| value = value.toString() | ||
| break | ||
| case value === null: | ||
| case value === undefined: | ||
| value = '' | ||
| break | ||
| case typeof value === 'string': | ||
| if (stringFormattingFn) { | ||
| value = stringFormattingFn(value) | ||
| } | ||
| break | ||
| case value instanceof RegExp: | ||
| break | ||
| case Array.isArray(value): { | ||
| value = value.map(function (val , idx ) { | ||
| return formatQueryValue(String(idx), val, stringFormattingFn)[1] | ||
| }) | ||
| break | ||
| } | ||
| case typeof value === 'object': { | ||
| value = Object.entries(value).reduce(function ( | ||
| acc , | ||
| [subKey, subVal], | ||
| ) { | ||
| const subPair = formatQueryValue(subKey, subVal, stringFormattingFn) | ||
| acc[subPair[0]] = subPair[1] | ||
| return acc | ||
| }, {}) | ||
| break | ||
| } | ||
| } | ||
| if (stringFormattingFn) key = stringFormattingFn(key) | ||
| return [key, value] | ||
| } | ||
| function isStream(obj ) { | ||
| return ( | ||
| obj && | ||
| typeof obj !== 'string' && | ||
| !Buffer.isBuffer(obj) && | ||
| typeof obj.setEncoding === 'function' | ||
| ) | ||
| } | ||
| const dataEqual = (expected , actual ) => { | ||
| if (isPlainObject(expected)) { | ||
| expected = expand(expected) | ||
| } | ||
| if (isPlainObject(actual)) { | ||
| actual = expand(actual) | ||
| } | ||
| return deepEqual(expected, actual) | ||
| } | ||
| function deepEqual(expected , actual ) { | ||
| debug('deepEqual comparing', typeof expected, expected, typeof actual, actual) | ||
| if (expected instanceof RegExp) { | ||
| return expected.test(actual) | ||
| } | ||
| if (Array.isArray(expected) && Array.isArray(actual)) { | ||
| if (expected.length !== actual.length) { | ||
| return false | ||
| } | ||
| return expected.every((expVal, idx) => deepEqual(expVal, actual[idx])) | ||
| } | ||
| if (isPlainObject(expected) && isPlainObject(actual)) { | ||
| const allKeys = Array.from( | ||
| new Set(Object.keys(expected).concat(Object.keys(actual))), | ||
| ) | ||
| return allKeys.every(key => deepEqual(expected[key], actual[key])) | ||
| } | ||
| return expected === actual | ||
| } | ||
| const timeouts = new Set() | ||
| const immediates = new Set() | ||
| const wrapTimer = | ||
| (timer , ids ) => | ||
| (callback , ...timerArgs ) => { | ||
| const cb = (...callbackArgs ) => { | ||
| try { | ||
| callback(...callbackArgs) | ||
| } finally { | ||
| ids.delete(id) | ||
| } | ||
| } | ||
| const id = timer(cb, ...timerArgs) | ||
| ids.add(id) | ||
| return id | ||
| } | ||
| const setTimeout = wrapTimer(timers.setTimeout, timeouts) | ||
| const setImmediate = wrapTimer(timers.setImmediate, immediates) | ||
| function clearTimer(clear , ids ) { | ||
| ids.forEach(clear) | ||
| ids.clear() | ||
| } | ||
| function removeAllTimers() { | ||
| debug('remove all timers') | ||
| clearTimer(clearTimeout, timeouts) | ||
| clearTimer(clearImmediate, immediates) | ||
| } | ||
| function isPlainObject(value ) { | ||
| if (typeof value !== 'object' || value === null) return false | ||
| if (Object.prototype.toString.call(value) !== '[object Object]') return false | ||
| const proto = Object.getPrototypeOf(value) | ||
| if (proto === null) return true | ||
| const Ctor = | ||
| Object.prototype.hasOwnProperty.call(proto, 'constructor') && | ||
| proto.constructor | ||
| return ( | ||
| typeof Ctor === 'function' && | ||
| Ctor instanceof Ctor && | ||
| Function.prototype.call(Ctor) === Function.prototype.call(value) | ||
| ) | ||
| } | ||
| const prototypePollutionBlockList = ['__proto__', 'prototype', 'constructor'] | ||
| const blocklistFilter = function (part ) { | ||
| return prototypePollutionBlockList.indexOf(part) === -1 | ||
| } | ||
| const expand = (input ) => { | ||
| if (input === undefined || input === null) { | ||
| return input | ||
| } | ||
| const keys = Object.keys(input) | ||
| const result = {} | ||
| let resultPtr = result | ||
| for (let path of keys) { | ||
| const originalPath = path | ||
| if (path.indexOf('[') >= 0) { | ||
| path = path.replace(/\[/g, '.').replace(/]/g, '') | ||
| } | ||
| const parts = path.split('.') | ||
| const check = parts.filter(blocklistFilter) | ||
| if (check.length !== parts.length) { | ||
| return undefined | ||
| } | ||
| resultPtr = result | ||
| const lastIndex = parts.length - 1 | ||
| for (let i = 0; i < parts.length; ++i) { | ||
| const part = parts[i] | ||
| if (i === lastIndex) { | ||
| if (Array.isArray(resultPtr)) { | ||
| resultPtr[+part] = input[originalPath] | ||
| } else { | ||
| resultPtr[part] = input[originalPath] | ||
| } | ||
| } else { | ||
| if (resultPtr[part] === undefined || resultPtr[part] === null) { | ||
| const nextPart = parts[i + 1] | ||
| if (/^\d+$/.test(nextPart)) { | ||
| resultPtr[part] = [] | ||
| } else { | ||
| resultPtr[part] = {} | ||
| } | ||
| } | ||
| resultPtr = resultPtr[part] | ||
| } | ||
| } | ||
| } | ||
| return result | ||
| } | ||
| function decompressRequestBody(buffer , contentEncoding ) { | ||
| const encodings = contentEncoding | ||
| .toLowerCase() | ||
| .split(',') | ||
| .map(coding => coding.trim()) | ||
| for (const encoding of encodings) { | ||
| if (encoding === 'gzip') { | ||
| return zlib.gunzipSync(buffer) | ||
| } else if (encoding === 'deflate') { | ||
| return zlib.inflateSync(buffer) | ||
| } else if (encoding === 'br') { | ||
| return zlib.brotliDecompressSync(buffer) | ||
| } | ||
| } | ||
| return buffer | ||
| } | ||
| function convertHeadersToRaw(headers ) { | ||
| const rawHeaders = [] | ||
| for (const [name, value] of headers.entries()) { | ||
| rawHeaders.push(name, value) | ||
| } | ||
| return rawHeaders | ||
| } | ||
| export { | ||
| contentEncoding, | ||
| dataEqual, | ||
| deleteHeadersField, | ||
| expand, | ||
| forEachHeader, | ||
| formatQueryValue, | ||
| headersArrayToObject, | ||
| headersFieldNamesToLowerCase, | ||
| headersFieldsArrayToLowerCase, | ||
| headersInputToRawArray, | ||
| isContentEncoded, | ||
| isJSONContent, | ||
| isPlainObject, | ||
| isStream, | ||
| isUtf8Representable, | ||
| matchStringOrRegexp, | ||
| normalizeOrigin, | ||
| normalizeRequestOptions, | ||
| percentDecode, | ||
| percentEncode, | ||
| removeAllTimers, | ||
| setImmediate, | ||
| setTimeout, | ||
| stringifyRequest, | ||
| decompressRequestBody, | ||
| convertHeadersToRaw, | ||
| } |
| import type { DebugLoggerFunction } from 'node:util'; | ||
| export declare const back: DebugLoggerFunction; | ||
| export declare const common: DebugLoggerFunction; | ||
| export declare const intercept: DebugLoggerFunction; | ||
| export declare const request_overrider: DebugLoggerFunction; | ||
| export declare const playback_interceptor: DebugLoggerFunction; | ||
| export declare const recorder: DebugLoggerFunction; | ||
| export declare const socket: DebugLoggerFunction; | ||
| export declare const scopeDebuglog: (namespace: string) => import("util").DebugLogger; |
| import { debuglog } from 'node:util' | ||
| export const back = debuglog('nock:back') | ||
| export const common = debuglog('nock:common') | ||
| export const intercept = debuglog('nock:intercept') | ||
| export const request_overrider = debuglog( | ||
| 'nock:request_overrider', | ||
| ) | ||
| export const playback_interceptor = debuglog( | ||
| 'nock:playback_interceptor', | ||
| ) | ||
| export const recorder = debuglog('nock:recorder') | ||
| export const socket = debuglog('nock:socket') | ||
| export const scopeDebuglog = (namespace ) => | ||
| debuglog(`nock:scope:${namespace}`) |
| import type { Interceptor } from './interceptor.ts'; | ||
| import { EventEmitter } from 'node:events'; | ||
| export interface InterceptorMatchResult { | ||
| interceptor: Interceptor; | ||
| reasons: string[]; | ||
| } | ||
| type EventMap = { | ||
| 'no match': [req: Request, interceptorResults?: InterceptorMatchResult[]]; | ||
| }; | ||
| declare const _default: EventEmitter<EventMap>; | ||
| export default _default; |
| import { EventEmitter } from 'node:events' | ||
| export default new EventEmitter () |
| declare function handleRequest(request: Request): Promise<import("undici-types").Response | undefined>; | ||
| export default handleRequest; |
| import globalEmitter from './global_emitter.js' | ||
| import * as common from './common.js' | ||
| import { playbackInterceptor } from './playback_interceptor.js' | ||
| import { interceptorsFor, isOn, isEnabledForNetConnect } from './intercept.js' | ||
| async function handleRequest(request ) { | ||
| const url = new URL(request.url) | ||
| const interceptors = interceptorsFor(url) | ||
| if (isOn() && interceptors) { | ||
| const matches = interceptors.some((interceptor ) => | ||
| interceptor.matchOrigin(request), | ||
| ) | ||
| const allowUnmocked = interceptors.some( | ||
| (interceptor ) => interceptor.options.allowUnmocked, | ||
| ) | ||
| if (!matches && allowUnmocked) { | ||
| globalEmitter.emit('no match', request) | ||
| } else { | ||
| const requestBodyBuffer = await request.clone().arrayBuffer() | ||
| // When request body is a binary buffer we internally use in its hexadecimal representation. | ||
| const requestBodyIsUtf8Representable = | ||
| common.isUtf8Representable(requestBodyBuffer) | ||
| const requestBodyString = Buffer.from(requestBodyBuffer).toString( | ||
| requestBodyIsUtf8Representable ? 'utf8' : 'hex', | ||
| ) | ||
| const matchResults = [] | ||
| const matchedInterceptor = interceptors.find((i ) => { | ||
| const reasons = i.match(request, requestBodyString) | ||
| if (reasons.length > 0) { | ||
| matchResults.push({ interceptor: i, reasons }) | ||
| return false | ||
| } else { | ||
| return true | ||
| } | ||
| }) | ||
| if (matchedInterceptor) { | ||
| matchedInterceptor.scope.logger( | ||
| 'interceptor identified, starting mocking', | ||
| ) | ||
| matchedInterceptor.markConsumed() | ||
| if (matchedInterceptor.isPassthrough) { | ||
| return | ||
| } | ||
| const response = await playbackInterceptor({ | ||
| decompressedRequest: request, | ||
| requestBodyString, | ||
| interceptor: matchedInterceptor, | ||
| requestBodyIsUtf8Representable, | ||
| }) | ||
| return response | ||
| } else { | ||
| globalEmitter.emit( | ||
| 'no match', | ||
| request, | ||
| matchResults.sort((a, b) => a.reasons.length - b.reasons.length), | ||
| ) | ||
| // Try to find a hostname match that allows unmocked. | ||
| const allowUnmocked = interceptors.some( | ||
| (i ) => | ||
| i.matchHostName(url.hostname) && i.options.allowUnmocked, | ||
| ) | ||
| if (!allowUnmocked) { | ||
| const body = JSON.stringify({ | ||
| code: 'ERR_NOCK_NO_MATCH', | ||
| message: `Nock: No match for request ${common.stringifyRequest(request, requestBodyString)}`, | ||
| }) | ||
| return new Response(body, { | ||
| status: 501, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| globalEmitter.emit('no match', request) | ||
| // Remove http(s):// for backward compatibility until we decide this Error. | ||
| const normalizedUrl = common | ||
| .normalizeOrigin(url) | ||
| .replace(`${url.protocol}//`, '') | ||
| if (isOn() && !isEnabledForNetConnect(normalizedUrl)) { | ||
| throw new NetConnectNotAllowedError(normalizedUrl, url.pathname) | ||
| } | ||
| } | ||
| } | ||
| class NetConnectNotAllowedError extends Error { | ||
| constructor(host , path ) { | ||
| super(`Nock: Disallowed net connect for "${host}${path}"`) | ||
| this.name = 'NetConnectNotAllowedError' | ||
| this.code = 'ENETUNREACH' | ||
| } | ||
| } | ||
| export default handleRequest |
| import type { Interceptor } from './interceptor.ts'; | ||
| import type { Scope } from './scope.ts'; | ||
| import * as common from './common.ts'; | ||
| declare function enableNetConnect(matcher?: string | RegExp | ((host: string) => boolean)): void; | ||
| declare function isEnabledForNetConnect(url: string): boolean; | ||
| declare function disableNetConnect(): void; | ||
| declare function isOn(): boolean; | ||
| declare function addInterceptor(key: string, interceptor: Interceptor, scope: Scope, scopeOptions: Record<string, any>, host: string): void; | ||
| declare function remove(interceptor: Interceptor): void; | ||
| declare function removeAll(): void; | ||
| declare function interceptorsFor(url: URL): any; | ||
| declare function removeInterceptor(options: Interceptor | { | ||
| proto?: string; | ||
| host?: string; | ||
| method?: string; | ||
| path?: string; | ||
| }): boolean; | ||
| declare function isDone(): boolean; | ||
| declare function pendingMocks(): string[]; | ||
| declare function activeMocks(): string[]; | ||
| declare function activate(): void; | ||
| declare function deactivate(): void; | ||
| declare const isActiveFn: () => boolean; | ||
| declare const abortPendingRequests: typeof common.removeAllTimers; | ||
| export { addInterceptor, remove, removeAll, removeInterceptor, isOn, activate, isActiveFn as isActive, isDone, pendingMocks, activeMocks, enableNetConnect, disableNetConnect, deactivate as restoreOverriddenClientRequest, abortPendingRequests, interceptorsFor, isEnabledForNetConnect, }; |
| import * as common from './common.js' | ||
| import { intercept as debug } from './debug.js' | ||
| import * as builtinMock from './interceptors/builtin.js' | ||
| import { createRequire } from 'node:module' | ||
| const _require = createRequire(import.meta.url) | ||
| let undiciMock | ||
| let allInterceptors = {} | ||
| let allowNetConnect | ||
| let _isActive = false | ||
| function enableNetConnect( | ||
| matcher , | ||
| ) { | ||
| if (typeof matcher === 'string') { | ||
| allowNetConnect = new RegExp(matcher) | ||
| } else if (matcher instanceof RegExp) { | ||
| allowNetConnect = matcher | ||
| } else if (typeof matcher === 'function') { | ||
| allowNetConnect = { test: matcher } | ||
| } else { | ||
| allowNetConnect = /.*/ | ||
| } | ||
| } | ||
| function isEnabledForNetConnect(url ) { | ||
| const enabled = !!(allowNetConnect && allowNetConnect.test(url)) | ||
| debug('Net connect', enabled ? '' : 'not', 'enabled for', url) | ||
| return enabled | ||
| } | ||
| function disableNetConnect() { | ||
| allowNetConnect = undefined | ||
| } | ||
| function isOn() { | ||
| return !isOff() | ||
| } | ||
| function isOff() { | ||
| return process.env.NOCK_OFF === 'true' | ||
| } | ||
| function addInterceptor( | ||
| key , | ||
| interceptor , | ||
| scope , | ||
| scopeOptions , | ||
| host , | ||
| ) { | ||
| if (!(key in allInterceptors)) { | ||
| allInterceptors[key] = { key, interceptors: [] } | ||
| } | ||
| interceptor.__nock_scope = scope | ||
| // We need scope's key and scope options for scope filtering function (if defined) | ||
| interceptor.__nock_scopeKey = key | ||
| interceptor.__nock_scopeOptions = scopeOptions | ||
| // We need scope's host for setting correct request headers for filtered scopes. | ||
| interceptor.__nock_scopeHost = host | ||
| interceptor.interceptionCounter = 0 | ||
| if (scopeOptions.allowUnmocked) allInterceptors[key].allowUnmocked = true | ||
| allInterceptors[key].interceptors.push(interceptor) | ||
| } | ||
| function remove(interceptor ) { | ||
| if (interceptor.__nock_scope?.shouldPersist() || --interceptor.counter > 0) { | ||
| return | ||
| } | ||
| const basePath = interceptor.basePath | ||
| const interceptors = | ||
| (allInterceptors[basePath] && allInterceptors[basePath].interceptors) || [] | ||
| // TODO: There is a clearer way to write that we want to delete the first | ||
| // matching instance. I'm also not sure why we couldn't delete _all_ | ||
| // matching instances. | ||
| interceptors.some(function (thisInterceptor , i ) { | ||
| return thisInterceptor === interceptor ? interceptors.splice(i, 1) : false | ||
| }) | ||
| } | ||
| function removeAll() { | ||
| Object.keys(allInterceptors).forEach(function (key) { | ||
| allInterceptors[key].interceptors.forEach(function ( | ||
| interceptor , | ||
| ) { | ||
| interceptor.scope.keyedInterceptors = {} | ||
| }) | ||
| }) | ||
| allInterceptors = {} | ||
| } | ||
| function interceptorsFor(url ) { | ||
| debug('interceptors for %j', url.host) | ||
| const port = url.port || (url.protocol === 'https:' ? 443 : 80) | ||
| // Remove brackets from hostname if IPV6 | ||
| const basePath = `${url.protocol}//${url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname}:${port}` | ||
| debug('filtering interceptors for basepath', basePath) | ||
| // First try to use filteringScope if any of the interceptors has it defined. | ||
| for (const { key, interceptors, allowUnmocked } of Object.values( | ||
| allInterceptors, | ||
| )) { | ||
| for (const interceptor of interceptors) { | ||
| const { filteringScope } = interceptor.__nock_scopeOptions | ||
| // If scope filtering function is defined and returns a truthy value then | ||
| // we have to treat this as a match. | ||
| if (filteringScope && filteringScope(basePath)) { | ||
| interceptor.scope.logger('found matching scope interceptor') | ||
| // Keep the filtered scope (its key) to signal the rest of the module | ||
| // that this wasn't an exact but filtered match. | ||
| interceptors.forEach((ic ) => { | ||
| ic.__nock_filteredScope = ic.__nock_scopeKey | ||
| }) | ||
| return interceptors | ||
| } | ||
| } | ||
| if (common.matchStringOrRegexp(basePath, key)) { | ||
| if (allowUnmocked && interceptors.length === 0) { | ||
| debug('matched base path with allowUnmocked (no matching interceptors)') | ||
| return [ | ||
| { | ||
| options: { allowUnmocked: true }, | ||
| matchOrigin() { | ||
| return false | ||
| }, | ||
| }, | ||
| ] | ||
| } else { | ||
| debug( | ||
| `matched base path (${interceptors.length} interceptor${ | ||
| interceptors.length > 1 ? 's' : '' | ||
| })`, | ||
| ) | ||
| return interceptors | ||
| } | ||
| } | ||
| } | ||
| return undefined | ||
| } | ||
| import { Interceptor as InterceptorClass } from './interceptor.js' | ||
| function removeInterceptor( | ||
| options | ||
| , | ||
| ) { | ||
| let baseUrl, key, method, proto | ||
| if (options instanceof InterceptorClass) { | ||
| baseUrl = (options ).basePath | ||
| key = (options )._key | ||
| } else { | ||
| const opts = options | ||
| proto = opts.proto ? opts.proto : 'http' | ||
| common.normalizeRequestOptions(opts) | ||
| baseUrl = `${proto}://${opts.host}` | ||
| method = (opts.method && opts.method.toUpperCase()) || 'GET' | ||
| key = `${method} ${baseUrl}${opts.path || '/'}` | ||
| } | ||
| if ( | ||
| allInterceptors[baseUrl] && | ||
| allInterceptors[baseUrl].interceptors.length > 0 | ||
| ) { | ||
| for (let i = 0; i < allInterceptors[baseUrl].interceptors.length; i++) { | ||
| const interceptor = allInterceptors[baseUrl].interceptors[i] | ||
| if ( | ||
| options instanceof InterceptorClass | ||
| ? interceptor === options | ||
| : interceptor._key === key | ||
| ) { | ||
| allInterceptors[baseUrl].interceptors.splice(i, 1) | ||
| interceptor.scope.remove(key, interceptor) | ||
| break | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| function interceptorScopes() { | ||
| const nestedInterceptors = Object.values(allInterceptors).map( | ||
| (i ) => i.interceptors, | ||
| ) | ||
| const scopes = new Set( | ||
| ([] ) | ||
| .concat(...nestedInterceptors) | ||
| .map((i ) => i.scope), | ||
| ) | ||
| return [...scopes] | ||
| } | ||
| function isDone() { | ||
| return interceptorScopes().every(scope => scope.isDone()) | ||
| } | ||
| function pendingMocks() { | ||
| return ([] ).concat( | ||
| ...interceptorScopes().map(scope => scope.pendingMocks()), | ||
| ) | ||
| } | ||
| function activeMocks() { | ||
| return ([] ).concat( | ||
| ...interceptorScopes().map(scope => scope.activeMocks()), | ||
| ) | ||
| } | ||
| function activate() { | ||
| if (!_isActive) { | ||
| builtinMock.activate() | ||
| if (!undiciMock) { | ||
| try { | ||
| undiciMock = _require('./interceptors/undici.js') | ||
| } catch (err ) { | ||
| if ( | ||
| err.code !== 'MODULE_NOT_FOUND' && | ||
| err.code !== 'ERR_MODULE_NOT_FOUND' && | ||
| err.code !== 'ERR_REQUIRE_ESM' | ||
| ) { | ||
| throw err | ||
| } | ||
| debug( | ||
| 'Undici mocking is disabled because the undici module is not installed', | ||
| ) | ||
| } | ||
| } | ||
| undiciMock?.activate() | ||
| _isActive = true | ||
| } else { | ||
| throw new Error('Nock already active') | ||
| } | ||
| } | ||
| function deactivate() { | ||
| if (_isActive) { | ||
| builtinMock.deactivate() | ||
| undiciMock?.deactivate() | ||
| _isActive = false | ||
| } | ||
| } | ||
| const isActiveFn = () => _isActive | ||
| const abortPendingRequests = common.removeAllTimers | ||
| export { | ||
| addInterceptor, | ||
| remove, | ||
| removeAll, | ||
| removeInterceptor, | ||
| isOn, | ||
| activate, | ||
| isActiveFn as isActive, | ||
| isDone, | ||
| pendingMocks, | ||
| activeMocks, | ||
| enableNetConnect, | ||
| disableNetConnect, | ||
| deactivate as restoreOverriddenClientRequest, | ||
| abortPendingRequests, | ||
| interceptorsFor, | ||
| isEnabledForNetConnect, | ||
| } |
| import type { ReadStream } from 'node:fs'; | ||
| import type { Scope, Options } from './scope.ts'; | ||
| export type DataMatcher = boolean | number | string | null | undefined | RegExp | DataMatcherArray | DataMatcherMap; | ||
| export type DataMatcherArray = ReadonlyArray<DataMatcher>; | ||
| export type DataMatcherMap = { | ||
| [key: string]: DataMatcher; | ||
| }; | ||
| export type RequestBodyMatcher = string | Buffer | RegExp | DataMatcherArray | DataMatcherMap | ((body: any) => boolean); | ||
| export type RequestHeaderMatcher = string | RegExp | ((fieldValue: string | null) => boolean); | ||
| export type Body = string | Record<string, any>; | ||
| export type ReplyBody = Body | Buffer | ReadStream; | ||
| export type ReplyHeaderFunction = (req: Request, body?: string | Buffer) => string | string[] | Promise<string | string[]>; | ||
| export type ReplyHeaderValue = string | string[] | ReplyHeaderFunction; | ||
| export type ReplyHeaders = Record<string, ReplyHeaderValue> | Map<string, ReplyHeaderValue> | ReplyHeaderValue[]; | ||
| export type StatusCode = number; | ||
| export type ReplyFnResult = readonly [StatusCode] | readonly [StatusCode, ReplyBody] | readonly [StatusCode, ReplyBody, ReplyHeaders]; | ||
| import { URLSearchParams } from 'node:url'; | ||
| declare class Interceptor { | ||
| constructor(scope: Scope, uri: string | RegExp | ((path: string) => boolean), method: string, requestBody?: RequestBodyMatcher, interceptorOptions?: Options); | ||
| optionally(flag?: boolean): this; | ||
| passthrough(): Scope; | ||
| replyWithError(errorMessage: string | Error | Record<string, any>): Scope; | ||
| reply(statusCode: any, body?: any, rawHeaders?: ReplyHeaders): Scope; | ||
| replyWithFile(statusCode: number, filePath: string, headers?: ReplyHeaders): Scope; | ||
| reqheaderMatches(expected: RequestHeaderMatcher, actual: string | null, key: string): boolean; | ||
| match(request: Request, body: string): string[]; | ||
| matchOrigin(request: Request): boolean; | ||
| matchHostName(hostname: string): boolean; | ||
| matchQuery(options: Record<string, any>): any; | ||
| filteringPath(...args: any[]): this; | ||
| markConsumed(): void; | ||
| matchHeader(name: string, value: RequestHeaderMatcher): this; | ||
| basicAuth({ user, pass }: { | ||
| user: string; | ||
| pass?: string; | ||
| }): this; | ||
| query(queries: boolean | string | URLSearchParams | Record<string, any> | ((queryObject: Record<string, any>) => boolean)): this; | ||
| times(newCounter: number): this; | ||
| once(): this; | ||
| twice(): this; | ||
| thrice(): this; | ||
| delay(ms: number): this; | ||
| } | ||
| export { Interceptor }; |
| import fs from 'node:fs' | ||
| import querystring from 'node:querystring' | ||
| import { URL, URLSearchParams } from 'node:url' | ||
| import stringify from './stringify.js' | ||
| import * as common from './common.js' | ||
| import { remove } from './intercept.js' | ||
| import matchBody from './match_body.js' | ||
| class Interceptor { | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| constructor( | ||
| scope , | ||
| uri , | ||
| method , | ||
| requestBody , | ||
| interceptorOptions , | ||
| ) { | ||
| const uriIsStr = typeof uri === 'string' | ||
| // Check for leading slash. Uri can be either a string or a regexp, but | ||
| // When enabled filteringScope ignores the passed URL entirely so we skip validation. | ||
| if ( | ||
| uriIsStr && | ||
| !scope.scopeOptions.filteringScope && | ||
| !scope.basePathname && | ||
| !(uri ).startsWith('/') && | ||
| !(uri ).startsWith('*') | ||
| ) { | ||
| throw Error( | ||
| `Non-wildcard URL path strings must begin with a slash (otherwise they won't match anything) (got: ${uri})`, | ||
| ) | ||
| } | ||
| if (!method) { | ||
| throw new Error( | ||
| 'The "method" parameter is required for an intercept call.', | ||
| ) | ||
| } | ||
| this.scope = scope | ||
| this.interceptorMatchHeaders = [] | ||
| this.method = method.toUpperCase() | ||
| this.uri = uri | ||
| this._key = `${this.method} ${scope.basePath}${scope.basePathname}${ | ||
| uriIsStr ? '' : '/' | ||
| }${uri}` | ||
| this.basePath = this.scope.basePath | ||
| this.path = uriIsStr ? scope.basePathname + uri : uri | ||
| this.queries = null | ||
| this.options = interceptorOptions || {} | ||
| this.counter = 1 | ||
| this._requestBody = requestBody | ||
| // We use lower-case header field names throughout Nock. | ||
| this.reqheaders = common.headersFieldNamesToLowerCase( | ||
| scope.scopeOptions.reqheaders || {}, | ||
| true, | ||
| ) | ||
| this.badheaders = common.headersFieldsArrayToLowerCase( | ||
| scope.scopeOptions.badheaders || [], | ||
| ) | ||
| this.delayBodyInMs = 0 | ||
| this.optional = false | ||
| this.isPassthrough = false | ||
| this.__nock_filteredScope = undefined | ||
| this.__nock_scopeKey = undefined | ||
| this.__nock_scope = undefined | ||
| this.__nock_scopeOptions = undefined | ||
| this.__nock_scopeHost = undefined | ||
| this.interceptionCounter = 0 | ||
| this.statusCode = undefined | ||
| this.headers = undefined | ||
| this.rawHeaders = [] | ||
| // strip off literal query parameters if they were provided as part of the URI | ||
| if (uriIsStr && (uri ).includes('?')) { | ||
| // localhost is a dummy value because the URL constructor errors for only relative inputs | ||
| const parsedURL = new URL(this.path, 'http://localhost') | ||
| this.path = parsedURL.pathname | ||
| this.query(parsedURL.searchParams) | ||
| this._key = `${this.method} ${scope.basePath}${this.path}` | ||
| } | ||
| } | ||
| optionally(flag = true) { | ||
| // The default behaviour of optionally() with no arguments is to make the mock optional. | ||
| if (typeof flag !== 'boolean') { | ||
| throw new Error('Invalid arguments: argument should be a boolean') | ||
| } | ||
| this.optional = flag | ||
| return this | ||
| } | ||
| passthrough() { | ||
| this.isPassthrough = true | ||
| this.options = { | ||
| ...this.scope.scopeOptions, | ||
| ...this.options, | ||
| } | ||
| this.scope.add(this._key, this) | ||
| return this.scope | ||
| } | ||
| replyWithError(errorMessage ) { | ||
| this.errorMessage = errorMessage | ||
| this.options = { | ||
| ...this.scope.scopeOptions, | ||
| ...this.options, | ||
| } | ||
| this.scope.add(this._key, this) | ||
| return this.scope | ||
| } | ||
| reply(statusCode , body , rawHeaders ) { | ||
| // support the format of only passing in a callback | ||
| if (typeof statusCode === 'function') { | ||
| if (arguments.length > 1) { | ||
| // It's not very Javascript-y to throw an error for extra args to a function, but because | ||
| // of legacy behavior, this error was added to reduce confusion for those migrating. | ||
| throw Error( | ||
| 'Invalid arguments. When providing a function for the first argument, .reply does not accept other arguments.', | ||
| ) | ||
| } | ||
| this.statusCode = null | ||
| this.fullReplyFunction = statusCode | ||
| } else { | ||
| if (statusCode !== undefined && !Number.isInteger(statusCode)) { | ||
| throw new Error(`Invalid ${typeof statusCode} value for status code`) | ||
| } | ||
| this.statusCode = statusCode || 200 | ||
| if (typeof body === 'function') { | ||
| this.replyFunction = body | ||
| body = null | ||
| } | ||
| } | ||
| this.options = { | ||
| ...this.scope.scopeOptions, | ||
| ...this.options, | ||
| } | ||
| this.rawHeaders = common.headersInputToRawArray(rawHeaders) | ||
| if (this.scope.date) { | ||
| // https://tools.ietf.org/html/rfc7231#section-7.1.1.2 | ||
| this.rawHeaders.push('Date', this.scope.date.toUTCString()) | ||
| } | ||
| // Prepare the headers temporarily so we can make best guesses about content-encoding and content-type | ||
| // below as well as while the response is being processed in RequestOverrider.end(). | ||
| // Including all the default headers is safe for our purposes because of the specific headers we introspect. | ||
| // A more thoughtful process is used to merge the default headers when the response headers are finally computed. | ||
| this.headers = common.headersArrayToObject( | ||
| this.rawHeaders.concat(this.scope._defaultReplyHeaders), | ||
| ) | ||
| // If the content is not encoded we may need to transform the response body. | ||
| // Otherwise, we leave it as it is. | ||
| if ( | ||
| body && | ||
| typeof body !== 'string' && | ||
| !Buffer.isBuffer(body) && | ||
| !common.isStream(body) && | ||
| !common.isContentEncoded(this.headers) | ||
| ) { | ||
| try { | ||
| body = stringify(body) | ||
| } catch { | ||
| throw new Error('Error encoding response body into JSON') | ||
| } | ||
| if (!this.headers ['content-type']) { | ||
| // https://tools.ietf.org/html/rfc7231#section-3.1.1.5 | ||
| this.rawHeaders.push('Content-Type', 'application/json') | ||
| } | ||
| // Fix content-length header if it exists and doesn't match the stringified body | ||
| const contentLengthIndex = this.rawHeaders.findIndex( | ||
| (value , index ) => | ||
| index % 2 === 0 && value.toLowerCase() === 'content-length', | ||
| ) | ||
| if (contentLengthIndex !== -1) { | ||
| const actualLength = Buffer.byteLength(body, 'utf8') | ||
| this.rawHeaders[contentLengthIndex + 1] = String(actualLength) | ||
| } | ||
| } | ||
| if (this.scope.contentLen) { | ||
| // https://tools.ietf.org/html/rfc7230#section-3.3.2 | ||
| if (typeof body === 'string') { | ||
| this.rawHeaders.push('Content-Length', body.length) | ||
| } else if (Buffer.isBuffer(body)) { | ||
| this.rawHeaders.push('Content-Length', body.byteLength) | ||
| } | ||
| } | ||
| this.scope.logger('reply.headers:', this.headers) | ||
| this.scope.logger('reply.rawHeaders:', this.rawHeaders) | ||
| this.body = body | ||
| this.scope.add(this._key, this) | ||
| return this.scope | ||
| } | ||
| replyWithFile(statusCode , filePath , headers ) { | ||
| if (!fs) { | ||
| throw new Error('No fs') | ||
| } | ||
| this.filePath = filePath | ||
| return this.reply( | ||
| statusCode, | ||
| () => { | ||
| const readStream = fs.createReadStream(filePath) | ||
| readStream.pause() | ||
| return readStream | ||
| }, | ||
| headers, | ||
| ) | ||
| } | ||
| reqheaderMatches( | ||
| expected , | ||
| actual , | ||
| key , | ||
| ) { | ||
| if (expected !== undefined && actual !== undefined) { | ||
| if (typeof expected === 'function') { | ||
| return expected(actual) | ||
| } else if (common.matchStringOrRegexp(actual, expected)) { | ||
| return true | ||
| } | ||
| } | ||
| this.scope.logger( | ||
| "request header field doesn't match:", | ||
| key, | ||
| actual, | ||
| expected, | ||
| ) | ||
| return false | ||
| } | ||
| match(request , body ) { | ||
| const url = new URL(request.url) | ||
| // TODO: fix request log to string | ||
| this.scope.logger('attempting match %j, body = %j', request, body) | ||
| const mismatches = [] | ||
| let path = url.pathname + url.search | ||
| let matchKey | ||
| if (this.method !== request.method) { | ||
| const msg = `Method mismatch: expected ${this.method}, got ${request.method}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if (this.scope.transformPathFunction) { | ||
| path = this.scope.transformPathFunction(path) | ||
| } | ||
| const requestMatchesFilter = ({ | ||
| name, | ||
| value: predicate, | ||
| } | ||
| ) => { | ||
| const headerValue = request.headers.get(name) | ||
| if (typeof predicate === 'function') { | ||
| return predicate(headerValue) | ||
| } else { | ||
| return common.matchStringOrRegexp(headerValue, predicate) | ||
| } | ||
| } | ||
| for (const header of [ | ||
| ...this.scope.matchHeaders, | ||
| ...this.interceptorMatchHeaders, | ||
| ]) { | ||
| if (!requestMatchesFilter(header)) { | ||
| const msg = `Header mismatch: expected ${header.name} to match ${header.value}, got ${request.headers.get(header.name)}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| } | ||
| const reqHeadersMatch = Object.keys(this.reqheaders).every((key ) => | ||
| this.reqheaderMatches( | ||
| this.reqheaders[key], | ||
| request.headers.get(key), | ||
| key, | ||
| ), | ||
| ) | ||
| if (!reqHeadersMatch) { | ||
| const msg = "Request headers don't match" | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if ( | ||
| this.scope.scopeOptions.conditionally && | ||
| !this.scope.scopeOptions.conditionally() | ||
| ) { | ||
| const msg = 'conditionally() did not validate' | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| const badHeaders = this.badheaders.filter((header ) => | ||
| request.headers.has(header), | ||
| ) | ||
| if (badHeaders.length) { | ||
| const msg = `Request contains bad headers: ${badHeaders.join(', ')}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| // Match query strings when using query() | ||
| if (this.queries === null) { | ||
| this.scope.logger('query matching skipped') | ||
| } else { | ||
| // can't rely on pathname or search being in the options, but path has a default | ||
| const [pathname, search] = (path ).split('?') | ||
| const matchQueries = this.matchQuery({ search }) | ||
| if (!matchQueries) { | ||
| const msg = 'query matching failed' | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| // If the query string was explicitly checked then subsequent checks against | ||
| // the path using a callback or regexp only validate the pathname. | ||
| path = pathname | ||
| } | ||
| // If we have a filtered scope then we use it instead reconstructing the | ||
| // scope from the request options (proto, host and port) as these two won't | ||
| // necessarily match and we have to remove the scope that was matched (vs. | ||
| // that was defined). | ||
| if (this.__nock_filteredScope) { | ||
| matchKey = this.__nock_filteredScope | ||
| } else { | ||
| matchKey = common.normalizeOrigin(url) | ||
| } | ||
| if ( | ||
| !common.matchStringOrRegexp(matchKey, this.basePath ) | ||
| ) { | ||
| const msg = `Base path mismatch: expected ${this.basePath}, got ${matchKey}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if (typeof this.uri === 'function') { | ||
| if (!this.uri.call(this, path)) { | ||
| const msg = `Path function mismatch: expected function to return true for ${path}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| } else if ( | ||
| !common.matchStringOrRegexp(path, this.path ) | ||
| ) { | ||
| const msg = `Path mismatch: expected ${this.path}, got ${path}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if (this._requestBody !== undefined) { | ||
| if (this.scope.transformRequestBodyFunction) { | ||
| body = this.scope.transformRequestBodyFunction(body, this._requestBody) | ||
| } | ||
| if (!matchBody(request, this._requestBody, body)) { | ||
| const msg = `Body mismatch: expected ${stringify(this._requestBody)}, got ${body}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| } | ||
| return mismatches | ||
| } | ||
| matchOrigin(request ) { | ||
| const url = new URL(request.url) | ||
| const isPathFn = typeof this.path === 'function' | ||
| const isRegex = this.path instanceof RegExp | ||
| const isRegexBasePath = this.scope.basePath instanceof RegExp | ||
| const method = (request.method || 'GET').toUpperCase() | ||
| const port = url.port || (url.protocol === 'https:' ? 443 : 80) | ||
| let path = url.pathname + url.search | ||
| // NOTE: Do not split off the query params as the regex could use them | ||
| if (!isRegex) { | ||
| path = path ? path.split('?')[0] : '' | ||
| } | ||
| if (this.scope.transformPathFunction) { | ||
| path = this.scope.transformPathFunction(path) | ||
| } | ||
| const comparisonKey = | ||
| isPathFn || isRegex ? (this.__nock_scopeKey ) : this._key | ||
| const matchKey = `${method} ${url.protocol}//${url.hostname}:${port}${path}` | ||
| if (isPathFn) { | ||
| return !!(matchKey.match(comparisonKey) && (this.path )(path)) | ||
| } | ||
| if (isRegex && !isRegexBasePath) { | ||
| return !!matchKey.match(comparisonKey) && (this.path ).test(path) | ||
| } | ||
| if (isRegexBasePath) { | ||
| return ( | ||
| (this.scope.basePath ).test(matchKey) && | ||
| !!path.match(this.path ) | ||
| ) | ||
| } | ||
| return comparisonKey === matchKey | ||
| } | ||
| matchHostName(hostname ) { | ||
| const { basePath } = this.scope | ||
| if (basePath instanceof RegExp) { | ||
| return basePath.test(hostname) | ||
| } | ||
| return hostname === this.scope.urlParts.hostname | ||
| } | ||
| matchQuery(options ) { | ||
| if (this.queries === true) { | ||
| return true | ||
| } | ||
| const reqQueries = querystring.parse(options.search) | ||
| this.scope.logger('Interceptor queries: %j', this.queries) | ||
| this.scope.logger(' Request queries: %j', reqQueries) | ||
| if (typeof this.queries === 'function') { | ||
| return this.queries(reqQueries) | ||
| } | ||
| return common.dataEqual(this.queries, reqQueries) | ||
| } | ||
| filteringPath(...args ) { | ||
| ;(this.scope.filteringPath ).apply(this.scope, args) | ||
| return this | ||
| } | ||
| // TODO filtering by path is valid on the intercept level, but not filtering | ||
| // by request body? | ||
| markConsumed() { | ||
| this.interceptionCounter = (this.interceptionCounter || 0) + 1 | ||
| remove(this) | ||
| if (!this.scope.shouldPersist() && this.counter < 1) { | ||
| this.scope.remove(this._key, this) | ||
| } | ||
| } | ||
| matchHeader(name , value ) { | ||
| this.interceptorMatchHeaders.push({ name, value }) | ||
| return this | ||
| } | ||
| basicAuth({ user, pass = '' } ) { | ||
| const encoded = Buffer.from(`${user}:${pass}`).toString('base64') | ||
| this.matchHeader('authorization', `Basic ${encoded}`) | ||
| return this | ||
| } | ||
| query( | ||
| queries | ||
| , | ||
| ) { | ||
| if (this.queries !== null) { | ||
| throw Error(`Query parameters have already been defined`) | ||
| } | ||
| // Allow all query strings to match this route | ||
| if (queries === true) { | ||
| this.queries = queries | ||
| return this | ||
| } | ||
| if (typeof queries === 'function') { | ||
| this.queries = queries | ||
| return this | ||
| } | ||
| let strFormattingFn | ||
| if (this.scope.scopeOptions.encodedQueryParams) { | ||
| strFormattingFn = common.percentDecode | ||
| } | ||
| if (queries instanceof URLSearchParams || typeof queries === 'string') { | ||
| // Normalize the data into the shape that is matched against. | ||
| // Duplicate keys are handled by combining the values into an array. | ||
| queries = querystring.parse(queries.toString()) | ||
| } else if (!common.isPlainObject(queries)) { | ||
| throw Error(`Argument Error: ${queries}`) | ||
| } | ||
| this.queries = {} | ||
| for (const [key, value] of Object.entries(queries)) { | ||
| const formatted = common.formatQueryValue(key, value, strFormattingFn) | ||
| const [formattedKey, formattedValue] = formatted | ||
| ;(this.queries )[formattedKey] = formattedValue | ||
| } | ||
| return this | ||
| } | ||
| times(newCounter ) { | ||
| if (newCounter < 1) { | ||
| return this | ||
| } | ||
| this.counter = newCounter | ||
| return this | ||
| } | ||
| once() { | ||
| return this.times(1) | ||
| } | ||
| twice() { | ||
| return this.times(2) | ||
| } | ||
| thrice() { | ||
| return this.times(3) | ||
| } | ||
| delay(ms ) { | ||
| if (typeof ms === 'number') { | ||
| this.delayBodyInMs = ms | ||
| return this | ||
| } else { | ||
| throw new Error(`Unexpected input ${ms}`) | ||
| } | ||
| } | ||
| } | ||
| export { Interceptor } |
| declare function activate(): void; | ||
| declare function deactivate(): void; | ||
| export { activate, deactivate }; |
| import http from 'node:http' | ||
| import { getRawRequest, BatchInterceptor } from '@mswjs/interceptors' | ||
| import nodeInterceptors from '@mswjs/interceptors/presets/node' | ||
| import * as common from '../common.js' | ||
| import handleRequest from '../handle-request.js' | ||
| import { arrayBuffer } from 'node:stream/consumers' | ||
| import { getClientRequestBodyStream } from '@mswjs/interceptors/utils/node' | ||
| import { setGetRequestBody } from '../utils/node/index.js' | ||
| const interceptor = new BatchInterceptor({ | ||
| name: 'nock-interceptor', | ||
| interceptors: nodeInterceptors, | ||
| }) | ||
| function activate() { | ||
| interceptor.apply() | ||
| // Force msw to forward Nock's error instead of coerce it into 500 error | ||
| interceptor.on('unhandledException', ({ controller, error } ) => { | ||
| controller.errorWith(error) | ||
| }) | ||
| interceptor.on('request', async function ({ request, controller } ) { | ||
| if (request.headers.get('expect') === '100-continue') { | ||
| // We currently do not support mocking 100-continue responses, so they are passed through for now. | ||
| return | ||
| } | ||
| const rawRequest = getRawRequest(request) | ||
| // If this is GET request with body, we need to read the body from the socket because Fetch API doesn't support this. | ||
| const requestBodyBuffer = common.decompressRequestBody( | ||
| rawRequest instanceof http.ClientRequest && | ||
| request.method === 'GET' && | ||
| Number(request.headers.get('content-length')) > 0 | ||
| ? await arrayBuffer(getClientRequestBodyStream(request)) | ||
| : await request.clone().arrayBuffer(), | ||
| request.headers.get('content-encoding') || '', | ||
| ) | ||
| const decompressedRequest = new Request(request, { | ||
| body: | ||
| requestBodyBuffer.byteLength > 0 && request.method !== 'GET' | ||
| ? requestBodyBuffer | ||
| : undefined, | ||
| }) | ||
| if (requestBodyBuffer.byteLength > 0 && request.method === 'GET') { | ||
| setGetRequestBody(decompressedRequest, requestBodyBuffer) | ||
| } | ||
| const response = await handleRequest(decompressedRequest) | ||
| if (response) { | ||
| controller.respondWith(response) | ||
| } | ||
| }) | ||
| } | ||
| function deactivate() { | ||
| interceptor.dispose() | ||
| } | ||
| export { activate, deactivate } |
| declare function activate(): void; | ||
| declare function deactivate(): void; | ||
| export { activate, deactivate }; |
| // This file is loaded lazily after confirming undici is installed | ||
| import undici from 'undici' | ||
| import handleRequest from '../handle-request.js' | ||
| import { URL } from 'node:url' | ||
| import { convertHeadersToRaw } from '../common.js' | ||
| class NockClient extends undici.Client { | ||
| constructor(origin , options ) { | ||
| super(origin, options) | ||
| } | ||
| dispatch(options , handler ) { | ||
| const url = new URL(options.path, options.origin) | ||
| if (options.query) { | ||
| url.search = new URLSearchParams(options.query).toString() | ||
| } | ||
| const decompressedRequest = new Request(url, { | ||
| method: options.method, | ||
| headers: options.headers, | ||
| body: options.body, | ||
| duplex: options.body ? 'half' : undefined, | ||
| }) | ||
| handleRequest(decompressedRequest) | ||
| .then(async (response ) => { | ||
| if (response) { | ||
| handler.onConnect?.((err ) => handler.onError(err), null) | ||
| handler.onHeaders?.( | ||
| response.status, | ||
| convertHeadersToRaw(response.headers), | ||
| () => {}, | ||
| response.statusText, | ||
| ) | ||
| handler.onData?.(Buffer.from(await response.arrayBuffer())) | ||
| handler.onComplete?.([]) // responseTrailers | ||
| } else { | ||
| const dispatcher = options.dispatcher || { | ||
| dispatch: super.dispatch.bind(this), | ||
| } | ||
| dispatcher.dispatch(options, handler) | ||
| } | ||
| }) | ||
| .catch((err ) => { | ||
| handler.onError?.(err) | ||
| }) | ||
| return true | ||
| } | ||
| } | ||
| class NockAgent extends undici.Dispatcher { | ||
| constructor(options ) { | ||
| super() | ||
| this.agent = new undici.Agent({ factory: this.factory.bind(this) }) | ||
| this.originalOptions = options | ||
| } | ||
| dispatch(options , handler ) { | ||
| return this.agent.dispatch(options, handler) | ||
| } | ||
| factory(origin ) { | ||
| const mockOptions = { ...this.originalOptions, agent: this } | ||
| return new NockClient(origin, mockOptions) | ||
| } | ||
| } | ||
| function activate() { | ||
| undici.setGlobalDispatcher(new NockAgent()) | ||
| } | ||
| function deactivate() { | ||
| undici.setGlobalDispatcher(new undici.Agent()) | ||
| } | ||
| export { activate, deactivate } |
| export default function matchBody(request: Request, spec: any, body: string): any; |
| import querystring from 'node:querystring' | ||
| import * as common from './common.js' | ||
| export default function matchBody(request , spec , body ) { | ||
| if (spec instanceof RegExp) { | ||
| return spec.test(body) | ||
| } | ||
| if (Buffer.isBuffer(spec)) { | ||
| const encoding = common.isUtf8Representable(spec) ? 'utf8' : 'hex' | ||
| spec = spec.toString(encoding) | ||
| } | ||
| const contentType = request.headers.get('content-type') || '' | ||
| const isMultipart = contentType.includes('multipart') | ||
| const isUrlencoded = contentType.includes('application/x-www-form-urlencoded') | ||
| // try to transform body to json or query string | ||
| let json | ||
| let matchBody = body | ||
| if (typeof spec === 'object' || typeof spec === 'function') { | ||
| try { | ||
| json = JSON.parse(body) | ||
| } catch { | ||
| // not a valid JSON string | ||
| } | ||
| if (json !== undefined) { | ||
| matchBody = json | ||
| } else if (isUrlencoded) { | ||
| matchBody = querystring.parse(body) | ||
| } | ||
| } | ||
| if (typeof spec === 'function') { | ||
| return spec(matchBody) | ||
| } | ||
| // strip line endings from both so that we get a match no matter what OS we are running on | ||
| // if Content-Type does not contain 'multipart' | ||
| if (!isMultipart && typeof matchBody === 'string') { | ||
| matchBody = matchBody.replace(/\r?\n|\r/g, '') | ||
| } | ||
| if (!isMultipart && typeof spec === 'string') { | ||
| spec = spec.replace(/\r?\n|\r/g, '') | ||
| } | ||
| // Because the nature of URL encoding, all the values in the body must be cast to strings. | ||
| // dataEqual does strict checking, so we have to cast the non-regexp values in the spec too. | ||
| if (isUrlencoded) { | ||
| spec = mapValuesDeep(spec, (val ) => | ||
| val instanceof RegExp ? val : `${val}`, | ||
| ) | ||
| } | ||
| return common.dataEqual(spec, matchBody) | ||
| } | ||
| function mapValues( | ||
| object , | ||
| cb , | ||
| ) { | ||
| const keys = Object.keys(object) | ||
| const clonedObject = { ...object } | ||
| for (const key of keys) { | ||
| clonedObject[key] = cb(clonedObject[key], key, clonedObject) | ||
| } | ||
| return clonedObject | ||
| } | ||
| function mapValuesDeep(obj , cb ) { | ||
| if (Array.isArray(obj)) { | ||
| return obj.map((v ) => mapValuesDeep(v, cb)) | ||
| } | ||
| if (common.isPlainObject(obj)) { | ||
| return mapValues(obj, (v ) => mapValuesDeep(v, cb)) | ||
| } | ||
| return cb(obj) | ||
| } |
| import type { Interceptor } from './interceptor.ts'; | ||
| declare function playbackInterceptor({ decompressedRequest, interceptor, requestBodyString, requestBodyIsUtf8Representable, }: { | ||
| decompressedRequest: Request; | ||
| requestBodyString: string; | ||
| requestBodyIsUtf8Representable: boolean; | ||
| interceptor: Interceptor; | ||
| }): Promise<import("undici-types").Response>; | ||
| export { playbackInterceptor }; |
| import { STATUS_CODES } from 'node:http' | ||
| import stream from 'node:stream' | ||
| import util from 'node:util' | ||
| import { playback_interceptor as debug } from './debug.js' | ||
| import * as common from './common.js' | ||
| import { FetchResponse } from '@mswjs/interceptors' | ||
| function parseFullReplyResult(fullReplyResult ) { | ||
| debug('full response from callback result: %j', fullReplyResult) | ||
| if (!Array.isArray(fullReplyResult)) { | ||
| throw Error('A single function provided to .reply MUST return an array') | ||
| } | ||
| if (fullReplyResult.length > 3) { | ||
| throw Error( | ||
| 'The array returned from the .reply callback contains too many values', | ||
| ) | ||
| } | ||
| const [status, body = '', headers] = fullReplyResult | ||
| if (!Number.isInteger(status)) { | ||
| throw new Error(`Invalid ${typeof status} value for status code`) | ||
| } | ||
| const rawHeaders = common.headersInputToRawArray(headers) | ||
| debug('response.rawHeaders after reply: %j', rawHeaders) | ||
| return [status, body, rawHeaders] | ||
| } | ||
| function selectDefaultHeaders(existingHeaders , defaultHeaders ) { | ||
| if (!defaultHeaders.length) { | ||
| return [] // return early if we don't need to bother | ||
| } | ||
| const definedHeaders = new Set() | ||
| const result = [] | ||
| common.forEachHeader(existingHeaders, (_ , fieldName ) => { | ||
| definedHeaders.add(fieldName.toLowerCase()) | ||
| }) | ||
| common.forEachHeader(defaultHeaders, (value , fieldName ) => { | ||
| if (!definedHeaders.has(fieldName.toLowerCase())) { | ||
| result.push([fieldName, value]) | ||
| } | ||
| }) | ||
| return result | ||
| } | ||
| // Presents a list of Buffers as a Readable | ||
| class ReadableBuffers extends stream.Readable { | ||
| constructor(buffers ) { | ||
| super() | ||
| this.buffers = buffers | ||
| } | ||
| _read() { | ||
| while (this.buffers.length) { | ||
| if (!this.push(this.buffers.shift())) { | ||
| return | ||
| } | ||
| } | ||
| this.push(null) | ||
| } | ||
| } | ||
| function convertBodyToStream(body ) { | ||
| if (common.isStream(body)) { | ||
| return body | ||
| } | ||
| if (body === undefined) { | ||
| return new ReadableBuffers([]) | ||
| } | ||
| if (Buffer.isBuffer(body)) { | ||
| return new ReadableBuffers([body]) | ||
| } | ||
| if (typeof body !== 'string') { | ||
| body = JSON.stringify(body) | ||
| } | ||
| return new ReadableBuffers([Buffer.from(body)]) | ||
| } | ||
| async function playbackInterceptor({ | ||
| decompressedRequest, | ||
| interceptor, | ||
| requestBodyString, | ||
| requestBodyIsUtf8Representable, | ||
| } | ||
| ) { | ||
| const { logger } = interceptor.scope | ||
| interceptor.scope.emit( | ||
| 'request', | ||
| decompressedRequest, | ||
| interceptor, | ||
| requestBodyString, | ||
| ) | ||
| if (typeof interceptor.errorMessage !== 'undefined') { | ||
| let error | ||
| if (typeof interceptor.errorMessage === 'object') { | ||
| error = interceptor.errorMessage | ||
| } else { | ||
| error = new Error(interceptor.errorMessage) | ||
| } | ||
| await new Promise(resolve => | ||
| common.setTimeout(resolve, interceptor.delayBodyInMs), | ||
| ) | ||
| throw error | ||
| } | ||
| logger('response.rawHeaders:', interceptor.rawHeaders) | ||
| // .reply(status, replyFunction) | ||
| if (interceptor.replyFunction) { | ||
| let fn = interceptor.replyFunction | ||
| if (fn.length === 2) { | ||
| // Handle the case of an async reply function, the third parameter being the callback. | ||
| fn = util.promisify(fn) | ||
| } | ||
| // At this point `fn` is either a synchronous function or a promise-returning function; | ||
| // wrapping in `Promise.resolve` makes it into a promise either way. | ||
| return Promise.resolve((fn ).call(interceptor, decompressedRequest)) | ||
| .then(continueWithResponseBody) | ||
| .catch((err ) => { | ||
| throw err | ||
| }) | ||
| } | ||
| // .reply(fullReplyFunction) | ||
| else if (interceptor.fullReplyFunction) { | ||
| let fn = interceptor.fullReplyFunction | ||
| if (fn.length === 2) { | ||
| fn = util.promisify(fn) | ||
| } | ||
| return Promise.resolve((fn ).call(interceptor, decompressedRequest)) | ||
| .then(continueWithFullResponse) | ||
| .catch((err ) => { | ||
| throw err | ||
| }) | ||
| } | ||
| if ( | ||
| common.isContentEncoded(interceptor.headers || {}) && | ||
| !common.isStream(interceptor.body) | ||
| ) { | ||
| // If the content is encoded we know that the response body *must* be an array | ||
| // of response buffers which should be mocked one by one. | ||
| const bufferData = Array.isArray(interceptor.body) | ||
| ? interceptor.body | ||
| : [interceptor.body] | ||
| const responseBuffers = bufferData.map((data ) => | ||
| Buffer.from(data, 'hex'), | ||
| ) | ||
| const responseBody = new ReadableBuffers(responseBuffers) | ||
| return continueWithResponseBody(responseBody) | ||
| } | ||
| // If we get to this point, the body is either a string or an object that | ||
| // will eventually be JSON stringified. | ||
| let responseBody = interceptor.body | ||
| // If the request was not UTF8-representable then we assume that the | ||
| // response won't be either. In that case we send the response as a Buffer | ||
| // object as that's what the client will expect. | ||
| if (!requestBodyIsUtf8Representable && typeof responseBody === 'string') { | ||
| // Try to create the buffer from the interceptor's body response as hex. | ||
| responseBody = Buffer.from(responseBody, 'hex') | ||
| // Creating buffers does not necessarily throw errors; check for difference in size. | ||
| if ( | ||
| !responseBody || | ||
| ((interceptor.body ).length > 0 && responseBody.length === 0) | ||
| ) { | ||
| // We fallback on constructing buffer from utf8 representation of the body. | ||
| responseBody = Buffer.from(interceptor.body , 'utf8') | ||
| } | ||
| } | ||
| return continueWithResponseBody(responseBody) | ||
| function continueWithFullResponse(fullReplyResult ) { | ||
| const [status, responseBody, rawHeaders] = | ||
| parseFullReplyResult(fullReplyResult) | ||
| return continueWithResponseBody(responseBody, status, rawHeaders) | ||
| } | ||
| async function prepareResponseHeaders(body , responseHeaders ) { | ||
| const defaultHeaders = [...interceptor.scope._defaultReplyHeaders] | ||
| const rawHeaders = [] | ||
| // Include a JSON content type when JSON.stringify is called on the body. | ||
| const isJSON = | ||
| body !== undefined && | ||
| typeof body !== 'string' && | ||
| !Buffer.isBuffer(body) && | ||
| !common.isStream(body) | ||
| if (isJSON) { | ||
| defaultHeaders.push('Content-Type', 'application/json') | ||
| } | ||
| common.forEachHeader( | ||
| [...(interceptor.rawHeaders || []), ...responseHeaders], | ||
| (value , fieldName ) => { | ||
| rawHeaders.push([fieldName, value]) | ||
| }, | ||
| ) | ||
| rawHeaders.push( | ||
| ...selectDefaultHeaders( | ||
| [...(interceptor.rawHeaders || []), ...responseHeaders], | ||
| defaultHeaders, | ||
| ), | ||
| ) | ||
| for (let i = 0; i < rawHeaders.length; i++) { | ||
| const [, value] = rawHeaders[i] | ||
| // Evaluate functional headers. | ||
| if (typeof value === 'function') { | ||
| rawHeaders[i][1] = await value(decompressedRequest, body) | ||
| } | ||
| } | ||
| return new Headers(rawHeaders) | ||
| } | ||
| async function continueWithResponseBody( | ||
| rawBody , | ||
| fullResponseStatus , | ||
| fullResponseRawHeaders = [], | ||
| ) { | ||
| const headers = await prepareResponseHeaders( | ||
| rawBody, | ||
| fullResponseRawHeaders, | ||
| ) | ||
| const bodyAsStream = convertBodyToStream(rawBody) | ||
| bodyAsStream.resume() | ||
| // TODO: there is probably a better way to support delay. | ||
| // Wrap the stream in a duplex stream to support the delay. | ||
| const readable = new stream.Readable({ | ||
| read() {}, | ||
| }) | ||
| bodyAsStream.on('data', function (chunk ) { | ||
| readable.push(chunk) | ||
| }) | ||
| bodyAsStream.on('end', function () { | ||
| common.setTimeout(() => { | ||
| readable.push(null) | ||
| interceptor.scope.emit('replied', decompressedRequest, interceptor) | ||
| }, interceptor.delayBodyInMs) | ||
| }) | ||
| bodyAsStream.on('error', function (err ) { | ||
| readable.emit('error', err) | ||
| }) | ||
| const status = interceptor.statusCode || fullResponseStatus | ||
| const hasBody = FetchResponse.isResponseWithBody(status ) | ||
| return new Response(hasBody ? readable : null, { | ||
| status: status , | ||
| statusText: STATUS_CODES[status ], | ||
| headers, | ||
| }) | ||
| } | ||
| } | ||
| export { playbackInterceptor } |
| import type { Definition } from './scope.ts'; | ||
| export interface RecorderOptions { | ||
| dont_print?: boolean; | ||
| output_objects?: boolean; | ||
| enable_reqheaders_recording?: boolean; | ||
| logging?: (content: string) => void; | ||
| use_separator?: boolean; | ||
| } | ||
| declare function record(recOptions?: boolean | RecorderOptions): void; | ||
| declare function restore(): void; | ||
| declare function clear(): void; | ||
| declare function outputs(): string[] | Definition[]; | ||
| export { record, outputs, restore, clear }; |
| import { recorder as debug } from './debug.js' | ||
| import querystring from 'node:querystring' | ||
| import { inspect } from 'node:util' | ||
| import * as common from './common.js' | ||
| import { restoreOverriddenClientRequest } from './intercept.js' | ||
| import { gzipSync, brotliCompressSync, deflateSync } from 'node:zlib' | ||
| import nodeInterceptors from '@mswjs/interceptors/presets/node' | ||
| const SEPARATOR = '\n<<<<<<-- cut here -->>>>>>\n' | ||
| let recordingInProgress = false | ||
| let _outputs = [] | ||
| // TODO: don't reuse the nodeInterceptors, create new ones. | ||
| const clientRequestInterceptor = nodeInterceptors[0] | ||
| const fetchRequestInterceptor = nodeInterceptors[2] | ||
| function getScope(url ) { | ||
| return common.normalizeOrigin(url) | ||
| } | ||
| function getMethod(request ) { | ||
| return request.method || 'GET' | ||
| } | ||
| function getBodyFromChunks(chunks , headers ) { | ||
| // If we have headers and there is content-encoding it means that the body | ||
| // shouldn't be merged but instead persisted as an array of hex strings so | ||
| // that the response chunks can be mocked one by one. | ||
| if (headers && common.isContentEncoded(headers)) { | ||
| return { | ||
| body: chunks.map((chunk ) => chunk.toString('hex')), | ||
| } | ||
| } | ||
| const mergedBuffer = Buffer.concat(chunks) | ||
| // The merged buffer can be one of three things: | ||
| // 1. A UTF-8-representable string buffer which represents a JSON object. | ||
| // 2. A UTF-8-representable buffer which doesn't represent a JSON object. | ||
| // 3. A non-UTF-8-representable buffer which then has to be recorded as a hex string. | ||
| const isUtf8Representable = common.isUtf8Representable(mergedBuffer) | ||
| if (isUtf8Representable) { | ||
| const maybeStringifiedJson = mergedBuffer.toString('utf8') | ||
| try { | ||
| return { | ||
| isUtf8Representable, | ||
| body: JSON.parse(maybeStringifiedJson), | ||
| } | ||
| } catch { | ||
| return { | ||
| isUtf8Representable, | ||
| body: maybeStringifiedJson, | ||
| } | ||
| } | ||
| } else { | ||
| return { | ||
| isUtf8Representable, | ||
| body: mergedBuffer.toString('hex'), | ||
| } | ||
| } | ||
| } | ||
| async function generateRequestAndResponseObject( | ||
| request , | ||
| response , | ||
| ) { | ||
| const { body, isUtf8Representable } = getBodyFromChunks( | ||
| [Buffer.from(await response.arrayBuffer())], | ||
| Object.fromEntries(response.headers.entries()), | ||
| ) | ||
| const url = new URL(request.url) | ||
| const reqheaders = Object.fromEntries(request.headers.entries()) | ||
| return { | ||
| scope: getScope(url), | ||
| method: getMethod(request), | ||
| path: url.pathname + url.search, | ||
| // Is it deliberate that `getBodyFromChunks()` is called a second time? | ||
| body: getBodyFromChunks([Buffer.from(await request.arrayBuffer())]).body, | ||
| status: response.status, | ||
| response: body, | ||
| rawHeaders: Object.fromEntries(response.headers.entries()), | ||
| reqheaders: Object.keys(reqheaders).length > 0 ? reqheaders : undefined, | ||
| // When content-encoding is enabled, isUtf8Representable is `undefined`, | ||
| // so we explicitly check for `false`. | ||
| responseIsBinary: isUtf8Representable === false, | ||
| } | ||
| } | ||
| async function generateRequestAndResponse( | ||
| request , | ||
| response , | ||
| ) { | ||
| const url = new URL(request.url) | ||
| const requestBody = getBodyFromChunks([ | ||
| Buffer.from(await request.arrayBuffer()), | ||
| ]).body | ||
| const responseBody = getBodyFromChunks( | ||
| [Buffer.from(await response.arrayBuffer())], | ||
| response.headers , | ||
| ).body | ||
| const encodedQueryObj = {} | ||
| for (const [key, value] of Object.entries( | ||
| querystring.parse(url.searchParams.toString()), | ||
| )) { | ||
| const formattedPair = common.formatQueryValue( | ||
| key, | ||
| value, | ||
| common.percentEncode, | ||
| ) | ||
| encodedQueryObj[formattedPair[0]] = formattedPair[1] | ||
| } | ||
| const lines = [] | ||
| // We want a leading newline. | ||
| lines.push('') | ||
| const scope = getScope(url) | ||
| lines.push(`nock('${scope}', {"encodedQueryParams":true})`) | ||
| const methodName = getMethod(request).toLowerCase() | ||
| // Escape any single quotes in the path as the output uses them | ||
| const escapedPath = url.pathname.replace(/'/g, `\\'`) | ||
| if (requestBody) { | ||
| lines.push( | ||
| ` .${methodName}('${escapedPath}', ${JSON.stringify(requestBody)})`, | ||
| ) | ||
| } else { | ||
| lines.push(` .${methodName}('${escapedPath}')`) | ||
| } | ||
| request.headers.forEach((value, name) => { | ||
| const safeName = JSON.stringify(name) | ||
| const safeValue = JSON.stringify(value) | ||
| lines.push(` .matchHeader(${safeName}, ${safeValue})`) | ||
| }) | ||
| if (Object.keys(encodedQueryObj).length > 0) { | ||
| lines.push(` .query(${JSON.stringify(encodedQueryObj)})`) | ||
| } | ||
| const statusCode = String(response.status) | ||
| const stringifiedResponseBody = JSON.stringify(responseBody) | ||
| const headers = inspect(Object.fromEntries(response.headers.entries())) | ||
| lines.push(` .reply(${statusCode}, ${stringifiedResponseBody}, ${headers});`) | ||
| return lines.join('\n') | ||
| } | ||
| // This module variable is used to identify a unique recording ID in order to skip | ||
| // spurious requests that sometimes happen. This problem has been, so far, | ||
| // exclusively detected in nock's unit testing where 'checks if callback is specified' | ||
| // interferes with other tests as its t.end() is invoked without waiting for request | ||
| // to finish (which is the point of the test). | ||
| let currentRecordingId = 0 | ||
| const defaultRecordOptions = { | ||
| dont_print: false, | ||
| enable_reqheaders_recording: false, | ||
| logging: console.log, | ||
| output_objects: false, | ||
| use_separator: true, | ||
| } | ||
| function record(recOptions ) { | ||
| // Trying to start recording with recording already in progress implies an error | ||
| // in the recording configuration (double recording makes no sense and used to lead | ||
| // to duplicates in output) | ||
| if (recordingInProgress) { | ||
| throw new Error('Nock recording already in progress') | ||
| } | ||
| recordingInProgress = true | ||
| // Set the new current recording ID and capture its value in this instance of record(). | ||
| currentRecordingId = currentRecordingId + 1 | ||
| const thisRecordingId = currentRecordingId | ||
| // Originally the parameter was a dont_print boolean flag. | ||
| // To keep the existing code compatible we take that case into account. | ||
| if (typeof recOptions === 'boolean') { | ||
| recOptions = { dont_print: recOptions } | ||
| } | ||
| recOptions = { ...defaultRecordOptions, ...recOptions } | ||
| debug('start recording', thisRecordingId, recOptions) | ||
| const { | ||
| dont_print: dontPrint, | ||
| enable_reqheaders_recording: enableReqHeadersRecording, | ||
| logging, | ||
| output_objects: outputObjects, | ||
| use_separator: useSeparator, | ||
| } = recOptions | ||
| debug( | ||
| String(thisRecordingId), | ||
| 'restoring overridden requests before new overrides', | ||
| ) | ||
| // To preserve backward compatibility (starting recording wasn't throwing if nock was already active) | ||
| // we restore any requests that may have been overridden by other parts of nock (e.g. intercept) | ||
| // NOTE: This is hacky as hell but it keeps the backward compatibility *and* allows correct | ||
| // behavior in the face of other modules also overriding ClientRequest. | ||
| // common.restoreOverriddenRequests() | ||
| // We restore ClientRequest as it messes with recording of modules that also override ClientRequest (e.g. xhr2) | ||
| restoreOverriddenClientRequest() | ||
| // We override the requests so that we can save information on them before executing. | ||
| clientRequestInterceptor.apply() | ||
| fetchRequestInterceptor.apply() | ||
| clientRequestInterceptor.on( | ||
| 'response', | ||
| async function ({ request, response } ) { | ||
| await recordResponse(request, response) | ||
| }, | ||
| ) | ||
| fetchRequestInterceptor.on( | ||
| 'response', | ||
| async function ({ request, response } ) { | ||
| // fetch decompresses the body automatically, so we need to recompress it | ||
| const codings = | ||
| response.headers | ||
| .get('content-encoding') | ||
| ?.toLowerCase() | ||
| .split(',') | ||
| .map((c ) => c.trim()) || [] | ||
| let body = await response.arrayBuffer() | ||
| for (const coding of codings) { | ||
| if (coding === 'gzip') { | ||
| body = gzipSync(body) | ||
| } else if (coding === 'deflate') { | ||
| body = deflateSync(body) | ||
| } else if (coding === 'br') { | ||
| body = brotliCompressSync(body) | ||
| } | ||
| } | ||
| await recordResponse(request, new Response(body, response)) | ||
| }, | ||
| ) | ||
| async function recordResponse(mswRequest , mswResponse ) { | ||
| const request = mswRequest.clone() | ||
| const response = mswResponse.clone() | ||
| debug( | ||
| String(thisRecordingId), | ||
| request.url.split(':', 1)[0], | ||
| 'intercepted request ended', | ||
| ) | ||
| // Ignore request headers completely unless it was explicitly enabled by the user (see README) | ||
| if (enableReqHeadersRecording) { | ||
| // We never record user-agent headers as they are worse than useless - | ||
| // they actually make testing more difficult without providing any benefit (see README) | ||
| request.headers.delete('user-agent') | ||
| } else { | ||
| // TODO: request.headers.forEach skip a header, need to investigate it. | ||
| const keys = Array.from(request.headers.keys()) | ||
| for (const header of keys) { | ||
| request.headers.delete(header) | ||
| } | ||
| } | ||
| const generateFn = outputObjects | ||
| ? generateRequestAndResponseObject | ||
| : generateRequestAndResponse | ||
| let out = await generateFn(request, response) | ||
| debug('out:', out) | ||
| // Check that the request was made during the current recording. | ||
| // If it hasn't then skip it. There is no other simple way to handle | ||
| // this as it depends on the timing of requests and responses. Throwing | ||
| // will make some recordings/unit tests fail randomly depending on how | ||
| // fast/slow the response arrived. | ||
| // If you are seeing this error then you need to make sure that all | ||
| // the requests made during a single recording session finish before | ||
| // ending the same recording session. | ||
| if (thisRecordingId !== currentRecordingId) { | ||
| debug('skipping recording of an out-of-order request', out) | ||
| return | ||
| } | ||
| _outputs.push(out) | ||
| if (!dontPrint) { | ||
| if (useSeparator) { | ||
| if (typeof out !== 'string') { | ||
| out = JSON.stringify(out, null, 2) | ||
| } | ||
| ;(logging )(SEPARATOR + out + SEPARATOR) | ||
| } else { | ||
| ;(logging )(out) | ||
| } | ||
| } | ||
| debug('finished setting up intercepting') | ||
| } | ||
| } | ||
| // Restore *all* the overridden http/https modules' properties. | ||
| function restore() { | ||
| debug( | ||
| String(currentRecordingId), | ||
| 'restoring all the overridden http/https properties', | ||
| ) | ||
| clientRequestInterceptor.dispose() | ||
| fetchRequestInterceptor.dispose() | ||
| restoreOverriddenClientRequest() | ||
| recordingInProgress = false | ||
| } | ||
| function clear() { | ||
| _outputs = [] | ||
| } | ||
| function outputs() { | ||
| return _outputs | ||
| } | ||
| export { record, outputs, restore, clear } |
| import type { ReplyBody, ReplyHeaders, RequestBodyMatcher, RequestHeaderMatcher } from './interceptor.ts'; | ||
| export interface Options { | ||
| allowUnmocked?: boolean; | ||
| reqheaders?: Record<string, RequestHeaderMatcher>; | ||
| badheaders?: string[]; | ||
| conditionally?: () => boolean; | ||
| filteringScope?: (scope: string) => boolean; | ||
| encodedQueryParams?: boolean; | ||
| } | ||
| export interface Definition { | ||
| scope: string | RegExp; | ||
| path: string | RegExp; | ||
| port?: number | string; | ||
| method?: string; | ||
| status?: number; | ||
| body?: RequestBodyMatcher; | ||
| reply?: string; | ||
| reqheaders?: Record<string, RequestHeaderMatcher>; | ||
| badheaders?: string[]; | ||
| rawHeaders?: ReplyHeaders; | ||
| response?: ReplyBody; | ||
| responseIsBinary?: boolean; | ||
| headers?: ReplyHeaders; | ||
| options?: Options; | ||
| } | ||
| import { EventEmitter } from 'node:events'; | ||
| import { Interceptor } from './interceptor.ts'; | ||
| declare class Scope extends EventEmitter { | ||
| constructor(basePath: string | RegExp | URL, options?: Options); | ||
| add(key: string, interceptor: Interceptor): void; | ||
| remove(key: string, interceptor: Interceptor): void; | ||
| intercept(uri: string | RegExp | ((path: string) => boolean), method: string, requestBody?: RequestBodyMatcher, interceptorOptions?: Options): Interceptor; | ||
| get(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| post(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| put(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| head(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| patch(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| merge(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| delete(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| options(uri: string | RegExp | ((path: string) => boolean), requestBody?: RequestBodyMatcher, options?: Options): Interceptor; | ||
| pendingMocks(): string[]; | ||
| activeMocks(): string[]; | ||
| isDone(): boolean; | ||
| done(): void; | ||
| buildFilter(): any; | ||
| filteringPath(): this; | ||
| filteringRequestBody(): this; | ||
| matchHeader(name: string, value: RequestHeaderMatcher): this; | ||
| defaultReplyHeaders(headers: ReplyHeaders): this; | ||
| persist(flag?: boolean): this; | ||
| shouldPersist(): boolean; | ||
| replyContentLength(): this; | ||
| replyDate(d?: Date): this; | ||
| clone(): Scope; | ||
| } | ||
| declare function loadDefs(path: string): any; | ||
| declare function load(path: string): Scope[]; | ||
| declare function define(nockDefs: Record<string, any>[]): Scope[]; | ||
| export { Scope, load, loadDefs, define }; |
| import fs from 'node:fs' | ||
| import { scopeDebuglog } from './debug.js' | ||
| import { addInterceptor, isOn } from './intercept.js' | ||
| import * as common from './common.js' | ||
| import assert from 'node:assert' | ||
| import { EventEmitter } from 'node:events' | ||
| import { Interceptor } from './interceptor.js' | ||
| function normalizeUrl(u ) { | ||
| if (typeof u === 'string') { | ||
| // If the url is invalid, let the URL library report it | ||
| return normalizeUrl(new URL(u)) | ||
| } | ||
| if (!/https?:/.test(u.protocol)) { | ||
| throw new TypeError( | ||
| `Protocol '${u.protocol}' not recognized. This commonly occurs when a hostname and port are included without a protocol, producing a URL that is valid but confusing, and probably not what you want.`, | ||
| ) | ||
| } | ||
| return { | ||
| href: u.href, | ||
| origin: u.origin, | ||
| protocol: u.protocol, | ||
| username: u.username, | ||
| password: u.password, | ||
| host: u.host, | ||
| hostname: | ||
| // strip brackets from IPv6 | ||
| typeof u.hostname === 'string' && u.hostname.startsWith('[') | ||
| ? u.hostname.slice(1, -1) | ||
| : u.hostname, | ||
| port: u.port || (u.protocol === 'http:' ? 80 : 443), | ||
| pathname: u.pathname, | ||
| search: u.search, | ||
| searchParams: u.searchParams, | ||
| hash: u.hash, | ||
| } | ||
| } | ||
| class Scope extends EventEmitter { | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| constructor(basePath , options ) { | ||
| super() | ||
| this.keyedInterceptors = {} | ||
| this.interceptors = [] | ||
| this.transformPathFunction = null | ||
| this.transformRequestBodyFunction = null | ||
| this.matchHeaders = [] | ||
| this.scopeOptions = options || {} | ||
| this.urlParts = {} | ||
| this._persist = false | ||
| this.contentLen = false | ||
| this.date = null | ||
| this.basePath = basePath | ||
| this.basePathname = '' | ||
| this.port = null | ||
| this._defaultReplyHeaders = [] | ||
| let logNamespace = String(basePath) | ||
| if (!(basePath instanceof RegExp)) { | ||
| this.urlParts = normalizeUrl(basePath ) | ||
| this.port = this.urlParts.port | ||
| this.basePathname = this.urlParts.pathname.replace(/\/$/, '') | ||
| this.basePath = `${this.urlParts.protocol}//${this.urlParts.hostname}:${this.port}` | ||
| logNamespace = this.urlParts.host | ||
| } | ||
| this.logger = scopeDebuglog(logNamespace) | ||
| } | ||
| add(key , interceptor ) { | ||
| if (!(key in this.keyedInterceptors)) { | ||
| this.keyedInterceptors[key] = [] | ||
| } | ||
| this.keyedInterceptors[key].push(interceptor) | ||
| addInterceptor( | ||
| this.basePath , | ||
| interceptor, | ||
| this, | ||
| this.scopeOptions, | ||
| this.urlParts.hostname , | ||
| ) | ||
| } | ||
| remove(key , interceptor ) { | ||
| if (this._persist) { | ||
| return | ||
| } | ||
| const arr = this.keyedInterceptors[key] | ||
| if (arr) { | ||
| arr.splice(arr.indexOf(interceptor), 1) | ||
| if (arr.length === 0) { | ||
| delete this.keyedInterceptors[key] | ||
| } | ||
| } | ||
| } | ||
| intercept( | ||
| uri , | ||
| method , | ||
| requestBody , | ||
| interceptorOptions , | ||
| ) { | ||
| const ic = new Interceptor( | ||
| this, | ||
| uri, | ||
| method, | ||
| requestBody, | ||
| interceptorOptions, | ||
| ) | ||
| this.interceptors.push(ic) | ||
| return ic | ||
| } | ||
| get( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'GET', requestBody, options) | ||
| } | ||
| post( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'POST', requestBody, options) | ||
| } | ||
| put( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'PUT', requestBody, options) | ||
| } | ||
| head( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'HEAD', requestBody, options) | ||
| } | ||
| patch( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'PATCH', requestBody, options) | ||
| } | ||
| merge( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'MERGE', requestBody, options) | ||
| } | ||
| delete( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'DELETE', requestBody, options) | ||
| } | ||
| options( | ||
| uri , | ||
| requestBody , | ||
| options , | ||
| ) { | ||
| return this.intercept(uri, 'OPTIONS', requestBody, options) | ||
| } | ||
| // Returns the list of keys for non-optional Interceptors that haven't been completed yet. | ||
| pendingMocks() { | ||
| return this.activeMocks().filter((key ) => | ||
| this.keyedInterceptors[key].some( | ||
| ({ interceptionCounter, optional } ) => { | ||
| const persistedAndUsed = this._persist && interceptionCounter > 0 | ||
| return !persistedAndUsed && !optional | ||
| }, | ||
| ), | ||
| ) | ||
| } | ||
| // Returns all keyedInterceptors that are active. | ||
| activeMocks() { | ||
| return Object.keys(this.keyedInterceptors) | ||
| } | ||
| isDone() { | ||
| if (!isOn()) { | ||
| return true | ||
| } | ||
| return this.pendingMocks().length === 0 | ||
| } | ||
| done() { | ||
| assert.ok( | ||
| this.isDone(), | ||
| `Mocks not yet satisfied:\n${this.pendingMocks().join('\n')}`, | ||
| ) | ||
| } | ||
| buildFilter() { | ||
| const filteringArguments = Array.from(arguments) | ||
| if (arguments[0] instanceof RegExp) { | ||
| return function (candidate ) { | ||
| /* istanbul ignore if */ | ||
| if (typeof candidate !== 'string') { | ||
| throw Error( | ||
| `Nock internal assertion failed: typeof candidate is ${typeof candidate}. If you encounter this error, please report it as a bug.`, | ||
| ) | ||
| } | ||
| return candidate.replace(filteringArguments[0], filteringArguments[1]) | ||
| } | ||
| } else if (typeof arguments[0] === 'function') { | ||
| return arguments[0] | ||
| } | ||
| } | ||
| filteringPath() { | ||
| this.transformPathFunction = this.buildFilter.apply(this, arguments ) | ||
| if (!this.transformPathFunction) { | ||
| throw new Error( | ||
| 'Invalid arguments: filtering path should be a function or a regular expression', | ||
| ) | ||
| } | ||
| return this | ||
| } | ||
| filteringRequestBody() { | ||
| this.transformRequestBodyFunction = this.buildFilter.apply( | ||
| this, | ||
| arguments , | ||
| ) | ||
| if (!this.transformRequestBodyFunction) { | ||
| throw new Error( | ||
| 'Invalid arguments: filtering request body should be a function or a regular expression', | ||
| ) | ||
| } | ||
| return this | ||
| } | ||
| matchHeader(name , value ) { | ||
| // We use lower-case header field names throughout Nock. | ||
| this.matchHeaders.push({ name: name.toLowerCase(), value }) | ||
| return this | ||
| } | ||
| defaultReplyHeaders(headers ) { | ||
| this._defaultReplyHeaders = common.headersInputToRawArray(headers) | ||
| return this | ||
| } | ||
| persist(flag = true) { | ||
| if (typeof flag !== 'boolean') { | ||
| throw new Error('Invalid arguments: argument should be a boolean') | ||
| } | ||
| this._persist = flag | ||
| return this | ||
| } | ||
| shouldPersist() { | ||
| return this._persist | ||
| } | ||
| replyContentLength() { | ||
| this.contentLen = true | ||
| return this | ||
| } | ||
| replyDate(d ) { | ||
| this.date = d || new Date() | ||
| return this | ||
| } | ||
| clone() { | ||
| return new Scope(this.basePath, this.scopeOptions) | ||
| } | ||
| } | ||
| function loadDefs(path ) { | ||
| if (!fs) { | ||
| throw new Error('No fs') | ||
| } | ||
| const contents = fs.readFileSync(path) | ||
| return JSON.parse(contents) | ||
| } | ||
| function load(path ) { | ||
| return define(loadDefs(path)) | ||
| } | ||
| function getStatusFromDefinition(nockDef ) { | ||
| // Backward compatibility for when `status` was encoded as string in `reply`. | ||
| if (nockDef.reply !== undefined) { | ||
| const parsedReply = parseInt(nockDef.reply, 10) | ||
| if (isNaN(parsedReply)) { | ||
| throw Error('`reply`, when present, must be a numeric string') | ||
| } | ||
| return parsedReply | ||
| } | ||
| const DEFAULT_STATUS_OK = 200 | ||
| return nockDef.status || DEFAULT_STATUS_OK | ||
| } | ||
| function getScopeFromDefinition(nockDef ) { | ||
| // Backward compatibility for when `port` was part of definition. | ||
| if (nockDef.port !== undefined) { | ||
| // Include `port` into scope if it doesn't exist. | ||
| const url = URL.parse(nockDef.scope) | ||
| if (url.port === '') { | ||
| return `${nockDef.scope}:${nockDef.port}` | ||
| } else { | ||
| if (parseInt(url.port) !== parseInt(nockDef.port)) { | ||
| throw new Error( | ||
| 'Mismatched port numbers in scope and port properties of nock definition.', | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| return nockDef.scope | ||
| } | ||
| function tryJsonParse(string ) { | ||
| try { | ||
| return JSON.parse(string) | ||
| } catch { | ||
| return string | ||
| } | ||
| } | ||
| function define(nockDefs ) { | ||
| const scopes = [] | ||
| nockDefs.forEach(function (nockDef ) { | ||
| const nscope = getScopeFromDefinition(nockDef) | ||
| const npath = nockDef.path | ||
| if (!nockDef.method) { | ||
| throw Error('Method is required') | ||
| } | ||
| const method = nockDef.method.toLowerCase() | ||
| const status = getStatusFromDefinition(nockDef) | ||
| const rawHeaders = nockDef.rawHeaders || [] | ||
| const reqheaders = nockDef.reqheaders || {} | ||
| const badheaders = nockDef.badheaders || [] | ||
| const options = { ...nockDef.options } | ||
| // We use request headers for both filtering (see below) and mocking. | ||
| // Here we are setting up mocked request headers but we don't want to | ||
| // be changing the user's options object so we clone it first. | ||
| options.reqheaders = reqheaders | ||
| options.badheaders = badheaders | ||
| // Response is not always JSON as it could be a string or binary data or | ||
| // even an array of binary buffers (e.g. when content is encoded). | ||
| let response | ||
| if (!nockDef.response) { | ||
| response = '' | ||
| // TODO: Rename `responseIsBinary` to `responseIsUtf8Representable`. | ||
| } else if (nockDef.responseIsBinary) { | ||
| response = Buffer.from(nockDef.response, 'hex') | ||
| } else { | ||
| response = | ||
| typeof nockDef.response === 'string' | ||
| ? tryJsonParse(nockDef.response) | ||
| : nockDef.response | ||
| } | ||
| const scope = new Scope(nscope, options) | ||
| // If request headers were specified filter by them. | ||
| Object.entries(reqheaders).forEach(([fieldName, value]) => { | ||
| scope.matchHeader(fieldName, value ) | ||
| }) | ||
| const acceptableFilters = ['filteringRequestBody', 'filteringPath'] | ||
| acceptableFilters.forEach(filter => { | ||
| if (nockDef[filter]) { | ||
| ;(scope )[filter](nockDef[filter]) | ||
| } | ||
| }) | ||
| scope | ||
| .intercept(npath, method, nockDef.body) | ||
| .reply(status, response, rawHeaders) | ||
| scopes.push(scope) | ||
| }) | ||
| return scopes | ||
| } | ||
| export { Scope, load, loadDefs, define } |
| /** | ||
| * @module stringify | ||
| * @private | ||
| * Safe JSON.stringify that handles circular references. | ||
| * @see https://github.com/moll/json-stringify-safe/blob/02cfafd45f06d076ac4bf0dd28be6738a07a72f9/stringify.js | ||
| * @license | ||
| * The ISC License | ||
| * | ||
| * Copyright (c) Isaac Z. Schlueter and Contributors | ||
| * | ||
| * Permission to use, copy, modify, and/or distribute this software for any | ||
| * purpose with or without fee is hereby granted, provided that the above | ||
| * copyright notice and this permission notice appear in all copies. | ||
| * | ||
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
| * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
| * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
| * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
| * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
| * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR | ||
| * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
| */ | ||
| declare function stringify(obj: any): string; | ||
| export default stringify; |
| /** | ||
| * @module stringify | ||
| * @private | ||
| * Safe JSON.stringify that handles circular references. | ||
| * @see https://github.com/moll/json-stringify-safe/blob/02cfafd45f06d076ac4bf0dd28be6738a07a72f9/stringify.js | ||
| * @license | ||
| * The ISC License | ||
| * | ||
| * Copyright (c) Isaac Z. Schlueter and Contributors | ||
| * | ||
| * Permission to use, copy, modify, and/or distribute this software for any | ||
| * purpose with or without fee is hereby granted, provided that the above | ||
| * copyright notice and this permission notice appear in all copies. | ||
| * | ||
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
| * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
| * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
| * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
| * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
| * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR | ||
| * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
| */ | ||
| function stringify(obj ) { | ||
| return JSON.stringify(obj, safeReplacer()) | ||
| } | ||
| function cycleReplacer(stack , keys , value ) { | ||
| if (stack[0] === value) return '[Circular ~]' | ||
| return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]` | ||
| } | ||
| function safeReplacer(stack = [], keys = []) { | ||
| return function ( key , value ) { | ||
| if (stack.length > 0) { | ||
| const thisPos = stack.indexOf(this) | ||
| ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) | ||
| ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) | ||
| if (~stack.indexOf(value)) { | ||
| value = cycleReplacer(stack, keys, value) | ||
| } | ||
| } else { | ||
| stack.push(value) | ||
| } | ||
| return value | ||
| } | ||
| } | ||
| export default stringify |
| import { Readable as ReadableStream } from 'node:stream'; | ||
| declare function getGetRequestBody(request: Request): ReadableStream; | ||
| declare function setGetRequestBody(request: Request, body: ArrayBuffer): void; | ||
| export { getGetRequestBody, setGetRequestBody }; |
| import { Readable as ReadableStream } from 'node:stream' | ||
| const kGetRequestBody = Symbol('kGetRequestBody') | ||
| function getGetRequestBody(request ) { | ||
| if (request.method !== 'GET') { | ||
| throw new Error('The request method must be GET') | ||
| } | ||
| return ReadableStream.from(Reflect.get(request, kGetRequestBody)) | ||
| } | ||
| function setGetRequestBody(request , body ) { | ||
| Reflect.set(request, kGetRequestBody, Buffer.from(body)) | ||
| } | ||
| export { getGetRequestBody, setGetRequestBody } |
+22
-17
@@ -10,3 +10,3 @@ { | ||
| ], | ||
| "version": "15.0.0-beta.10", | ||
| "version": "15.0.0-beta.11", | ||
| "author": "Pedro Teixeira <pedro.teixeira@gmail.com>", | ||
@@ -21,6 +21,13 @@ "repository": { | ||
| "engines": { | ||
| "node": ">=18.20.0 <20 || >=20.12.1" | ||
| "node": ">=20.12.1" | ||
| }, | ||
| "main": "./index.js", | ||
| "types": "types", | ||
| "main": "./dist/index.js", | ||
| "type": "module", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| } | ||
| }, | ||
| "dependencies": { | ||
@@ -30,8 +37,7 @@ "@mswjs/interceptors": "^0.41.2" | ||
| "devDependencies": { | ||
| "@definitelytyped/dtslint": "^0.0.163", | ||
| "@eslint/js": "^9.27.0", | ||
| "@jest/globals": "^29.7.0", | ||
| "@sinonjs/fake-timers": "^11.2.2", | ||
| "@types/node": "^22.15.3", | ||
| "assert-rejects": "^1.0.0", | ||
| "c8": "^11.0.0", | ||
| "chai": "^4.1.2", | ||
@@ -46,6 +52,4 @@ "dirty-chai": "^2.0.1", | ||
| "got": "^11.8.6", | ||
| "jest": "^29.7.0", | ||
| "mocha": "^11.7.2", | ||
| "npm-run-all": "^4.1.5", | ||
| "nyc": "^15.0.0", | ||
| "prettier": "3.6.2", | ||
@@ -56,3 +60,4 @@ "rimraf": "^3.0.0", | ||
| "sinon-chai": "^3.7.0", | ||
| "typescript": "^5.0.4", | ||
| "typescript": "^6.0.3", | ||
| "typescript-eslint": "^8.58.2", | ||
| "undici": "^7.9.0" | ||
@@ -64,14 +69,14 @@ }, | ||
| "lint": "run-p lint:js lint:ts", | ||
| "lint:js": "eslint --cache --cache-location './.cache/eslint' '**/*.js'", | ||
| "lint:js:fix": "eslint --cache --cache-location './.cache/eslint' --fix '**/*.js'", | ||
| "lint:ts": "dtslint --expectOnly types", | ||
| "test": "nyc --reporter=lcov --reporter=text mocha --recursive tests", | ||
| "lint:js": "eslint --cache --cache-location './.cache/eslint' '**/*.{js,ts}'", | ||
| "lint:fix": "eslint --cache --cache-location './.cache/eslint' --fix '**/*.{js,ts}'", | ||
| "lint:ts": "tsc --noEmit", | ||
| "build": "rimraf dist && node scripts/build.js && tsc -p tsconfig.build.json", | ||
| "test": "c8 --reporter=lcov --reporter=text mocha --recursive tests", | ||
| "test:coverage": "open coverage/lcov-report/index.html", | ||
| "test:jest": "jest tests_jest --detectLeaks" | ||
| "test:leak": "node --expose-gc scripts/test-leak.js", | ||
| "test:build": "npm run build && node scripts/test-build.js" | ||
| }, | ||
| "license": "MIT", | ||
| "files": [ | ||
| "index.js", | ||
| "lib", | ||
| "types/index.d.ts" | ||
| "dist" | ||
| ], | ||
@@ -78,0 +83,0 @@ "release": { |
-55
| 'use strict' | ||
| const back = require('./lib/back') | ||
| const emitter = require('./lib/global_emitter') | ||
| const { | ||
| activate, | ||
| isActive, | ||
| isDone, | ||
| isOn, | ||
| pendingMocks, | ||
| activeMocks, | ||
| removeInterceptor, | ||
| disableNetConnect, | ||
| enableNetConnect, | ||
| removeAll, | ||
| abortPendingRequests, | ||
| } = require('./lib/intercept') | ||
| const recorder = require('./lib/recorder') | ||
| const { Scope, load, loadDefs, define } = require('./lib/scope') | ||
| const { getGetRequestBody } = require('./lib/utils/node') | ||
| module.exports = (basePath, options) => new Scope(basePath, options) | ||
| Object.assign(module.exports, { | ||
| activate, | ||
| isActive, | ||
| isDone, | ||
| pendingMocks, | ||
| activeMocks, | ||
| removeInterceptor, | ||
| disableNetConnect, | ||
| enableNetConnect, | ||
| cleanAll: removeAll, | ||
| abortPendingRequests, | ||
| load, | ||
| loadDefs, | ||
| define, | ||
| emitter, | ||
| recorder: { | ||
| rec: recorder.record, | ||
| clear: recorder.clear, | ||
| play: recorder.outputs, | ||
| }, | ||
| restore: recorder.restore, | ||
| back, | ||
| getGetRequestBody, | ||
| }) | ||
| // We always activate Nock on import, overriding the globals. | ||
| // Setting the Back mode "activates" Nock by overriding the global entries in the `http/s` modules. | ||
| // If Nock Back is configured, we need to honor that setting for backward compatibility, | ||
| // otherwise we rely on Nock Back's default initializing side effect. | ||
| if (isOn()) { | ||
| back.setMode(process.env.NOCK_BACK_MODE || 'dryrun') | ||
| } |
-344
| 'use strict' | ||
| const fs = require('node:fs') | ||
| const assert = require('node:assert') | ||
| const recorder = require('./recorder') | ||
| const { | ||
| activate, | ||
| disableNetConnect, | ||
| enableNetConnect, | ||
| removeAll: cleanAll, | ||
| } = require('./intercept') | ||
| const { loadDefs, define } = require('./scope') | ||
| const { back: debug } = require('./debug') | ||
| const { format } = require('node:util') | ||
| const path = require('node:path') | ||
| let _mode = null | ||
| /** | ||
| * nock the current function with the fixture given | ||
| * | ||
| * @param {string} fixtureName - the name of the fixture, e.x. 'foo.json' | ||
| * @param {object} options - [optional] extra options for nock with, e.x. `{ assert: true }` | ||
| * @param {function} nockedFn - [optional] callback function to be executed with the given fixture being loaded; | ||
| * if defined the function will be called with context `{ scopes: loaded_nocks || [] }` | ||
| * set as `this` and `nockDone` callback function as first and only parameter; | ||
| * if not defined a promise resolving to `{nockDone, context}` where `context` is | ||
| * aforementioned `{ scopes: loaded_nocks || [] }` | ||
| * | ||
| * List of options: | ||
| * | ||
| * @param {function} before - a preprocessing function, gets called before nock.define | ||
| * @param {function} after - a postprocessing function, gets called after nock.define | ||
| * @param {function} afterRecord - a postprocessing function, gets called after recording. Is passed the array | ||
| * of scopes recorded and should return the array scopes to save to the fixture | ||
| * @param {function} recorder - custom options to pass to the recorder | ||
| * | ||
| */ | ||
| function Back(fixtureName, options, nockedFn) { | ||
| if (!Back.fixtures) { | ||
| throw new Error( | ||
| 'Back requires nock.back.fixtures to be set\n' + | ||
| 'Ex:\n' + | ||
| "\trequire(nock).back.fixtures = '/path/to/fixtures/'", | ||
| ) | ||
| } | ||
| if (typeof fixtureName !== 'string') { | ||
| throw new Error('Parameter fixtureName must be a string') | ||
| } | ||
| if (arguments.length === 1) { | ||
| options = {} | ||
| } else if (arguments.length === 2) { | ||
| // If 2nd parameter is a function then `options` has been omitted | ||
| // otherwise `options` haven't been omitted but `nockedFn` was. | ||
| if (typeof options === 'function') { | ||
| nockedFn = options | ||
| options = {} | ||
| } | ||
| } | ||
| _mode.setup() | ||
| const fixture = path.join(Back.fixtures, fixtureName) | ||
| const context = _mode.start(fixture, options) | ||
| const nockDone = function () { | ||
| _mode.finish(fixture, options, context) | ||
| } | ||
| debug('context:', context) | ||
| // If nockedFn is a function then invoke it, otherwise return a promise resolving to nockDone. | ||
| if (typeof nockedFn === 'function') { | ||
| nockedFn.call(context, nockDone) | ||
| } else { | ||
| return Promise.resolve({ nockDone, context }) | ||
| } | ||
| } | ||
| /******************************************************************************* | ||
| * Modes * | ||
| *******************************************************************************/ | ||
| const wild = { | ||
| setup: function () { | ||
| cleanAll() | ||
| recorder.restore() | ||
| activate() | ||
| enableNetConnect() | ||
| }, | ||
| start: function () { | ||
| return load() // don't load anything but get correct context | ||
| }, | ||
| finish: function () { | ||
| // nothing to do | ||
| }, | ||
| } | ||
| const dryrun = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| cleanAll() | ||
| activate() | ||
| // We have to explicitly enable net connectivity as by default it's off. | ||
| enableNetConnect() | ||
| }, | ||
| start: function (fixture, options) { | ||
| const contexts = load(fixture, options) | ||
| enableNetConnect() | ||
| return contexts | ||
| }, | ||
| finish: function () { | ||
| // nothing to do | ||
| }, | ||
| } | ||
| const record = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| recorder.clear() | ||
| cleanAll() | ||
| activate() | ||
| disableNetConnect() | ||
| }, | ||
| start: function (fixture, options) { | ||
| if (!fs) { | ||
| throw new Error('no fs') | ||
| } | ||
| const context = load(fixture, options) | ||
| if (!context.isLoaded) { | ||
| recorder.record({ | ||
| dont_print: true, | ||
| output_objects: true, | ||
| ...options.recorder, | ||
| }) | ||
| context.isRecording = true | ||
| } | ||
| return context | ||
| }, | ||
| finish: function (fixture, options, context) { | ||
| if (context.isRecording) { | ||
| let outputs = recorder.outputs() | ||
| if (typeof options.afterRecord === 'function') { | ||
| outputs = options.afterRecord(outputs) | ||
| } | ||
| outputs = | ||
| typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4) | ||
| debug('recorder outputs:', outputs) | ||
| fs.mkdirSync(path.dirname(fixture), { recursive: true }) | ||
| fs.writeFileSync(fixture, outputs) | ||
| } | ||
| }, | ||
| } | ||
| const update = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| recorder.clear() | ||
| cleanAll() | ||
| activate() | ||
| disableNetConnect() | ||
| }, | ||
| start: function (fixture, options) { | ||
| if (!fs) { | ||
| throw new Error('no fs') | ||
| } | ||
| const context = removeFixture(fixture) | ||
| recorder.record({ | ||
| dont_print: true, | ||
| output_objects: true, | ||
| ...options.recorder, | ||
| }) | ||
| context.isRecording = true | ||
| return context | ||
| }, | ||
| finish: function (fixture, options) { | ||
| let outputs = recorder.outputs() | ||
| if (typeof options.afterRecord === 'function') { | ||
| outputs = options.afterRecord(outputs) | ||
| } | ||
| outputs = | ||
| typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4) | ||
| debug('recorder outputs:', outputs) | ||
| fs.mkdirSync(path.dirname(fixture), { recursive: true }) | ||
| fs.writeFileSync(fixture, outputs) | ||
| }, | ||
| } | ||
| const lockdown = { | ||
| setup: function () { | ||
| recorder.restore() | ||
| recorder.clear() | ||
| cleanAll() | ||
| activate() | ||
| disableNetConnect() | ||
| }, | ||
| start: function (fixture, options) { | ||
| return load(fixture, options) | ||
| }, | ||
| finish: function () { | ||
| // nothing to do | ||
| }, | ||
| } | ||
| function load(fixture, options) { | ||
| const context = { | ||
| scopes: [], | ||
| assertScopesFinished: function () { | ||
| assertScopes(this.scopes, fixture) | ||
| }, | ||
| query: function () { | ||
| const nested = this.scopes.map(scope => | ||
| scope.interceptors.map(interceptor => ({ | ||
| method: interceptor.method, | ||
| uri: interceptor.uri, | ||
| basePath: interceptor.basePath, | ||
| path: interceptor.path, | ||
| queries: interceptor.queries, | ||
| counter: interceptor.counter, | ||
| body: interceptor.body, | ||
| statusCode: interceptor.statusCode, | ||
| optional: interceptor.optional, | ||
| })), | ||
| ) | ||
| return [].concat.apply([], nested) | ||
| }, | ||
| } | ||
| if (fixture && fixtureExists(fixture)) { | ||
| let scopes = loadDefs(fixture) | ||
| applyHook(scopes, options.before) | ||
| scopes = define(scopes) | ||
| applyHook(scopes, options.after) | ||
| context.scopes = scopes | ||
| context.isLoaded = true | ||
| } | ||
| return context | ||
| } | ||
| function removeFixture(fixture) { | ||
| const context = { | ||
| scopes: [], | ||
| assertScopesFinished: function () {}, | ||
| } | ||
| if (fixture && fixtureExists(fixture)) { | ||
| /* istanbul ignore next - fs.unlinkSync is for node 10 support */ | ||
| fs.rmSync ? fs.rmSync(fixture) : fs.unlinkSync(fixture) | ||
| } | ||
| context.isLoaded = false | ||
| return context | ||
| } | ||
| function applyHook(scopes, fn) { | ||
| if (!fn) { | ||
| return | ||
| } | ||
| if (typeof fn !== 'function') { | ||
| throw new Error('processing hooks must be a function') | ||
| } | ||
| scopes.forEach(fn) | ||
| } | ||
| function fixtureExists(fixture) { | ||
| if (!fs) { | ||
| throw new Error('no fs') | ||
| } | ||
| return fs.existsSync(fixture) | ||
| } | ||
| function assertScopes(scopes, fixture) { | ||
| const pending = scopes | ||
| .filter(scope => !scope.isDone()) | ||
| .map(scope => scope.pendingMocks()) | ||
| if (pending.length) { | ||
| assert.fail( | ||
| format( | ||
| '%j was not used, consider removing %s to rerecord fixture', | ||
| [].concat(...pending), | ||
| fixture, | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
| const Modes = { | ||
| wild, // all requests go out to the internet, dont replay anything, doesnt record anything | ||
| dryrun, // use recorded nocks, allow http calls, doesnt record anything, useful for writing new tests (default) | ||
| record, // use recorded nocks, record new nocks | ||
| update, // allow http calls, record all nocks, don't use recorded nocks | ||
| lockdown, // use recorded nocks, disables all http calls even when not nocked, doesnt record | ||
| } | ||
| Back.setMode = function (mode) { | ||
| if (!(mode in Modes)) { | ||
| throw new Error(`Unknown mode: ${mode}`) | ||
| } | ||
| Back.currentMode = mode | ||
| debug('New nock back mode:', Back.currentMode) | ||
| _mode = Modes[mode] | ||
| _mode.setup() | ||
| } | ||
| Back.fixtures = null | ||
| Back.currentMode = null | ||
| module.exports = Back |
-647
| 'use strict' | ||
| const { common: debug } = require('./debug') | ||
| const timers = require('node:timers') | ||
| const util = require('node:util') | ||
| const zlib = require('node:zlib') | ||
| /** | ||
| * Normalizes the request options so that it always has `host` property. | ||
| * | ||
| * @param {Object} options - a parsed options object of the request | ||
| */ | ||
| function normalizeRequestOptions(options) { | ||
| options.proto = options.proto || 'http' | ||
| options.port = options.port || (options.proto === 'http' ? 80 : 443) | ||
| if (options.host) { | ||
| debug('options.host:', options.host) | ||
| if (!options.hostname) { | ||
| if (options.host.split(':').length === 2) { | ||
| options.hostname = options.host.split(':')[0] | ||
| } else { | ||
| options.hostname = options.host | ||
| } | ||
| } | ||
| } | ||
| debug('options.hostname in the end: %j', options.hostname) | ||
| options.host = `${options.hostname || 'localhost'}:${options.port}` | ||
| debug('options.host in the end: %j', options.host) | ||
| /// lowercase host names | ||
| ;['hostname', 'host'].forEach(function (attr) { | ||
| if (options[attr]) { | ||
| options[attr] = options[attr].toLowerCase() | ||
| } | ||
| }) | ||
| return options | ||
| } | ||
| /** | ||
| * Returns true if the data contained in buffer can be reconstructed | ||
| * from its utf8 representation. | ||
| * | ||
| * @param {ArrayBuffer} buffer | ||
| * @returns {boolean} | ||
| */ | ||
| function isUtf8Representable(buffer) { | ||
| try { | ||
| new TextDecoder('utf8', { fatal: true }).decode(buffer) | ||
| return true | ||
| } catch { | ||
| return false | ||
| } | ||
| } | ||
| /** | ||
| * In WHATWG URL vernacular, this returns the origin portion of a URL. | ||
| * However, the port is not included if it's standard and not already present on the host. | ||
| * | ||
| * @param {URL} url | ||
| */ | ||
| function normalizeOrigin(url) { | ||
| // Remove brackets from hostname if IPV6 | ||
| const normalizedOrigin = url.hostname.startsWith('[') | ||
| ? `${url.protocol}//${url.hostname.slice(1, -1)}${url.port ? `:${url.port}` : ''}` | ||
| : url.origin | ||
| if (url.port) { | ||
| return normalizedOrigin | ||
| } else { | ||
| return normalizedOrigin + (url.protocol === 'http:' ? ':80' : ':443') | ||
| } | ||
| } | ||
| /** | ||
| * Get high level information about request as string | ||
| * @param {Request} request | ||
| * @param {string} body | ||
| */ | ||
| function stringifyRequest(request, body) { | ||
| const url = new URL(request.url) | ||
| const log = { | ||
| method: request.method, | ||
| url: `${url.origin}${url.pathname}`, | ||
| headers: Object.fromEntries(request.headers.entries()), | ||
| } | ||
| if (body) { | ||
| log.body = body | ||
| } | ||
| return JSON.stringify(log, null, 2) | ||
| } | ||
| function isContentEncoded(headers) { | ||
| const contentEncoding = headers['content-encoding'] | ||
| return typeof contentEncoding === 'string' && contentEncoding !== '' | ||
| } | ||
| /** | ||
| * @param {Headers} headers | ||
| * @param {'gzip' | 'deflate'} encoder | ||
| * @returns | ||
| */ | ||
| function contentEncoding(headers, encoder) { | ||
| const contentEncoding = headers.get('content-encoding') | ||
| return contentEncoding?.toString() === encoder | ||
| } | ||
| /** | ||
| * @param {Headers} headers | ||
| */ | ||
| function isJSONContent(headers) { | ||
| // https://tools.ietf.org/html/rfc8259 | ||
| const contentType = String(headers.get('content-type') || '').toLowerCase() | ||
| return contentType.startsWith('application/json') | ||
| } | ||
| /** | ||
| * Return a new object with all field names of the headers lower-cased. | ||
| * | ||
| * Duplicates throw an error. | ||
| */ | ||
| function headersFieldNamesToLowerCase(headers, throwOnDuplicate) { | ||
| if (!isPlainObject(headers)) { | ||
| throw Error('Headers must be provided as an object') | ||
| } | ||
| const lowerCaseHeaders = {} | ||
| Object.entries(headers).forEach(([fieldName, fieldValue]) => { | ||
| const key = fieldName.toLowerCase() | ||
| if (lowerCaseHeaders[key] !== undefined) { | ||
| if (throwOnDuplicate) { | ||
| throw Error( | ||
| `Failed to convert header keys to lower case due to field name conflict: ${key}`, | ||
| ) | ||
| } else { | ||
| debug( | ||
| `Duplicate header provided in request: ${key}. Only the last value can be matched.`, | ||
| ) | ||
| } | ||
| } | ||
| lowerCaseHeaders[key] = fieldValue | ||
| }) | ||
| return lowerCaseHeaders | ||
| } | ||
| const headersFieldsArrayToLowerCase = headers => [ | ||
| ...new Set(headers.map(fieldName => fieldName.toLowerCase())), | ||
| ] | ||
| /** | ||
| * Converts the various accepted formats of headers into a flat array representing "raw headers". | ||
| * | ||
| * Nock allows headers to be provided as a raw array, a plain object, or a Map. | ||
| * | ||
| * While all the header names are expected to be strings, the values are left intact as they can | ||
| * be functions, strings, or arrays of strings. | ||
| * | ||
| * https://nodejs.org/api/http.html#http_message_rawheaders | ||
| */ | ||
| function headersInputToRawArray(headers) { | ||
| if (headers === undefined) { | ||
| return [] | ||
| } | ||
| if (Array.isArray(headers)) { | ||
| // If the input is an array, assume it's already in the raw format and simply return a copy | ||
| // but throw an error if there aren't an even number of items in the array | ||
| if (headers.length % 2) { | ||
| throw new Error( | ||
| `Raw headers must be provided as an array with an even number of items. [fieldName, value, ...]`, | ||
| ) | ||
| } | ||
| return [...headers] | ||
| } | ||
| // [].concat(...) is used instead of Array.flat until v11 is the minimum Node version | ||
| if (util.types.isMap(headers)) { | ||
| return [].concat(...Array.from(headers, ([k, v]) => [k.toString(), v])) | ||
| } | ||
| if (isPlainObject(headers)) { | ||
| return [].concat(...Object.entries(headers)) | ||
| } | ||
| throw new Error( | ||
| `Headers must be provided as an array of raw values, a Map, or a plain Object. ${headers}`, | ||
| ) | ||
| } | ||
| /** | ||
| * Converts an array of raw headers to an object, using the same rules as Nodes `http.IncomingMessage.headers`. | ||
| * | ||
| * Header names/keys are lower-cased. | ||
| */ | ||
| function headersArrayToObject(rawHeaders) { | ||
| if (!Array.isArray(rawHeaders)) { | ||
| throw Error('Expected a header array') | ||
| } | ||
| const accumulator = {} | ||
| forEachHeader(rawHeaders, (value, fieldName) => { | ||
| addHeaderLine(accumulator, fieldName, value) | ||
| }) | ||
| return accumulator | ||
| } | ||
| const noDuplicatesHeaders = new Set([ | ||
| 'age', | ||
| 'authorization', | ||
| 'content-length', | ||
| 'content-type', | ||
| 'etag', | ||
| 'expires', | ||
| 'from', | ||
| 'host', | ||
| 'if-modified-since', | ||
| 'if-unmodified-since', | ||
| 'last-modified', | ||
| 'location', | ||
| 'max-forwards', | ||
| 'proxy-authorization', | ||
| 'referer', | ||
| 'retry-after', | ||
| 'user-agent', | ||
| ]) | ||
| /** | ||
| * Set key/value data in accordance with Node's logic for folding duplicate headers. | ||
| * | ||
| * The `value` param should be a function, string, or array of strings. | ||
| * | ||
| * Node's docs and source: | ||
| * https://nodejs.org/api/http.html#http_message_headers | ||
| * https://github.com/nodejs/node/blob/908292cf1f551c614a733d858528ffb13fb3a524/lib/_http_incoming.js#L245 | ||
| * | ||
| * Header names are lower-cased. | ||
| * Duplicates in raw headers are handled in the following ways, depending on the header name: | ||
| * - Duplicates of field names listed in `noDuplicatesHeaders` (above) are discarded. | ||
| * - `set-cookie` is always an array. Duplicates are added to the array. | ||
| * - For duplicate `cookie` headers, the values are joined together with '; '. | ||
| * - For all other headers, the values are joined together with ', '. | ||
| * | ||
| * Node's implementation is larger because it highly optimizes for not having to call `toLowerCase()`. | ||
| * We've opted to always call `toLowerCase` in exchange for a more concise function. | ||
| * | ||
| * While Node has the luxury of knowing `value` is always a string, we do an extra step of coercion at the top. | ||
| */ | ||
| function addHeaderLine(headers, name, value) { | ||
| let values // code below expects `values` to be an array of strings | ||
| if (typeof value === 'function') { | ||
| // Function values are evaluated towards the end of the response, before that we use a placeholder | ||
| // string just to designate that the header exists. Useful when `Content-Type` is set with a function. | ||
| values = [value.name] | ||
| } else if (Array.isArray(value)) { | ||
| values = value.map(String) | ||
| } else { | ||
| values = [String(value)] | ||
| } | ||
| const key = name.toLowerCase() | ||
| if (key === 'set-cookie') { | ||
| // Array header -- only Set-Cookie at the moment | ||
| if (headers['set-cookie'] === undefined) { | ||
| headers['set-cookie'] = values | ||
| } else { | ||
| headers['set-cookie'].push(...values) | ||
| } | ||
| } else if (noDuplicatesHeaders.has(key)) { | ||
| if (headers[key] === undefined) { | ||
| // Drop duplicates | ||
| headers[key] = values[0] | ||
| } | ||
| } else { | ||
| if (headers[key] !== undefined) { | ||
| values = [headers[key], ...values] | ||
| } | ||
| const separator = key === 'cookie' ? '; ' : ', ' | ||
| headers[key] = values.join(separator) | ||
| } | ||
| } | ||
| /** | ||
| * Deletes the given `fieldName` property from `headers` object by performing | ||
| * case-insensitive search through keys. | ||
| * | ||
| * @headers {Object} headers - object of header field names and values | ||
| * @fieldName {String} field name - string with the case-insensitive field name | ||
| */ | ||
| function deleteHeadersField(headers, fieldNameToDelete) { | ||
| if (!isPlainObject(headers)) { | ||
| throw Error('headers must be an object') | ||
| } | ||
| if (typeof fieldNameToDelete !== 'string') { | ||
| throw Error('field name must be a string') | ||
| } | ||
| const lowerCaseFieldNameToDelete = fieldNameToDelete.toLowerCase() | ||
| // Search through the headers and delete all values whose field name matches the given field name. | ||
| Object.keys(headers) | ||
| .filter(fieldName => fieldName.toLowerCase() === lowerCaseFieldNameToDelete) | ||
| .forEach(fieldName => delete headers[fieldName]) | ||
| } | ||
| /** | ||
| * Utility for iterating over a raw headers array. | ||
| * | ||
| * The callback is called with: | ||
| * - The header value. string, array of strings, or a function | ||
| * - The header field name. string | ||
| * - Index of the header field in the raw header array. | ||
| */ | ||
| function forEachHeader(rawHeaders, callback) { | ||
| for (let i = 0; i < rawHeaders.length; i += 2) { | ||
| callback(rawHeaders[i + 1], rawHeaders[i], i) | ||
| } | ||
| } | ||
| function percentDecode(str) { | ||
| try { | ||
| return decodeURIComponent(str.replace(/\+/g, ' ')) | ||
| } catch { | ||
| return str | ||
| } | ||
| } | ||
| /** | ||
| * URI encode the provided string, stringently adhering to RFC 3986. | ||
| * | ||
| * RFC 3986 reserves !, ', (, ), and * but encodeURIComponent does not encode them so we do it manually. | ||
| * | ||
| * https://tools.ietf.org/html/rfc3986 | ||
| * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent | ||
| */ | ||
| function percentEncode(str) { | ||
| return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { | ||
| return `%${c.charCodeAt(0).toString(16).toUpperCase()}` | ||
| }) | ||
| } | ||
| function matchStringOrRegexp(target, pattern) { | ||
| const targetStr = | ||
| target === undefined || target === null ? '' : String(target) | ||
| if (pattern instanceof RegExp) { | ||
| // if the regexp happens to have a global flag, we want to ensure we test the entire target | ||
| pattern.lastIndex = 0 | ||
| return pattern.test(targetStr) | ||
| } | ||
| return targetStr === String(pattern) | ||
| } | ||
| /** | ||
| * Formats a query parameter. | ||
| * | ||
| * @param key The key of the query parameter to format. | ||
| * @param value The value of the query parameter to format. | ||
| * @param stringFormattingFn The function used to format string values. Can | ||
| * be used to encode or decode the query value. | ||
| * | ||
| * @returns *[] the formatted [key, value] pair. | ||
| */ | ||
| function formatQueryValue(key, value, stringFormattingFn) { | ||
| // TODO: Probably refactor code to replace `switch(true)` with `if`/`else`. | ||
| switch (true) { | ||
| case typeof value === 'number': // fall-through | ||
| case typeof value === 'boolean': | ||
| value = value.toString() | ||
| break | ||
| case value === null: | ||
| case value === undefined: | ||
| value = '' | ||
| break | ||
| case typeof value === 'string': | ||
| if (stringFormattingFn) { | ||
| value = stringFormattingFn(value) | ||
| } | ||
| break | ||
| case value instanceof RegExp: | ||
| break | ||
| case Array.isArray(value): { | ||
| value = value.map(function (val, idx) { | ||
| return formatQueryValue(idx, val, stringFormattingFn)[1] | ||
| }) | ||
| break | ||
| } | ||
| case typeof value === 'object': { | ||
| value = Object.entries(value).reduce(function (acc, [subKey, subVal]) { | ||
| const subPair = formatQueryValue(subKey, subVal, stringFormattingFn) | ||
| acc[subPair[0]] = subPair[1] | ||
| return acc | ||
| }, {}) | ||
| break | ||
| } | ||
| } | ||
| if (stringFormattingFn) key = stringFormattingFn(key) | ||
| return [key, value] | ||
| } | ||
| function isStream(obj) { | ||
| return ( | ||
| obj && | ||
| typeof obj !== 'string' && | ||
| !Buffer.isBuffer(obj) && | ||
| typeof obj.setEncoding === 'function' | ||
| ) | ||
| } | ||
| /** | ||
| * Determines if request data matches the expected schema. | ||
| * | ||
| * Used for comparing decoded search parameters, request body JSON objects, | ||
| * and URL decoded request form bodies. | ||
| * | ||
| * Performs a general recursive strict comparison with two caveats: | ||
| * - The expected data can use regexp to compare values | ||
| * - JSON path notation and nested objects are considered equal | ||
| */ | ||
| const dataEqual = (expected, actual) => { | ||
| if (isPlainObject(expected)) { | ||
| expected = expand(expected) | ||
| } | ||
| if (isPlainObject(actual)) { | ||
| actual = expand(actual) | ||
| } | ||
| return deepEqual(expected, actual) | ||
| } | ||
| /** | ||
| * Performs a recursive strict comparison between two values. | ||
| * | ||
| * Expected values or leaf nodes of expected object values that are RegExp use test() for comparison. | ||
| */ | ||
| function deepEqual(expected, actual) { | ||
| debug('deepEqual comparing', typeof expected, expected, typeof actual, actual) | ||
| if (expected instanceof RegExp) { | ||
| return expected.test(actual) | ||
| } | ||
| if (Array.isArray(expected) && Array.isArray(actual)) { | ||
| if (expected.length !== actual.length) { | ||
| return false | ||
| } | ||
| return expected.every((expVal, idx) => deepEqual(expVal, actual[idx])) | ||
| } | ||
| if (isPlainObject(expected) && isPlainObject(actual)) { | ||
| const allKeys = Array.from( | ||
| new Set(Object.keys(expected).concat(Object.keys(actual))), | ||
| ) | ||
| return allKeys.every(key => deepEqual(expected[key], actual[key])) | ||
| } | ||
| return expected === actual | ||
| } | ||
| const timeouts = new Set() | ||
| const immediates = new Set() | ||
| const wrapTimer = | ||
| (timer, ids) => | ||
| (callback, ...timerArgs) => { | ||
| const cb = (...callbackArgs) => { | ||
| try { | ||
| callback(...callbackArgs) | ||
| } finally { | ||
| ids.delete(id) | ||
| } | ||
| } | ||
| const id = timer(cb, ...timerArgs) | ||
| ids.add(id) | ||
| return id | ||
| } | ||
| const setTimeout = wrapTimer(timers.setTimeout, timeouts) | ||
| const setImmediate = wrapTimer(timers.setImmediate, immediates) | ||
| function clearTimer(clear, ids) { | ||
| ids.forEach(clear) | ||
| ids.clear() | ||
| } | ||
| function removeAllTimers() { | ||
| debug('remove all timers') | ||
| clearTimer(clearTimeout, timeouts) | ||
| clearTimer(clearImmediate, immediates) | ||
| } | ||
| /** | ||
| * Returns true if the given value is a plain object and not an Array. | ||
| * @param {*} value | ||
| * @returns {boolean} | ||
| */ | ||
| function isPlainObject(value) { | ||
| if (typeof value !== 'object' || value === null) return false | ||
| if (Object.prototype.toString.call(value) !== '[object Object]') return false | ||
| const proto = Object.getPrototypeOf(value) | ||
| if (proto === null) return true | ||
| const Ctor = | ||
| Object.prototype.hasOwnProperty.call(proto, 'constructor') && | ||
| proto.constructor | ||
| return ( | ||
| typeof Ctor === 'function' && | ||
| Ctor instanceof Ctor && | ||
| Function.prototype.call(Ctor) === Function.prototype.call(value) | ||
| ) | ||
| } | ||
| const prototypePollutionBlockList = ['__proto__', 'prototype', 'constructor'] | ||
| const blocklistFilter = function (part) { | ||
| return prototypePollutionBlockList.indexOf(part) === -1 | ||
| } | ||
| /** | ||
| * Converts flat objects whose keys use JSON path notation to nested objects. | ||
| * | ||
| * The input object is not mutated. | ||
| * | ||
| * @example | ||
| * { 'foo[bar][0]': 'baz' } -> { foo: { bar: [ 'baz' ] } } | ||
| */ | ||
| const expand = input => { | ||
| if (input === undefined || input === null) { | ||
| return input | ||
| } | ||
| const keys = Object.keys(input) | ||
| const result = {} | ||
| let resultPtr = result | ||
| for (let path of keys) { | ||
| const originalPath = path | ||
| if (path.indexOf('[') >= 0) { | ||
| path = path.replace(/\[/g, '.').replace(/]/g, '') | ||
| } | ||
| const parts = path.split('.') | ||
| const check = parts.filter(blocklistFilter) | ||
| if (check.length !== parts.length) { | ||
| return undefined | ||
| } | ||
| resultPtr = result | ||
| const lastIndex = parts.length - 1 | ||
| for (let i = 0; i < parts.length; ++i) { | ||
| const part = parts[i] | ||
| if (i === lastIndex) { | ||
| if (Array.isArray(resultPtr)) { | ||
| resultPtr[+part] = input[originalPath] | ||
| } else { | ||
| resultPtr[part] = input[originalPath] | ||
| } | ||
| } else { | ||
| if (resultPtr[part] === undefined || resultPtr[part] === null) { | ||
| const nextPart = parts[i + 1] | ||
| if (/^\d+$/.test(nextPart)) { | ||
| resultPtr[part] = [] | ||
| } else { | ||
| resultPtr[part] = {} | ||
| } | ||
| } | ||
| resultPtr = resultPtr[part] | ||
| } | ||
| } | ||
| } | ||
| return result | ||
| } | ||
| /** | ||
| * @param {ArrayBuffer} buffer | ||
| * @param {string} contentEncoding | ||
| */ | ||
| function decompressRequestBody(buffer, contentEncoding) { | ||
| const encodings = contentEncoding | ||
| .toLowerCase() | ||
| .split(',') | ||
| .map(coding => coding.trim()) | ||
| for (const encoding of encodings) { | ||
| if (encoding === 'gzip') { | ||
| return zlib.gunzipSync(buffer) | ||
| } else if (encoding === 'deflate') { | ||
| return zlib.inflateSync(buffer) | ||
| } else if (encoding === 'br') { | ||
| return zlib.brotliDecompressSync(buffer) | ||
| } | ||
| } | ||
| return buffer | ||
| } | ||
| /** | ||
| * @param {Headers} headers | ||
| */ | ||
| function convertHeadersToRaw(headers) { | ||
| const rawHeaders = [] | ||
| for (const [name, value] of headers.entries()) { | ||
| rawHeaders.push(name, value) | ||
| } | ||
| return rawHeaders | ||
| } | ||
| module.exports = { | ||
| contentEncoding, | ||
| dataEqual, | ||
| deleteHeadersField, | ||
| expand, | ||
| forEachHeader, | ||
| formatQueryValue, | ||
| headersArrayToObject, | ||
| headersFieldNamesToLowerCase, | ||
| headersFieldsArrayToLowerCase, | ||
| headersInputToRawArray, | ||
| isContentEncoded, | ||
| isJSONContent, | ||
| isPlainObject, | ||
| isStream, | ||
| isUtf8Representable, | ||
| matchStringOrRegexp, | ||
| normalizeOrigin, | ||
| normalizeRequestOptions, | ||
| percentDecode, | ||
| percentEncode, | ||
| removeAllTimers, | ||
| setImmediate, | ||
| setTimeout, | ||
| stringifyRequest, | ||
| decompressRequestBody, | ||
| convertHeadersToRaw, | ||
| } |
| 'use strict' | ||
| const { STATUS_CODES } = require('node:http') | ||
| /** | ||
| * Creates a Fetch API `Response` instance from the given | ||
| * `http.IncomingMessage` instance. | ||
| * Inspired by: https://github.com/mswjs/interceptors/blob/04152ed914f8041272b6e92ed374216b8177e1b2/src/interceptors/ClientRequest/utils/createResponse.ts#L8 | ||
| */ | ||
| /** | ||
| * Response status codes for responses that cannot have body. | ||
| * @see https://fetch.spec.whatwg.org/#statuses | ||
| */ | ||
| const responseStatusCodesWithoutBody = [204, 205, 304] | ||
| /** | ||
| * @param {import('http').IncomingMessage} message | ||
| * @param {AbortSignal} signal | ||
| */ | ||
| function createResponse(message, signal) { | ||
| const responseBodyOrNull = responseStatusCodesWithoutBody.includes( | ||
| message.statusCode || 200, | ||
| ) | ||
| ? null | ||
| : new ReadableStream({ | ||
| start(controller) { | ||
| message.on('data', chunk => controller.enqueue(chunk)) | ||
| message.on('end', () => controller.close()) | ||
| message.on('error', error => controller.error(error)) | ||
| signal.addEventListener('abort', () => message.destroy(signal.reason)) | ||
| }, | ||
| cancel() { | ||
| message.destroy() | ||
| }, | ||
| }) | ||
| const rawHeaders = new Headers() | ||
| for (let i = 0; i < message.rawHeaders.length; i += 2) { | ||
| rawHeaders.append(message.rawHeaders[i], message.rawHeaders[i + 1]) | ||
| } | ||
| // @mswjs/interceptors supports rawHeaders. https://github.com/mswjs/interceptors/pull/598 | ||
| const response = new Response(responseBodyOrNull, { | ||
| status: message.statusCode, | ||
| statusText: message.statusMessage || STATUS_CODES[message.statusCode], | ||
| headers: rawHeaders, | ||
| }) | ||
| return response | ||
| } | ||
| module.exports = { createResponse } |
-12
| 'use strict' | ||
| const { debuglog } = require('node:util') | ||
| module.exports.back = debuglog('nock:back') | ||
| module.exports.common = debuglog('nock:common') | ||
| module.exports.intercept = debuglog('nock:intercept') | ||
| module.exports.request_overrider = debuglog('nock:request_overrider') | ||
| module.exports.playback_interceptor = debuglog('nock:playback_interceptor') | ||
| module.exports.recorder = debuglog('nock:recorder') | ||
| module.exports.socket = debuglog('nock:socket') | ||
| module.exports.scopeDebuglog = namespace => debuglog(`nock:scope:${namespace}`) |
| 'use strict' | ||
| const { EventEmitter } = require('node:events') | ||
| module.exports = new EventEmitter() |
| 'use strict' | ||
| const { inherits } = require('node:util') | ||
| const globalEmitter = require('./global_emitter') | ||
| const common = require('./common') | ||
| const { playbackInterceptor } = require('./playback_interceptor') | ||
| /** | ||
| * @param {Request} request | ||
| */ | ||
| async function handleRequest(request) { | ||
| const { | ||
| interceptorsFor, | ||
| isOn, | ||
| isEnabledForNetConnect, | ||
| } = require('./intercept') | ||
| const url = new URL(request.url) | ||
| const interceptors = interceptorsFor(url) | ||
| if (isOn() && interceptors) { | ||
| const matches = interceptors.some(interceptor => | ||
| interceptor.matchOrigin(request), | ||
| ) | ||
| const allowUnmocked = interceptors.some( | ||
| interceptor => interceptor.options.allowUnmocked, | ||
| ) | ||
| if (!matches && allowUnmocked) { | ||
| globalEmitter.emit('no match', request) | ||
| } else { | ||
| const requestBodyBuffer = await request.clone().arrayBuffer() | ||
| // When request body is a binary buffer we internally use in its hexadecimal representation. | ||
| const requestBodyIsUtf8Representable = | ||
| common.isUtf8Representable(requestBodyBuffer) | ||
| const requestBodyString = Buffer.from(requestBodyBuffer).toString( | ||
| requestBodyIsUtf8Representable ? 'utf8' : 'hex', | ||
| ) | ||
| const matchResults = [] | ||
| const matchedInterceptor = interceptors.find(i => { | ||
| const reasons = i.match(request, requestBodyString) | ||
| if (reasons.length > 0) { | ||
| matchResults.push({ interceptor: i, reasons }) | ||
| return false | ||
| } else { | ||
| return true | ||
| } | ||
| }) | ||
| if (matchedInterceptor) { | ||
| matchedInterceptor.scope.logger( | ||
| 'interceptor identified, starting mocking', | ||
| ) | ||
| matchedInterceptor.markConsumed() | ||
| if (matchedInterceptor.isPassthrough) { | ||
| return | ||
| } | ||
| const response = await playbackInterceptor({ | ||
| decompressedRequest: request, | ||
| requestBodyString, | ||
| interceptor: matchedInterceptor, | ||
| requestBodyIsUtf8Representable, | ||
| }) | ||
| return response | ||
| } else { | ||
| globalEmitter.emit( | ||
| 'no match', | ||
| request, | ||
| matchResults.sort((a, b) => a.reasons.length - b.reasons.length), | ||
| ) | ||
| // Try to find a hostname match that allows unmocked. | ||
| const allowUnmocked = interceptors.some( | ||
| i => i.matchHostName(url.hostname) && i.options.allowUnmocked, | ||
| ) | ||
| if (!allowUnmocked) { | ||
| const body = JSON.stringify({ | ||
| code: 'ERR_NOCK_NO_MATCH', | ||
| message: `Nock: No match for request ${common.stringifyRequest(request, requestBodyString)}`, | ||
| }) | ||
| return new Response(body, { | ||
| status: 501, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| globalEmitter.emit('no match', request) | ||
| // Remove http(s):// for backward compatibility until we decide this Error. | ||
| const normalizedUrl = common | ||
| .normalizeOrigin(url) | ||
| .replace(`${url.protocol}//`, '') | ||
| if (isOn() && !isEnabledForNetConnect(normalizedUrl)) { | ||
| throw new NetConnectNotAllowedError(normalizedUrl, url.pathname) | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * @name NetConnectNotAllowedError | ||
| * @private | ||
| * @desc Error trying to make a connection when disabled external access. | ||
| * @class | ||
| * @example | ||
| * nock.disableNetConnect(); | ||
| * http.get('http://zombo.com'); | ||
| * // throw NetConnectNotAllowedError | ||
| */ | ||
| function NetConnectNotAllowedError(host, path) { | ||
| Error.call(this) | ||
| this.name = 'NetConnectNotAllowedError' | ||
| this.code = 'ENETUNREACH' | ||
| this.message = `Nock: Disallowed net connect for "${host}${path}"` | ||
| Error.captureStackTrace(this, this.constructor) | ||
| } | ||
| inherits(NetConnectNotAllowedError, Error) | ||
| module.exports = handleRequest |
-280
| 'use strict' | ||
| const common = require('./common') | ||
| const { intercept: debug } = require('./debug') | ||
| const builtinMock = require('./interceptors/builtin') | ||
| let undiciMock // Lazy-loaded, optional peer dependency | ||
| let allInterceptors = {} | ||
| let allowNetConnect | ||
| let isActive = false | ||
| /** | ||
| * Enabled real request. | ||
| * @public | ||
| * @param {String|RegExp} matcher=RegExp.new('.*') Expression to match | ||
| * @example | ||
| * // Enables all real requests | ||
| * nock.enableNetConnect(); | ||
| * @example | ||
| * // Enables real requests for url that matches google | ||
| * nock.enableNetConnect('google'); | ||
| * @example | ||
| * // Enables real requests for url that matches google and amazon | ||
| * nock.enableNetConnect(/(google|amazon)/); | ||
| * @example | ||
| * // Enables real requests for url that includes google | ||
| * nock.enableNetConnect(host => host.includes('google')); | ||
| */ | ||
| function enableNetConnect(matcher) { | ||
| if (typeof matcher === 'string') { | ||
| allowNetConnect = new RegExp(matcher) | ||
| } else if (matcher instanceof RegExp) { | ||
| allowNetConnect = matcher | ||
| } else if (typeof matcher === 'function') { | ||
| allowNetConnect = { test: matcher } | ||
| } else { | ||
| allowNetConnect = /.*/ | ||
| } | ||
| } | ||
| function isEnabledForNetConnect(url) { | ||
| const enabled = allowNetConnect && allowNetConnect.test(url) | ||
| debug('Net connect', enabled ? '' : 'not', 'enabled for', url) | ||
| return enabled | ||
| } | ||
| /** | ||
| * Disable all real requests. | ||
| * @public | ||
| * @example | ||
| * nock.disableNetConnect(); | ||
| */ | ||
| function disableNetConnect() { | ||
| allowNetConnect = undefined | ||
| } | ||
| function isOn() { | ||
| return !isOff() | ||
| } | ||
| function isOff() { | ||
| return process.env.NOCK_OFF === 'true' | ||
| } | ||
| function addInterceptor(key, interceptor, scope, scopeOptions, host) { | ||
| if (!(key in allInterceptors)) { | ||
| allInterceptors[key] = { key, interceptors: [] } | ||
| } | ||
| interceptor.__nock_scope = scope | ||
| // We need scope's key and scope options for scope filtering function (if defined) | ||
| interceptor.__nock_scopeKey = key | ||
| interceptor.__nock_scopeOptions = scopeOptions | ||
| // We need scope's host for setting correct request headers for filtered scopes. | ||
| interceptor.__nock_scopeHost = host | ||
| interceptor.interceptionCounter = 0 | ||
| if (scopeOptions.allowUnmocked) allInterceptors[key].allowUnmocked = true | ||
| allInterceptors[key].interceptors.push(interceptor) | ||
| } | ||
| function remove(interceptor) { | ||
| if (interceptor.__nock_scope.shouldPersist() || --interceptor.counter > 0) { | ||
| return | ||
| } | ||
| const { basePath } = interceptor | ||
| const interceptors = | ||
| (allInterceptors[basePath] && allInterceptors[basePath].interceptors) || [] | ||
| // TODO: There is a clearer way to write that we want to delete the first | ||
| // matching instance. I'm also not sure why we couldn't delete _all_ | ||
| // matching instances. | ||
| interceptors.some(function (thisInterceptor, i) { | ||
| return thisInterceptor === interceptor ? interceptors.splice(i, 1) : false | ||
| }) | ||
| } | ||
| function removeAll() { | ||
| Object.keys(allInterceptors).forEach(function (key) { | ||
| allInterceptors[key].interceptors.forEach(function (interceptor) { | ||
| interceptor.scope.keyedInterceptors = {} | ||
| }) | ||
| }) | ||
| allInterceptors = {} | ||
| } | ||
| /** | ||
| * Return all the Interceptors whose Scopes match against the base path of the provided options. | ||
| * | ||
| * @param {URL} url | ||
| * @returns {Interceptor[]} | ||
| */ | ||
| function interceptorsFor(url) { | ||
| debug('interceptors for %j', url.host) | ||
| const port = url.port || (url.protocol === 'https:' ? 443 : 80) | ||
| // Remove brackets from hostname if IPV6 | ||
| const basePath = `${url.protocol}//${url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname}:${port}` | ||
| debug('filtering interceptors for basepath', basePath) | ||
| // First try to use filteringScope if any of the interceptors has it defined. | ||
| for (const { key, interceptors, allowUnmocked } of Object.values( | ||
| allInterceptors, | ||
| )) { | ||
| for (const interceptor of interceptors) { | ||
| const { filteringScope } = interceptor.__nock_scopeOptions | ||
| // If scope filtering function is defined and returns a truthy value then | ||
| // we have to treat this as a match. | ||
| if (filteringScope && filteringScope(basePath)) { | ||
| interceptor.scope.logger('found matching scope interceptor') | ||
| // Keep the filtered scope (its key) to signal the rest of the module | ||
| // that this wasn't an exact but filtered match. | ||
| interceptors.forEach(ic => { | ||
| ic.__nock_filteredScope = ic.__nock_scopeKey | ||
| }) | ||
| return interceptors | ||
| } | ||
| } | ||
| if (common.matchStringOrRegexp(basePath, key)) { | ||
| if (allowUnmocked && interceptors.length === 0) { | ||
| debug('matched base path with allowUnmocked (no matching interceptors)') | ||
| return [ | ||
| { | ||
| options: { allowUnmocked: true }, | ||
| matchOrigin() { | ||
| return false | ||
| }, | ||
| }, | ||
| ] | ||
| } else { | ||
| debug( | ||
| `matched base path (${interceptors.length} interceptor${ | ||
| interceptors.length > 1 ? 's' : '' | ||
| })`, | ||
| ) | ||
| return interceptors | ||
| } | ||
| } | ||
| } | ||
| return undefined | ||
| } | ||
| function removeInterceptor(options) { | ||
| // Lazily import to avoid circular imports. | ||
| const Interceptor = require('./interceptor') | ||
| let baseUrl, key, method, proto | ||
| if (options instanceof Interceptor) { | ||
| baseUrl = options.basePath | ||
| key = options._key | ||
| } else { | ||
| proto = options.proto ? options.proto : 'http' | ||
| common.normalizeRequestOptions(options) | ||
| baseUrl = `${proto}://${options.host}` | ||
| method = (options.method && options.method.toUpperCase()) || 'GET' | ||
| key = `${method} ${baseUrl}${options.path || '/'}` | ||
| } | ||
| if ( | ||
| allInterceptors[baseUrl] && | ||
| allInterceptors[baseUrl].interceptors.length > 0 | ||
| ) { | ||
| for (let i = 0; i < allInterceptors[baseUrl].interceptors.length; i++) { | ||
| const interceptor = allInterceptors[baseUrl].interceptors[i] | ||
| if ( | ||
| options instanceof Interceptor | ||
| ? interceptor === options | ||
| : interceptor._key === key | ||
| ) { | ||
| allInterceptors[baseUrl].interceptors.splice(i, 1) | ||
| interceptor.scope.remove(key, interceptor) | ||
| break | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| function interceptorScopes() { | ||
| const nestedInterceptors = Object.values(allInterceptors).map( | ||
| i => i.interceptors, | ||
| ) | ||
| const scopes = new Set([].concat(...nestedInterceptors).map(i => i.scope)) | ||
| return [...scopes] | ||
| } | ||
| function isDone() { | ||
| return interceptorScopes().every(scope => scope.isDone()) | ||
| } | ||
| function pendingMocks() { | ||
| return [].concat(...interceptorScopes().map(scope => scope.pendingMocks())) | ||
| } | ||
| function activeMocks() { | ||
| return [].concat(...interceptorScopes().map(scope => scope.activeMocks())) | ||
| } | ||
| function activate() { | ||
| if (!isActive) { | ||
| builtinMock.activate() | ||
| // Lazy load undiciMock only when activate is called | ||
| if (!undiciMock) { | ||
| try { | ||
| undiciMock = require('./interceptors/undici') | ||
| } catch (err) { | ||
| if (err.code !== 'MODULE_NOT_FOUND') { | ||
| throw err | ||
| } | ||
| debug( | ||
| 'Undici mocking is disabled because the undici module is not installed', | ||
| ) | ||
| } | ||
| } | ||
| undiciMock?.activate() | ||
| isActive = true | ||
| } else { | ||
| throw new Error('Nock already active') | ||
| } | ||
| } | ||
| function deactivate() { | ||
| if (isActive) { | ||
| builtinMock.deactivate() | ||
| undiciMock?.deactivate() | ||
| isActive = false | ||
| } | ||
| } | ||
| module.exports = { | ||
| addInterceptor, | ||
| remove, | ||
| removeAll, | ||
| removeInterceptor, | ||
| isOn, | ||
| activate, | ||
| isActive: () => isActive, | ||
| isDone, | ||
| pendingMocks, | ||
| activeMocks, | ||
| enableNetConnect, | ||
| disableNetConnect, | ||
| restoreOverriddenClientRequest: deactivate, | ||
| abortPendingRequests: common.removeAllTimers, | ||
| interceptorsFor, | ||
| isEnabledForNetConnect, | ||
| } |
| 'use strict' | ||
| const fs = require('node:fs') | ||
| const querystring = require('node:querystring') | ||
| const { URL, URLSearchParams } = require('node:url') | ||
| const stringify = require('./stringify') | ||
| const common = require('./common') | ||
| const { remove } = require('./intercept') | ||
| const matchBody = require('./match_body') | ||
| module.exports = class Interceptor { | ||
| /** | ||
| * | ||
| * Valid argument types for `uri`: | ||
| * - A string used for strict comparisons with pathname. | ||
| * The search portion of the URI may also be postfixed, in which case the search params | ||
| * are striped and added via the `query` method. | ||
| * - A RegExp instance that tests against only the pathname of requests. | ||
| * - A synchronous function bound to this Interceptor instance. It's provided the pathname | ||
| * of requests and must return a boolean denoting if the request is considered a match. | ||
| */ | ||
| constructor(scope, uri, method, requestBody, interceptorOptions) { | ||
| const uriIsStr = typeof uri === 'string' | ||
| // Check for leading slash. Uri can be either a string or a regexp, but | ||
| // When enabled filteringScope ignores the passed URL entirely so we skip validation. | ||
| if ( | ||
| uriIsStr && | ||
| !scope.scopeOptions.filteringScope && | ||
| !scope.basePathname && | ||
| !uri.startsWith('/') && | ||
| !uri.startsWith('*') | ||
| ) { | ||
| throw Error( | ||
| `Non-wildcard URL path strings must begin with a slash (otherwise they won't match anything) (got: ${uri})`, | ||
| ) | ||
| } | ||
| if (!method) { | ||
| throw new Error( | ||
| 'The "method" parameter is required for an intercept call.', | ||
| ) | ||
| } | ||
| this.scope = scope | ||
| this.interceptorMatchHeaders = [] | ||
| this.method = method.toUpperCase() | ||
| this.uri = uri | ||
| this._key = `${this.method} ${scope.basePath}${scope.basePathname}${ | ||
| uriIsStr ? '' : '/' | ||
| }${uri}` | ||
| this.basePath = this.scope.basePath | ||
| this.path = uriIsStr ? scope.basePathname + uri : uri | ||
| this.queries = null | ||
| this.options = interceptorOptions || {} | ||
| this.counter = 1 | ||
| this._requestBody = requestBody | ||
| // We use lower-case header field names throughout Nock. | ||
| this.reqheaders = common.headersFieldNamesToLowerCase( | ||
| scope.scopeOptions.reqheaders || {}, | ||
| true, | ||
| ) | ||
| this.badheaders = common.headersFieldsArrayToLowerCase( | ||
| scope.scopeOptions.badheaders || [], | ||
| ) | ||
| this.delayBodyInMs = 0 | ||
| this.optional = false | ||
| this.isPassthrough = false | ||
| // strip off literal query parameters if they were provided as part of the URI | ||
| if (uriIsStr && uri.includes('?')) { | ||
| // localhost is a dummy value because the URL constructor errors for only relative inputs | ||
| const parsedURL = new URL(this.path, 'http://localhost') | ||
| this.path = parsedURL.pathname | ||
| this.query(parsedURL.searchParams) | ||
| this._key = `${this.method} ${scope.basePath}${this.path}` | ||
| } | ||
| } | ||
| optionally(flag = true) { | ||
| // The default behaviour of optionally() with no arguments is to make the mock optional. | ||
| if (typeof flag !== 'boolean') { | ||
| throw new Error('Invalid arguments: argument should be a boolean') | ||
| } | ||
| this.optional = flag | ||
| return this | ||
| } | ||
| passthrough() { | ||
| this.isPassthrough = true | ||
| this.options = { | ||
| ...this.scope.scopeOptions, | ||
| ...this.options, | ||
| } | ||
| this.scope.add(this._key, this) | ||
| return this.scope | ||
| } | ||
| replyWithError(errorMessage) { | ||
| this.errorMessage = errorMessage | ||
| this.options = { | ||
| ...this.scope.scopeOptions, | ||
| ...this.options, | ||
| } | ||
| this.scope.add(this._key, this) | ||
| return this.scope | ||
| } | ||
| reply(statusCode, body, rawHeaders) { | ||
| // support the format of only passing in a callback | ||
| if (typeof statusCode === 'function') { | ||
| if (arguments.length > 1) { | ||
| // It's not very Javascript-y to throw an error for extra args to a function, but because | ||
| // of legacy behavior, this error was added to reduce confusion for those migrating. | ||
| throw Error( | ||
| 'Invalid arguments. When providing a function for the first argument, .reply does not accept other arguments.', | ||
| ) | ||
| } | ||
| this.statusCode = null | ||
| this.fullReplyFunction = statusCode | ||
| } else { | ||
| if (statusCode !== undefined && !Number.isInteger(statusCode)) { | ||
| throw new Error(`Invalid ${typeof statusCode} value for status code`) | ||
| } | ||
| this.statusCode = statusCode || 200 | ||
| if (typeof body === 'function') { | ||
| this.replyFunction = body | ||
| body = null | ||
| } | ||
| } | ||
| this.options = { | ||
| ...this.scope.scopeOptions, | ||
| ...this.options, | ||
| } | ||
| this.rawHeaders = common.headersInputToRawArray(rawHeaders) | ||
| if (this.scope.date) { | ||
| // https://tools.ietf.org/html/rfc7231#section-7.1.1.2 | ||
| this.rawHeaders.push('Date', this.scope.date.toUTCString()) | ||
| } | ||
| // Prepare the headers temporarily so we can make best guesses about content-encoding and content-type | ||
| // below as well as while the response is being processed in RequestOverrider.end(). | ||
| // Including all the default headers is safe for our purposes because of the specific headers we introspect. | ||
| // A more thoughtful process is used to merge the default headers when the response headers are finally computed. | ||
| this.headers = common.headersArrayToObject( | ||
| this.rawHeaders.concat(this.scope._defaultReplyHeaders), | ||
| ) | ||
| // If the content is not encoded we may need to transform the response body. | ||
| // Otherwise, we leave it as it is. | ||
| if ( | ||
| body && | ||
| typeof body !== 'string' && | ||
| !Buffer.isBuffer(body) && | ||
| !common.isStream(body) && | ||
| !common.isContentEncoded(this.headers) | ||
| ) { | ||
| try { | ||
| body = stringify(body) | ||
| } catch { | ||
| throw new Error('Error encoding response body into JSON') | ||
| } | ||
| if (!this.headers['content-type']) { | ||
| // https://tools.ietf.org/html/rfc7231#section-3.1.1.5 | ||
| this.rawHeaders.push('Content-Type', 'application/json') | ||
| } | ||
| // Fix content-length header if it exists and doesn't match the stringified body | ||
| // This addresses the issue where recorded fixtures have a content-length | ||
| // that matches the original formatted JSON, but nock serializes it differently | ||
| const contentLengthIndex = this.rawHeaders.findIndex( | ||
| (value, index) => | ||
| index % 2 === 0 && value.toLowerCase() === 'content-length', | ||
| ) | ||
| if (contentLengthIndex !== -1) { | ||
| const actualLength = Buffer.byteLength(body, 'utf8') | ||
| this.rawHeaders[contentLengthIndex + 1] = String(actualLength) | ||
| } | ||
| } | ||
| if (this.scope.contentLen) { | ||
| // https://tools.ietf.org/html/rfc7230#section-3.3.2 | ||
| if (typeof body === 'string') { | ||
| this.rawHeaders.push('Content-Length', body.length) | ||
| } else if (Buffer.isBuffer(body)) { | ||
| this.rawHeaders.push('Content-Length', body.byteLength) | ||
| } | ||
| } | ||
| this.scope.logger('reply.headers:', this.headers) | ||
| this.scope.logger('reply.rawHeaders:', this.rawHeaders) | ||
| this.body = body | ||
| this.scope.add(this._key, this) | ||
| return this.scope | ||
| } | ||
| replyWithFile(statusCode, filePath, headers) { | ||
| if (!fs) { | ||
| throw new Error('No fs') | ||
| } | ||
| this.filePath = filePath | ||
| return this.reply( | ||
| statusCode, | ||
| () => { | ||
| const readStream = fs.createReadStream(filePath) | ||
| readStream.pause() | ||
| return readStream | ||
| }, | ||
| headers, | ||
| ) | ||
| } | ||
| reqheaderMatches(expected, actual, key) { | ||
| if (expected !== undefined && actual !== undefined) { | ||
| if (typeof expected === 'function') { | ||
| return expected(actual) | ||
| } else if (common.matchStringOrRegexp(actual, expected)) { | ||
| return true | ||
| } | ||
| } | ||
| this.scope.logger( | ||
| "request header field doesn't match:", | ||
| key, | ||
| actual, | ||
| expected, | ||
| ) | ||
| return false | ||
| } | ||
| /** | ||
| * @param {Request} request | ||
| * @param {string} body - string or hex | ||
| * @returns {string[]} | ||
| */ | ||
| match(request, body) { | ||
| const url = new URL(request.url) | ||
| // TODO: fix request log to string | ||
| this.scope.logger('attempting match %j, body = %j', request, body) | ||
| const mismatches = [] | ||
| let path = url.pathname + url.search | ||
| let matchKey | ||
| if (this.method !== request.method) { | ||
| const msg = `Method mismatch: expected ${this.method}, got ${request.method}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if (this.scope.transformPathFunction) { | ||
| path = this.scope.transformPathFunction(path) | ||
| } | ||
| const requestMatchesFilter = ({ name, value: predicate }) => { | ||
| const headerValue = request.headers.get(name) | ||
| if (typeof predicate === 'function') { | ||
| return predicate(headerValue) | ||
| } else { | ||
| return common.matchStringOrRegexp(headerValue, predicate) | ||
| } | ||
| } | ||
| for (const header of [ | ||
| ...this.scope.matchHeaders, | ||
| ...this.interceptorMatchHeaders, | ||
| ]) { | ||
| if (!requestMatchesFilter(header)) { | ||
| const msg = `Header mismatch: expected ${header.name} to match ${header.value}, got ${request.headers.get(header.name)}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| } | ||
| const reqHeadersMatch = Object.keys(this.reqheaders).every(key => | ||
| this.reqheaderMatches( | ||
| this.reqheaders[key], | ||
| request.headers.get(key), | ||
| key, | ||
| ), | ||
| ) | ||
| if (!reqHeadersMatch) { | ||
| const msg = "Request headers don't match" | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if ( | ||
| this.scope.scopeOptions.conditionally && | ||
| !this.scope.scopeOptions.conditionally() | ||
| ) { | ||
| const msg = 'conditionally() did not validate' | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| const badHeaders = this.badheaders.filter(header => | ||
| request.headers.has(header), | ||
| ) | ||
| if (badHeaders.length) { | ||
| const msg = `Request contains bad headers: ${badHeaders.join(', ')}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| // Match query strings when using query() | ||
| if (this.queries === null) { | ||
| this.scope.logger('query matching skipped') | ||
| } else { | ||
| // can't rely on pathname or search being in the options, but path has a default | ||
| const [pathname, search] = path.split('?') | ||
| const matchQueries = this.matchQuery({ search }) | ||
| if (!matchQueries) { | ||
| const msg = 'query matching failed' | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| // If the query string was explicitly checked then subsequent checks against | ||
| // the path using a callback or regexp only validate the pathname. | ||
| path = pathname | ||
| } | ||
| // If we have a filtered scope then we use it instead reconstructing the | ||
| // scope from the request options (proto, host and port) as these two won't | ||
| // necessarily match and we have to remove the scope that was matched (vs. | ||
| // that was defined). | ||
| if (this.__nock_filteredScope) { | ||
| matchKey = this.__nock_filteredScope | ||
| } else { | ||
| matchKey = common.normalizeOrigin(url) | ||
| } | ||
| if (!common.matchStringOrRegexp(matchKey, this.basePath)) { | ||
| const msg = `Base path mismatch: expected ${this.basePath}, got ${matchKey}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if (typeof this.uri === 'function') { | ||
| if (!this.uri.call(this, path)) { | ||
| const msg = `Path function mismatch: expected function to return true for ${path}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| } else if (!common.matchStringOrRegexp(path, this.path)) { | ||
| const msg = `Path mismatch: expected ${this.path}, got ${path}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| if (this._requestBody !== undefined) { | ||
| if (this.scope.transformRequestBodyFunction) { | ||
| body = this.scope.transformRequestBodyFunction(body, this._requestBody) | ||
| } | ||
| if (!matchBody(request, this._requestBody, body)) { | ||
| const msg = `Body mismatch: expected ${stringify(this._requestBody)}, got ${body}` | ||
| this.scope.logger(msg) | ||
| mismatches.push(msg) | ||
| } | ||
| } | ||
| return mismatches | ||
| } | ||
| /** | ||
| * Return true when the interceptor's method, protocol, host, port, and path | ||
| * match the provided options. | ||
| * | ||
| * @param {Request} request | ||
| */ | ||
| matchOrigin(request) { | ||
| const url = new URL(request.url) | ||
| const isPathFn = typeof this.path === 'function' | ||
| const isRegex = this.path instanceof RegExp | ||
| const isRegexBasePath = this.scope.basePath instanceof RegExp | ||
| const method = (request.method || 'GET').toUpperCase() | ||
| const port = url.port || (url.protocol === 'https:' ? 443 : 80) | ||
| let path = url.pathname + url.search | ||
| // NOTE: Do not split off the query params as the regex could use them | ||
| if (!isRegex) { | ||
| path = path ? path.split('?')[0] : '' | ||
| } | ||
| if (this.scope.transformPathFunction) { | ||
| path = this.scope.transformPathFunction(path) | ||
| } | ||
| const comparisonKey = isPathFn || isRegex ? this.__nock_scopeKey : this._key | ||
| const matchKey = `${method} ${url.protocol}//${url.hostname}:${port}${path}` | ||
| if (isPathFn) { | ||
| return !!(matchKey.match(comparisonKey) && this.path(path)) | ||
| } | ||
| if (isRegex && !isRegexBasePath) { | ||
| return !!matchKey.match(comparisonKey) && this.path.test(path) | ||
| } | ||
| if (isRegexBasePath) { | ||
| return this.scope.basePath.test(matchKey) && !!path.match(this.path) | ||
| } | ||
| return comparisonKey === matchKey | ||
| } | ||
| /** | ||
| * @param {string} hostname | ||
| */ | ||
| matchHostName(hostname) { | ||
| const { basePath } = this.scope | ||
| if (basePath instanceof RegExp) { | ||
| return basePath.test(hostname) | ||
| } | ||
| return hostname === this.scope.urlParts.hostname | ||
| } | ||
| matchQuery(options) { | ||
| if (this.queries === true) { | ||
| return true | ||
| } | ||
| const reqQueries = querystring.parse(options.search) | ||
| this.scope.logger('Interceptor queries: %j', this.queries) | ||
| this.scope.logger(' Request queries: %j', reqQueries) | ||
| if (typeof this.queries === 'function') { | ||
| return this.queries(reqQueries) | ||
| } | ||
| return common.dataEqual(this.queries, reqQueries) | ||
| } | ||
| filteringPath(...args) { | ||
| this.scope.filteringPath(...args) | ||
| return this | ||
| } | ||
| // TODO filtering by path is valid on the intercept level, but not filtering | ||
| // by request body? | ||
| markConsumed() { | ||
| this.interceptionCounter++ | ||
| remove(this) | ||
| if (!this.scope.shouldPersist() && this.counter < 1) { | ||
| this.scope.remove(this._key, this) | ||
| } | ||
| } | ||
| matchHeader(name, value) { | ||
| this.interceptorMatchHeaders.push({ name, value }) | ||
| return this | ||
| } | ||
| basicAuth({ user, pass = '' }) { | ||
| const encoded = Buffer.from(`${user}:${pass}`).toString('base64') | ||
| this.matchHeader('authorization', `Basic ${encoded}`) | ||
| return this | ||
| } | ||
| /** | ||
| * Set query strings for the interceptor | ||
| * @name query | ||
| * @param queries Object of query string name,values (accepts regexp values) | ||
| * @public | ||
| * @example | ||
| * // Will match 'http://zombo.com/?q=t' | ||
| * nock('http://zombo.com').get('/').query({q: 't'}); | ||
| */ | ||
| query(queries) { | ||
| if (this.queries !== null) { | ||
| throw Error(`Query parameters have already been defined`) | ||
| } | ||
| // Allow all query strings to match this route | ||
| if (queries === true) { | ||
| this.queries = queries | ||
| return this | ||
| } | ||
| if (typeof queries === 'function') { | ||
| this.queries = queries | ||
| return this | ||
| } | ||
| let strFormattingFn | ||
| if (this.scope.scopeOptions.encodedQueryParams) { | ||
| strFormattingFn = common.percentDecode | ||
| } | ||
| if (queries instanceof URLSearchParams || typeof queries === 'string') { | ||
| // Normalize the data into the shape that is matched against. | ||
| // Duplicate keys are handled by combining the values into an array. | ||
| queries = querystring.parse(queries.toString()) | ||
| } else if (!common.isPlainObject(queries)) { | ||
| throw Error(`Argument Error: ${queries}`) | ||
| } | ||
| this.queries = {} | ||
| for (const [key, value] of Object.entries(queries)) { | ||
| const formatted = common.formatQueryValue(key, value, strFormattingFn) | ||
| const [formattedKey, formattedValue] = formatted | ||
| this.queries[formattedKey] = formattedValue | ||
| } | ||
| return this | ||
| } | ||
| /** | ||
| * Set number of times will repeat the interceptor | ||
| * @name times | ||
| * @param newCounter Number of times to repeat (should be > 0) | ||
| * @public | ||
| * @example | ||
| * // Will repeat mock 5 times for same king of request | ||
| * nock('http://zombo.com).get('/').times(5).reply(200, 'Ok'); | ||
| */ | ||
| times(newCounter) { | ||
| if (newCounter < 1) { | ||
| return this | ||
| } | ||
| this.counter = newCounter | ||
| return this | ||
| } | ||
| /** | ||
| * An sugar syntax for times(1) | ||
| * @name once | ||
| * @see {@link times} | ||
| * @public | ||
| * @example | ||
| * nock('http://zombo.com).get('/').once().reply(200, 'Ok'); | ||
| */ | ||
| once() { | ||
| return this.times(1) | ||
| } | ||
| /** | ||
| * An sugar syntax for times(2) | ||
| * @name twice | ||
| * @see {@link times} | ||
| * @public | ||
| * @example | ||
| * nock('http://zombo.com).get('/').twice().reply(200, 'Ok'); | ||
| */ | ||
| twice() { | ||
| return this.times(2) | ||
| } | ||
| /** | ||
| * An sugar syntax for times(3). | ||
| * @name thrice | ||
| * @see {@link times} | ||
| * @public | ||
| * @example | ||
| * nock('http://zombo.com).get('/').thrice().reply(200, 'Ok'); | ||
| */ | ||
| thrice() { | ||
| return this.times(3) | ||
| } | ||
| /** | ||
| * Delay the response by a certain number of ms. | ||
| * | ||
| * @param {number} ms - Number of milliseconds to wait before response is sent | ||
| * @return {Interceptor} - the current interceptor for chaining | ||
| */ | ||
| delay(ms) { | ||
| if (typeof ms === 'number') { | ||
| this.delayBodyInMs = ms | ||
| return this | ||
| } else { | ||
| throw new Error(`Unexpected input ${ms}`) | ||
| } | ||
| } | ||
| } |
| 'use strict' | ||
| const http = require('node:http') | ||
| const { getRawRequest, BatchInterceptor } = require('@mswjs/interceptors') | ||
| const nodeInterceptors = require('@mswjs/interceptors/presets/node') | ||
| const common = require('../common') | ||
| const handleRequest = require('../handle-request') | ||
| const { arrayBuffer } = require('node:stream/consumers') | ||
| const { getClientRequestBodyStream } = require('@mswjs/interceptors/utils/node') | ||
| const { setGetRequestBody } = require('../utils/node') | ||
| const interceptor = new BatchInterceptor({ | ||
| name: 'nock-interceptor', | ||
| interceptors: nodeInterceptors, | ||
| }) | ||
| function activate() { | ||
| interceptor.apply() | ||
| // Force msw to forward Nock's error instead of coerce it into 500 error | ||
| interceptor.on('unhandledException', ({ controller, error }) => { | ||
| controller.errorWith(error) | ||
| }) | ||
| interceptor.on('request', async function ({ request, controller }) { | ||
| if (request.headers.get('expect') === '100-continue') { | ||
| // We currently do not support mocking 100-continue responses, so they are passed through for now. | ||
| return | ||
| } | ||
| const rawRequest = getRawRequest(request) | ||
| // If this is GET request with body, we need to read the body from the socket because Fetch API doesn't support this. | ||
| const requestBodyBuffer = common.decompressRequestBody( | ||
| rawRequest instanceof http.ClientRequest && | ||
| request.method === 'GET' && | ||
| request.headers.get('content-length') > 0 | ||
| ? await arrayBuffer(getClientRequestBodyStream(request)) | ||
| : await request.clone().arrayBuffer(), | ||
| request.headers.get('content-encoding') || '', | ||
| ) | ||
| const decompressedRequest = new Request(request, { | ||
| body: | ||
| requestBodyBuffer.length > 0 && request.method !== 'GET' | ||
| ? requestBodyBuffer | ||
| : undefined, | ||
| }) | ||
| if (requestBodyBuffer.byteLength > 0 && request.method === 'GET') { | ||
| setGetRequestBody(decompressedRequest, requestBodyBuffer) | ||
| } | ||
| const response = await handleRequest(decompressedRequest) | ||
| if (response) { | ||
| controller.respondWith(response) | ||
| } | ||
| }) | ||
| } | ||
| function deactivate() { | ||
| interceptor.dispose() | ||
| } | ||
| module.exports = { | ||
| activate, | ||
| deactivate, | ||
| } |
| 'use strict' | ||
| // This file is loaded lazily after confirming undici is installed | ||
| // eslint-disable-next-line n/no-unpublished-require | ||
| const undici = require('undici') | ||
| const handleRequest = require('../handle-request') | ||
| const { URL } = require('node:url') | ||
| const { convertHeadersToRaw } = require('../common') | ||
| class NockClient extends undici.Client { | ||
| dispatch(options, handler) { | ||
| const url = new URL(options.path, options.origin) | ||
| if (options.query) { | ||
| url.search = new URLSearchParams(options.query).toString() | ||
| } | ||
| const decompressedRequest = new Request(url, { | ||
| method: options.method, | ||
| headers: options.headers, | ||
| body: options.body, | ||
| duplex: options.body ? 'half' : undefined, | ||
| }) | ||
| handleRequest(decompressedRequest) | ||
| .then(async response => { | ||
| if (response) { | ||
| handler.onConnect?.(err => handler.onError(err), null) | ||
| handler.onHeaders?.( | ||
| response.status, | ||
| convertHeadersToRaw(response.headers), | ||
| () => {}, | ||
| response.statusText, | ||
| ) | ||
| handler.onData?.(Buffer.from(await response.arrayBuffer())) | ||
| handler.onComplete?.([]) // responseTrailers | ||
| } else { | ||
| const dispatcher = options.dispatcher || { | ||
| dispatch: super.dispatch.bind(this), | ||
| } | ||
| dispatcher.dispatch(options, handler) | ||
| } | ||
| }) | ||
| .catch(err => { | ||
| handler.onError?.(err) | ||
| }) | ||
| } | ||
| } | ||
| class NockAgent extends undici.Dispatcher { | ||
| constructor(options) { | ||
| super(options) | ||
| this.agent = new undici.Agent({ options, factory: this.factory.bind(this) }) | ||
| this.originalOptions = options | ||
| } | ||
| dispatch(options, handler) { | ||
| this.agent.dispatch(options, handler) | ||
| } | ||
| factory(origin) { | ||
| const mockOptions = { ...this.originalOptions, agent: this } | ||
| return new NockClient(origin, mockOptions) | ||
| } | ||
| } | ||
| function activate() { | ||
| undici.setGlobalDispatcher(new NockAgent()) | ||
| } | ||
| function deactivate() { | ||
| undici.setGlobalDispatcher(new undici.Agent()) | ||
| } | ||
| module.exports = { | ||
| activate, | ||
| deactivate, | ||
| } |
| 'use strict' | ||
| const querystring = require('node:querystring') | ||
| const common = require('./common') | ||
| /** | ||
| * @param {Request} request | ||
| * @param {*} spec | ||
| * @param {string} body - string or hex | ||
| * @returns | ||
| */ | ||
| module.exports = function matchBody(request, spec, body) { | ||
| if (spec instanceof RegExp) { | ||
| return spec.test(body) | ||
| } | ||
| if (Buffer.isBuffer(spec)) { | ||
| const encoding = common.isUtf8Representable(spec) ? 'utf8' : 'hex' | ||
| spec = spec.toString(encoding) | ||
| } | ||
| const contentType = request.headers.get('content-type') || '' | ||
| const isMultipart = contentType.includes('multipart') | ||
| const isUrlencoded = contentType.includes('application/x-www-form-urlencoded') | ||
| // try to transform body to json or query string | ||
| let json | ||
| if (typeof spec === 'object' || typeof spec === 'function') { | ||
| try { | ||
| json = JSON.parse(body) | ||
| } catch { | ||
| // not a valid JSON string | ||
| } | ||
| if (json !== undefined) { | ||
| body = json | ||
| } else if (isUrlencoded) { | ||
| body = querystring.parse(body) | ||
| } | ||
| } | ||
| if (typeof spec === 'function') { | ||
| return spec(body) | ||
| } | ||
| // strip line endings from both so that we get a match no matter what OS we are running on | ||
| // if Content-Type does not contain 'multipart' | ||
| if (!isMultipart && typeof body === 'string') { | ||
| body = body.replace(/\r?\n|\r/g, '') | ||
| } | ||
| if (!isMultipart && typeof spec === 'string') { | ||
| spec = spec.replace(/\r?\n|\r/g, '') | ||
| } | ||
| // Because the nature of URL encoding, all the values in the body must be cast to strings. | ||
| // dataEqual does strict checking, so we have to cast the non-regexp values in the spec too. | ||
| if (isUrlencoded) { | ||
| spec = mapValuesDeep(spec, val => (val instanceof RegExp ? val : `${val}`)) | ||
| } | ||
| return common.dataEqual(spec, body) | ||
| } | ||
| function mapValues(object, cb) { | ||
| const keys = Object.keys(object) | ||
| const clonedObject = { ...object } | ||
| for (const key of keys) { | ||
| clonedObject[key] = cb(clonedObject[key], key, clonedObject) | ||
| } | ||
| return clonedObject | ||
| } | ||
| /** | ||
| * Based on lodash issue discussion | ||
| * https://github.com/lodash/lodash/issues/1244 | ||
| */ | ||
| function mapValuesDeep(obj, cb) { | ||
| if (Array.isArray(obj)) { | ||
| return obj.map(v => mapValuesDeep(v, cb)) | ||
| } | ||
| if (common.isPlainObject(obj)) { | ||
| return mapValues(obj, v => mapValuesDeep(v, cb)) | ||
| } | ||
| return cb(obj) | ||
| } |
| 'use strict' | ||
| const { STATUS_CODES } = require('node:http') | ||
| const stream = require('node:stream') | ||
| const util = require('node:util') | ||
| const { playback_interceptor: debug } = require('./debug') | ||
| const common = require('./common') | ||
| const { FetchResponse } = require('@mswjs/interceptors') | ||
| function parseFullReplyResult(fullReplyResult) { | ||
| debug('full response from callback result: %j', fullReplyResult) | ||
| if (!Array.isArray(fullReplyResult)) { | ||
| throw Error('A single function provided to .reply MUST return an array') | ||
| } | ||
| if (fullReplyResult.length > 3) { | ||
| throw Error( | ||
| 'The array returned from the .reply callback contains too many values', | ||
| ) | ||
| } | ||
| const [status, body = '', headers] = fullReplyResult | ||
| if (!Number.isInteger(status)) { | ||
| throw new Error(`Invalid ${typeof status} value for status code`) | ||
| } | ||
| const rawHeaders = common.headersInputToRawArray(headers) | ||
| debug('response.rawHeaders after reply: %j', rawHeaders) | ||
| return [status, body, rawHeaders] | ||
| } | ||
| /** | ||
| * Determine which of the default headers should be added to the response. | ||
| * | ||
| * Don't include any defaults whose case-insensitive keys are already on the response. | ||
| */ | ||
| function selectDefaultHeaders(existingHeaders, defaultHeaders) { | ||
| if (!defaultHeaders.length) { | ||
| return [] // return early if we don't need to bother | ||
| } | ||
| const definedHeaders = new Set() | ||
| const result = [] | ||
| common.forEachHeader(existingHeaders, (_, fieldName) => { | ||
| definedHeaders.add(fieldName.toLowerCase()) | ||
| }) | ||
| common.forEachHeader(defaultHeaders, (value, fieldName) => { | ||
| if (!definedHeaders.has(fieldName.toLowerCase())) { | ||
| result.push([fieldName, value]) | ||
| } | ||
| }) | ||
| return result | ||
| } | ||
| // Presents a list of Buffers as a Readable | ||
| class ReadableBuffers extends stream.Readable { | ||
| constructor(buffers) { | ||
| super() | ||
| this.buffers = buffers | ||
| } | ||
| _read() { | ||
| while (this.buffers.length) { | ||
| if (!this.push(this.buffers.shift())) { | ||
| return | ||
| } | ||
| } | ||
| this.push(null) | ||
| } | ||
| } | ||
| function convertBodyToStream(body) { | ||
| if (common.isStream(body)) { | ||
| return body | ||
| } | ||
| if (body === undefined) { | ||
| return new ReadableBuffers([]) | ||
| } | ||
| if (Buffer.isBuffer(body)) { | ||
| return new ReadableBuffers([body]) | ||
| } | ||
| if (typeof body !== 'string') { | ||
| body = JSON.stringify(body) | ||
| } | ||
| return new ReadableBuffers([Buffer.from(body)]) | ||
| } | ||
| /** | ||
| * Play back an interceptor using the given request and mock response. | ||
| * | ||
| * @param {Object} param0 | ||
| * @param {Request} param0.decompressedRequest | ||
| * @param {string} param0.requestBodyString | ||
| * @param {boolean} param0.requestBodyIsUtf8Representable | ||
| * @param {import('./interceptor').Interceptor} param0.interceptor | ||
| */ | ||
| async function playbackInterceptor({ | ||
| decompressedRequest, | ||
| interceptor, | ||
| requestBodyString, | ||
| requestBodyIsUtf8Representable, | ||
| }) { | ||
| const { logger } = interceptor.scope | ||
| interceptor.scope.emit( | ||
| 'request', | ||
| decompressedRequest, | ||
| interceptor, | ||
| requestBodyString, | ||
| ) | ||
| if (typeof interceptor.errorMessage !== 'undefined') { | ||
| let error | ||
| if (typeof interceptor.errorMessage === 'object') { | ||
| error = interceptor.errorMessage | ||
| } else { | ||
| error = new Error(interceptor.errorMessage) | ||
| } | ||
| await new Promise(resolve => | ||
| common.setTimeout(resolve, interceptor.delayBodyInMs), | ||
| ) | ||
| throw error | ||
| } | ||
| logger('response.rawHeaders:', interceptor.rawHeaders) | ||
| // .reply(status, replyFunction) | ||
| if (interceptor.replyFunction) { | ||
| let fn = interceptor.replyFunction | ||
| if (fn.length === 2) { | ||
| // Handle the case of an async reply function, the third parameter being the callback. | ||
| fn = util.promisify(fn) | ||
| } | ||
| // At this point `fn` is either a synchronous function or a promise-returning function; | ||
| // wrapping in `Promise.resolve` makes it into a promise either way. | ||
| return Promise.resolve(fn.call(interceptor, decompressedRequest)) | ||
| .then(continueWithResponseBody) | ||
| .catch(err => { | ||
| throw err | ||
| }) | ||
| } | ||
| // .reply(fullReplyFunction) | ||
| else if (interceptor.fullReplyFunction) { | ||
| let fn = interceptor.fullReplyFunction | ||
| if (fn.length === 2) { | ||
| fn = util.promisify(fn) | ||
| } | ||
| return Promise.resolve(fn.call(interceptor, decompressedRequest)) | ||
| .then(continueWithFullResponse) | ||
| .catch(err => { | ||
| throw err | ||
| }) | ||
| } | ||
| if ( | ||
| common.isContentEncoded(interceptor.headers) && | ||
| !common.isStream(interceptor.body) | ||
| ) { | ||
| // If the content is encoded we know that the response body *must* be an array | ||
| // of response buffers which should be mocked one by one. | ||
| // (otherwise decompressions after the first one fails as unzip expects to receive | ||
| // buffer by buffer and not one single merged buffer) | ||
| const bufferData = Array.isArray(interceptor.body) | ||
| ? interceptor.body | ||
| : [interceptor.body] | ||
| const responseBuffers = bufferData.map(data => Buffer.from(data, 'hex')) | ||
| const responseBody = new ReadableBuffers(responseBuffers) | ||
| return continueWithResponseBody(responseBody) | ||
| } | ||
| // If we get to this point, the body is either a string or an object that | ||
| // will eventually be JSON stringified. | ||
| let responseBody = interceptor.body | ||
| // If the request was not UTF8-representable then we assume that the | ||
| // response won't be either. In that case we send the response as a Buffer | ||
| // object as that's what the client will expect. | ||
| if (!requestBodyIsUtf8Representable && typeof responseBody === 'string') { | ||
| // Try to create the buffer from the interceptor's body response as hex. | ||
| responseBody = Buffer.from(responseBody, 'hex') | ||
| // Creating buffers does not necessarily throw errors; check for difference in size. | ||
| if ( | ||
| !responseBody || | ||
| (interceptor.body.length > 0 && responseBody.length === 0) | ||
| ) { | ||
| // We fallback on constructing buffer from utf8 representation of the body. | ||
| responseBody = Buffer.from(interceptor.body, 'utf8') | ||
| } | ||
| } | ||
| return continueWithResponseBody(responseBody) | ||
| function continueWithFullResponse(fullReplyResult) { | ||
| const [status, responseBody, rawHeaders] = | ||
| parseFullReplyResult(fullReplyResult) | ||
| return continueWithResponseBody(responseBody, status, rawHeaders) | ||
| } | ||
| async function prepareResponseHeaders(body, responseHeaders) { | ||
| const defaultHeaders = [...interceptor.scope._defaultReplyHeaders] | ||
| const rawHeaders = [] | ||
| // Include a JSON content type when JSON.stringify is called on the body. | ||
| // This is a convenience added by Nock that has no analog in Node. It's added to the | ||
| // defaults, so it will be ignored if the caller explicitly provided the header already. | ||
| const isJSON = | ||
| body !== undefined && | ||
| typeof body !== 'string' && | ||
| !Buffer.isBuffer(body) && | ||
| !common.isStream(body) | ||
| if (isJSON) { | ||
| defaultHeaders.push('Content-Type', 'application/json') | ||
| } | ||
| common.forEachHeader( | ||
| [...interceptor.rawHeaders, ...responseHeaders], | ||
| (value, fieldName) => { | ||
| rawHeaders.push([fieldName, value]) | ||
| }, | ||
| ) | ||
| rawHeaders.push( | ||
| ...selectDefaultHeaders( | ||
| [...interceptor.rawHeaders, ...responseHeaders], | ||
| defaultHeaders, | ||
| ), | ||
| ) | ||
| for (let i = 0; i < rawHeaders.length; i++) { | ||
| const [, value] = rawHeaders[i] | ||
| // Evaluate functional headers. | ||
| if (typeof value === 'function') { | ||
| rawHeaders[i][1] = await value(decompressedRequest, body) | ||
| } | ||
| } | ||
| return new Headers(rawHeaders) | ||
| } | ||
| async function continueWithResponseBody( | ||
| rawBody, | ||
| fullResponseStatus, | ||
| fullResponseRawHeaders = [], | ||
| ) { | ||
| const headers = await prepareResponseHeaders( | ||
| rawBody, | ||
| fullResponseRawHeaders, | ||
| ) | ||
| const bodyAsStream = convertBodyToStream(rawBody) | ||
| bodyAsStream.resume() | ||
| // TODO: there is probably a better way to support delay. | ||
| // Wrap the stream in a duplex stream to support the delay. | ||
| const readable = new stream.Readable({ | ||
| read() {}, | ||
| }) | ||
| bodyAsStream.on('data', function (chunk) { | ||
| readable.push(chunk) | ||
| }) | ||
| bodyAsStream.on('end', function () { | ||
| common.setTimeout(() => { | ||
| readable.push(null) | ||
| interceptor.scope.emit('replied', decompressedRequest, interceptor) | ||
| }, interceptor.delayBodyInMs) | ||
| }) | ||
| bodyAsStream.on('error', function (err) { | ||
| readable.emit('error', err) | ||
| }) | ||
| const status = interceptor.statusCode || fullResponseStatus | ||
| const hasBody = FetchResponse.isResponseWithBody(status) | ||
| return new Response(hasBody ? readable : null, { | ||
| status, | ||
| statusText: STATUS_CODES[status], | ||
| headers, | ||
| }) | ||
| } | ||
| } | ||
| module.exports = { playbackInterceptor } |
-336
| 'use strict' | ||
| const { recorder: debug } = require('./debug') | ||
| const querystring = require('node:querystring') | ||
| const { inspect } = require('node:util') | ||
| const common = require('./common') | ||
| const { restoreOverriddenClientRequest } = require('./intercept') | ||
| const { gzipSync, brotliCompressSync, deflateSync } = require('node:zlib') | ||
| const nodeInterceptors = require('@mswjs/interceptors/presets/node') | ||
| const SEPARATOR = '\n<<<<<<-- cut here -->>>>>>\n' | ||
| let recordingInProgress = false | ||
| let outputs = [] | ||
| // TODO: don't reuse the nodeInterceptors, create new ones. | ||
| const clientRequestInterceptor = nodeInterceptors[0] | ||
| const fetchRequestInterceptor = nodeInterceptors[2] | ||
| /** | ||
| * @param {URL} url | ||
| */ | ||
| function getScope(url) { | ||
| return common.normalizeOrigin(url) | ||
| } | ||
| function getMethod(request) { | ||
| return request.method || 'GET' | ||
| } | ||
| function getBodyFromChunks(chunks, headers) { | ||
| // If we have headers and there is content-encoding it means that the body | ||
| // shouldn't be merged but instead persisted as an array of hex strings so | ||
| // that the response chunks can be mocked one by one. | ||
| if (headers && common.isContentEncoded(headers)) { | ||
| return { | ||
| body: chunks.map(chunk => chunk.toString('hex')), | ||
| } | ||
| } | ||
| const mergedBuffer = Buffer.concat(chunks) | ||
| // The merged buffer can be one of three things: | ||
| // 1. A UTF-8-representable string buffer which represents a JSON object. | ||
| // 2. A UTF-8-representable buffer which doesn't represent a JSON object. | ||
| // 3. A non-UTF-8-representable buffer which then has to be recorded as a hex string. | ||
| const isUtf8Representable = common.isUtf8Representable(mergedBuffer) | ||
| if (isUtf8Representable) { | ||
| const maybeStringifiedJson = mergedBuffer.toString('utf8') | ||
| try { | ||
| return { | ||
| isUtf8Representable, | ||
| body: JSON.parse(maybeStringifiedJson), | ||
| } | ||
| } catch { | ||
| return { | ||
| isUtf8Representable, | ||
| body: maybeStringifiedJson, | ||
| } | ||
| } | ||
| } else { | ||
| return { | ||
| isUtf8Representable, | ||
| body: mergedBuffer.toString('hex'), | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| async function generateRequestAndResponseObject(request, response) { | ||
| const { body, isUtf8Representable } = getBodyFromChunks( | ||
| [Buffer.from(await response.arrayBuffer())], | ||
| Object.fromEntries(response.headers.entries()), | ||
| ) | ||
| const url = new URL(request.url) | ||
| const reqheaders = Object.fromEntries(request.headers.entries()) | ||
| return { | ||
| scope: getScope(url), | ||
| method: getMethod(request), | ||
| path: url.pathname + url.search, | ||
| // Is it deliberate that `getBodyFromChunks()` is called a second time? | ||
| body: getBodyFromChunks([Buffer.from(await request.arrayBuffer())]).body, | ||
| status: response.status, | ||
| response: body, | ||
| rawHeaders: Object.fromEntries(response.headers.entries()), | ||
| reqheaders: Object.keys(reqheaders).length > 0 ? reqheaders : undefined, | ||
| // When content-encoding is enabled, isUtf8Representable is `undefined`, | ||
| // so we explicitly check for `false`. | ||
| responseIsBinary: isUtf8Representable === false, | ||
| } | ||
| } | ||
| /** | ||
| * @param {Request} request | ||
| * @param {Response} response | ||
| */ | ||
| async function generateRequestAndResponse(request, response) { | ||
| const url = new URL(request.url) | ||
| const requestBody = getBodyFromChunks([ | ||
| Buffer.from(await request.arrayBuffer()), | ||
| ]).body | ||
| const responseBody = getBodyFromChunks( | ||
| [Buffer.from(await response.arrayBuffer())], | ||
| response.headers, | ||
| ).body | ||
| // Always encode the query parameters when recording. | ||
| const encodedQueryObj = {} | ||
| for (const [key, value] of Object.entries( | ||
| querystring.parse(url.searchParams.toString()), | ||
| )) { | ||
| const formattedPair = common.formatQueryValue( | ||
| key, | ||
| value, | ||
| common.percentEncode, | ||
| ) | ||
| encodedQueryObj[formattedPair[0]] = formattedPair[1] | ||
| } | ||
| const lines = [] | ||
| // We want a leading newline. | ||
| lines.push('') | ||
| const scope = getScope(url) | ||
| lines.push(`nock('${scope}', {"encodedQueryParams":true})`) | ||
| const methodName = getMethod(request).toLowerCase() | ||
| // Escape any single quotes in the path as the output uses them | ||
| const escapedPath = url.pathname.replace(/'/g, `\\'`) | ||
| if (requestBody) { | ||
| lines.push( | ||
| ` .${methodName}('${escapedPath}', ${JSON.stringify(requestBody)})`, | ||
| ) | ||
| } else { | ||
| lines.push(` .${methodName}('${escapedPath}')`) | ||
| } | ||
| request.headers.forEach((value, name) => { | ||
| const safeName = JSON.stringify(name) | ||
| const safeValue = JSON.stringify(value) | ||
| lines.push(` .matchHeader(${safeName}, ${safeValue})`) | ||
| }) | ||
| if (Object.keys(encodedQueryObj).length > 0) { | ||
| lines.push(` .query(${JSON.stringify(encodedQueryObj)})`) | ||
| } | ||
| const statusCode = response.status.toString() | ||
| const stringifiedResponseBody = JSON.stringify(responseBody) | ||
| const headers = inspect(Object.fromEntries(response.headers.entries())) | ||
| lines.push(` .reply(${statusCode}, ${stringifiedResponseBody}, ${headers});`) | ||
| return lines.join('\n') | ||
| } | ||
| // This module variable is used to identify a unique recording ID in order to skip | ||
| // spurious requests that sometimes happen. This problem has been, so far, | ||
| // exclusively detected in nock's unit testing where 'checks if callback is specified' | ||
| // interferes with other tests as its t.end() is invoked without waiting for request | ||
| // to finish (which is the point of the test). | ||
| let currentRecordingId = 0 | ||
| const defaultRecordOptions = { | ||
| dont_print: false, | ||
| enable_reqheaders_recording: false, | ||
| logging: console.log, | ||
| output_objects: false, | ||
| use_separator: true, | ||
| } | ||
| function record(recOptions) { | ||
| // Trying to start recording with recording already in progress implies an error | ||
| // in the recording configuration (double recording makes no sense and used to lead | ||
| // to duplicates in output) | ||
| if (recordingInProgress) { | ||
| throw new Error('Nock recording already in progress') | ||
| } | ||
| recordingInProgress = true | ||
| // Set the new current recording ID and capture its value in this instance of record(). | ||
| currentRecordingId = currentRecordingId + 1 | ||
| const thisRecordingId = currentRecordingId | ||
| // Originally the parameter was a dont_print boolean flag. | ||
| // To keep the existing code compatible we take that case into account. | ||
| if (typeof recOptions === 'boolean') { | ||
| recOptions = { dont_print: recOptions } | ||
| } | ||
| recOptions = { ...defaultRecordOptions, ...recOptions } | ||
| debug('start recording', thisRecordingId, recOptions) | ||
| const { | ||
| dont_print: dontPrint, | ||
| enable_reqheaders_recording: enableReqHeadersRecording, | ||
| logging, | ||
| output_objects: outputObjects, | ||
| use_separator: useSeparator, | ||
| } = recOptions | ||
| debug(thisRecordingId, 'restoring overridden requests before new overrides') | ||
| // To preserve backward compatibility (starting recording wasn't throwing if nock was already active) | ||
| // we restore any requests that may have been overridden by other parts of nock (e.g. intercept) | ||
| // NOTE: This is hacky as hell but it keeps the backward compatibility *and* allows correct | ||
| // behavior in the face of other modules also overriding ClientRequest. | ||
| // common.restoreOverriddenRequests() | ||
| // We restore ClientRequest as it messes with recording of modules that also override ClientRequest (e.g. xhr2) | ||
| restoreOverriddenClientRequest() | ||
| // We override the requests so that we can save information on them before executing. | ||
| clientRequestInterceptor.apply() | ||
| fetchRequestInterceptor.apply() | ||
| clientRequestInterceptor.on( | ||
| 'response', | ||
| async function ({ request, response }) { | ||
| await recordResponse(request, response) | ||
| }, | ||
| ) | ||
| fetchRequestInterceptor.on( | ||
| 'response', | ||
| async function ({ request, response }) { | ||
| // fetch decompresses the body automatically, so we need to recompress it | ||
| const codings = | ||
| response.headers | ||
| .get('content-encoding') | ||
| ?.toLowerCase() | ||
| .split(',') | ||
| .map(c => c.trim()) || [] | ||
| let body = await response.arrayBuffer() | ||
| for (const coding of codings) { | ||
| if (coding === 'gzip') { | ||
| body = gzipSync(body) | ||
| } else if (coding === 'deflate') { | ||
| body = deflateSync(body) | ||
| } else if (coding === 'br') { | ||
| body = brotliCompressSync(body) | ||
| } | ||
| } | ||
| await recordResponse(request, new Response(body, response)) | ||
| }, | ||
| ) | ||
| /** | ||
| * @param {Request} mswRequest | ||
| * @param {Response} mswResponse | ||
| */ | ||
| async function recordResponse(mswRequest, mswResponse) { | ||
| const request = mswRequest.clone() | ||
| const response = mswResponse.clone() | ||
| debug( | ||
| thisRecordingId, | ||
| request.url.split(':', 1)[0], | ||
| 'intercepted request ended', | ||
| ) | ||
| // Ignore request headers completely unless it was explicitly enabled by the user (see README) | ||
| if (enableReqHeadersRecording) { | ||
| // We never record user-agent headers as they are worse than useless - | ||
| // they actually make testing more difficult without providing any benefit (see README) | ||
| request.headers.delete('user-agent') | ||
| } else { | ||
| // TODO: request.headers.forEach skip a header, need to investigate it. | ||
| const keys = Array.from(request.headers.keys()) | ||
| for (const header of keys) { | ||
| request.headers.delete(header) | ||
| } | ||
| } | ||
| const generateFn = outputObjects | ||
| ? generateRequestAndResponseObject | ||
| : generateRequestAndResponse | ||
| let out = await generateFn(request, response) | ||
| debug('out:', out) | ||
| // Check that the request was made during the current recording. | ||
| // If it hasn't then skip it. There is no other simple way to handle | ||
| // this as it depends on the timing of requests and responses. Throwing | ||
| // will make some recordings/unit tests fail randomly depending on how | ||
| // fast/slow the response arrived. | ||
| // If you are seeing this error then you need to make sure that all | ||
| // the requests made during a single recording session finish before | ||
| // ending the same recording session. | ||
| if (thisRecordingId !== currentRecordingId) { | ||
| debug('skipping recording of an out-of-order request', out) | ||
| return | ||
| } | ||
| outputs.push(out) | ||
| if (!dontPrint) { | ||
| if (useSeparator) { | ||
| if (typeof out !== 'string') { | ||
| out = JSON.stringify(out, null, 2) | ||
| } | ||
| logging(SEPARATOR + out + SEPARATOR) | ||
| } else { | ||
| logging(out) | ||
| } | ||
| } | ||
| debug('finished setting up intercepting') | ||
| } | ||
| } | ||
| // Restore *all* the overridden http/https modules' properties. | ||
| function restore() { | ||
| debug( | ||
| currentRecordingId, | ||
| 'restoring all the overridden http/https properties', | ||
| ) | ||
| clientRequestInterceptor.dispose() | ||
| fetchRequestInterceptor.dispose() | ||
| restoreOverriddenClientRequest() | ||
| recordingInProgress = false | ||
| } | ||
| function clear() { | ||
| outputs = [] | ||
| } | ||
| module.exports = { | ||
| record, | ||
| outputs: () => outputs, | ||
| restore, | ||
| clear, | ||
| } |
-405
| 'use strict' | ||
| /** | ||
| * @module nock/scope | ||
| */ | ||
| const fs = require('node:fs') | ||
| const { scopeDebuglog } = require('./debug') | ||
| const { addInterceptor, isOn } = require('./intercept') | ||
| const common = require('./common') | ||
| const assert = require('node:assert') | ||
| const { EventEmitter } = require('node:events') | ||
| const Interceptor = require('./interceptor') | ||
| /** | ||
| * Normalizes the passed url for consistent internal processing | ||
| * @param {string|URL} u | ||
| */ | ||
| function normalizeUrl(u) { | ||
| if (typeof u === 'string') { | ||
| // If the url is invalid, let the URL library report it | ||
| return normalizeUrl(new URL(u)) | ||
| } | ||
| if (!/https?:/.test(u.protocol)) { | ||
| throw new TypeError( | ||
| `Protocol '${u.protocol}' not recognized. This commonly occurs when a hostname and port are included without a protocol, producing a URL that is valid but confusing, and probably not what you want.`, | ||
| ) | ||
| } | ||
| return { | ||
| href: u.href, | ||
| origin: u.origin, | ||
| protocol: u.protocol, | ||
| username: u.username, | ||
| password: u.password, | ||
| host: u.host, | ||
| hostname: | ||
| // strip brackets from IPv6 | ||
| typeof u.hostname === 'string' && u.hostname.startsWith('[') | ||
| ? u.hostname.slice(1, -1) | ||
| : u.hostname, | ||
| port: u.port || (u.protocol === 'http:' ? 80 : 443), | ||
| pathname: u.pathname, | ||
| search: u.search, | ||
| searchParams: u.searchParams, | ||
| hash: u.hash, | ||
| } | ||
| } | ||
| /** | ||
| * @param {string|RegExp|URL} basePath | ||
| * @param {Object} options | ||
| * @param {boolean} options.allowUnmocked | ||
| * @param {string[]} options.badheaders | ||
| * @param {function} options.conditionally | ||
| * @param {boolean} options.encodedQueryParams | ||
| * @param {function} options.filteringScope | ||
| * @param {Object} options.reqheaders | ||
| * @constructor | ||
| */ | ||
| class Scope extends EventEmitter { | ||
| constructor(basePath, options) { | ||
| super() | ||
| this.keyedInterceptors = {} | ||
| this.interceptors = [] | ||
| this.transformPathFunction = null | ||
| this.transformRequestBodyFunction = null | ||
| this.matchHeaders = [] | ||
| this.scopeOptions = options || {} | ||
| this.urlParts = {} | ||
| this._persist = false | ||
| this.contentLen = false | ||
| this.date = null | ||
| this.basePath = basePath | ||
| this.basePathname = '' | ||
| this.port = null | ||
| this._defaultReplyHeaders = [] | ||
| let logNamespace = String(basePath) | ||
| if (!(basePath instanceof RegExp)) { | ||
| this.urlParts = normalizeUrl(basePath) | ||
| this.port = this.urlParts.port | ||
| this.basePathname = this.urlParts.pathname.replace(/\/$/, '') | ||
| this.basePath = `${this.urlParts.protocol}//${this.urlParts.hostname}:${this.port}` | ||
| logNamespace = this.urlParts.host | ||
| } | ||
| this.logger = scopeDebuglog(logNamespace) | ||
| } | ||
| add(key, interceptor) { | ||
| if (!(key in this.keyedInterceptors)) { | ||
| this.keyedInterceptors[key] = [] | ||
| } | ||
| this.keyedInterceptors[key].push(interceptor) | ||
| addInterceptor( | ||
| this.basePath, | ||
| interceptor, | ||
| this, | ||
| this.scopeOptions, | ||
| this.urlParts.hostname, | ||
| ) | ||
| } | ||
| remove(key, interceptor) { | ||
| if (this._persist) { | ||
| return | ||
| } | ||
| const arr = this.keyedInterceptors[key] | ||
| if (arr) { | ||
| arr.splice(arr.indexOf(interceptor), 1) | ||
| if (arr.length === 0) { | ||
| delete this.keyedInterceptors[key] | ||
| } | ||
| } | ||
| } | ||
| intercept(uri, method, requestBody, interceptorOptions) { | ||
| const ic = new Interceptor( | ||
| this, | ||
| uri, | ||
| method, | ||
| requestBody, | ||
| interceptorOptions, | ||
| ) | ||
| this.interceptors.push(ic) | ||
| return ic | ||
| } | ||
| get(uri, requestBody, options) { | ||
| return this.intercept(uri, 'GET', requestBody, options) | ||
| } | ||
| post(uri, requestBody, options) { | ||
| return this.intercept(uri, 'POST', requestBody, options) | ||
| } | ||
| put(uri, requestBody, options) { | ||
| return this.intercept(uri, 'PUT', requestBody, options) | ||
| } | ||
| head(uri, requestBody, options) { | ||
| return this.intercept(uri, 'HEAD', requestBody, options) | ||
| } | ||
| patch(uri, requestBody, options) { | ||
| return this.intercept(uri, 'PATCH', requestBody, options) | ||
| } | ||
| merge(uri, requestBody, options) { | ||
| return this.intercept(uri, 'MERGE', requestBody, options) | ||
| } | ||
| delete(uri, requestBody, options) { | ||
| return this.intercept(uri, 'DELETE', requestBody, options) | ||
| } | ||
| options(uri, requestBody, options) { | ||
| return this.intercept(uri, 'OPTIONS', requestBody, options) | ||
| } | ||
| // Returns the list of keys for non-optional Interceptors that haven't been completed yet. | ||
| // TODO: This assumes that completed mocks are removed from the keyedInterceptors list | ||
| // (when persistence is off). We should change that (and this) in future. | ||
| pendingMocks() { | ||
| return this.activeMocks().filter(key => | ||
| this.keyedInterceptors[key].some(({ interceptionCounter, optional }) => { | ||
| const persistedAndUsed = this._persist && interceptionCounter > 0 | ||
| return !persistedAndUsed && !optional | ||
| }), | ||
| ) | ||
| } | ||
| // Returns all keyedInterceptors that are active. | ||
| // This includes incomplete interceptors, persisted but complete interceptors, and | ||
| // optional interceptors, but not non-persisted and completed interceptors. | ||
| activeMocks() { | ||
| return Object.keys(this.keyedInterceptors) | ||
| } | ||
| isDone() { | ||
| if (!isOn()) { | ||
| return true | ||
| } | ||
| return this.pendingMocks().length === 0 | ||
| } | ||
| done() { | ||
| assert.ok( | ||
| this.isDone(), | ||
| `Mocks not yet satisfied:\n${this.pendingMocks().join('\n')}`, | ||
| ) | ||
| } | ||
| buildFilter() { | ||
| const filteringArguments = arguments | ||
| if (arguments[0] instanceof RegExp) { | ||
| return function (candidate) { | ||
| /* istanbul ignore if */ | ||
| if (typeof candidate !== 'string') { | ||
| // Given the way nock is written, it seems like `candidate` will always | ||
| // be a string, regardless of what options might be passed to it. | ||
| // However the code used to contain a truthiness test of `candidate`. | ||
| // The check is being preserved for now. | ||
| throw Error( | ||
| `Nock internal assertion failed: typeof candidate is ${typeof candidate}. If you encounter this error, please report it as a bug.`, | ||
| ) | ||
| } | ||
| return candidate.replace(filteringArguments[0], filteringArguments[1]) | ||
| } | ||
| } else if (typeof arguments[0] === 'function') { | ||
| return arguments[0] | ||
| } | ||
| } | ||
| filteringPath() { | ||
| this.transformPathFunction = this.buildFilter.apply(this, arguments) | ||
| if (!this.transformPathFunction) { | ||
| throw new Error( | ||
| 'Invalid arguments: filtering path should be a function or a regular expression', | ||
| ) | ||
| } | ||
| return this | ||
| } | ||
| filteringRequestBody() { | ||
| this.transformRequestBodyFunction = this.buildFilter.apply(this, arguments) | ||
| if (!this.transformRequestBodyFunction) { | ||
| throw new Error( | ||
| 'Invalid arguments: filtering request body should be a function or a regular expression', | ||
| ) | ||
| } | ||
| return this | ||
| } | ||
| matchHeader(name, value) { | ||
| // We use lower-case header field names throughout Nock. | ||
| this.matchHeaders.push({ name: name.toLowerCase(), value }) | ||
| return this | ||
| } | ||
| defaultReplyHeaders(headers) { | ||
| this._defaultReplyHeaders = common.headersInputToRawArray(headers) | ||
| return this | ||
| } | ||
| persist(flag = true) { | ||
| if (typeof flag !== 'boolean') { | ||
| throw new Error('Invalid arguments: argument should be a boolean') | ||
| } | ||
| this._persist = flag | ||
| return this | ||
| } | ||
| /** | ||
| * @private | ||
| * @returns {boolean} | ||
| */ | ||
| shouldPersist() { | ||
| return this._persist | ||
| } | ||
| replyContentLength() { | ||
| this.contentLen = true | ||
| return this | ||
| } | ||
| replyDate(d) { | ||
| this.date = d || new Date() | ||
| return this | ||
| } | ||
| clone() { | ||
| return new Scope(this.basePath, this.scopeOptions) | ||
| } | ||
| } | ||
| function loadDefs(path) { | ||
| if (!fs) { | ||
| throw new Error('No fs') | ||
| } | ||
| const contents = fs.readFileSync(path) | ||
| return JSON.parse(contents) | ||
| } | ||
| function load(path) { | ||
| return define(loadDefs(path)) | ||
| } | ||
| function getStatusFromDefinition(nockDef) { | ||
| // Backward compatibility for when `status` was encoded as string in `reply`. | ||
| if (nockDef.reply !== undefined) { | ||
| const parsedReply = parseInt(nockDef.reply, 10) | ||
| if (isNaN(parsedReply)) { | ||
| throw Error('`reply`, when present, must be a numeric string') | ||
| } | ||
| return parsedReply | ||
| } | ||
| const DEFAULT_STATUS_OK = 200 | ||
| return nockDef.status || DEFAULT_STATUS_OK | ||
| } | ||
| function getScopeFromDefinition(nockDef) { | ||
| // Backward compatibility for when `port` was part of definition. | ||
| if (nockDef.port !== undefined) { | ||
| // Include `port` into scope if it doesn't exist. | ||
| const url = URL.parse(nockDef.scope) | ||
| if (url.port === '') { | ||
| return `${nockDef.scope}:${nockDef.port}` | ||
| } else { | ||
| if (parseInt(url.port) !== parseInt(nockDef.port)) { | ||
| throw new Error( | ||
| 'Mismatched port numbers in scope and port properties of nock definition.', | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| return nockDef.scope | ||
| } | ||
| function tryJsonParse(string) { | ||
| try { | ||
| return JSON.parse(string) | ||
| } catch { | ||
| return string | ||
| } | ||
| } | ||
| function define(nockDefs) { | ||
| const scopes = [] | ||
| nockDefs.forEach(function (nockDef) { | ||
| const nscope = getScopeFromDefinition(nockDef) | ||
| const npath = nockDef.path | ||
| if (!nockDef.method) { | ||
| throw Error('Method is required') | ||
| } | ||
| const method = nockDef.method.toLowerCase() | ||
| const status = getStatusFromDefinition(nockDef) | ||
| const rawHeaders = nockDef.rawHeaders || [] | ||
| const reqheaders = nockDef.reqheaders || {} | ||
| const badheaders = nockDef.badheaders || [] | ||
| const options = { ...nockDef.options } | ||
| // We use request headers for both filtering (see below) and mocking. | ||
| // Here we are setting up mocked request headers but we don't want to | ||
| // be changing the user's options object so we clone it first. | ||
| options.reqheaders = reqheaders | ||
| options.badheaders = badheaders | ||
| // Response is not always JSON as it could be a string or binary data or | ||
| // even an array of binary buffers (e.g. when content is encoded). | ||
| let response | ||
| if (!nockDef.response) { | ||
| response = '' | ||
| // TODO: Rename `responseIsBinary` to `responseIsUtf8Representable`. | ||
| } else if (nockDef.responseIsBinary) { | ||
| response = Buffer.from(nockDef.response, 'hex') | ||
| } else { | ||
| response = | ||
| typeof nockDef.response === 'string' | ||
| ? tryJsonParse(nockDef.response) | ||
| : nockDef.response | ||
| } | ||
| const scope = new Scope(nscope, options) | ||
| // If request headers were specified filter by them. | ||
| Object.entries(reqheaders).forEach(([fieldName, value]) => { | ||
| scope.matchHeader(fieldName, value) | ||
| }) | ||
| const acceptableFilters = ['filteringRequestBody', 'filteringPath'] | ||
| acceptableFilters.forEach(filter => { | ||
| if (nockDef[filter]) { | ||
| scope[filter](nockDef[filter]) | ||
| } | ||
| }) | ||
| scope | ||
| .intercept(npath, method, nockDef.body) | ||
| .reply(status, response, rawHeaders) | ||
| scopes.push(scope) | ||
| }) | ||
| return scopes | ||
| } | ||
| module.exports = { | ||
| Scope, | ||
| load, | ||
| loadDefs, | ||
| define, | ||
| } |
| 'use strict' | ||
| /** | ||
| * @see https://github.com/moll/json-stringify-safe/blob/02cfafd45f06d076ac4bf0dd28be6738a07a72f9/stringify.js | ||
| * @license | ||
| * The ISC License | ||
| * | ||
| * Copyright (c) Isaac Z. Schlueter and Contributors | ||
| * | ||
| * Permission to use, copy, modify, and/or distribute this software for any | ||
| * purpose with or without fee is hereby granted, provided that the above | ||
| * copyright notice and this permission notice appear in all copies. | ||
| * | ||
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
| * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
| * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
| * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
| * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
| * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR | ||
| * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
| */ | ||
| /** | ||
| * @param {*} obj | ||
| * @returns {string} | ||
| */ | ||
| function stringify(obj) { | ||
| return JSON.stringify(obj, safeReplacer()) | ||
| } | ||
| /** | ||
| * @param {Array<*>} stack | ||
| * @param {Array<string>} keys | ||
| * @param {*} value | ||
| * @returns {string} | ||
| */ | ||
| function cycleReplacer(stack, keys, value) { | ||
| if (stack[0] === value) return '[Circular ~]' | ||
| return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]` | ||
| } | ||
| /** | ||
| * @param {Array<*>} [stack] | ||
| * @param {Array<string>} [keys] | ||
| * @returns {(key: string, value: *) => *} */ | ||
| function safeReplacer(stack = [], keys = []) { | ||
| return function (key, value) { | ||
| if (stack.length > 0) { | ||
| const thisPos = stack.indexOf(this) | ||
| ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) | ||
| ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) | ||
| if (~stack.indexOf(value)) { | ||
| value = cycleReplacer(stack, keys, value) | ||
| } | ||
| } else { | ||
| stack.push(value) | ||
| } | ||
| return value | ||
| } | ||
| } | ||
| module.exports = stringify |
| 'use strict' | ||
| const { Readable } = require('node:stream') | ||
| const kGetRequestBody = Symbol('kGetRequestBody') | ||
| /** | ||
| * @param {Request} request | ||
| * @returns {Readable} | ||
| * Returns the request body stream of the given request. | ||
| * @note This is only relevant in the context of `http.ClientRequest`. | ||
| * This function will throw if the given `request` wasn't created based on | ||
| * the `http.ClientRequest` instance. | ||
| * You must rely on the web stream consumers for other request clients. | ||
| */ | ||
| function getGetRequestBody(request) { | ||
| if (request.method !== 'GET') { | ||
| throw new Error('The request method must be GET') | ||
| } | ||
| return Readable.from(Reflect.get(request, kGetRequestBody)) | ||
| } | ||
| /** | ||
| * @param {Request} request | ||
| * @param {ArrayBuffer} body | ||
| */ | ||
| function setGetRequestBody(request, body) { | ||
| Reflect.set(request, kGetRequestBody, Buffer.from(body)) | ||
| } | ||
| module.exports = { | ||
| getGetRequestBody, | ||
| setGetRequestBody, | ||
| } |
-323
| // TypeScript Version: 5.0 | ||
| import { ReadStream } from 'fs' | ||
| import { RequestOptions } from 'http' | ||
| import { ParsedUrlQuery } from 'querystring' | ||
| import { Readable } from 'stream' | ||
| import { Url, URLSearchParams } from 'url' | ||
| export = nock | ||
| declare function nock( | ||
| basePath: string | RegExp | URL, | ||
| options?: nock.Options, | ||
| ): nock.Scope | ||
| declare namespace nock { | ||
| function cleanAll(): void | ||
| function activate(): void | ||
| function isActive(): boolean | ||
| function isDone(): boolean | ||
| function pendingMocks(): string[] | ||
| function activeMocks(): string[] | ||
| function removeInterceptor(interceptor: Interceptor | ReqOptions): boolean | ||
| function disableNetConnect(): void | ||
| function enableNetConnect( | ||
| matcher?: string | RegExp | ((host: string) => boolean), | ||
| ): void | ||
| function load(path: string): Scope[] | ||
| function loadDefs(path: string): Definition[] | ||
| function define(defs: Definition[]): Scope[] | ||
| function restore(): void | ||
| function abortPendingRequests(): void | ||
| let back: Back | ||
| let emitter: NockEmitter | ||
| let recorder: Recorder | ||
| type InterceptFunction = ( | ||
| uri: string | RegExp | { (uri: string): boolean }, | ||
| requestBody?: RequestBodyMatcher, | ||
| interceptorOptions?: Options, | ||
| ) => Interceptor | ||
| // Essentially valid, decoded JSON with the addition of possible RegExp. TS doesn't currently have | ||
| // a great way to represent JSON type data, this data matcher design is based off this comment. | ||
| // https://github.com/microsoft/TypeScript/issues/1897#issuecomment-338650717 | ||
| type DataMatcher = | ||
| | boolean | ||
| | number | ||
| | string | ||
| | null | ||
| | undefined | ||
| | RegExp | ||
| | DataMatcherArray | ||
| | DataMatcherMap | ||
| interface DataMatcherArray extends ReadonlyArray<DataMatcher> {} | ||
| interface DataMatcherMap { | ||
| [key: string]: DataMatcher | ||
| } | ||
| type RequestBodyMatcher = | ||
| | string | ||
| | Buffer | ||
| | RegExp | ||
| | DataMatcherArray | ||
| | DataMatcherMap | ||
| | { (body: any): boolean } | ||
| type RequestHeaderMatcher = | ||
| | string | ||
| | RegExp | ||
| | { (fieldValue: string): boolean } | ||
| type Body = string | Record<string, any> // a string or decoded JSON | ||
| type ReplyBody = Body | Buffer | ReadStream | ||
| type ReplyHeaderFunction = ( | ||
| req: Request, | ||
| body?: string | Buffer, | ||
| ) => string | string[] | Promise<string | string[]> | ||
| type ReplyHeaderValue = string | string[] | ReplyHeaderFunction | ||
| type ReplyHeaders = | ||
| | Record<string, ReplyHeaderValue> | ||
| | Map<string, ReplyHeaderValue> | ||
| | ReplyHeaderValue[] | ||
| type StatusCode = number | ||
| type ReplyFnResult = | ||
| | readonly [StatusCode] | ||
| | readonly [StatusCode, ReplyBody] | ||
| | readonly [StatusCode, ReplyBody, ReplyHeaders] | ||
| /** | ||
| * Detailed mismatch information for the 'no match' event | ||
| * @experimental This interface may change in future versions based on community feedback. | ||
| */ | ||
| interface InterceptorMatchResult { | ||
| interceptor: Interceptor | ||
| reasons: string[] | ||
| } | ||
| /** | ||
| * Enhanced global emitter with typed 'no match' event | ||
| */ | ||
| interface NockEmitter extends NodeJS.EventEmitter { | ||
| on(event: 'no match', listener: (req: Request) => void): this | ||
| on( | ||
| event: 'no match', | ||
| listener: ( | ||
| req: Request, | ||
| interceptorResults?: InterceptorMatchResult[], | ||
| ) => void, | ||
| ): this | ||
| once(event: 'no match', listener: (req: Request) => void): this | ||
| once( | ||
| event: 'no match', | ||
| listener: ( | ||
| req: Request, | ||
| interceptorResults?: InterceptorMatchResult[], | ||
| ) => void, | ||
| ): this | ||
| emit( | ||
| event: 'no match', | ||
| req: Request, | ||
| interceptorResults?: InterceptorMatchResult[], | ||
| ): boolean | ||
| } | ||
| interface Scope extends NodeJS.EventEmitter { | ||
| get: InterceptFunction | ||
| post: InterceptFunction | ||
| put: InterceptFunction | ||
| head: InterceptFunction | ||
| patch: InterceptFunction | ||
| merge: InterceptFunction | ||
| delete: InterceptFunction | ||
| options: InterceptFunction | ||
| intercept: ( | ||
| uri: string | RegExp | { (uri: string): boolean }, | ||
| method: string, | ||
| requestBody?: RequestBodyMatcher, | ||
| options?: Options, | ||
| ) => Interceptor | ||
| defaultReplyHeaders(headers: ReplyHeaders): this | ||
| matchHeader(name: string, value: RequestHeaderMatcher): this | ||
| filteringPath(regex: RegExp, replace: string): this | ||
| filteringPath(fn: (path: string) => string): this | ||
| filteringRequestBody(regex: RegExp, replace: string): this | ||
| filteringRequestBody( | ||
| fn: (body: string, recordedBody: string) => string, | ||
| ): this | ||
| persist(flag?: boolean): this | ||
| replyContentLength(): this | ||
| replyDate(d?: Date): this | ||
| done(): void | ||
| isDone(): boolean | ||
| pendingMocks(): string[] | ||
| activeMocks(): string[] | ||
| } | ||
| interface Interceptor { | ||
| query( | ||
| matcher: | ||
| | boolean | ||
| | string | ||
| | DataMatcherMap | ||
| | URLSearchParams | ||
| | { (parsedObj: ParsedUrlQuery): boolean }, | ||
| ): this | ||
| // tslint (as of 5.16) is under the impression that the callback types can be unified, | ||
| // however, doing so causes the params to lose their inherited types during use. | ||
| // the order of the overrides is important for determining the param types in the replay fns. | ||
| /* tslint:disable:unified-signatures */ | ||
| reply( | ||
| replyFnWithCallback: ( | ||
| request: Request, | ||
| callback: ( | ||
| err: NodeJS.ErrnoException | null, | ||
| result: ReplyFnResult, | ||
| ) => void, | ||
| ) => void, | ||
| ): Scope | ||
| reply( | ||
| replyFn: (request: Request) => ReplyFnResult | Promise<ReplyFnResult>, | ||
| ): Scope | ||
| reply( | ||
| statusCode: StatusCode, | ||
| replyBodyFnWithCallback: ( | ||
| request: Request, | ||
| callback: ( | ||
| err: NodeJS.ErrnoException | null, | ||
| result: ReplyBody, | ||
| ) => void, | ||
| ) => void, | ||
| headers?: ReplyHeaders, | ||
| ): Scope | ||
| reply( | ||
| statusCode: StatusCode, | ||
| replyBodyFn: (request: Request) => ReplyBody | Promise<ReplyBody>, | ||
| headers?: ReplyHeaders, | ||
| ): Scope | ||
| reply(responseCode?: StatusCode, body?: Body, headers?: ReplyHeaders): Scope | ||
| /* tslint:enable:unified-signatures */ | ||
| replyWithError(errorMessage: string | object): Scope | ||
| passthrough(): Scope | ||
| replyWithFile( | ||
| statusCode: StatusCode, | ||
| fileName: string, | ||
| headers?: ReplyHeaders, | ||
| ): Scope | ||
| matchHeader(name: string, value: RequestHeaderMatcher): this | ||
| basicAuth(options: { user: string; pass?: string }): this | ||
| times(newCounter: number): this | ||
| once(): this | ||
| twice(): this | ||
| thrice(): this | ||
| optionally(flag?: boolean): this | ||
| delay(opts: number): this | ||
| } | ||
| /** | ||
| * Retrieves the decompressed body of a GET request. | ||
| * This function handles the edge case of GET requests with a body. | ||
| * | ||
| * @param request - The Request object. | ||
| * @returns A Promise resolving to the decompressed body. | ||
| */ | ||
| function getGetRequestBody(request: Request): Promise<Readable> | ||
| interface Options { | ||
| allowUnmocked?: boolean | ||
| reqheaders?: Record<string, RequestHeaderMatcher> | ||
| badheaders?: string[] | ||
| filteringScope?: { (scope: string): boolean } | ||
| encodedQueryParams?: boolean | ||
| } | ||
| interface Recorder { | ||
| rec(options?: boolean | RecorderOptions): void | ||
| clear(): void | ||
| play(): string[] | Definition[] | ||
| } | ||
| interface RecorderOptions { | ||
| dont_print?: boolean | ||
| output_objects?: boolean | ||
| enable_reqheaders_recording?: boolean | ||
| logging?: (content: string) => void | ||
| use_separator?: boolean | ||
| } | ||
| interface Definition { | ||
| scope: string | RegExp | ||
| path: string | RegExp | ||
| port?: number | string | ||
| method?: string | ||
| status?: number | ||
| body?: RequestBodyMatcher | ||
| reqheaders?: Record<string, RequestHeaderMatcher> | ||
| response?: ReplyBody | ||
| headers?: ReplyHeaders | ||
| options?: Options | ||
| } | ||
| type BackMode = 'wild' | 'dryrun' | 'record' | 'update' | 'lockdown' | ||
| interface Back { | ||
| currentMode: BackMode | ||
| fixtures: string | ||
| setMode(mode: BackMode): void | ||
| (fixtureName: string, nockedFn: (nockDone: () => void) => void): void | ||
| ( | ||
| fixtureName: string, | ||
| options: BackOptions, | ||
| nockedFn: (nockDone: () => void) => void, | ||
| ): void | ||
| ( | ||
| fixtureName: string, | ||
| options?: BackOptions, | ||
| ): Promise<{ | ||
| nockDone: () => void | ||
| context: BackContext | ||
| }> | ||
| } | ||
| interface InterceptorSurface { | ||
| method: string | ||
| uri: string | ||
| basePath: string | ||
| path: string | ||
| queries?: string | ||
| counter: number | ||
| body: string | ||
| statusCode: number | ||
| optional: boolean | ||
| } | ||
| interface BackContext { | ||
| isLoaded: boolean | ||
| scopes: Scope[] | ||
| assertScopesFinished(): void | ||
| query: InterceptorSurface[] | ||
| } | ||
| interface BackOptions { | ||
| before?: (def: Definition) => void | ||
| after?: (scope: Scope) => void | ||
| afterRecord?: (defs: Definition[]) => Definition[] | string | ||
| recorder?: RecorderOptions | ||
| } | ||
| } | ||
| type ReqOptions = RequestOptions & { proto?: string } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
183069
5.75%24
-7.69%35
66.67%3
-25%Yes
NaN3098
-5.72%