1 | 'use strict'
|
2 |
|
3 | const stream = require('stream')
|
4 | const util = require('util')
|
5 | const zlib = require('zlib')
|
6 | const debug = require('debug')('nock.playback_interceptor')
|
7 | const common = require('./common')
|
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 | class ReadableBuffers extends stream.Readable {
|
76 | constructor(buffers, opts = {}) {
|
77 | super(opts)
|
78 |
|
79 | this.buffers = buffers
|
80 | }
|
81 |
|
82 | _read(size) {
|
83 | while (this.buffers.length) {
|
84 | if (!this.push(this.buffers.shift())) {
|
85 | return
|
86 | }
|
87 | }
|
88 | this.push(null)
|
89 | }
|
90 | }
|
91 |
|
92 | function convertBodyToStream(body) {
|
93 | if (common.isStream(body)) {
|
94 | return body
|
95 | }
|
96 |
|
97 | if (body === undefined) {
|
98 | return new ReadableBuffers([])
|
99 | }
|
100 |
|
101 | if (Buffer.isBuffer(body)) {
|
102 | return new ReadableBuffers([body])
|
103 | }
|
104 |
|
105 | if (typeof body !== 'string') {
|
106 | body = JSON.stringify(body)
|
107 | }
|
108 |
|
109 | return new ReadableBuffers([Buffer.from(body)])
|
110 | }
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | function playbackInterceptor({
|
116 | req,
|
117 | socket,
|
118 | options,
|
119 | requestBodyString,
|
120 | requestBodyIsUtf8Representable,
|
121 | response,
|
122 | interceptor,
|
123 | }) {
|
124 | const { logger } = interceptor.scope
|
125 |
|
126 | function start() {
|
127 | req.headers = req.getHeaders()
|
128 |
|
129 | interceptor.scope.emit('request', req, interceptor, requestBodyString)
|
130 |
|
131 | if (typeof interceptor.errorMessage !== 'undefined') {
|
132 | let error
|
133 | if (typeof interceptor.errorMessage === 'object') {
|
134 | error = interceptor.errorMessage
|
135 | } else {
|
136 | error = new Error(interceptor.errorMessage)
|
137 | }
|
138 |
|
139 | const delay = interceptor.delayBodyInMs + interceptor.delayConnectionInMs
|
140 | common.setTimeout(() => req.destroy(error), delay)
|
141 | return
|
142 | }
|
143 |
|
144 |
|
145 |
|
146 | response.statusCode = interceptor.statusCode
|
147 |
|
148 |
|
149 | response.rawHeaders = [...interceptor.rawHeaders]
|
150 | logger('response.rawHeaders:', response.rawHeaders)
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | interceptor.req = req
|
158 |
|
159 | if (interceptor.replyFunction) {
|
160 | const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
|
161 |
|
162 | let fn = interceptor.replyFunction
|
163 | if (fn.length === 3) {
|
164 |
|
165 | fn = util.promisify(fn)
|
166 | }
|
167 |
|
168 |
|
169 |
|
170 | Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
|
171 | .then(continueWithResponseBody)
|
172 | .catch(err => req.destroy(err))
|
173 | return
|
174 | }
|
175 |
|
176 | if (interceptor.fullReplyFunction) {
|
177 | const parsedRequestBody = parseJSONRequestBody(req, requestBodyString)
|
178 |
|
179 | let fn = interceptor.fullReplyFunction
|
180 | if (fn.length === 3) {
|
181 | fn = util.promisify(fn)
|
182 | }
|
183 |
|
184 | Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
|
185 | .then(continueWithFullResponse)
|
186 | .catch(err => req.destroy(err))
|
187 | return
|
188 | }
|
189 |
|
190 | if (
|
191 | common.isContentEncoded(interceptor.headers) &&
|
192 | !common.isStream(interceptor.body)
|
193 | ) {
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | const bufferData = Array.isArray(interceptor.body)
|
199 | ? interceptor.body
|
200 | : [interceptor.body]
|
201 | const responseBuffers = bufferData.map(data => Buffer.from(data, 'hex'))
|
202 | const responseBody = new ReadableBuffers(responseBuffers)
|
203 | continueWithResponseBody(responseBody)
|
204 | return
|
205 | }
|
206 |
|
207 |
|
208 |
|
209 | let responseBody = interceptor.body
|
210 |
|
211 |
|
212 |
|
213 |
|
214 | if (!requestBodyIsUtf8Representable && typeof responseBody === 'string') {
|
215 |
|
216 | responseBody = Buffer.from(responseBody, 'hex')
|
217 |
|
218 |
|
219 | if (
|
220 | !responseBody ||
|
221 | (interceptor.body.length > 0 && responseBody.length === 0)
|
222 | ) {
|
223 |
|
224 | responseBody = Buffer.from(interceptor.body, 'utf8')
|
225 | }
|
226 | }
|
227 |
|
228 | return continueWithResponseBody(responseBody)
|
229 | }
|
230 |
|
231 | function continueWithFullResponse(fullReplyResult) {
|
232 | let responseBody
|
233 | try {
|
234 | responseBody = parseFullReplyResult(response, fullReplyResult)
|
235 | } catch (err) {
|
236 | req.destroy(err)
|
237 | return
|
238 | }
|
239 |
|
240 | continueWithResponseBody(responseBody)
|
241 | }
|
242 |
|
243 | function prepareResponseHeaders(body) {
|
244 | const defaultHeaders = [...interceptor.scope._defaultReplyHeaders]
|
245 |
|
246 |
|
247 |
|
248 |
|
249 | const isJSON =
|
250 | body !== undefined &&
|
251 | typeof body !== 'string' &&
|
252 | !Buffer.isBuffer(body) &&
|
253 | !common.isStream(body)
|
254 |
|
255 | if (isJSON) {
|
256 | defaultHeaders.push('Content-Type', 'application/json')
|
257 | }
|
258 |
|
259 | response.rawHeaders.push(
|
260 | ...selectDefaultHeaders(response.rawHeaders, defaultHeaders)
|
261 | )
|
262 |
|
263 |
|
264 | common.forEachHeader(response.rawHeaders, (value, fieldName, i) => {
|
265 | if (typeof value === 'function') {
|
266 | response.rawHeaders[i + 1] = value(req, response, body)
|
267 | }
|
268 | })
|
269 |
|
270 | response.headers = common.headersArrayToObject(response.rawHeaders)
|
271 | }
|
272 |
|
273 | function continueWithResponseBody(rawBody) {
|
274 | prepareResponseHeaders(rawBody)
|
275 | const bodyAsStream = convertBodyToStream(rawBody)
|
276 | bodyAsStream.pause()
|
277 |
|
278 |
|
279 | bodyAsStream.on('data', function (chunk) {
|
280 | response.push(chunk)
|
281 | })
|
282 | bodyAsStream.on('end', function () {
|
283 |
|
284 | response.complete = true
|
285 | response.push(null)
|
286 |
|
287 | interceptor.scope.emit('replied', req, interceptor)
|
288 | })
|
289 | bodyAsStream.on('error', function (err) {
|
290 | response.emit('error', err)
|
291 | })
|
292 |
|
293 | const { delayBodyInMs, delayConnectionInMs } = interceptor
|
294 |
|
295 | function respond() {
|
296 | if (req.aborted) {
|
297 | return
|
298 | }
|
299 |
|
300 |
|
301 |
|
302 |
|
303 | req.res = response
|
304 | response.req = req
|
305 |
|
306 | logger('emitting response')
|
307 | req.emit('response', response)
|
308 |
|
309 | common.setTimeout(() => bodyAsStream.resume(), delayBodyInMs)
|
310 | }
|
311 |
|
312 | socket.applyDelay(delayConnectionInMs)
|
313 | common.setTimeout(respond, delayConnectionInMs)
|
314 | }
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 | common.setImmediate(() => {
|
321 | if (!req.aborted) {
|
322 | start()
|
323 | }
|
324 | })
|
325 | }
|
326 |
|
327 | module.exports = { playbackInterceptor }
|