diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ba9ac09 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# INSPIRATION + +* https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md +* https://github.com/hoodiehq/hoodie.js/blob/master/CONTRIBUTING.md +* https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md +* https://github.com/twbs/bootstrap/blob/master/CONTRIBUTING.md + +# Contributing to Interfake + +So you've been using Interfake and have decided that you'd like to help out? Fantastic! There's plenty to do and it could always benefit from another pair of eyes to make it even better and more useful. Hopefully this document will answer some of your questions. Give it a read, and if you're still confused you can to [get in touch](#get-in-touch). + +## How to make a bug report + +If you've found something which is *demonstrably behaving incorrectly* with Interfake, that's really awesome, because it means there's a great opportunity to improve the tool. Here's some guidelines for bug reports: + +1. *Search the repo for Issues*: Use GitHub's search tool to search just this repository with some keywords about the bug you've found. It's possible it has already been reported. +2. **Check if it's really a bug**: It's possible that if you found the bug on the latest release of Interfake, the `master` branch on GitHub has a fix for it. Check it out, try and reproduce the bug using that code and if it's still there, there's probably a real issue. +3. **Create a test case for it**: Try writing a test case for the bug using Interfake's existing test files which can be used to help track down the responsible code. Essentially, extract only the code wrote which caused the bug and strip away everything else. + +So, if you're sure you've found a real bug you need to file a bug report. To be as helpful as possible, please try to include **reproduceable steps**, the **test case mentioned above**, and the **environment on which you are running Interfake**. + +## How to make a feature request + +I'm very welcome to hear ideas for how to add useful new features to Interfake, and there are probably a lot of things that could be added. However, please don't be offended if your idea is rejected. There could be many reasons for why this is happening - it may already be a feature, or it may be too large in scope for a project of this size, or it may just not be in alignment with the aims of the project. + +## How to make a Pull Request + +I love pull requests! However, there are some guidelines about how to make a good pull request, and how to further the chances of your pull request being merged. Here they are: + +1. **Run the unit tests before you're done**: This is the number one golden rule of pull requests. If any of the existing tests in the Interfake test suite fail as a result of the code in your pull request, your request will simply not be accepted. You may be lucky in that a contributor is willing to help make them pass, but it won't be merged until those tests pass. +2. **Write some unit tests to cover your addition**: This is the number one point five golden rule of pull requests. You must create test cases for your feature or fix alongside the existing test cases, and they must pass. + +## Code guidelines diff --git a/lib/fluent.js b/lib/fluent.js index 2c39a9c..1db29a6 100644 --- a/lib/fluent.js +++ b/lib/fluent.js @@ -5,35 +5,25 @@ function FluentInterface(server, o) { o = o || { debug: false }; var debug = require('./debug')('interfake-fluent', o.debug); - function forMethod(method, parent, top) { + function forMethod(method, parent) { return function (originalPath) { - var lookupHash, route; + var route; var routeDescriptor = { request: { url: originalPath, method: method - }, - response: { - code: 200, - body: {} } }; function cr() { - if (!parent && !top) { + if (!parent) { debug('Fluent setup called for', routeDescriptor.request.url); route = server.createRoute(routeDescriptor); - } else if (parent && top) { - debug('Fluent setup called for', routeDescriptor.request.url, 'with parent', parent.request.url, 'and top', top.request.url); - if (!parent.afterResponse) { - parent.afterResponse = { - endpoints: [] - }; - } - parent.afterResponse.endpoints.push(routeDescriptor); - server.createRoute(top); } else { - throw new Error('You cannot specify a parent without a top, dummy!'); + debug('Fluent setup called for', routeDescriptor.request.url, 'with parent', parent.request.url); + route = parent.addAfterResponse(routeDescriptor); + // parent.afterResponse.endpoints.push(routeDescriptor); + // server.createRoute(top); } } @@ -41,39 +31,44 @@ function FluentInterface(server, o) { return { query: function (query) { - routeDescriptor.request.query = merge(routeDescriptor.request.query, query); - if (!parent) { - cr(); - } else { - debug('Parent exists so not doing another cr yet'); - } + route.setQueryStrings(query); + // routeDescriptor.request.query = merge(routeDescriptor.request.query, query); + // if (!parent) { + // cr(); + // } else { + // debug('Parent exists so not doing another cr yet'); + // } return this; }, status: function (status) { - debug('Replacing status for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', status); - routeDescriptor.response.code = status; + route.setStatusCode(status); + // debug('Replacing status for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', status); + // routeDescriptor.response.code = status; return this; }, body: function (body) { - debug('Replacing body for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', body); - routeDescriptor.response.body = body; + route.setResponseBody(body); + // debug('Replacing body for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', body); + // routeDescriptor.response.body = body; return this; }, delay: function(delay) { - debug('Replacing delay for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', delay); - routeDescriptor.response.delay = delay; + route.setResponseDelay(delay); + // debug('Replacing delay for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', delay); + // routeDescriptor.response.delay = delay; return this; }, responseHeaders: function (headers) { - debug('Replacing response headers for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', headers); - routeDescriptor.response.headers = headers; + route.setResponseHeaders(headers); + // debug('Replacing response headers for', originalPath, JSON.stringify(routeDescriptor.request.query), 'with', headers); + // routeDescriptor.response.headers = headers; return this; }, creates: { - get: forMethod('get', routeDescriptor, top || routeDescriptor), - put: forMethod('put', routeDescriptor, top || routeDescriptor), - post: forMethod('post', routeDescriptor, top || routeDescriptor), - delete: forMethod('delete', routeDescriptor, top || routeDescriptor) + get: forMethod('get', parent || route), + put: forMethod('put', parent || route), + post: forMethod('post', parent || route), + delete: forMethod('delete', parent || route) } }; }; diff --git a/lib/route.js b/lib/route.js new file mode 100644 index 0000000..15e239f --- /dev/null +++ b/lib/route.js @@ -0,0 +1,162 @@ +var util = require('core-util-is'); +var url = require('url'); +var merge = require('merge'); + +function Route(descriptor, o) { + o = o || { debug: false }; + this.debug = require('./debug')('interfake-route', o.debug); + this.request = descriptor.request; + this.response = descriptor.response; + this.afterResponse = descriptor.afterResponse; + this.o = o; + + if (!this.response) { + this.response = { + delay: 0, + status: 200, + body: {}, + headers: {} + }; + } else { + this.response = merge(this.response, { + delay: 0, + status: 200, + body: {}, + headers: {} + }); + } + + this.response.query = {}; + + var path = url.parse(this.request.url, true); + + this.request.url = path.pathname; + + this.setQueryStrings(path.query); + + if (this.afterResponse && util.isArray(this.afterResponse.endpoints)) { + this.afterResponse.endpoints = (this.afterResponse.endpoints || []).map(function (descriptor) { + return new Route(descriptor); + }); + } else { + this.afterResponse = { + endpoints: [] + }; + } + + this.debug('Setting up ' + this.request.method + ' ' + this.request.url + ' to return ' + this.response.code + ' with a body of length ' + JSON.stringify(this.response.body).length + ' and ' + this.afterResponse.endpoints.length + ' after-responses'); +} + +Route.prototype.setQueryStrings = function (query) { + this.request.query = merge(this.request.query, query || {}); +}; + +Route.prototype.setStatusCode = function (statusCode) { + this.response.status = statusCode; +}; + +Route.prototype.setResponseBody = function (body) { + this.response.body = body; +}; + +Route.prototype.setResponseDelay = function (delay) { + this.response.delay = delay; +}; + +Route.prototype.setResponseHeaders = function (headers) { + this.response.headers = headers; +}; + +Route.prototype.addAfterResponse = function (descriptors) { + var newRoute = new Route(descriptors, this.o); + this.afterResponse.endpoints.push(newRoute); + return newRoute; +}; + +Route.prototype.creates = function (routeDescriptor) { + var newRoute = new Route(routeDescriptor, this.o); + if (!this.afterResponse) { + this.afterResponse = {}; + } + if (!this.afterResponse.endpoints) { + this.afterResponse.endpoints = []; + } + this.afterResponse.endpoints.push(newRoute); + return newRoute; +}; + +Route.prototype.simpleHash = function () { + var routeHash = this.request.method.toUpperCase() + ' ' + this.request.url; + + this.debug('Simple lookup hash key will be: ' + routeHash); + return routeHash; +}; + +Route.prototype.queryKeys = function () { + var queryKeys = Object.keys(this.request.query || {}).filter(function (key) { + return key !== 'callback'; + }); + return queryKeys; +}; + +Route.prototype.hash = function () { + var routeHash = this.request.method.toUpperCase() + ' ' + this.request.url; + var fullQuerystring, querystringArray = []; + if (this.request.query) { + var queryKeys = this.queryKeys(); + + if (queryKeys.length) { + this.debug('Query keys are', queryKeys); + + fullQuerystring = this.request.query; + delete fullQuerystring.callback; + + queryKeys.sort().forEach(function (k) { + querystringArray.push(k + '=' + fullQuerystring[k]); + }); + + this.debug('Full query string items are', querystringArray); + + routeHash += '?' + querystringArray.join('&'); + this.debug('Final route is', routeHash); + } + } + + this.debug('Lookup hash key will be: ' + routeHash); + return routeHash; +}; + +Route.prototype.compare = function (route) { + + this.debug('Comparing', this.request.query, 'and', route.request.query); + + var queryKeys; + var routeQueryKeys; + var same = true; + + this.debug('First comparing', route.simpleHash(), 'and', this.simpleHash()); + if (route.simpleHash() !== this.simpleHash()) { + return false; + } + + queryKeys = this.queryKeys(); + routeQueryKeys = route.queryKeys(); + + if (queryKeys.length !== routeQueryKeys.length) { + return false; + } + + queryKeys.forEach(function (key) { + var routeQueryValue = route.request.query[key]; + if (util.isRegExp(routeQueryValue)) { + same = same && routeQueryValue.test(this.request.query[key]); + } else { + // Not checking type because 1 and '1' are the same in this case + same = same && this.request.query[key] == routeQueryValue; + } + }.bind(this)); + + return same; +}; + +module.exports = Route; diff --git a/lib/server.js b/lib/server.js index 4f9dc1b..cb49d04 100644 --- a/lib/server.js +++ b/lib/server.js @@ -3,11 +3,9 @@ var path = require('path'); var FluentInterface = require('./fluent'); var corsMiddleware = require('./cors'); var util = require('core-util-is'); -var url = require('url'); -var merge = require('merge'); var connectJson = require('connect-json'); var bodyParser = require('body-parser'); -var querystring = require('querystring'); +var Route = require('./route'); function createInvalidDataException(data) { return new Error('You have to provide a JSON object with the following structure: \n' + JSON.stringify({ request : { method : '[GET|PUT|POST|DELETE]', url : '(relative URL e.g. /hello)' }, response : { code : '(HTTP Response code e.g. 200/400/500)', body : '(a JSON object)' } }, null, 4) + ' but you provided: \n' + JSON.stringify(data, null, 4)); @@ -27,125 +25,6 @@ function determineDelay(delayInput) { return result; } -function Route(descriptor, o) { - this.debug = require('./debug')('interfake-route', o.debug); - this.request = descriptor.request; - this.response = descriptor.response; - this.o = o; - - if (!this.response) { - this.response = { - delay: 0, - status: 200, - body: {} - }; - } - - this.response.query = {}; - - var path = url.parse(this.request.url, true); - - this.request.url = path.pathname; - - this.setQueryStrings(path.query); - - if (this.afterResponse && util.isArray(this.afterResponse.endpoints)) { - this.afterResponse.endpoints = (this.afterResponse.endpoints || []).map(function (descriptor) { - return new Route(descriptor); - }); - } -} - -Route.prototype.setQueryStrings = function (query) { - this.request.query = merge(this.request.query, query || {}); -}; - -Route.prototype.creates = function (routeDescriptor) { - var newRoute = new Route(routeDescriptor, this.o); - if (!this.afterResponse) { - this.afterResponse = {}; - } - if (!this.afterResponse.endpoints) { - this.afterResponse.endpoints = []; - } - this.afterResponse.endpoints.push(newRoute); - return newRoute; -}; - -Route.prototype.simpleHash = function () { - var routeHash = this.request.method.toUpperCase() + ' ' + this.request.url; - - this.debug('Simple lookup hash key will be: ' + routeHash); - return routeHash; -}; - -Route.prototype.queryKeys = function () { - var queryKeys = Object.keys(this.request.query || {}).filter(function (key) { - return key !== 'callback'; - }); - return queryKeys; -}; - -Route.prototype.hash = function () { - var routeHash = this.request.method.toUpperCase() + ' ' + this.request.url; - var fullQuerystring, querystringArray = []; - if (this.request.query) { - var queryKeys = this.queryKeys(); - - if (queryKeys.length) { - this.debug('Query keys are', queryKeys); - - fullQuerystring = this.request.query; - delete fullQuerystring.callback; - - queryKeys.sort().forEach(function (k) { - querystringArray.push(k + '=' + fullQuerystring[k]); - }); - - this.debug('Full query string items are', querystringArray); - - routeHash += '?' + querystringArray.join('&'); - this.debug('Final route is', routeHash); - } - } - - this.debug('Lookup hash key will be: ' + routeHash); - return routeHash; -}; - -Route.prototype.compare = function (route) { - - this.debug('Comparing', this.request.query, 'and', route.request.query); - - var queryKeys; - var routeQueryKeys; - var same = true; - - this.debug('First comparing', route.simpleHash(), 'and', this.simpleHash()); - if (route.simpleHash() !== this.simpleHash()) { - return false; - } - - queryKeys = this.queryKeys(); - routeQueryKeys = route.queryKeys(); - - if (queryKeys.length !== routeQueryKeys.length) { - return false; - } - - queryKeys.forEach(function (key) { - var routeQueryValue = route.request.query[key]; - if (util.isRegExp(routeQueryValue)) { - same = same && routeQueryValue.test(this.request.query[key]); - } else { - // Not checking type because 1 and '1' are the same in this case - same = same && this.request.query[key] == routeQueryValue; - } - }.bind(this)); - - return same; -}; - function Interfake(o) { o = o || { debug: false }; var app = express(); @@ -219,7 +98,7 @@ function Interfake(o) { debug(req.method, 'request to', req.url, 'returning', specifiedResponse.code); debug(req.method, 'request to', req.url, 'will be delayed by', responseDelay, 'millis'); - // debug('After response is', afterSpecifiedResponse); + debug('Expected route is', expectedRoute); var responseBody = specifiedResponse.body; @@ -258,27 +137,32 @@ function Interfake(o) { function createRoute(data) { var newRoute; - if (!data.request || !data.request.method || !data.request.url || !data.response || !data.response.code) { - throw createInvalidDataException(data); - } + // This may no longer be necessary + // if (!data.request || !data.request.method || !data.request.url || !data.response || !data.response.code) { + // throw createInvalidDataException(data); + // } - if (data.response.body) { - debug('Setting up ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with a body of length ' + JSON.stringify(data.response.body).length); - } else { - debug('Setting up ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with no body'); - } + // var numAfterResponse = (data.afterResponse && data.afterResponse.endpoints) ? data.afterResponse.endpoints.length : 0; + + // if (data.response.body) { + // debug('Setting up ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with a body of length ' + JSON.stringify(data.response.body).length + ' and ' + numAfterResponse + ' after-responses'); + // } else { + // debug('Setting up ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with no body'); + // } + + debug('Setting up new route'); newRoute = new Route(data, o); addRoute(newRoute); - var numAfterResponse = (data.afterResponse && data.afterResponse.endpoints) ? data.afterResponse.endpoints.length : 0; + debug('Setup complete'); - if (data.response.body) { - debug('Setup complete: ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with a body of length ' + JSON.stringify(data.response.body).length + ' and ' + numAfterResponse + ' after-responses'); - } else { - debug('Setup complete: ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with no body and ' + numAfterResponse + ' after-responses'); - } + // if (data.response.body) { + // debug('Setup complete: ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with a body of length ' + JSON.stringify(data.response.body).length + ' and ' + numAfterResponse + ' after-responses'); + // } else { + // debug('Setup complete: ' + data.request.method + ' ' + data.request.url + ' to return ' + data.response.code + ' with no body and ' + numAfterResponse + ' after-responses'); + // } return newRoute; } diff --git a/tests/javascript.test.js b/tests/javascript.test.js index 6a9c83a..46fe386 100644 --- a/tests/javascript.test.js +++ b/tests/javascript.test.js @@ -289,6 +289,7 @@ describe('Interfake JavaScript API', function () { }); it('should create a dynamic endpoint', function (done) { + interfake = new Interfake({debug:true}); interfake.createRoute({ request: { url: '/dynamic', @@ -327,7 +328,8 @@ describe('Interfake JavaScript API', function () { .then(function (results) { assert.equal(results[0].statusCode, 200); done(); - }); + }) + .done(); }); it('should create a dynamic endpoint within a dynamic endpoint', function (done) { @@ -688,7 +690,8 @@ describe('Interfake JavaScript API', function () { .then(function (results) { assert.equal(results[0].statusCode, 200); done(); - }); + }) + .done(); }); it('should create a GET endpoint which creates another GET endpoint which accepts a query string with a regex', function (done) { var first = interfake.get('/fluent'); @@ -712,7 +715,8 @@ describe('Interfake JavaScript API', function () { .then(function (results) { assert.equal(results[0].statusCode, 200); done(); - }); + }) + .done(); }); }); });