🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

nock

Package Overview
Dependencies
Maintainers
4
Versions
468
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

nock - npm Package Compare versions

Comparing version
15.0.0-beta.10
to
15.0.0-beta.11
+60
dist/index.d.ts
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 };
}
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;
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": {

'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')
}
'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
'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 }
'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
'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 }
'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,
}
'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,
}
// 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 }