mm.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. 'use strict';
  2. const EventEmitter = require('events');
  3. const muk = require('muk-prop');
  4. const http = require('http');
  5. const https = require('https');
  6. const cp = require('child_process');
  7. const thenify = require('thenify').withCallback;
  8. const Readable = require('stream').Readable;
  9. const Duplex = require('stream').Duplex;
  10. const mock = module.exports = function mock() {
  11. return muk.apply(null, arguments);
  12. };
  13. exports = mock;
  14. function getCallback(args) {
  15. let index = args.length - 1;
  16. let callback = args[index];
  17. while (typeof callback !== 'function') {
  18. index--;
  19. if (index < 0) {
  20. break;
  21. }
  22. callback = args[index];
  23. }
  24. if (!callback) {
  25. throw new TypeError('Can\'t find callback function');
  26. }
  27. // support thunk fn(a1, a2, cb, cbThunk)
  28. if (typeof args[index - 1] === 'function') {
  29. callback = args[index - 1];
  30. }
  31. return callback;
  32. }
  33. exports.isMocked = muk.isMocked;
  34. /**
  35. * create an error instance
  36. *
  37. * @param {String|Error} error - error
  38. * @param {Object} props - props
  39. * @return {Error} error - error
  40. */
  41. exports._createError = function(error, props) {
  42. if (!error) {
  43. error = new Error('mm mock error');
  44. error.name = 'MockError';
  45. }
  46. if (typeof error === 'string') {
  47. error = new Error(error);
  48. error.name = 'MockError';
  49. }
  50. props = props || {};
  51. for (const key in props) {
  52. error[key] = props[key];
  53. }
  54. return error;
  55. };
  56. exports._mockError = function(mod, method, error, props, timeout, once) {
  57. if (typeof props === 'number') {
  58. timeout = props;
  59. props = {};
  60. }
  61. error = exports._createError(error, props);
  62. if (timeout) {
  63. timeout = parseInt(timeout, 10);
  64. }
  65. timeout = timeout || 0;
  66. mock(mod, method, thenify(function() {
  67. const callback = getCallback(arguments);
  68. setTimeout(function() {
  69. if (once) {
  70. exports.restore();
  71. }
  72. callback(error);
  73. }, timeout);
  74. }));
  75. return this;
  76. };
  77. /**
  78. * Mock async function error.
  79. * @param {Object} mod, module object
  80. * @param {String} method, mock module object method name.
  81. * @param {String|Error} error, error string message or error instance.
  82. * @param {Object} props, error properties
  83. * @param {Number} timeout, mock async callback timeout, default is 0.
  84. * @return {mm} this - mm
  85. */
  86. exports.error = function(mod, method, error, props, timeout) {
  87. return exports._mockError(mod, method, error, props, timeout);
  88. };
  89. /**
  90. * Mock async function error once.
  91. * @param {Object} mod, module object
  92. * @param {String} method, mock module object method name.
  93. * @param {String|Error} error, error string message or error instance.
  94. * @param {Object} props, error properties
  95. * @param {Number} timeout, mock async callback timeout, default is 0.
  96. * @return {mm} this - mm
  97. */
  98. exports.errorOnce = function(mod, method, error, props, timeout) {
  99. return exports._mockError(mod, method, error, props, timeout, true);
  100. };
  101. /**
  102. * mock return callback(null, data1, data2).
  103. *
  104. * @param {Object} mod, module object
  105. * @param {String} method, mock module object method name.
  106. * @param {Array} datas, return datas array.
  107. * @param {Number} timeout, mock async callback timeout, default is 10.
  108. * @return {mm} this - mm
  109. */
  110. exports.datas = function(mod, method, datas, timeout) {
  111. if (timeout) {
  112. timeout = parseInt(timeout, 10);
  113. }
  114. timeout = timeout || 0;
  115. if (!Array.isArray(datas)) {
  116. datas = [ datas ];
  117. }
  118. mock(mod, method, thenify(function() {
  119. const callback = getCallback(arguments);
  120. setTimeout(function() {
  121. callback.apply(mod, [ null ].concat(datas));
  122. }, timeout);
  123. }));
  124. return this;
  125. };
  126. /**
  127. * mock return callback(null, data).
  128. *
  129. * @param {Object} mod, module object
  130. * @param {String} method, mock module object method name.
  131. * @param {Object} data, return data.
  132. * @param {Number} timeout, mock async callback timeout, default is 0.
  133. * @return {mm} this - mm
  134. */
  135. exports.data = function(mod, method, data, timeout) {
  136. return exports.datas(mod, method, [ data ], timeout);
  137. };
  138. /**
  139. * mock return callback(null, null).
  140. *
  141. * @param {Object} mod, module object
  142. * @param {String} method, mock module object method name.
  143. * @param {Number} [timeout], mock async callback timeout, default is 0.
  144. * @return {mm} this - mm
  145. */
  146. exports.empty = function(mod, method, timeout) {
  147. return exports.datas(mod, method, null, timeout);
  148. };
  149. /**
  150. * mock function sync throw error
  151. *
  152. * @param {Object} mod, module object
  153. * @param {String} method, mock module object method name.
  154. * @param {String|Error} error, error string message or error instance.
  155. * @param {Object} [props], error properties
  156. */
  157. exports.syncError = function(mod, method, error, props) {
  158. error = exports._createError(error, props);
  159. mock(mod, method, function() {
  160. throw error;
  161. });
  162. };
  163. /**
  164. * mock function sync return data
  165. *
  166. * @param {Object} mod, module object
  167. * @param {String} method, mock module object method name.
  168. * @param {Object} data, return data.
  169. */
  170. exports.syncData = function(mod, method, data) {
  171. mock(mod, method, function() {
  172. return data;
  173. });
  174. };
  175. /**
  176. * mock function sync return nothing
  177. *
  178. * @param {Object} mod, module object
  179. * @param {String} method, mock module object method name.
  180. */
  181. exports.syncEmpty = function(mod, method) {
  182. exports.syncData(mod, method);
  183. };
  184. exports.http = {};
  185. exports.https = {};
  186. function matchURL(options, params) {
  187. const url = params && params.url || params;
  188. const host = params && params.host;
  189. const pathname = options.path || options.pathname;
  190. const hostname = options.host || options.hostname;
  191. let match = false;
  192. if (pathname) {
  193. if (!url) {
  194. match = true;
  195. } else if (typeof url === 'string') {
  196. match = pathname === url;
  197. } else if (url instanceof RegExp) {
  198. match = url.test(pathname);
  199. } else if (typeof host === 'string') {
  200. match = host === hostname;
  201. } else if (host instanceof RegExp) {
  202. match = host.test(hostname);
  203. }
  204. }
  205. return match;
  206. }
  207. function mockRequest() {
  208. const req = new Duplex({
  209. write() {},
  210. read() {},
  211. });
  212. req.abort = function() {
  213. req._aborted = true;
  214. process.nextTick(function() {
  215. const err = new Error('socket hang up');
  216. err.code = 'ECONNRESET';
  217. req.emit('error', err);
  218. });
  219. };
  220. req.socket = {};
  221. return req;
  222. }
  223. /**
  224. * Mock http.request().
  225. * @param {String|RegExp|Object} url, request url path.
  226. * If url is Object, should be {url: $url, host: $host}
  227. * @param {String|Buffer|ReadStream} data, mock response data.
  228. * If data is Array, then res will emit `data` event many times.
  229. * @param {Object} headers, mock response headers.
  230. * @param {Number} [delay], response delay time, default is 10.
  231. * @return {mm} this - mm
  232. */
  233. exports.http.request = function(url, data, headers, delay) {
  234. backupOriginalRequest(http);
  235. return _request.call(this, http, url, data, headers, delay);
  236. };
  237. /**
  238. * Mock https.request().
  239. * @param {String|RegExp|Object} url, request url path.
  240. * If url is Object, should be {url: $url, host: $host}
  241. * @param {String|Buffer|ReadStream} data, mock response data.
  242. * If data is Array, then res will emit `data` event many times.
  243. * @param {Object} headers, mock response headers.
  244. * @param {Number} [delay], response delay time, default is 0.
  245. * @return {mm} this - mm
  246. */
  247. exports.https.request = function(url, data, headers, delay) {
  248. backupOriginalRequest(https);
  249. return _request.call(this, https, url, data, headers, delay);
  250. };
  251. function backupOriginalRequest(mod) {
  252. if (!mod.__sourceRequest) {
  253. mod.__sourceRequest = mod.request;
  254. }
  255. if (!mod.__sourceGet) {
  256. mod.__sourceGet = mod.get;
  257. }
  258. }
  259. function _request(mod, url, data, headers, delay) {
  260. headers = headers || {};
  261. if (delay) {
  262. delay = parseInt(delay, 10);
  263. }
  264. delay = delay || 0;
  265. mod.get = function(options, callback) {
  266. const req = mod.request(options, callback);
  267. req.end();
  268. return req;
  269. };
  270. mod.request = function(options, callback) {
  271. let datas = [];
  272. let stream = null; // read stream
  273. if (typeof data.read === 'function') {
  274. stream = data;
  275. } else if (!Array.isArray(data)) {
  276. datas = [ data ];
  277. } else {
  278. for (let i = 0; i < data.length; i++) {
  279. datas.push(data[i]);
  280. }
  281. }
  282. const match = matchURL(options, url);
  283. if (!match) {
  284. return mod.__sourceRequest(options, callback);
  285. }
  286. const req = mockRequest();
  287. if (callback) {
  288. req.on('response', callback);
  289. }
  290. let res;
  291. if (stream) {
  292. res = stream;
  293. } else {
  294. res = new Readable({
  295. read() {
  296. let chunk = datas.shift();
  297. if (!chunk) {
  298. if (!req._aborted) {
  299. this.push(null);
  300. }
  301. return;
  302. }
  303. if (!req._aborted) {
  304. if (typeof chunk === 'string') {
  305. chunk = Buffer.from ? Buffer.from(chunk) : new Buffer(chunk);
  306. }
  307. if (this.charset) {
  308. chunk = chunk.toString(this.charset);
  309. }
  310. this.push(chunk);
  311. }
  312. },
  313. });
  314. res.setEncoding = function(charset) {
  315. res.charset = charset;
  316. };
  317. }
  318. res.statusCode = headers.statusCode || 200;
  319. res.headers = omit(headers, 'statusCode');
  320. res.socket = req.socket;
  321. function sendResponse() {
  322. if (!req._aborted) {
  323. req.emit('response', res);
  324. }
  325. }
  326. if (delay) {
  327. setTimeout(sendResponse, delay);
  328. } else {
  329. setImmediate(sendResponse);
  330. }
  331. return req;
  332. };
  333. return this;
  334. }
  335. /**
  336. * Mock http.request() error.
  337. * @param {String|RegExp} url, request url path.
  338. * @param {String|Error} reqError, request error.
  339. * @param {String|Error} resError, response error.
  340. * @param {Number} [delay], request error delay time, default is 0.
  341. */
  342. exports.http.requestError = function(url, reqError, resError, delay) {
  343. backupOriginalRequest(http);
  344. _requestError.call(this, http, url, reqError, resError, delay);
  345. };
  346. /**
  347. * Mock https.request() error.
  348. * @param {String|RegExp} url, request url path.
  349. * @param {String|Error} reqError, request error.
  350. * @param {String|Error} resError, response error.
  351. * @param {Number} [delay], request error delay time, default is 0.
  352. */
  353. exports.https.requestError = function(url, reqError, resError, delay) {
  354. backupOriginalRequest(https);
  355. _requestError.call(this, https, url, reqError, resError, delay);
  356. };
  357. function _requestError(mod, url, reqError, resError, delay) {
  358. if (delay) {
  359. delay = parseInt(delay, 10);
  360. }
  361. delay = delay || 0;
  362. if (reqError && typeof reqError === 'string') {
  363. reqError = new Error(reqError);
  364. reqError.name = 'MockHttpRequestError';
  365. }
  366. if (resError && typeof resError === 'string') {
  367. resError = new Error(resError);
  368. resError.name = 'MockHttpResponseError';
  369. }
  370. mod.get = function(options, callback) {
  371. const req = mod.request(options, callback);
  372. req.end();
  373. return req;
  374. };
  375. mod.request = function(options, callback) {
  376. const match = matchURL(options, url);
  377. if (!match) {
  378. return mod.__sourceRequest(options, callback);
  379. }
  380. const req = mockRequest();
  381. if (callback) {
  382. req.on('response', callback);
  383. }
  384. setTimeout(function() {
  385. if (reqError) {
  386. return req.emit('error', reqError);
  387. }
  388. const res = new Duplex({
  389. read() {},
  390. write() {},
  391. });
  392. res.socket = req.socket;
  393. res.statusCode = 200;
  394. res.headers = {
  395. server: 'MockMateServer',
  396. };
  397. process.nextTick(function() {
  398. if (!req._aborted) {
  399. req.emit('error', resError);
  400. }
  401. });
  402. if (!req._aborted) {
  403. req.emit('response', res);
  404. }
  405. }, delay);
  406. return req;
  407. };
  408. return this;
  409. }
  410. /**
  411. * mock child_process spawn
  412. * @param {Integer} code exit code
  413. * @param {String} stdout stdout
  414. * @param {String} stderr stderr
  415. * @param {Integer} timeout stdout/stderr/close event emit timeout
  416. */
  417. exports.spawn = function(code, stdout, stderr, timeout) {
  418. const evt = new EventEmitter();
  419. mock(cp, 'spawn', function() {
  420. return evt;
  421. });
  422. setTimeout(function() {
  423. stdout && evt.emit('stdout', stdout);
  424. stderr && evt.emit('stderr', stderr);
  425. evt.emit('close', code);
  426. evt.emit('exit', code);
  427. }, timeout);
  428. };
  429. /**
  430. * remove all mock effects.
  431. * @return {mm} this - mm
  432. */
  433. exports.restore = function() {
  434. if (http.__sourceRequest) {
  435. http.request = http.__sourceRequest;
  436. http.__sourceRequest = null;
  437. }
  438. if (http.__sourceGet) {
  439. http.get = http.__sourceGet;
  440. http.__sourceGet = null;
  441. }
  442. if (https.__sourceRequest) {
  443. https.request = https.__sourceRequest;
  444. https.__sourceRequest = null;
  445. }
  446. muk.restore();
  447. return this;
  448. };
  449. function omit(obj, key) {
  450. const newObj = {};
  451. for (const k in obj) {
  452. if (k !== key) {
  453. newObj[k] = obj[k];
  454. }
  455. }
  456. return newObj;
  457. }