1 | 'use strict'
|
2 |
|
3 | const util = require('util')
|
4 | const zlib = require('zlib')
|
5 | const debug = require('debug')('nock.playback_interceptor')
|
6 | const common = require('./common')
|
7 | const DelayedBody = require('./delayed_body')
|
8 |
|
9 | function 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 |
|
23 | function 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 |
|
51 |
|
52 |
|
53 |
|
54 | function selectDefaultHeaders(existingHeaders, defaultHeaders) {
|
55 | if (!defaultHeaders.length) {
|
56 | return []
|
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 |
|
76 |
|
77 | function 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 |
|
112 |
|
113 | response.statusCode = interceptor.statusCode
|
114 |
|
115 |
|
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 |
|
125 | fn = util.promisify(fn)
|
126 | }
|
127 |
|
128 |
|
129 |
|
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 |
|
155 |
|
156 |
|
157 |
|
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 |
|
177 |
|
178 | let responseBody = interceptor.body
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | if (!requestBodyIsUtf8Representable && typeof responseBody === 'string') {
|
184 |
|
185 | responseBody = Buffer.from(responseBody, 'hex')
|
186 |
|
187 |
|
188 | if (
|
189 | !responseBody ||
|
190 | (interceptor.body.length > 0 && responseBody.length === 0)
|
191 | ) {
|
192 |
|
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 |
|
214 |
|
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 |
|
225 |
|
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 |
|
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 |
|
255 |
|
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 |
|
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 |
|
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 |
|
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 |
|
348 | module.exports = { playbackInterceptor }
|