UNPKG

replay

Version:

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

282 lines (229 loc) 8.61 kB
'use strict'; var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _set = require('babel-runtime/core-js/set'); var _set2 = _interopRequireDefault(_set); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // The Replay module holds global configution properties and methods. const Catalog = require('./catalog'); const Chain = require('./chain'); const debug = require('./debug'); var _require = require('events'); const EventEmitter = _require.EventEmitter; const logger = require('./logger'); const passThrough = require('./pass_through'); const recorder = require('./recorder'); // Supported modes const MODES = [ // Allow outbound HTTP requests, don't replay anything. Use this to test your // code against changes to 3rd party API. 'bloody', // Allow outbound HTTP requests, replay captured responses. This mode is // particularly useful when new code makes new requests, but unstable yet and // you don't want these requests saved. 'cheat', // Allow outbound HTTP requests, capture responses for future replay. This // mode allows you to capture and record new requests, e.g. when adding tests // or making code changes. 'record', // Do not allow outbound HTTP requests, replay captured responses. This is // the default mode and the one most useful for running tests 'replay']; // This is the standard mode for running tests const DEFAULT_MODE = 'replay'; // Headers that are recorded/matched during replay. const MATCH_HEADERS = [/^accept/, /^authorization/, /^body/, /^content-type/, /^host/, /^if-/, /^x-/]; // Instance properties: // // catalog - The catalog is responsible for loading pre-recorded responses // into memory, from where they can be replayed, and storing captured responses. // // chain - The proxy chain. Essentially an array of handlers through which // each request goes, and concludes when the last handler returns a // response. // // headers - Only these headers are matched when recording/replaying. A list // of regular expressions. // // fixtures - Main directory for replay fixtures. // // mode - The mode we're running in, see MODES. class Replay extends EventEmitter { constructor(mode) { if (!~MODES.indexOf(mode)) throw new Error(`Unsupported mode '${mode}', must be one of ${MODES.join(', ')}.`); super(); this.mode = mode; this.chain = new Chain(); // Localhost servers: pass request to localhost this._localhosts = new _set2.default(['localhost', '127.0.0.1', '::1']); // Pass through requests to these servers this._passThrough = new _set2.default(); // Dropp connections to these servers this._dropped = new _set2.default(); this.catalog = new Catalog(this); this.headers = MATCH_HEADERS; // Automatically emit connection errors and such, also prevent process from crashing this.on('error', function (error) { debug(`Replay: ${error.message || error}`); }); } // Addes a proxy to the beginning of the processing chain, so it executes ahead of any existing proxy. // // Example // replay.use(replay.logger()) use(proxy) { this.chain.prepend(proxy); return this; } // Pass through all requests to these hosts passThrough(...hosts) { this.reset(...hosts); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = (0, _getIterator3.default)(hosts), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { let host = _step.value; this._passThrough.add(host); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return this; } // True to pass through requests to this host isPassThrough(host) { const domain = host.replace(/^[^.]+/, '*'); return !!(this._passThrough.has(host) || this._passThrough.has(domain) || this._passThrough.has(`*.${host}`)); } // Do not allow network access to these hosts (drop connection) drop(...hosts) { this.reset(...hosts); var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = (0, _getIterator3.default)(hosts), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { let host = _step2.value; this._dropped.add(host); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } return this; } // True if this host is on the dropped list isDropped(host) { const domain = host.replace(/^[^.]+/, '*'); return !!(this._dropped.has(host) || this._dropped.has(domain) || this._dropped.has(`*.${host}`)); } // Treats this host as localhost: requests are routed directly to 127.0.0.1, no // replay. Useful when you want to send requests to the test server using its // production host name. localhost(...hosts) { this.reset(...hosts); var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = (0, _getIterator3.default)(hosts), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { let host = _step3.value; this._localhosts.add(host); } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return this; } // True if this host should be treated as localhost. isLocalhost(host) { const domain = host.replace(/^[^.]+/, '*'); return !!(this._localhosts.has(host) || this._localhosts.has(domain) || this._localhosts.has(`*.${host}`)); } // Use this when you want to exclude host from dropped/pass-through/localhost reset(...hosts) { var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = (0, _getIterator3.default)(hosts), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { let host = _step4.value; this._localhosts.delete(host); this._passThrough.delete(host); this._dropped.delete(host); } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } return this; } get fixtures() { return this.catalog.getFixturesDir(); } set fixtures(dir) { // Clears loaded fixtures, and updates to new dir this.catalog.setFixturesDir(dir); } } const replay = new Replay(process.env.REPLAY || DEFAULT_MODE); function passWhenBloodyOrCheat(request) { return replay.isPassThrough(request.url.hostname) || replay.mode === 'cheat' && !replay.isDropped(request.url.hostname); } function passToLocalhost(request) { return replay.isLocalhost(request.url.hostname) || replay.mode === 'bloody'; } // The default processing chain (from first to last): // - Pass through requests to localhost // - Log request to console is `debug` is true // - Replay recorded responses // - Pass through requests in bloody and cheat modes replay.use(passThrough(passWhenBloodyOrCheat)).use(recorder(replay)).use(logger(replay)).use(passThrough(passToLocalhost)); module.exports = replay; // These must come last since they need module.exports to exist require('./patch_http_request'); require('./patch_dns_lookup'); //# sourceMappingURL=index.js.map