UNPKG

7.01 kBJavaScriptView Raw
1'use strict'
2
3const assert = require('assert')
4const _ = require('lodash')
5const recorder = require('./recorder')
6const {
7 activate,
8 disableNetConnect,
9 enableNetConnect,
10 removeAll: cleanAll,
11} = require('./intercept')
12const { loadDefs, define } = require('./scope')
13
14const { format } = require('util')
15const path = require('path')
16const debug = require('debug')('nock.back')
17
18let _mode = null
19
20let fs
21
22try {
23 fs = require('fs')
24} catch (err) {
25 // do nothing, probably in browser
26}
27
28let mkdirp
29try {
30 mkdirp = require('mkdirp')
31} catch (err) {
32 // do nothing, probably in browser
33}
34
35/**
36 * nock the current function with the fixture given
37 *
38 * @param {string} fixtureName - the name of the fixture, e.x. 'foo.json'
39 * @param {object} options - [optional] extra options for nock with, e.x. `{ assert: true }`
40 * @param {function} nockedFn - [optional] callback function to be executed with the given fixture being loaded;
41 * if defined the function will be called with context `{ scopes: loaded_nocks || [] }`
42 * set as `this` and `nockDone` callback function as first and only parameter;
43 * if not defined a promise resolving to `{nockDone, context}` where `context` is
44 * aforementioned `{ scopes: loaded_nocks || [] }`
45 *
46 * List of options:
47 *
48 * @param {function} before - a preprocessing function, gets called before nock.define
49 * @param {function} after - a postprocessing function, gets called after nock.define
50 * @param {function} afterRecord - a postprocessing function, gets called after recording. Is passed the array
51 * of scopes recorded and should return the array scopes to save to the fixture
52 * @param {function} recorder - custom options to pass to the recorder
53 *
54 */
55function Back(fixtureName, options, nockedFn) {
56 if (!Back.fixtures) {
57 throw new Error(
58 'Back requires nock.back.fixtures to be set\n' +
59 'Ex:\n' +
60 "\trequire(nock).back.fixtures = '/path/to/fixtures/'"
61 )
62 }
63
64 // TODO-12.x: Replace with `typeof fixtureName === 'string'`.
65 if (!_.isString(fixtureName)) {
66 throw new Error('Parameter fixtureName must be a string')
67 }
68
69 if (arguments.length === 1) {
70 options = {}
71 } else if (arguments.length === 2) {
72 // If 2nd parameter is a function then `options` has been omitted
73 // otherwise `options` haven't been omitted but `nockedFn` was.
74 if (typeof options === 'function') {
75 nockedFn = options
76 options = {}
77 }
78 }
79
80 _mode.setup()
81
82 const fixture = path.join(Back.fixtures, fixtureName)
83 const context = _mode.start(fixture, options)
84
85 const nockDone = function() {
86 _mode.finish(fixture, options, context)
87 }
88
89 debug('context:', context)
90
91 // If nockedFn is a function then invoke it, otherwise return a promise resolving to nockDone.
92 if (typeof nockedFn === 'function') {
93 nockedFn.call(context, nockDone)
94 } else {
95 return Promise.resolve({ nockDone, context })
96 }
97}
98
99/*******************************************************************************
100 * Modes *
101 *******************************************************************************/
102
103const wild = {
104 setup: function() {
105 cleanAll()
106 recorder.restore()
107 activate()
108 enableNetConnect()
109 },
110
111 start: function() {
112 return load() // don't load anything but get correct context
113 },
114
115 finish: function() {
116 // nothing to do
117 },
118}
119
120const dryrun = {
121 setup: function() {
122 recorder.restore()
123 cleanAll()
124 activate()
125 // We have to explicitly enable net connectivity as by default it's off.
126 enableNetConnect()
127 },
128
129 start: function(fixture, options) {
130 const contexts = load(fixture, options)
131
132 enableNetConnect()
133 return contexts
134 },
135
136 finish: function() {
137 // nothing to do
138 },
139}
140
141const record = {
142 setup: function() {
143 recorder.restore()
144 recorder.clear()
145 cleanAll()
146 activate()
147 disableNetConnect()
148 },
149
150 start: function(fixture, options) {
151 if (!fs) {
152 throw new Error('no fs')
153 }
154 const context = load(fixture, options)
155
156 if (!context.isLoaded) {
157 recorder.record({
158 dont_print: true,
159 output_objects: true,
160 ...options.recorder,
161 })
162
163 context.isRecording = true
164 }
165
166 return context
167 },
168
169 finish: function(fixture, options, context) {
170 if (context.isRecording) {
171 let outputs = recorder.outputs()
172
173 if (typeof options.afterRecord === 'function') {
174 outputs = options.afterRecord(outputs)
175 }
176
177 outputs =
178 typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4)
179 debug('recorder outputs:', outputs)
180
181 mkdirp.sync(path.dirname(fixture))
182 fs.writeFileSync(fixture, outputs)
183 }
184 },
185}
186
187const lockdown = {
188 setup: function() {
189 recorder.restore()
190 recorder.clear()
191 cleanAll()
192 activate()
193 disableNetConnect()
194 },
195
196 start: function(fixture, options) {
197 return load(fixture, options)
198 },
199
200 finish: function() {
201 // nothing to do
202 },
203}
204
205function load(fixture, options) {
206 const context = {
207 scopes: [],
208 assertScopesFinished: function() {
209 assertScopes(this.scopes, fixture)
210 },
211 }
212
213 if (fixture && fixtureExists(fixture)) {
214 let scopes = loadDefs(fixture)
215 applyHook(scopes, options.before)
216
217 scopes = define(scopes)
218 applyHook(scopes, options.after)
219
220 context.scopes = scopes
221 context.isLoaded = true
222 }
223
224 return context
225}
226
227function applyHook(scopes, fn) {
228 if (!fn) {
229 return
230 }
231
232 if (typeof fn !== 'function') {
233 throw new Error('processing hooks must be a function')
234 }
235
236 scopes.forEach(fn)
237}
238
239function fixtureExists(fixture) {
240 if (!fs) {
241 throw new Error('no fs')
242 }
243
244 return fs.existsSync(fixture)
245}
246
247function assertScopes(scopes, fixture) {
248 const pending = scopes
249 .filter(scope => !scope.isDone())
250 .map(scope => scope.pendingMocks())
251
252 if (pending.length) {
253 assert.fail(
254 format(
255 '%j was not used, consider removing %s to rerecord fixture',
256 [].concat(...pending),
257 fixture
258 )
259 )
260 }
261}
262
263const Modes = {
264 wild, // all requests go out to the internet, dont replay anything, doesnt record anything
265
266 dryrun, // use recorded nocks, allow http calls, doesnt record anything, useful for writing new tests (default)
267
268 record, // use recorded nocks, record new nocks
269
270 lockdown, // use recorded nocks, disables all http calls even when not nocked, doesnt record
271}
272
273Back.setMode = function(mode) {
274 if (!(mode in Modes)) {
275 throw new Error(`Unknown mode: ${mode}`)
276 }
277
278 Back.currentMode = mode
279 debug('New nock back mode:', Back.currentMode)
280
281 _mode = Modes[mode]
282 _mode.setup()
283}
284
285Back.fixtures = null
286Back.currentMode = null
287Back.setMode(process.env.NOCK_BACK_MODE || 'dryrun')
288
289module.exports = Back