Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

highwind

Package Overview
Dependencies
Maintainers
18
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

highwind - npm Package Compare versions

Comparing version 1.0.4 to 2.0.0

spec/responses/persisted_html_route.html

1

.eslintrc.js

@@ -28,4 +28,5 @@ module.exports = {

"space-before-function-paren": [2, "never"],
"space-after-keywords": 2,
"no-console": 0
}
};

158

lib/mock_api.js

@@ -62,7 +62,7 @@ 'use strict';

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;
var corsWhitelist = settings.corsWhitelist,
encoding = settings.encoding,
latency = settings.latency,
overrides = settings.overrides,
ports = settings.ports;

@@ -73,5 +73,7 @@

}
if (isValidDuration(latency)) {
simulateLatency(app, latency);
}
if (overrides) {

@@ -83,10 +85,22 @@ delegateRouteOverrides(app, settings);

var path = getURLPathWithQueryString(req);
var fileName = getFileName(path, settings);
_fs2.default.readFile(fileName, encoding, function (err, data) {
if (err) {
fetchResponse(req, res, _extends({}, settings, { fileName: fileName, path: path }));
} else {
serveResponse(res, _extends({}, settings, { fileName: fileName, data: data }));
}
});
var jsonFileName = getFileName(path, 'json', settings);
var jsFileName = getFileName(path, 'js', settings);
var htmlFileName = getFileName(path, 'html', settings);
// Handles JSON, JS, and HTML files.
// If the file is not found, fetch a JSON response from production.
if (_fs2.default.existsSync(jsonFileName)) {
_fs2.default.readFile(jsonFileName, encoding, function (err, data) {
serveResponse(res, data, jsonFileName, _extends({}, settings));
});
} else if (_fs2.default.existsSync(jsFileName)) {
delete require.cache[require.resolve(jsFileName)]; // clear cache to keep JS require dynamic
var data = require(jsFileName).default();
serveResponse(res, data, jsFileName, _extends({}, settings));
} else if (_fs2.default.existsSync(htmlFileName)) {
_fs2.default.readFile(htmlFileName, encoding, function (err, data) {
serveResponse(res, data, htmlFileName, _extends({}, settings));
});
} else {
fetchResponse(req, res, jsonFileName, _extends({}, settings, { path: path }));
}
});

@@ -112,4 +126,4 @@

var tasks = activeServers.map(function (serverEntry) {
var server = serverEntry.server;
var port = serverEntry.port;
var server = serverEntry.server,
port = serverEntry.port;

@@ -220,5 +234,6 @@

function delegateRouteOverrides(app, options) {
var overrides = options.overrides;
var encoding = options.encoding;
var quiet = options.quiet;
// Setup default values
var overrides = options.overrides,
encoding = options.encoding,
quiet = options.quiet;

@@ -233,15 +248,18 @@ var methods = ['get', 'post', 'put', 'delete', 'all'];

Object.keys(overrides).forEach(function (method) {
// Check for invalid protocol
if (!methods.includes(method)) {
throw Error('Couldn\'t override route with invalid HTTP method: \'' + method + '\'');
}
// Iterate through get, post, etc
overrides[method].forEach(function (params) {
var fixture = void 0;
var routeParams = _extends({}, defaults, params);
var route = routeParams.route;
var status = routeParams.status;
var response = routeParams.response;
var headers = routeParams.headers;
var mergeParams = routeParams.mergeParams;
var _routeParams$withQuer = routeParams.withQueryParams;
var queryParams = _routeParams$withQuer === undefined ? {} : _routeParams$withQuer;
var route = routeParams.route,
status = routeParams.status,
response = routeParams.response,
headers = routeParams.headers,
mergeParams = routeParams.mergeParams,
_routeParams$withQuer = routeParams.withQueryParams,
queryParams = _routeParams$withQuer === undefined ? {} : _routeParams$withQuer;

@@ -255,3 +273,3 @@ var responseIsJson = JSON_CONTENT_TYPE_REGEXP.test(headers['Content-Type']);

if (!response) {
var fileName = getFileName(route, options);
var fileName = getFileName(route, options, 'json');
_fs2.default.readFile(fileName, encoding, function (err, data) {

@@ -272,3 +290,3 @@ if (err) {

if (!quiet) {
console.info('==> 📁 Serving local fixture for ' + method.toUpperCase() + ' -> \'' + route + '\'');
console.info('==> \uD83D\uDCC1 Serving local fixture for ' + method.toUpperCase() + ' -> \'' + route + '\'');
}

@@ -281,7 +299,6 @@ var _iteratorNormalCompletion2 = true;

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 _step2$value = _slicedToArray(_step2.value, 2),
param = _step2$value[0],
value = _step2$value[1];
var param = _step2$value[0];
var value = _step2$value[1];
if (req.query[param] !== value) {

@@ -313,5 +330,5 @@ return next();

function fetchResponse(req, res, options) {
function fetchResponse(req, res, fileName, options) {
if (req.method !== 'GET') {
console.error('==> ⛔️ Couldn\'t complete fetch with non-GET method');
console.error('==> \u26D4\uFE0F Couldn\'t complete fetch with non-GET method');
return res.status(500).end();

@@ -321,6 +338,5 @@ }

var responseIsJson = void 0;
var prodRootURL = options.prodRootURL;
var saveFixtures = options.saveFixtures;
var path = options.path;
var fileName = options.fileName;
var prodRootURL = options.prodRootURL,
saveFixtures = options.saveFixtures,
path = options.path;

@@ -330,6 +346,6 @@ var prodURL = prodRootURL + path;

console.info('==> 📡 GET ' + prodRootURL + ' -> ' + path);
console.info('==> \uD83D\uDCE1 GET ' + prodRootURL + ' -> ' + path);
(0, _nodeFetch2.default)(prodRootURL + path).then(function (response) {
if (response.ok) {
console.info('==> 📡 STATUS ' + response.status);
console.info('==> \uD83D\uDCE1 STATUS ' + response.status);
} else {

@@ -352,5 +368,5 @@ throw Error('Couldn\'t complete fetch with status ' + response.status);

}
serveResponse(res, _extends({}, options, { data: data, newResponse: true }));
serveResponse(res, data, fileName, _extends({}, options, { newResponse: true }));
}).catch(function (err) {
console.error('==> ⛔️ ' + err);
console.error('==> \u26D4\uFE0F ' + err);
res.status(500).end();

@@ -360,20 +376,38 @@ });

function serveResponse(res, options) {
var data = options.data;
var fileName = options.fileName;
var quiet = options.quiet;
var newResponse = options.newResponse;
function serveResponse(res, data, fileName, options) {
var quiet = options.quiet,
newResponse = options.newResponse;
if (!quiet && !newResponse) {
console.info('==> 📁 Serving local response from ' + fileName);
console.info('==> \uD83D\uDCC1 Serving local response from ' + fileName);
}
if (fileName.match(/callback\=/)) {
res.set({ 'Content-Type': 'application/javascript' }).send(data);
} else {
return res.set({ 'Content-Type': 'application/javascript' }).send(data);
}
if (fileName.match(/.json/)) {
try {
res.json(JSON.parse(data));
if (newResponse) {
// data is from fetch's response.json() and does not need parsing
return res.json(data);
}
// data is from fs.readFile() and needs parsing
return res.json(JSON.parse(data));
} catch (e) {
res.set({ 'Content-Type': 'text/html' }).send(data);
console.error('\u26D4\uFE0F Could not parse and serve invalid JSON: ' + e);
return res.json({});
}
}
if (fileName.match(/.js/)) {
return res.json(data);
}
if (fileName.match(/.html/)) {
return res.set({ 'Content-Type': 'text/html' }).send(data);
}
console.error('⛔️ Filename extension was not recognized. Please check that your fixture ends in .js, .json, or .html!');
}

@@ -384,13 +418,14 @@

_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);
});
try {
_fs2.default.writeFile(fileName, data, function () {
console.info('==> \uD83D\uDCBE Saved response to ' + fileName);
});
} catch (e) {
throw Error('Couldn\'t write response locally, received fs error: \'' + e + '\'');
}
}
function getFileName(path, options) {
var queryStringIgnore = options.queryStringIgnore;
var fixturesPath = options.fixturesPath;
function getFileName(path, ext, options) {
var queryStringIgnore = options.queryStringIgnore,
fixturesPath = options.fixturesPath;

@@ -400,3 +435,4 @@ var fileNameInDirectory = queryStringIgnore.reduce(function (fileName, regex) {

}, path).replace(/\//, '').replace(/\//g, ':');
return fixturesPath + '/' + fileNameInDirectory + '.json';
return fixturesPath + '/' + fileNameInDirectory + '.' + ext;
}

@@ -403,0 +439,0 @@

{
"name": "highwind",
"version": "1.0.4",
"version": "2.0.0",
"description": "Mock API express server",

@@ -5,0 +5,0 @@ "main": "lib/mock_api.js",

@@ -54,4 +54,61 @@ Highwind

By default, Highwind listens for requests on port **4567**. You can change that
in the config, however.
in the [config, however](#configuration-options).
## Fixtures
You may supply your fixtures as JSON, JS, or HTML files:
#### JSON Fixture
*Filepath:* `./fixtures/myApiResponse.json`
```json
{
"result": {
"foo": "bar"
}
}
```
In this example, requesting `localhost:4567/myApiResponse` would return the above JSON.
#### HTML Fixture
*Filepath:* `./fixtures/myApiResponse.html`
```json
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
```
In this example, requesting `localhost:4567/myApiResponse` would return the above HTML.
#### JS Fixture
*Filepath:* `./fixtures/myApiResponse.js`
```js
export default () => {
return {
result: {
foo: "bar"
}
};
}
```
In this example, requesting `localhost:4567/myApiResponse` would return the following JSON.
```json
{
"result": {
"foo": "bar"
}
}
```
## Configuration Options

@@ -87,3 +144,7 @@ These are dropped in to the options object passed to `highwind.start()` during instantiation.

## HTTP Route Overrides
In some instances you may want to override the behavior for a specific route.
Here are some examples of HTTP route overrides and their use cases.

@@ -162,7 +223,31 @@

## Serving a JSONP response
## JS as JSON Responses
Highwind serves all routes with a `callback` specified in the query string as JSONP by default. This is easy to disable, though, either by specifying a non-JS `'Content-Type'` header in an override for a specific route or by adding something like `/callback\=([^\&]+)/` to your `queryStringIgnore` collection.
Highwind recognizes when a fixture file ends in `.js` instead of `.json`. When this is the case, Highwind evaluates the `export default` function of that file and attempts to return its output as JSON.
This can be useful for creating many fixtures based off of a few base fixtures without the hassle of copying and maintaining 100s of lines of identical JSON in each fixture file.
```
export default () => {
// Import a complex JSON fixture as a JS object
const baseJSON = JSON.parse(fs.readFileSync('./fixtures/persisted_json_route.json'));
// Modify a few values
baseJSON.title = "I am overriding the title value";
// Return the modified JS object back to Highwind for parsing back into JSON
return baseJSON;
}
```
## JSONP and the Callback Query
Highwind serves all routes with `callback` specified in the query string as
JSONP, not JSON, by default.
You can disable this by specifying a non-JS `'Content-Type'` header in an
override for a specific route or by adding something like
`/callback\=([^\&]+)/` to your `queryStringIgnore` collection.
## License
[MIT License](http://mit-license.org/) © Refinery29, Inc. 2016-2017
[MIT License](http://mit-license.org/) © Refinery29, Inc. 2016-2018

@@ -68,3 +68,4 @@ import 'babel-polyfill';

const route = '/non_persisted_json_route';
const response = { source: 'Remote API' };
const response = JSON.stringify({'source': 'Remote API'});
const responsePath = RESPONSES_DIR + route + '.json';

@@ -79,3 +80,5 @@ const responsePathWithCallback = RESPONSES_DIR + route + JSONP_CALLBACK + '.json';

.query(true)
.reply(200, response);
.reply(200, response, {
'Content-Type': 'application/json'
});

@@ -106,3 +109,3 @@ start(DEFAULT_OPTIONS, (err, result) => {

.end((err, res) => {
expect(res.text).to.equal(JSON.stringify(response));
expect(res.text).to.equal(response);
fs.access(responsePath, fs.F_OK, done);

@@ -118,3 +121,3 @@ });

.end((err, res) => {
expect(res.text).to.equal(JSON.stringify(response));
expect(res.text).to.equal(response);
fs.access(responsePath, fs.F_OK, done);

@@ -130,3 +133,3 @@ });

.end((err, res) => {
expect(res.text).to.equal(JSON.stringify(response));
expect(res.text).to.equal(response);
fs.access(responsePathWithCallback, fs.F_OK, done);

@@ -142,3 +145,5 @@ });

.query(true)
.reply(200, response);
.reply(200, response, {
'Content-Type': 'application/json'
});

@@ -158,6 +163,6 @@ start({ ...DEFAULT_OPTIONS, saveFixtures: false }, (err, result) => {

.get(route)
.expect('Content-Type', /application\/json/)
.expect('Content-Type', 'application/json')
.expect(200)
.end((err, res) => {
expect(res.text).to.equal(JSON.stringify(response));
expect(res.text).to.equal(response);
fs.access(responsePath, fs.F_OK, (err) => {

@@ -196,3 +201,3 @@ if (err) {

describe('And there is a JSON response matching a given route', function() {
describe('And there is a JSON file matching a given route', function() {
let mockAPI;

@@ -243,6 +248,77 @@ const route = '/persisted_json_route';

describe('And there is a non-JSON response matching a given route', function() {
describe('And there is a invalid JSON file matching a given route', function() {
let mockAPI;
const route = '/persisted_invalid_json_route';
beforeEach(function(done) {
nock(PROD_ROOT_URL)
.get(route)
.query(true)
.replyWithError('Fake API hit the production API');
start(DEFAULT_OPTIONS, (err, result) => {
mockAPI = result;
done();
});
spyOn(console, 'error');
});
afterEach(function() {
close(mockAPI.servers);
console.error.restore();
});
it('serves a blank resonse and logs an error', function(done) {
request(mockAPI.app)
.get(route)
.expect('Content-Type', /application\/json/)
.expect(200, {}, () => {
expect(console.error.calledWith("⛔️ Could not parse and serve invalid JSON: SyntaxError: Unexpected token r in JSON at position 32")).to.be.true;
done()
});
});
});
describe('And there is a JS file matching a given route', function() {
let mockAPI;
const route = '/persisted_js_route';
const responsePath = RESPONSES_DIR + route + '.js';
const jsResponse = require(responsePath).default();
beforeEach(function(done) {
nock(PROD_ROOT_URL)
.get(route)
.query(true)
.replyWithError('Fake API hit the production API');
start(DEFAULT_OPTIONS, (err, result) => {
mockAPI = result;
done();
});
});
afterEach(function() {
close(mockAPI.servers);
});
it('evaluates the JS then serves the output as JSON and does not hit the production API', function(done) {
request(mockAPI.app)
.get(route)
.expect('Content-Type', /application\/json/)
.expect(200, jsResponse, done);
});
it('ignores truncated query string expressions when identifying the persisted response filename and does not hit the production API', function(done) {
request(mockAPI.app)
.get(route + IGNORED_QUERY_PARAMS)
.expect('Content-Type', /application\/json/)
.expect(200, jsResponse, done);
});
});
describe('And there is a non-JSON/non-JS file matching a given route', function() {
let mockAPI;
const route = '/persisted_html_route';
const responsePath = RESPONSES_DIR + route + '.json';
const responsePath = RESPONSES_DIR + route + '.html';
const response = fs.readFileSync(responsePath, 'utf8');

@@ -249,0 +325,0 @@

@@ -39,5 +39,7 @@ import 'babel-polyfill';

}
if (isValidDuration(latency)) {
simulateLatency(app, latency);
}
if (overrides) {

@@ -49,10 +51,22 @@ delegateRouteOverrides(app, settings);

const path = getURLPathWithQueryString(req);
const fileName = getFileName(path, settings);
fs.readFile(fileName, encoding, (err, data) => {
if (err) {
fetchResponse(req, res, { ...settings, fileName, path });
} else {
serveResponse(res, { ...settings, fileName, data });
}
});
const jsonFileName = getFileName(path, 'json', settings);
const jsFileName = getFileName(path, 'js', settings);
const htmlFileName = getFileName(path, 'html', settings);
// Handles JSON, JS, and HTML files.
// If the file is not found, fetch a JSON response from production.
if (fs.existsSync(jsonFileName)) {
fs.readFile(jsonFileName, encoding, (err, data) => {
serveResponse(res, data, jsonFileName, { ...settings });
});
} else if (fs.existsSync(jsFileName)) {
delete require.cache[require.resolve(jsFileName)] // clear cache to keep JS require dynamic
const data = require(jsFileName).default();
serveResponse(res, data, jsFileName, { ...settings });
} else if (fs.existsSync(htmlFileName)) {
fs.readFile(htmlFileName, encoding, (err, data) => {
serveResponse(res, data, htmlFileName, { ...settings });
});
} else {
fetchResponse(req, res, jsonFileName, { ...settings, path });
}
});

@@ -152,3 +166,5 @@

function delegateRouteOverrides(app, options) {
// Setup default values
const { overrides, encoding, quiet } = options;

@@ -166,5 +182,8 @@ const methods = ['get', 'post', 'put', 'delete', 'all'];

Object.keys(overrides).forEach(method => {
// Check for invalid protocol
if (!methods.includes(method)) {
throw Error(`Couldn't override route with invalid HTTP method: '${method}'`);
}
// Iterate through get, post, etc
overrides[method].forEach(params => {

@@ -188,3 +207,3 @@ let fixture;

if (!response) {
const fileName = getFileName(route, options);
const fileName = getFileName(route, options, 'json');
fs.readFile(fileName, encoding, (err, data) => {

@@ -224,3 +243,3 @@ if (err) {

function fetchResponse(req, res, options) {
function fetchResponse(req, res, fileName, options) {
if (req.method !== 'GET') {

@@ -232,3 +251,3 @@ console.error(`==> ⛔️ Couldn't complete fetch with non-GET method`);

let responseIsJson;
const { prodRootURL, saveFixtures, path, fileName } = options;
const { prodRootURL, saveFixtures, path } = options;
const prodURL = prodRootURL + path;

@@ -260,3 +279,3 @@ const responseIsJsonp = prodURL.match(/callback\=([^\&]+)/);

}
serveResponse(res, { ...options, data, newResponse: true });
serveResponse(res, data, fileName, { ...options, newResponse: true });
})

@@ -269,20 +288,40 @@ .catch(err => {

function serveResponse(res, options) {
const { data, fileName, quiet, newResponse } = options;
function serveResponse(res, data, fileName, options) {
const { quiet, newResponse } = options;
if (!quiet && !newResponse) {
console.info(`==> 📁 Serving local response from ${fileName}`);
}
if (fileName.match(/callback\=/)) {
res
return res
.set({ 'Content-Type': 'application/javascript' })
.send(data);
} else {
}
if (fileName.match(/.json/)) {
try {
res.json(JSON.parse(data));
if (newResponse) {
// data is from fetch's response.json() and does not need parsing
return res.json(data);
}
// data is from fs.readFile() and needs parsing
return res.json(JSON.parse(data));
} catch (e) {
res
.set({ 'Content-Type': 'text/html' })
.send(data);
console.error(`⛔️ Could not parse and serve invalid JSON: ${e}`);
return res.json({});
}
}
if (fileName.match(/.js/)) {
return res.json(data);
}
if (fileName.match(/.html/)) {
return res
.set({ 'Content-Type': 'text/html' })
.send(data);
}
console.error('⛔️ Filename extension was not recognized. Please check that your fixture ends in .js, .json, or .html!');
}

@@ -295,11 +334,12 @@

fs.writeFile(fileName, data, (err) => {
if (err) {
throw Error(`Couldn't write response locally, received fs error: '${err}'`)
}
console.info(`==> 💾 Saved response to ${fileName}`);
});
try {
fs.writeFile(fileName, data, () => {
console.info(`==> 💾 Saved response to ${fileName}`);
});
} catch (e) {
throw Error(`Couldn't write response locally, received fs error: '${e}'`)
}
}
function getFileName(path, options) {
function getFileName(path, ext, options) {
const { queryStringIgnore, fixturesPath } = options;

@@ -310,3 +350,4 @@ const fileNameInDirectory = queryStringIgnore

.replace(/\//g, ':');
return `${fixturesPath}/${fileNameInDirectory}.json`;
return `${fixturesPath}/${fileNameInDirectory}.${ext}`;
}

@@ -313,0 +354,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc