123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- 'use strict';
- const EventEmitter = require('events');
- const muk = require('muk-prop');
- const http = require('http');
- const https = require('https');
- const cp = require('child_process');
- const thenify = require('thenify').withCallback;
- const Readable = require('stream').Readable;
- const Duplex = require('stream').Duplex;
- const mock = module.exports = function mock() {
- return muk.apply(null, arguments);
- };
- exports = mock;
- function getCallback(args) {
- let index = args.length - 1;
- let callback = args[index];
- while (typeof callback !== 'function') {
- index--;
- if (index < 0) {
- break;
- }
- callback = args[index];
- }
- if (!callback) {
- throw new TypeError('Can\'t find callback function');
- }
- // support thunk fn(a1, a2, cb, cbThunk)
- if (typeof args[index - 1] === 'function') {
- callback = args[index - 1];
- }
- return callback;
- }
- exports.isMocked = muk.isMocked;
- /**
- * create an error instance
- *
- * @param {String|Error} error - error
- * @param {Object} props - props
- * @return {Error} error - error
- */
- exports._createError = function(error, props) {
- if (!error) {
- error = new Error('mm mock error');
- error.name = 'MockError';
- }
- if (typeof error === 'string') {
- error = new Error(error);
- error.name = 'MockError';
- }
- props = props || {};
- for (const key in props) {
- error[key] = props[key];
- }
- return error;
- };
- exports._mockError = function(mod, method, error, props, timeout, once) {
- if (typeof props === 'number') {
- timeout = props;
- props = {};
- }
- error = exports._createError(error, props);
- if (timeout) {
- timeout = parseInt(timeout, 10);
- }
- timeout = timeout || 0;
- mock(mod, method, thenify(function() {
- const callback = getCallback(arguments);
- setTimeout(function() {
- if (once) {
- exports.restore();
- }
- callback(error);
- }, timeout);
- }));
- return this;
- };
- /**
- * Mock async function error.
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {String|Error} error, error string message or error instance.
- * @param {Object} props, error properties
- * @param {Number} timeout, mock async callback timeout, default is 0.
- * @return {mm} this - mm
- */
- exports.error = function(mod, method, error, props, timeout) {
- return exports._mockError(mod, method, error, props, timeout);
- };
- /**
- * Mock async function error once.
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {String|Error} error, error string message or error instance.
- * @param {Object} props, error properties
- * @param {Number} timeout, mock async callback timeout, default is 0.
- * @return {mm} this - mm
- */
- exports.errorOnce = function(mod, method, error, props, timeout) {
- return exports._mockError(mod, method, error, props, timeout, true);
- };
- /**
- * mock return callback(null, data1, data2).
- *
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {Array} datas, return datas array.
- * @param {Number} timeout, mock async callback timeout, default is 10.
- * @return {mm} this - mm
- */
- exports.datas = function(mod, method, datas, timeout) {
- if (timeout) {
- timeout = parseInt(timeout, 10);
- }
- timeout = timeout || 0;
- if (!Array.isArray(datas)) {
- datas = [ datas ];
- }
- mock(mod, method, thenify(function() {
- const callback = getCallback(arguments);
- setTimeout(function() {
- callback.apply(mod, [ null ].concat(datas));
- }, timeout);
- }));
- return this;
- };
- /**
- * mock return callback(null, data).
- *
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {Object} data, return data.
- * @param {Number} timeout, mock async callback timeout, default is 0.
- * @return {mm} this - mm
- */
- exports.data = function(mod, method, data, timeout) {
- return exports.datas(mod, method, [ data ], timeout);
- };
- /**
- * mock return callback(null, null).
- *
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {Number} [timeout], mock async callback timeout, default is 0.
- * @return {mm} this - mm
- */
- exports.empty = function(mod, method, timeout) {
- return exports.datas(mod, method, null, timeout);
- };
- /**
- * mock function sync throw error
- *
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {String|Error} error, error string message or error instance.
- * @param {Object} [props], error properties
- */
- exports.syncError = function(mod, method, error, props) {
- error = exports._createError(error, props);
- mock(mod, method, function() {
- throw error;
- });
- };
- /**
- * mock function sync return data
- *
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- * @param {Object} data, return data.
- */
- exports.syncData = function(mod, method, data) {
- mock(mod, method, function() {
- return data;
- });
- };
- /**
- * mock function sync return nothing
- *
- * @param {Object} mod, module object
- * @param {String} method, mock module object method name.
- */
- exports.syncEmpty = function(mod, method) {
- exports.syncData(mod, method);
- };
- exports.http = {};
- exports.https = {};
- function matchURL(options, params) {
- const url = params && params.url || params;
- const host = params && params.host;
- const pathname = options.path || options.pathname;
- const hostname = options.host || options.hostname;
- let match = false;
- if (pathname) {
- if (!url) {
- match = true;
- } else if (typeof url === 'string') {
- match = pathname === url;
- } else if (url instanceof RegExp) {
- match = url.test(pathname);
- } else if (typeof host === 'string') {
- match = host === hostname;
- } else if (host instanceof RegExp) {
- match = host.test(hostname);
- }
- }
- return match;
- }
- function mockRequest() {
- const req = new Duplex({
- write() {},
- read() {},
- });
- req.abort = function() {
- req._aborted = true;
- process.nextTick(function() {
- const err = new Error('socket hang up');
- err.code = 'ECONNRESET';
- req.emit('error', err);
- });
- };
- req.socket = {};
- return req;
- }
- /**
- * Mock http.request().
- * @param {String|RegExp|Object} url, request url path.
- * If url is Object, should be {url: $url, host: $host}
- * @param {String|Buffer|ReadStream} data, mock response data.
- * If data is Array, then res will emit `data` event many times.
- * @param {Object} headers, mock response headers.
- * @param {Number} [delay], response delay time, default is 10.
- * @return {mm} this - mm
- */
- exports.http.request = function(url, data, headers, delay) {
- backupOriginalRequest(http);
- return _request.call(this, http, url, data, headers, delay);
- };
- /**
- * Mock https.request().
- * @param {String|RegExp|Object} url, request url path.
- * If url is Object, should be {url: $url, host: $host}
- * @param {String|Buffer|ReadStream} data, mock response data.
- * If data is Array, then res will emit `data` event many times.
- * @param {Object} headers, mock response headers.
- * @param {Number} [delay], response delay time, default is 0.
- * @return {mm} this - mm
- */
- exports.https.request = function(url, data, headers, delay) {
- backupOriginalRequest(https);
- return _request.call(this, https, url, data, headers, delay);
- };
- function backupOriginalRequest(mod) {
- if (!mod.__sourceRequest) {
- mod.__sourceRequest = mod.request;
- }
- if (!mod.__sourceGet) {
- mod.__sourceGet = mod.get;
- }
- }
- function _request(mod, url, data, headers, delay) {
- headers = headers || {};
- if (delay) {
- delay = parseInt(delay, 10);
- }
- delay = delay || 0;
- mod.get = function(options, callback) {
- const req = mod.request(options, callback);
- req.end();
- return req;
- };
- mod.request = function(options, callback) {
- let datas = [];
- let stream = null; // read stream
- if (typeof data.read === 'function') {
- stream = data;
- } else if (!Array.isArray(data)) {
- datas = [ data ];
- } else {
- for (let i = 0; i < data.length; i++) {
- datas.push(data[i]);
- }
- }
- const match = matchURL(options, url);
- if (!match) {
- return mod.__sourceRequest(options, callback);
- }
- const req = mockRequest();
- if (callback) {
- req.on('response', callback);
- }
- let res;
- if (stream) {
- res = stream;
- } else {
- res = new Readable({
- read() {
- let chunk = datas.shift();
- if (!chunk) {
- if (!req._aborted) {
- this.push(null);
- }
- return;
- }
- if (!req._aborted) {
- if (typeof chunk === 'string') {
- chunk = Buffer.from ? Buffer.from(chunk) : new Buffer(chunk);
- }
- if (this.charset) {
- chunk = chunk.toString(this.charset);
- }
- this.push(chunk);
- }
- },
- });
- res.setEncoding = function(charset) {
- res.charset = charset;
- };
- }
- res.statusCode = headers.statusCode || 200;
- res.headers = omit(headers, 'statusCode');
- res.socket = req.socket;
- function sendResponse() {
- if (!req._aborted) {
- req.emit('response', res);
- }
- }
- if (delay) {
- setTimeout(sendResponse, delay);
- } else {
- setImmediate(sendResponse);
- }
- return req;
- };
- return this;
- }
- /**
- * Mock http.request() error.
- * @param {String|RegExp} url, request url path.
- * @param {String|Error} reqError, request error.
- * @param {String|Error} resError, response error.
- * @param {Number} [delay], request error delay time, default is 0.
- */
- exports.http.requestError = function(url, reqError, resError, delay) {
- backupOriginalRequest(http);
- _requestError.call(this, http, url, reqError, resError, delay);
- };
- /**
- * Mock https.request() error.
- * @param {String|RegExp} url, request url path.
- * @param {String|Error} reqError, request error.
- * @param {String|Error} resError, response error.
- * @param {Number} [delay], request error delay time, default is 0.
- */
- exports.https.requestError = function(url, reqError, resError, delay) {
- backupOriginalRequest(https);
- _requestError.call(this, https, url, reqError, resError, delay);
- };
- function _requestError(mod, url, reqError, resError, delay) {
- if (delay) {
- delay = parseInt(delay, 10);
- }
- delay = delay || 0;
- if (reqError && typeof reqError === 'string') {
- reqError = new Error(reqError);
- reqError.name = 'MockHttpRequestError';
- }
- if (resError && typeof resError === 'string') {
- resError = new Error(resError);
- resError.name = 'MockHttpResponseError';
- }
- mod.get = function(options, callback) {
- const req = mod.request(options, callback);
- req.end();
- return req;
- };
- mod.request = function(options, callback) {
- const match = matchURL(options, url);
- if (!match) {
- return mod.__sourceRequest(options, callback);
- }
- const req = mockRequest();
- if (callback) {
- req.on('response', callback);
- }
- setTimeout(function() {
- if (reqError) {
- return req.emit('error', reqError);
- }
- const res = new Duplex({
- read() {},
- write() {},
- });
- res.socket = req.socket;
- res.statusCode = 200;
- res.headers = {
- server: 'MockMateServer',
- };
- process.nextTick(function() {
- if (!req._aborted) {
- req.emit('error', resError);
- }
- });
- if (!req._aborted) {
- req.emit('response', res);
- }
- }, delay);
- return req;
- };
- return this;
- }
- /**
- * mock child_process spawn
- * @param {Integer} code exit code
- * @param {String} stdout stdout
- * @param {String} stderr stderr
- * @param {Integer} timeout stdout/stderr/close event emit timeout
- */
- exports.spawn = function(code, stdout, stderr, timeout) {
- const evt = new EventEmitter();
- mock(cp, 'spawn', function() {
- return evt;
- });
- setTimeout(function() {
- stdout && evt.emit('stdout', stdout);
- stderr && evt.emit('stderr', stderr);
- evt.emit('close', code);
- evt.emit('exit', code);
- }, timeout);
- };
- /**
- * remove all mock effects.
- * @return {mm} this - mm
- */
- exports.restore = function() {
- if (http.__sourceRequest) {
- http.request = http.__sourceRequest;
- http.__sourceRequest = null;
- }
- if (http.__sourceGet) {
- http.get = http.__sourceGet;
- http.__sourceGet = null;
- }
- if (https.__sourceRequest) {
- https.request = https.__sourceRequest;
- https.__sourceRequest = null;
- }
- muk.restore();
- return this;
- };
- function omit(obj, key) {
- const newObj = {};
- for (const k in obj) {
- if (k !== key) {
- newObj[k] = obj[k];
- }
- }
- return newObj;
- }
|