Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to mock requests in record mode #2457

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
177 changes: 100 additions & 77 deletions README.md
Expand Up @@ -19,81 +19,83 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT

<!-- toc -->

- [How does it work?](#how-does-it-work)
- [Install](#install)
- [Node version support](#node-version-support)
- [Usage](#usage)
- [READ THIS! - About interceptors](#read-this---about-interceptors)
- [Specifying hostname](#specifying-hostname)
- [Specifying path](#specifying-path)
- [Specifying request body](#specifying-request-body)
- [Specifying request query string](#specifying-request-query-string)
- [Specifying replies](#specifying-replies)
- [Access original request and headers](#access-original-request-and-headers)
- [Replying with errors](#replying-with-errors)
- [Specifying headers](#specifying-headers)
- [Header field names are case-insensitive](#header-field-names-are-case-insensitive)
- [Specifying Request Headers](#specifying-request-headers)
- [Specifying Reply Headers](#specifying-reply-headers)
- [Default Reply Headers](#default-reply-headers)
- [Including Content-Length Header Automatically](#including-content-length-header-automatically)
- [Including Date Header Automatically](#including-date-header-automatically)
- [HTTP Verbs](#http-verbs)
- [Support for HTTP and HTTPS](#support-for-http-and-https)
- [Non-standard ports](#non-standard-ports)
- [Repeat response n times](#repeat-response-n-times)
- [Delay the response](#delay-the-response)
- [Delay the connection](#delay-the-connection)
- [Technical Details](#technical-details)
- [Delay the response body](#delay-the-response-body)
- [Technical Details](#technical-details-1)
- [Chaining](#chaining)
- [Scope filtering](#scope-filtering)
- [Conditional scope filtering](#conditional-scope-filtering)
- [Path filtering](#path-filtering)
- [Request Body filtering](#request-body-filtering)
- [Request Headers Matching](#request-headers-matching)
- [Optional Requests](#optional-requests)
- [Allow **unmocked** requests on a mocked hostname](#allow-unmocked-requests-on-a-mocked-hostname)
- [Expectations](#expectations)
- [.isDone()](#isdone)
- [.cleanAll()](#cleanall)
- [.abortPendingRequests()](#abortpendingrequests)
- [.persist()](#persist)
- [.pendingMocks()](#pendingmocks)
- [.activeMocks()](#activemocks)
- [.isActive()](#isactive)
- [Restoring](#restoring)
- [Activating](#activating)
- [Turning Nock Off (experimental!)](#turning-nock-off-experimental)
- [Enable/Disable real HTTP requests](#enabledisable-real-http-requests)
- [Disabling requests](#disabling-requests)
- [Enabling requests](#enabling-requests)
- [Resetting NetConnect](#resetting-netconnect)
- [Recording](#recording)
- [`dont_print` option](#dont_print-option)
- [`output_objects` option](#output_objects-option)
- [`enable_reqheaders_recording` option](#enable_reqheaders_recording-option)
- [`logging` option](#logging-option)
- [`use_separator` option](#use_separator-option)
- [.removeInterceptor()](#removeinterceptor)
- [Events](#events)
- [Global no match event](#global-no-match-event)
- [Nock Back](#nock-back)
- [Setup](#setup)
- [Options](#options)
- [Usage](#usage-1)
- [Options](#options-1)
- [Example](#example)
- [Modes](#modes)
- [Common issues](#common-issues)
- [Axios](#axios)
- [Memory issues with Jest](#memory-issues-with-jest)
- [Debugging](#debugging)
- [Contributing](#contributing)
- [Contributors](#contributors)
- [Sponsors](#sponsors)
- [License](#license)
- [Nock](#nock)
- [How does it work?](#how-does-it-work)
- [Install](#install)
- [Node version support](#node-version-support)
- [Usage](#usage)
- [READ THIS! - About interceptors](#read-this---about-interceptors)
- [Specifying hostname](#specifying-hostname)
- [Specifying path](#specifying-path)
- [Specifying request body](#specifying-request-body)
- [Specifying request query string](#specifying-request-query-string)
- [Specifying replies](#specifying-replies)
- [Access original request and headers](#access-original-request-and-headers)
- [Replying with errors](#replying-with-errors)
- [Specifying headers](#specifying-headers)
- [Header field names are case-insensitive](#header-field-names-are-case-insensitive)
- [Specifying Request Headers](#specifying-request-headers)
- [Specifying Reply Headers](#specifying-reply-headers)
- [Default Reply Headers](#default-reply-headers)
- [Including Content-Length Header Automatically](#including-content-length-header-automatically)
- [Including Date Header Automatically](#including-date-header-automatically)
- [HTTP Verbs](#http-verbs)
- [Support for HTTP and HTTPS](#support-for-http-and-https)
- [Non-standard ports](#non-standard-ports)
- [Repeat response n times](#repeat-response-n-times)
- [Delay the response](#delay-the-response)
- [Delay the connection](#delay-the-connection)
- [Technical Details](#technical-details)
- [Delay the response body](#delay-the-response-body)
- [Technical Details](#technical-details-1)
- [Chaining](#chaining)
- [Scope filtering](#scope-filtering)
- [Conditional scope filtering](#conditional-scope-filtering)
- [Path filtering](#path-filtering)
- [Request Body filtering](#request-body-filtering)
- [Request Headers Matching](#request-headers-matching)
- [Optional Requests](#optional-requests)
- [Allow **unmocked** requests on a mocked hostname](#allow-unmocked-requests-on-a-mocked-hostname)
- [Expectations](#expectations)
- [.isDone()](#isdone)
- [.cleanAll()](#cleanall)
- [.abortPendingRequests()](#abortpendingrequests)
- [.persist()](#persist)
- [.pendingMocks()](#pendingmocks)
- [.activeMocks()](#activemocks)
- [.isActive()](#isactive)
- [Restoring](#restoring)
- [Activating](#activating)
- [Turning Nock Off (experimental!)](#turning-nock-off-experimental)
- [Enable/Disable real HTTP requests](#enabledisable-real-http-requests)
- [Disabling requests](#disabling-requests)
- [Enabling requests](#enabling-requests)
- [Resetting NetConnect](#resetting-netconnect)
- [Recording](#recording)
- [`dont_print` option](#dont_print-option)
- [`allow_mocked` option](#allow_mocked-option)
- [`output_objects` option](#output_objects-option)
- [`enable_reqheaders_recording` option](#enable_reqheaders_recording-option)
- [`logging` option](#logging-option)
- [`use_separator` option](#use_separator-option)
- [.removeInterceptor()](#removeinterceptor)
- [Events](#events)
- [Global no match event](#global-no-match-event)
- [Nock Back](#nock-back)
- [Setup](#setup)
- [Options](#options)
- [Usage](#usage-1)
- [Options](#options-1)
- [Example](#example)
- [Modes](#modes)
- [Common issues](#common-issues)
- [Axios](#axios)
- [Memory issues with Jest](#memory-issues-with-jest)
- [Debugging](#debugging)
- [Contributing](#contributing)
- [Contributors](#contributors)
- [Sponsors](#sponsors)
- [License](#license)

<!-- tocstop -->

Expand Down Expand Up @@ -1185,8 +1187,6 @@ Recording relies on intercepting real requests and responses and then persisting

In order to stop recording you should call `nock.restore()` and recording will stop.

**ATTENTION!:** when recording is enabled, nock does no validation, nor will any mocks be enabled. Please be sure to turn off recording before attempting to use any mocks in your tests.

### `dont_print` option

If you just want to capture the generated code into a var as an array you can use:
Expand All @@ -1205,6 +1205,29 @@ Copy and paste that code into your tests, customize at will, and you're done! Yo

(Remember that you should do this one test at a time).

### `allow_mocked` option

By default, when recording is enabled, nock does no validation, nor will any mocks be enabled. Please be sure to turn off recording before attempting to use any mocks in your tests.

If you want to intercept and return recorded responses for some requests, you can use:

```js
nock.recorder.rec({
allow_mocked: true,
dont_print: true,
})
nock('http://example.com')
.get('/resource')
.reply(200, 'override')
// Http request to 'http://example.com/resource'
// Http request to 'http://example.com/resource2'
const nockCalls = nock.recorder.play()
```

The `nockCalls` var will only record the non-mocked http requests (i.e. `/resource2`). Calls to matched mocked requests will return the mocked response and will not be recorded.

This is useful when you want your tests to use specific responses for certain requests but to record the rest.

### `output_objects` option

In case you want to generate the code yourself or use the test data in some other way, you can pass the `output_objects` option to `rec`:
Expand Down
2 changes: 2 additions & 0 deletions lib/intercept.js
Expand Up @@ -437,6 +437,7 @@ module.exports = {
addInterceptor,
remove,
removeAll,
interceptorsFor,
removeInterceptor,
isOn,
activate,
Expand All @@ -446,6 +447,7 @@ module.exports = {
activeMocks,
enableNetConnect,
disableNetConnect,
overrideClientRequest,
restoreOverriddenClientRequest,
abortPendingRequests: common.removeAllTimers,
}
30 changes: 27 additions & 3 deletions lib/recorder.js
@@ -1,11 +1,12 @@
'use strict'

const debug = require('debug')('nock.recorder')
const http = require('http')
const querystring = require('querystring')
const { inspect } = require('util')

const common = require('./common')
const { restoreOverriddenClientRequest } = require('./intercept')
const { restoreOverriddenClientRequest, interceptorsFor, isOn, overrideClientRequest } = require('./intercept')

const SEPARATOR = '\n<<<<<<-- cut here -->>>>>>\n'
let recordingInProgress = false
Expand Down Expand Up @@ -166,6 +167,7 @@ function generateRequestAndResponse({
let currentRecordingId = 0

const defaultRecordOptions = {
allow_mocked: false,
dont_print: false,
enable_reqheaders_recording: false,
logging: console.log, // eslint-disable-line no-console
Expand Down Expand Up @@ -198,6 +200,7 @@ function record(recOptions) {
debug('start recording', thisRecordingId, recOptions)

const {
allow_mocked: allowMocked,
dont_print: dontPrint,
enable_reqheaders_recording: enableReqHeadersRecording,
logging,
Expand All @@ -211,8 +214,14 @@ function record(recOptions) {
// 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()

if (allowMocked) {
// We override the client request so we can intercept and return recording when matched.
overrideClientRequest();
} else {
// 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.
common.overrideRequests(function (proto, overriddenRequest, rawArgs) {
Expand All @@ -227,6 +236,21 @@ function record(recOptions) {
}
options._recording = true

// If we allow mocks in recording mode and the request matches, return the
// recorded reply.
if (isOn() && allowMocked) {
options.proto = proto

const interceptors = interceptorsFor(options)

if (interceptors && interceptors.some(interceptor => interceptor.matchOrigin(options))) {
debug(thisRecordingId, 'matched mocked request')
// NOTE: Since we already overrode the http.ClientRequest we are in
// fact constructing our own OverriddenClientRequest.
return new http.ClientRequest(options, callback)
}
}

const req = overriddenRequest(options, function (res) {
debug(thisRecordingId, 'intercepting', proto, 'request to record')

Expand Down
42 changes: 42 additions & 0 deletions tests/test_recorder.js
Expand Up @@ -1261,4 +1261,46 @@ describe('Recorder', () => {
req.end()
})
})

it('allows mocks', async () => {
const exampleText = '<html><body>example</body></html>'
const exampleTextOverride = '<html><body>example override</body></html>'

const { origin } = await servers.startHttpServer((request, response) => {
switch (require('url').parse(request.url).pathname) {
case '/abc':
response.write(exampleText)
break
case '/xyz':
response.write(exampleText)
break
}
response.end()
})

nock.restore()
nock.recorder.clear()
expect(nock.recorder.play()).to.be.empty()

nock.recorder.rec({
allow_mocked: true,
dont_print: true,
output_objects: true,
})

// Override /xyz to return a different response.
nock(origin).get('/xyz').reply(200, exampleTextOverride)
expect(nock.activeMocks()).to.have.lengthOf(1)

const response1 = await got(`${origin}/abc`)
expect(response1.body).to.equal(exampleText)

const response2 = await got(`${origin}/xyz`)
expect(response2.body).to.equal(exampleTextOverride)

// Expect only the non-overridden request to be recorded.
const recorded = nock.recorder.play()
expect(recorded).to.have.lengthOf(1)
expect(recorded[0].path).to.equal('/abc');
})
})
1 change: 1 addition & 0 deletions types/index.d.ts
Expand Up @@ -220,6 +220,7 @@ declare namespace nock {
}

interface RecorderOptions {
allow_mocked?: boolean
dont_print?: boolean
output_objects?: boolean
enable_reqheaders_recording?: boolean
Expand Down