UNPKG

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