replay
Version:
When API testing slows you down: record and replay HTTP responses like a boss
452 lines (371 loc) • 14.3 kB
JavaScript
;
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