Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Some systems require you to `sudo` before running services on certain ports (lik

```
stubby [-a <port>] [-c <file>] [-d <file>] [-h] [-k <file>] [-l <hostname>] [-p <file>] [-q]
[-s <port>] [-t <port>] [-v] [-w]
[-s <port>] [-t <port>] [-v] [-w] [-g]

-a, --admin <port> Port for admin portal. Defaults to 8889.
-c, --cert <file> Certificate file. Use with --key.
Expand All @@ -67,6 +67,7 @@ stubby [-a <port>] [-c <file>] [-d <file>] [-h] [-k <file>] [-l <hostname>] [-p
-t, --tls <port> 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.
Expand Down Expand Up @@ -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])
Expand Down
5 changes: 5 additions & 0 deletions src/console/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions src/console/out.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -65,6 +67,7 @@ function setupStartOptions (options, callback) {
}

out.quiet = options.quiet;
out.debugStubs = options.debugStubs;
return [options, callback];
}

Expand Down
66 changes: 57 additions & 9 deletions src/models/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
};

Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
});

Expand Down Expand Up @@ -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; }
}

Expand All @@ -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'));
}

Expand Down
5 changes: 5 additions & 0 deletions src/models/endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.";

Expand Down Expand Up @@ -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; }

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 21 additions & 2 deletions test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
};
Expand Down