http-fake.helper.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /*
  2. * HTTP server with programmable API for adding stubs/mocks.
  3. */
  4. var express = require("express");
  5. var http = require("http");
  6. var expect = require("chai").expect;
  7. // compare a requestMatcher with a request; if each property
  8. // in the requestMatcher matches with a property in the request,
  9. // return true; otherwise, return false
  10. //
  11. // note that chai's eql is abused to do the matches
  12. var meetsCriteria = function (request, requestMatcher) {
  13. var good = true;
  14. // if we're recursing into an object in the requestMatcher,
  15. // but request doesn't have a corresponding object, just
  16. // return false immediately
  17. if (!request) {
  18. return false;
  19. }
  20. for (var key in requestMatcher) {
  21. if (typeof requestMatcher[key] === "function") {
  22. good = requestMatcher[key](request[key]);
  23. }
  24. else if (typeof requestMatcher[key] === "object") {
  25. good = meetsCriteria(request[key], requestMatcher[key]);
  26. }
  27. else {
  28. try {
  29. expect(requestMatcher[key]).to.eql(request[key]);
  30. }
  31. catch (e) {
  32. good = false;
  33. }
  34. }
  35. if (!good) {
  36. break;
  37. }
  38. }
  39. return good;
  40. };
  41. // matches the request req against the requestMatchers in
  42. // map (see Server.map property, below); when a match is found, send it
  43. // via the Express response object res, using the responseConfig in
  44. // the map
  45. var matcher = function (map, req, res) {
  46. var toSend = null;
  47. var requestMatcher;
  48. var responseConfig;
  49. for (var i = 0; i < map.length; i += 1) {
  50. requestMatcher = map[i].requestMatcher;
  51. if (meetsCriteria(req, requestMatcher)) {
  52. toSend = map[i].responseConfig;
  53. }
  54. }
  55. return toSend;
  56. };
  57. var Server = function () {
  58. var self = this;
  59. // this contains objects with the structure
  60. // {requestMatcher: {...}, responseConfig: {...}};
  61. // when the server receives a request, it iterates through
  62. // these objects until it finds a requestMatcher which matches
  63. // the request; then, it returns a response generated from
  64. // responseConfig
  65. this.map = [];
  66. this.app = express();
  67. // use middleware to parse request body
  68. this.app.use(express.bodyParser());
  69. // hand off requests to the request/response matcher
  70. this.app.all(/.*/, function (req, res) {
  71. // defaults if no response config found
  72. var data = "No matching response for request";
  73. var statusCode = 503;
  74. var pause = 0;
  75. // try to find an appropriate response for this request
  76. var responseConfig = matcher(self.map, req, res);
  77. if (responseConfig) {
  78. statusCode = responseConfig.status || 200;
  79. // should we generate the response body data using the
  80. // responseConfig's data function?
  81. if (typeof responseConfig.data === "function") {
  82. data = responseConfig.data(req);
  83. }
  84. else {
  85. data = responseConfig.data || '';
  86. }
  87. pause = responseConfig.pause || 0;
  88. }
  89. else {
  90. console.error("could not find a response configuration for request");
  91. }
  92. setTimeout(function () {
  93. res.status(statusCode)
  94. .send(data);
  95. }, pause);
  96. });
  97. this.server = require("http").createServer(this.app);
  98. this.port = null;
  99. };
  100. // register a fake response for a given request pattern;
  101. // responseConfig specifies how the response should
  102. // be constructed; requestMatcher is an object to compare with
  103. // each received request, to determine if the response is
  104. // an appropriate candidate to return;
  105. // NB if requestMatcher is not specified, the fake response
  106. // will match every request
  107. //
  108. // responseConfig currently uses the following properties:
  109. // * data: sets the response body; this can be set to a function:
  110. // if it is, that function is passed the original express request
  111. // object, and should return a string to be used as the response body
  112. // * status: sets the status code for the response (default: 200)
  113. // * pause: sets a pause (ms) before the response is returned (default: 0)
  114. //
  115. // in the requestMatcher, set a property for each property of the
  116. // request you want to test; the value of the property in the
  117. // requestMatcher can either be a value for comparison (using
  118. // chai's eql() function) or a function which will be passed
  119. // the corresponding value from the request; for example:
  120. //
  121. // {
  122. // query: function (reqQuery) {
  123. // return reqQuery["id"] === "1";
  124. // }
  125. // }
  126. //
  127. // note that if you want requestMatcher to compare headers,
  128. // you should lowercase the names of the headers so they match
  129. // headers as perceived by express; also note that the server
  130. // will parse application/json, multipart/form-data and
  131. // application/x-www-form-urlencoded requests into objects, so
  132. // any matchers for the request body should use objects rather than
  133. // strings
  134. Server.prototype.registerFake = function (responseConfig, requestMatcher) {
  135. this.map.push({
  136. responseConfig: responseConfig,
  137. requestMatcher: requestMatcher || {}
  138. });
  139. };
  140. Server.prototype.clearFakes = function () {
  141. this.map = [];
  142. };
  143. // cb is invoked when the server emits a "listening" event
  144. // or with any thrown error
  145. Server.prototype.start = function (cb) {
  146. var self = this;
  147. var realCb = function () {
  148. if (typeof cb === "function") {
  149. cb.apply(null, arguments);
  150. }
  151. // if first argument is set, it's an error,
  152. // so throw it if a callback is not defined
  153. else if (arguments[0]) {
  154. throw arguments[0];
  155. }
  156. };
  157. try {
  158. var maxConnectionsQueueLength = 511;
  159. // "listening" event handler
  160. var handler = function () {
  161. self.port = self.server.address().port;
  162. realCb();
  163. };
  164. this.server.listen(0, "localhost", maxConnectionsQueueLength, handler);
  165. }
  166. catch (e) {
  167. realCb(e);
  168. }
  169. };
  170. // cb is invoked with an error if any occurs, otherwise
  171. // with no arguments
  172. Server.prototype.stop = function (cb) {
  173. var self = this;
  174. var realCb = function () {
  175. if (typeof cb === "function") {
  176. cb.apply(null, arguments);
  177. }
  178. else if (arguments[0]) {
  179. throw arguments[0];
  180. }
  181. };
  182. try {
  183. this.server.on("close", realCb);
  184. }
  185. catch (e) {
  186. realCb(e);
  187. }
  188. this.server.close();
  189. };
  190. module.exports = {
  191. createServer: function () {
  192. return new Server();
  193. }
  194. };