Skip to content

Commit

Permalink
Added new "switch" feature to allow different responses based on a re…
Browse files Browse the repository at this point in the history
…quest parameter. Bump to v1.8.
  • Loading branch information
gstroup committed Feb 8, 2014
1 parent 468b7f1 commit cd9d7fa
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 78 deletions.
18 changes: 18 additions & 0 deletions .jshintrc
@@ -0,0 +1,18 @@
{
"node": true,
"browser": true,
"esnext": true,
"bitwise": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"regexp": true,
"undef": true,
"unused": true,
"strict": false,
"trailing": true,
"smarttabs": true
}
23 changes: 19 additions & 4 deletions README.md
Expand Up @@ -41,11 +41,12 @@ On startup, config values are loaded from the config.json file.
During runtime, mock services can be configured on the fly.
See the sample config.json file in this package.

* config.json file format has changed with the 0.1.6 release. See below for the new format. (Old config.json file format is deprecated, but still functioning.)
* Content-type for a service response can be set for each service. If not set, content-type defaults to applicatoin/xml for .xml files, and application/json for .json files.
* config.json file format has changed with the 0.1.6 release. See below for the new format. (Old config.json file format is deprecated and doesn't support new features, but still functioning.)
* Content-type for a service response can be set for each service. If not set, content-type defaults to application/xml for .xml files, and application/json for .json files.
* Latency (ms) can be set to simulate slow service responses. Latency can be set for a single service, or globally for all services.
* mockDirectory value should be an absolute path.
* Allowed domains can be set to restrict CORS requests to certain domains.
* Services can be configured to return different responses, depending on a request parameter.

```js
{
Expand All @@ -68,11 +69,13 @@ See the sample config.json file in this package.
},
"nested/ace": {
"mockFile": "ace.json",
"verbs": ["post", "get"]
"verbs": ["post", "get"],
"switch": "customerId"
},
"var/:id": {
"mockFile": "xml/queen.xml",
"verbs": ["all"]
"verbs": ["all"],
"switch": "id"
}
}
}
Expand All @@ -81,6 +84,18 @@ The most interesting part of the configuration file is the webServices section.
This section contains a JSON object describing each service. The key for each service object is the service URL (endpoint.) Inside each service object, the "mockFile" and "verbs" are required. "latency" and "contentType" are optional.
For instance, a GET request sent to "http://server:port/first" will return the king.json file from the samplemocks directory, with a 20 ms delay.

### Switches
In your configuration, you can set up a "switch" parameter for each service. If set, apimocker will check the request for this parameter, and return a different file based on the value. For instance, if you set up a switch as seen above for "nested/ace", then you can will get different responses based on the request sent to apimocker. A JSON POST request to the URL "http://localhost:7878/nested/ace" with this data:
```js
{
"customerId": 1234
}
```
will return data from the mock file called "customerId1234.json". Switch values can also be passed in as query parameters:
http://localhost:7878/nested/ace?customerId=1234
or as part of the URL, if you have configured your service to handle variables, like the "var/:id" service above:
http://localhost:7878/var/789

## Runtime configuration
After starting apimocker, mocks can be configured using a simple http api.
This http api can be called easily from your functional tests, to test your code's handling of different responses.
Expand Down
6 changes: 4 additions & 2 deletions config.json
Expand Up @@ -17,11 +17,13 @@
},
"nested/ace": {
"mockFile": "ace.json",
"verbs": ["post", "get"]
"verbs": ["post", "get"],
"switch": "customerId"
},
"var/:id": {
"mockFile": "xml/queen.xml",
"verbs": ["all"]
"verbs": ["all"],
"switch": "id"
}
}
}
99 changes: 69 additions & 30 deletions lib/apimocker.js
Expand Up @@ -5,11 +5,11 @@ var express = require('express'),
apiMocker = {};

apiMocker.defaults = {
"port": "8888",
"mockDirectory": "./mocks/",
"allowedDomains": ["*"],
"webServices": {}
};
"port": "8888",
"mockDirectory": "./mocks/",
"allowedDomains": ["*"],
"webServices": {}
};

apiMocker.createServer = function(options) {
options = options || {};
Expand All @@ -32,7 +32,7 @@ apiMocker.createServer = function(options) {

apiMocker.setConfigFile = function (file) {
if (!file) {
return apiMocker;
return apiMocker;
} else if (path.sep !== file.substr(0,1)) {
//relative path from command line
apiMocker.configFilePath = path.resolve(process.cwd(), file);
Expand All @@ -44,17 +44,17 @@ apiMocker.setConfigFile = function (file) {

apiMocker.loadConfigFile = function() {
if (apiMocker.configFilePath) {
apiMocker.log("Loading config file: " + apiMocker.configFilePath);
// Switched to use fs.readFileSync instead of "require"
// this makes testing easier, and avoids messing with require cache.
var newOptions = _.clone(apiMocker.defaults),
configJson = JSON.parse(fs.readFileSync(apiMocker.configFilePath));
newOptions = _.extend(newOptions, apiMocker.options, configJson);
apiMocker.options = newOptions;
apiMocker.options.webServices = apiMocker.normalizeWebServicesConfig(apiMocker.options.webServices);
apiMocker.setRoutes(apiMocker.options.webServices);
apiMocker.log("Loading config file: " + apiMocker.configFilePath);
// Switched to use fs.readFileSync instead of "require"
// this makes testing easier, and avoids messing with require cache.
var newOptions = _.clone(apiMocker.defaults),
configJson = JSON.parse(fs.readFileSync(apiMocker.configFilePath));
newOptions = _.extend(newOptions, apiMocker.options, configJson);
apiMocker.options = newOptions;
apiMocker.options.webServices = apiMocker.normalizeWebServicesConfig(apiMocker.options.webServices);
apiMocker.setRoutes(apiMocker.options.webServices);
} else {
apiMocker.log("No config file path set.");
apiMocker.log("No config file path set.");
}
};

Expand All @@ -65,7 +65,7 @@ apiMocker.normalizeWebServicesConfig = function(webServices) {
} else {
apiMocker.log("WARNING: apimocker config file format is deprecated.");
_.each(topLevelKeys, function(verb) {
var serviceKeys = _.keys(webServices[verb]);
var newSvc, serviceKeys = _.keys(webServices[verb]);
_.each(serviceKeys, function(key) {
if (newWebServices[key]) {
newSvc = newWebServices[key];
Expand Down Expand Up @@ -125,7 +125,6 @@ apiMocker.setRoutes = function(webServices) {
var svc = _.clone(webServices[key]);
// apiMocker.log("about to add a new service: " + JSON.stringify(svc));
_.each(svc.verbs, function(v) {
// apiMocker.log("adding a service");
svc.verb = v.toLowerCase();
svc.serviceUrl = key;
if (typeof svc.latency === "undefined") {
Expand All @@ -137,16 +136,27 @@ apiMocker.setRoutes = function(webServices) {
// apiMocker.log(apiMocker.express.routes);
};

apiMocker.sendResponse = function(res, options) {
apiMocker.sendResponse = function(req, res, options) {
var originalOptions, mockPath;
if (options.switch) {
options = _.clone(options);
originalOptions = _.clone(options);
apiMocker.setMockFile(options, req);
mockPath = path.join(apiMocker.options.mockDirectory, options.mockFile);
if (!fs.existsSync(mockPath)) {
apiMocker.log("No file found: " + options.mockFile + " attempting base file: " + originalOptions.mockFile);
options = originalOptions;
}
}
mockPath = path.join(apiMocker.options.mockDirectory, options.mockFile);
apiMocker.log("Returning mock: " + options.verb.toUpperCase() + " " + options.serviceUrl + " : " +
options.mockFile);

if (options.contentType) {
// apiMocker.log("Content-Type: " + options.contentType);
res.header('Content-Type', options.contentType);
var mockPath = path.join(apiMocker.options.mockDirectory, options.mockFile);
fs.readFile(mockPath, {encoding: "utf8"}, function(err, data) {
if (err) throw err;
if (err) { throw err; }
var buff = new Buffer(data, 'utf8');
res.send(buff);
});
Expand All @@ -155,20 +165,40 @@ apiMocker.sendResponse = function(res, options) {
}
};

apiMocker.setMockFile = function(options, req) {
var switchValue = "",
mockFileParts, mockFilePrefix = "", mockFileBaseName;
if (req.body[options.switch]) { // json post request
switchValue = req.body[options.switch];
} else if (req.param(options.switch)) { // query param in get request
switchValue = req.param(options.switch);
} else {
return;
}
mockFileParts = options.mockFile.split("/");
mockFileBaseName = mockFileParts.pop();
if (mockFileParts.length > 0) {
mockFilePrefix = mockFileParts.join("/") + "/";
}
options.mockFile = mockFilePrefix + options.switch + switchValue + "." + mockFileBaseName;
};

// Sets the route for express, in case it was not set yet.
apiMocker.setRoute = function(options) {
var latency = options.latency ? options.latency : 0;
apiMocker.express[options.verb]("/" + options.serviceUrl, function(req, res) {
if (options.latency && options.latency > 0) {
setTimeout(function() {
apiMocker.sendResponse(res, options);
apiMocker.sendResponse(req, res, options);
}, options.latency);
} else {
apiMocker.sendResponse(res, options);
apiMocker.sendResponse(req, res, options);
}
});
apiMocker.log("Set route: " + options.verb.toUpperCase() + " " + options.serviceUrl + " : " +
options.mockFile + " " + options.latency + " ms");
if (options.switch) {
apiMocker.log(" with switch on param: " + options.switch);
}
};

apiMocker.removeRoute = function(options) {
Expand All @@ -188,23 +218,32 @@ apiMocker.removeRoute = function(options) {

// CORS middleware
apiMocker.corsMiddleware = function(req, res, next) {
res.header('Access-Control-Allow-Origin', apiMocker.options.allowedDomains);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Origin', apiMocker.options.allowedDomains);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');

next();
next();
};

apiMocker.start = function (port) {
apiMocker.createAdminServices();
apiMocker.loadConfigFile();
port = port || apiMocker.options.port;
apiMocker.express.listen(port);
apiMocker.expressInstance = apiMocker.express.listen(port);
apiMocker.log("Mock server listening on port " + port);
return apiMocker;
};

apiMocker.stop = function() {
if (apiMocker.expressInstance) {
apiMocker.log("Stopping mock server.");
apiMocker.expressInstance.close();
}
return apiMocker;
};

// expose all the "public" methods.
exports.createServer = apiMocker.createServer;
exports.start = apiMocker.start;
exports.setConfigFile = apiMocker.setConfigFile;
exports.setConfigFile = apiMocker.setConfigFile;
exports.stop = apiMocker.stop;
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "apimocker",
"description": "Simple HTTP server that returns mock service API responses to your front end.",
"version": "0.1.7",
"version": "0.1.8",
"engines": {"node": ">=0.10.0"},
"author": "Greg Stroup <gstroup@gmail.com>",
"dependencies": {
Expand Down
4 changes: 4 additions & 0 deletions samplemocks/customerId1234.ace.json
@@ -0,0 +1,4 @@
{
"ace": "greg",
"note": "request contained customerId = 1234"
}
3 changes: 3 additions & 0 deletions samplemocks/xml/id789.queen.xml
@@ -0,0 +1,3 @@
<queen>
<suit>hearts</suit>
</queen>
4 changes: 2 additions & 2 deletions test/test-config.json
@@ -1,7 +1,6 @@
{
"note": "used for functional tests.",
"mockDirectory": "./samplemocks/",
"quiet": true,
"port": "7879",
"latency": 0,
"allowedDomains": ["abc"],
Expand All @@ -13,7 +12,8 @@
},
"nested/ace": {
"mockFile": "ace.json",
"verbs": ["post", "get"]
"verbs": ["post", "get"],
"switch": "customerId"
},
"var/:id": {
"mockFile": "xml/queen.xml",
Expand Down

0 comments on commit cd9d7fa

Please sign in to comment.