UNPKG

10.6 kBJavaScriptView Raw
1'use strict'
2
3const util = require('util')
4const zlib = require('zlib')
5const debug = require('debug')('nock.playback_interceptor')
6const common = require('./common')
7const DelayedBody = require('./delayed_body')
8
9function parseJSONRequestBody(req, requestBody) {
10 if (!requestBody || !common.isJSONContent(req.headers)) {
11 return requestBody
12 }
13
14 if (common.contentEncoding(req.headers, 'gzip')) {
15 requestBody = String(zlib.gunzipSync(Buffer.from(requestBody, 'hex')))
16 } else if (common.contentEncoding(req.headers, 'deflate')) {
17 requestBody = String(zlib.inflateSync(Buffer.from(requestBody, 'hex')))
18 }
19
20 return JSON.parse(requestBody)
21}
22
23function parseFullReplyResult(response, fullReplyResult) {
24 debug('full response from callback result: %j', fullReplyResult)
25
26 if (!Array.isArray(fullReplyResult)) {
27 throw Error('A single function provided to .reply MUST return an array')
28 }
29
30 if (fullReplyResult.length > 3) {
31 throw Error(
32 'The array returned from the .reply callback contains too many values'
33 )
34 }
35
36 const [status, body = '', headers] = fullReplyResult
37
38 if (!Number.isInteger(status)) {
39 throw new Error(`Invalid ${typeof status} value for status code`)
40 }
41
42 response.statusCode = status
43 response.rawHeaders.push(...common.headersInputToRawArray(headers))
44 debug('response.rawHeaders after reply: %j', response.rawHeaders)
45
46 return body
47}
48
49/**
50 * Determine which of the default headers should be added to the response.
51 *
52 * Don't include any defaults whose case-insensitive keys are already on the response.
53 */
54function selectDefaultHeaders(existingHeaders, defaultHeaders) {
55 if (!defaultHeaders.length) {
56 return [] // return early if we don't need to bother
57 }
58
59 const definedHeaders = new Set()
60 const result = []
61
62 common.forEachHeader(existingHeaders, (_, fieldName) => {
63 definedHeaders.add(fieldName.toLowerCase())
64 })
65 common.forEachHeader(defaultHeaders, (value, fieldName) => {
66 if (!definedHeaders.has(fieldName.toLowerCase())) {
67 result.push(fieldName, value)
68 }
69 })
70
71 return result
72}
73
74/**
75 * Play back an intercepto using the given request and mock response.
76 */
77function playbackInterceptor({
78 req,
79 socket,
80 options,
81 requestBodyString,
82 requestBodyIsUtf8Representable,
83 response,
84 interceptor,
85}) {
86 function emitError(error) {
87 process.nextTick(() => {
88 req.emit('error', error)
89 })
90 }
91
92 function start() {
93 interceptor.req = req
94 req.headers = req.getHeaders()
95
96 interceptor.scope.emit('request', req, interceptor, requestBodyString)
97
98 if (typeof interceptor.errorMessage !== 'undefined') {
99 interceptor.markConsumed()
100
101 let error
102 if (typeof interceptor.errorMessage === 'object') {
103 error = interceptor.errorMessage
104 } else {
105 error = new Error(interceptor.errorMessage)
106 }
107 common.setTimeout(() => emitError(error), interceptor.getTotalDelay())
108 return
109 }
110
111 // This will be null if we have a fullReplyFunction,
112 // in that case status code will be set in `parseFullReplyResult`
113 response.statusCode = interceptor.statusCode
114
115 // Clone headers/rawHeaders to not override them when evaluating later
116 response.rawHeaders = [...interceptor.rawHeaders]
117 debug('response.rawHeaders:', response.rawHeaders)
118
119 if (interceptor.replyFunction) {
120 const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
121
122 let fn = interceptor.replyFunction
123 if (fn.length === 3) {
124 // Handle the case of an async reply function, the third parameter being the callback.
125 fn = util.promisify(fn)
126 }
127
128 // At this point `fn` is either a synchronous function or a promise-returning function;
129 // wrapping in `Promise.resolve` makes it into a promise either way.
130 Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
131 .then(responseBody => continueWithResponseBody({ responseBody }))
132 .catch(err => emitError(err))
133 return
134 }
135
136 if (interceptor.fullReplyFunction) {
137 const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
138
139 let fn = interceptor.fullReplyFunction
140 if (fn.length === 3) {
141 fn = util.promisify(fn)
142 }
143
144 Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
145 .then(fullReplyResult => continueWithFullResponse({ fullReplyResult }))
146 .catch(err => emitError(err))
147 return
148 }
149
150 if (
151 common.isContentEncoded(interceptor.headers) &&
152 !common.isStream(interceptor.body)
153 ) {
154 // If the content is encoded we know that the response body *must* be an array
155 // of response buffers which should be mocked one by one.
156 // (otherwise decompressions after the first one fails as unzip expects to receive
157 // buffer by buffer and not one single merged buffer)
158
159 if (interceptor.delayInMs) {
160 emitError(
161 new Error(
162 'Response delay of the body is currently not supported with content-encoded responses.'
163 )
164 )
165 return
166 }
167
168 const bufferData = Array.isArray(interceptor.body)
169 ? interceptor.body
170 : [interceptor.body]
171 const responseBuffers = bufferData.map(data => Buffer.from(data, 'hex'))
172 continueWithResponseBody({ responseBuffers })
173 return
174 }
175
176 // If we get to this point, the body is either a string or an object that
177 // will eventually be JSON stringified.
178 let responseBody = interceptor.body
179
180 // If the request was not UTF8-representable then we assume that the
181 // response won't be either. In that case we send the response as a Buffer
182 // object as that's what the client will expect.
183 if (!requestBodyIsUtf8Representable && typeof responseBody === 'string') {
184 // Try to create the buffer from the interceptor's body response as hex.
185 responseBody = Buffer.from(responseBody, 'hex')
186
187 // Creating buffers does not necessarily throw errors; check for difference in size.
188 if (
189 !responseBody ||
190 (interceptor.body.length > 0 && responseBody.length === 0)
191 ) {
192 // We fallback on constructing buffer from utf8 representation of the body.
193 responseBody = Buffer.from(interceptor.body, 'utf8')
194 }
195 }
196
197 return continueWithResponseBody({ responseBody })
198 }
199
200 function continueWithFullResponse({ fullReplyResult }) {
201 let responseBody
202 try {
203 responseBody = parseFullReplyResult(response, fullReplyResult)
204 } catch (innerErr) {
205 emitError(innerErr)
206 return
207 }
208
209 continueWithResponseBody({ responseBody })
210 }
211
212 function continueWithResponseBody({ responseBuffers, responseBody }) {
213 // Transform the response body if it exists (it may not exist
214 // if we have `responseBuffers` instead)
215 if (responseBody !== undefined) {
216 debug('transform the response body')
217
218 if (interceptor.delayInMs) {
219 debug(
220 'delaying the response for',
221 interceptor.delayInMs,
222 'milliseconds'
223 )
224 // Because setTimeout is called immediately in DelayedBody(), so we
225 // need count in the delayConnectionInMs.
226 responseBody = new DelayedBody(
227 interceptor.getTotalDelay(),
228 responseBody
229 )
230 }
231
232 if (common.isStream(responseBody)) {
233 debug('response body is a stream')
234 responseBody.pause()
235 responseBody.on('data', function(d) {
236 response.push(d)
237 })
238 responseBody.on('end', function() {
239 response.push(null)
240 // https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
241 response.complete = true
242 })
243 responseBody.on('error', function(err) {
244 response.emit('error', err)
245 })
246 } else if (!Buffer.isBuffer(responseBody)) {
247 if (typeof responseBody === 'string') {
248 responseBody = Buffer.from(responseBody)
249 } else {
250 responseBody = JSON.stringify(responseBody)
251 response.rawHeaders.push('Content-Type', 'application/json')
252 }
253 }
254 // Why are strings converted to a Buffer, but JSON data is left as a string?
255 // Related to https://github.com/nock/nock/issues/1542 ?
256 }
257
258 interceptor.markConsumed()
259
260 if (req.aborted) {
261 return
262 }
263
264 response.rawHeaders.push(
265 ...selectDefaultHeaders(
266 response.rawHeaders,
267 interceptor.scope._defaultReplyHeaders
268 )
269 )
270
271 // Evaluate functional headers.
272 common.forEachHeader(response.rawHeaders, (value, fieldName, i) => {
273 if (typeof value === 'function') {
274 response.rawHeaders[i + 1] = value(req, response, responseBody)
275 }
276 })
277
278 response.headers = common.headersArrayToObject(response.rawHeaders)
279
280 process.nextTick(() =>
281 respondUsingInterceptor({
282 responseBody,
283 responseBuffers,
284 })
285 )
286 }
287
288 function respondUsingInterceptor({ responseBody, responseBuffers }) {
289 if (req.aborted) {
290 return
291 }
292
293 function respond() {
294 if (req.aborted) {
295 return
296 }
297
298 debug('emitting response')
299 req.emit('response', response)
300
301 if (common.isStream(responseBody)) {
302 debug('resuming response stream')
303 responseBody.resume()
304 } else {
305 responseBuffers = responseBuffers || []
306 if (typeof responseBody !== 'undefined') {
307 debug('adding body to buffer list')
308 responseBuffers.push(responseBody)
309 }
310
311 // Stream the response chunks one at a time.
312 common.setImmediate(function emitChunk() {
313 const chunk = responseBuffers.shift()
314
315 if (chunk) {
316 debug('emitting response chunk')
317 response.push(chunk)
318 common.setImmediate(emitChunk)
319 } else {
320 debug('ending response stream')
321 response.push(null)
322 // https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_message_complete
323 response.complete = true
324 interceptor.scope.emit('replied', req, interceptor)
325 }
326 })
327 }
328 }
329
330 if (interceptor.socketDelayInMs && interceptor.socketDelayInMs > 0) {
331 socket.applyDelay(interceptor.socketDelayInMs)
332 }
333
334 if (
335 interceptor.delayConnectionInMs &&
336 interceptor.delayConnectionInMs > 0
337 ) {
338 socket.applyDelay(interceptor.delayConnectionInMs)
339 common.setTimeout(respond, interceptor.delayConnectionInMs)
340 } else {
341 respond()
342 }
343 }
344
345 start()
346}
347
348module.exports = { playbackInterceptor }