diff --git a/README.md b/README.md index 24eb691..35aeb57 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,20 @@ note that I will be adding an 'at' parameter to configure the special behavior t > responseBody: 'Thou shall not pass!', > at: 3 } +##### Same endpoint with different verbs/method + +Now, you can do a GET and POST at the same endpoint + +> { route: 'login', +> responseCode: 200, +> responseBody: 'ok', +> verb: 'GET' } + +> { route: 'login', +> responseCode: 200, +> responseBody: 'You are doing a POST', +> verb: 'POST' } + ##### Delay response @@ -138,10 +152,23 @@ The following will delay server response in one second: > responseBody: 'OK', > delay: 1000 } -##### Resetting server configuration + +### Delete one petition + +Sometimes, you want to remove an entry. So, now you can. + +All you have to do is 'DELETE' to http://localhost:3012/delOne the following data: + +Delete /test by deleting: +> { route: 'test', +> responseCode: 200, +> verb: "GET" } + + +#### Resetting server configuration To avoid the need to restart fake-server in order to clear the configuration, we've implemented a special endpoint called `/flush`. By sending a `DELETE` request to http://localhost:3012/flush, you will erase all previously configured responses. ### Limitations -- There are two reserved endpoints: POST '/add' and `DELETE` '/flush'. These cannot be used by your application. +- There are reserved endpoints: POST '/add', GET '/getAll', DELETE '/delOne' and DELETE, '/flush'. These cannot be used by your application. diff --git a/README2.html b/README2.html new file mode 100644 index 0000000..5f99625 --- /dev/null +++ b/README2.html @@ -0,0 +1,305 @@ +#fake-server +
+
+[![Build +Status](https://travis-ci.org/yahoo/fake-server.svg)](https://travis-ci.org/yahoo/fake-server) +
+
+Fake-server is a generic and non-intrusive tool used to mock any server +response. It has been designed to address issues when running tests +against unstable or slow external servers. +
+
+=========== +
+
+## How it works +
+
+The idea is to create a webserver listening in a different port and make +your tests bring it up and configure how it should behave against each +different request. "Configuration" can be done by posting the parameters +and desired response to the server, or through configuration files +inside ./default_routes/. +
+
+For every request, fake-server will try to match against the configured +URIs and return the expected response. +
+
+### Advantages +
+- No need to instrument your code (as long as the external server endpoint is configurable :P)
+- Generic enough to work with blackbox or whitebox tests.
+- No database required
+
+
+### Quickstart (two really basic scenarios)
+
+Clone this repository (npm package coming soon)
+> git clone git@github.com:yahoo/fake-server.git
+
+Start (it will start a server on port 3012)
+> node server.js
+
+
+Let's say you want "/test" to always return "hello" and +"/foo" to return a 404. +
+
+All you have to do is `POST` to http://localhost:3012/add/ the +following data: +
+
+Configure /test by posting: +
+> { route: '/test', +
+> responseCode: 200, +
+> responseBody: "hello" } +
+
+one of the many ways to do this is using cURL: +
+``` +
+curl http://localhost:3012/add -X POST -H +"Content-Type:application/json" -H "Accept:application/json" \ +
+-d +'{"route":"/test","responseCode":200,"responseBody":"hello"}' +
+``` +
+
+now let's configure our 404 example by sending this to the server: +
+> { route: '/foo', +
+> responseCode: 404, +
+> responseBody: "Not found" } +
+
+using cURL: +
+``` +
+curl http://localhost:3012/add -X POST -H +"Content-Type:application/json" -H "Accept:application/json" \ +
+-d '{"route":"/foo","responseCode":404,"responseBody":"Not +found"}' +
+``` +
+
+now, in your browser you can see the results: +
+http://localhost:3012/foo +
+http://localhost:3012/test +
+
+
+### What else can fake-server do? +
+
+Configuration is done by sending a POST request to /add or by placing a +json file containing configurations inside a "routes" object (see +default_routes/sample.json for reference). Here are the supported +features for this version: +
+
+##### Routes can be RegEx +
+
+This will match http://localhost:3012/news/007 as well +as http://localhost:3012/news/1231293871293827: +
+
+> { route: '/news/[0-9]' +
+> responseCode: 200, +
+> responseBody: 'whatever you want' } +
+
+##### Fake-server supports "POST" calls and uses payload for matching. +Regexs are supported for payload matching, and paths can be used to +specify inner properties of JSON payloads: +
+
+> { route: '/news' +
+> payload: { +
+> id: [\\d+], +
+> requests[1].user.login: 'jdoe', +
+> month: "february" +
+> }, +
+> responseCode: 200, +
+> responseBody: 'yay! it matches' +
+> } +
+
+##### Support for query string matching. All query params are evaluated +as a RegEx. +
+
+> { route: '/news', +
+> queryParams: { +
+> id: "[\\d+]", +
+> location: "Hawaii" +
+> } +
+> responseCode: 200, +
+> responseBody: 'Regex matching rocks' +
+> } +
+
+##### ... can also use the request Headers. So you can check if specific +cookies are present, for instance +
+
+> { route: '/secure', +
+> requiredHeaders: { +
+> X-Auth: "secret", +
+> }, +
+> responseCode: 200, +
+> responseBody: 'header is there' +
+> } +
+
+
+##### Response can be a file. In this case, fake-server will respond +with the output of that file. +
+
+The following configuration example will return the output of +./mock_data/sample.json *(notice the parameter is called responseData +instead of responseBody)* +
+
+Para esto, teneis que poneros en contacto conmigo --> crodriguez +
+
+> { route: '/', +
+> responseCode: 200, +
+> responseData: './mock_data/sample.json' } +
+
+##### Same endpoint with different verbs/method +
+
+Now, you can do a GET and POST at the same endpoint +
+
+> { route: '/login', +
+> responseCode: 200, +
+> responseBody: 'ok', +
+> verb: 'GET' } +
+
+> { route: '/login', +
+> responseCode: 200, +
+> responseBody: 'You are doing a POST', +
+> verb: 'POST' } +
+
+##### Same endpoint can have different responses +
+
+This will return '200' in the first two requests to '/' and +403 on the third request +
+
+> { route: '/', +
+> responseCode: 200, +
+> responseBody: 'ok' } +
+
+note that I will be adding an 'at' parameter to configure the special +behavior to the third request: +
+
+> { route: '/', +
+> responseCode: 403, +
+> responseBody: 'Thou shall not pass!', +
+> at: 3 } +
+
+
+##### Delay response +
+
+The following will delay server response in one second: +
+
+> { route: '/slow/.*', +
+> responseCode: 200, +
+> responseBody: 'OK', +
+> delay: 1000 } +
+
+## Delete one petition +
+
+Sometimes, you want to remove an entry. So, now you can. +
+
+All you have to do is 'DELETE' to http://localhost:3012/delOne the +following data: +
+
+Delete /test by deleting: +
+> { route: '/test', +
+> responseCode: 200, +
+> verb: "GET" } +
+
+
+
+### Limitations +
+- There are reserved endpoints: POST '/add', GET '/getAll', DELETE +'/delOne' and DELETE, '/flush'. These cannot be used by your application. +
\ No newline at end of file diff --git a/controller.js b/controller.js index da84bbe..b7d0cff 100644 --- a/controller.js +++ b/controller.js @@ -13,21 +13,24 @@ var argv = require('yargs').argv; var FakeResponse = require('./fakeresponse.js'); var merge = require('merge'); -// Preload routes +// Preload routes FakeResponse.preload(argv.configDir); var controller = { - fakeResponse: FakeResponse, // of course this is here just so that it can be overwritten easily in the tests. + fakeResponse : FakeResponse, // of course this is here just so that it + // can be overwritten easily in the tests. - add: function (req, res, next) { + add : function(req, res, next) { var obj = { - delay: req.params.delay, - at: req.params.at, - route: req.params.route, - queryParams: req.params.queryParams, - payload: req.params.payload, - responseCode: req.params.responseCode, - responseBody: decodeURIComponent(req.params.responseBody.replace(/"/g, '"')), + verb : req.params.verb, + delay : req.params.delay, + at : req.params.at, + route : req.params.route, + queryParams : req.params.queryParams, + payload : req.params.payload, + responseCode : req.params.responseCode, + responseBody : decodeURIComponent(req.params.responseBody.replace( + /"/g, '"')), }; controller.fakeResponse.add(obj); @@ -36,9 +39,55 @@ var controller = { next(); }, - match: function (req, res, next) { + howto : function(req, res, next) { - function send (statusCode, responseHeaders, responseBody) { + var headers = { + 'Content-Type' : 'text/html' + }; + + function send(statusCode, responseHeaders, responseBody) { + + responseHeaders['Content-Length'] = Buffer.byteLength(responseBody); + res.writeHead(statusCode, responseHeaders); + res.write(responseBody); + res.end(); + } + + fs.readFile(path.join(__dirname, "./README2.html"), 'utf8', function(err, + data) { + if (err) { + res.send(500, "FAKE-SERVER is misconfigured"); + } + send(200, headers, data); + }); + + next(); + }, + + delOne : function(req, res, next) { + + + var bestMatch = controller.fakeResponse.match(true, req, res); + + if (bestMatch) { + res.send(410, 'GONE'); + } else { + res.send(404, 'NOT FOUND!'); + } + next(); + }, + + getAll : function(req, res, next) { + var obj = controller.fakeResponse.getAll(obj); + + res.send(200, obj); + next(); + }, + + match : function(req, res, next) { + + function send(statusCode, responseHeaders, responseBody) { + if (typeof responseBody === "object") { try { responseBody = JSON.stringify(responseBody); @@ -53,26 +102,30 @@ var controller = { res.end(); } - var bestMatch = controller.fakeResponse.match(req.url, req.body, req.headers); + var bestMatch = controller.fakeResponse.match(false, req, res); if (bestMatch) { var headers = { - 'Content-Type': 'application/json' + 'Content-Type' : 'application/json' }; - if(bestMatch.responseHeaders) { + if (bestMatch.responseHeaders) { headers = merge(headers, bestMatch.responseHeaders); } - if(bestMatch.responseData) { - - fs.readFile(path.join(__dirname, bestMatch.responseData),'utf8', function(err, data) { - if (err) { - res.send(500, "FAKE-SERVER is misconfigured"); - } - send(parseInt(bestMatch.responseCode, 10), headers, data); - }); + if (bestMatch.responseData) { + + fs.readFile(path.join(__dirname, bestMatch.responseData), + 'utf8', function(err, data) { + if (err) { + res.send(500, "FAKE-SERVER is misconfigured"); + } + send(parseInt(bestMatch.responseCode, 10), headers, + data); + }); + } else { - send(parseInt(bestMatch.responseCode, 10), headers, bestMatch.responseBody); + send(parseInt(bestMatch.responseCode, 10), headers, + bestMatch.responseBody); } if (bestMatch.delay) { diff --git a/fakeresponse.js b/fakeresponse.js index 0a49766..d3de6eb 100644 --- a/fakeresponse.js +++ b/fakeresponse.js @@ -47,6 +47,7 @@ var FakeResponse = { FakeResponse._items.push(item); }, + flush: function () { FakeResponse._items = []; }, @@ -76,25 +77,68 @@ var FakeResponse = { }, - /* Filters all items that match the URL and then tries to check if there is a specific behavior for the Nth call on the same endpoint */ - match: function (uri, payload, headers) { - uri = url.parse(uri, true); - - return FakeResponse._items.filter(function (item) { - var doPathsMatch = uri.pathname.match(new RegExp(item.route)); - - if (doPathsMatch !== null) { - item.numCalls += 1; - if(item.queryParams && !FakeResponse.matchRegex(item.queryParams, uri.query)) return false; - if(item.payload && !FakeResponse.matchRegex(item.payload, payload)) return false; - if(item.requiredHeaders && !FakeResponse.matchRegex(item.requiredHeaders, headers)) return false; - if (item.at) return (item.numCalls === item.at); - return true; - } - return false; - }).sort(FakeResponse.compareMatches)[0] || null; + /* + * If del --> false + * Filters all items that match the URL and then tries to check if there is + * a specific behavior for the Nth call on the same endpoint. + * If del --> true + * Filters all items that match the URL, verb and responseCode and then del this endpoint. + */ + match: function (del, req, res) { + + var uri =''; + var verb = ''; + var payload=''; + var headers=''; + + return FakeResponse._items.filter(function (item) { + if(!del){ + uri = req.url; + uri= url.parse(uri, true); + var doPathsMatch = uri.pathname.match(new RegExp(item.route)); + if (doPathsMatch !== null) { + item.numCalls += 1; + + verb = req.method; + if(item.verb && !(item.verb==verb)) return false; + + if(item.queryParams && !FakeResponse.matchRegex(item.queryParams, uri.query)) return false; + + payload=req.body; + if(item.payload && !FakeResponse.matchRegex(item.payload, payload)) return false; + + headers = req.headers; + if(item.requiredHeaders && !FakeResponse.matchRegex(item.requiredHeaders, headers)) return false; + + if (item.at) return (item.numCalls === item.at); + return true; + } + }else{ + uri = req.params.route; + uri= url.parse(uri, true); + + var doPathsMatch = uri.pathname.match(new RegExp(item.route)); + + if (doPathsMatch !== null) { + verb = req.params.verb; + if(item.verb && !(item.verb==verb)) return false; + + var responseCode = req.params.responseCode; + if(item.responseCode && !(item.responseCode==responseCode)) return false; + var index = FakeResponse._items.indexOf(item); + if (index > -1) { + FakeResponse._items.splice(index, 1); + return true; + }else{ + return false; + } + } + } + return false; + }).sort(FakeResponse.compareMatches)[0] || null; + }, - + /* * Match objB's values against regular expressions stored in objA. Key equality determines values to test. * @param {objA} An object whose string values represent regular expressions diff --git a/mock_data/README.md b/mock_data/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/routes.js b/routes.js index 9f02d38..3d4da6c 100644 --- a/routes.js +++ b/routes.js @@ -10,7 +10,12 @@ var controller = require('./controller.js'); module.exports = function (server) { server.post('/add', controller.add); + server.del('/delOne', controller.delOne); server.del('/flush', controller.flush); + server.get('/getAll', controller.getAll); + server.get('/HOWTO', controller.howto); server.get(/(.*)/, controller.match); server.post(/(.*)/, controller.match); + server.del(/(.*)/, controller.match); + server.put(/(.*)/, controller.match); }; diff --git a/test/controller.js b/test/controller.js index b1b6979..1518546 100644 --- a/test/controller.js +++ b/test/controller.js @@ -301,4 +301,80 @@ describe('Integration tests', function () { assert.isTrue(res.writeHead.calledWith(123)); }); + + it('should provide a way to delete one response for a given endpoint', function () { + var req = { + params: { + route: '/foo/bar', + responseCode: 404, + verb: 'GET', + responseBody: 'You want to delete something', + } + }; + + var res = { + send: sinon.stub(), + write: sinon.stub(), + writeHead: sinon.stub(), + end: sinon.stub() + }; + + sinon.stub(controller.fakeResponse, 'add'); + + controller.add(req, res, function () {}); + + sinon.stub(controller.fakeResponse, 'match'); + + controller.delOne(req, res, function () {}); + + assert.isTrue(controller.fakeResponse.match.calledOnce); + + controller.fakeResponse.match.restore(); + }); + + + it('should allow different behaviours for the same request based on the verb', function () { + var responses = [{ + route: '/', + responseCode: 200, + responseBody: 'OK', + numCalls: 0, + verb: 'GET' + }, { + route: '/', + responseCode: 403, + responseBody: 'yay!', + at: 3, + numCalls: 0, + verb: 'POST' + }]; + + var req = { + url: '/', + method: 'GET' + }; + + var req2 = { + url: '/', + method: 'POST' + }; + + var res = { + write: sinon.stub(), + writeHead: sinon.stub(), + send: sinon.stub(), + end: sinon.stub() + }; + + controller.fakeResponse._items = responses; + + controller.match(req, res, function () {}); + res.writeHead.calledWithExactly(200, {'Content-Type': 'application/json', 'Content-Length': 2}) + res.write.lastCall.calledWithExactly('OK'); + + controller.match(req2, res, function () {}); + res.writeHead.calledWithExactly(403, {'Content-Type': 'application/json', 'Content-Length': 4}) + res.write.lastCall.calledWithExactly('yay!'); + + }); }); diff --git a/test/fakeresponse.js b/test/fakeresponse.js index 3c25cc9..ced5e30 100644 --- a/test/fakeresponse.js +++ b/test/fakeresponse.js @@ -18,7 +18,7 @@ describe('FakeResponse model tests', function () { }); it('should return null if no routes have been added', function() { - var response = model.match('/match/me'); + var response = model.match(false, '/match/me'); assert.equal(null, response); }); @@ -63,11 +63,36 @@ describe('FakeResponse model tests', function () { model.add(obj); - var response = model.match('/foo/bar'); + var response = model.match(false,{url:'/foo/bar'}); assert.deepEqual(response, obj); }); + + + it('should match successfully a route with the expected answers based in request with verb', function () { + var obj = { + route: '/foo/bar', + responseCode: 200, + verb: 'GET', + responseBody: 'foo', + }; + + model.add({ + route: '/foo/bar', + responseCode: 200, + verb: 'PUT', + responseBody: 'foo2', + }); + + model.add(obj); + + var response = model.match(false,{url:'/foo/bar', method:'GET'}); + + assert.deepEqual(response, obj); + }); + + it('should match based on regexp', function () { var obj = { route: '/foo.*', @@ -83,7 +108,7 @@ describe('FakeResponse model tests', function () { responseBody: '§xxx', }); - var response = model.match('/foo/bar'); + var response = model.match(false,{url:'/foo/bar'}); assert.deepEqual(response, obj); }); @@ -108,11 +133,11 @@ describe('FakeResponse model tests', function () { at: 2 }); - var firstReq = model.match('/match/me'); + var firstReq = model.match(false,{url:'/match/me'}); assert.equal(200, firstReq.responseCode); - var secondReq = model.match('/match/me'); + var secondReq = model.match(false,{url:'/match/me'}); assert.equal(204, secondReq.responseCode); - var thirdReq = model.match('/match/me'); + var thirdReq = model.match(false,{url:'/match/me'}); assert.equal(200, thirdReq.responseCode); }); @@ -144,7 +169,7 @@ describe('FakeResponse model tests', function () { responseBody: 'weba', }); /*even though "uri" is the same, we are only matching if payload contains id:1 */ - var response = model.match('/match/me'); + var response = model.match(false,{url:'/match/me'}); assert.equal(null, response); }); @@ -166,7 +191,10 @@ describe('FakeResponse model tests', function () { responseBody: 'buuu' }); /*even though "uri" is the same, we are only matching if payload contains id:1 */ - var response = model.match('/match/me', { id: 1 }); + var response = model.match(false,{url:'/match/me', body:{'id': 1}}); + + assert.equal(2, model.getAll().length); + assert.deepEqual(response.responseBody, 'weba'); assert.deepEqual(response.responseCode, 200); }); @@ -189,7 +217,7 @@ describe('FakeResponse model tests', function () { responseBody: 'buuu' }); - var response = model.match('/match/me', { outer: [{inner: 1 }]}); + var response = model.match(false,{url:'/match/me', body:{ outer: [{inner: 1 }]}}); assert.deepEqual(response.responseBody, 'weba'); assert.deepEqual(response.responseCode, 200); }); @@ -204,11 +232,11 @@ describe('FakeResponse model tests', function () { responseBody: 'weba' }); - var response = model.match('/match/me?outer[0].inner=1'); + var response = model.match(false,{url:'/match/me?outer[0].inner=1'}); assert.deepEqual(response.responseBody, 'weba'); assert.deepEqual(response.responseCode, 200); - response = model.match('/match/me?param=1'); + response = model.match(false,{url:'/match/me?param=1'}); assert.deepEqual(response, null); }); it('should match POST request payloads using explicit regular expressions', function() { @@ -220,7 +248,7 @@ describe('FakeResponse model tests', function () { responseCode: 200, responseBody: 'Regex success', }); - var response = model.match('/match/me', { id: 9273892 }); + var response = model.match(false,{url:'/match/me', body:{ id: 9273892 }}); assert.deepEqual(response.responseBody, 'Regex success'); assert.deepEqual(response.responseCode, 200); @@ -233,12 +261,12 @@ describe('FakeResponse model tests', function () { responseCode: 200, responseBody: 'Regex success', }); - response = model.match('/match/me', { a: 2, b:"baz" }); + response = model.match(false,{url:'/match/me', body:{ a: 2, b:"baz" }}); assert.deepEqual(response.responseBody, 'Regex success'); assert.deepEqual(response.responseCode, 200); // Non-matching payload (bazz) should fail - response = model.match('/match/me', { a: 2, foo:"bazz" }); + response = model.match(false,{url:'/match/me', body:{ a: 2, foo:"bazz" }}); assert.equal(null, response); }); @@ -255,12 +283,12 @@ describe('FakeResponse model tests', function () { responseBody: 'Query param success' }); - var response = model.match('/match/me?a=1&b=2'); + var response = model.match(false,{url:'/match/me?a=1&b=2'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Query param success'); // Varying order of query params shouldn't affect matching - response = model.match('/match/me?a=1&b=2'); + response = model.match(false,{url:'/match/me?a=1&b=2'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Query param success'); @@ -274,12 +302,12 @@ describe('FakeResponse model tests', function () { }); // query params with spaces should work - response = model.match('/match/me?name=Fabio Hirata'); + response = model.match(false,{url:'/match/me?name=Fabio Hirata'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Space success'); // ...even if encoded with + - response = model.match('/match/me?name=Fabio+Hirata'); + response = model.match(false,{url:'/match/me?name=Fabio+Hirata'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Space success'); }); @@ -295,15 +323,15 @@ describe('FakeResponse model tests', function () { responseBody: 'Regex success' }); - var response = model.match('/match/me?a=0'); + var response = model.match(false,{url:'/match/me?a=0'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Regex success'); - response = model.match('/match/me?a=9'); + response = model.match(false,{url:'/match/me?a=9'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Regex success'); - response = model.match('/match/me?a=1234567890'); + response = model.match(false,{url:'/match/me?a=1234567890'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Regex success'); @@ -320,7 +348,7 @@ describe('FakeResponse model tests', function () { responseBody: 'Regex success' }); - response = model.match('/match/me?e=bar&b=12345&c=6789&d=1'); + response = model.match(false,{url:'/match/me?e=bar&b=12345&c=6789&d=1'}); assert.deepEqual(response.responseCode, 200); assert.deepEqual(response.responseBody, 'Regex success'); }); @@ -346,18 +374,18 @@ describe('FakeResponse model tests', function () { model.add(route1); model.add(route2); - var response = model.match('/match/me?a=1234', {b: 'abcd'}); + var response = model.match(false,{url:'/match/me?a=1234', body:{b: 'abcd'}}); assert.equal(response.responseCode, 200); - response = model.match('/match/me?a=1234', {b: 'abc123'}); + response = model.match(false,{url:'/match/me?a=1234', body:{b: 'abc123'}}); assert.equal(response.responseCode, 400); model.flush(); model.add(route2); model.add(route1); - response = model.match('/match/me?a=1234', {b: 'abcd'}); + response = model.match(false,{url:'/match/me?a=1234', body:{b: 'abcd'}}); assert.equal(response.responseCode, 200); - response = model.match('/match/me?a=1234', {b: 'abc123'}); + response = model.match(false,{url:'/match/me?a=1234', body:{b: 'abc123'}}); assert.equal(response.responseCode, 400); }); @@ -383,14 +411,14 @@ describe('FakeResponse model tests', function () { }; model.add(route1); model.add(route2); - var response = model.match('/match/me?a=1234', null, { Cookie: 'Y=abcd' }); + var response = model.match(false,{url:'/match/me?a=1234', headers:{ Cookie: 'Y=abcd' }}); assert.equal(response.responseCode, 200); model.flush(); model.add(route1); model.add(route2); - response = model.match('/match/me?a=1234'); + response = model.match(false,{url:'/match/me?a=1234'}); assert.equal(response.responseCode, 400); }); });