| 1 | | /*! |
| 2 | | * mm - lib/mm.js |
| 3 | | * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com> |
| 4 | | * MIT Licensed |
| 5 | | */ |
| 6 | | |
| 7 | 1 | "use strict"; |
| 8 | | |
| 9 | | /** |
| 10 | | * Module dependencies. |
| 11 | | */ |
| 12 | | |
| 13 | 1 | var EventEmitter = require('events').EventEmitter; |
| 14 | 1 | var muk = require('muk'); |
| 15 | 1 | var http = require('http'); |
| 16 | 1 | var https = require('https'); |
| 17 | 1 | var cp = require('child_process'); |
| 18 | 1 | var EventEmitter = require('events').EventEmitter; |
| 19 | | |
| 20 | 1 | exports = module.exports = function mock(obj, key, method) { |
| 21 | 1 | return muk.apply(null, arguments); |
| 22 | | }; |
| 23 | | |
| 24 | 1 | function getCallback(args) { |
| 25 | 17 | var index = args.length - 1; |
| 26 | 17 | var callback = args[index]; |
| 27 | 17 | while (typeof callback !== 'function') { |
| 28 | 5 | index--; |
| 29 | 5 | if (index < 0) { |
| 30 | 1 | break; |
| 31 | | } |
| 32 | 4 | callback = args[index]; |
| 33 | | } |
| 34 | 17 | if (!callback) { |
| 35 | 1 | throw new TypeError('Can\'t find callback function'); |
| 36 | | } |
| 37 | 16 | return callback; |
| 38 | | } |
| 39 | | |
| 40 | | /** |
| 41 | | * Mock async function error. |
| 42 | | * @param {Object} mod, module object |
| 43 | | * @param {String} method, mock module object method name. |
| 44 | | * @param {String|Error} error, error string message or error instance. |
| 45 | | * @param {Number} [tiemout], mock async callback timeout, default is 0. |
| 46 | | */ |
| 47 | 1 | exports.error = function (mod, method, error, timeout) { |
| 48 | 7 | if (!error) { |
| 49 | 1 | error = new Error('mm mock error'); |
| 50 | 1 | error.name = 'MockError'; |
| 51 | | } |
| 52 | 7 | if (typeof error === 'string') { |
| 53 | 5 | error = new Error(error); |
| 54 | 5 | error.name = 'MockError'; |
| 55 | | } |
| 56 | 7 | if (timeout) { |
| 57 | 1 | timeout = parseInt(timeout, 10); |
| 58 | | } |
| 59 | 7 | timeout = timeout || 0; |
| 60 | 7 | muk(mod, method, function () { |
| 61 | 8 | var callback = getCallback(arguments); |
| 62 | 7 | setTimeout(function () { |
| 63 | 7 | callback(error); |
| 64 | | }, timeout); |
| 65 | | }); |
| 66 | 7 | return this; |
| 67 | | }; |
| 68 | | |
| 69 | | /** |
| 70 | | * mock return callback(null, data1, data2). |
| 71 | | * |
| 72 | | * @param {Object} mod, module object |
| 73 | | * @param {String} method, mock module object method name. |
| 74 | | * @param {Array} datas, return datas array. |
| 75 | | * @param {Number} [tiemout], mock async callback timeout, default is 0. |
| 76 | | */ |
| 77 | 1 | exports.datas = function (mod, method, datas, timeout) { |
| 78 | 8 | if (timeout) { |
| 79 | 1 | timeout = parseInt(timeout, 10); |
| 80 | | } |
| 81 | 8 | timeout = timeout || 0; |
| 82 | 8 | if (!Array.isArray(datas)) { |
| 83 | 2 | datas = [ datas ]; |
| 84 | | } |
| 85 | 8 | muk(mod, method, function () { |
| 86 | 9 | var callback = getCallback(arguments); |
| 87 | 9 | setTimeout(function () { |
| 88 | 9 | callback.apply(null, [null].concat(datas)); |
| 89 | | }, timeout); |
| 90 | | }); |
| 91 | 8 | return this; |
| 92 | | }; |
| 93 | | |
| 94 | | /** |
| 95 | | * mock return callback(null, data). |
| 96 | | * |
| 97 | | * @param {Object} mod, module object |
| 98 | | * @param {String} method, mock module object method name. |
| 99 | | * @param {Object} data, return data. |
| 100 | | * @param {Number} [tiemout], mock async callback timeout, default is 0. |
| 101 | | */ |
| 102 | 1 | exports.data = function (mod, method, data, timeout) { |
| 103 | 3 | return exports.datas(mod, method, [ data ], timeout); |
| 104 | | }; |
| 105 | | |
| 106 | | /** |
| 107 | | * mock return callback(null, null). |
| 108 | | * |
| 109 | | * @param {Object} mod, module object |
| 110 | | * @param {String} method, mock module object method name. |
| 111 | | * @param {Number} [tiemout], mock async callback timeout, default is 0. |
| 112 | | */ |
| 113 | 1 | exports.empty = function (mod, method, timeout) { |
| 114 | 1 | return exports.datas(mod, method, null, timeout); |
| 115 | | }; |
| 116 | | |
| 117 | 1 | exports.http = {}; |
| 118 | 1 | exports.https = {}; |
| 119 | | |
| 120 | 1 | http.__sourceRequest = http.request; |
| 121 | 1 | https.__sourceRequest = https.request; |
| 122 | | |
| 123 | 1 | function matchURL(options, url) { |
| 124 | 24 | var pathname = options.path || options.pathname; |
| 125 | 24 | var match = false; |
| 126 | 24 | if (pathname) { |
| 127 | 24 | if (typeof url === 'string') { |
| 128 | 14 | match = pathname === url; |
| 129 | | } else { |
| 130 | 10 | match = url.test(pathname); |
| 131 | | } |
| 132 | | } |
| 133 | 24 | return match; |
| 134 | | } |
| 135 | | |
| 136 | 1 | function mockRequest() { |
| 137 | 20 | var req = new EventEmitter(); |
| 138 | 20 | req.write = function () {}; |
| 139 | 20 | req.end = function () {}; |
| 140 | 20 | req.abort = function () { |
| 141 | 6 | req._aborted = true; |
| 142 | 6 | process.nextTick(function () { |
| 143 | 6 | var err = new Error('socket hang up'); |
| 144 | 6 | err.code = 'ECONNRESET'; |
| 145 | 6 | req.emit('error', err); |
| 146 | | }); |
| 147 | | }; |
| 148 | 20 | return req; |
| 149 | | } |
| 150 | | |
| 151 | | /** |
| 152 | | * Mock http.request(). |
| 153 | | * @param {String|RegExp} url, request url path. |
| 154 | | * @param {String|Buffer} data, mock response data. |
| 155 | | * If data is Array, then res will emit `data` event many times. |
| 156 | | * @param {Object} headers, mock response headers. |
| 157 | | * @param {Number} [delay], response delay time, default is 0. |
| 158 | | */ |
| 159 | 1 | exports.http.request = function (url, data, headers, delay) { |
| 160 | 5 | return _request.call(this, http, url, data, headers, delay); |
| 161 | | }; |
| 162 | | |
| 163 | | /** |
| 164 | | * Mock https.request(). |
| 165 | | * @param {String|RegExp} url, request url path. |
| 166 | | * @param {String|Buffer} data, mock response data. |
| 167 | | * If data is Array, then res will emit `data` event many times. |
| 168 | | * @param {Object} headers, mock response headers. |
| 169 | | * @param {Number} [delay], response delay time, default is 0. |
| 170 | | */ |
| 171 | 1 | exports.https.request = function (url, data, headers, delay) { |
| 172 | 5 | return _request.call(this, https, url, data, headers, delay); |
| 173 | | }; |
| 174 | | |
| 175 | 1 | function _request(mod, url, data, headers, delay) { |
| 176 | 10 | headers = headers || {}; |
| 177 | 10 | if (delay) { |
| 178 | 6 | delay = parseInt(delay, 10); |
| 179 | | } |
| 180 | 10 | delay = delay || 0; |
| 181 | 10 | mod.request = function (options, callback) { |
| 182 | 14 | var datas = []; |
| 183 | 14 | if (!Array.isArray(data)) { |
| 184 | 12 | datas = [data]; |
| 185 | | } else { |
| 186 | 2 | for (var i = 0; i < data.length; i++) { |
| 187 | 4 | datas.push(data[i]); |
| 188 | | } |
| 189 | | } |
| 190 | | |
| 191 | 14 | var match = matchURL(options, url); |
| 192 | 14 | if (!match) { |
| 193 | 2 | return mod.__sourceRequest(options, callback); |
| 194 | | } |
| 195 | | |
| 196 | 12 | var req = mockRequest(); |
| 197 | | |
| 198 | 12 | if (callback) { |
| 199 | 10 | req.on('response', callback); |
| 200 | | } |
| 201 | | |
| 202 | 12 | var res = new EventEmitter(); |
| 203 | 12 | res.statusCode = headers.statusCode || 200; |
| 204 | 12 | res.setEncoding = function (charset) { |
| 205 | 2 | res.charset = charset; |
| 206 | | }; |
| 207 | 12 | res.headers = headers; |
| 208 | 12 | var ondata = function () { |
| 209 | 22 | var chunk = datas.shift(); |
| 210 | 22 | if (!chunk) { |
| 211 | 10 | if (!req._aborted) { |
| 212 | 10 | res.emit('end'); |
| 213 | | } |
| 214 | 10 | return; |
| 215 | | } |
| 216 | | |
| 217 | 12 | if (!req._aborted) { |
| 218 | 12 | if (typeof chunk === 'string') { |
| 219 | 12 | chunk = new Buffer(chunk); |
| 220 | | } |
| 221 | 12 | if (res.charset) { |
| 222 | 4 | chunk = chunk.toString(res.charset); |
| 223 | | } |
| 224 | 12 | res.emit('data', chunk); |
| 225 | | } |
| 226 | 12 | process.nextTick(ondata); |
| 227 | | }; |
| 228 | | |
| 229 | 12 | setTimeout(function () { |
| 230 | 12 | if (!req._aborted) { |
| 231 | 10 | req.emit('response', res); |
| 232 | 10 | process.nextTick(ondata); |
| 233 | | } |
| 234 | | }, delay); |
| 235 | | |
| 236 | 12 | return req; |
| 237 | | }; |
| 238 | 10 | return this; |
| 239 | | } |
| 240 | | |
| 241 | | /** |
| 242 | | * Mock http.request() error. |
| 243 | | * @param {String|RegExp} url, request url path. |
| 244 | | * @param {String|Error} reqError, request error. |
| 245 | | * @param {String|Error} resError, response error. |
| 246 | | * @param {Number} [delay], request error delay time, default is 0. |
| 247 | | */ |
| 248 | 1 | exports.http.requestError = function (url, reqError, resError, delay) { |
| 249 | 4 | _requestError.call(this, http, url, reqError, resError, delay); |
| 250 | | }; |
| 251 | | |
| 252 | | /** |
| 253 | | * Mock https.request() error. |
| 254 | | * @param {String|RegExp} url, request url path. |
| 255 | | * @param {String|Error} reqError, request error. |
| 256 | | * @param {String|Error} resError, response error. |
| 257 | | * @param {Number} [delay], request error delay time, default is 0. |
| 258 | | */ |
| 259 | 1 | exports.https.requestError = function (url, reqError, resError, delay) { |
| 260 | 4 | _requestError.call(this, https, url, reqError, resError, delay); |
| 261 | | }; |
| 262 | | |
| 263 | 1 | function _requestError(mod, url, reqError, resError, delay) { |
| 264 | 8 | if (delay) { |
| 265 | 4 | delay = parseInt(delay, 10); |
| 266 | | } |
| 267 | 8 | delay = delay || 0; |
| 268 | 8 | if (reqError && typeof reqError === 'string') { |
| 269 | 2 | reqError = new Error(reqError); |
| 270 | 2 | reqError.name = 'MockHttpRequestError'; |
| 271 | | } |
| 272 | 8 | if (resError && typeof resError === 'string') { |
| 273 | 6 | resError = new Error(resError); |
| 274 | 6 | resError.name = 'MockHttpResponseError'; |
| 275 | | } |
| 276 | 8 | mod.request = function (options, callback) { |
| 277 | 10 | var match = matchURL(options, url); |
| 278 | 10 | if (!match) { |
| 279 | 2 | return mod.__sourceRequest(options, callback); |
| 280 | | } |
| 281 | | |
| 282 | 8 | var req = mockRequest(); |
| 283 | | |
| 284 | 8 | if (callback) { |
| 285 | 8 | req.on('response', callback); |
| 286 | | } |
| 287 | | |
| 288 | 8 | setTimeout(function () { |
| 289 | 6 | if (reqError) { |
| 290 | 2 | return req.emit('error', reqError); |
| 291 | | } |
| 292 | | |
| 293 | 4 | var res = new EventEmitter(); |
| 294 | 4 | res.statusCode = 200; |
| 295 | 4 | res.headers = { |
| 296 | | server: 'MockMateServer' |
| 297 | | }; |
| 298 | 4 | process.nextTick(function () { |
| 299 | 4 | if (!req._aborted) { |
| 300 | 4 | req.emit('error', resError); |
| 301 | | } |
| 302 | | }); |
| 303 | 4 | if (!req._aborted) { |
| 304 | 4 | req.emit('response', res); |
| 305 | | } |
| 306 | | }, delay); |
| 307 | | |
| 308 | 8 | return req; |
| 309 | | }; |
| 310 | 8 | return this; |
| 311 | | } |
| 312 | | |
| 313 | | /** |
| 314 | | * mock child_process spawn |
| 315 | | * @param {Integer} code exit code |
| 316 | | * @param {String} stdout |
| 317 | | * @param {String} stderr |
| 318 | | * @param {Integer} timeout stdout/stderr/close event emit timeout |
| 319 | | */ |
| 320 | 1 | exports.spawn = function (code, stdout, stderr, timeout) { |
| 321 | 1 | var evt = new EventEmitter(); |
| 322 | 1 | muk(cp, 'spawn', function () { |
| 323 | 1 | return evt; |
| 324 | | }); |
| 325 | 1 | setTimeout(function () { |
| 326 | 1 | stdout && evt.emit('stdout', stdout); |
| 327 | 1 | stderr && evt.emit('stderr', stderr); |
| 328 | 1 | evt.emit('close', code); |
| 329 | 1 | evt.emit('exit', code); |
| 330 | | }, timeout); |
| 331 | | }; |
| 332 | | |
| 333 | | /** |
| 334 | | * remove all mock effects. |
| 335 | | */ |
| 336 | 1 | exports.restore = function () { |
| 337 | 37 | http.request = http.__sourceRequest; |
| 338 | 37 | https.request = https.__sourceRequest; |
| 339 | 37 | muk.restore(); |
| 340 | 37 | return this; |
| 341 | | }; |