mock-http-server
Version:
Controllable HTTP Server Mock for your functional tests
420 lines (340 loc) • 12.2 kB
JavaScript
var connect = require('connect'),
url = require('url'),
fs = require('fs'),
http = require('http'),
https = require('https'),
util = require('util'),
_ = require('underscore'),
multiparty = require('multiparty'),
bodyParser = require('body-parser');
/**
* @param {String} host Server host
* @param {String} port Server port
* @param {String} key HTTPS key (if missing the server is HTTP)
* @param {String} cert HTTPS certificate (if missing the server is HTTP)
*/
function Server(host, port, key, cert)
{
var server = null,
address = null,
handlers = [],
requests = [],
connections = [],
self = this;
function _saveRequest(req, res, next) {
requests.push(req);
next();
}
function _multipart(req, res, next) {
if (req.method !== 'POST' && req.method !== 'PUT') {
return next();
}
if ('multipart/form-data' !== (req.headers['content-type'] || '').split(';')[0]) {
return next();
}
var form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
// mark the body as parsed so other parsers don't attempt to parse the request
req._body = true;
req.body = {};
req.files = {};
if (err) { return next(); }
_.each(fields, function(value, name){
if (Array.isArray(value) && value.length === 1) {
req.body[name] = value[0];
} else {
req.body[name] = value;
}
});
_.each(files, function(value, name){
if (Array.isArray(value) && value.length === 1) {
req.files[name] = value[0];
} else {
req.files[name] = value;
}
});
next();
});
}
/**
* Supports:
* - { reply: { body: "data" }}
* - { reply: { body: function(req) { return "data"; }}}
* - { reply: { body: function(req, send) { setTimeout(function() { send("data"); }, 1000); }}}
*/
function _getResponseBody(handler, req, callback) {
// String
if (!_(handler.reply.body).isFunction()) {
return callback(handler.reply.body || "");
}
// Synch function
if (handler.reply.body.length <= 1) {
return callback(handler.reply.body(req) || "");
}
// Asynch function
handler.reply.body(req, callback);
}
/**
* Supports:
* - { reply: { status: 200 }}
* - { reply: { status: function(req) { return 200; }}}
*/
function _getResponseStatus(handler, req, callback) {
// Number
if (!_(handler.reply.status).isFunction()) {
return callback(handler.reply.status || 0);
}
// Synch function
return callback(handler.reply.status(req) || 0);
}
function _handleMockedRequest(req, res, next) {
var handled = false;
_(handlers).each(function(handler) {
// Parse request URL
var reqParts = url.parse(req.url, true);
req.pathname = reqParts.pathname;
req.query = reqParts.query;
// Check if we can handle the request
if (handled ||
(handler.method != "*" && req.method != handler.method.toUpperCase()) ||
(handler.path != "*" && reqParts.pathname != handler.path) ||
(handler.filter && handler.filter(req) !== true)) {
return;
}
// Flag as handled
handled = true;
// Get response status and body
_getResponseBody(handler, req, function(content) {
_getResponseStatus(handler, req, function(status) {
// Prepare response data
var encoding = Buffer.isBuffer(content) ? undefined : "utf8";
var length = Buffer.isBuffer(content) ? content.length : Buffer.byteLength(content, "utf8");
// Prepare headers
var headersToSend = {};
var headers = _(handler.reply.headers)
.extend({ "content-length": length }, handler.reply.headersOverrides || {});
// Remove undefined values from headers (useful to remove content-length from the response if
// overridden with undefined)
_.each(headers, function(headerValue, headerName) {
if (headerValue === undefined) {
delete headers[headerName];
}
});
// Remove "null" values from headers
_(headers).each(function(value, name) {
if (value !== null) {
headersToSend[name] = value;
}
});
// Send response (supports delay)
setTimeout(function() {
// Send headers
res.writeHead(status, headersToSend);
// Send content
if (req.method != "HEAD") {
res.write(content, encoding);
}
// End response (unless explicitly stated to keep it open)
if (handler.reply.end !== false) {
res.end();
}
}, handler.delay || 0);
});
});
});
if (!handled) {
next();
}
}
function _handleDefaultRequest(req, res, next)
{
res.writeHead(404, {
"content-type": "plain/text",
"content-length": 9
});
res.end("Not Found");
}
this.start = function(callback)
{
// Create app stack
var connectApp = connect()
.use(_saveRequest)
.use(_multipart)
.use(bodyParser.json())
.use(bodyParser.text())
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.text({type: function() { return true; }}))
.use(_handleMockedRequest)
.use(_handleDefaultRequest);
// Create server
if (key && cert) {
server = https.createServer({key: key, cert: cert}, connectApp);
} else {
server = http.createServer(connectApp);
}
server.on("connection", function (connection) {
connection.on("close", function () {
connections = _(connections).without(connection);
});
connections.push(connection);
});
server.on("listening", function () {
address = server.address();
callback();
});
server.listen(port, host);
};
this.stop = function (callback) {
if (!server) {
return callback();
}
// Close connections
_(connections).forEach(function (connection) {
connection.end();
});
server.on("close", function() {
server = null;
address = null;
handlers = [];
// Wait until all connections are closed
if (connections.length === 0) {
requests = [];
callback();
} else {
_(connections).forEach(function (connection) {
connection.on("close", function() {
connections = _(connections).without(connection);
if (connections.length === 0) {
requests = [];
callback();
}
});
});
}
});
server.close();
};
this.on = function(handler) {
// Add default reply
handler.reply = _({}).extend({ "status": 200, "body": "" }, handler.reply);
handler.reply.headers = _({}).extend({ "content-type": "application/json" }, handler.reply.headers);
// Add default method
handler = _({}).extend({ "method": "GET" }, handler);
handlers.unshift(handler);
return this;
};
/**
* Clears request handlers and requests received by the HTTP server.
*/
this.reset = function() {
self.resetHandlers();
self.resetRequests();
}
/**
* Clears all request handlers that were previously set using `on()` method.
*/
this.resetHandlers = function() {
handlers = [];
};
/**
* Clears all requests received by the HTTP server.
*/
this.resetRequests = function() {
requests = [];
}
/**
* Returns an array containing all requests received. If `filter` is defined,
* filters the requests by:
* - method
* - path
*
* @param {Object} filter
* @return {Array}
*/
this.requests = function(filter) {
return _(requests).filter(function(req) {
var reqParts = url.parse(req.url, true);
const methodMatch = !filter || !filter.method || filter.method === req.method;
const pathMatch = !filter || !filter.path || filter.path === reqParts.pathname;
return methodMatch && pathMatch;
});
};
/**
* Returns an array containing all active connections.
*
* @return {Array}
*/
this.connections = function() {
return connections;
};
/**
* Returns the port number if set or null otherwise
*
* @return {Number|null}
*/
this.getPort = function() {
return address ? address.port : null;
}
}
function ServerVoid() {
this.on = function() {};
this.start = function(callback) { callback(); };
this.stop = function(callback) { callback(); };
this.requests = function() { return []; };
this.connections = function() { return []; };
this.getPort = function() { return null; };
this.reset = function() {};
this.resetHandlers = function() {};
this.resetRequests = function() {};
}
/**
* @param {Object} httpConfig
* @param {Object} httpsConfig
*/
function ServerMock(httpConfig, httpsConfig)
{
var httpServerMock = httpConfig ? new Server(httpConfig.host, httpConfig.port) : new ServerVoid();
var httpsServerMock = httpsConfig ? new Server(httpsConfig.host, httpsConfig.port, httpsConfig.key, httpsConfig.cert) : new ServerVoid();
this.start = function(callback) {
httpServerMock.start(function() {
httpsServerMock.start(function() {
callback();
});
});
};
this.stop = function(callback) {
httpServerMock.stop(function() {
httpsServerMock.stop(callback);
});
};
this.on = function(handler) {
httpServerMock.on(handler);
httpsServerMock.on(handler);
return this;
};
this.requests = function(filter) {
return httpServerMock.requests(filter).concat(httpsServerMock.requests(filter));
}
this.connections = function() {
return httpServerMock.connections().concat(httpsServerMock.connections());
}
this.getHttpPort = function() {
return httpServerMock.getPort();
}
this.getHttpsPort = function() {
return httpsServerMock.getPort();
}
this.reset = function() {
httpServerMock.reset();
httpsServerMock.reset();
}
this.resetHandlers = function() {
httpServerMock.resetHandlers();
httpsServerMock.resetHandlers();
}
this.resetRequests = function() {
httpServerMock.resetRequests();
httpsServerMock.resetRequests();
}
}
module.exports = ServerMock;