test.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /**
  2. * Module dependencies.
  3. */
  4. var request = require('superagent');
  5. var util = require('util');
  6. var http = require('http');
  7. var https = require('https');
  8. var assert = require('assert');
  9. var Request = request.Request;
  10. /**
  11. * Expose `Test`.
  12. */
  13. module.exports = Test;
  14. /**
  15. * Initialize a new `Test` with the given `app`,
  16. * request `method` and `path`.
  17. *
  18. * @param {Server} app
  19. * @param {String} method
  20. * @param {String} path
  21. * @api public
  22. */
  23. function Test(app, method, path, host) {
  24. Request.call(this, method.toUpperCase(), path);
  25. this.redirects(0);
  26. this.buffer();
  27. this.app = app;
  28. this._asserts = [];
  29. this.url = typeof app === 'string'
  30. ? app + path
  31. : this.serverAddress(app, path, host);
  32. }
  33. /**
  34. * Inherits from `Request.prototype`.
  35. */
  36. Object.setPrototypeOf(Test.prototype, Request.prototype);
  37. /**
  38. * Returns a URL, extracted from a server.
  39. *
  40. * @param {Server} app
  41. * @param {String} path
  42. * @returns {String} URL address
  43. * @api private
  44. */
  45. Test.prototype.serverAddress = function(app, path, host) {
  46. var addr = app.address();
  47. var port;
  48. var protocol;
  49. if (!addr) this._server = app.listen(0);
  50. port = app.address().port;
  51. protocol = app instanceof https.Server ? 'https' : 'http';
  52. return protocol + '://' + (host || '127.0.0.1') + ':' + port + path;
  53. };
  54. /**
  55. * Expectations:
  56. *
  57. * .expect(200)
  58. * .expect(200, fn)
  59. * .expect(200, body)
  60. * .expect('Some body')
  61. * .expect('Some body', fn)
  62. * .expect('Content-Type', 'application/json')
  63. * .expect('Content-Type', 'application/json', fn)
  64. * .expect(fn)
  65. *
  66. * @return {Test}
  67. * @api public
  68. */
  69. Test.prototype.expect = function(a, b, c) {
  70. // callback
  71. if (typeof a === 'function') {
  72. this._asserts.push(a);
  73. return this;
  74. }
  75. if (typeof b === 'function') this.end(b);
  76. if (typeof c === 'function') this.end(c);
  77. // status
  78. if (typeof a === 'number') {
  79. this._asserts.push(this._assertStatus.bind(this, a));
  80. // body
  81. if (typeof b !== 'function' && arguments.length > 1) {
  82. this._asserts.push(this._assertBody.bind(this, b));
  83. }
  84. return this;
  85. }
  86. // header field
  87. if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) {
  88. this._asserts.push(this._assertHeader.bind(this, { name: '' + a, value: b }));
  89. return this;
  90. }
  91. // body
  92. this._asserts.push(this._assertBody.bind(this, a));
  93. return this;
  94. };
  95. /**
  96. * Defer invoking superagent's `.end()` until
  97. * the server is listening.
  98. *
  99. * @param {Function} fn
  100. * @api public
  101. */
  102. Test.prototype.end = function(fn) {
  103. var self = this;
  104. var server = this._server;
  105. var end = Request.prototype.end;
  106. end.call(this, function(err, res) {
  107. if (server && server._handle) return server.close(localAssert);
  108. localAssert();
  109. function localAssert() {
  110. self.assert(err, res, fn);
  111. }
  112. });
  113. return this;
  114. };
  115. /**
  116. * Perform assertions and invoke `fn(err, res)`.
  117. *
  118. * @param {?Error} resError
  119. * @param {Response} res
  120. * @param {Function} fn
  121. * @api private
  122. */
  123. Test.prototype.assert = function(resError, res, fn) {
  124. var error;
  125. var i;
  126. // check for unexpected network errors or server not running/reachable errors
  127. // when there is no response and superagent sends back a System Error
  128. // do not check further for other asserts, if any, in such case
  129. // https://nodejs.org/api/errors.html#errors_common_system_errors
  130. var sysErrors = {
  131. ECONNREFUSED: 'Connection refused',
  132. ECONNRESET: 'Connection reset by peer',
  133. EPIPE: 'Broken pipe',
  134. ETIMEDOUT: 'Operation timed out'
  135. };
  136. if (!res && resError) {
  137. if (resError instanceof Error && resError.syscall === 'connect'
  138. && Object.getOwnPropertyNames(sysErrors).indexOf(resError.code) >= 0) {
  139. error = new Error(resError.code + ': ' + sysErrors[resError.code]);
  140. } else {
  141. error = resError;
  142. }
  143. }
  144. // asserts
  145. for (i = 0; i < this._asserts.length && !error; i += 1) {
  146. error = this._assertFunction(this._asserts[i], res);
  147. }
  148. // set unexpected superagent error if no other error has occurred.
  149. if (!error && resError instanceof Error && (!res || resError.status !== res.status)) {
  150. error = resError;
  151. }
  152. fn.call(this, error || null, res);
  153. };
  154. /**
  155. * Perform assertions on a response body and return an Error upon failure.
  156. *
  157. * @param {Mixed} body
  158. * @param {Response} res
  159. * @return {?Error}
  160. * @api private
  161. */
  162. Test.prototype._assertBody = function(body, res) {
  163. var isregexp = body instanceof RegExp;
  164. var a;
  165. var b;
  166. // parsed
  167. if (typeof body === 'object' && !isregexp) {
  168. try {
  169. assert.deepStrictEqual(body, res.body);
  170. } catch (err) {
  171. a = util.inspect(body);
  172. b = util.inspect(res.body);
  173. return error('expected ' + a + ' response body, got ' + b, body, res.body);
  174. }
  175. } else if (body !== res.text) {
  176. // string
  177. a = util.inspect(body);
  178. b = util.inspect(res.text);
  179. // regexp
  180. if (isregexp) {
  181. if (!body.test(res.text)) {
  182. return error('expected body ' + b + ' to match ' + body, body, res.body);
  183. }
  184. } else {
  185. return error('expected ' + a + ' response body, got ' + b, body, res.body);
  186. }
  187. }
  188. };
  189. /**
  190. * Perform assertions on a response header and return an Error upon failure.
  191. *
  192. * @param {Object} header
  193. * @param {Response} res
  194. * @return {?Error}
  195. * @api private
  196. */
  197. Test.prototype._assertHeader = function(header, res) {
  198. var field = header.name;
  199. var actual = res.header[field.toLowerCase()];
  200. var fieldExpected = header.value;
  201. if (typeof actual === 'undefined') return new Error('expected "' + field + '" header field');
  202. // This check handles header values that may be a String or single element Array
  203. if ((Array.isArray(actual) && actual.toString() === fieldExpected)
  204. || fieldExpected === actual) {
  205. return;
  206. }
  207. if (fieldExpected instanceof RegExp) {
  208. if (!fieldExpected.test(actual)) {
  209. return new Error('expected "' + field + '" matching '
  210. + fieldExpected + ', got "' + actual + '"');
  211. }
  212. } else {
  213. return new Error('expected "' + field + '" of "' + fieldExpected + '", got "' + actual + '"');
  214. }
  215. };
  216. /**
  217. * Perform assertions on the response status and return an Error upon failure.
  218. *
  219. * @param {Number} status
  220. * @param {Response} res
  221. * @return {?Error}
  222. * @api private
  223. */
  224. Test.prototype._assertStatus = function(status, res) {
  225. var a;
  226. var b;
  227. if (res.status !== status) {
  228. a = http.STATUS_CODES[status];
  229. b = http.STATUS_CODES[res.status];
  230. return new Error('expected ' + status + ' "' + a + '", got ' + res.status + ' "' + b + '"');
  231. }
  232. };
  233. /**
  234. * Performs an assertion by calling a function and return an Error upon failure.
  235. *
  236. * @param {Function} fn
  237. * @param {Response} res
  238. * @return {?Error}
  239. * @api private
  240. */
  241. Test.prototype._assertFunction = function(fn, res) {
  242. var err;
  243. try {
  244. err = fn(res);
  245. } catch (e) {
  246. err = e;
  247. }
  248. if (err instanceof Error) return err;
  249. };
  250. /**
  251. * Return an `Error` with `msg` and results properties.
  252. *
  253. * @param {String} msg
  254. * @param {Mixed} expected
  255. * @param {Mixed} actual
  256. * @return {Error}
  257. * @api private
  258. */
  259. function error(msg, expected, actual) {
  260. var err = new Error(msg);
  261. err.expected = expected;
  262. err.actual = actual;
  263. err.showDiff = true;
  264. return err;
  265. }