From a98f71e011a37a0a342aabc38b8d70a0640212bf Mon Sep 17 00:00:00 2001 From: "Matt R. Wilson" Date: Mon, 20 May 2019 11:18:33 -0600 Subject: [PATCH] fix: Update and clarify how .reply() can be invoked with functions (#1520) Ref https://github.com/nock/nock/pull/1517/files#r280139478 Closes #1222 --- README.md | 8 + lib/common.js | 8 +- lib/interceptor.js | 86 ++++++---- lib/request_overrider.js | 254 ++++++++++++++++++----------- lib/scope.js | 6 +- tests/test_content_encoding.js | 3 +- tests/test_delay.js | 9 + tests/test_dynamic_mock.js | 35 +--- tests/test_events.js | 2 +- tests/test_gzip_request.js | 4 +- tests/test_repeating.js | 15 ++ tests/test_reply_body.js | 67 ++++++++ tests/test_reply_function_async.js | 6 +- tests/test_reply_function_sync.js | 178 ++++++++++++++------ tests/test_reply_headers.js | 71 ++++++++ tests/test_stream.js | 3 +- 16 files changed, 536 insertions(+), 219 deletions(-) diff --git a/README.md b/README.md index 44b720c6f..c6d9897b8 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,14 @@ const scope = nock('http://www.google.com') .reply(201, (uri, requestBody) => requestBody) ``` +In Nock 11.x it was possible to invoke `.reply()` with a status code and a +function that returns an array containing a status code and body. (The status +code from the array would take precedence over the one passed directly to +reply.) This is no longer allowed. In 12.x, either call `.reply()` with a +status code and a function that returns the body, or call it with a single +argument: a function that returns an array containing both the status code and +body. + An asynchronous function that gets an error-first callback as its last argument also works: ```js diff --git a/lib/common.js b/lib/common.js index 9a4213911..a839c68ff 100644 --- a/lib/common.js +++ b/lib/common.js @@ -40,7 +40,7 @@ const normalizeRequestOptions = function(options) { * from its utf8 representation. * * TODO: Reverse the semantics of this method and refactor calling code - * accordingly. We've inadvertantly gotten it flipped. + * accordingly. We've inadvertently gotten it flipped. * * @param {Object} buffer - a Buffer object */ @@ -245,7 +245,7 @@ const headersArrayToObject = function(rawHeaders) { const value = rawHeaders[i + 1] if (headers[key]) { - headers[key] = _.isArray(headers[key]) ? headers[key] : [headers[key]] + headers[key] = Array.isArray(headers[key]) ? headers[key] : [headers[key]] headers[key].push(value) } else { headers[key] = value @@ -324,7 +324,7 @@ function matchStringOrRegexp(target, pattern) { * @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. + * @returns *[] the formatted [key, value] pair. */ function formatQueryValue(key, value, stringFormattingFn) { // TODO-coverage: Find out what's not covered. Probably refactor code to @@ -371,7 +371,7 @@ function formatQueryValue(key, value, stringFormattingFn) { function isStream(obj) { return ( obj && - typeof a !== 'string' && + typeof obj !== 'string' && !Buffer.isBuffer(obj) && _.isFunction(obj.setEncoding) ) diff --git a/lib/interceptor.js b/lib/interceptor.js index 7efa74f12..3877f602c 100644 --- a/lib/interceptor.js +++ b/lib/interceptor.js @@ -78,12 +78,28 @@ Interceptor.prototype.replyWithError = function replyWithError(errorMessage) { } Interceptor.prototype.reply = function reply(statusCode, body, rawHeaders) { - if (arguments.length <= 2 && _.isFunction(statusCode)) { - body = statusCode - statusCode = 200 - } + // support the format of only passing in a callback + if (_.isFunction(statusCode)) { + 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 && !Number.isInteger(statusCode)) { + throw new Error(`Invalid ${typeof statusCode} value for status code`) + } - this.statusCode = statusCode + this.statusCode = statusCode || 200 + if (_.isFunction(body)) { + this.replyFunction = body + body = null + } + } _.defaults(this.options, this.scope.scopeOptions) @@ -94,6 +110,8 @@ Interceptor.prototype.reply = function reply(statusCode, body, rawHeaders) { if (this.scope._defaultReplyHeaders) { headers = headers || {} + // Because of this, this.rawHeaders gets lower-cased versions of the `rawHeaders` param. + // https://github.com/nock/nock/issues/1553 headers = common.headersFieldNamesToLowerCase(headers) headers = mixin(this.scope._defaultReplyHeaders, headers) } @@ -107,8 +125,7 @@ Interceptor.prototype.reply = function reply(statusCode, body, rawHeaders) { this.rawHeaders = [] for (const key in headers) { - this.rawHeaders.push(key) - this.rawHeaders.push(headers[key]) + this.rawHeaders.push(key, headers[key]) } // We use lower-case headers throughout Nock. @@ -120,28 +137,27 @@ Interceptor.prototype.reply = function reply(statusCode, body, rawHeaders) { // If the content is not encoded we may need to transform the response body. // Otherwise we leave it as it is. - if (!common.isContentEncoded(this.headers)) { - if ( - body && - typeof body !== 'string' && - typeof body !== 'function' && - !Buffer.isBuffer(body) && - !common.isStream(body) - ) { - try { - body = stringify(body) - if (!this.headers) { - this.headers = {} - } - if (!this.headers['content-type']) { - this.headers['content-type'] = 'application/json' - } - if (this.scope.contentLen) { - this.headers['content-length'] = body.length - } - } catch (err) { - throw new Error('Error encoding response body into JSON') + if ( + body && + typeof body !== 'string' && + typeof body !== 'function' && + !Buffer.isBuffer(body) && + !common.isStream(body) && + !common.isContentEncoded(this.headers) + ) { + try { + body = stringify(body) + if (!this.headers) { + this.headers = {} + } + if (!this.headers['content-type']) { + this.headers['content-type'] = 'application/json' + } + if (this.scope.contentLen) { + this.headers['content-length'] = body.length } + } catch (err) { + throw new Error('Error encoding response body into JSON') } } @@ -454,7 +470,7 @@ Interceptor.prototype.basicAuth = function basicAuth(options) { /** * Set query strings for the interceptor * @name query - * @param Object Object of query string name,values (accepts regexp values) + * @param queries Object of query string name,values (accepts regexp values) * @public * @example * // Will match 'http://zombo.com/?q=t' @@ -496,7 +512,7 @@ Interceptor.prototype.query = function query(queries) { /** * Set number of times will repeat the interceptor * @name times - * @param Integer Number of times to repeat (should be > 0) + * @param newCounter Number of times to repeat (should be > 0) * @public * @example * // Will repeat mock 5 times for same king of request @@ -554,7 +570,7 @@ Interceptor.prototype.thrice = function thrice() { * @param {(integer|object)} opts - Number of milliseconds to wait, or an object * @param {integer} [opts.head] - Number of milliseconds to wait before response is sent * @param {integer} [opts.body] - Number of milliseconds to wait before response body is sent - * @return {interceptor} - the current interceptor for chaining + * @return {Interceptor} - the current interceptor for chaining */ Interceptor.prototype.delay = function delay(opts) { let headDelay = 0 @@ -565,7 +581,7 @@ Interceptor.prototype.delay = function delay(opts) { headDelay = opts.head || 0 bodyDelay = opts.body || 0 } else { - throw new Error(`Unexpected input opts${opts}`) + throw new Error(`Unexpected input opts ${opts}`) } return this.delayConnection(headDelay).delayBody(bodyDelay) @@ -575,7 +591,7 @@ Interceptor.prototype.delay = function delay(opts) { * Delay the response body by a certain number of ms. * * @param {integer} ms - Number of milliseconds to wait before response is sent - * @return {interceptor} - the current interceptor for chaining + * @return {Interceptor} - the current interceptor for chaining */ Interceptor.prototype.delayBody = function delayBody(ms) { this.delayInMs += ms @@ -586,7 +602,7 @@ Interceptor.prototype.delayBody = function delayBody(ms) { * Delay the connection by a certain number of ms. * * @param {integer} ms - Number of milliseconds to wait - * @return {interceptor} - the current interceptor for chaining + * @return {Interceptor} - the current interceptor for chaining */ Interceptor.prototype.delayConnection = function delayConnection(ms) { this.delayConnectionInMs += ms @@ -601,7 +617,7 @@ Interceptor.prototype.getTotalDelay = function getTotalDelay() { * Make the socket idle for a certain number of ms (simulated). * * @param {integer} ms - Number of milliseconds to wait - * @return {interceptor} - the current interceptor for chaining + * @return {Interceptor} - the current interceptor for chaining */ Interceptor.prototype.socketDelay = function socketDelay(ms) { this.socketDelayInMs = ms diff --git a/lib/request_overrider.js b/lib/request_overrider.js index 8ad2c0a44..693dde4af 100644 --- a/lib/request_overrider.js +++ b/lib/request_overrider.js @@ -35,7 +35,7 @@ function setHeader(request, name, value) { function setRequestHeaders(req, options, interceptor) { // If a filtered scope is being used we have to use scope's host // in the header, otherwise 'host' header won't match. - // NOTE: We use lower-case header field names throught Nock. + // NOTE: We use lower-case header field names throughout Nock. const HOST_HEADER = 'host' if (interceptor.__nock_filteredScope && interceptor.__nock_scopeHost) { if (options && options.headers) { @@ -292,97 +292,129 @@ function RequestOverrider(req, options, interceptors, remove, cb) { timers.setTimeout(emitError, interceptor.getTotalDelay(), error) return } - // TODO-coverage: Either add a test or remove `|| 200` if it's not reachable. - response.statusCode = Number(interceptor.statusCode) || 200 + + // 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.headers = _.extend({}, interceptor.headers) + response.headers = { ...interceptor.headers } response.rawHeaders = (interceptor.rawHeaders || []).slice() debug('response.rawHeaders:', response.rawHeaders) - if (typeof interceptor.body === 'function') { - if (requestBody && common.isJSONContent(req.headers)) { - 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')) - ) - } + if (interceptor.replyFunction) { + const parsedRequestBody = parseJSONRequestBody(req, requestBody) - requestBody = JSON.parse(requestBody) + if (interceptor.replyFunction.length === 3) { + // Handle the case of an async reply function, the third parameter being the callback. + interceptor.replyFunction( + options.path, + parsedRequestBody, + continueWithResponseBody + ) + return } - // In case we are waiting for a callback - if (interceptor.body.length === 3) { - return interceptor.body( + const replyResponseBody = interceptor.replyFunction( + options.path, + parsedRequestBody + ) + continueWithResponseBody(null, replyResponseBody) + return + } + + if (interceptor.fullReplyFunction) { + const parsedRequestBody = parseJSONRequestBody(req, requestBody) + + if (interceptor.fullReplyFunction.length === 3) { + interceptor.fullReplyFunction( options.path, - requestBody || '', - continueWithResponseBody + parsedRequestBody, + continueWithFullResponse ) + return } - responseBody = interceptor.body(options.path, requestBody) || '' - } else { + const fullReplyResult = interceptor.fullReplyFunction( + options.path, + parsedRequestBody + ) + continueWithFullResponse(null, fullReplyResult) + return + } + + if ( + common.isContentEncoded(response.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 ( - common.isContentEncoded(response.headers) && - !common.isStream(interceptor.body) - ) { - if (interceptor.delayInMs) { - // TODO-coverage: Add a test of this error case. - emitError( - new Error( - 'Response delay is currently not supported with content-encoded responses.' - ) + + if (interceptor.delayInMs) { + // TODO-coverage: Add a test of this error case. + emitError( + new Error( + 'Response delay is currently not supported with content-encoded responses.' ) - return - } + ) + return + } - let buffers = interceptor.body - if (!_.isArray(buffers)) { - buffers = [buffers] - } + const bufferData = Array.isArray(interceptor.body) + ? interceptor.body + : [interceptor.body] + responseBuffers = bufferData.map(data => Buffer.from(data, 'hex')) + continueWithResponseBody(null, undefined) + return + } - responseBuffers = _.map(buffers, function(buffer) { - return Buffer.from(buffer, 'hex') - }) - } else { - responseBody = interceptor.body - - // If the request was binary then we assume that the response will be binary as well. - // In that case we send the response as a Buffer object as that's what the client will expect. - if (isBinaryRequestBodyBuffer && typeof responseBody === 'string') { - // Try to create the buffer from the interceptor's body response as hex. - try { - responseBody = Buffer.from(responseBody, 'hex') - } catch (err) { - // TODO-coverage: Add a test of this error case. - debug( - 'exception during Buffer construction from hex data:', - responseBody, - '-', - err - ) - } + // If we get to this point, the body is either a string or an + // object that will eventually be JSON stringified + responseBody = interceptor.body + + // If the request was binary then we assume that the response will be binary as well. + // In that case we send the response as a Buffer object as that's what the client will expect. + if (isBinaryRequestBodyBuffer && typeof responseBody === 'string') { + // Try to create the buffer from the interceptor's body response as hex. + try { + responseBody = Buffer.from(responseBody, 'hex') + } catch (err) { + // TODO-coverage: Add a test of this error case. + debug( + 'exception during Buffer construction from hex data:', + responseBody, + '-', + err + ) + } - // 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') - } - } + // 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(null, responseBody) + function continueWithFullResponse(err, fullReplyResult) { + if (!err) { + try { + responseBody = parseFullReplyResult(response, fullReplyResult) + } catch (innerErr) { + emitError(innerErr) + return + } + } + + continueWithResponseBody(err, responseBody) + } + function continueWithResponseBody(err, responseBody) { // TODO-coverage: Try to find out when this happens and add a test. if (continued) { @@ -398,29 +430,9 @@ function RequestOverrider(req, options, interceptors, remove, cb) { // Transform the response body if it exists (it may not exist // if we have `responseBuffers` instead) - if (responseBody) { + if (responseBody !== undefined) { debug('transform the response body') - if (Array.isArray(responseBody)) { - debug('response body is array: %j', responseBody) - - // TODO-coverage: else, throw an error. Add a test. - if (!isNaN(Number(responseBody[0]))) { - response.statusCode = Number(responseBody[0]) - } - - if (responseBody.length >= 2 && responseBody.length <= 3) { - debug('new headers: %j', responseBody[2]) - _.assign(response.headers, responseBody[2] || {}) - debug('response.headers after: %j', response.headers) - responseBody = responseBody[1] - - Object.keys(response.headers).forEach(function(key) { - response.rawHeaders.push(key, response.headers[key]) - }) - } - } - if (interceptor.delayInMs) { debug( 'delaying the response for', @@ -448,7 +460,7 @@ function RequestOverrider(req, options, interceptors, remove, cb) { // TODO-coverage: Add a test. response.emit('error', err) }) - } else if (responseBody && !Buffer.isBuffer(responseBody)) { + } else if (!Buffer.isBuffer(responseBody)) { if (typeof responseBody === 'string') { responseBody = Buffer.from(responseBody) } else { @@ -456,6 +468,8 @@ function RequestOverrider(req, options, interceptors, remove, cb) { response.headers['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.interceptionCounter++ @@ -480,9 +494,7 @@ function RequestOverrider(req, options, interceptors, remove, cb) { // Evaluate functional headers. const evaluatedHeaders = {} - Object.keys(response.headers).forEach(function(key) { - const value = response.headers[key] - + Object.entries(response.headers).forEach(function([key, value]) { if (typeof value === 'function') { response.headers[key] = evaluatedHeaders[key] = value( req, @@ -579,4 +591,62 @@ function RequestOverrider(req, options, interceptors, remove, cb) { return req } +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 + + if (headers) { + debug('new headers: %j', headers) + + const rawHeaders = Array.isArray(headers) + ? headers + : [].concat(...Object.entries(headers)) + response.rawHeaders.push(...rawHeaders) + + const castHeaders = Array.isArray(headers) + ? common.headersArrayToObject(headers) + : common.headersFieldNamesToLowerCase(headers) + + response.headers = { + ...response.headers, + ...castHeaders, + } + + debug('response.headers after: %j', response.headers) + } + + return body +} + module.exports = RequestOverrider diff --git a/lib/scope.js b/lib/scope.js index c19339169..02298330b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -218,6 +218,8 @@ Scope.prototype.matchHeader = function matchHeader(name, value) { } Scope.prototype.defaultReplyHeaders = function defaultReplyHeaders(headers) { + // Because these are lower-cased, res.rawHeaders can have the wrong keys. + // https://github.com/nock/nock/issues/1553 this._defaultReplyHeaders = common.headersFieldNamesToLowerCase(headers) return this } @@ -333,7 +335,7 @@ function define(nockDefs) { 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 enconded) + // even an array of binary buffers (e.g. when content is encoded) let response if (!nockDef.response) { response = '' @@ -348,7 +350,7 @@ function define(nockDefs) { let nock // TODO-coverage: Find out what `body === '*'` means. Add a comment here // explaining it, and a test case if necessary. Also, update the readme - // example of `filteringRequestBody()` beacuse it doesn't explain what the + // example of `filteringRequestBody()` because it doesn't explain what the // `*` really means. if (body === '*') { nock = startScope(nscope, options) diff --git a/tests/test_content_encoding.js b/tests/test_content_encoding.js index 19bc4b1bd..fac64840f 100644 --- a/tests/test_content_encoding.js +++ b/tests/test_content_encoding.js @@ -18,7 +18,8 @@ test('accepts gzipped content', async t => { 'Content-Length': undefined, 'Content-Encoding': 'gzip', }) - const { body } = await got('http://example.test/foo') + const { body, statusCode } = await got('http://example.test/foo') t.equal(body, message) + t.equal(statusCode, 200) }) diff --git a/tests/test_delay.js b/tests/test_delay.js index 92f6f438a..7f9d34830 100644 --- a/tests/test_delay.js +++ b/tests/test_delay.js @@ -274,6 +274,15 @@ test('using reply callback with delay can reply JSON', t => { ) }) +test('delay with invalid arguments', t => { + const interceptor = nock('http://example.test').get('/') + t.throws( + () => interceptor.delay('one million seconds'), + Error('Unexpected input') + ) + t.end() +}) + test('delay works with replyWithFile', t => { // Do not base new tests on this one. Write async tests using // `resolvesInAtLeast` instead. diff --git a/tests/test_dynamic_mock.js b/tests/test_dynamic_mock.js index 1b7b9fd08..28145bf2a 100644 --- a/tests/test_dynamic_mock.js +++ b/tests/test_dynamic_mock.js @@ -6,23 +6,6 @@ const nock = require('..') require('./cleanup_after_each')() -test('one function returning the body defines a full mock', function(t) { - nock('http://example.test') - .get('/abc') - .reply(function() { - return 'ABC' - }) - - request.get('http://example.test/abc', function(err, resp, body) { - if (err) { - throw err - } - t.equal(resp.statusCode, 200) - t.equal(body, 'ABC') - t.end() - }) -}) - test('one function returning the status code and body defines a full mock', function(t) { nock('http://example.test') .get('/def') @@ -31,9 +14,7 @@ test('one function returning the status code and body defines a full mock', func }) request.get('http://example.test/def', function(err, resp, body) { - if (err) { - throw err - } + t.error(err) t.equal(resp.statusCode, 201) t.equal(body, 'DEF') t.end() @@ -50,9 +31,7 @@ test('one asynchronous function returning the status code and body defines a ful }) request.get('http://example.test/ghi', function(err, resp, body) { - if (err) { - throw err - } + t.error(err) t.equal(resp.statusCode, 201) t.equal(body, 'GHI') t.end() @@ -62,7 +41,7 @@ test('one asynchronous function returning the status code and body defines a ful test('asynchronous function gets request headers', function(t) { nock('http://example.test') .get('/yo') - .reply(200, function(path, reqBody, cb) { + .reply(201, function(path, reqBody, cb) { t.equal(this.req.path, '/yo') t.deepEqual(this.req.headers, { 'x-my-header': 'some-value', @@ -70,7 +49,7 @@ test('asynchronous function gets request headers', function(t) { host: 'example.test', }) setTimeout(function() { - cb(null, [201, 'GHI']) + cb(null, 'foobar') }, 1e3) }) @@ -84,11 +63,9 @@ test('asynchronous function gets request headers', function(t) { }, }, function(err, resp, body) { - if (err) { - throw err - } + t.error(err) t.equal(resp.statusCode, 201) - t.equal(body, 'GHI') + t.equal(body, 'foobar') t.end() } ) diff --git a/tests/test_events.js b/tests/test_events.js index 45d21f90a..9c039c90d 100644 --- a/tests/test_events.js +++ b/tests/test_events.js @@ -71,7 +71,7 @@ test('emits no match when no match and no mock', function(t) { test('emits no match when no match and mocked', function(t) { nock('http://example.test') .get('/') - .reply('howdy') + .reply(418) const assertion = function(req) { t.equal(req.path, '/definitelymaybe') diff --git a/tests/test_gzip_request.js b/tests/test_gzip_request.js index 9d1f8d0d8..abf84cc1a 100644 --- a/tests/test_gzip_request.js +++ b/tests/test_gzip_request.js @@ -19,7 +19,7 @@ test('accepts and decodes gzip encoded application/json', t => { .reply(function(url, actual) { t.same(actual, message) t.end() - return 200 + return [200] }) const req = http.request({ @@ -50,7 +50,7 @@ test('accepts and decodes deflate encoded application/json', t => { .reply(function(url, actual) { t.same(actual, message) t.end() - return 200 + return [200] }) const req = http.request({ diff --git a/tests/test_repeating.js b/tests/test_repeating.js index 78316613a..6f19f7f2f 100644 --- a/tests/test_repeating.js +++ b/tests/test_repeating.js @@ -4,6 +4,7 @@ const http = require('http') const async = require('async') const { test } = require('tap') const nock = require('..') +const got = require('./got_client') require('./cleanup_after_each')() @@ -81,6 +82,20 @@ test('repeating response 4 times', t => { ) }) +test('times with invalid argument is ignored', async t => { + nock.disableNetConnect() + + const scope = nock('http://example.test') + .get('/') + .times(0) + .reply(200, 'Hello World!') + + const { statusCode } = await got('http://example.test/') + t.is(statusCode, 200) + + scope.done() +}) + test('isDone() must consider repeated responses', t => { const scope = nock('http://example.test') .get('/') diff --git a/tests/test_reply_body.js b/tests/test_reply_body.js index 17f116a57..ba006a43b 100644 --- a/tests/test_reply_body.js +++ b/tests/test_reply_body.js @@ -23,6 +23,21 @@ test('reply with JSON', async t => { scope.done() }) +test('reply with JSON array', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(200, [{ hello: 'world' }]) + + const { statusCode, headers, body } = await got('http://example.test/') + + t.equal(statusCode, 200) + t.type(headers.date, 'undefined') + t.type(headers['content-length'], 'undefined') + t.equal(headers['content-type'], 'application/json') + t.equal(body, '[{"hello":"world"}]', 'response should match') + scope.done() +}) + test('JSON encoded replies set the content-type header', async t => { const scope = nock('http://example.test') .get('/') @@ -91,3 +106,55 @@ test('unencodable object throws the expected error', t => { t.end() }) + +test('reply with missing body defaults to empty', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(204) + + const { statusCode, body } = await got('http://example.test/') + + t.is(statusCode, 204) + t.equal(body, '') + scope.done() +}) + +// while `false` and `null` are falsy, they are valid JSON value so they should be returned as a strings +// that JSON.parse would convert back to native values +test('reply with native boolean as the body', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(204, false) + + const { statusCode, body } = await got('http://example.test/') + + t.is(statusCode, 204) + // `'false'` is json-stringified `false`. + t.equal(body, 'false') + scope.done() +}) + +test('reply with native null as the body', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(204, null) + + const { statusCode, body } = await got('http://example.test/') + + t.is(statusCode, 204) + // `'null'` is json-stringified `null`. + t.equal(body, 'null') + scope.done() +}) + +test('reply with missing status code defaults to 200', async t => { + const scope = nock('http://example.test') + .get('/') + .reply() + + const { statusCode, body } = await got('http://example.test/') + + t.is(statusCode, 200) + t.equal(body, '') + scope.done() +}) diff --git a/tests/test_reply_function_async.js b/tests/test_reply_function_async.js index 256111b88..b78eccfea 100644 --- a/tests/test_reply_function_async.js +++ b/tests/test_reply_function_async.js @@ -41,7 +41,11 @@ test('reply takes a callback for status code', async t => { const response = await got('http://example.com/') t.equal(response.statusCode, expectedStatusCode, 'sends status code') - t.deepEqual(response.headers, headers, 'sends headers') + t.deepEqual( + response.headers, + { 'x-custom-header': 'abcdef' }, + 'sends headers' + ) t.equal(response.body, responseBody, 'sends request body') scope.done() }) diff --git a/tests/test_reply_function_sync.js b/tests/test_reply_function_sync.js index fbd992c54..5b8a1151e 100644 --- a/tests/test_reply_function_sync.js +++ b/tests/test_reply_function_sync.js @@ -14,9 +14,10 @@ require('./cleanup_after_each')() test('reply with status code and function returning body as string', async t => { const scope = nock('http://example.com') .get('/') - .reply(200, () => 'OK!') + .reply(201, () => 'OK!') - const { body } = await got('http://example.com') + const { statusCode, body } = await got('http://example.com') + t.is(statusCode, 201) t.equal(body, 'OK!') scope.done() }) @@ -26,9 +27,10 @@ test('reply with status code and function returning body object', async t => { const scope = nock('http://example.test') .get('/') - .reply(200, () => exampleResponse) + .reply(201, () => exampleResponse) - const { body } = await got('http://example.test') + const { statusCode, body } = await got('http://example.test') + t.is(statusCode, 201) t.equal(body, JSON.stringify(exampleResponse)) scope.done() }) @@ -36,30 +38,46 @@ test('reply with status code and function returning body object', async t => { test('reply with status code and function returning body as number', async t => { const scope = nock('http://example.test') .get('/') - .reply(200, () => 123) + .reply(201, () => 123) - const { body } = await got('http://example.test') + const { statusCode, body } = await got('http://example.test') + t.is(statusCode, 201) t.equal(body, '123') scope.done() }) -// The observed behavior is that this returns a 123 status code. -// -// The expected behavior is that this should either throw an error or reply -// with 200 and the JSON-stringified '[123]'. -test( - 'reply with status code and function returning array', - { skip: true }, - async t => { - const scope = nock('http://example.test') - .get('/') - .reply(200, () => [123]) +test('reply with status code and function returning array', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(201, () => [123]) + + const { statusCode, body } = await got('http://example.test') + t.is(statusCode, 201) + t.equal(body, '[123]') + scope.done() +}) + +test('reply with status code and function returning a native boolean', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(201, () => false) + + const { statusCode, body } = await got('http://example.test') + t.is(statusCode, 201) + t.equal(body, 'false') + scope.done() +}) - const { body } = await got('http://example.test') - t.equal(body, '[123]') - scope.done() - } -) +test('reply with status code and function returning a native null', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(201, () => null) + + const { statusCode, body } = await got('http://example.test') + t.is(statusCode, 201) + t.equal(body, 'null') + scope.done() +}) test('reply function with string body using POST', async t => { const exampleRequestBody = 'key=val' @@ -74,7 +92,7 @@ test('reply function with string body using POST', async t => { body: exampleRequestBody, }), ({ statusCode, body }) => { - t.equal(statusCode, 404) + t.is(statusCode, 404) t.equal(body, exampleResponseBody) return true } @@ -83,7 +101,7 @@ test('reply function with string body using POST', async t => { }) test('reply function receives the request URL and body', async t => { - t.plan(3) + t.plan(4) const exampleRequestBody = 'key=val' @@ -100,6 +118,7 @@ test('reply function receives the request URL and body', async t => { }), ({ statusCode, body }) => { t.equal(statusCode, 404) + t.equal(body, '') return true } ) @@ -107,49 +126,51 @@ test('reply function receives the request URL and body', async t => { }) test('when content-type is json, reply function receives parsed body', async t => { - t.plan(3) + t.plan(4) const exampleRequestBody = JSON.stringify({ id: 1, name: 'bob' }) const scope = nock('http://example.test') .post('/') - .reply(200, (uri, requestBody) => { + .reply(201, (uri, requestBody) => { t.type(requestBody, 'object') t.deepEqual(requestBody, JSON.parse(exampleRequestBody)) }) - const { statusCode } = await got('http://example.test/', { + const { statusCode, body } = await got('http://example.test/', { headers: { 'Content-Type': 'application/json' }, body: exampleRequestBody, }) - t.is(statusCode, 200) + t.is(statusCode, 201) + t.equal(body, '') scope.done() }) test('without content-type header, body sent to reply function is not parsed', async t => { - t.plan(3) + t.plan(4) const exampleRequestBody = JSON.stringify({ id: 1, name: 'bob' }) const scope = nock('http://example.test') .post('/') - .reply(200, (uri, requestBody) => { + .reply(201, (uri, requestBody) => { t.type(requestBody, 'string') t.equal(requestBody, exampleRequestBody) }) - const { statusCode } = await got.post('http://example.test/', { + const { statusCode, body } = await got.post('http://example.test/', { body: exampleRequestBody, }) - t.is(statusCode, 200) + t.is(statusCode, 201) + t.equal(body, '') scope.done() }) // This signature is supported today, however it seems unnecessary. This is // just as easily accomplished with a function returning an array: -// `.reply(() => [200, 'ABC', { 'X-My-Headers': 'My custom header value' }])` +// `.reply(() => [201, 'ABC', { 'X-My-Headers': 'My custom header value' }])` test('reply with status code, function returning string body, and header object', async t => { const scope = nock('http://example.com') .get('/') - .reply(200, () => 'ABC', { 'X-My-Headers': 'My custom header value' }) + .reply(201, () => 'ABC', { 'X-My-Headers': 'My custom header value' }) const { headers } = await got('http://example.com/') @@ -158,31 +179,26 @@ test('reply with status code, function returning string body, and header object' scope.done() }) -test( - 'reply function returning array with status code', - // Seems likely a bug related to https://github.com/nock/nock/issues/1222. - { skip: true }, - async t => { - const scope = nock('http://example.test') - .get('/') - .reply(() => [202]) +test('reply function returning array with only status code', async t => { + const scope = nock('http://example.test') + .get('/') + .reply(() => [202]) - const { statusCode, body } = await got('http://example.test/') + const { statusCode, body } = await got('http://example.test/') - t.is(statusCode, 202) - t.equal(body, '') - scope.done() - } -) + t.equal(statusCode, 202) + t.equal(body, '') + scope.done() +}) test('reply function returning array with status code and string body', async t => { const scope = nock('http://example.com') .get('/') .reply(() => [401, 'This is a body']) - await assertRejects(got('http://example.com/'), err => { - t.equal(err.statusCode, 401) - t.equal(err.body, 'This is a body') + await assertRejects(got('http://example.com/'), ({ statusCode, body }) => { + t.is(statusCode, 401) + t.equal(body, 'This is a body') return true }) scope.done() @@ -233,3 +249,63 @@ test('reply function returning array with status code, string body, and headers t.deepEqual(rawHeaders, ['x-key', 'value', 'x-key-2', 'value 2']) scope.done() }) + +test('one function not returning an array causes an error', async t => { + nock('http://example.test') + .get('/abc') + .reply(function() { + return 'ABC' + }) + + await assertRejects(got('http://example.test/abc'), err => { + t.match( + err, + Error('A single function provided to .reply MUST return an array') + ) + return true + }) + t.end() +}) + +test('one function returning an empty array causes an error', async t => { + nock('http://example.test') + .get('/abc') + .reply(function() { + return [] + }) + + await assertRejects(got('http://example.test/abc'), err => { + t.match(err, Error('Invalid undefined value for status code')) + return true + }) + t.end() +}) + +test('one function returning too large an array causes an error', async t => { + nock('http://example.test') + .get('/abc') + .reply(function() { + return ['user', 'probably', 'intended', 'this', 'to', 'be', 'JSON'] + }) + + await assertRejects(got('http://example.test/abc'), err => { + t.match( + err, + Error( + 'The array returned from the .reply callback contains too many values' + ) + ) + return true + }) + t.end() +}) + +test('one function throws an error if extraneous args are provided', async t => { + const interceptor = nock('http://example.test').get('/') + t.throws( + () => interceptor.reply(() => [200], { 'x-my-header': 'some-value' }), + Error('Invalid arguments') + ) + + t.end() +}) diff --git a/tests/test_reply_headers.js b/tests/test_reply_headers.js index 00fe99d6a..4568f350c 100644 --- a/tests/test_reply_headers.js +++ b/tests/test_reply_headers.js @@ -65,6 +65,77 @@ test('reply header function is evaluated and the result sent in the mock respons scope.done() }) +// Skipping these two test because of the inconsistencies around raw headers. +// - they often receive the lower-cased versions of the keys +// - the resulting order differs depending if overrides are provided to .reply directly or via a callback +// - replacing values with function results isn't guaranteed to keep the correct order +// - the resulting `headers` object itself is fine and these assertions pass +// https://github.com/nock/nock/issues/1553 +test('reply headers and defaults', { skip: true }, async t => { + const scope = nock('http://example.com') + .defaultReplyHeaders({ + 'X-Powered-By': 'Meeee', + 'X-Another-Header': 'Hey man!', + }) + .get('/') + .reply(200, 'Success!', { + 'X-Custom-Header': 'boo!', + 'x-another-header': 'foobar', + }) + + const { headers, rawHeaders } = await got('http://example.com/') + + t.equivalent(headers, { + 'x-custom-header': 'boo!', + 'x-another-header': 'foobar', // note this overrode the default value, despite the case difference + 'x-powered-by': 'Meeee', + }) + t.equivalent(rawHeaders, [ + 'X-Powered-By', + 'Meeee', + 'X-Another-Header', + 'Hey man!', + 'X-Custom-Header', + 'boo!', + 'x-another-header', + 'foobar', + ]) + scope.done() +}) + +test('reply headers from callback and defaults', { skip: true }, async t => { + const scope = nock('http://example.com') + .defaultReplyHeaders({ + 'X-Powered-By': 'Meeee', + 'X-Another-Header': 'Hey man!', + }) + .get('/') + .reply(() => [ + 200, + 'Success!', + { 'X-Custom-Header': 'boo!', 'x-another-header': 'foobar' }, + ]) + + const { headers, rawHeaders } = await got('http://example.com/') + + t.equivalent(headers, { + 'x-custom-header': 'boo!', + 'x-another-header': 'foobar', + 'x-powered-by': 'Meeee', + }) + t.equivalent(rawHeaders, [ + 'X-Powered-By', + 'Meeee', + 'X-Another-Header', + 'Hey man!', + 'X-Custom-Header', + 'boo!', + 'x-another-header', + 'foobar', + ]) + scope.done() +}) + test('reply header function receives the correct arguments', async t => { t.plan(4) diff --git a/tests/test_stream.js b/tests/test_stream.js index b74eb82be..0a640d4ab 100644 --- a/tests/test_stream.js +++ b/tests/test_stream.js @@ -236,7 +236,7 @@ test( nock('http://localhost') .get('/') - .reply(200, function(path, reqBody) { + .reply(201, function(path, reqBody) { return new SimpleStream() }) @@ -245,6 +245,7 @@ test( res.setEncoding('utf8') let body = '' + t.equal(res.statusCode, 201) res.on('data', function(chunk) { body += chunk