You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
348 lines
10 KiB
348 lines
10 KiB
'use strict'
|
|
|
|
const util = require('util')
|
|
const zlib = require('zlib')
|
|
const debug = require('debug')('nock.playback_interceptor')
|
|
const common = require('./common')
|
|
const DelayedBody = require('./delayed_body')
|
|
|
|
function parseJSONRequestBody(req, requestBody) {
|
|
if (!requestBody || !common.isJSONContent(req.headers)) {
|
|
return requestBody
|
|
}
|
|
|
|
if (common.contentEncoding(req.headers, 'gzip')) {
|
|
requestBody = String(zlib.gunzipSync(Buffer.from(requestBody, 'hex')))
|
|
} else if (common.contentEncoding(req.headers, 'deflate')) {
|
|
requestBody = String(zlib.inflateSync(Buffer.from(requestBody, 'hex')))
|
|
}
|
|
|
|
return JSON.parse(requestBody)
|
|
}
|
|
|
|
function parseFullReplyResult(response, 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`)
|
|
}
|
|
|
|
response.statusCode = status
|
|
response.rawHeaders.push(...common.headersInputToRawArray(headers))
|
|
debug('response.rawHeaders after reply: %j', response.rawHeaders)
|
|
|
|
return body
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* Play back an interceptor using the given request and mock response.
|
|
*/
|
|
function playbackInterceptor({
|
|
req,
|
|
socket,
|
|
options,
|
|
requestBodyString,
|
|
requestBodyIsUtf8Representable,
|
|
response,
|
|
interceptor,
|
|
}) {
|
|
function emitError(error) {
|
|
process.nextTick(() => {
|
|
req.emit('error', error)
|
|
})
|
|
}
|
|
|
|
function start() {
|
|
interceptor.req = req
|
|
req.headers = req.getHeaders()
|
|
|
|
interceptor.scope.emit('request', req, interceptor, requestBodyString)
|
|
|
|
if (typeof interceptor.errorMessage !== 'undefined') {
|
|
interceptor.markConsumed()
|
|
|
|
let error
|
|
if (typeof interceptor.errorMessage === 'object') {
|
|
error = interceptor.errorMessage
|
|
} else {
|
|
error = new Error(interceptor.errorMessage)
|
|
}
|
|
common.setTimeout(() => emitError(error), interceptor.getTotalDelay())
|
|
return
|
|
}
|
|
|
|
// This will be null if we have a fullReplyFunction,
|
|
// in that case status code will be set in `parseFullReplyResult`
|
|
response.statusCode = interceptor.statusCode
|
|
|
|
// Clone headers/rawHeaders to not override them when evaluating later
|
|
response.rawHeaders = [...interceptor.rawHeaders]
|
|
debug('response.rawHeaders:', response.rawHeaders)
|
|
|
|
if (interceptor.replyFunction) {
|
|
const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
|
|
|
|
let fn = interceptor.replyFunction
|
|
if (fn.length === 3) {
|
|
// 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.
|
|
Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
|
|
.then(responseBody => continueWithResponseBody({ responseBody }))
|
|
.catch(err => emitError(err))
|
|
return
|
|
}
|
|
|
|
if (interceptor.fullReplyFunction) {
|
|
const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
|
|
|
|
let fn = interceptor.fullReplyFunction
|
|
if (fn.length === 3) {
|
|
fn = util.promisify(fn)
|
|
}
|
|
|
|
Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
|
|
.then(fullReplyResult => continueWithFullResponse({ fullReplyResult }))
|
|
.catch(err => emitError(err))
|
|
return
|
|
}
|
|
|
|
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)
|
|
|
|
if (interceptor.delayInMs) {
|
|
emitError(
|
|
new Error(
|
|
'Response delay of the body is currently not supported with content-encoded responses.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
const bufferData = Array.isArray(interceptor.body)
|
|
? interceptor.body
|
|
: [interceptor.body]
|
|
const responseBuffers = bufferData.map(data => Buffer.from(data, 'hex'))
|
|
continueWithResponseBody({ responseBuffers })
|
|
return
|
|
}
|
|
|
|
// 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 }) {
|
|
let responseBody
|
|
try {
|
|
responseBody = parseFullReplyResult(response, fullReplyResult)
|
|
} catch (innerErr) {
|
|
emitError(innerErr)
|
|
return
|
|
}
|
|
|
|
continueWithResponseBody({ responseBody })
|
|
}
|
|
|
|
function continueWithResponseBody({ responseBuffers, responseBody }) {
|
|
// Transform the response body if it exists (it may not exist
|
|
// if we have `responseBuffers` instead)
|
|
if (responseBody !== undefined) {
|
|
debug('transform the response body')
|
|
|
|
if (interceptor.delayInMs) {
|
|
debug(
|
|
'delaying the response for',
|
|
interceptor.delayInMs,
|
|
'milliseconds'
|
|
)
|
|
// Because setTimeout is called immediately in DelayedBody(), so we
|
|
// need count in the delayConnectionInMs.
|
|
responseBody = new DelayedBody(
|
|
interceptor.getTotalDelay(),
|
|
responseBody
|
|
)
|
|
}
|
|
|
|
if (common.isStream(responseBody)) {
|
|
debug('response body is a stream')
|
|
responseBody.pause()
|
|
responseBody.on('data', function(d) {
|
|
response.push(d)
|
|
})
|
|
responseBody.on('end', function() {
|
|
response.push(null)
|
|
// https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
|
|
response.complete = true
|
|
})
|
|
responseBody.on('error', function(err) {
|
|
response.emit('error', err)
|
|
})
|
|
} else if (!Buffer.isBuffer(responseBody)) {
|
|
if (typeof responseBody === 'string') {
|
|
responseBody = Buffer.from(responseBody)
|
|
} else {
|
|
responseBody = JSON.stringify(responseBody)
|
|
response.rawHeaders.push('Content-Type', 'application/json')
|
|
}
|
|
}
|
|
// Why are strings converted to a Buffer, but JSON data is left as a string?
|
|
// Related to https://github.com/nock/nock/issues/1542 ?
|
|
}
|
|
|
|
interceptor.markConsumed()
|
|
|
|
if (req.aborted) {
|
|
return
|
|
}
|
|
|
|
response.rawHeaders.push(
|
|
...selectDefaultHeaders(
|
|
response.rawHeaders,
|
|
interceptor.scope._defaultReplyHeaders
|
|
)
|
|
)
|
|
|
|
// Evaluate functional headers.
|
|
common.forEachHeader(response.rawHeaders, (value, fieldName, i) => {
|
|
if (typeof value === 'function') {
|
|
response.rawHeaders[i + 1] = value(req, response, responseBody)
|
|
}
|
|
})
|
|
|
|
response.headers = common.headersArrayToObject(response.rawHeaders)
|
|
|
|
process.nextTick(() =>
|
|
respondUsingInterceptor({
|
|
responseBody,
|
|
responseBuffers,
|
|
})
|
|
)
|
|
}
|
|
|
|
function respondUsingInterceptor({ responseBody, responseBuffers }) {
|
|
if (req.aborted) {
|
|
return
|
|
}
|
|
|
|
function respond() {
|
|
if (req.aborted) {
|
|
return
|
|
}
|
|
|
|
debug('emitting response')
|
|
req.emit('response', response)
|
|
|
|
if (common.isStream(responseBody)) {
|
|
debug('resuming response stream')
|
|
responseBody.resume()
|
|
} else {
|
|
responseBuffers = responseBuffers || []
|
|
if (typeof responseBody !== 'undefined') {
|
|
debug('adding body to buffer list')
|
|
responseBuffers.push(responseBody)
|
|
}
|
|
|
|
// Stream the response chunks one at a time.
|
|
common.setImmediate(function emitChunk() {
|
|
const chunk = responseBuffers.shift()
|
|
|
|
if (chunk) {
|
|
debug('emitting response chunk')
|
|
response.push(chunk)
|
|
common.setImmediate(emitChunk)
|
|
} else {
|
|
debug('ending response stream')
|
|
response.push(null)
|
|
// https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
|
|
response.complete = true
|
|
interceptor.scope.emit('replied', req, interceptor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if (interceptor.socketDelayInMs && interceptor.socketDelayInMs > 0) {
|
|
socket.applyDelay(interceptor.socketDelayInMs)
|
|
}
|
|
|
|
if (
|
|
interceptor.delayConnectionInMs &&
|
|
interceptor.delayConnectionInMs > 0
|
|
) {
|
|
socket.applyDelay(interceptor.delayConnectionInMs)
|
|
common.setTimeout(respond, interceptor.delayConnectionInMs)
|
|
} else {
|
|
respond()
|
|
}
|
|
}
|
|
|
|
start()
|
|
}
|
|
|
|
module.exports = { playbackInterceptor }
|
|
|