Skip to content

Commit

Permalink
Merge branch 'beta' into next
Browse files Browse the repository at this point in the history
# Conflicts:
#	README.md
  • Loading branch information
mastermatt committed Jun 15, 2020
2 parents 9f0ddac + a7b0c13 commit ce6809f
Show file tree
Hide file tree
Showing 38 changed files with 1,603 additions and 1,298 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yaml
Expand Up @@ -161,5 +161,5 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [10, 12, 13]
node-version: [10, 12, 13, 14]
os: [macos-latest, ubuntu-latest, windows-latest]
5 changes: 4 additions & 1 deletion CHANGELOG.md
@@ -1,3 +1,6 @@
# Changelog

Nock’s changelog can be found directly in the [GitHub release notes](https://github.com/nock/nock/releases). These are automatically created by [semantic-release](https://github.com/semantic-release/semantic-release) based on their [commit message conventions](https://semantic-release.gitbook.io/semantic-release#commit-message-format).
Nock’s changelog can be found directly in the [GitHub release notes](https://github.com/nock/nock/releases).
These are automatically created by [semantic-release](https://github.com/semantic-release/semantic-release) based on their [commit message conventions](https://semantic-release.gitbook.io/semantic-release#commit-message-format).

Migration guides are available for major versions in the [migration guides directory](https://github.com/nock/nock/tree/master/migration_guides).
108 changes: 52 additions & 56 deletions README.md
Expand Up @@ -43,10 +43,11 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT
- [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 body](#delay-the-response-body)
- [Delay the response](#delay-the-response)
- [Delay the connection](#delay-the-connection)
- [Socket timeout](#socket-timeout)
- [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)
Expand All @@ -63,7 +64,6 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT
- [.pendingMocks()](#pendingmocks)
- [.activeMocks()](#activemocks)
- [.isActive()](#isactive)
- [Logging](#logging)
- [Restoring](#restoring)
- [Activating](#activating)
- [Turning Nock Off (experimental!)](#turning-nock-off-experimental)
Expand Down Expand Up @@ -661,22 +661,9 @@ nock('http://zombo.com').get('/').thrice().reply(200, 'Ok')

To repeat this response for as long as nock is active, use [.persist()](#persist).

### Delay the response body

You are able to specify the number of milliseconds that the response body should be delayed. Response header will be replied immediately.
`delayBody(1000)` is equivalent to `delay({body: 1000})`.

```js
nock('http://my.server.com')
.get('/')
.delayBody(2000) // 2 seconds
.reply(200, '<html></html>')
```

NOTE: the [`'response'`](http://nodejs.org/api/http.html#http_event_response) event will occur immediately, but the [IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) will not emit its `'end'` event until after the delay.

### Delay the response

Nock can simulate response latency to allow you to test timeouts, race conditions, an other timing related scenarios.
You are able to specify the number of milliseconds that your reply should be delayed.

```js
Expand All @@ -686,53 +673,54 @@ nock('http://my.server.com')
.reply(200, '<html></html>')
```

`delay()` could also be used as
`delay(1000)` is an alias for `delayConnection(1000).delayBody(0)`
`delay({ head: 1000, body: 2000 })` is an alias for `delayConnection(1000).delayBody(2000)`
Both of which are covered in detail below.

```js
delay({
head: headDelayInMs,
body: bodyDelayInMs,
})
```
#### Delay the connection

for example
You are able to specify the number of milliseconds that your connection should be idle before it starts to receive the response.

To simulate a socket timeout, provide a larger value than the timeout setting on the request.

```js
nock('http://my.server.com')
.get('/')
.delay({
head: 2000, // header will be delayed for 2 seconds, i.e. the whole response will be delayed for 2 seconds.
body: 3000, // body will be delayed for another 3 seconds after header is sent out.
})
.delayConnection(2000) // 2 seconds
.reply(200, '<html></html>')

req = http.request('http://my.server.com', { timeout: 1000 })
```

### Delay the connection
Nock emits timeout events almost immediately by comparing the requested connection delay to the timeout parameter passed to `http.request()` or `http.ClientRequest#setTimeout()`.
This allows you to test timeouts without using fake timers or slowing down your tests.
If the client chooses to _not_ take an action (e.g. abort the request), the request and response will continue on as normal, after real clock time has passed.

##### Technical Details

`delayConnection(1000)` is equivalent to `delay({ head: 1000 })`.
Following the `'finish'` event being emitted by `ClientRequest`, Nock will wait for the next event loop iteration before checking if the request has been aborted.
At this point, any connection delay value is compared against any request timeout setting and a [`'timeout'`](https://nodejs.org/api/http.html#http_event_timeout) is emitted when appropriate from the socket and the request objects.
A Node timeout timer is then registered with any connection delay value to delay real time before checking again if the request has been aborted and the [`'response'`](http://nodejs.org/api/http.html#http_event_response) is emitted by the request.

### Socket timeout
A similar method, `.socketDelay()` was removed in version 13. It was thought that having two methods so subtlety similar was confusing.
The discussion can be found at https://github.com/nock/nock/pull/1974.

You are able to specify the number of milliseconds that your connection should be idle, to simulate a socket timeout.
#### Delay the response body

You are able to specify the number of milliseconds that the response body should be delayed.
This is the time between the headers being received and the body starting to be received.

```js
nock('http://my.server.com')
.get('/')
.socketDelay(2000) // 2 seconds
.delayBody(2000) // 2 seconds
.reply(200, '<html></html>')
```

To test a request like the following:
##### Technical Details

```js
req = http.request('http://my.server.com', res => {
...
})
req.setTimeout(1000, () => { req.abort() })
req.end()
```

NOTE: the timeout will be fired immediately, and will not leave the simulated connection idle for the specified period of time.
Following the [`'response'`](http://nodejs.org/api/http.html#http_event_response) being emitted by `ClientRequest`,
Nock will register a timeout timer with the body delay value to delay real time before the [IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) emits its first `'data'` or the `'end'` event.

### Chaining

Expand Down Expand Up @@ -1048,16 +1036,6 @@ if (!nock.isActive()) {
}
```

## Logging

Nock can log matches if you pass in a log function like this:

```js
const scope = nock('http://google.com')
.log(console.log)
...
```

## Restoring

You can restore the HTTP interceptor to the normal unmocked behaviour by calling:
Expand Down Expand Up @@ -1548,8 +1526,26 @@ It does this by manipulating the modules cache of Node in a way that conflicts w
Nock uses [`debug`](https://github.com/visionmedia/debug), so just run with environmental variable `DEBUG` set to `nock.*`.
```console
user@local$ DEBUG=nock.* node my_test.js
```
Each step in the matching process is logged this way and can be useful when determining why a request was not intercepted by Nock.
For example the following shows that matching failed because the request had an extra search parameter.
```js
$ DEBUG=nock.* node my_test.js
nock('http://example.com').get('/').query({ foo: 'bar' }).reply()

await got('http://example.com/?foo=bar&baz=foz')
```
```console
user@local$ DEBUG=nock.scope:example.com node my_test.js
...
nock.scope:example.com Interceptor queries: {"foo":"bar"} +1ms
nock.scope:example.com Request queries: {"foo":"bar","baz":"foz"} +0ms
nock.scope:example.com query matching failed +0ms
```
## Contributing
Expand Down
118 changes: 84 additions & 34 deletions lib/common.js
@@ -1,9 +1,10 @@
'use strict'

const _ = require('lodash')
const debug = require('debug')('nock.common')
const url = require('url')
const set = require('lodash.set')
const timers = require('timers')
const url = require('url')
const util = require('util')

/**
* Normalizes the request options so that it always has `host` property.
Expand Down Expand Up @@ -194,7 +195,7 @@ function isJSONContent(headers) {
* Duplicates throw an error.
*/
function headersFieldNamesToLowerCase(headers) {
if (!_.isPlainObject(headers)) {
if (!isPlainObject(headers)) {
throw Error('Headers must be provided as an object')
}

Expand Down Expand Up @@ -243,11 +244,11 @@ function headersInputToRawArray(headers) {
}

// [].concat(...) is used instead of Array.flat until v11 is the minimum Node version
if (_.isMap(headers)) {
if (util.types.isMap(headers)) {
return [].concat(...Array.from(headers, ([k, v]) => [k.toString(), v]))
}

if (_.isPlainObject(headers)) {
if (isPlainObject(headers)) {
return [].concat(...Object.entries(headers))
}

Expand Down Expand Up @@ -359,7 +360,7 @@ function addHeaderLine(headers, name, value) {
* @fieldName {String} field name - string with the case-insensitive field name
*/
function deleteHeadersField(headers, fieldNameToDelete) {
if (!_.isPlainObject(headers)) {
if (!isPlainObject(headers)) {
throw Error('headers must be an object')
}

Expand Down Expand Up @@ -559,7 +560,7 @@ const dataEqual = (expected, actual) =>
* { 'foo[bar][0]': 'baz' } -> { foo: { bar: [ 'baz' ] } }
*/
const expand = input =>
Object.entries(input).reduce((acc, [k, v]) => _.set(acc, k, v), {})
Object.entries(input).reduce((acc, [k, v]) => set(acc, k, v), {})

/**
* Performs a recursive strict comparison between two values.
Expand All @@ -572,7 +573,7 @@ function deepEqual(expected, actual) {
return expected.test(actual)
}

if (Array.isArray(expected) || _.isPlainObject(expected)) {
if (Array.isArray(expected) || isPlainObject(expected)) {
if (actual === undefined) {
return false
}
Expand All @@ -588,6 +589,51 @@ function deepEqual(expected, actual) {
return expected === actual
}

/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
* https://github.com/lodash/lodash/blob/588bf3e20db0ae039a822a14a8fa238c5b298e65/isPlainObject.js
*
* @param {*} value The value to check.
* @return {boolean}
*/
function isPlainObject(value) {
const isObjectLike = typeof value === 'object' && value !== null
const tag = Object.prototype.toString.call(value)
if (!isObjectLike || tag !== '[object Object]') {
return false
}
if (Object.getPrototypeOf(value) === null) {
return true
}
let proto = value
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(value) === proto
}

/**
* Creates an object with the same keys as `object` and values generated
* by running each own enumerable string keyed property of `object` thru
* `iteratee`. (iteration order is not guaranteed)
* The iteratee is invoked with three arguments: (value, key, object).
* https://github.com/lodash/lodash/blob/588bf3e20db0ae039a822a14a8fa238c5b298e65/mapValue.js
*
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Object} Returns the new mapped object.
*/
function mapValue(object, iteratee) {
object = Object(object)
const result = {}

Object.keys(object).forEach(key => {
result[key] = iteratee(object[key], key, object)
})
return result
}

const timeouts = []
const intervals = []
const immediates = []
Expand All @@ -614,29 +660,33 @@ function removeAllTimers() {
clearTimer(clearImmediate, immediates)
}

exports.normalizeClientRequestArgs = normalizeClientRequestArgs
exports.normalizeRequestOptions = normalizeRequestOptions
exports.normalizeOrigin = normalizeOrigin
exports.isUtf8Representable = isUtf8Representable
exports.overrideRequests = overrideRequests
exports.restoreOverriddenRequests = restoreOverriddenRequests
exports.stringifyRequest = stringifyRequest
exports.isContentEncoded = isContentEncoded
exports.contentEncoding = contentEncoding
exports.isJSONContent = isJSONContent
exports.headersFieldNamesToLowerCase = headersFieldNamesToLowerCase
exports.headersFieldsArrayToLowerCase = headersFieldsArrayToLowerCase
exports.headersArrayToObject = headersArrayToObject
exports.headersInputToRawArray = headersInputToRawArray
exports.deleteHeadersField = deleteHeadersField
exports.forEachHeader = forEachHeader
exports.percentEncode = percentEncode
exports.percentDecode = percentDecode
exports.matchStringOrRegexp = matchStringOrRegexp
exports.formatQueryValue = formatQueryValue
exports.isStream = isStream
exports.dataEqual = dataEqual
exports.setTimeout = setTimeout
exports.setInterval = setInterval
exports.setImmediate = setImmediate
exports.removeAllTimers = removeAllTimers
module.exports = {
contentEncoding,
dataEqual,
deleteHeadersField,
forEachHeader,
formatQueryValue,
headersArrayToObject,
headersFieldNamesToLowerCase,
headersFieldsArrayToLowerCase,
headersInputToRawArray,
isContentEncoded,
isJSONContent,
isPlainObject,
isStream,
isUtf8Representable,
mapValue,
matchStringOrRegexp,
normalizeClientRequestArgs,
normalizeOrigin,
normalizeRequestOptions,
overrideRequests,
percentDecode,
percentEncode,
removeAllTimers,
restoreOverriddenRequests,
setImmediate,
setInterval,
setTimeout,
stringifyRequest,
}

0 comments on commit ce6809f

Please sign in to comment.