UNPKG

replay

Version:

When API testing slows you down: record and replay HTTP responses like a boss

452 lines (371 loc) 14.3 kB
'use strict'; var _setImmediate2 = require('babel-runtime/core-js/set-immediate'); var _setImmediate3 = _interopRequireDefault(_setImmediate2); var _toArray2 = require('babel-runtime/helpers/toArray'); var _toArray3 = _interopRequireDefault(_toArray2); var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray'); var _slicedToArray3 = _interopRequireDefault(_slicedToArray2); var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _create = require('babel-runtime/core-js/object/create'); var _create2 = _interopRequireDefault(_create); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const assert = require('assert'); const debug = require('./debug'); const File = require('fs'); const Path = require('path'); const Matcher = require('./matcher'); const jsStringEscape = require('js-string-escape'); const NEW_RESPONSE_FORMAT = /HTTP\/(\d\.\d)\s+(\d{3})\s*(.*)/; const OLD_RESPONSE_FORMAT = /(\d{3})\s+HTTP\/(\d\.\d)/; function mkpathSync(pathname) { if (File.existsSync(pathname)) return; const parent = Path.dirname(pathname); if (File.existsSync(parent)) File.mkdirSync(pathname);else { mkpathSync(parent); File.mkdirSync(pathname); } } // Parse headers from headerLines. Optional argument `only` is an array of // regular expressions; only headers matching one of these expressions are // parsed. Returns a object with name/value pairs. function parseHeaders(filename, headerLines, only = null) { const headers = (0, _create2.default)(null); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = (0, _getIterator3.default)(headerLines), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { let line = _step.value; if (line === '') continue; var _line$match$slice = line.match(/^(.*?)\:\s+(.*)$/).slice(1), _line$match$slice2 = (0, _slicedToArray3.default)(_line$match$slice, 2); let name = _line$match$slice2[0], value = _line$match$slice2[1]; if (only && !match(name, only)) continue; const key = (name || '').toLowerCase(); value = (value || '').trim().replace(/^"(.*)"$/, '$1'); if (Array.isArray(headers[key])) headers[key].push(value);else if (headers[key]) headers[key] = [headers[key], value];else headers[key] = value; } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return headers; } function parseRequest(filename, request, requestHeaders) { function parseRegexp(rawRegexp) { var _rawRegexp$match$slic = rawRegexp.match(/^\/(.+)\/(i|m|g)?$/).slice(1), _rawRegexp$match$slic2 = (0, _slicedToArray3.default)(_rawRegexp$match$slic, 2); const inRegexp = _rawRegexp$match$slic2[0], flags = _rawRegexp$match$slic2[1]; return new RegExp(inRegexp, flags || ''); } assert(request, `${filename} missing request section`); var _request$split = request.split(/\n/), _request$split2 = (0, _toArray3.default)(_request$split); const methodAndPath = _request$split2[0], headerLines = _request$split2.slice(1); let method; let path; let rawRegexp; let regexp; if (/\sREGEXP\s/.test(methodAndPath)) { var _methodAndPath$split = methodAndPath.split(' REGEXP '); var _methodAndPath$split2 = (0, _slicedToArray3.default)(_methodAndPath$split, 2); method = _methodAndPath$split2[0]; rawRegexp = _methodAndPath$split2[1]; regexp = parseRegexp(rawRegexp); } else { ; var _methodAndPath$split3 = methodAndPath.split(/\s/); var _methodAndPath$split4 = (0, _slicedToArray3.default)(_methodAndPath$split3, 2); method = _methodAndPath$split4[0]; path = _methodAndPath$split4[1]; }assert(method && (path || regexp), `${filename}: first line must be <method> <path>`); assert(/^[a-zA-Z]+$/.test(method), `${filename}: method not valid`); const headers = parseHeaders(filename, headerLines, requestHeaders); let body = headers.body; delete headers.body; if (body && /^REGEXP\s+/.test(body)) { rawRegexp = body.split(/REGEXP\s+/)[1]; body = parseRegexp(rawRegexp); } const url = path || regexp; return { url, method, headers, body }; } function parseResponse(filename, response, body) { if (response) { var _response$split = response.split(/\n/), _response$split2 = (0, _toArray3.default)(_response$split); const statusLine = _response$split2[0], headerLines = _response$split2.slice(1); var _statusComponents = statusComponents(statusLine); const version = _statusComponents.version, statusCode = _statusComponents.statusCode, statusMessage = _statusComponents.statusMessage; const headers = parseHeaders(filename, headerLines); const rawHeaders = headerLines.reduce(function (raw, header) { var _header$split = header.split(/:\s+/), _header$split2 = (0, _slicedToArray3.default)(_header$split, 2); const name = _header$split2[0], value = _header$split2[1]; raw.push(name); raw.push(value); return raw; }, []); return { statusCode, statusMessage, version, headers, rawHeaders, body, trailers: {}, rawTrailers: [] }; } } function statusComponents(statusLine) { let response = {}; if (isResponseFormatNew(statusLine)) { const formattedStatus = statusLine.match(NEW_RESPONSE_FORMAT); response.version = formattedStatus[1]; response.statusCode = parseInt(formattedStatus[2], 10); response.statusMessage = formattedStatus[3].trim(); } else { const formattedStatus = statusLine.match(OLD_RESPONSE_FORMAT); response.version = formattedStatus[2]; response.statusCode = parseInt(formattedStatus[0], 10); } return response; } function isResponseFormatNew(statusLine) { return (/^HTTP/.test(statusLine) ); } function readAndInitialParseFile(filename) { const buffer = File.readFileSync(filename); const parts = buffer.toString('utf8').split('\n\n'); if (parts.length > 2) { const parts0 = new Buffer(parts[0], 'utf8'); const parts1 = new Buffer(parts[1], 'utf8'); const body = buffer.slice(parts0.length + parts1.length + 4); return [parts[0], parts[1], body]; } else return [parts[0], parts[1], '']; } // Write headers to the File object. Optional argument `only` is an array of // regular expressions; only headers matching one of these expressions are // written. function writeHeaders(file, headers, only = null) { for (let name in headers) { let value = headers[name]; if (only && !match(name, only)) continue; if (Array.isArray(value)) { var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = (0, _getIterator3.default)(value), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { let item = _step2.value; file.write(`${name}: ${item}\n`); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } else file.write(`${name}: ${value}\n`); } } // Returns true if header name matches one of the regular expressions. function match(name, regexps) { var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = (0, _getIterator3.default)(regexps), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { let regexp = _step3.value; if (regexp.test(name)) return true; } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return false; } module.exports = class Catalog { constructor(settings) { this.settings = settings; // We use this to cache host/host:port mapped to array of matchers. this.matchers = {}; this._basedir = Path.resolve('fixtures'); } getFixturesDir() { return this._basedir; } setFixturesDir(dir) { this._basedir = Path.resolve(dir); this.matchers = {}; } find(host) { // Return result from cache. const matchers = this.matchers[host]; if (matchers) return matchers; // Start by looking for directory and loading each of the files. // Look for host-port (windows friendly) or host:port (legacy) let pathname = `${this.getFixturesDir()}/${host.replace(':', '-')}`; if (!File.existsSync(pathname)) pathname = `${this.getFixturesDir()}/${host}`; if (!File.existsSync(pathname)) return null; const newMatchers = this.matchers[host] || []; this.matchers[host] = newMatchers; const stat = File.statSync(pathname); if (stat.isDirectory()) { let files = File.readdirSync(pathname); // remove dot files from the list files = files.filter(f => !/^\./.test(f)); var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = (0, _getIterator3.default)(files), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { let file = _step4.value; let mapping = this._read(`${pathname}/${file}`); newMatchers.push(Matcher.fromMapping(host, mapping)); } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } } else { const mapping = this._read(pathname); newMatchers.push(Matcher.fromMapping(host, mapping)); } return newMatchers; } save(host, request, response, callback) { const matcher = Matcher.fromMapping(host, { request, response }); const matchers = this.matchers[host] || []; matchers.push(matcher); const requestHeaders = this.settings.headers; const uid = `${Date.now()}${Math.floor(Math.random() * 100000)}`; const tmpfile = `${this.getFixturesDir()}/node-replay.${uid}`; const pathname = `${this.getFixturesDir()}/${host.replace(':', '-')}`; debug(`Creating ${pathname}`); try { mkpathSync(pathname); } catch (error) { (0, _setImmediate3.default)(function () { callback(error); }); return; } const filename = `${pathname}/${uid}`; try { const file = File.createWriteStream(tmpfile, { encoding: 'utf-8' }); file.write(`${request.method.toUpperCase()} ${request.url.path || '/'}\n`); writeHeaders(file, request.headers, requestHeaders); if (request.body) { let body = ''; var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = (0, _getIterator3.default)(request.body), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { let chunks = _step5.value; body += chunks[0]; } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } writeHeaders(file, { body: jsStringEscape(body) }); } file.write('\n'); // Response part file.write(`HTTP/${response.version || '1.1'} ${response.statusCode || 200} ${response.statusMessage}\n`); writeHeaders(file, response.headers); file.write('\n'); var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = (0, _getIterator3.default)(response.body), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { let part = _step6.value; file.write(part[0], part[1]); } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } file.end(function () { File.rename(tmpfile, filename, callback); }); } catch (error) { callback(error); } } _read(filename) { var _readAndInitialParseF = readAndInitialParseFile(filename), _readAndInitialParseF2 = (0, _slicedToArray3.default)(_readAndInitialParseF, 3); const request = _readAndInitialParseF2[0], response = _readAndInitialParseF2[1], part = _readAndInitialParseF2[2]; const body = [[part, undefined]]; return { request: parseRequest(filename, request, this.settings.headers), response: parseResponse(filename, response, body) }; } }; //# sourceMappingURL=catalog.js.map