123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- /*
- * HTTP server with programmable API for adding stubs/mocks.
- */
- var express = require("express");
- var http = require("http");
- var expect = require("chai").expect;
- // compare a requestMatcher with a request; if each property
- // in the requestMatcher matches with a property in the request,
- // return true; otherwise, return false
- //
- // note that chai's eql is abused to do the matches
- var meetsCriteria = function (request, requestMatcher) {
- var good = true;
- // if we're recursing into an object in the requestMatcher,
- // but request doesn't have a corresponding object, just
- // return false immediately
- if (!request) {
- return false;
- }
- for (var key in requestMatcher) {
- if (typeof requestMatcher[key] === "function") {
- good = requestMatcher[key](request[key]);
- }
- else if (typeof requestMatcher[key] === "object") {
- good = meetsCriteria(request[key], requestMatcher[key]);
- }
- else {
- try {
- expect(requestMatcher[key]).to.eql(request[key]);
- }
- catch (e) {
- good = false;
- }
- }
- if (!good) {
- break;
- }
- }
- return good;
- };
- // matches the request req against the requestMatchers in
- // map (see Server.map property, below); when a match is found, send it
- // via the Express response object res, using the responseConfig in
- // the map
- var matcher = function (map, req, res) {
- var toSend = null;
- var requestMatcher;
- var responseConfig;
- for (var i = 0; i < map.length; i += 1) {
- requestMatcher = map[i].requestMatcher;
- if (meetsCriteria(req, requestMatcher)) {
- toSend = map[i].responseConfig;
- }
- }
- return toSend;
- };
- var Server = function () {
- var self = this;
- // this contains objects with the structure
- // {requestMatcher: {...}, responseConfig: {...}};
- // when the server receives a request, it iterates through
- // these objects until it finds a requestMatcher which matches
- // the request; then, it returns a response generated from
- // responseConfig
- this.map = [];
- this.app = express();
- // use middleware to parse request body
- this.app.use(express.bodyParser());
- // hand off requests to the request/response matcher
- this.app.all(/.*/, function (req, res) {
- // defaults if no response config found
- var data = "No matching response for request";
- var statusCode = 503;
- var pause = 0;
- // try to find an appropriate response for this request
- var responseConfig = matcher(self.map, req, res);
- if (responseConfig) {
- statusCode = responseConfig.status || 200;
- // should we generate the response body data using the
- // responseConfig's data function?
- if (typeof responseConfig.data === "function") {
- data = responseConfig.data(req);
- }
- else {
- data = responseConfig.data || '';
- }
- pause = responseConfig.pause || 0;
- }
- else {
- console.error("could not find a response configuration for request");
- }
- setTimeout(function () {
- res.status(statusCode)
- .send(data);
- }, pause);
- });
- this.server = require("http").createServer(this.app);
- this.port = null;
- };
- // register a fake response for a given request pattern;
- // responseConfig specifies how the response should
- // be constructed; requestMatcher is an object to compare with
- // each received request, to determine if the response is
- // an appropriate candidate to return;
- // NB if requestMatcher is not specified, the fake response
- // will match every request
- //
- // responseConfig currently uses the following properties:
- // * data: sets the response body; this can be set to a function:
- // if it is, that function is passed the original express request
- // object, and should return a string to be used as the response body
- // * status: sets the status code for the response (default: 200)
- // * pause: sets a pause (ms) before the response is returned (default: 0)
- //
- // in the requestMatcher, set a property for each property of the
- // request you want to test; the value of the property in the
- // requestMatcher can either be a value for comparison (using
- // chai's eql() function) or a function which will be passed
- // the corresponding value from the request; for example:
- //
- // {
- // query: function (reqQuery) {
- // return reqQuery["id"] === "1";
- // }
- // }
- //
- // note that if you want requestMatcher to compare headers,
- // you should lowercase the names of the headers so they match
- // headers as perceived by express; also note that the server
- // will parse application/json, multipart/form-data and
- // application/x-www-form-urlencoded requests into objects, so
- // any matchers for the request body should use objects rather than
- // strings
- Server.prototype.registerFake = function (responseConfig, requestMatcher) {
- this.map.push({
- responseConfig: responseConfig,
- requestMatcher: requestMatcher || {}
- });
- };
- Server.prototype.clearFakes = function () {
- this.map = [];
- };
- // cb is invoked when the server emits a "listening" event
- // or with any thrown error
- Server.prototype.start = function (cb) {
- var self = this;
- var realCb = function () {
- if (typeof cb === "function") {
- cb.apply(null, arguments);
- }
- // if first argument is set, it's an error,
- // so throw it if a callback is not defined
- else if (arguments[0]) {
- throw arguments[0];
- }
- };
- try {
- var maxConnectionsQueueLength = 511;
- // "listening" event handler
- var handler = function () {
- self.port = self.server.address().port;
- realCb();
- };
- this.server.listen(0, "localhost", maxConnectionsQueueLength, handler);
- }
- catch (e) {
- realCb(e);
- }
- };
- // cb is invoked with an error if any occurs, otherwise
- // with no arguments
- Server.prototype.stop = function (cb) {
- var self = this;
- var realCb = function () {
- if (typeof cb === "function") {
- cb.apply(null, arguments);
- }
- else if (arguments[0]) {
- throw arguments[0];
- }
- };
- try {
- this.server.on("close", realCb);
- }
- catch (e) {
- realCb(e);
- }
- this.server.close();
- };
- module.exports = {
- createServer: function () {
- return new Server();
- }
- };
|