mirror of
https://github.com/zhigang1992/interfake.git
synced 2026-01-12 22:48:04 +08:00
274 lines
9.1 KiB
JavaScript
274 lines
9.1 KiB
JavaScript
var express = require('express');
|
|
var path = require('path');
|
|
var FluentInterface = require('./fluent');
|
|
var corsMiddleware = require('./cors');
|
|
var util = require('core-util-is');
|
|
var url = require('url');
|
|
var merge = require('merge');
|
|
var connectJson = require('connect-json');
|
|
var bodyParser = require('body-parser');
|
|
|
|
function createInvalidDataException(data) {
|
|
return new Error('You have to provide a JSON object with the following structure: \n' + JSON.stringify({ request : { method : '[GET|PUT|POST|DELETE]', url : '(relative URL e.g. /hello)' }, response : { code : '(HTTP Response code e.g. 200/400/500)', body : '(a JSON object)' } }, null, 4) + ' but you provided: \n' + JSON.stringify(data, null, 4));
|
|
}
|
|
|
|
function Interfake(o) {
|
|
o = o || { debug: false };
|
|
var debug = require('./debug')('interfake-server', o.debug);
|
|
var app = express();
|
|
var router = express.Router();
|
|
var fluentInterface = new FluentInterface(this, o);
|
|
var expectationsLookup = {};
|
|
var server;
|
|
|
|
o.path = url.resolve('/', o.path || '');
|
|
|
|
debug('Root path is', o.path);
|
|
|
|
app.use(o.path, router);
|
|
app.use(connectJson());
|
|
app.use(bodyParser());
|
|
app.use(corsMiddleware);
|
|
|
|
app.post('/_requests?', function(req, res){
|
|
try {
|
|
createRoute(req.body);
|
|
res.send(201, { done : true });
|
|
} catch (e) {
|
|
debug('Error: ', e);
|
|
res.send(400, e);
|
|
}
|
|
});
|
|
|
|
function determineDelay(delayInput) {
|
|
var result = 0, range, upper, lower;
|
|
if(util.isNumber(delayInput)) {
|
|
result = delayInput;
|
|
} else if(util.isString(delayInput)) {
|
|
range = /([0-9]+)..([0-9]+)/.exec(delayInput);
|
|
upper = +range[2];
|
|
lower = +range[1];
|
|
result = Math.floor( Math.random() * (upper - lower + 1) + lower );
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function createRouteHash(requestDescriptor) {
|
|
var finalRoute;
|
|
|
|
var path = url.parse(requestDescriptor.url, true);
|
|
requestDescriptor.query = merge(path.query || {}, requestDescriptor.query || {});
|
|
|
|
requestDescriptor.url = path.pathname;
|
|
|
|
var initialRoute = requestDescriptor.method.toUpperCase() + ' ' + requestDescriptor.url;
|
|
var fullQueryString;
|
|
if (requestDescriptor.query) {
|
|
var queryKeys = Object.keys(requestDescriptor.query).filter(function (key) {
|
|
return key !== 'callback';
|
|
});
|
|
if (queryKeys.length) {
|
|
debug('Query keys are', queryKeys);
|
|
|
|
// fullQueryString = queryKeys.sort().map(function (key) {
|
|
// // return encodeURIComponent(key);
|
|
// if (requestDescriptor.query[key] instanceof RegExp) {
|
|
// return '';
|
|
// }
|
|
// return encodeURIComponent(key) + '=' + encodeURIComponent(requestDescriptor.query[key]);
|
|
// }).join(';');
|
|
finalRoute = initialRoute;
|
|
if (fullQueryString && fullQueryString.length) {
|
|
finalRoute += '?' + fullQueryString;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!finalRoute) {
|
|
finalRoute = initialRoute;
|
|
}
|
|
|
|
debug('Lookup hash key will be: ' + finalRoute);
|
|
return finalRoute;
|
|
}
|
|
|
|
function createRoute(data, existingLookupHash) {
|
|
var specifiedRequest, specifiedResponse, afterSpecifiedResponse, lookupHash, existingExpectations;
|
|
if (!data.request || !data.request.method || !data.request.url || !data.response || !data.response.code) {
|
|
throw createInvalidDataException(data);
|
|
}
|
|
|
|
if (data.response.body) {
|
|
debug('Setting up ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with a body of length ' + JSON.stringify(data.response.body).length);
|
|
} else {
|
|
debug('Setting up ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with no body');
|
|
}
|
|
|
|
specifiedRequest = data.request;
|
|
|
|
if (existingLookupHash) {
|
|
existingExpectations = expectationsLookup[existingLookupHash].pop();
|
|
debug('Looking for existing lookup hash ', existingLookupHash);
|
|
// existingExpectations.request = merge(data.request, existingExpectations.request);
|
|
lookupHash = createRouteHash(specifiedRequest);
|
|
debug('New lookup hash is', lookupHash);
|
|
} else {
|
|
// Register query params/response in lookup hash
|
|
lookupHash = createRouteHash(specifiedRequest);
|
|
}
|
|
|
|
if (expectationsLookup[lookupHash]) {
|
|
debug('Lookup hash', lookupHash, 'already has a route associated with it - there must be more to come.');
|
|
expectationsLookup[lookupHash].push({
|
|
request: data.request,
|
|
response: data.response,
|
|
afterResponse: data.afterResponse
|
|
});
|
|
} else {
|
|
expectationsLookup[lookupHash] = [{
|
|
request: data.request,
|
|
response: data.response,
|
|
afterResponse: data.afterResponse
|
|
}];
|
|
}
|
|
|
|
router[specifiedRequest.method.toLowerCase()](specifiedRequest.url, function (req, res) {
|
|
debug('Incoming request at', req.url)
|
|
var lookupHash = createRouteHash({
|
|
method: req.method,
|
|
url: req.path,
|
|
query: req.query
|
|
});
|
|
var expectDataArray = expectationsLookup[lookupHash];
|
|
var requestQueryKeys = Object.keys(req.query);
|
|
var expectData, testIndex, matchedQueryKeys;
|
|
var sameLengthWithCallback = function (a, b) { return req.query.callback && a.length - 1 === b.length; };
|
|
var sameLength = function (a, b) { return a.length === b.length; };
|
|
var queryStringsMatch = function (potentialMatch) {
|
|
return function (pq, q) {
|
|
debug('Comparing', potentialMatch.request.query[q], 'and', req.query[q]);
|
|
if (potentialMatch.request.query[q] instanceof RegExp) {
|
|
return pq && potentialMatch.request.query[q].test('' + req.query[q]);
|
|
} else {
|
|
return ('' + req.query[q]) === ('' + potentialMatch.request.query[q]);
|
|
}
|
|
};
|
|
};
|
|
|
|
testIndex = !!expectDataArray ? expectDataArray.length - 1 : -1;
|
|
|
|
while(!expectData && testIndex >= 0) {
|
|
debug('Test index is', testIndex);
|
|
matchedQueryKeys = Object.keys(expectDataArray[testIndex].request.query);
|
|
|
|
if (req.query.callback && (requestQueryKeys.length - 1 === matchedQueryKeys.length === 0)) {
|
|
// We know it's gonna be this one
|
|
expectData = expectDataArray[testIndex];
|
|
break;
|
|
}
|
|
|
|
if ((sameLengthWithCallback(requestQueryKeys, matchedQueryKeys) || sameLength(requestQueryKeys, matchedQueryKeys)) &&
|
|
matchedQueryKeys.reduce(queryStringsMatch(expectDataArray[testIndex]), true)) {
|
|
expectData = expectDataArray[testIndex];
|
|
}
|
|
--testIndex;
|
|
}
|
|
|
|
if (!expectData) {
|
|
return res.send(404);
|
|
}
|
|
|
|
var specifiedResponse = expectData.response; // req.route.responseData;
|
|
var afterSpecifiedResponse = expectData.afterResponse; //req.route.afterResponseData;
|
|
var responseDelay = determineDelay(specifiedResponse.delay);
|
|
|
|
debug(req.method, 'request to', req.url, 'returning', specifiedResponse.code);
|
|
debug(req.method, 'request to', req.url, 'will be delayed by', responseDelay, 'millis');
|
|
// debug('After response is', afterSpecifiedResponse);
|
|
|
|
var responseBody = specifiedResponse.body;
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
|
|
if (specifiedResponse.headers) {
|
|
Object.keys(specifiedResponse.headers).forEach(function (k) {
|
|
res.setHeader(k, specifiedResponse.headers[k]);
|
|
});
|
|
}
|
|
|
|
if (req.query.callback) {
|
|
debug('Request is asking for jsonp');
|
|
if (typeof responseBody !== 'string') responseBody = JSON.stringify(responseBody);
|
|
responseBody = req.query.callback.trim() + '(' + responseBody + ');';
|
|
}
|
|
setTimeout(function() {
|
|
res.send(specifiedResponse.code, responseBody);
|
|
|
|
if (afterSpecifiedResponse && afterSpecifiedResponse.endpoints) {
|
|
debug('Response sent, setting up', afterSpecifiedResponse.endpoints.length, 'endpoints');
|
|
afterSpecifiedResponse.endpoints.forEach(function (endpoint) {
|
|
createRoute(endpoint);
|
|
});
|
|
}
|
|
}, responseDelay);
|
|
});
|
|
|
|
var numAfterResponse = (data.afterResponse && data.afterResponse.endpoints) ? data.afterResponse.endpoints.length : 0;
|
|
|
|
if (data.response.body) {
|
|
debug('Setup complete: ' + data.request.method.toUpperCase() + ' ' + data.request.url + ' to return ' + data.response.code + ' with a body of length ' + JSON.stringify(data.response.body).length + ' and ' + numAfterResponse + ' after-responses');
|
|
} else {
|
|
debug('Setup complete: ' + data.request.method.toUpperCase() + ' ' + data.request.url + ' to return ' + data.response.code + ' with no body and ' + numAfterResponse + ' after-responses');
|
|
}
|
|
return lookupHash;
|
|
}
|
|
|
|
this.createRoute = createRoute;
|
|
|
|
this.get = fluentInterface.forMethod('get');
|
|
this.post = fluentInterface.forMethod('post');
|
|
this.put = fluentInterface.forMethod('put');
|
|
this.delete = fluentInterface.forMethod('delete');
|
|
|
|
this.serveStatic = function (path, directory) {
|
|
path = path || '/_static';
|
|
app.use(path, express.static(directory));
|
|
};
|
|
|
|
this.listen = function (port, callback) {
|
|
port = port || 3000;
|
|
server = app.listen(port, function () {
|
|
debug('Interfake is listening for requests on port', server.address().port);
|
|
// debug('Interfake is listening for requests on port ' + port);
|
|
if(util.isFunction(callback)) {
|
|
callback();
|
|
}
|
|
});
|
|
};
|
|
|
|
this.stop = function () {
|
|
if (server) {
|
|
debug('Interfake is stopping');
|
|
server.close(function () {
|
|
debug('Interfake has stopped');
|
|
server = undefined;
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
Interfake.prototype.loadFile = function (filePath) {
|
|
var file;
|
|
|
|
filePath = path.resolve(process.cwd(), filePath);
|
|
|
|
file = require(filePath);
|
|
|
|
file.forEach(function (endpoint) {
|
|
this.createRoute(endpoint);
|
|
}.bind(this));
|
|
};
|
|
|
|
module.exports = Interfake;
|