replay
Version:
When API testing slows you down: record and replay HTTP responses like a boss
170 lines (145 loc) • 5.52 kB
JavaScript
'use strict';
var _getIterator2 = require('babel-runtime/core-js/get-iterator');
var _getIterator3 = _interopRequireDefault(_getIterator2);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// A matcher is a function that, given a request, returns an appropriate response or nothing.
//
// The most common use case is to calling `Matcher.fromMapping(mapping)`.
//
// The request consists of:
// url - URL object
// method - Request method (lower case)
// headers - Headers object (names are lower case)
// body - Request body (for some requests)
//
// The response consists of:
// version - HTTP version
// status - Status code
// headers - Headers object (names are lower case)
// body - Array of body parts
// trailers - Trailers object (names are lower case)
const assert = require('assert');
const URL = require('url');
const jsStringEscape = require('js-string-escape');
// Simple implementation of a matcher.
//
// To create a matcher from request/response mapping use `fromMapping`.
module.exports = class Matcher {
constructor(request, response) {
// Map requests to object properties. We do this for quick matching.
assert(request.url || request.regexp, 'I need at least a URL to match request to response');
if (request.regexp) {
this.hostname = request.hostname;
this.regexp = request.regexp;
} else {
const url = URL.parse(request.url);
this.hostname = url.hostname;
this.port = url.port;
this.path = url.path;
}
this.method = request.method && request.method.toUpperCase() || 'GET';
this.headers = {};
if (request.headers) for (let name in request.headers) {
let value = request.headers[name];
this.headers[name.toLowerCase()] = value;
}
this.body = request.body;
// Create a normalized response object that we return.
this.response = {
version: response.version || '1.1',
statusCode: response.statusCode && parseInt(response.statusCode, 10) || 200,
statusMessage: response.statusMessage || '',
headers: {},
body: response.body ? response.body.slice(0) : [],
trailers: {}
};
// Copy over header to response, downcase header names.
if (response.headers) {
const headers = this.response.headers;
for (let name in response.headers) {
let value = response.headers[name];
headers[name.toLowerCase()] = value;
}
}
// Copy over trailers to response, downcase trailers names.
if (response.trailers) {
const trailers = this.response.trailers;
for (let name in response.trailers) {
let value = response.trailers[name];
trailers[name.toLowerCase()] = value;
}
}
}
// Quick and effective matching.
match(request) {
const url = request.url,
method = request.method,
headers = request.headers,
body = request.body;
if (this.hostname && this.hostname !== url.hostname) return false;
if (this.regexp) {
if (!this.regexp.test(url.path)) return false;
} else {
if (this.port && this.port !== url.port) return false;
if (this.path && this.path !== url.path) return false;
}
if (this.method !== method) return false;
for (let name in this.headers) if (this.headers[name] !== headers[name]) return false;
if (this.body && body) {
let data = '';
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = (0, _getIterator3.default)(body), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
let chunks = _step.value;
data += chunks[0];
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
data = jsStringEscape(data);
return this.body instanceof RegExp ? this.body.test(data) : this.body === data;
}
return true;
}
// Returns new matcher function based on the supplied mapping.
//
// Mapping can contain `request` and `response` object. As shortcut, mapping can specify `path` and `method` (optional)
// directly, and also any of the response properties.
static fromMapping(host, mapping) {
assert(!!mapping.path ^ !!mapping.request, 'Mapping must specify path or request object');
let matchingRequest;
if (mapping.path) matchingRequest = {
url: URL.resolve(`http://${host}/`, mapping.path),
method: mapping.method
};else if (mapping.request.url instanceof RegExp) matchingRequest = {
host: host,
regexp: mapping.request.url,
method: mapping.request.method,
headers: mapping.request.headers,
body: mapping.request.body
};else matchingRequest = {
url: URL.resolve(`http://${host}`, mapping.request.url),
method: mapping.request.method,
headers: mapping.request.headers,
body: mapping.request.body
};
const matcher = new Matcher(matchingRequest, mapping.response || {});
return function (request) {
if (matcher.match(request)) return matcher.response;
};
}
};
//# sourceMappingURL=matcher.js.map