diff --git a/.gitignore b/.gitignore index 824af4f..c9d7c23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/* mocks/* -.DS_Store \ No newline at end of file +.DS_Store +.vscode/* \ No newline at end of file diff --git a/README.md b/README.md index 5b3a598..150243e 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ will return data from the mock file called "king.json", with HTTP status 200. Any other password will return "sorry.json" with HTTP status 401. #### JsonPath Support -For complex JSON requests, JsonPath expressions are supported in the switch parameter. If your switch parameter begins with "$." then it will be evaluated as a JsonPath expression. +For complex JSON requests, JsonPath expressions are supported in the switch parameter. If your switch parameter begins with "$." then it will be evaluated as a JsonPath expression. For example to switch the response based on the value of the last occurence of ItemId in a JSON request, use configuration as shown for "aceinsleeve": ```js "switch": "$..ItemId[(@.length-1)]", @@ -227,6 +227,76 @@ For example to switch the response based on the value of the last occurence of I According to this configuration, if the value of the last occurence of ItemId is 4, the mockFile "ItemId4.aceinsleeve.json" will be retured with a HTTP status code of 500. Otherwise, mockFile "aceinsleeve.json" will be returned with HTTP status 200. Note: If the JsonPath expression evaluates to more then 1 element (for example, all books cheaper than 10 as in $.store.book[?(@.price < 10)] ) then the first element is considered for testing the value. +#### JsonPath with Switch Response support +For requests that without any params should be returning a list of items (e.g. `/users`), and with some param just single item (e.g. `/users/:id`) there are special configuration options provided to select those single items from prepared mock json file containing list of items. No need to create separate files per each parameter. +Example mock file could look like this: +```js +[ + { + "name": "Han Solo", + "role": "pilot", + "id": 1 + }, + { + "name": "Chewbacca", + "role": "first officer", + "id": 2 + }, + { + "name": "C3P0", + "role": "droid", + "id": 3 + }, + { + "name": "R2D2", + "role": "droid", + "id": 4 + } +] +``` + +and example configurataion like this: + +```js +"users": { + "mockFile": "users.json", + "verbs": [ + "get" + ] +}, +"users/:id": { + "mockFile": "users.json", + "verbs": [ + "get" + ], + "switch": "id", + "jsonPathSwitchResponse": { + "jsonpath": "$[?(@.id==#id#)]", + "mockFile": "users.json", + "forceFirstObject": true + } +}, +"users/role/:role": { + "mockFile": "users.json", + "verbs": [ + "get" + ], + "switch": "role", + "jsonPathSwitchResponse": { + "jsonpath": "$[?(@.role==\"#role#\")]", + "mockFile": "users.json", + "forceFirstObject": false + } +} +``` + +The first config property (`users`) contains just a standard `get` for all users. The second (`users/:id`) and third though (`users/role/:role`), contains a proper switch configuration and `jsonPathSwitchResponse` config that contains following parameters: +* jsonpath - this is a JsonPath selector for objects to match inside `mockFile`; parameters values from switch are transferred to it's corresponding names wrapped in `#` characters, +* mockFile - a file name with mocked response to search through, +* forceFirstOject - (default: false) this is a switch telling if we should return all found items as an array, or select first one and return it as an object. + +So it is possible to select just a single user by id as an object (`/users/1`), but it is also possible to return multiple users as an array (`users/role/droid`). + #### RegExp Support As an alternative to JsonPath, Javascript Regular Expressions are supported in the switch parameter. See unit tests in the test.js file for examples of using Regular Expressions. @@ -262,7 +332,7 @@ templateSample.json { "Name": "@Name", "Number": "@Number" -} +} ``` When you call /John/12345 you will be returned: diff --git a/config.json b/config.json index 74468dd..c24ed8e 100644 --- a/config.json +++ b/config.json @@ -56,11 +56,11 @@ "userIduser1passwordgood": {"httpStatus": 200, "mockFile": "king.json"}, "userIdadminpasswordgood": {"httpStatus": 200} } - }, + }, "template/:name/:number" :{ "mockFile": "templateSample.json", - "enableTemplate": true, - "verbs":["get"], + "enableTemplate": true, + "verbs":["get"], "contentType":"application/json" }, "templateSwitchGetParams" : { @@ -88,6 +88,36 @@ "switch": "$.data.user.userAge", "type": "jsonpath"}], "contentType": "application/json" + }, + "users": { + "mockFile": "users.json", + "verbs": [ + "get" + ] + }, + "users/:id": { + "mockFile": "users.json", + "verbs": [ + "get" + ], + "switch": "id", + "jsonPathSwitchResponse": { + "jsonpath": "$[?(@.id==#id#)]", + "mockFile": "users.json", + "forceFirstObject": true + } + }, + "users/role/:role": { + "mockFile": "users.json", + "verbs": [ + "get" + ], + "switch": "role", + "jsonPathSwitchResponse": { + "jsonpath": "$[?(@.role==\"#role#\")]", + "mockFile": "users.json", + "forceFirstObject": false + } } } } diff --git a/lib/apimocker.js b/lib/apimocker.js index 09ac09b..f8ab57c 100644 --- a/lib/apimocker.js +++ b/lib/apimocker.js @@ -239,7 +239,8 @@ apiMocker.sendResponse = function(req, res, serviceKeys) { res.send(options.httpStatus); return; } - if (options.switch) { + + if (options.switch && !options.jsonPathSwitchResponse) { options = _.clone(options); originalOptions = _.clone(options); apiMocker.setSwitchOptions(options, req); @@ -270,6 +271,26 @@ apiMocker.sendResponse = function(req, res, serviceKeys) { return; } + if (options.switch && options.jsonPathSwitchResponse) { + var jpath = options.jsonPathSwitchResponse.jsonpath; + var fpath = path.join(apiMocker.options.mockDirectory, options.jsonPathSwitchResponse.mockFile); + var forceFirstObject = options.jsonPathSwitchResponse.forceFirstObject || false; + _.each(_.keys(req.params), function (key) { + var param = '#key#'.replace('key', key); + jpath = jpath.replace(param, req.params[key]); + }); + try { + var fd = fs.openSync(fpath, 'r'); + var mockFile = fs.readFileSync(fpath, { encoding: 'utf8' }); + var allElems = jsonPath.eval(JSON.parse(mockFile), jpath); + res.status(options.httpStatus || 200).send(forceFirstObject ? allElems[0] : allElems); + } catch (err) { + apiMocker.log(err); + res.send(options.httpStatus || 404); + } + return; + } + mockPath = path.join(apiMocker.options.mockDirectory, options.mockFile); apiMocker.log('Returning mock: ' + options.verb.toUpperCase() + ' ' + options.serviceUrl + ' : ' + options.mockFile); diff --git a/samplemocks/users.json b/samplemocks/users.json new file mode 100644 index 0000000..eac5cb9 --- /dev/null +++ b/samplemocks/users.json @@ -0,0 +1,22 @@ +[ + { + "name": "Han Solo", + "role": "pilot", + "id": 1 + }, + { + "name": "Chewbacca", + "role": "first officer", + "id": 2 + }, + { + "name": "C3P0", + "role": "droid", + "id": 3 + }, + { + "name": "R2D2", + "role": "droid", + "id": 4 + } +] \ No newline at end of file diff --git a/test/test-config.json b/test/test-config.json index b3f7da7..a8d4a1b 100644 --- a/test/test-config.json +++ b/test/test-config.json @@ -108,6 +108,36 @@ "switch": "$.data.user.userAge", "type": "jsonpath"}], "contentType": "application/json" + }, + "users": { + "mockFile": "users.json", + "verbs": [ + "get" + ] + }, + "users/:id": { + "mockFile": "users.json", + "verbs": [ + "get" + ], + "switch": "id", + "jsonPathSwitchResponse": { + "jsonpath": "$[?(@.id==#id#)]", + "mockFile": "users.json", + "forceFirstObject": true + } + }, + "users/role/:role": { + "mockFile": "users.json", + "verbs": [ + "get" + ], + "switch": "role", + "jsonPathSwitchResponse": { + "jsonpath": "$[?(@.role==\"#role#\")]", + "mockFile": "users.json", + "forceFirstObject": false + } } } } diff --git a/test/test-functional.js b/test/test-functional.js index fbe7055..2e78f59 100644 --- a/test/test-functional.js +++ b/test/test-functional.js @@ -228,6 +228,26 @@ describe('Functional tests using an http client to test "end-to-end": ', functio }); }); + describe('jsonPatch switch response', function () { + it('returns proper single object from mockFile', function (done) { + var reqOptions = httpReqOptions('/users/1'); + verifyResponseBody(reqOptions, null, { name: 'Han Solo', role: 'pilot', id: 1 }, done); + }); + it('returns proper array of results', function (done) { + var reqOptions = httpReqOptions('/users/role/droid'); + var expected = [{ + name: 'C3P0', + role: 'droid', + id: 3 + }, { + name: 'R2D2', + role: 'droid', + id: 4 + }]; + verifyResponseBody(reqOptions, null, expected, done); + }); + }); + describe('http status: ', function() { it('returns 404 for incorrect path', function(done) { stRequest.get('/badurl')