UNPKG

http-request-mock

Version:

Intercept & mock http requests issued by XMLHttpRequest, fetch, nodejs https/http module, axios, jquery, superagent, ky, node-fetch, request, got or any other request libraries by intercepting XMLHttpRequest, fetch and nodejs native requests in low level.

529 lines 24.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable @typescript-eslint/ban-types */ var http_1 = __importDefault(require("http")); var net_1 = require("net"); var util_1 = require("util"); var bypass_1 = __importDefault(require("../../common/bypass")); var request_1 = __importStar(require("../../common/request")); var utils_1 = require("../../common/utils"); var config_1 = require("../../config"); /** * ClientRequest constructor * @param {string} url * @param {object} options options of http.get, https.get, http.request or https.request method. * @param {function} callback callback of http.get, https.get, http.request or https.request method. */ function ClientRequest(url, options, callback) { var _this = this; // http.OutgoingMessage serves as the parent class of http.ClientRequest and http.ServerResponse. // It is an abstract of outgoing message from the perspective of the participants of HTTP transaction. http_1.default.OutgoingMessage.call(this); this.requestBody = Buffer.alloc(0); this.url = url; this.options = options; this.callback = callback; this.nativeInstance = null; /** * Initialize socket & response object */ this.init = function () { var _a, _b, _c; var _d = [_this.options, _this.callback], options = _d[0], callback = _d[1]; _this.method = options.method || 'GET'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore _this.path = options.path || '/'; // The optional callback parameter will be added as // a one-time listener for the 'response' event. if (callback) { _this.once('response', callback); } // outgoingMessage.headersSent if (!_this.headersSent && options.headers) { for (var key in options.headers) { _this.setHeader(key, options.headers[key]); } } // make an empty socket var emptySocket = new net_1.Socket(); Object.assign(_this, { socket: emptySocket, connection: emptySocket, }); if (/^https/i.test(_this.url)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore _this.socket.authorized = true; } if (options.timeout) { _this.setTimeout(options.timeout); (_a = _this.socket) === null || _a === void 0 ? void 0 : _a.setTimeout(options.timeout); } if (((_b = options.headers) === null || _b === void 0 ? void 0 : _b.expect) === '100-continue') { _this.emit('continue'); } _this.response = new http_1.default.IncomingMessage(_this.socket); _this.emit('socket', _this.socket); (_c = _this.socket) === null || _c === void 0 ? void 0 : _c.emit('connect'); }; this.init(); /** * Set mock item resolver. 'mockItemResolver' will be used in end method.` * @param {Promise<MockItem>} mockItemResolver */ this.setMockItemResolver = function (mockItemResolver) { _this.mockItemResolver = mockItemResolver; return _this; }; this.setOriginalRequestInfo = function (getOrRequest, nativeReqestMethod, nativeRequestArgs) { _this.nativeReqestName = getOrRequest; // get or request _this.nativeReqestMethod = nativeReqestMethod; _this.nativeRequestArgs = nativeRequestArgs; }; /** * Destroy the request. Optionally emit an 'error' event, and emit a 'close' event. * Calling this will cause remaining data in the response to be dropped and the socket to be destroyed. */ this.destroy = function () { if (_this.aborted || _this.destroyed) return _this; _this.aborted = true; _this.destroyed = true; _this.response.emit('close', __assign(__assign({}, new Error()), { code: 'aborted' })); // socket.destroy() _this.emit('abort'); return _this; }; /** * We keep abort method for compatibility. * 'abort' has been Deprecated; Use request.destroy() instead. */ this.abort = function () { _this.destroy(); return _this; }; /** * Send error event to the request. * @param {string} msg */ this.sendError = function (msg) { process.nextTick(function () { _this.emit('error', new Error(msg)); }); }; /** * Sends a chunk of the body. This method can be called multiple times. * Simulation: request.write(chunk[, encoding][, callback]) * @param {string | Buffer} chunk * @param {unknown[]} args */ this.write = function (chunk) { var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) { _this.sendError('The first argument must be of type string or an instance of Buffer.'); return false; } var callback = typeof args[1] === 'function' ? args[1] : args[2]; if (_this.aborted || _this.destroyed) { _this.sendError('The request has been aborted.'); } else { if (chunk.length) { var buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); _this.requestBody = Buffer.concat([_this.requestBody, buf]); } // The callback argument is optional and will be called when this // chunk of data is flushed, but only if the chunk is non-empty. if (chunk.length && typeof callback === 'function') { callback(); } } setTimeout(function () { return _this.emit('drain'); }, 1); return false; }; /** * https://nodejs.org/api/http.html#http_request_end_data_encodingcallback * * Finishes sending the request. If any parts of the body are unsent, it will flush them to the stream. * Simulation: request.end([data[, encoding]][, callback]) * @param {unknown[]} args */ this.end = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var _a = _this.getEndArguments(args), data = _a[0], encoding = _a[1], callback = _a[2]; // If data is specified, it is equivalent to calling // request.write(data, encoding) followed by request.end(callback). if (data) { _this.write(data, encoding); _this.end(callback); return _this; } if (!_this.response.complete) { _this.sendResponseResult.apply(_this, __spreadArray([callback], args, false)); return _this; } _this.sendEndingEvent(callback); return _this; }; /** * It awaits mock item resolver & set response result. */ this.sendResponseResult = function (endCallback) { var endArgs = []; for (var _i = 1; _i < arguments.length; _i++) { endArgs[_i - 1] = arguments[_i]; } var now = Date.now(); _this.mockItemResolver(function (mockItem, mocker) { return __awaiter(_this, void 0, void 0, function () { var method, requestInfo, remoteResponse, remoteInfo, _a, body, json, response, err_1; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: method = this.options.method || 'GET'; requestInfo = { url: this.url, method: method, query: (0, utils_1.getQuery)(this.url), headers: this.getRemoteRequestHeaders(), body: method === 'GET' ? undefined : this.bufferToString(this.requestBody) }; requestInfo.doOriginalCall = function () { return __awaiter(_this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.getOriginalResponse()]; case 1: res = _a.sent(); requestInfo.doOriginalCall = undefined; return [2 /*return*/, res]; } }); }); }; remoteResponse = null; remoteInfo = mockItem === null || mockItem === void 0 ? void 0 : mockItem.getRemoteInfo(url); if (!remoteInfo) return [3 /*break*/, 4]; _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, (0, request_1.default)({ url: remoteInfo.url, method: remoteInfo.method || this.options.method || 'GET', headers: __assign(__assign({}, requestInfo.headers), mockItem.remoteRequestHeaders), body: this.requestBody })]; case 2: _a = _b.sent(), body = _a.body, json = _a.json, response = _a.response; remoteResponse = { status: response.statusCode, headers: response.headers, response: json || body, responseText: body, responseJson: json, }; return [3 /*break*/, 4]; case 3: err_1 = _b.sent(); this.sendError('Get remote result error: ' + err_1.message); return [2 /*return*/, false]; case 4: mockItem.sendBody(requestInfo, remoteResponse).then(function (responseBody) { if (responseBody instanceof bypass_1.default) { if (remoteResponse) { throw new Error('[http-request-mock] A request which is marked by @remote tag cannot be bypassed.'); } return _this.fallbackToNativeRequest.apply(_this, endArgs); } var spent = Date.now() - now; mocker.sendResponseLog(spent, responseBody, requestInfo, mockItem); _this.response.statusCode = mockItem.status; _this.response.statusMessage = config_1.HTTPStatusCodes[_this.response.statusCode] || '', _this.response.headers = __assign(__assign(__assign({}, mockItem.headers), ((remoteResponse === null || remoteResponse === void 0 ? void 0 : remoteResponse.headers) || {})), { 'x-powered-by': 'http-request-mock' }); _this.response.rawHeaders = Object.entries(_this.response.headers).reduce(function (res, item) { return res.concat(item); }, []); // push: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. if (typeof responseBody === 'string' || (responseBody instanceof Buffer) || (responseBody instanceof ArrayBuffer) || (responseBody instanceof SharedArrayBuffer) || (responseBody instanceof Uint8Array)) { _this.response.push(Buffer.from(responseBody)); } else { _this.response.push(JSON.stringify(responseBody)); } _this.sendEndingEvent(endCallback); }).catch(function (err) { console.warn('[http-request-mock] mock response error, ' + err.message); _this.response.statusCode = mockItem.status; _this.response.statusMessage = config_1.HTTPStatusCodes[_this.response.statusCode] || '', _this.response.headers = __assign(__assign({}, mockItem.headers), { 'x-powered-by': 'http-request-mock' }); _this.response.rawHeaders = Object.entries(_this.response.headers).reduce(function (res, item) { return res.concat(item); }, []); var responseBody = ''; _this.response.push(Buffer.from(responseBody)); mocker.sendResponseLog(Date.now() - now, responseBody, requestInfo, mockItem); _this.sendEndingEvent(endCallback); }); return [2 /*return*/]; } }); }); }); }; /** * Send completed event. */ this.sendEndingEvent = function (callback) { if (typeof callback === 'function') { callback(); } _this.finished = true; // We keep the finish property for compatibility. _this.emit('finish'); _this.emit('response', _this.response); // The message.complete property will be true if a complete // HTTP message has been received and successfully parsed. _this.response.push(null); _this.response.complete = true; return _this; }; this.fallbackToNativeRequest = function () { var _a; var endArgs = []; for (var _i = 0; _i < arguments.length; _i++) { endArgs[_i] = arguments[_i]; } _this.nativeInstance = _this.nativeReqestMethod.apply(_this, _this.nativeRequestArgs); Object.entries(_this.getHeaders()).forEach(function (entry) { if (entry[1] !== null && entry[1] !== undefined) { _this.nativeInstance && _this.nativeInstance.setHeader(entry[0], entry[1]); } }); if (_this.requestBody.length) { _this.nativeInstance && _this.nativeInstance.write(_this.requestBody); } if (_this.nativeInstance) { _this.nativeInstance.on('connect', function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return _this.emit.apply(_this, __spreadArray(['connect'], args, false)); }); _this.nativeInstance.on('finish', function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return _this.emit.apply(_this, __spreadArray(['finish'], args, false)); }); _this.nativeInstance.on('abort', function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return _this.emit.apply(_this, __spreadArray(['abort'], args, false)); }); _this.nativeInstance.on('error', function (error) { return _this.emit('error', error); }); _this.nativeInstance.on('information', function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return _this.emit.apply(_this, __spreadArray(['information'], args, false)); }); _this.nativeInstance.on('response', function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return _this.emit.apply(_this, __spreadArray(['response'], args, false)); }); _this.nativeInstance.on('timeout', function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return _this.emit.apply(_this, __spreadArray(['timeout'], args, false)); }); (_a = _this.nativeInstance).end.apply(_a, endArgs); } return _this.nativeInstance; }; this.getOriginalResponse = function () { var callback = _this.nativeRequestArgs[_this.nativeRequestArgs.length - 1]; var defaultResponse = { status: null, headers: {}, responseText: null, responseJson: null, responseBuffer: null, responseBlob: null, error: null, }; return new Promise(function (resolve) { var newCallback = function (res) { (0, request_1.parseResponseBody)(res).then(function (data) { resolve(data); }).catch(function (err) { resolve(__assign(__assign({}, defaultResponse), { error: err })); }); if (typeof callback === 'function') { callback(res); } }; var callbackIndex = typeof callback === 'function' ? _this.nativeRequestArgs.length - 1 : _this.nativeRequestArgs.length; _this.nativeRequestArgs[callbackIndex] = newCallback; // do original call var req = _this.nativeReqestMethod.apply(_this, _this.nativeRequestArgs); req.on('error', function (err) { resolve(__assign(__assign({}, defaultResponse), { error: err })); }); if (_this.nativeReqestName = 'get') { req.end(); } }); }; /** * https://nodejs.org/api/http.html#http_request_end_data_encodingcallback * * Get arguments of end method. * @param {unknown[]} args [data[, encoding]][, callback] * @returns */ this.getEndArguments = function (args) { var data; var encoding; var callback; if (args.length === 3) { data = args[0], encoding = args[1], callback = args[2]; } else if (args.length === 2) { data = args[0], encoding = args[1]; } else if (args.length === 1) { data = typeof args[0] === 'function' ? undefined : args[0]; callback = typeof args[0] === 'function' ? args[0] : undefined; } return [data, encoding, callback]; }; /** * Convert a buffer to a string. * @param {Buffer} buffer */ this.bufferToString = function (buffer) { var str = buffer.toString('utf8'); return Buffer.from(str).equals(buffer) ? str : buffer.toString('hex'); }; /** * Get request headers. */ this.getRemoteRequestHeaders = function () { return Object.entries(__assign(__assign({}, _this.getHeaders()), _this.options.headers)).reduce(function (res, _a) { var key = _a[0], val = _a[1]; if (val !== undefined && val !== null) { res[key.toLowerCase()] = Array.isArray(val) ? val.join('; ') : (val + ''); } return res; }, {}); }; } // Note: 'class extends' is not work here. // It'll trigger a default socket connection that we don't expect. (0, util_1.inherits)(ClientRequest, http_1.default.ClientRequest); exports.default = ClientRequest; //# sourceMappingURL=client-request.js.map