diff --git a/README.md b/README.md index 7f51401..8a2dab3 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Some systems require you to `sudo` before running services on certain ports (lik ``` stubby [-a ] [-c ] [-d ] [-h] [-k ] [-l ] [-p ] [-q] - [-s ] [-t ] [-v] [-w] + [-s ] [-t ] [-v] [-w] [-g] -a, --admin Port for admin portal. Defaults to 8889. -c, --cert Certificate file. Use with --key. @@ -67,6 +67,7 @@ stubby [-a ] [-c ] [-d ] [-h] [-k ] [-l ] [-p -t, --tls Port for https stubs portal. Defaults to 7443. -v, --version Prints stubby's version number. -w, --watch Auto-reload data file when edits are made. +-g, --debugStubs Prints debug logs for stub matching. ``` When used from the command-line, `stubby` responds to the `SIGHUP` signal to reload its configuration. @@ -671,6 +672,7 @@ What can I do with it, you ask? Read on! * `quiet`: defaults to `true`. Pass in `false` to have console output (if available) * `_httpsOptions`: additional options to pass to the [underlying tls server](http://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener). + * `debugStubs`: prints debug logs for stub matching * `callback`: takes one parameter: the error message (if there is one), undefined otherwise #### start([callback]) diff --git a/src/console/cli.js b/src/console/cli.js index 8028e23..26e41a6 100644 --- a/src/console/cli.js +++ b/src/console/cli.js @@ -44,6 +44,11 @@ var options = [{ name: 'quiet', flag: 'q', description: 'Prevent stubby from printing to the console.' +}, { + name: 'debugStubs', + flag: 'g', + default: false, + description: 'Emit logs to help debug stub matching.' }, { name: 'pfx', flag: 'p', diff --git a/src/console/out.js b/src/console/out.js index a78b824..b1cfee9 100644 --- a/src/console/out.js +++ b/src/console/out.js @@ -13,6 +13,16 @@ var RESET = '\x1B[0m'; var out = { quiet: false, + debugStubs: false, + debugHeader: function (msg) { + if (!this.debugStubs) { return; } + console.log('----- ' + msg.toUpperCase() + ' ------'); + }, + debug: function (msg, header) { + if (!this.debugStubs) { return; } + if (header !== undefined) { console.log('--- ' + header.toUpperCase() + ' ---'); } + console.log(msg); + }, log: function (msg) { if (this.quiet) { return; } console.log(msg); diff --git a/src/main.js b/src/main.js index a946a67..74d86df 100644 --- a/src/main.js +++ b/src/main.js @@ -56,6 +56,8 @@ function setupStartOptions (options, callback) { } if (options.quiet == null) { options.quiet = true; } + if (options.debugStubs == null) { options.debugStubs = false; } + options.debugStubs = !options.quiet && options.debugStubs; defaults = CLI.getArgs([]); for (key in defaults) { @@ -65,6 +67,7 @@ function setupStartOptions (options, callback) { } out.quiet = options.quiet; + out.debugStubs = options.debugStubs; return [options, callback]; } diff --git a/src/models/endpoint.js b/src/models/endpoint.js index 6eed102..9d408ce 100644 --- a/src/models/endpoint.js +++ b/src/models/endpoint.js @@ -11,6 +11,7 @@ function Endpoint (endpoint, datadir) { if (datadir == null) { datadir = process.cwd(); } Object.defineProperty(this, 'datadir', { value: datadir }); + out.debug('datadir: ' + this.datadir, 'Datadir for files specified in endpoint configuration'); this.request = purifyRequest(endpoint.request); this.response = purifyResponse(this, endpoint.response); @@ -21,45 +22,68 @@ Endpoint.prototype.matches = function (request) { var file, post, json, upperMethods; var matches = {}; + out.debugHeader('Endpoint matches'); + out.debug(this); + out.debugHeader('URL match'); matches.url = matchRegex(this.request.url, request.url); + out.debug(!(!matches.url), 'URL matches'); if (!matches.url) { return null; } + out.debugHeader('Header match'); matches.headers = compareHashMaps(this.request.headers, request.headers); + out.debug(!(!matches.headers), 'Header matches'); if (!matches.headers) { return null; } + out.debugHeader('Query match'); matches.query = compareHashMaps(this.request.query, request.query); + out.debug(!(!matches.query), 'Query matches'); if (!matches.query) { return null; } file = null; if (this.request.file != null) { try { file = fs.readFileSync(path.resolve(this.datadir, this.request.file), 'utf8'); - } catch (e) { /* ignored */ } + } catch (e) { + out.debug('Failed to read ' + this.request.file + ': ' + e); + } } + out.debugHeader('Post match'); post = file || this.request.post; if (post && request.post) { matches.post = matchRegex(normalizeEOL(post), normalizeEOL(request.post)); + out.debug(!(!matches.post), 'Post matches'); if (!matches.post) { return null; } } else if (this.request.json && request.post) { try { json = JSON.parse(request.post); - if (!compareObjects(this.request.json, json)) { return null; } + matches.post = compareObjects(this.request.json, json); + out.debug(!(!matches.post), 'Post matches'); + if (!matches.post) { return null; } } catch (e) { return null; } } else if (this.request.form && request.post) { matches.post = compareHashMaps(this.request.form, q.decode(request.post)); + out.debug(!(!matches.post), 'Post matches'); if (!matches.post) { return null; } } + out.debugHeader('Method match'); if (this.request.method instanceof Array) { upperMethods = this.request.method.map(function (it) { return it.toUpperCase(); }); - if (upperMethods.indexOf(request.method) === -1) { return null; } + if (upperMethods.indexOf(request.method) === -1) { + out.debug(request.method + ' not present in ' + upperMethods); + return null; + } } else if (this.request.method.toUpperCase() !== request.method) { + out.debug(request.method + ' not equal to ' + this.request.method); return null; + } else { + out.debug('Method matches'); } + out.debug(matches, 'Endpoint matches'); return matches; }; @@ -114,6 +138,8 @@ function purifyRequest (incoming) { if (incoming == null) { incoming = {}; } + out.debugHeader('Request'); + out.debug(incoming, 'Configured request'); outgoing = { url: incoming.url, method: incoming.method == null ? 'GET' : incoming.method, @@ -130,27 +156,34 @@ function purifyRequest (incoming) { outgoing.headers = purifyAuthorization(outgoing.headers); outgoing = pruneUndefined(outgoing); + out.debug(outgoing, 'Purified request'); return outgoing; } function purifyResponse (me, incoming) { var outgoing = []; + out.debugHeader('Response'); if (incoming == null) { incoming = []; } if (!(incoming instanceof Array)) { incoming = [incoming]; } if (incoming.length === 0) { incoming.push({}); } incoming.forEach(function (response) { + out.debug(incoming, 'Configured response'); if (typeof response === 'string') { - outgoing.push(record(me, response)); + const outgoingResponse = record(me, response); + out.debug(outgoingResponse, 'Purified response'); + outgoing.push(outgoingResponse); } else { - outgoing.push(pruneUndefined({ + const outgoingResponse = pruneUndefined({ headers: purifyHeaders(response.headers), status: parseInt(response.status, 10) || 200, latency: parseInt(response.latency, 10) || null, file: response.file, body: purifyBody(response.body) - })); + }); + out.debug(outgoingResponse, 'Purified response'); + outgoing.push(outgoingResponse); } }); @@ -218,6 +251,7 @@ function compareHashMaps (configured, incoming) { for (key in configured) { if (!Object.prototype.hasOwnProperty.call(configured, key)) { continue; } headers[key] = matchRegex(configured[key], incoming[key]); + out.debug('Header ' + key + ' matches: ' + !(!headers[key])); if (!headers[key]) { return null; } } @@ -227,19 +261,33 @@ function compareHashMaps (configured, incoming) { function compareObjects (configured, incoming) { var key; + out.debug(configured, 'Configured object'); + out.debug(incoming, 'Incoming object'); for (key in configured) { - if (typeof configured[key] !== typeof incoming[key]) { return false; } + if (typeof configured[key] !== typeof incoming[key]) { + out.debug('Types are different for ' + key); + return false; + } if (typeof configured[key] === 'object') { - if (!compareObjects(configured[key], incoming[key])) { return false; } - } else if (configured[key] !== incoming[key]) { return false; } + if (!compareObjects(configured[key], incoming[key])) { + out.debug('Types are different for ' + key); + return false; + } + } else if (configured[key] !== incoming[key]) { + out.debug(key + ' does not match'); + return false; + } } + out.debug('Objects match'); return true; } function matchRegex (compileMe, testMe) { if (testMe == null) { testMe = ''; } + out.debug('Regex: ' + compileMe); + out.debug('String: ' + testMe); return String(testMe).match(RegExp(compileMe, 'm')); } diff --git a/src/models/endpoints.js b/src/models/endpoints.js index fd23043..4b71eb5 100644 --- a/src/models/endpoints.js +++ b/src/models/endpoints.js @@ -6,6 +6,7 @@ var path = require('path'); var isutf8 = require('isutf8'); var Endpoint = require('./endpoint'); var clone = require('../lib/clone'); +const out = require('../console/out'); var NOT_FOUND = "Endpoint with the given id doesn't exist."; var NO_MATCH = "Endpoint with given request doesn't exist."; @@ -89,6 +90,8 @@ Endpoints.prototype.find = function (data, callback) { var id, endpoint, captures, matched; if (callback == null) { callback = noop; } + out.debugHeader('Incoming request'); + out.debug(data); for (id in this.db) { if (!Object.prototype.hasOwnProperty.call(this.db, id)) { continue; } @@ -122,6 +125,8 @@ Endpoints.prototype.found = function (endpoint, captures, callback) { applyCaptures(response, captures); + out.debugHeader('Outgoing response'); + out.debug(response); if (parseInt(response.latency, 10)) { setTimeout(function () { callback(null, response); }, response.latency); } else { diff --git a/test/cli.js b/test/cli.js index f96e084..0db94c2 100644 --- a/test/cli.js +++ b/test/cli.js @@ -224,13 +224,31 @@ describe('CLI', function () { describe('pfx', function () { it('should return contents of file', function () { var expected = 'some generated pfx'; - var actual = sut.pfx('test/data/cli.getPfx.pfx'); - assert.strictEqual(actual, expected); }); }); + describe('-g, --debugStubs', function () { + it('should return default if no flag provided', function () { + const expected = false; + const actual = sut.getArgs([]); + assert.strictEqual(actual.debugStubs, expected); + }); + + it('should return supplied value when provided', function () { + const expected = true; + const actual = sut.getArgs(['-g', expected]); + assert.strictEqual(actual.debugStubs, expected); + }); + + it('should return supplied value when provided with full flag', function () { + const expected = true; + const actual = sut.getArgs(['--debugStubs', expected]); + assert.strictEqual(actual.debugStubs, expected); + }); + }); + describe('getArgs', function () { it('should gather all arguments', function () { var actual; @@ -247,6 +265,7 @@ describe('CLI', function () { quiet: true, watch: filename, datadir: process.cwd(), + debugStubs: false, help: undefined, // eslint-disable-line no-undefined version: (require('../package.json')).version };