Comparing version 1.0.1 to 1.0.3
'use strict'; | ||
var _entries = require('babel-runtime/core-js/object/entries'); | ||
var _entries2 = _interopRequireDefault(_entries); | ||
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
@@ -25,2 +31,4 @@ | ||
var _url2 = _interopRequireDefault(_url); | ||
var _fs = require('fs'); | ||
@@ -34,8 +42,13 @@ | ||
var PROD_ROOT_URL = void 0; | ||
var FIXTURES_PATH = void 0; | ||
var QUERY_STRING_IGNORE = void 0; | ||
var QUIET_MODE = void 0; | ||
var SERVERS = []; | ||
var REQUIRED_CONFIG_OPTIONS = ['prodRootURL', 'fixturesPath']; | ||
var DEFAULT_OPTIONS = { | ||
encoding: 'utf8', | ||
latency: 0, | ||
ports: [4567], | ||
queryStringIgnore: [], | ||
quiet: false, | ||
saveFixtures: true | ||
}; | ||
var JSON_CONTENT_TYPE_REGEXP = /javascript|json/; | ||
@@ -50,39 +63,28 @@ module.exports = { | ||
var app = (0, _express2.default)(); | ||
var defaults = { | ||
ports: [4567], | ||
encoding: 'utf8', | ||
queryStringIgnore: [], | ||
quiet: false | ||
}; | ||
var modOptions = _extends({}, defaults, options); | ||
var prodRootURL = modOptions.prodRootURL; | ||
var corsWhitelist = modOptions.corsWhitelist; | ||
var fixturesPath = modOptions.fixturesPath; | ||
var overrides = modOptions.overrides; | ||
var queryStringIgnore = modOptions.queryStringIgnore; | ||
var ports = modOptions.ports; | ||
var encoding = modOptions.encoding; | ||
var quiet = modOptions.quiet; | ||
var settings = _extends({}, DEFAULT_OPTIONS, options); | ||
var corsWhitelist = settings.corsWhitelist; | ||
var encoding = settings.encoding; | ||
var latency = settings.latency; | ||
var overrides = settings.overrides; | ||
var ports = settings.ports; | ||
PROD_ROOT_URL = prodRootURL; | ||
FIXTURES_PATH = fixturesPath; | ||
QUERY_STRING_IGNORE = queryStringIgnore; | ||
QUIET_MODE = quiet; | ||
if (corsWhitelist) { | ||
setCorsMiddleware(app, corsWhitelist); | ||
} | ||
if (isValidDuration(latency)) { | ||
simulateLatency(app, latency); | ||
} | ||
if (overrides) { | ||
delegateRouteOverrides(app, overrides, encoding); | ||
delegateRouteOverrides(app, settings); | ||
} | ||
app.get('*', function (req, res) { | ||
app.all('*', function (req, res) { | ||
var path = getURLPathWithQueryString(req); | ||
var fileName = getFileName(path); | ||
var fileName = getFileName(path, settings); | ||
_fs2.default.readFile(fileName, encoding, function (err, data) { | ||
if (err) { | ||
recordFromProd(req, res); | ||
fetchResponse(req, res, _extends({}, settings, { fileName: fileName, path: path })); | ||
} else { | ||
serveLocalResponse(res, fileName, data, { quiet: QUIET_MODE }); | ||
serveResponse(res, _extends({}, settings, { fileName: fileName, data: data })); | ||
} | ||
@@ -96,2 +98,3 @@ }); | ||
}; | ||
return startListening(app, ports, function (err) { | ||
@@ -126,2 +129,6 @@ return callback(err, result); | ||
function isValidDuration(latency) { | ||
return Number.isFinite(latency) && latency > 0; | ||
} | ||
function generateMissingParamsError(options, callback) { | ||
@@ -132,7 +139,27 @@ if (typeof callback !== 'function') { | ||
for (var i = 0; i < REQUIRED_CONFIG_OPTIONS.length; i++) { | ||
var key = REQUIRED_CONFIG_OPTIONS[i]; | ||
if (typeof options[key] !== 'string') { | ||
return new Error('Missing definition of ' + key + ' in config file'); | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
try { | ||
for (var _iterator = REQUIRED_CONFIG_OPTIONS[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var key = _step.value; | ||
if (typeof options[key] !== 'string') { | ||
return new Error('Missing definition of ' + key + ' in config file'); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
@@ -156,2 +183,10 @@ | ||
function simulateLatency(app, latency) { | ||
var latencyMiddleware = function latencyMiddleware(_req, _res, next) { | ||
return global.setTimeout(next, latency); | ||
}; | ||
app.use(latencyMiddleware); | ||
} | ||
function startListening(app, ports, callback) { | ||
@@ -187,9 +222,11 @@ var tasks = ports.map(function (port) { | ||
function delegateRouteOverrides(app, overrides, encoding) { | ||
function delegateRouteOverrides(app, options) { | ||
var overrides = options.overrides; | ||
var encoding = options.encoding; | ||
var quiet = options.quiet; | ||
var methods = ['get', 'post', 'put', 'delete', 'all']; | ||
var defaults = { | ||
status: 200, | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
headers: { 'Content-Type': 'application/json' } | ||
}; | ||
@@ -210,4 +247,6 @@ var jsonMiddleware = [_bodyParser2.default.json(), _bodyParser2.default.urlencoded({ extended: true })]; | ||
var mergeParams = routeParams.mergeParams; | ||
var _routeParams$withQuer = routeParams.withQueryParams; | ||
var queryParams = _routeParams$withQuer === undefined ? {} : _routeParams$withQuer; | ||
var responseIsJson = /(javascript|json)/.test(headers['Content-Type']); | ||
var responseIsJson = JSON_CONTENT_TYPE_REGEXP.test(headers['Content-Type']); | ||
@@ -219,3 +258,3 @@ if (!route) { | ||
if (!response) { | ||
var fileName = getFileName(route); | ||
var fileName = getFileName(route, options); | ||
_fs2.default.readFile(fileName, encoding, function (err, data) { | ||
@@ -234,6 +273,36 @@ if (err) { | ||
app[method].call(app, route, jsonMiddleware, function (req, res) { | ||
if (!QUIET_MODE) { | ||
app[method].call(app, route, jsonMiddleware, function (req, res, next) { | ||
if (!quiet) { | ||
console.info('==> 📁 Serving local fixture for ' + method.toUpperCase() + ' -> \'' + route + '\''); | ||
} | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
try { | ||
for (var _iterator2 = (0, _entries2.default)(queryParams)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var _step2$value = _slicedToArray(_step2.value, 2); | ||
var param = _step2$value[0]; | ||
var value = _step2$value[1]; | ||
if (req.query[param] !== value) { | ||
return next(); | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
} | ||
var payload = responseIsJson && typeof mergeParams === 'function' ? mergeParams(JSON.parse(fixture), req.body) : fixture; | ||
@@ -246,10 +315,19 @@ res.status(status).set(headers).send(payload); | ||
function recordFromProd(req, res) { | ||
function fetchResponse(req, res, options) { | ||
if (req.method !== 'GET') { | ||
console.error('==> ⛔️ Couldn\'t complete fetch with non-GET method'); | ||
return res.status(500).end(); | ||
} | ||
var responseIsJson = void 0; | ||
var path = getURLPathWithQueryString(req); | ||
var prodURL = getProdURL(path); | ||
var prodRootURL = options.prodRootURL; | ||
var saveFixtures = options.saveFixtures; | ||
var path = options.path; | ||
var fileName = options.fileName; | ||
var prodURL = prodRootURL + path; | ||
var responseIsJsonp = prodURL.match(/callback\=([^\&]+)/); | ||
console.info('==> 📡 GET ' + PROD_ROOT_URL + ' -> ' + path); | ||
(0, _nodeFetch2.default)(prodURL).then(function (response) { | ||
console.info('==> 📡 GET ' + prodRootURL + ' -> ' + path); | ||
(0, _nodeFetch2.default)(prodRootURL + path).then(function (response) { | ||
if (response.ok) { | ||
@@ -271,12 +349,6 @@ console.info('==> 📡 STATUS ' + response.status); | ||
}).then(function (response) { | ||
var fileName = getFileName(path); | ||
var data = responseIsJson ? JSON.stringify(response) : response; | ||
_fs2.default.writeFile(fileName, data, function (err) { | ||
if (err) { | ||
throw Error('Couldn\'t write response locally, received fs error: \'' + err + '\''); | ||
} | ||
console.info('==> 💾 Saved response to ' + fileName); | ||
}); | ||
serveLocalResponse(res, fileName, data, { quiet: true }); | ||
if (saveFixtures) { | ||
saveFixture(fileName, response, responseIsJson); | ||
} | ||
serveResponse(res, _extends({}, options, { newResponse: true })); | ||
}).catch(function (err) { | ||
@@ -288,7 +360,9 @@ console.error('==> ⛔️ ' + err); | ||
function serveLocalResponse(res, fileName, data) { | ||
var options = arguments.length <= 3 || arguments[3] === undefined ? { quiet: false } : arguments[3]; | ||
function serveResponse(res, options) { | ||
var data = options.data; | ||
var fileName = options.fileName; | ||
var quiet = options.quiet; | ||
var newResponse = options.newResponse; | ||
if (quiet !== true) { | ||
if (!quiet && !newResponse) { | ||
console.info('==> 📁 Serving local response from ' + fileName); | ||
@@ -307,15 +381,26 @@ } | ||
function getFileName(path) { | ||
var fileNameInDirectory = QUERY_STRING_IGNORE.reduce(function (fileName, regex) { | ||
function saveFixture(fileName, response, responseIsJson) { | ||
var data = responseIsJson ? JSON.stringify(response) : response; | ||
_fs2.default.writeFile(fileName, data, function (err) { | ||
if (err) { | ||
throw Error('Couldn\'t write response locally, received fs error: \'' + err + '\''); | ||
} | ||
console.info('==> 💾 Saved response to ' + fileName); | ||
}); | ||
} | ||
function getFileName(path, options) { | ||
var queryStringIgnore = options.queryStringIgnore; | ||
var fixturesPath = options.fixturesPath; | ||
var fileNameInDirectory = queryStringIgnore.reduce(function (fileName, regex) { | ||
return fileName.replace(regex, ''); | ||
}, path).replace(/\//, '').replace(/\//g, ':'); | ||
return FIXTURES_PATH + '/' + fileNameInDirectory + '.json'; | ||
return fixturesPath + '/' + fileNameInDirectory + '.json'; | ||
} | ||
function getProdURL(path) { | ||
return PROD_ROOT_URL + path; | ||
} | ||
function getURLPathWithQueryString(req) { | ||
var queryString = _url2.default.parse(req.url).query; | ||
function getURLPathWithQueryString(req) { | ||
var queryString = (0, _url.parse)(req.url).query; | ||
if (queryString && queryString.length > 0) { | ||
@@ -322,0 +407,0 @@ return req.path + '?' + queryString; |
{ | ||
"name": "highwind", | ||
"version": "1.0.1", | ||
"version": "1.0.3", | ||
"description": "Mock API express server", | ||
@@ -25,5 +25,7 @@ "main": "lib/mock_api.js", | ||
"babel-eslint": "^4.1.6", | ||
"babel-plugin-transform-object-assign": "^6.3.13", | ||
"babel-plugin-transform-object-entries": "^1.0.0", | ||
"babel-plugin-transform-object-rest-spread": "^6.8.0", | ||
"babel-preset-es2015": "^6.3.13", | ||
"chai": "^3.4.1", | ||
"eslint": "^1.10.3", | ||
"mocha": "^2.3.4", | ||
@@ -39,3 +41,2 @@ "nock": "^5.2.1", | ||
"cors": "^2.7.1", | ||
"eslint": "^1.10.3", | ||
"express": "^4.13.3", | ||
@@ -42,0 +43,0 @@ "node-fetch": "^1.3.3", |
@@ -65,3 +65,3 @@ Highwind | ||
* `overrides` *(object)*: | ||
* HTTP methods for which specific routes should be overridden. Each property of this object should have a key specifying an HTTP method known to Express's Application object (`get`, `post`, `put`, `delete`, `all`). Examples follow below. | ||
* HTTP methods for which specific routes should be overridden. Each property of this object should have a key specifying an HTTP method known to Express's `app` object (`get`, `post`, `put`, `delete`, `all`). Examples follow below. | ||
* `queryStringIgnore` *(array of RegExp)*: | ||
@@ -80,2 +80,8 @@ * **Default:** `[]` | ||
* Silences console output when an API response is being served locally. One possible use case is feature tests, in which you'll (ideally) be serving everything locally, to minimize spec pollution. | ||
* `saveFixtures`: *(boolean)* | ||
* **Default:** `true`. | ||
* Toggles persisting responses from the production API as local fixtures. | ||
* `latency`: *(number)* | ||
* **Default:** 0 | ||
* Number of milliseconds to delay responses in order to simulate latency. | ||
@@ -138,2 +144,20 @@ ## HTTP Route Overrides | ||
### Serving a JSON response with dynamic query params | ||
```js | ||
overrides: { | ||
get: [ | ||
{ | ||
route: '/api/search', | ||
withQueryParams: { query: 'foo' }, | ||
response: { | ||
keyword: 'foo', | ||
result_count: 15 | ||
}, | ||
status: 200 | ||
} | ||
] | ||
} | ||
``` | ||
This serves the specified response _only_ when the query string matches the params specified in the `withQueryParams` object; in all other cases, it defers to the default response. | ||
## Serving a JSONP response | ||
@@ -144,2 +168,2 @@ | ||
## License | ||
[MIT License](http://mit-license.org/) © Refinery29, Inc. 2016 | ||
[MIT License](http://mit-license.org/) © Refinery29, Inc. 2016-2017 |
@@ -52,7 +52,9 @@ import 'babel-polyfill'; | ||
expect(servers).to.have.length(1); | ||
expect(servers[0].port).to.equal(4567); | ||
expect(servers[0].server).to.be.a('object'); | ||
expect(servers[0].active).to.be.true; | ||
close(result.servers, done); | ||
const [ server ] = servers; | ||
expect(server.port).to.equal(4567); | ||
expect(server.server).to.be.a('object'); | ||
expect(server.active).to.be.true; | ||
close(servers, done); | ||
}); | ||
@@ -71,44 +73,98 @@ }); | ||
beforeEach(function(done) { | ||
nock(PROD_ROOT_URL) | ||
.get(route) | ||
.query(true) | ||
.reply(200, response); | ||
start(DEFAULT_OPTIONS, (err, result) => { | ||
mockAPI = result.app; | ||
done(); | ||
describe('When the request method is GET', function() { | ||
describe('And the saveFixtures setting is set to true', function() { | ||
beforeEach(function(done) { | ||
nock(PROD_ROOT_URL) | ||
.get(route) | ||
.query(true) | ||
.reply(200, response); | ||
start(DEFAULT_OPTIONS, (err, result) => { | ||
mockAPI = result; | ||
done(); | ||
}); | ||
}); | ||
afterEach(function() { | ||
close(mockAPI.servers); | ||
[responsePath, responsePathWithCallback].forEach(path => { | ||
try { | ||
fs.accessSync(path, fs.F_OK); | ||
} catch (e) { | ||
return; | ||
} | ||
fs.unlinkSync(path); | ||
}); | ||
}); | ||
it('persists and responds with a response from the production API', function(done) { | ||
request(mockAPI.app) | ||
.get(route) | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, response, () => fs.access(responsePath, fs.F_OK, done)); | ||
}); | ||
it('truncates ignored query string expressions in the persisted response filename', function(done) { | ||
request(mockAPI.app) | ||
.get(route + IGNORED_QUERY_PARAMS) | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, response, () => fs.access(responsePath, fs.F_OK, done)); | ||
}); | ||
it('renders the endpoint as JSONP when a callback is specified in the query string', function(done) { | ||
request(mockAPI.app) | ||
.get(route + JSONP_CALLBACK) | ||
.expect('Content-Type', /application\/javascript/) | ||
.expect(200, response, () => fs.access(responsePathWithCallback, fs.F_OK, done)); | ||
}); | ||
}); | ||
}); | ||
afterEach(function() { | ||
close(mockAPI.servers); | ||
[responsePath, responsePathWithCallback].forEach(path => { | ||
try { | ||
fs.accessSync(path, fs.F_OK); | ||
} catch (e) { | ||
return; | ||
} | ||
fs.unlinkSync(path); | ||
describe('And the saveFixtures setting is set to false', function() { | ||
beforeEach(function(done) { | ||
nock(PROD_ROOT_URL) | ||
.get(route) | ||
.query(true) | ||
.reply(200, response); | ||
start({ ...DEFAULT_OPTIONS, saveFixtures: false }, (err, result) => { | ||
mockAPI = result; | ||
done(); | ||
}); | ||
}); | ||
afterEach(function() { | ||
close(mockAPI.servers); | ||
}); | ||
it('responses with a response from the production API and does not persist the response', function(done) { | ||
request(mockAPI.app) | ||
.get(route) | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, response, () => fs.access(responsePath, fs.F_OK, (err) => { | ||
if (err) { | ||
done(); | ||
} | ||
})); | ||
}); | ||
}); | ||
}); | ||
it('persists and responds with a response from the production API', function(done) { | ||
request(mockAPI) | ||
.get(route) | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, response, () => fs.access(responsePath, fs.F_OK, done)); | ||
}); | ||
describe('When the request method is not GET', function() { | ||
beforeEach(function(done) { | ||
start(DEFAULT_OPTIONS, (err, result) => { | ||
mockAPI = result; | ||
done(); | ||
}); | ||
}); | ||
it('truncates ignored query string expressions in the persisted response filename', function(done) { | ||
request(mockAPI) | ||
.get(route + IGNORED_QUERY_PARAMS) | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, response, () => fs.access(responsePath, fs.F_OK, done)); | ||
}); | ||
afterEach(function() { | ||
close(mockAPI.servers); | ||
}); | ||
it('renders the endpoint as JSONP when a callback is specified in the query string', function(done) { | ||
request(mockAPI) | ||
.get(route + JSONP_CALLBACK) | ||
.expect('Content-Type', /application\/javascript/) | ||
.expect(200, response, () => fs.access(responsePathWithCallback, fs.F_OK, done)); | ||
it('does not call the production API and returns an error', function(done) { | ||
request(mockAPI.app) | ||
.put(route) | ||
.expect(500, '', done); | ||
}); | ||
}); | ||
@@ -130,4 +186,5 @@ }); | ||
.replyWithError('Fake API hit the production API'); | ||
start(DEFAULT_OPTIONS, (err, result) => { | ||
mockAPI = result.app; | ||
mockAPI = result; | ||
done(); | ||
@@ -142,3 +199,3 @@ }); | ||
it('serves the locally persisted response as JSON and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route) | ||
@@ -150,3 +207,3 @@ .expect('Content-Type', /application\/json/) | ||
it('ignores truncated query string expressions when identifying the persisted response filename and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route + IGNORED_QUERY_PARAMS) | ||
@@ -158,3 +215,3 @@ .expect('Content-Type', /application\/json/) | ||
it('renders the endpoint as JSONP when a callback is specified in the query string and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route + JSONP_CALLBACK) | ||
@@ -176,4 +233,5 @@ .expect('Content-Type', /application\/javascript/) | ||
.replyWithError('Fake API hit the production API'); | ||
start(DEFAULT_OPTIONS, (err, result) => { | ||
mockAPI = result.app; | ||
mockAPI = result; | ||
done(); | ||
@@ -188,3 +246,3 @@ }); | ||
it('responds with the locally persisted response as `text/html` and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route) | ||
@@ -202,16 +260,15 @@ .expect('Content-Type', /text\/html/) | ||
const response = 'overridden response'; | ||
const modOptions = Object.assign({}, DEFAULT_OPTIONS, { | ||
const modOptions = { | ||
...DEFAULT_OPTIONS, | ||
overrides: { | ||
get: [ | ||
{ | ||
route: route, | ||
response: response, | ||
route, | ||
response, | ||
status: 503, | ||
headers: { | ||
'Content-Type': 'text/plain' | ||
} | ||
headers: { 'Content-Type': 'text/plain' } | ||
} | ||
] | ||
} | ||
}); | ||
}; | ||
@@ -223,4 +280,5 @@ before(function(done) { | ||
.replyWithError('Fake API hit the production API'); | ||
start(modOptions, (err, result) => { | ||
mockAPI = result.app; | ||
mockAPI = result; | ||
done(); | ||
@@ -235,3 +293,3 @@ }); | ||
it('responds with the specified response, status, and headers and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route) | ||
@@ -243,3 +301,3 @@ .expect('Content-Type', /text\/plain/) | ||
it('responds with the specified headers even if the filename specifies a JSONP callback', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route + JSONP_CALLBACK) | ||
@@ -254,12 +312,13 @@ .expect('Content-Type', /text\/plain/, done); | ||
const response = { status: 'overridden response' }; | ||
const modOptions = Object.assign({}, DEFAULT_OPTIONS, { | ||
const modOptions = { | ||
...DEFAULT_OPTIONS, | ||
overrides: { | ||
get: [ | ||
{ | ||
route: route, | ||
response: response | ||
route, | ||
response | ||
} | ||
] | ||
} | ||
}); | ||
}; | ||
@@ -270,4 +329,5 @@ before(function(done) { | ||
.replyWithError('Fake API hit the production API'); | ||
start(modOptions, (err, result) => { | ||
mockAPI = result.app; | ||
mockAPI = result; | ||
done(); | ||
@@ -282,3 +342,3 @@ }); | ||
it('responds with the specified response rendered as JSON, status 200, and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.get(route) | ||
@@ -290,2 +350,53 @@ .expect('Content-Type', /application\/json/) | ||
describe('And there is a JSON response with query param expectations specified in the override', function() { | ||
let mockAPI; | ||
const route = '/overridden_route'; | ||
const response = { status: 'overridden response' }; | ||
const modOptions = { | ||
...DEFAULT_OPTIONS, | ||
overrides: { | ||
get: [ | ||
{ | ||
route, | ||
response, | ||
withQueryParams: { foo: 'bar' } | ||
} | ||
] | ||
} | ||
}; | ||
before(function(done) { | ||
nock(PROD_ROOT_URL) | ||
.get(route) | ||
.replyWithError('Fake API hit the production API'); | ||
start(modOptions, (err, result) => { | ||
mockAPI = result; | ||
done(); | ||
}); | ||
}); | ||
after(function() { | ||
close(mockAPI.servers); | ||
}); | ||
describe('when the query params are specified in the route', function() { | ||
it('responds with the specified response, status 200, and does not hit the production API', function(done) { | ||
request(mockAPI.app) | ||
.get(route + '?foo=bar') | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, response, done); | ||
}); | ||
}); | ||
describe('when the query params are not specified in the route', function() { | ||
it('does not respond with the specified response, status 200, and does not hit the production API', function(done) { | ||
request(mockAPI.app) | ||
.get(route + '?foo=quux') | ||
.expect(500, '', done); | ||
}); | ||
}); | ||
}); | ||
describe('And there is a JSON response with a mergeParmas callback specified in the override', function() { | ||
@@ -296,15 +407,14 @@ let mockAPI; | ||
const response = { status: 'overridden response' }; | ||
const modOptions = Object.assign({}, DEFAULT_OPTIONS, { | ||
const modOptions = { | ||
...DEFAULT_OPTIONS, | ||
overrides: { | ||
post: [ | ||
{ | ||
route: route, | ||
response: response, | ||
mergeParams(response, params) { | ||
return Object.assign({}, response, params); | ||
} | ||
route, | ||
response, | ||
mergeParams: (response, params) => ({ ...response, ...params }) | ||
} | ||
] | ||
} | ||
}); | ||
}; | ||
@@ -315,4 +425,5 @@ before(function(done) { | ||
.replyWithError('Fake API hit the production API'); | ||
start(modOptions, (err, result) => { | ||
mockAPI = result.app; | ||
mockAPI = result; | ||
done(); | ||
@@ -327,7 +438,7 @@ }); | ||
it('responds with the specified response rendered as JSON, status 200, and does not hit the production API', function(done) { | ||
request(mockAPI) | ||
request(mockAPI.app) | ||
.post(route) | ||
.send(reqBody) | ||
.expect('Content-Type', /application\/json/) | ||
.expect(200, Object.assign({}, response, reqBody), done); | ||
.expect(200, { ...response, ...reqBody }, done); | ||
}); | ||
@@ -350,3 +461,3 @@ }); | ||
const ports = [5000, 5001, 5002]; | ||
const modOptions = Object.assign({}, DEFAULT_OPTIONS, { ports }); | ||
const modOptions = { ...DEFAULT_OPTIONS, ports }; | ||
start(modOptions, (err, result) => { | ||
@@ -370,3 +481,3 @@ close(result.servers, done); | ||
ports = [1111, 1112, 1113]; | ||
const modOptions = Object.assign({}, DEFAULT_OPTIONS, { ports: ports }); | ||
const modOptions = { ...DEFAULT_OPTIONS, ports }; | ||
start(modOptions, (err, result) => { | ||
@@ -373,0 +484,0 @@ servers = result.servers; |
@@ -6,10 +6,6 @@ import 'babel-polyfill'; | ||
import bodyParser from 'body-parser'; | ||
import { parse as urlParse } from 'url'; | ||
import url from 'url'; | ||
import fs from 'fs'; | ||
import { parallel } from 'async'; | ||
let PROD_ROOT_URL; | ||
let FIXTURES_PATH; | ||
let QUERY_STRING_IGNORE; | ||
let QUIET_MODE; | ||
const SERVERS = []; | ||
@@ -20,2 +16,11 @@ const REQUIRED_CONFIG_OPTIONS = [ | ||
]; | ||
const DEFAULT_OPTIONS = { | ||
encoding: 'utf8', | ||
latency: 0, | ||
ports: [4567], | ||
queryStringIgnore: [], | ||
quiet: false, | ||
saveFixtures: true | ||
}; | ||
const JSON_CONTENT_TYPE_REGEXP = /javascript|json/; | ||
@@ -30,40 +35,23 @@ module.exports = { | ||
const app = express(); | ||
const defaults = { | ||
ports: [4567], | ||
encoding: 'utf8', | ||
queryStringIgnore: [], | ||
quiet: false | ||
}; | ||
const modOptions = Object.assign({}, defaults, options); | ||
const { | ||
prodRootURL, | ||
corsWhitelist, | ||
fixturesPath, | ||
overrides, | ||
queryStringIgnore, | ||
ports, | ||
encoding, | ||
quiet | ||
} = modOptions; | ||
const settings = { ...DEFAULT_OPTIONS, ...options }; | ||
const { corsWhitelist, encoding, latency, overrides, ports } = settings; | ||
PROD_ROOT_URL = prodRootURL; | ||
FIXTURES_PATH = fixturesPath; | ||
QUERY_STRING_IGNORE = queryStringIgnore; | ||
QUIET_MODE = quiet; | ||
if (corsWhitelist) { | ||
setCorsMiddleware(app, corsWhitelist); | ||
} | ||
if (isValidDuration(latency)) { | ||
simulateLatency(app, latency); | ||
} | ||
if (overrides) { | ||
delegateRouteOverrides(app, overrides, encoding); | ||
delegateRouteOverrides(app, settings); | ||
} | ||
app.get('*', (req, res) => { | ||
app.all('*', (req, res) => { | ||
const path = getURLPathWithQueryString(req); | ||
const fileName = getFileName(path); | ||
const fileName = getFileName(path, settings); | ||
fs.readFile(fileName, encoding, (err, data) => { | ||
if (err) { | ||
recordFromProd(req, res); | ||
fetchResponse(req, res, { ...settings, fileName, path }); | ||
} else { | ||
serveLocalResponse(res, fileName, data, { quiet: QUIET_MODE }); | ||
serveResponse(res, { ...settings, fileName, data }); | ||
} | ||
@@ -74,6 +62,7 @@ }); | ||
const result = { | ||
app: app, | ||
app, | ||
servers: SERVERS | ||
}; | ||
return startListening(app, ports, err => callback(err, result)); | ||
return startListening(app, ports, (err) => callback(err, result)); | ||
}, | ||
@@ -102,2 +91,6 @@ | ||
function isValidDuration(latency) { | ||
return Number.isFinite(latency) && latency > 0; | ||
} | ||
function generateMissingParamsError(options, callback) { | ||
@@ -108,4 +101,3 @@ if (typeof callback !== 'function') { | ||
for (let i = 0; i < REQUIRED_CONFIG_OPTIONS.length; i++) { | ||
const key = REQUIRED_CONFIG_OPTIONS[i]; | ||
for (const key of REQUIRED_CONFIG_OPTIONS) { | ||
if (typeof options[key] !== 'string') { | ||
@@ -131,2 +123,9 @@ return new Error(`Missing definition of ${key} in config file`); | ||
function simulateLatency(app, latency) { | ||
const latencyMiddleware = (_req, _res, next) => | ||
global.setTimeout(next, latency); | ||
app.use(latencyMiddleware); | ||
} | ||
function startListening(app, ports, callback) { | ||
@@ -158,9 +157,8 @@ const tasks = ports.map(port => { | ||
function delegateRouteOverrides(app, overrides, encoding) { | ||
function delegateRouteOverrides(app, options) { | ||
const { overrides, encoding, quiet } = options; | ||
const methods = ['get', 'post', 'put', 'delete', 'all']; | ||
const defaults = { | ||
status: 200, | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
headers: { 'Content-Type': 'application/json' } | ||
}; | ||
@@ -178,5 +176,12 @@ const jsonMiddleware = [ | ||
let fixture; | ||
const routeParams = Object.assign({}, defaults, params); | ||
const { route, status, response, headers, mergeParams } = routeParams; | ||
const responseIsJson = /(javascript|json)/.test(headers['Content-Type']); | ||
const routeParams = { ...defaults, ...params }; | ||
const { | ||
route, | ||
status, | ||
response, | ||
headers, | ||
mergeParams, | ||
withQueryParams: queryParams = {} | ||
} = routeParams; | ||
const responseIsJson = JSON_CONTENT_TYPE_REGEXP.test(headers['Content-Type']); | ||
@@ -188,3 +193,3 @@ if (!route) { | ||
if (!response) { | ||
const fileName = getFileName(route); | ||
const fileName = getFileName(route, options); | ||
fs.readFile(fileName, encoding, (err, data) => { | ||
@@ -203,6 +208,11 @@ if (err) { | ||
app[method].call(app, route, jsonMiddleware, (req, res) => { | ||
if (!QUIET_MODE) { | ||
app[method].call(app, route, jsonMiddleware, (req, res, next) => { | ||
if (!quiet) { | ||
console.info(`==> 📁 Serving local fixture for ${method.toUpperCase()} -> '${route}'`); | ||
} | ||
for (const [param, value] of Object.entries(queryParams)) { | ||
if (req.query[param] !== value) { | ||
return next(); | ||
} | ||
} | ||
const payload = responseIsJson && typeof mergeParams === 'function' | ||
@@ -220,10 +230,15 @@ ? mergeParams(JSON.parse(fixture), req.body) | ||
function recordFromProd(req, res) { | ||
function fetchResponse(req, res, options) { | ||
if (req.method !== 'GET') { | ||
console.error(`==> ⛔️ Couldn't complete fetch with non-GET method`); | ||
return res.status(500).end(); | ||
} | ||
let responseIsJson; | ||
const path = getURLPathWithQueryString(req); | ||
const prodURL = getProdURL(path); | ||
const { prodRootURL, saveFixtures, path, fileName } = options; | ||
const prodURL = prodRootURL + path; | ||
const responseIsJsonp = prodURL.match(/callback\=([^\&]+)/); | ||
console.info(`==> 📡 GET ${PROD_ROOT_URL} -> ${path}`); | ||
fetch(prodURL) | ||
console.info(`==> 📡 GET ${prodRootURL} -> ${path}`); | ||
fetch(prodRootURL + path) | ||
.then(response => { | ||
@@ -247,14 +262,6 @@ if (response.ok) { | ||
.then(response => { | ||
const fileName = getFileName(path); | ||
const data = responseIsJson | ||
? JSON.stringify(response) | ||
: response; | ||
fs.writeFile(fileName, data, (err) => { | ||
if (err) { | ||
throw Error(`Couldn't write response locally, received fs error: '${err}'`) | ||
} | ||
console.info(`==> 💾 Saved response to ${fileName}`); | ||
}); | ||
serveLocalResponse(res, fileName, data, { quiet: true }); | ||
if (saveFixtures) { | ||
saveFixture(fileName, response, responseIsJson); | ||
} | ||
serveResponse(res, { ...options, newResponse: true }); | ||
}) | ||
@@ -267,5 +274,5 @@ .catch(err => { | ||
function serveLocalResponse(res, fileName, data, options = { quiet: false }) { | ||
const { quiet } = options; | ||
if (quiet !== true) { | ||
function serveResponse(res, options) { | ||
const { data, fileName, quiet, newResponse } = options; | ||
if (!quiet && !newResponse) { | ||
console.info(`==> 📁 Serving local response from ${fileName}`); | ||
@@ -288,16 +295,27 @@ } | ||
function getFileName(path) { | ||
const fileNameInDirectory = QUERY_STRING_IGNORE | ||
function saveFixture(fileName, response, responseIsJson) { | ||
const data = responseIsJson | ||
? JSON.stringify(response) | ||
: response; | ||
fs.writeFile(fileName, data, (err) => { | ||
if (err) { | ||
throw Error(`Couldn't write response locally, received fs error: '${err}'`) | ||
} | ||
console.info(`==> 💾 Saved response to ${fileName}`); | ||
}); | ||
} | ||
function getFileName(path, options) { | ||
const { queryStringIgnore, fixturesPath } = options; | ||
const fileNameInDirectory = queryStringIgnore | ||
.reduce((fileName, regex) => fileName.replace(regex, ''), path) | ||
.replace(/\//, '') | ||
.replace(/\//g, ':'); | ||
return `${FIXTURES_PATH}/${fileNameInDirectory}.json`; | ||
return `${fixturesPath}/${fileNameInDirectory}.json`; | ||
} | ||
function getProdURL(path) { | ||
return PROD_ROOT_URL + path; | ||
} | ||
function getURLPathWithQueryString(req) { | ||
const queryString = url.parse(req.url).query; | ||
function getURLPathWithQueryString(req) { | ||
const queryString = urlParse(req.url).query; | ||
if (queryString && queryString.length > 0) { | ||
@@ -304,0 +322,0 @@ return req.path + '?' + queryString; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47506
7
16
1105
166
13
- Removedeslint@^1.10.3
- Removedansi-escapes@1.4.0(transitive)
- Removedansi-regex@2.1.1(transitive)
- Removedansi-styles@2.2.1(transitive)
- Removedargparse@1.0.10(transitive)
- Removedbalanced-match@1.0.2(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedbuffer-from@1.1.2(transitive)
- Removedchalk@1.1.3(transitive)
- Removedcircular-json@0.3.3(transitive)
- Removedcli-cursor@1.0.2(transitive)
- Removedcli-width@1.1.1(transitive)
- Removedcode-point-at@1.1.0(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedconcat-stream@1.6.2(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removedd@1.0.2(transitive)
- Removeddeep-is@0.1.4(transitive)
- Removeddoctrine@0.7.2(transitive)
- Removedes5-ext@0.10.64(transitive)
- Removedes6-iterator@2.0.3(transitive)
- Removedes6-map@0.1.5(transitive)
- Removedes6-set@0.1.6(transitive)
- Removedes6-symbol@3.1.4(transitive)
- Removedes6-weak-map@2.0.3(transitive)
- Removedescape-string-regexp@1.0.5(transitive)
- Removedescope@3.6.0(transitive)
- Removedeslint@1.10.3(transitive)
- Removedesniff@2.0.1(transitive)
- Removedespree@2.2.5(transitive)
- Removedesprima@2.7.3(transitive)
- Removedesrecurse@4.3.0(transitive)
- Removedestraverse@4.3.05.3.0(transitive)
- Removedestraverse-fb@1.3.2(transitive)
- Removedesutils@1.1.62.0.3(transitive)
- Removedevent-emitter@0.3.5(transitive)
- Removedexit-hook@1.1.1(transitive)
- Removedext@1.7.0(transitive)
- Removedfast-levenshtein@1.0.7(transitive)
- Removedfigures@1.7.0(transitive)
- Removedfile-entry-cache@1.3.1(transitive)
- Removedflat-cache@1.3.4(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedgenerate-function@2.3.1(transitive)
- Removedgenerate-object-property@1.2.0(transitive)
- Removedglob@5.0.157.2.3(transitive)
- Removedglobals@8.18.0(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedhandlebars@4.7.8(transitive)
- Removedhas-ansi@2.0.0(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinquirer@0.11.4(transitive)
- Removedis-fullwidth-code-point@1.0.0(transitive)
- Removedis-my-ip-valid@1.0.1(transitive)
- Removedis-my-json-valid@2.20.6(transitive)
- Removedis-property@1.0.2(transitive)
- Removedis-resolvable@1.1.0(transitive)
- Removedisarray@0.0.11.0.02.0.5(transitive)
- Removedjs-yaml@3.4.5(transitive)
- Removedjson-stable-stringify@1.1.1(transitive)
- Removedjsonify@0.0.1(transitive)
- Removedjsonpointer@5.0.1(transitive)
- Removedlevn@0.2.5(transitive)
- Removedlodash@3.10.1(transitive)
- Removedlodash._arraycopy@3.0.0(transitive)
- Removedlodash._arrayeach@3.0.0(transitive)
- Removedlodash._arraymap@3.0.0(transitive)
- Removedlodash._baseassign@3.2.0(transitive)
- Removedlodash._baseclone@3.3.0(transitive)
- Removedlodash._basecopy@3.0.1(transitive)
- Removedlodash._basedifference@3.0.3(transitive)
- Removedlodash._baseflatten@3.1.4(transitive)
- Removedlodash._basefor@3.0.3(transitive)
- Removedlodash._baseindexof@3.1.0(transitive)
- Removedlodash._bindcallback@3.0.1(transitive)
- Removedlodash._cacheindexof@3.0.2(transitive)
- Removedlodash._createassigner@3.1.1(transitive)
- Removedlodash._createcache@3.1.2(transitive)
- Removedlodash._getnative@3.9.1(transitive)
- Removedlodash._isiterateecall@3.0.9(transitive)
- Removedlodash._pickbyarray@3.0.2(transitive)
- Removedlodash._pickbycallback@3.0.0(transitive)
- Removedlodash.clonedeep@3.0.2(transitive)
- Removedlodash.isarguments@3.1.0(transitive)
- Removedlodash.isarray@3.0.4(transitive)
- Removedlodash.isplainobject@3.2.0(transitive)
- Removedlodash.istypedarray@3.0.6(transitive)
- Removedlodash.keys@3.1.2(transitive)
- Removedlodash.keysin@3.0.8(transitive)
- Removedlodash.merge@3.3.2(transitive)
- Removedlodash.omit@3.1.0(transitive)
- Removedlodash.restparam@3.6.1(transitive)
- Removedlodash.toplainobject@3.0.0(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedminimist@1.2.8(transitive)
- Removedmkdirp@0.5.6(transitive)
- Removedmute-stream@0.0.5(transitive)
- Removedneo-async@2.6.2(transitive)
- Removednext-tick@1.1.0(transitive)
- Removednumber-is-nan@1.0.1(transitive)
- Removedobject-keys@1.1.1(transitive)
- Removedonce@1.4.0(transitive)
- Removedonetime@1.1.0(transitive)
- Removedoptionator@0.6.0(transitive)
- Removedos-homedir@1.0.2(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedpath-is-inside@1.0.2(transitive)
- Removedprelude-ls@1.1.2(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedreadline2@1.0.1(transitive)
- Removedrestore-cursor@1.0.1(transitive)
- Removedrimraf@2.6.3(transitive)
- Removedrun-async@0.1.0(transitive)
- Removedrx-lite@3.1.2(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedshelljs@0.5.3(transitive)
- Removedsource-map@0.6.1(transitive)
- Removedsprintf-js@1.0.3(transitive)
- Removedstring-width@1.0.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedstrip-ansi@3.0.1(transitive)
- Removedstrip-json-comments@1.0.4(transitive)
- Removedsupports-color@2.0.0(transitive)
- Removedtext-table@0.2.0(transitive)
- Removedthrough@2.3.8(transitive)
- Removedtype@2.7.3(transitive)
- Removedtype-check@0.3.2(transitive)
- Removedtypedarray@0.0.6(transitive)
- Removeduglify-js@3.19.3(transitive)
- Removeduser-home@2.0.0(transitive)
- Removedutil-deprecate@1.0.2(transitive)
- Removedwordwrap@0.0.31.0.0(transitive)
- Removedwrappy@1.0.2(transitive)
- Removedwrite@0.2.1(transitive)
- Removedxml-escape@1.0.0(transitive)
- Removedxtend@4.0.2(transitive)