├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENCE.md ├── examples-command-line ├── crud.json ├── get-and-post-with-after-response.json ├── modify-with-patch.json ├── ranged-delay.json ├── regexp-get.json └── simple-get.json ├── examples-javascript ├── echo.js ├── fluent-crud.js ├── fluent-echo.js ├── fluent-extends.js ├── fluent-proxy.js ├── fluent-regex.js ├── fluent-simple.js ├── fluent-web-page-test.html ├── fluent-web-page-test.js ├── load-file-reload-unload.js ├── load-file-reload-unload.json ├── load-file-watch.js ├── regexp-url.js ├── simple.js └── testwatch.js ├── index.js ├── lib ├── cors.js ├── debug.js ├── fluent.js ├── grammar.pegjs ├── route.js └── server.js ├── npm-shrinkwrap.json ├── package.json ├── readme.md └── tests ├── fluent.test.js ├── http.test.js ├── javascript.test.js ├── loadFileTest-1.json ├── loadFileTest-2.json ├── loadFileTest-3.json └── static-test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | before_install: 5 | - npm install npm -g -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Interfake 2 | 3 | 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). 4 | 5 | ## Goals 6 | 7 | Once upon a time, someone needed an "easier way than Sinatra" to spin up some test APIs for a bunch of iPhone UI tests. Soon, Interfake was born and allowed JSON files to be easily turned into APIs. Once the fluent interface was developed, things took off. It is fairly easy to summarise the goals of the project: 8 | 9 | > Provide a simple way to *define* JSON-based HTTP APIs 10 | 11 | In doing so, Interfake should... 12 | 13 | 1. Not impose strong opinions about how an API should look 14 | 2. Keep dependencies to a minimum 15 | 3. Improve productivity for developers at any level of the stack 16 | 4. Work on a wide range of platforms 17 | 18 | As well as that, anybody working on Interfake should strive to: 19 | 20 | 1. Keep the code well-covered by tests 21 | 2. Take strong measures to *not* break the existing interface for developers 22 | 3. Enjoy themselves. If it ever becomes a chore, then you shouldn't feel obliged to continue :) 23 | 24 | So without further ado, here's some recommendations of how to get started! 25 | 26 | ## How to make a bug report 27 | 28 | 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: 29 | 30 | 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. 31 | 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. 32 | 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. 33 | 34 | 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**. 35 | 36 | ## How to make a feature request 37 | 38 | 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. 39 | 40 | 1. **Search through the existing issues for your feature**: If you find it, give it a :+1: (`:+1:`) and await a response. If it's been rejected, move on :) If you don't find it... 41 | 2. **Create an issue!** Please try to be as descriptive as possible with your feature, and mention in the title of the issue that it is a feature request so it can be tagged as such. I'll review it, but if it isn't appropriate, again, **please** don't take it personally. I mean you no offense :) 42 | 43 | ## How to make a Pull Request 44 | 45 | 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: 46 | 47 | 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. 48 | 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. 49 | 3. **Make your pull request**: If you don't know how to do this, I recommend you read this [excellent GitHub help article](https://help.github.com/articles/creating-a-pull-request). 50 | 4. **Wait**: I will have a look. If your request is useful it may still take a while to be merged, and it's possible that you will be asked to change a couple of things - or I might change a couple of things myself. Whatever the case, please co-operate, it's for the good of the project. 51 | 52 | ## Code Guidelines 53 | 54 | Please try to keep to the same style used throughout Interfake. In general, it favours brevity over verbosity for public interfaces, but verbosity where it helps in private interfaces and variables. In other words, keep things as short and intuitive as possible. No crucial variables called things like `grMod` or anything strange and obfuscated like that. 55 | 56 | Also, we use tabs, not spaces. No, this doesn't mean tabs are better than spaces. I just chose tabs, and we're sticking with it. 57 | 58 | ## Versioning 59 | 60 | Please **do not include version bumps in your pull requests**. If you have a bug fix by all means make a pull request with it, but don't bump the version. This is the responsibility of official contributors (currently just @basicallydan). If you'd like to be an official contributor, please make some contributions first, and then get in touch if you think you can make a lasting, more permanent contribution to the project. 61 | 62 | # Thanks! 63 | 64 | Thanks for reading this, it's important we're all on the same page for a project like this. I look forward to your contribution :) -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | Interfake is licensed under the MIT License: 2 | 3 | > Copyright (c) 2012-2014: Daniel Hough, and other contributors: 4 | > 5 | > https://github.com/basicallydan/interfake/contributors 6 | > 7 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | > 9 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | > 11 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples-command-line/crud.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/posts/1", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 404 9 | } 10 | }, 11 | { 12 | "request": { 13 | "url": "/posts", 14 | "method": "post" 15 | }, 16 | "response": { 17 | "code": 201, 18 | "body" : { 19 | "title": "First post", 20 | "version": 1, 21 | "links": { 22 | "self": "/posts/1" 23 | } 24 | } 25 | }, 26 | "afterResponse": { 27 | "endpoints": [ 28 | { 29 | "request": { 30 | "url": "/posts/1", 31 | "method": "get" 32 | }, 33 | "response": { 34 | "code": 200, 35 | "body": { 36 | "title": "First post", 37 | "version": 1, 38 | "links": { 39 | "self": "/posts/1" 40 | } 41 | } 42 | } 43 | }, 44 | { 45 | "request": { 46 | "url": "/posts/1", 47 | "method": "put" 48 | }, 49 | "response": { 50 | "code": 200 51 | }, 52 | "afterResponse": { 53 | "endpoints": [ 54 | { 55 | "request": { 56 | "url": "/posts/1", 57 | "method": "get" 58 | }, 59 | "response": { 60 | "code": 200, 61 | "body": { 62 | "title": "Modified First post", 63 | "version": 2, 64 | "links": { 65 | "self": "/posts/1" 66 | } 67 | } 68 | } 69 | } 70 | ] 71 | } 72 | }, 73 | { 74 | "request": { 75 | "url": "/posts/1", 76 | "method": "delete" 77 | }, 78 | "response": { 79 | "code": 210, 80 | "body": {} 81 | }, 82 | "afterResponse": { 83 | "endpoints": [ 84 | { 85 | "request": { 86 | "url": "/posts/1", 87 | "method": "get" 88 | }, 89 | "response": { 90 | "code": 410 91 | } 92 | } 93 | ] 94 | } 95 | } 96 | ] 97 | } 98 | } 99 | ] -------------------------------------------------------------------------------- /examples-command-line/get-and-post-with-after-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/books", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code":200, 9 | "body": { 10 | "books": [{ 11 | "id":1, 12 | "title":"The War of the Worlds" 13 | }] 14 | } 15 | } 16 | }, 17 | { 18 | "request": { 19 | "url": "/books", 20 | "method": "post" 21 | }, 22 | "response": { 23 | "code":201, 24 | "body": {} 25 | }, 26 | "afterResponse": { 27 | "endpoints": [ 28 | { 29 | "request": { 30 | "url": "/books", 31 | "method": "get" 32 | }, 33 | "response": { 34 | "code":200, 35 | "body": { 36 | "books": [ 37 | { 38 | "id":1, 39 | "title":"The War of the Worlds" 40 | }, 41 | { 42 | "id":2, 43 | "title":"Ender's Game" 44 | } 45 | ] 46 | } 47 | } 48 | } 49 | ] 50 | } 51 | } 52 | ] -------------------------------------------------------------------------------- /examples-command-line/modify-with-patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/posts/1", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 200, 9 | "body" : { 10 | "title": "First post", 11 | "version": 1, 12 | "links": { 13 | "self": "/posts/1" 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | "request": { 20 | "url": "/posts/1", 21 | "method": "patch" 22 | }, 23 | "response": { 24 | "code": 200, 25 | "body" : {} 26 | }, 27 | "afterResponse": { 28 | "modifications": [ 29 | { 30 | "request": { 31 | "url": "/posts/1", 32 | "method": "get" 33 | }, 34 | "response": { 35 | "body": { 36 | "links": { 37 | "next": "/posts/2" 38 | } 39 | } 40 | } 41 | } 42 | ] 43 | } 44 | } 45 | ] -------------------------------------------------------------------------------- /examples-command-line/ranged-delay.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/whattimeisit", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 200, 9 | "delay": "200..3000", 10 | "body": { 11 | "theTime": "Adventure Time!", 12 | "starring": [ 13 | "Finn", 14 | "Jake" 15 | ], 16 | "location": "ooo" 17 | } 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /examples-command-line/regexp-get.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": { 5 | "pattern" : "/what/the/.*", 6 | "regexp" : true 7 | }, 8 | "method": "get" 9 | }, 10 | "response": { 11 | "code": 200, 12 | "body": { 13 | "theTime": "Adventure Time!", 14 | "starring": [ 15 | "Finn", 16 | "Jake" 17 | ], 18 | "location": "ooo" 19 | }, 20 | "headers": { 21 | "X-Powered-By": "Interfake" 22 | } 23 | } 24 | } 25 | ] -------------------------------------------------------------------------------- /examples-command-line/simple-get.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/whattimeisit", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 200, 9 | "body": { 10 | "theTime": "Adventure Time!", 11 | "starring": [ 12 | "Finn", 13 | "Jake" 14 | ], 15 | "location": "ooo" 16 | }, 17 | "headers": { 18 | "X-Powered-By": "Interfake" 19 | } 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /examples-javascript/echo.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake({ debug: true }); 5 | interfake.createRoute({ 6 | request: { 7 | url: '/echo', 8 | method: 'post' 9 | }, 10 | response: { 11 | code: 200, 12 | echo: true 13 | } 14 | }); 15 | 16 | interfake.listen(3030); // The server will listen on port 3030 17 | 18 | request({ method : 'post', url : 'http://localhost:3030/echo', json : true, body : { echo : 'Echo!' } }, function (error, response, body) { 19 | console.log(response.statusCode); // prints 200 20 | console.log(body); // prints { "echo" : "Echo!" } 21 | }); 22 | -------------------------------------------------------------------------------- /examples-javascript/fluent-crud.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake(); 5 | 6 | // We start with an empty set of items 7 | interfake.get('/items').body({ items: [] }); 8 | 9 | // When an item is created, update the items set request 10 | var postRequest = interfake.post('/items').status(201); 11 | postRequest.extends.get('/items').body({ items: [ 12 | { id: 1, name: 'Item 1' } 13 | ]}); 14 | 15 | // And also create an endpoint for our new item 16 | postRequest.creates.get('/items/1').body({ id: 1, name: 'Item 1' }); 17 | 18 | // Also create a PUT request for our new item so we can edit it 19 | var putRequest = postRequest.creates.put('/items/1').body({ items: [ 20 | { id: 1, name: 'Item One' } 21 | ]}); 22 | var deleteRequest = postRequest.creates.delete('/items/1').status(204); 23 | 24 | // But when the put request is hit we need to edit both 25 | putRequest.extends.get('/items').body({ items: { 26 | 0 : { name: 'Item One' } 27 | }}); 28 | 29 | // And also create an endpoint for our new item 30 | putRequest.extends.get('/items/1').body({ name: 'Item One' }); 31 | 32 | deleteRequest.creates.get('/items/1').status(410); 33 | 34 | interfake.listen(3030); // The server will listen on port 3030 35 | 36 | function printStatusAndBody(error, response, body) { 37 | console.log(response.statusCode); 38 | console.log(body); 39 | } 40 | 41 | request('http://localhost:3030/items', function (error, response, body) { 42 | console.log('1. No items from GET /items.'); 43 | printStatusAndBody.apply(null, arguments); 44 | request.post('http://localhost:3030/items', function () { 45 | console.log('2. Create an item with POST /items.'); 46 | printStatusAndBody.apply(null, arguments); 47 | request.get('http://localhost:3030/items', function() { 48 | console.log('3. New item in GET /items.'); 49 | printStatusAndBody.apply(null, arguments); 50 | request.get('http://localhost:3030/items/1', function () { 51 | console.log('4. New item at GET /items/1.'); 52 | printStatusAndBody.apply(null, arguments); 53 | request.put('http://localhost:3030/items/1', function () { 54 | console.log('5. Update item with PUT /items/1.'); 55 | printStatusAndBody.apply(null, arguments); 56 | request.get('http://localhost:3030/items/1', function () { 57 | console.log('6. Updated item at GET /items/1.'); 58 | printStatusAndBody.apply(null, arguments); 59 | request.del('http://localhost:3030/items/1', function () { 60 | console.log('7. Deleted item at GET /items/1.'); 61 | printStatusAndBody.apply(null, arguments); 62 | request.get('http://localhost:3030/items/1', function () { 63 | console.log('7. Can no longer request item at GET /items/1.'); 64 | printStatusAndBody.apply(null, arguments); 65 | }); 66 | }); 67 | }); 68 | }); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /examples-javascript/fluent-echo.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake({ debug: true }); 5 | interfake.post('/echo').echo(); 6 | interfake.listen(3030); // The server will listen on port 3030 7 | 8 | request({ method : 'post', url : 'http://localhost:3030/echo', json : true, body : { echo : 'Echo!' } }, function (error, response, body) { 9 | console.log(response.statusCode); // prints 200 10 | console.log(body); // prints { "echo" : "Echo!" } 11 | }); 12 | -------------------------------------------------------------------------------- /examples-javascript/fluent-extends.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var interfake = new Interfake(); 3 | interfake.get('/items').status(200).body({ items: [ { id: 1, name: 'Item 1' } ] }); 4 | interfake.get('/items/1').status(200).body({ id: 1, name: 'Item 1' }); 5 | var postResponse = interfake.post('/items').status(201).body({ created : true }); 6 | postResponse.creates.get('/items/2').status(200).body({ id: 2, name: 'Item 2' }); 7 | postResponse.extends.get('/items').status(200).body({ items: [ { id: 2, name: 'Item 2' } ] }); 8 | interfake.listen(3000); 9 | 10 | /* 11 | # Request: 12 | $ curl http://localhost:3000/items -X GET 13 | # Response: 14 | 200 15 | { 16 | "items" : [ 17 | { 18 | "id":1 19 | "name":"Item 1" 20 | } 21 | ] 22 | } 23 | 24 | # Request: 25 | $ curl http://localhost:3000/items -X POST 26 | # Response: 27 | 201 28 | { 29 | "created":true 30 | } 31 | 32 | 33 | # Request: 34 | $ curl http://localhost:3000/items -X GET 35 | # Response: 36 | 200 37 | { 38 | "items" : [ 39 | { 40 | "id":1 41 | "name":"Item 1" 42 | }, 43 | { 44 | "id":2 45 | "name":"Item 2" 46 | } 47 | ] 48 | } 49 | */ -------------------------------------------------------------------------------- /examples-javascript/fluent-proxy.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake({ debug: true }); 5 | interfake.get('/interfake-tags').proxy({ 6 | url: 'https://api.github.com/repos/basicallydan/interfake/tags', 7 | headers: { 8 | 'User-Agent': 'Interfake Proxy Whoop', 9 | 'Accept': 'application/vnd.github.full+json' 10 | } 11 | }); 12 | interfake.listen(3030); // The server will listen on port 3030 13 | 14 | request('http://localhost:3030/interfake-tags', function (error, response, body) { 15 | console.log(error); 16 | console.log(response.statusCode); // prints 200 17 | console.log(body); // prints { "next" : "more stuff" } 18 | }); 19 | -------------------------------------------------------------------------------- /examples-javascript/fluent-regex.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake(); 5 | interfake.get(/\/*.?-time/).body({ time : 'It\'s something time!'}); 6 | // If you give it something more specific it'll respect that (see below) 7 | interfake.get(/\/great-time/).body({ time : 'It\'s not regular expression time!'}); 8 | interfake.listen(3030); // The server will listen on port 3030 9 | 10 | request('http://localhost:3030/adventure-time', function (error, response, body) { 11 | console.log(response.statusCode); // prints 200 12 | console.log(body); // prints { "time" : "It's something time!" } 13 | }); 14 | 15 | request('http://localhost:3030/lunch-time', function (error, response, body) { 16 | console.log(response.statusCode); // prints 200 17 | console.log(body); // prints { "time" : "It's something time!" } 18 | }); 19 | 20 | request('http://localhost:3030/whatever-time', function (error, response, body) { 21 | console.log(response.statusCode); // prints 200 22 | console.log(body); // prints { "time" : "It's something time!" } 23 | }); 24 | 25 | request('http://localhost:3030/great-time', function (error, response, body) { 26 | console.log(response.statusCode); // prints 200 27 | console.log(body); // prints { "time" : "It's not regex time!" } 28 | }); 29 | -------------------------------------------------------------------------------- /examples-javascript/fluent-simple.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake({ debug: true }); 5 | interfake.get('/whats-next').body({ next : 'more stuff '}); 6 | interfake.listen(3030); // The server will listen on port 3030 7 | 8 | request('http://localhost:3030/whats-next', function (error, response, body) { 9 | console.log(response.statusCode); // prints 200 10 | console.log(body); // prints { "next" : "more stuff" } 11 | }); 12 | -------------------------------------------------------------------------------- /examples-javascript/fluent-web-page-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Interfake it 'til you make it 4 | 5 | 16 | 17 | 18 | Not updated! 19 | Update 20 | 21 | -------------------------------------------------------------------------------- /examples-javascript/fluent-web-page-test.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var assert = require('assert'); 3 | var path = require('path'); 4 | var filePath = path.join(__dirname, './'); 5 | console.log('Gonna open', filePath); 6 | 7 | var interfake = new Interfake(); 8 | // var browser = new Browser(); 9 | 10 | // Serve up this folder as a website so we can test it 11 | interfake.serveStatic('/static', filePath); 12 | // Create the /update endpoint 13 | interfake.get('/update').body({ text : 'Updated text!'}); 14 | // Start the server 15 | interfake.listen(3000); 16 | 17 | // Zombie is annoyingly buggy and unreliable but if you can get it to work you can use this script below... 18 | // Use zombie to visit the page 19 | // browser.visit('http://localhost:3000/static/fluent-web-page-test.html') 20 | // .then(function() { 21 | // // When we start, the text of the #target element is 'Not updated!' 22 | // assert.equal(browser.text("#target"), 'Not updated!'); 23 | // }) 24 | // .then(function() { 25 | // // The 'Update' link will trigger an XHR call to /update and update the text with the response data 26 | // return browser.clickLink('Update'); 27 | // }) 28 | // .then(function () { 29 | // // Give it a sec... 30 | // return browser.wait(150); 31 | // }) 32 | // .then(function () { 33 | // // Voila! It has updated. Magic. 34 | // assert.equal(browser.text('#target'), 'Updated text!'); 35 | // }) 36 | // .fail(function(error) { 37 | // console.log('Error', error); 38 | // browser.close(); 39 | // }) 40 | // .done(function() { 41 | // console.log('All asserts passed just fine!'); 42 | // browser.close(); 43 | // }); -------------------------------------------------------------------------------- /examples-javascript/load-file-reload-unload.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake(); 5 | var file = interfake.loadFile(require.resolve('./load-file-reload-unload.json')); // We can get a reference to the file we just loaded 6 | 7 | interfake.listen(3030); // The server will listen on port 3030 8 | 9 | request('http://localhost:3030/whattimeisit', function(error, response, body) { 10 | console.log(response.statusCode); // prints 200 11 | console.log(body); // prints { "theTime" : "Adventure Time" } 12 | file = file.reload(); // The file will be reloaded, in case you changed it 13 | 14 | request('http://localhost:3030/whattimeisit', function(error, response, body) { 15 | console.log(response.statusCode); // prints 200 16 | console.log(body); // prints { "theTime" : "Adventure Time" } 17 | file = file.unload(); // The file can be unloaded, but this won't affect other routes 18 | 19 | request('http://localhost:3030/whattimeisit', function(error, response, body) { 20 | console.log(response.statusCode); // prints 404 21 | console.log(body); // prints blank line 22 | }); 23 | }); 24 | }); -------------------------------------------------------------------------------- /examples-javascript/load-file-reload-unload.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/whattimeisit", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 200, 9 | "body": { 10 | "theTime": "Adventure Time!", 11 | "starring": [ 12 | "Finn", 13 | "Jake" 14 | ], 15 | "location": "Ooo" 16 | }, 17 | "headers": { 18 | "X-Powered-By": "Interfake" 19 | } 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /examples-javascript/load-file-watch.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake({ }); 5 | var file = interfake.loadFile(require.resolve('./load-file-reload-unload.json'), { watch : true }); 6 | 7 | interfake.listen(3000); 8 | 9 | console.log('Try changing the file load-file-reload-unload.json and then make reuqests to it, at http://localhost:3000/whattimeisit'); -------------------------------------------------------------------------------- /examples-javascript/regexp-url.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); 3 | 4 | // Example showing how a regular expression can be used for the URL without using a RegExp type (e.g. you're specifying the endpoints in JSON) 5 | 6 | var interfake = new Interfake({ debug: true }); 7 | interfake.createRoute({ 8 | request: { 9 | url: { 10 | pattern: '/what/the/.*', 11 | regexp: true 12 | }, 13 | method: 'get' 14 | }, 15 | response: { 16 | code: 200, 17 | body: { 18 | next:'more stuff' 19 | } 20 | } 21 | }); 22 | 23 | interfake.listen(3030); // The server will listen on port 3030 24 | 25 | request('http://localhost:3030/what/the/heck', function (error, response, body) { 26 | console.log(response.statusCode); // prints 200 27 | console.log(body); // prints { "next" : "more stuff" } 28 | }); 29 | 30 | request('http://localhost:3030/what/the/fuzz', function (error, response, body) { 31 | console.log(response.statusCode); // prints 200 32 | console.log(body); // prints { "next" : "more stuff" } 33 | }); 34 | 35 | request('http://localhost:3030/what/the/what', function (error, response, body) { 36 | console.log(response.statusCode); // prints 200 37 | console.log(body); // prints { "next" : "more stuff" } 38 | }); 39 | -------------------------------------------------------------------------------- /examples-javascript/simple.js: -------------------------------------------------------------------------------- 1 | var Interfake = require('..'); 2 | var request = require('request'); // Let's use request for this example 3 | 4 | var interfake = new Interfake({ debug: true }); 5 | interfake.createRoute({ 6 | request: { 7 | url: '/whats-next', 8 | method: 'get' 9 | }, 10 | response: { 11 | code: 200, 12 | body: { 13 | next:'more stuff' 14 | } 15 | } 16 | }); 17 | 18 | interfake.listen(3030); // The server will listen on port 3030 19 | 20 | request('http://localhost:3030/whats-next', function (error, response, body) { 21 | console.log(response.statusCode); // prints 200 22 | console.log(body); // prints { "next" : "more stuff" } 23 | }); 24 | -------------------------------------------------------------------------------- /examples-javascript/testwatch.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var onChange = function () { 4 | console.log('Changed'); 5 | fs.watch(require.resolve('./load-file-reload-unload.json'), onChange); 6 | }; 7 | 8 | fs.watch(require.resolve('./load-file-reload-unload.json'), onChange); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var program = require('commander'); 4 | var Interfake = require('./lib/server'); 5 | var packageInfo = require('./package.json'); 6 | program 7 | .version(packageInfo.version) 8 | .option('-f, --file [file]', 'Load an API from a JSON file [file]') 9 | .option('-p --port [port]', 'Specify a port for Interfake to listen on', 3000) 10 | .option('-d --debug', 'Debug mode, turns on console messages') 11 | .option('-w --watch', 'If a loaded file changes, it will be re-loaded') 12 | .parse(process.argv); 13 | 14 | // console.log(program.args[0]); 15 | 16 | var opts = { 17 | debug: false 18 | }; 19 | 20 | if (program.debug) { 21 | opts.debug = true; 22 | } 23 | 24 | if (program.watch) { 25 | opts.watch = true; 26 | } 27 | 28 | var interfake = new Interfake(opts); 29 | 30 | if (program.file) { 31 | interfake.loadFile(program.file, opts); 32 | } 33 | 34 | interfake.listen(program.port); -------------------------------------------------------------------------------- /lib/cors.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res, next) { 2 | res.header('Access-Control-Allow-Origin', '*'); 3 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 4 | res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 5 | 6 | next(); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | function Deformic(name, enabled) { 2 | this.enabled = enabled || false; 3 | this.log = function() { 4 | if (this.enabled) { 5 | var args = Array.prototype.concat.call([ '(' + name + ')' ], Array.prototype.slice.call(arguments)); 6 | console.log.apply(console, args); 7 | } 8 | }.bind(this); 9 | this.log.enable = function () { 10 | console.log('Debugger enabled for', name); 11 | this.enabled = true; 12 | }.bind(this); 13 | return this.log; 14 | } 15 | 16 | function debug(name, enabled) { 17 | return new Deformic(name, enabled); 18 | } 19 | 20 | module.exports = debug; -------------------------------------------------------------------------------- /lib/fluent.js: -------------------------------------------------------------------------------- 1 | function FluentInterface(server, o) { 2 | o = o || { debug: false }; 3 | var debug = require('./debug')('interfake-fluent', o.debug); 4 | 5 | function throwCreatesNotSupportedError () { 6 | throw new Error('Sorry, but modified routes cannot yet create new routes after their response. This is planned for a future version of Interfake.'); 7 | } 8 | 9 | function throwExtendsNotSupportedError () { 10 | throw new Error('Sorry, but modified routes cannot yet modify existing routes after their response. This is planned for a future version of Interfake.'); 11 | } 12 | 13 | function fluentModify(method, route) { 14 | return function (path) { 15 | var modifyDescriptor = { 16 | routeDescriptor: { 17 | method: method, 18 | url: path 19 | } 20 | }; 21 | 22 | route.addModification(modifyDescriptor); 23 | 24 | return { 25 | query: function (query) { 26 | modifyDescriptor.routeDescriptor.query = query; 27 | return this; 28 | }, 29 | status: function (status) { 30 | modifyDescriptor.code = status; 31 | return this; 32 | }, 33 | body: function (body) { 34 | modifyDescriptor.body = body; 35 | return this; 36 | }, 37 | echo: function (echo) { 38 | if (typeof echo === 'undefined') { 39 | echo = true; 40 | } 41 | modifyDescriptor.echo = echo; 42 | return this; 43 | }, 44 | delay: function (delay) { 45 | modifyDescriptor.delay = delay; 46 | return this; 47 | }, 48 | responseHeaders: function (headers) { 49 | modifyDescriptor.responseHeaders = headers; 50 | return this; 51 | }, 52 | creates: { 53 | get: throwCreatesNotSupportedError, 54 | put: throwCreatesNotSupportedError, 55 | post: throwCreatesNotSupportedError, 56 | patch: throwCreatesNotSupportedError, 57 | delete: throwCreatesNotSupportedError 58 | }, 59 | extends: { 60 | get: throwExtendsNotSupportedError, 61 | put: throwExtendsNotSupportedError, 62 | post: throwExtendsNotSupportedError, 63 | patch: throwCreatesNotSupportedError, 64 | delete: throwExtendsNotSupportedError 65 | } 66 | }; 67 | }; 68 | } 69 | 70 | function fluentCreate(method, parent) { 71 | return function (originalPath) { 72 | var route; 73 | var routeDescriptor = { 74 | request: { 75 | url: originalPath, 76 | method: method 77 | } 78 | }; 79 | 80 | if (!parent) { 81 | debug('Fluent setup called for', routeDescriptor.request.url); 82 | route = server.createRoute(routeDescriptor); 83 | } else { 84 | debug('Fluent setup called for', routeDescriptor.request.url, 'with parent', parent.request.url); 85 | route = parent.addAfterResponse(routeDescriptor); 86 | } 87 | 88 | var fluentInterface = { 89 | query: function (query) { 90 | route.setQueryStrings(query); 91 | return this; 92 | }, 93 | status: function (status) { 94 | route.setStatusCode(status); 95 | return this; 96 | }, 97 | body: function (body) { 98 | debug('Changing body of', route.simpleHash(), 'to', body); 99 | route.setResponseBody(body); 100 | return this; 101 | }, 102 | echo: function (echo) { 103 | if (typeof echo === 'undefined') { 104 | echo = true; 105 | } 106 | route.setEcho(echo); 107 | return this; 108 | }, 109 | proxy: function (url) { 110 | route.setProxyURL(url); 111 | return this; 112 | }, 113 | delay: function (delay) { 114 | route.setResponseDelay(delay); 115 | return this; 116 | }, 117 | responseHeaders: function (headers) { 118 | route.setResponseHeaders(headers); 119 | return this; 120 | }, 121 | creates: { 122 | get: fluentCreate('get', route), 123 | put: fluentCreate('put', route), 124 | post: fluentCreate('post', route), 125 | patch: fluentCreate('patch', route), 126 | delete: fluentCreate('delete', route), 127 | }, 128 | extends: { 129 | get: fluentModify('get', route), 130 | put: fluentModify('put', route), 131 | post: fluentModify('post', route), 132 | patch: fluentModify('patch', route), 133 | delete: fluentModify('delete', route), 134 | } 135 | }; 136 | return fluentInterface; 137 | }; 138 | } 139 | 140 | this.fluentCreate = fluentCreate; 141 | } 142 | 143 | module.exports = FluentInterface; 144 | -------------------------------------------------------------------------------- /lib/grammar.pegjs: -------------------------------------------------------------------------------- 1 | start 2 | = endpoint + 3 | 4 | endpoint 5 | = m:method " " p:path " " s:status c:(" creates " e:endpoint + { return e; }) ? { return { method: m, path: p, status: s, creates: c } } 6 | 7 | method 8 | = $ "get" / "put" / "post" / "del" 9 | 10 | path 11 | = path:([/a-z] +) { return path.join(''); } 12 | 13 | status 14 | = status:[0-9]+ { return parseInt(status.join(""), 10); } -------------------------------------------------------------------------------- /lib/route.js: -------------------------------------------------------------------------------- 1 | var util = require('core-util-is'); 2 | var url = require('url'); 3 | var merge = require('merge'); 4 | var deepmerge = require('deepmerge'); 5 | 6 | function Route(descriptor, o) { 7 | var path; 8 | o = o || { debug: false }; 9 | this.debug = require('./debug')('interfake-route', o.debug); 10 | this.request = descriptor.request; 11 | this.response = descriptor.response; 12 | this.afterResponse = descriptor.afterResponse; 13 | this.o = o; 14 | 15 | if (!this.response) { 16 | this.response = { 17 | delay: 0, 18 | code: 200, 19 | body: {}, 20 | echo: false, 21 | headers: {} 22 | }; 23 | } else { 24 | this.response = merge({ 25 | delay: 0, 26 | code: 200, 27 | body: {}, 28 | echo: false, 29 | headers: {} 30 | }, this.response); 31 | } 32 | 33 | this.response.query = {}; 34 | 35 | if (!util.isRegExp(this.request.url) && util.isObject(this.request.url) && this.request.url.regexp) { 36 | this.debug('RegExp specified for URL', this.request.url.pattern); 37 | this.request.url = new RegExp(this.request.url.pattern); 38 | } 39 | 40 | if (util.isString(this.request.url)) { 41 | path = url.parse(this.request.url, true); 42 | this.request.url = path.pathname; 43 | this.setQueryStrings(path.query); 44 | } 45 | 46 | if (this.afterResponse && util.isArray(this.afterResponse.endpoints)) { 47 | this.afterResponse.endpoints = (this.afterResponse.endpoints || []).map(function (descriptor) { 48 | return new Route(descriptor); 49 | }); 50 | } 51 | 52 | if (this.afterResponse && util.isArray(this.afterResponse.modifications)) { 53 | this.afterResponse.modifications = (this.afterResponse.modifications || []).map(function (descriptor) { 54 | return { 55 | routeDescriptor: descriptor.request, 56 | body: descriptor.response.body, 57 | echo: descriptor.response.echo, 58 | code: descriptor.response.code, 59 | delay: descriptor.response.delay, 60 | responseHeaders: descriptor.response.headers 61 | }; 62 | }.bind(this)); 63 | } 64 | 65 | if (!this.afterResponse) { 66 | this.afterResponse = { 67 | endpoints: [], 68 | modifications: [] 69 | }; 70 | } 71 | 72 | if (!this.afterResponse.endpoints) { 73 | this.afterResponse.endpoints = []; 74 | } 75 | 76 | if (!this.afterResponse.modifications) { 77 | this.afterResponse.modifications = []; 78 | } 79 | 80 | 81 | this.debug('Creating route', this); 82 | this.debug('Creating route: ' + 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'); 83 | } 84 | 85 | Route.prototype.setQueryStrings = function (query) { 86 | this.request.query = merge(this.request.query, query || {}); 87 | }; 88 | 89 | Route.prototype.setStatusCode = function (statusCode) { 90 | this.debug('Now returning', statusCode); 91 | this.response.code = statusCode; 92 | }; 93 | 94 | Route.prototype.setResponseBody = function (body) { 95 | this.debug('Now returning body of length', JSON.stringify(body).length); 96 | this.response.body = body; 97 | }; 98 | 99 | Route.prototype.setProxyURL = function (url) { 100 | this.debug('The route is now a proxy of', this.request.method, url); 101 | this.response.proxy = url; 102 | }; 103 | 104 | Route.prototype.mergeResponseBody = function (body) { 105 | this.debug('Merging', body, 'into', this.response.body); 106 | this.response.body = deepmerge(this.response.body, body, { alwaysPush: true }); 107 | this.debug('Response body is now', this.response.body); 108 | }; 109 | 110 | Route.prototype.mergeResponseHeaders = function (headers) { 111 | this.debug('Merging', headers, 'into', this.response.headers); 112 | this.response.headers = deepmerge(this.response.headers, headers); 113 | this.debug('Response headers is now', this.response.headers); 114 | }; 115 | 116 | Route.prototype.setResponseDelay = function (delay) { 117 | this.debug('New delay is', delay); 118 | this.response.delay = delay; 119 | }; 120 | 121 | Route.prototype.setResponseHeaders = function (headers) { 122 | this.debug('Setting response headers to', headers); 123 | this.response.headers = headers; 124 | }; 125 | 126 | Route.prototype.setEcho = function (echo) { 127 | this.response.echo = echo; 128 | }; 129 | 130 | Route.prototype.addAfterResponse = function (descriptors) { 131 | var newRoute = new Route(descriptors, this.o); 132 | this.debug('Adding after response', newRoute.hash()); 133 | this.afterResponse.endpoints.push(newRoute); 134 | return newRoute; 135 | }; 136 | 137 | Route.prototype.addModification = function (modifyDescriptor) { 138 | if (!this.afterResponse.modifications) { 139 | this.afterResponse.modifications = []; 140 | } 141 | 142 | this.afterResponse.modifications.push(modifyDescriptor); 143 | }; 144 | 145 | Route.prototype.creates = function (routeDescriptor) { 146 | var newRoute = new Route(routeDescriptor, this.o); 147 | if (!this.afterResponse) { 148 | this.afterResponse = {}; 149 | } 150 | if (!this.afterResponse.endpoints) { 151 | this.afterResponse.endpoints = []; 152 | } 153 | this.afterResponse.endpoints.push(newRoute); 154 | return newRoute; 155 | }; 156 | 157 | Route.prototype.hasRegexpURL = function () { 158 | return util.isRegExp(this.request.url); 159 | }; 160 | 161 | Route.prototype.simpleHash = function () { 162 | var routeHash = this.request.method.toUpperCase() + ' ' + this.request.url; 163 | return routeHash; 164 | }; 165 | 166 | Route.prototype.queryKeys = function () { 167 | var queryKeys = Object.keys(this.request.query || {}).filter(function (key) { 168 | return key !== 'callback'; 169 | }); 170 | return queryKeys; 171 | }; 172 | 173 | Route.prototype.hash = function () { 174 | var routeHash; 175 | if (util.isString(this.request.url)) { 176 | routeHash = this.request.method.toUpperCase() + ' ' + this.request.url; 177 | } else { 178 | routeHash = new RegExp(this.request.method.toUpperCase() + ' ' + this.request.url); 179 | } 180 | var fullQuerystring, querystringArray = []; 181 | if (this.request.query) { 182 | var queryKeys = this.queryKeys(); 183 | 184 | if (queryKeys.length) { 185 | this.debug('Query keys are', queryKeys); 186 | 187 | fullQuerystring = this.request.query; 188 | delete fullQuerystring.callback; 189 | 190 | queryKeys.sort().forEach(function (k) { 191 | querystringArray.push(k + '=' + fullQuerystring[k]); 192 | }); 193 | 194 | this.debug('Full query string items are', querystringArray); 195 | 196 | routeHash += '?' + querystringArray.join('&'); 197 | this.debug('Final route is', routeHash); 198 | } 199 | } 200 | 201 | this.debug('Lookup hash key will be: ' + routeHash); 202 | return routeHash; 203 | }; 204 | 205 | Route.prototype.compare = function (route) { 206 | var queryKeys; 207 | var routeQueryKeys; 208 | var same = true; 209 | 210 | if (this.hasRegexpURL()) { 211 | this.debug('1. Testing', route.simpleHash(), 'matches', this.simpleHash()); 212 | return this.request.url.test(route.request.url) && this.request.method.toLowerCase() === route.request.method.toLowerCase(); 213 | } else if (route.simpleHash() !== this.simpleHash()) { 214 | this.debug('1. Comparing', route.simpleHash(), 'and', this.simpleHash()); 215 | this.debug('No match'); 216 | return false; 217 | } 218 | 219 | this.debug('2. Testing', route.request.url, 'matches', this.request.url); 220 | if (util.isRegExp(this.request.url)) { 221 | if (!this.request.url.test(route.request.url)) { 222 | this.debug('No match.'); 223 | return false; 224 | } 225 | } 226 | 227 | queryKeys = this.queryKeys(); 228 | routeQueryKeys = route.queryKeys(); 229 | 230 | this.debug('3. Comparing', queryKeys.length, 'and', routeQueryKeys.length); 231 | if (queryKeys.length !== routeQueryKeys.length) { 232 | this.debug('No match'); 233 | return false; 234 | } 235 | 236 | this.debug('4. Comparing', this.request.query, 'and', route.request.query); 237 | queryKeys.forEach(function (key) { 238 | var routeQueryValue = route.request.query[key]; 239 | if (util.isRegExp(this.request.query[key])) { 240 | this.debug('4b. Testing', routeQueryValue, 'matches', this.request.query[key]); 241 | same = same && this.request.query[key].test(routeQueryValue); 242 | this.debug('4c. Result is', same); 243 | } else if (util.isArray(this.request.query[key])) { 244 | this.debug('4b. Testing', routeQueryValue, 'matches', this.request.query[key]); 245 | same = same && JSON.stringify(this.request.query[key].sort()) === JSON.stringify(routeQueryValue.sort()); 246 | this.debug('4c. Result is', same); 247 | } else { 248 | // Not checking type because 1 and '1' are the same in this case 249 | this.debug('4b. Comparing', this.request.query[key], 'and', routeQueryValue); 250 | same = same && this.request.query[key] == routeQueryValue; 251 | this.debug('4c. Result is', same); 252 | } 253 | }.bind(this)); 254 | 255 | return same; 256 | }; 257 | 258 | module.exports = Route; 259 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var serveStatic = require('serve-static'); 3 | var path = require('path'); 4 | var FluentInterface = require('./fluent'); 5 | var corsMiddleware = require('./cors'); 6 | var util = require('core-util-is'); 7 | var filter = require('lodash/filter'); 8 | var each = require('lodash/each'); 9 | var last = require('lodash/last'); 10 | var url = require('url'); 11 | var connectJson = require('connect-json'); 12 | var bodyParser = require('body-parser'); 13 | var Route = require('./route'); 14 | var watchr = require('watchr'); 15 | var fs = require('fs'); 16 | var request = require('request'); 17 | var supportedNonOptionsMethods = ['get','post','put','patch','delete']; 18 | 19 | function determineDelay(delayInput) { 20 | var result = 0, 21 | range, upper, lower; 22 | if (util.isNumber(delayInput)) { 23 | result = delayInput; 24 | } else if (util.isString(delayInput)) { 25 | range = /([0-9]+)..([0-9]+)/.exec(delayInput); 26 | upper = +range[2]; 27 | lower = +range[1]; 28 | result = Math.floor(Math.random() * (upper - lower + 1) + lower); 29 | } 30 | 31 | return result; 32 | } 33 | 34 | function Interfake(o) { 35 | o = o || { 36 | debug: false 37 | }; 38 | this.o = o; 39 | var app = express(); 40 | var router = express.Router(); 41 | var fluentInterface = new FluentInterface(this, o); 42 | var debug = require('./debug')('interfake-server', o.debug); 43 | var expectationsLookup = {}; 44 | var server; 45 | 46 | o.path = url.resolve('/', o.path || ''); 47 | 48 | debug('Root path is', o.path); 49 | 50 | app.use(connectJson()); 51 | app.use(bodyParser.urlencoded({ 52 | extended: true 53 | })); 54 | app.use(bodyParser.json()); 55 | app.use(corsMiddleware); 56 | 57 | app.post('/_requests?', function(req, res) { 58 | try { 59 | debug('Being hit with a _request request'); 60 | createRoute(req.body); 61 | debug('Returning'); 62 | res.status(201).send({ 63 | done: true 64 | }); 65 | } catch (e) { 66 | debug('Error: ', e); 67 | res.status(400).send(e); 68 | } 69 | }); 70 | 71 | app.all(o.path + '*', function(req, res, next) { 72 | debug(req.method, 'request to', req.url); 73 | if (o.onRequest && typeof(o.onRequest) === 'function') { 74 | o.onRequest(req); 75 | } 76 | var hashData = { 77 | method: req.method, 78 | url: req.path, 79 | query: req.query 80 | }; 81 | var rootPathRegex, responseBody, responseStatusCode, responseHeaders, expectedRoute, expectedRoutes; 82 | var proxyQueryStrings; 83 | var sendResponse = function () { 84 | var responseBodyLength = 0; 85 | if (responseBody) { 86 | responseBodyLength = JSON.stringify(responseBody).length; 87 | responseBody = JSON.parse(JSON.stringify(responseBody)); 88 | } 89 | debug('Sending a', responseStatusCode, 'response with a body whose length is', responseBodyLength); 90 | if (responseHeaders) { 91 | Object.keys(responseHeaders).forEach(function(k) { 92 | res.setHeader(k, responseHeaders[k]); 93 | }); 94 | } 95 | 96 | if (req.query.callback) { 97 | debug('Request is asking for jsonp'); 98 | if (typeof responseBody !== 'string') responseBody = JSON.stringify(responseBody); 99 | responseBody = req.query.callback.trim() + '(' + responseBody + ');'; 100 | } 101 | 102 | debug('Sending response now'); 103 | 104 | res.status(responseStatusCode).send(responseBody); 105 | }; 106 | 107 | if (o.path.length > 1) { 108 | rootPathRegex = new RegExp('^' + o.path); 109 | if (!rootPathRegex.test(hashData.url)) { 110 | return res.send(404); 111 | } 112 | hashData.url = hashData.url.replace(rootPathRegex, ''); 113 | } 114 | 115 | if (req.method.toLowerCase() === 'options') { 116 | expectedRoutes = this.routesMatchingPartial({ 117 | url : hashData.url, 118 | query : hashData.query 119 | }); 120 | if (!expectedRoutes.length) { 121 | debug('Couldn\'t find any routes for', hashData.url); 122 | return next(); 123 | } 124 | 125 | responseStatusCode = 200; 126 | responseHeaders = { 127 | 'Access-Control-Allow-Methods' : expectedRoutes.map(function (route) { 128 | return route.request.method.toUpperCase(); 129 | }).join(', ') + ', OPTIONS' 130 | }; 131 | 132 | debug('Response headers are now', responseHeaders); 133 | 134 | return sendResponse(); 135 | } 136 | 137 | expectedRoute = this.routeMatching(hashData); 138 | 139 | if (!expectedRoute) { 140 | debug('Couldn\'t find a route for', hashData.method, hashData.url); 141 | return next(); 142 | // return res.status(404).end(); 143 | } 144 | 145 | var specifiedResponse = expectedRoute.response; // req.route.responseData; 146 | var afterSpecifiedResponse = expectedRoute.afterResponse; //req.route.afterResponseData; 147 | var responseDelay = determineDelay(specifiedResponse.delay); 148 | 149 | if (specifiedResponse.proxy) { 150 | proxyQueryStrings = {}; 151 | if (req.query) { 152 | Object.keys(req.query).forEach(function(key) { 153 | if (key !== 'callback') { 154 | proxyQueryStrings[key] = req.query[key]; 155 | } 156 | }); 157 | } 158 | debug('Proxy query strings are', proxyQueryStrings); 159 | request[req.method.toLowerCase()]({ url : specifiedResponse.proxy.url || specifiedResponse.proxy, json : true, body : req.body, qs: proxyQueryStrings, headers : specifiedResponse.proxy.headers || {} }, function (error, response, body) { 160 | debug('Response from ', specifiedResponse.proxy, 'is', body, 'with headers', response.headers); 161 | responseBody = body; 162 | responseStatusCode = response.statusCode; 163 | responseHeaders = response.headers; 164 | delete responseHeaders['content-length']; 165 | sendResponse(); 166 | }); 167 | } else { 168 | responseBody = specifiedResponse.body; 169 | responseStatusCode = specifiedResponse.code; 170 | responseHeaders = specifiedResponse.headers; 171 | 172 | debug('specifiedResponse is', specifiedResponse); 173 | 174 | if (specifiedResponse.echo) { 175 | debug('Echo! Gonna return', req.body); 176 | responseBody = req.body; 177 | } 178 | 179 | debug(req.method, 'request to', req.url, 'returning', responseStatusCode); 180 | debug(req.method, 'request to', req.url, 'will be delayed by', responseDelay, 'millis'); 181 | debug('Expected route is', expectedRoute); 182 | 183 | res.setHeader('Content-Type', 'application/json'); 184 | 185 | setTimeout(function() { 186 | var modification; 187 | debug('Expected response is', specifiedResponse); 188 | 189 | sendResponse(); 190 | 191 | if (afterSpecifiedResponse && afterSpecifiedResponse.endpoints) { 192 | debug('Response sent, setting up', afterSpecifiedResponse.endpoints.length, 'endpoints'); 193 | afterSpecifiedResponse.endpoints.forEach(function(route) { 194 | debug('Setting up route', route.hash()); 195 | addRoute(route); 196 | }); 197 | } 198 | 199 | if (afterSpecifiedResponse && afterSpecifiedResponse.modifications) { 200 | debug('Response sent, making', afterSpecifiedResponse.modifications.length, 'modifications'); 201 | while (afterSpecifiedResponse.modifications.length > 0) { 202 | modification = afterSpecifiedResponse.modifications.pop(); 203 | 204 | modification.route = this.routeMatching(modification.routeDescriptor); 205 | 206 | if (!modification.route) { 207 | throw new Error('No route matching', modification.routeDescriptor, 'was found'); 208 | } 209 | 210 | if (modification.body) { 211 | debug('Modifying body'); 212 | modification.route.mergeResponseBody(modification.body); 213 | } 214 | 215 | if (!util.isNullOrUndefined(modification.echo)) { 216 | debug('Modifying echo to', modification.echo); 217 | modification.route.setEcho(modification.echo); 218 | } 219 | 220 | if (modification.code) { 221 | debug('Modifying code to', modification.code); 222 | modification.route.setStatusCode(modification.code); 223 | } 224 | 225 | if (modification.delay) { 226 | debug('Modifying delay to', modification.delay); 227 | modification.route.setResponseDelay(modification.delay); 228 | } 229 | 230 | if (modification.responseHeaders) { 231 | debug('Modifying response headers'); 232 | modification.route.mergeResponseHeaders(modification.responseHeaders); 233 | } 234 | } 235 | } 236 | }.bind(this), responseDelay); 237 | } 238 | }.bind(this)); 239 | 240 | function addRoute(route) { 241 | if (expectationsLookup[route.simpleHash()]) { 242 | expectationsLookup[route.simpleHash()].push(route); 243 | } else { 244 | expectationsLookup[route.simpleHash()] = [route]; 245 | } 246 | } 247 | 248 | function createRoute(data) { 249 | var newRoute; 250 | 251 | debug('Setting up new route'); 252 | 253 | newRoute = new Route(data, o); 254 | 255 | addRoute(newRoute); 256 | 257 | debug('Setup complete'); 258 | 259 | return newRoute; 260 | } 261 | 262 | function removeRoute(route) { 263 | var indexInLookup; 264 | 265 | if (!route.simpleHash) { 266 | route = new Route(route, o); 267 | } 268 | 269 | if (expectationsLookup[route.simpleHash()]) { 270 | indexInLookup = expectationsLookup[route.simpleHash()].indexOf(route); 271 | expectationsLookup[route.simpleHash()].splice(indexInLookup, 1); 272 | } else { 273 | debug('No route could be found with the hash', route.simpleHash(), 'so no route was removed'); 274 | } 275 | } 276 | 277 | this.createRoute = createRoute; 278 | this.removeRoute = removeRoute; 279 | 280 | this.get = fluentInterface.fluentCreate('get'); 281 | this.post = fluentInterface.fluentCreate('post'); 282 | this.put = fluentInterface.fluentCreate('put'); 283 | this.patch = fluentInterface.fluentCreate('patch'); 284 | this.delete = fluentInterface.fluentCreate('delete'); 285 | this.options = fluentInterface.fluentCreate('options'); 286 | 287 | this.expressApp = app; 288 | 289 | this.serveStatic = function(urlPath, directory) { 290 | urlPath = urlPath || '/_static'; 291 | var directoryToUse = path.resolve(process.cwd(), directory); 292 | debug('Serving to', urlPath, 'with directory', directoryToUse); 293 | app.use(urlPath, express.static(directoryToUse + '')); 294 | }; 295 | 296 | // Returns an array of routes matching the descriptor where the method is blank 297 | this.routesMatchingPartial = function(partialRequestDescriptor) { 298 | var routes = []; 299 | supportedNonOptionsMethods.forEach(function (method) { 300 | var fullDescriptor = { 301 | url : partialRequestDescriptor.url, 302 | method : method, 303 | query : partialRequestDescriptor.query 304 | }; 305 | var route = this.routeMatching(fullDescriptor); 306 | if (route) { 307 | routes.push(route); 308 | } 309 | }.bind(this)); 310 | return routes; 311 | }; 312 | 313 | this.routeMatching = function(requestDescriptor) { 314 | var incomingRoute = new Route({ 315 | request: requestDescriptor 316 | }, o); 317 | 318 | var expectData = expectationsLookup[incomingRoute.simpleHash()]; 319 | 320 | if (util.isArray(expectData)) { 321 | expectData = expectData.filter(function(route) { 322 | return route.compare(incomingRoute); 323 | }); 324 | } else if (!expectData) { 325 | // Nothing matched, but there may be a regex in there 326 | expectData = last(filter(expectationsLookup, function (routes) { 327 | var route = last(routes); 328 | return route.compare(incomingRoute); 329 | })) || []; 330 | } 331 | 332 | if (expectData.length) { 333 | return last(expectData); 334 | } 335 | 336 | return; 337 | }; 338 | 339 | this.listen = function(port, callback) { 340 | port = port || 3000; 341 | server = app.listen(port, function() { 342 | debug('Interfake is listening for requests on port', server.address().port); 343 | if (util.isFunction(callback)) { 344 | callback(); 345 | } 346 | }); 347 | }; 348 | 349 | this.stop = function() { 350 | if (server) { 351 | debug('Interfake is stopping'); 352 | server.close(function() { 353 | debug('Interfake has stopped'); 354 | server = undefined; 355 | }); 356 | } 357 | }; 358 | 359 | this.clearAllRoutes = function() { 360 | expectationsLookup = {}; 361 | }; 362 | } 363 | 364 | Interfake.prototype.loadFile = function(filePath, opts) { 365 | var file; 366 | var debug = require('./debug')('interfake-server-load-file', this.o.debug); 367 | 368 | filePath = path.resolve(process.cwd(), filePath); 369 | 370 | delete require.cache[require.resolve(filePath)]; 371 | file = require(filePath); 372 | 373 | file.forEach(function(endpoint) { 374 | this.createRoute(endpoint); 375 | }.bind(this)); 376 | 377 | var unload = function() { 378 | file.forEach(function(endpoint) { 379 | this.removeRoute(endpoint); 380 | }.bind(this)); 381 | }.bind(this); 382 | 383 | var reload = function() { 384 | unload(); 385 | return this.loadFile(filePath, opts); 386 | }.bind(this); 387 | 388 | var onFileChange = function (type) { 389 | reload(); 390 | if (opts && opts.watch) { 391 | debug('Watching for changes at', filePath); 392 | fs.watch(filePath, onFileChange); 393 | } 394 | }; 395 | 396 | if (opts && opts.watch) { 397 | debug('Watching for changes at', filePath); 398 | fs.watch(filePath, onFileChange); 399 | } 400 | 401 | return { 402 | unload: unload, 403 | reload: reload 404 | }; 405 | }; 406 | 407 | module.exports = Interfake; -------------------------------------------------------------------------------- /npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interfake", 3 | "version": "1.20.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "ajv": { 17 | "version": "6.10.2", 18 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", 19 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", 20 | "dev": true, 21 | "requires": { 22 | "fast-deep-equal": "^2.0.1", 23 | "fast-json-stable-stringify": "^2.0.0", 24 | "json-schema-traverse": "^0.4.1", 25 | "uri-js": "^4.2.2" 26 | } 27 | }, 28 | "ambi": { 29 | "version": "2.5.0", 30 | "resolved": "https://registry.npmjs.org/ambi/-/ambi-2.5.0.tgz", 31 | "integrity": "sha1-fI43K+SIkRV+fOoBy2+RQ9H3QiA=", 32 | "requires": { 33 | "editions": "^1.1.1", 34 | "typechecker": "^4.3.0" 35 | } 36 | }, 37 | "api-easy": { 38 | "version": "0.4.0", 39 | "resolved": "https://registry.npmjs.org/api-easy/-/api-easy-0.4.0.tgz", 40 | "integrity": "sha1-jKLLH/dtxnkW6DUH2qbFCVnnFu8=", 41 | "dev": true, 42 | "requires": { 43 | "qs": "~2.3.3", 44 | "request": "2.x.x", 45 | "vows": "0.8.x" 46 | }, 47 | "dependencies": { 48 | "qs": { 49 | "version": "2.3.3", 50 | "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", 51 | "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=", 52 | "dev": true 53 | } 54 | } 55 | }, 56 | "array-flatten": { 57 | "version": "1.1.1", 58 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 59 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 60 | }, 61 | "asn1": { 62 | "version": "0.2.4", 63 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 64 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 65 | "dev": true, 66 | "requires": { 67 | "safer-buffer": "~2.1.0" 68 | } 69 | }, 70 | "assert-plus": { 71 | "version": "1.0.0", 72 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 73 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 74 | "dev": true 75 | }, 76 | "asynckit": { 77 | "version": "0.4.0", 78 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 79 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 80 | "dev": true 81 | }, 82 | "aws-sign2": { 83 | "version": "0.7.0", 84 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 85 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 86 | "dev": true 87 | }, 88 | "aws4": { 89 | "version": "1.8.0", 90 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 91 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", 92 | "dev": true 93 | }, 94 | "balanced-match": { 95 | "version": "1.0.0", 96 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 97 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 98 | "dev": true 99 | }, 100 | "bcrypt-pbkdf": { 101 | "version": "1.0.2", 102 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 103 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 104 | "dev": true, 105 | "requires": { 106 | "tweetnacl": "^0.14.3" 107 | } 108 | }, 109 | "body-parser": { 110 | "version": "1.19.0", 111 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 112 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 113 | "requires": { 114 | "bytes": "3.1.0", 115 | "content-type": "~1.0.4", 116 | "debug": "2.6.9", 117 | "depd": "~1.1.2", 118 | "http-errors": "1.7.2", 119 | "iconv-lite": "0.4.24", 120 | "on-finished": "~2.3.0", 121 | "qs": "6.7.0", 122 | "raw-body": "2.4.0", 123 | "type-is": "~1.6.17" 124 | } 125 | }, 126 | "brace-expansion": { 127 | "version": "1.1.11", 128 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 129 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 130 | "dev": true, 131 | "requires": { 132 | "balanced-match": "^1.0.0", 133 | "concat-map": "0.0.1" 134 | } 135 | }, 136 | "browser-stdout": { 137 | "version": "1.3.1", 138 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 139 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 140 | "dev": true 141 | }, 142 | "bytes": { 143 | "version": "3.1.0", 144 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 145 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 146 | }, 147 | "caseless": { 148 | "version": "0.12.0", 149 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 150 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 151 | "dev": true 152 | }, 153 | "combined-stream": { 154 | "version": "1.0.8", 155 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 156 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 157 | "dev": true, 158 | "requires": { 159 | "delayed-stream": "~1.0.0" 160 | } 161 | }, 162 | "commander": { 163 | "version": "2.3.0", 164 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", 165 | "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=" 166 | }, 167 | "concat-map": { 168 | "version": "0.0.1", 169 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 170 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 171 | "dev": true 172 | }, 173 | "connect": { 174 | "version": "3.7.0", 175 | "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", 176 | "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", 177 | "dev": true, 178 | "requires": { 179 | "debug": "2.6.9", 180 | "finalhandler": "1.1.2", 181 | "parseurl": "~1.3.3", 182 | "utils-merge": "1.0.1" 183 | } 184 | }, 185 | "connect-json": { 186 | "version": "0.0.0", 187 | "resolved": "https://registry.npmjs.org/connect-json/-/connect-json-0.0.0.tgz", 188 | "integrity": "sha1-jmNEKKy16+zW7EiE/BEGzbKCGAg=" 189 | }, 190 | "content-disposition": { 191 | "version": "0.5.3", 192 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 193 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 194 | "requires": { 195 | "safe-buffer": "5.1.2" 196 | } 197 | }, 198 | "content-type": { 199 | "version": "1.0.4", 200 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 201 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 202 | }, 203 | "cookie": { 204 | "version": "0.4.0", 205 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 206 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 207 | }, 208 | "cookie-signature": { 209 | "version": "1.0.6", 210 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 211 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 212 | }, 213 | "core-util-is": { 214 | "version": "1.0.2", 215 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 216 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 217 | }, 218 | "csextends": { 219 | "version": "1.2.0", 220 | "resolved": "https://registry.npmjs.org/csextends/-/csextends-1.2.0.tgz", 221 | "integrity": "sha512-S/8k1bDTJIwuGgQYmsRoE+8P+ohV32WhQ0l4zqrc0XDdxOhjQQD7/wTZwCzoZX53jSX3V/qwjT+OkPTxWQcmjg==" 222 | }, 223 | "dashdash": { 224 | "version": "1.14.1", 225 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 226 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 227 | "dev": true, 228 | "requires": { 229 | "assert-plus": "^1.0.0" 230 | } 231 | }, 232 | "debug": { 233 | "version": "2.6.9", 234 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 235 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 236 | "requires": { 237 | "ms": "2.0.0" 238 | } 239 | }, 240 | "deepmerge": { 241 | "version": "4.0.0", 242 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.0.0.tgz", 243 | "integrity": "sha512-YZ1rOP5+kHor4hMAH+HRQnBQHg+wvS1un1hAOuIcxcBy0hzcUf6Jg2a1w65kpoOUnurOfZbERwjI1TfZxNjcww==" 244 | }, 245 | "delayed-stream": { 246 | "version": "1.0.0", 247 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 248 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 249 | "dev": true 250 | }, 251 | "depd": { 252 | "version": "1.1.2", 253 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 254 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 255 | }, 256 | "destroy": { 257 | "version": "1.0.4", 258 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 259 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 260 | }, 261 | "diff": { 262 | "version": "1.0.8", 263 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz", 264 | "integrity": "sha1-NDJ2MI7Jkbe8giZ+1VvBQR+XFmY=", 265 | "dev": true 266 | }, 267 | "eachr": { 268 | "version": "3.2.0", 269 | "resolved": "https://registry.npmjs.org/eachr/-/eachr-3.2.0.tgz", 270 | "integrity": "sha1-LDXkPqCGUW95l8+At6pk1VpKRIQ=", 271 | "requires": { 272 | "editions": "^1.1.1", 273 | "typechecker": "^4.3.0" 274 | } 275 | }, 276 | "ecc-jsbn": { 277 | "version": "0.1.2", 278 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 279 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 280 | "dev": true, 281 | "requires": { 282 | "jsbn": "~0.1.0", 283 | "safer-buffer": "^2.1.0" 284 | } 285 | }, 286 | "editions": { 287 | "version": "1.3.4", 288 | "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", 289 | "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==" 290 | }, 291 | "ee-first": { 292 | "version": "1.1.1", 293 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 294 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 295 | }, 296 | "encodeurl": { 297 | "version": "1.0.2", 298 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 299 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 300 | }, 301 | "errlop": { 302 | "version": "1.1.1", 303 | "resolved": "https://registry.npmjs.org/errlop/-/errlop-1.1.1.tgz", 304 | "integrity": "sha512-WX7QjiPHhsny7/PQvrhS5VMizXXKoKCS3udaBp8gjlARdbn+XmK300eKBAAN0hGyRaTCtRpOaxK+xFVPUJ3zkw==", 305 | "requires": { 306 | "editions": "^2.1.2" 307 | }, 308 | "dependencies": { 309 | "editions": { 310 | "version": "2.1.3", 311 | "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", 312 | "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", 313 | "requires": { 314 | "errlop": "^1.1.1", 315 | "semver": "^5.6.0" 316 | } 317 | } 318 | } 319 | }, 320 | "escape-html": { 321 | "version": "1.0.3", 322 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 323 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 324 | }, 325 | "escape-string-regexp": { 326 | "version": "1.0.5", 327 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 328 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 329 | "dev": true 330 | }, 331 | "etag": { 332 | "version": "1.8.1", 333 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 334 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 335 | }, 336 | "express": { 337 | "version": "4.17.1", 338 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 339 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 340 | "requires": { 341 | "accepts": "~1.3.7", 342 | "array-flatten": "1.1.1", 343 | "body-parser": "1.19.0", 344 | "content-disposition": "0.5.3", 345 | "content-type": "~1.0.4", 346 | "cookie": "0.4.0", 347 | "cookie-signature": "1.0.6", 348 | "debug": "2.6.9", 349 | "depd": "~1.1.2", 350 | "encodeurl": "~1.0.2", 351 | "escape-html": "~1.0.3", 352 | "etag": "~1.8.1", 353 | "finalhandler": "~1.1.2", 354 | "fresh": "0.5.2", 355 | "merge-descriptors": "1.0.1", 356 | "methods": "~1.1.2", 357 | "on-finished": "~2.3.0", 358 | "parseurl": "~1.3.3", 359 | "path-to-regexp": "0.1.7", 360 | "proxy-addr": "~2.0.5", 361 | "qs": "6.7.0", 362 | "range-parser": "~1.2.1", 363 | "safe-buffer": "5.1.2", 364 | "send": "0.17.1", 365 | "serve-static": "1.14.1", 366 | "setprototypeof": "1.1.1", 367 | "statuses": "~1.5.0", 368 | "type-is": "~1.6.18", 369 | "utils-merge": "1.0.1", 370 | "vary": "~1.1.2" 371 | } 372 | }, 373 | "extend": { 374 | "version": "3.0.2", 375 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 376 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 377 | "dev": true 378 | }, 379 | "extendr": { 380 | "version": "3.4.1", 381 | "resolved": "https://registry.npmjs.org/extendr/-/extendr-3.4.1.tgz", 382 | "integrity": "sha512-xcdwhNFctt6U7XVBYgB35YG/CZFOZUZw+OCNiMPsgbENLlajgrZLVV6bgS/9zLHnY7s5nW6nJZcNrBvq033l1w==", 383 | "requires": { 384 | "editions": "^2.1.0", 385 | "typechecker": "^4.7.0" 386 | }, 387 | "dependencies": { 388 | "editions": { 389 | "version": "2.1.3", 390 | "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", 391 | "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", 392 | "requires": { 393 | "errlop": "^1.1.1", 394 | "semver": "^5.6.0" 395 | } 396 | } 397 | } 398 | }, 399 | "extract-opts": { 400 | "version": "3.3.1", 401 | "resolved": "https://registry.npmjs.org/extract-opts/-/extract-opts-3.3.1.tgz", 402 | "integrity": "sha1-WrvtyYwNUgLjJ4cn+Rktfghsa+E=", 403 | "requires": { 404 | "eachr": "^3.2.0", 405 | "editions": "^1.1.1", 406 | "typechecker": "^4.3.0" 407 | } 408 | }, 409 | "extsprintf": { 410 | "version": "1.3.0", 411 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 412 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 413 | "dev": true 414 | }, 415 | "eyes": { 416 | "version": "0.1.8", 417 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 418 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", 419 | "dev": true 420 | }, 421 | "fast-deep-equal": { 422 | "version": "2.0.1", 423 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 424 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 425 | "dev": true 426 | }, 427 | "fast-json-stable-stringify": { 428 | "version": "2.0.0", 429 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 430 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 431 | "dev": true 432 | }, 433 | "finalhandler": { 434 | "version": "1.1.2", 435 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 436 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 437 | "requires": { 438 | "debug": "2.6.9", 439 | "encodeurl": "~1.0.2", 440 | "escape-html": "~1.0.3", 441 | "on-finished": "~2.3.0", 442 | "parseurl": "~1.3.3", 443 | "statuses": "~1.5.0", 444 | "unpipe": "~1.0.0" 445 | } 446 | }, 447 | "forever-agent": { 448 | "version": "0.6.1", 449 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 450 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 451 | "dev": true 452 | }, 453 | "form-data": { 454 | "version": "2.3.3", 455 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 456 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 457 | "dev": true, 458 | "requires": { 459 | "asynckit": "^0.4.0", 460 | "combined-stream": "^1.0.6", 461 | "mime-types": "^2.1.12" 462 | } 463 | }, 464 | "forwarded": { 465 | "version": "0.1.2", 466 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 467 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 468 | }, 469 | "fresh": { 470 | "version": "0.5.2", 471 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 472 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 473 | }, 474 | "fs.realpath": { 475 | "version": "1.0.0", 476 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 477 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 478 | "dev": true 479 | }, 480 | "getpass": { 481 | "version": "0.1.7", 482 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 483 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 484 | "dev": true, 485 | "requires": { 486 | "assert-plus": "^1.0.0" 487 | } 488 | }, 489 | "glob": { 490 | "version": "7.1.4", 491 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", 492 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", 493 | "dev": true, 494 | "requires": { 495 | "fs.realpath": "^1.0.0", 496 | "inflight": "^1.0.4", 497 | "inherits": "2", 498 | "minimatch": "^3.0.4", 499 | "once": "^1.3.0", 500 | "path-is-absolute": "^1.0.0" 501 | } 502 | }, 503 | "graceful-fs": { 504 | "version": "4.2.0", 505 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", 506 | "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" 507 | }, 508 | "growl": { 509 | "version": "1.10.5", 510 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 511 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 512 | "dev": true 513 | }, 514 | "har-schema": { 515 | "version": "2.0.0", 516 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 517 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 518 | "dev": true 519 | }, 520 | "har-validator": { 521 | "version": "5.1.3", 522 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 523 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 524 | "dev": true, 525 | "requires": { 526 | "ajv": "^6.5.5", 527 | "har-schema": "^2.0.0" 528 | } 529 | }, 530 | "has-flag": { 531 | "version": "3.0.0", 532 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 533 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 534 | "dev": true 535 | }, 536 | "he": { 537 | "version": "1.1.1", 538 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 539 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 540 | "dev": true 541 | }, 542 | "http-errors": { 543 | "version": "1.7.2", 544 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 545 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 546 | "requires": { 547 | "depd": "~1.1.2", 548 | "inherits": "2.0.3", 549 | "setprototypeof": "1.1.1", 550 | "statuses": ">= 1.5.0 < 2", 551 | "toidentifier": "1.0.0" 552 | } 553 | }, 554 | "http-signature": { 555 | "version": "1.2.0", 556 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 557 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 558 | "dev": true, 559 | "requires": { 560 | "assert-plus": "^1.0.0", 561 | "jsprim": "^1.2.2", 562 | "sshpk": "^1.7.0" 563 | } 564 | }, 565 | "iconv-lite": { 566 | "version": "0.4.24", 567 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 568 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 569 | "requires": { 570 | "safer-buffer": ">= 2.1.2 < 3" 571 | } 572 | }, 573 | "ignorefs": { 574 | "version": "1.2.0", 575 | "resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.2.0.tgz", 576 | "integrity": "sha1-2ln7hYl25KXkNwLM0fKC/byeV1Y=", 577 | "requires": { 578 | "editions": "^1.3.3", 579 | "ignorepatterns": "^1.1.0" 580 | } 581 | }, 582 | "ignorepatterns": { 583 | "version": "1.1.0", 584 | "resolved": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.1.0.tgz", 585 | "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=" 586 | }, 587 | "inflight": { 588 | "version": "1.0.6", 589 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 590 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 591 | "dev": true, 592 | "requires": { 593 | "once": "^1.3.0", 594 | "wrappy": "1" 595 | } 596 | }, 597 | "inherits": { 598 | "version": "2.0.3", 599 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 600 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 601 | }, 602 | "ipaddr.js": { 603 | "version": "1.9.0", 604 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 605 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 606 | }, 607 | "is-typedarray": { 608 | "version": "1.0.0", 609 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 610 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 611 | "dev": true 612 | }, 613 | "isstream": { 614 | "version": "0.1.2", 615 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 616 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 617 | "dev": true 618 | }, 619 | "jsbn": { 620 | "version": "0.1.1", 621 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 622 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 623 | "dev": true 624 | }, 625 | "json-schema": { 626 | "version": "0.2.3", 627 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 628 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 629 | "dev": true 630 | }, 631 | "json-schema-traverse": { 632 | "version": "0.4.1", 633 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 634 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 635 | "dev": true 636 | }, 637 | "json-stringify-safe": { 638 | "version": "5.0.1", 639 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 640 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 641 | "dev": true 642 | }, 643 | "jsprim": { 644 | "version": "1.4.1", 645 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 646 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 647 | "dev": true, 648 | "requires": { 649 | "assert-plus": "1.0.0", 650 | "extsprintf": "1.3.0", 651 | "json-schema": "0.2.3", 652 | "verror": "1.10.0" 653 | } 654 | }, 655 | "lodash": { 656 | "version": "4.17.15", 657 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 658 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 659 | }, 660 | "media-typer": { 661 | "version": "0.3.0", 662 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 663 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 664 | }, 665 | "merge": { 666 | "version": "1.2.1", 667 | "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", 668 | "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" 669 | }, 670 | "merge-descriptors": { 671 | "version": "1.0.1", 672 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 673 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 674 | }, 675 | "methods": { 676 | "version": "1.1.2", 677 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 678 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 679 | }, 680 | "mime": { 681 | "version": "1.6.0", 682 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 683 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 684 | }, 685 | "mime-db": { 686 | "version": "1.40.0", 687 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 688 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 689 | }, 690 | "mime-types": { 691 | "version": "2.1.24", 692 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 693 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 694 | "requires": { 695 | "mime-db": "1.40.0" 696 | } 697 | }, 698 | "minimatch": { 699 | "version": "3.0.4", 700 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 701 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 702 | "dev": true, 703 | "requires": { 704 | "brace-expansion": "^1.1.7" 705 | } 706 | }, 707 | "minimist": { 708 | "version": "0.0.8", 709 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 710 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 711 | "dev": true 712 | }, 713 | "mkdirp": { 714 | "version": "0.5.1", 715 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 716 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 717 | "dev": true, 718 | "requires": { 719 | "minimist": "0.0.8" 720 | } 721 | }, 722 | "mocha": { 723 | "version": "5.2.0", 724 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 725 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 726 | "dev": true, 727 | "requires": { 728 | "browser-stdout": "1.3.1", 729 | "commander": "2.15.1", 730 | "debug": "3.1.0", 731 | "diff": "3.5.0", 732 | "escape-string-regexp": "1.0.5", 733 | "glob": "7.1.2", 734 | "growl": "1.10.5", 735 | "he": "1.1.1", 736 | "minimatch": "3.0.4", 737 | "mkdirp": "0.5.1", 738 | "supports-color": "5.4.0" 739 | }, 740 | "dependencies": { 741 | "commander": { 742 | "version": "2.15.1", 743 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 744 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 745 | "dev": true 746 | }, 747 | "debug": { 748 | "version": "3.1.0", 749 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 750 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 751 | "dev": true, 752 | "requires": { 753 | "ms": "2.0.0" 754 | } 755 | }, 756 | "diff": { 757 | "version": "3.5.0", 758 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 759 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 760 | "dev": true 761 | }, 762 | "glob": { 763 | "version": "7.1.2", 764 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 765 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 766 | "dev": true, 767 | "requires": { 768 | "fs.realpath": "^1.0.0", 769 | "inflight": "^1.0.4", 770 | "inherits": "2", 771 | "minimatch": "^3.0.4", 772 | "once": "^1.3.0", 773 | "path-is-absolute": "^1.0.0" 774 | } 775 | } 776 | } 777 | }, 778 | "ms": { 779 | "version": "2.0.0", 780 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 781 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 782 | }, 783 | "negotiator": { 784 | "version": "0.6.2", 785 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 786 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 787 | }, 788 | "oauth-sign": { 789 | "version": "0.9.0", 790 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 791 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 792 | "dev": true 793 | }, 794 | "on-finished": { 795 | "version": "2.3.0", 796 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 797 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 798 | "requires": { 799 | "ee-first": "1.1.1" 800 | } 801 | }, 802 | "once": { 803 | "version": "1.4.0", 804 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 805 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 806 | "dev": true, 807 | "requires": { 808 | "wrappy": "1" 809 | } 810 | }, 811 | "parseurl": { 812 | "version": "1.3.3", 813 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 814 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 815 | }, 816 | "path-is-absolute": { 817 | "version": "1.0.1", 818 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 819 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 820 | "dev": true 821 | }, 822 | "path-to-regexp": { 823 | "version": "0.1.7", 824 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 825 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 826 | }, 827 | "performance-now": { 828 | "version": "2.1.0", 829 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 830 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 831 | "dev": true 832 | }, 833 | "proxy-addr": { 834 | "version": "2.0.5", 835 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 836 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 837 | "requires": { 838 | "forwarded": "~0.1.2", 839 | "ipaddr.js": "1.9.0" 840 | } 841 | }, 842 | "psl": { 843 | "version": "1.2.0", 844 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", 845 | "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", 846 | "dev": true 847 | }, 848 | "punycode": { 849 | "version": "2.1.1", 850 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 851 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 852 | "dev": true 853 | }, 854 | "q": { 855 | "version": "1.0.1", 856 | "resolved": "https://registry.npmjs.org/q/-/q-1.0.1.tgz", 857 | "integrity": "sha1-EYcq7t7okmgRCxCnGESP+xARKhQ=", 858 | "dev": true 859 | }, 860 | "qs": { 861 | "version": "6.7.0", 862 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 863 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 864 | }, 865 | "range-parser": { 866 | "version": "1.2.1", 867 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 868 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 869 | }, 870 | "raw-body": { 871 | "version": "2.4.0", 872 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 873 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 874 | "requires": { 875 | "bytes": "3.1.0", 876 | "http-errors": "1.7.2", 877 | "iconv-lite": "0.4.24", 878 | "unpipe": "1.0.0" 879 | } 880 | }, 881 | "request": { 882 | "version": "2.88.0", 883 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 884 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 885 | "dev": true, 886 | "requires": { 887 | "aws-sign2": "~0.7.0", 888 | "aws4": "^1.8.0", 889 | "caseless": "~0.12.0", 890 | "combined-stream": "~1.0.6", 891 | "extend": "~3.0.2", 892 | "forever-agent": "~0.6.1", 893 | "form-data": "~2.3.2", 894 | "har-validator": "~5.1.0", 895 | "http-signature": "~1.2.0", 896 | "is-typedarray": "~1.0.0", 897 | "isstream": "~0.1.2", 898 | "json-stringify-safe": "~5.0.1", 899 | "mime-types": "~2.1.19", 900 | "oauth-sign": "~0.9.0", 901 | "performance-now": "^2.1.0", 902 | "qs": "~6.5.2", 903 | "safe-buffer": "^5.1.2", 904 | "tough-cookie": "~2.4.3", 905 | "tunnel-agent": "^0.6.0", 906 | "uuid": "^3.3.2" 907 | }, 908 | "dependencies": { 909 | "qs": { 910 | "version": "6.5.2", 911 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 912 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 913 | "dev": true 914 | } 915 | } 916 | }, 917 | "safe-buffer": { 918 | "version": "5.1.2", 919 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 920 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 921 | }, 922 | "safefs": { 923 | "version": "4.1.0", 924 | "resolved": "https://registry.npmjs.org/safefs/-/safefs-4.1.0.tgz", 925 | "integrity": "sha1-+CrrS9165R9lPrIPZyizBYyNZEU=", 926 | "requires": { 927 | "editions": "^1.1.1", 928 | "graceful-fs": "^4.1.4" 929 | } 930 | }, 931 | "safer-buffer": { 932 | "version": "2.1.2", 933 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 934 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 935 | }, 936 | "scandirectory": { 937 | "version": "2.5.0", 938 | "resolved": "https://registry.npmjs.org/scandirectory/-/scandirectory-2.5.0.tgz", 939 | "integrity": "sha1-bOA/VKCQtmjjy+2/IO354xBZPnI=", 940 | "requires": { 941 | "ignorefs": "^1.0.0", 942 | "safefs": "^3.1.2", 943 | "taskgroup": "^4.0.5" 944 | }, 945 | "dependencies": { 946 | "safefs": { 947 | "version": "3.2.2", 948 | "resolved": "https://registry.npmjs.org/safefs/-/safefs-3.2.2.tgz", 949 | "integrity": "sha1-gXDBRE1wOOCMrqBaN0+uL6NJ4Vw=", 950 | "requires": { 951 | "graceful-fs": "*" 952 | } 953 | }, 954 | "taskgroup": { 955 | "version": "4.3.1", 956 | "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz", 957 | "integrity": "sha1-feGT/r12gnPEV3MElwJNUSwnkVo=", 958 | "requires": { 959 | "ambi": "^2.2.0", 960 | "csextends": "^1.0.3" 961 | } 962 | } 963 | } 964 | }, 965 | "semver": { 966 | "version": "5.7.0", 967 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 968 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" 969 | }, 970 | "send": { 971 | "version": "0.17.1", 972 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 973 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 974 | "requires": { 975 | "debug": "2.6.9", 976 | "depd": "~1.1.2", 977 | "destroy": "~1.0.4", 978 | "encodeurl": "~1.0.2", 979 | "escape-html": "~1.0.3", 980 | "etag": "~1.8.1", 981 | "fresh": "0.5.2", 982 | "http-errors": "~1.7.2", 983 | "mime": "1.6.0", 984 | "ms": "2.1.1", 985 | "on-finished": "~2.3.0", 986 | "range-parser": "~1.2.1", 987 | "statuses": "~1.5.0" 988 | }, 989 | "dependencies": { 990 | "ms": { 991 | "version": "2.1.1", 992 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 993 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 994 | } 995 | } 996 | }, 997 | "serve-static": { 998 | "version": "1.14.1", 999 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 1000 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 1001 | "requires": { 1002 | "encodeurl": "~1.0.2", 1003 | "escape-html": "~1.0.3", 1004 | "parseurl": "~1.3.3", 1005 | "send": "0.17.1" 1006 | } 1007 | }, 1008 | "setprototypeof": { 1009 | "version": "1.1.1", 1010 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1011 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1012 | }, 1013 | "sshpk": { 1014 | "version": "1.16.1", 1015 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1016 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1017 | "dev": true, 1018 | "requires": { 1019 | "asn1": "~0.2.3", 1020 | "assert-plus": "^1.0.0", 1021 | "bcrypt-pbkdf": "^1.0.0", 1022 | "dashdash": "^1.12.0", 1023 | "ecc-jsbn": "~0.1.1", 1024 | "getpass": "^0.1.1", 1025 | "jsbn": "~0.1.0", 1026 | "safer-buffer": "^2.0.2", 1027 | "tweetnacl": "~0.14.0" 1028 | } 1029 | }, 1030 | "statuses": { 1031 | "version": "1.5.0", 1032 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1033 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1034 | }, 1035 | "supports-color": { 1036 | "version": "5.4.0", 1037 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 1038 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 1039 | "dev": true, 1040 | "requires": { 1041 | "has-flag": "^3.0.0" 1042 | } 1043 | }, 1044 | "taskgroup": { 1045 | "version": "5.3.0", 1046 | "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-5.3.0.tgz", 1047 | "integrity": "sha512-++j3Yi3XZGYgAvmGzRtNa+BnDvkPbdroyMffCY+Gj9A4iH2IJ1S7/g6LewGVXQkVw/KOzlfE1TimARYXvOEsgQ==", 1048 | "requires": { 1049 | "ambi": "^3.0.0", 1050 | "eachr": "^3.2.0", 1051 | "editions": "^1.3.4", 1052 | "extendr": "^3.2.2", 1053 | "unbounded": "^1.1.0" 1054 | }, 1055 | "dependencies": { 1056 | "ambi": { 1057 | "version": "3.2.0", 1058 | "resolved": "https://registry.npmjs.org/ambi/-/ambi-3.2.0.tgz", 1059 | "integrity": "sha512-nj5sHLPFd7u2OLmHdFs4DHt3gK6edpNw35hTRIKyI/Vd2Th5e4io50rw1lhmCdUNO2Mm4/4FkHmv6shEANAWcw==", 1060 | "requires": { 1061 | "editions": "^2.1.0", 1062 | "typechecker": "^4.3.0" 1063 | }, 1064 | "dependencies": { 1065 | "editions": { 1066 | "version": "2.1.3", 1067 | "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", 1068 | "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", 1069 | "requires": { 1070 | "errlop": "^1.1.1", 1071 | "semver": "^5.6.0" 1072 | } 1073 | } 1074 | } 1075 | } 1076 | } 1077 | }, 1078 | "toidentifier": { 1079 | "version": "1.0.0", 1080 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1081 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1082 | }, 1083 | "tough-cookie": { 1084 | "version": "2.4.3", 1085 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 1086 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 1087 | "dev": true, 1088 | "requires": { 1089 | "psl": "^1.1.24", 1090 | "punycode": "^1.4.1" 1091 | }, 1092 | "dependencies": { 1093 | "punycode": { 1094 | "version": "1.4.1", 1095 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 1096 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", 1097 | "dev": true 1098 | } 1099 | } 1100 | }, 1101 | "tunnel-agent": { 1102 | "version": "0.6.0", 1103 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1104 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1105 | "dev": true, 1106 | "requires": { 1107 | "safe-buffer": "^5.0.1" 1108 | } 1109 | }, 1110 | "tweetnacl": { 1111 | "version": "0.14.5", 1112 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1113 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1114 | "dev": true 1115 | }, 1116 | "type-is": { 1117 | "version": "1.6.18", 1118 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1119 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1120 | "requires": { 1121 | "media-typer": "0.3.0", 1122 | "mime-types": "~2.1.24" 1123 | } 1124 | }, 1125 | "typechecker": { 1126 | "version": "4.7.0", 1127 | "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-4.7.0.tgz", 1128 | "integrity": "sha512-4LHc1KMNJ6NDGO+dSM/yNfZQRtp8NN7psYrPHUblD62Dvkwsp3VShsbM78kOgpcmMkRTgvwdKOTjctS+uMllgQ==", 1129 | "requires": { 1130 | "editions": "^2.1.0" 1131 | }, 1132 | "dependencies": { 1133 | "editions": { 1134 | "version": "2.1.3", 1135 | "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", 1136 | "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", 1137 | "requires": { 1138 | "errlop": "^1.1.1", 1139 | "semver": "^5.6.0" 1140 | } 1141 | } 1142 | } 1143 | }, 1144 | "unbounded": { 1145 | "version": "1.2.0", 1146 | "resolved": "https://registry.npmjs.org/unbounded/-/unbounded-1.2.0.tgz", 1147 | "integrity": "sha512-D/ENuBtxxwhxEiSXHgHwM/UXsDRuXz7JwqBQ54zrbaqmbmSWPvsGMzslr5mEftsDo1KOfl7vEIXGCamWBlAT6Q==", 1148 | "requires": { 1149 | "editions": "^2.1.3" 1150 | }, 1151 | "dependencies": { 1152 | "editions": { 1153 | "version": "2.1.3", 1154 | "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", 1155 | "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", 1156 | "requires": { 1157 | "errlop": "^1.1.1", 1158 | "semver": "^5.6.0" 1159 | } 1160 | } 1161 | } 1162 | }, 1163 | "unpipe": { 1164 | "version": "1.0.0", 1165 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1166 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1167 | }, 1168 | "uri-js": { 1169 | "version": "4.2.2", 1170 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1171 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1172 | "dev": true, 1173 | "requires": { 1174 | "punycode": "^2.1.0" 1175 | } 1176 | }, 1177 | "utils-merge": { 1178 | "version": "1.0.1", 1179 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1180 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1181 | }, 1182 | "uuid": { 1183 | "version": "3.3.2", 1184 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1185 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", 1186 | "dev": true 1187 | }, 1188 | "vary": { 1189 | "version": "1.1.2", 1190 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1191 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1192 | }, 1193 | "verror": { 1194 | "version": "1.10.0", 1195 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1196 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1197 | "dev": true, 1198 | "requires": { 1199 | "assert-plus": "^1.0.0", 1200 | "core-util-is": "1.0.2", 1201 | "extsprintf": "^1.2.0" 1202 | } 1203 | }, 1204 | "vows": { 1205 | "version": "0.8.2", 1206 | "resolved": "https://registry.npmjs.org/vows/-/vows-0.8.2.tgz", 1207 | "integrity": "sha1-aR95qybM3oC6cm3en+yOc9a88us=", 1208 | "dev": true, 1209 | "requires": { 1210 | "diff": "~1.0.8", 1211 | "eyes": "~0.1.6", 1212 | "glob": "^7.1.2" 1213 | } 1214 | }, 1215 | "watchr": { 1216 | "version": "2.6.0", 1217 | "resolved": "https://registry.npmjs.org/watchr/-/watchr-2.6.0.tgz", 1218 | "integrity": "sha1-51xCOxC+eSZ6DD73bi6hBP4CZ6U=", 1219 | "requires": { 1220 | "eachr": "^3.2.0", 1221 | "extendr": "^3.2.2", 1222 | "extract-opts": "^3.3.1", 1223 | "ignorefs": "^1.1.1", 1224 | "safefs": "^4.1.0", 1225 | "scandirectory": "^2.5.0", 1226 | "taskgroup": "^5.0.1", 1227 | "typechecker": "^4.3.0" 1228 | } 1229 | }, 1230 | "wrappy": { 1231 | "version": "1.0.2", 1232 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1233 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1234 | "dev": true 1235 | } 1236 | } 1237 | } 1238 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interfake", 3 | "preferGlobal": true, 4 | "version": "1.20.0", 5 | "author": "Daniel Hough ", 6 | "description": "A simple way to create dummy APIs", 7 | "contributors": [ 8 | { 9 | "name": "Daniel Hough", 10 | "email": "daniel.hough@gmail.com", 11 | "url": "https://danhough.com" 12 | }, 13 | { 14 | "name": "rajit", 15 | "url": "https://github.com/rajit" 16 | }, 17 | { 18 | "name": "bruce-one", 19 | "url": "https://github.com/bruce-one" 20 | }, 21 | { 22 | "name": "roychoo", 23 | "url": "https://github.com/roychoo" 24 | } 25 | ], 26 | "scripts": { 27 | "test": "mocha tests/*.test.js --reporter spec" 28 | }, 29 | "files": [ 30 | "example-apis", 31 | "index.js", 32 | "lib" 33 | ], 34 | "keywords": [ 35 | "cli", 36 | "http", 37 | "server", 38 | "mocking", 39 | "mocks", 40 | "testing", 41 | "api", 42 | "rest" 43 | ], 44 | "repository": { 45 | "type": "git", 46 | "url": "git@github.com:basicallydan/interfake.git" 47 | }, 48 | "dependencies": { 49 | "body-parser": "^1.6.7", 50 | "commander": "2.3.0", 51 | "connect-json": "0.0.0", 52 | "core-util-is": "^1.0.1", 53 | "deepmerge": "^4.0.0", 54 | "express": "^4.16.4", 55 | "lodash": "^4.17.15", 56 | "merge": "^1.2.1", 57 | "serve-static": "^1.13.2", 58 | "watchr": "^2.4.13" 59 | }, 60 | "license": "MIT", 61 | "engines": { 62 | "node": ">=4" 63 | }, 64 | "main": "./lib/server.js", 65 | "bin": "./index.js", 66 | "devDependencies": { 67 | "api-easy": "^0.4.0", 68 | "connect": "^3.6.6", 69 | "mocha": "^5.2.0", 70 | "q": "~1.0.1", 71 | "request": "^2.88.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Interfake: Quick JSON APIs 2 | 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/basicallydan/interfake?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Interfake is a tool which allows developers of client-side applications of *any* platform to easily create dummy HTTP APIs to develop against. Let's get started with a simple example. 6 | 7 | ## Get started 8 | 9 | If you don't want to use the JavaScript method to create your Interfake API, go read up on [all the methods](https://github.com/basicallydan/interfake/wiki). Otherwise, read on. 10 | 11 | Install Interfake in your project. 12 | 13 | ``` 14 | npm install interfake --save 15 | ``` 16 | 17 | Let's write a simple fake API: 18 | 19 | ```js 20 | var Interfake = require('interfake'); 21 | var interfake = new Interfake(); 22 | interfake.get('/whats-next').body({ next : 'more stuff '}); 23 | interfake.listen(3000); // The server will listen on port 3000 24 | ``` 25 | 26 | Now go to http://localhost:3000/whats-next in your browser (or [`curl`](http://curl.haxx.se)), and you will see the following: 27 | 28 | ```json 29 | { 30 | "next":"more stuff" 31 | } 32 | ``` 33 | 34 | You can also chain response properties: 35 | 36 | ```js 37 | var Interfake = require('interfake'); 38 | var interfake = new Interfake(); 39 | interfake.get('/whats-next').status(400).body({ error : 'such a bad request'}); 40 | interfake.listen(3000); 41 | 42 | /* 43 | # Request: 44 | $ curl http://localhost:3000/whats-next -X GET 45 | # Response: 46 | 400 47 | { 48 | "error":"such a bad request" 49 | } 50 | */ 51 | ``` 52 | 53 | You can use different HTTP methods: 54 | 55 | ```js 56 | var Interfake = require('interfake'); 57 | var interfake = new Interfake(); 58 | interfake.post('/next-items').status(201).body({ created : true }); 59 | interfake.listen(3000); 60 | 61 | /* 62 | # Request: 63 | $ curl http://localhost:3000/next-items -X POST 64 | # Response: 65 | 201 66 | { 67 | "created":true 68 | } 69 | */ 70 | ``` 71 | 72 | You can specify endpoints which should only be **created** once other ones have been hit. 73 | 74 | ```js 75 | var Interfake = require('interfake'); 76 | var interfake = new Interfake(); 77 | var postResponse = interfake.post('/next-items').status(201).body({ created : true }); 78 | postResponse.creates.get('/items/1').status(200).body({ id: 1, name: 'Item 1' }); 79 | postResponse.creates.get('/next-items').status(200).body({ items: [ { id: 1, name: 'Item 1' } ] }); 80 | interfake.listen(3000); 81 | 82 | /* 83 | # Request: 84 | $ curl http://localhost:3000/next-items -X POST 85 | # Response: 86 | 201 87 | { 88 | "created":true 89 | } 90 | 91 | 92 | # Request: 93 | $ curl http://localhost:3000/items/1 -X GET 94 | # Response: 95 | 200 96 | { 97 | "id":1 98 | "name":"Item 1" 99 | } 100 | */ 101 | ``` 102 | 103 | You can even specify how endpoints should be **extended** once others have been hit. 104 | 105 | ```js 106 | var Interfake = require('interfake'); 107 | var interfake = new Interfake(); 108 | interfake.get('/items').status(200).body({ items: [ { id: 1, name: 'Item 1' } ] }); 109 | interfake.get('/items/1').status(200).body({ id: 1, name: 'Item 1' }); 110 | var postResponse = interfake.post('/items').status(201).body({ created : true }); 111 | postResponse.creates.get('/items/2').status(200).body({ id: 2, name: 'Item 2' }); 112 | postResponse.extends.get('/items').status(200).body({ items: [ { id: 2, name: 'Item 2' } ] }); 113 | interfake.listen(3000); 114 | 115 | /* 116 | # Request: 117 | $ curl http://localhost:3000/items -X GET 118 | # Response: 119 | 200 120 | { 121 | "items" : [ 122 | { 123 | "id":1 124 | "name":"Item 1" 125 | } 126 | ] 127 | } 128 | 129 | # Request: 130 | $ curl http://localhost:3000/items -X POST 131 | # Response: 132 | 201 133 | { 134 | "created":true 135 | } 136 | 137 | 138 | # Request: 139 | $ curl http://localhost:3000/items -X GET 140 | # Response: 141 | 200 142 | { 143 | "items" : [ 144 | { 145 | "id":1 146 | "name":"Item 1" 147 | }, 148 | { 149 | "id":2 150 | "name":"Item 2" 151 | } 152 | ] 153 | } 154 | */ 155 | ``` 156 | 157 | There's more options, though, including delays, custom response headers, and handling query string parameters. 158 | 159 | --- 160 | 161 | ## API 162 | 163 | The majority of Interfake users will probably be interested in the JavaScript API, which is covered below. However, there are in fact [three ways to use Interfake: JavaScript, on the Command Line (using static JSON files), or using an HTTP meta-API](https://github.com/basicallydan/interfake/wiki). These are covered in detail in [the Wiki](https://github.com/basicallydan/interfake/wiki). 164 | 165 | ### JavaScript 166 | 167 | * `new Interfake(options)`: creates an Interfake object. Options are: 168 | * `debug`: If `true`, outputs lots of annoying but helpful log messages. Default is `false`. 169 | * `path`: Sets the API root path. E.g. if `api` is used then the route at `/users` will be accessible at `/api/path` 170 | * `#createRoute(route)`: Takes a JSON object with the following: 171 | * `request` 172 | * `response` 173 | * `afterResponse` (optional) 174 | * `#listen(port, callback)`: Takes a port and starts the server, and a callback which executes when the server is running 175 | * `#stop()`: Stops the server if it's been started 176 | * `#serveStatic(path, directory)`: Serve static (usually a website) files from a certain path. This is useful for testing [SPAs](http://en.wikipedia.org/wiki/Single-page_application). ([Example use.](/examples-javascript/fluent-web-page-test.js)) 177 | * `#loadFile(path, options)`: Load a JSON file containing an Interfake-shaped API configuration. Options includes `watch`, which, if true, means that the file loaded there will be reloaded when it changes. 178 | 179 | #### Fluent Interface 180 | 181 | * `#get|post|put|patch|delete(url)`: Create an endpoint at the specified URL. Can then be followed by each of the following, which can follow each other too e.g. `get().query().body().status().body().creates.get()` etc. 182 | * `#query(queryParameters)`: An object containing query parameters to accept. Overwrites matching URL params. E.g. `get('/a?b=1').query({b:2})` means `/a?b=2` will work but `/a?b=1` will not. You can also use arrays as the value, e.g. `.query({b:[1,2]})` or even a valid ```RegExp```. All values which will be matched regardless of order. 183 | * `#status(statusCode)`: Set the response status code for the endpoint 184 | * `#body(body)`: Set the JSON response body of the end point 185 | * `#echo(true|false)`: The response body should be the same as the request body. Can be used after `extends` too. ([Example use](/examples-javascript/fluent-echo.js)) 186 | * `#proxy(url|options)`: The response should be a proxy of another URL. Currently, options accepts both `url` and `headers` properties. The `headers` property specifies the headers which should be sent in the request to the proxy URL 187 | * `#delay(milliseconds)`: Set the number of milliseconds to delay the response by to mimic network of processing lag 188 | * Also accepts a delay range in the format 'ms..ms' e.g. '50..100' 189 | * `#responseHeaders(headers)`: An object containing response headers. The keys are header names. 190 | * `#creates#get|post|put|patch|delete(url)`: Specify an endpoint to create *after* the first execution of this one. API is the same as above. 191 | * `#extends#get|post|put|patch|delete(url)`: Specify an endpoint to modify *after* the first execution of this one. API is the same as above. The endpoints you extend are matched based on `url` and `query`. The `status`, `body`, `delay` and `responseHeaders` are the extendable bits. Keep in mind that keys will be replaced, and arrays will be added to. 192 | 193 | ## JSONP 194 | 195 | Interfake supports [JSONP](http://en.wikipedia.org/wiki/JSONP). Just put `?callback` on the end of the URLs being called. 196 | 197 | ``` 198 | $ curl http://localhost:3000/whattimeisit?callback=handleSomeJson 199 | ``` 200 | 201 | ## Use Cases 202 | 203 | ### Backend/API Prototype for a Single-Page Application (SPA) 204 | 205 | By using Interfake's `.serveStatic()` method, you can serve some front-end HTML, JavaScript and CSS which uses the API you've created as the backend. Not only does this massively speed up development time by not having to have a real API, it serves as a great prototype for the real API, and avoids having to mock requests. This is my most common use for Interfake. 206 | 207 | ### Backend for a Mobile Application 208 | 209 | If you'd like to develop an API-driven mobile application you might not yet have a finished API available. This is a perfect example of where Interfake is useful. You can quickly mock up some dummy APIs and work on the mobile application. In parallel, perhaps another developer will be creating the real API, or you could create it later. 210 | 211 | ### Automated Testing 212 | 213 | You can use Interfake to create dummy APIs which use data from your test setup with the HTTP method above, or by using a static set of test data. If you're writing your test suite using a NodeJS library, you can use the JavaScript API. 214 | 215 | The HTTP API is particularly useful for developing iOS Applications which uses Automated tests written in JavaScript, or developing Node.js applications which rely on external APIs. 216 | 217 | For an example of how to do this, please see the [web page test example](/examples-javascript/fluent-web-page-test.js). 218 | 219 | ### Regular Expressions for URLs 220 | 221 | Regular expressions can be used to specify endpoint URLs in two different ways depending on which interface you use. For the fluent API, you simply put a JavaScript regular expression as the URL, e.g. 222 | 223 | ```javascript 224 | interfake.get(/\/regular\/expression/).status(200); 225 | ``` 226 | 227 | This is also supported when using `createRoute`, but since JSON does not support regular expressions, a different method must be used here: 228 | 229 | ```json 230 | [ 231 | { 232 | "request": { 233 | "url": { 234 | "pattern" : "/what/the/.*", 235 | "regexp" : true 236 | }, 237 | "method": "get" 238 | }, 239 | "response": { 240 | "code": 200 241 | } 242 | } 243 | ] 244 | ``` 245 | 246 | The pattern specified in the `request.url.pattern` string will be parsed and treated as a regular expression. 247 | 248 | ### Proxying another API 249 | 250 | There are a number of reasons you might want to proxy another API. Three of the more common ones are: 251 | 252 | * It requires some authorization options which you want to hide from a client-side script but nonetheless want to use every time you make a request 253 | * There is a [cross-origin request sharing (CORS)](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) issue which prevents your client-side code from using the API 254 | * It requires some tedious header setup 255 | 256 | Interfake allows you to proxy another URL quite easily and also specify any headers you like while doing so, using the `proxy` option. 257 | 258 | ```js 259 | interfake.get('/github-issues').proxy('https://api.github.com/repos/basicallydan/interfake/tags'); 260 | ``` 261 | 262 | The example above creates a simple proxy against the URL `https://api.github.com/repos/basicallydan/interfake/tags` and will return whatever a public, non-authorized user will see. However, consider an endpoint which requires authorization. 263 | 264 | ```js 265 | interfake.get('/github-issues').proxy({ 266 | url: 'https://api.github.com/repos/basicallydan/interfake/tags', 267 | headers: { 268 | 'Authorization': 'Token qoinfiu13jfcikwkhf1od091dj0' 269 | } 270 | }); 271 | ``` 272 | 273 | This example uses an authorization token to authorize the request. This is one of the common use-cases. However, the first one will easily solve CORS issues, and any other headers apart from `Authorization` can be specified instead. 274 | 275 | ### Echoing the request body 276 | 277 | This can be easily achieved using the `.echo()` method in the fluent interface, or by specifying the following for route options: 278 | 279 | ```json 280 | { 281 | request : { 282 | url : '/echo', 283 | method: 'post' 284 | }, 285 | response : { 286 | echo : true 287 | } 288 | } 289 | ``` 290 | 291 | A request to the `/echo` endpint will return whatever body it is sent. You can see more examples of this in the [examples folder](/examples-javascript/). 292 | 293 | ### Creating a static API 294 | 295 | If you have a website or mobile application which only needs static data, deploy Interfake to a server somewhere with a JSON file serving up the data, and point your application at it. 296 | 297 | ## Compatibility 298 | 299 | I tested this on my Mac. If you have trouble on Windows or any other platform, [raise an issue](https://github.com/basicallydan/interfake/issues). 300 | 301 | ## Version History 302 | 303 | * 1.19.0: Using the npm version of `deepmerge`, not my fork on gh. Exposing underlying express app as instance variable `expressApp` 304 | * 1.18.0: Array support in query string params added thanks to [@roychoo](https://github.com/roychoo). Also, fixed a couple of tests which broke in Node 5.0. 305 | * 1.17.0: Regular expressions can now be specified in JSON route files and in the normal JavaScript API (`.createRoute()`) using `{ url : { pattern : '', regexp : true } }` 306 | * 1.16.0: Added automatic `OPTIONS` support for any routes specified (e.g. if `GET` has been defined then `OPTIONS` will say so. Also includes `access-control-allow-origin`) 307 | * 1.15.0: Added `.echo` or `{ echo : true }` support for response. Now, the response body can an echo of the request body. 308 | * 1.14.0: Fixed `serveStatic` but also accidental new feature. Now, 404 responses include some text: the default express text too. 309 | * 1.13.0: Regex URL support 310 | * 1.12.1: Bug fix from [Alexander Pope](https://github.com/popeindustries), proxy and query params not playing well together 311 | * 1.12.0: Proxy support 312 | * 1.11.0: Config reload 313 | * 1.10.0: Support for PATCH 314 | * 1.9.2: Updated deepmerge dependency, since it included a bug 315 | * 1.9.1: Updated dependencies, and fixed a bug where `.serveStatic` was not working on Windows because of the directory being wrong. 316 | * 1.9.0: Created the `.extends` methods to extend existing endpoints 317 | * 1.8.2: Bug fix for Windows - paths were screwed up 318 | * 1.8.1: Bug fix for responseheaders 319 | * 1.8.0: Querystring parameter values can now be regular expressions 320 | * 1.7.2: Fixed a bug where `.delay()` was not allowing chaining 321 | * 1.7.1: Added ability to set a root path for the API only (skipped 1.7.0 which was a bit broken) 322 | * 1.6.2: Can add a callback to `listen` so that you know when the server has started (by [bruce-one](https://github.com/bruce-one)) 323 | * 1.6.1: Upgraded to Express 4.0.0 (thanks to [Sebastian Schürmann](https://github.com/sebs)). 324 | * 1.6.0: Custom response headers (thanks to [Sebastian Schürmann](https://github.com/sebs)). 325 | * 1.5.0: Can now use querystring params (thanks to [rajit](https://github.com/rajit)). Massive. 326 | * 1.4.0: Can specify delay range using `delay(10..50)` (by [bruce-one](https://github.com/bruce-one)) 327 | * 1.3.0: Can mimic slow responses using `delay()` (by [bruce-one](https://github.com/bruce-one)) 328 | * 1.2.0: Added ability to do static files 329 | * 1.1.1: Fixed the response to `POST /_request` to be a 201, and `POST /_requests` is now the path used 330 | * 1.1.0: Added the fluent interface for easier creation of endpoints 331 | * 1.0.0: Backwards-incompatible changes for JavaScript API, now creating an `Interfake` instance 332 | * 0.2.0: Added JSONP support 333 | * 0.1.0: Support for creating mocked JSON APIs using HTTP, JavaScript or command line 334 | 335 | ## Contribute 336 | 337 | Interfake is a labour of love, created for front-end and mobile developers to increase their prototyping and development speeds. If you can contribute by getting through some issues, I would be very grateful. Please read more about how to contribute in the [CONTRIBUTING.md](https://github.com/basicallydan/interfake/blob/master/CONTRIBUTING.md) document. 338 | 339 | It's important that the tests all pass so we can keep this little badge green: 340 | 341 | [![Travis](http://img.shields.io/travis/basicallydan/interfake.svg)](https://travis-ci.org/basicallydan/interfake) 342 | 343 | <3 Open Source! 344 | 345 | [![I Love Open Source](http://www.iloveopensource.io/images/logo-lightbg.png)](http://www.iloveopensource.io/projects/5319884587659fce66000943) 346 | 347 | ## Dependencies 348 | 349 | * [express](https://github.com/visionmedia/express) 350 | * [express body-parser](https://github.com/expressjs/body-parser) 351 | * [connect-json](https://github.com/dtinth/connect-json) 352 | * [commander](https://github.com/visionmedia/commander.js/) 353 | * [core-util-is](https://github.com/isaacs/core-util-is) 354 | * [merge](https://github.com/yeikos/js.merge) 355 | * [basicallydan/deepmerge](https://github.com/basicallydan/deepmerge) forked from [nrf110/deepmerge](https://github.com/nrf110/deepmerge) 356 | 357 | ## Works well with 358 | 359 | * [Mocha](http://mochajs.org/) - the test framework 360 | * [Zombie.js](http://zombie.labnotes.org/) - the Node.js-powered headless web browser 361 | 362 | ## Thank yous 363 | 364 | [Alun](https://github.com/4lun) for reading this readme. 365 | 366 | ## Author & Contributors 367 | 368 | * [Dan Hough](https://github.com/basicallydan) ([Twitter](https://twitter.com/basicallydan) | [Website](http://danielhough.co.uk)) 369 | * [bruce-one](https://github.com/bruce-one) 370 | * [rajit](https://github.com/rajit) 371 | * [Sebastian Schürmann](https://github.com/sebs) 372 | * [roychoo](https://github.com/roychoo) 373 | 374 | ## Future work 375 | 376 | * Create a guide/some examples for how to integrate this with existing test frameworks, whether written in JavaScript or not 377 | * Improve the templating, so that a response might include a repeated structure with an incrementing counter or randomized data 378 | -------------------------------------------------------------------------------- /tests/fluent.test.js: -------------------------------------------------------------------------------- 1 | /* globals describe, beforeEach, afterEach, it */ 2 | var assert = require('assert'); 3 | var request = require('request'); 4 | var Q = require('q'); 5 | 6 | request = request.defaults({ 7 | json: true 8 | }); 9 | 10 | var get = Q.denodeify(request.get); 11 | var post = Q.denodeify(request.post); 12 | var put = Q.denodeify(request.put); 13 | var del = Q.denodeify(request.del); 14 | var patch = Q.denodeify(request.patch); 15 | 16 | // The thing we're testing 17 | var Interfake = require('..'); 18 | var interfake; 19 | 20 | describe('Interfake Fluent JavaScript API', function () { 21 | beforeEach(function () { 22 | interfake = new Interfake(); 23 | }); 24 | afterEach(function () { 25 | if (interfake) { 26 | interfake.stop(); 27 | } 28 | }); 29 | 30 | // Testing the fluent interface 31 | describe('#get()', function () { 32 | it('should create one GET endpoint', function (done) { 33 | interfake.get('/fluent'); 34 | interfake.listen(3000); 35 | 36 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 37 | assert.equal(response.statusCode, 200); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('should create one GET endpoint with a querystring', function (done) { 43 | interfake.get('/fluent?query=1'); 44 | interfake.listen(3000); 45 | 46 | request({ url : 'http://localhost:3000/fluent?query=1', json : true }, function (error, response, body) { 47 | assert.equal(response.statusCode, 200); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should create one GET endpoint with a RegExp path', function (done) { 53 | interfake = new Interfake(); 54 | interfake.get(/\/fluent\/.*/); 55 | interfake.listen(3000); 56 | 57 | request({ url : 'http://localhost:3000/fluent/whatever', json : true }, function (error, response, body) { 58 | assert.equal(response.statusCode, 200); 59 | done(); 60 | }); 61 | }); 62 | 63 | describe('#responseHeaders()', function () { 64 | it('should create one GET endpoint which returns custom headers', function (done) { 65 | interfake.get('/fluent').responseHeaders({ 'X-Request-Type': 'test', 'X-lol-TEST': 'bleep' }); 66 | interfake.listen(3000); 67 | 68 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response) { 69 | assert.equal(response.statusCode, 200); 70 | assert.equal(response.headers['x-request-type'], 'test'); 71 | assert.equal(response.headers['x-lol-test'], 'bleep'); 72 | assert.equal(response.headers['x-undef'], undefined); 73 | done(); 74 | }); 75 | }); 76 | 77 | describe('#status()', function () { 78 | it('should return a 300 status', function (done) { 79 | interfake.get('/fluent').responseHeaders({ 'X-Request-Type': 'test', 'X-lol-TEST': 'bleep' }).status(300); 80 | interfake.listen(3000); 81 | 82 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response) { 83 | assert.equal(response.statusCode, 300); 84 | assert.equal(response.headers['x-request-type'], 'test'); 85 | assert.equal(response.headers['x-lol-test'], 'bleep'); 86 | assert.equal(response.headers['x-undef'], undefined); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('#proxy()', function () { 94 | it('should create one GET endpoint which acts as a proxy for another', function (done) { 95 | var proxiedInterfake = new Interfake(); 96 | proxiedInterfake.get('/whatever').status(404).body({ 97 | message: 'This is something you proxied!' 98 | }); 99 | proxiedInterfake.listen(3051); 100 | interfake.get('/proxy').proxy('http://localhost:3051/whatever'); 101 | interfake.listen(3000); 102 | 103 | request('http://localhost:3000/proxy', function (error, response, body) { 104 | assert.equal(response.statusCode, 404); 105 | assert.equal(body.message, 'This is something you proxied!'); 106 | proxiedInterfake.stop(); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('should create one GET endpoint which acts as a proxy for another and sends the specified header', function (done) { 112 | var proxiedInterfake = new Interfake({ 113 | onRequest: function (req) { 114 | assert.equal(req.get('Authorization'), 'Basic username:password'); 115 | proxiedInterfake.stop(); 116 | done(); 117 | } 118 | }); 119 | proxiedInterfake.get('/whatever').status(404).body({ 120 | message: 'This is something you proxied!' 121 | }); 122 | proxiedInterfake.listen(3051); 123 | interfake.get('/proxy').proxy({ 124 | url: 'http://localhost:3051/whatever', 125 | headers: { 126 | 'Authorization': 'Basic username:password' 127 | } 128 | }); 129 | interfake.listen(3000); 130 | 131 | request('http://localhost:3000/proxy', function (error, response, body) { 132 | assert.equal(response.statusCode, 404); 133 | assert.equal(body.message, 'This is something you proxied!'); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('#query()', function () { 139 | it('should use the query object as priority', function (done) { 140 | interfake.get('/fluent?query=1').query({ query: 2 }).status(200); 141 | interfake.listen(3000); 142 | 143 | Q.all([get({url:'http://localhost:3000/fluent?query=1',json:true}), get({url:'http://localhost:3000/fluent?query=2',json:true})]) 144 | .then(function (results) { 145 | assert.equal(results[0][0].statusCode, 404); 146 | assert.equal(results[1][0].statusCode, 200); 147 | done(); 148 | }); 149 | }); 150 | 151 | it('should use a RegExp to find a partially-matched query string param', function (done) { 152 | interfake.get('/fluent').query({ query: /[0-9]+/ }).status(200); 153 | interfake.listen(3000); 154 | 155 | Q.all([get({url:'http://localhost:3000/fluent?query=1',json:true}), get({url:'http://localhost:3000/fluent?query=2',json:true})]) 156 | .then(function (results) { 157 | assert.equal(results[0][0].statusCode, 200); 158 | assert.equal(results[1][0].statusCode, 200); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('should use a RegExp to find a partially-matched query string param and a fully-matched one', function (done) { 164 | interfake.get('/fluent').query({ query: /[0-9]+/, page: 2 }).status(200); 165 | interfake.listen(3000); 166 | 167 | Q.all([get({url:'http://localhost:3000/fluent?query=1&page=5',json:true}), get({url:'http://localhost:3000/fluent?query=2&page=2',json:true})]) 168 | .then(function (results) { 169 | assert.equal(results[0][0].statusCode, 404, 'The non-existent page should not be found'); 170 | assert.equal(results[1][0].statusCode, 200, 'The existing page should be found'); 171 | done(); 172 | }); 173 | }); 174 | 175 | it('should use a RegExp to find a partially-matched query string param and a fully-matched one, when there is also a query-free endpoint', function (done) { 176 | interfake.get('/fluent').query({ query: /[0-9]+/, page: 2 }).status(200); 177 | interfake.get('/fluent').status(300); 178 | interfake.get('/fluent?page=8').status(512); 179 | interfake.listen(3000); 180 | 181 | Q.all([get({url:'http://localhost:3000/fluent?query=1&page=5',json:true}), get({url:'http://localhost:3000/fluent?query=2&page=2',json:true}), get({url:'http://localhost:3000/fluent',json:true}), get({url:'http://localhost:3000/fluent?page=8',json:true})]) 182 | .then(function (results) { 183 | assert.equal(results[0][0].statusCode, 404, 'The non-existent page should not be found'); 184 | assert.equal(results[1][0].statusCode, 200, 'The existing page should be found'); 185 | assert.equal(results[2][0].statusCode, 300, 'The non-query-string page should be found'); 186 | assert.equal(results[3][0].statusCode, 512, 'The query-string page without additional params should be found'); 187 | done(); 188 | }); 189 | }); 190 | 191 | describe('when a query parameter has an array value', function () { 192 | it('should use an array to find array query string params regardless of order', function (done) { 193 | interfake.get('/fluent').query({ pages: [ '1', '2' ]}).status(200); 194 | interfake.listen(3000); 195 | 196 | Q.all([get({url:'http://localhost:3000/fluent?pages=1&pages=2',json:true}), get({url:'http://localhost:3000/fluent?pages=2&pages=1',json:true})]) 197 | .then(function (results) { 198 | assert.equal(results[0][0].statusCode, 200); 199 | assert.equal(results[1][0].statusCode, 200); 200 | done(); 201 | }); 202 | }); 203 | 204 | it('should use an array to find array query string params using the older square-bracket syntax', function (done) { 205 | interfake.get('/fluent').query({ pages: [ '1', '2' ]}).status(200); 206 | interfake.listen(3000); 207 | 208 | Q.all([get({url:'http://localhost:3000/fluent?pages[]=1&pages[]=2',json:true}), get({url:'http://localhost:3000/fluent?pages[]=2&pages[]=1',json:true})]) 209 | .then(function (results) { 210 | assert.equal(results[0][0].statusCode, 200); 211 | assert.equal(results[1][0].statusCode, 200); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | 217 | describe('#status()', function () { 218 | it('should create a GET endpoint which accepts different querystrings using both methods of querystring specification', function (done) { 219 | interfake.get('/fluent?query=1').query({ page: 1 }).status(400); 220 | interfake.get('/fluent?query=1').query({ page: 2 }).status(500); 221 | interfake.listen(3000); 222 | 223 | 224 | Q.all([get({url:'http://localhost:3000/fluent?query=1&page=1',json:true}), get({url:'http://localhost:3000/fluent?query=1&page=2',json:true})]) 225 | .then(function (results) { 226 | assert.equal(results[0][0].statusCode, 400); 227 | assert.equal(results[1][0].statusCode, 500); 228 | done(); 229 | }); 230 | }); 231 | 232 | it('should create a GET endpoint which accepts and does not accept a query string', function (done) { 233 | interfake.get('/fluent'); 234 | interfake.get('/fluent').query({ page: 2 }).status(500); 235 | interfake.listen(3000); 236 | 237 | 238 | Q.all([get({url:'http://localhost:3000/fluent',json:true}), get({url:'http://localhost:3000/fluent?page=2',json:true})]) 239 | .then(function (results) { 240 | assert.equal(results[0][0].statusCode, 200); 241 | assert.equal(results[1][0].statusCode, 500); 242 | done(); 243 | }); 244 | }); 245 | }); 246 | 247 | describe('#creates', function () { 248 | it('should create a GET endpoint which creates another GET endpoint which accepts a query string', function (done) { 249 | interfake.get('/fluent').creates.get('/fluent').query({ page : 2 }).status(300); 250 | interfake.listen(3000); 251 | 252 | get('http://localhost:3000/fluent') 253 | .then(function (results) { 254 | assert.equal(results[0].statusCode, 200); 255 | return get('http://localhost:3000/fluent?page=2'); 256 | }) 257 | .then(function (results) { 258 | assert.equal(results[0].statusCode, 300); 259 | return get('http://localhost:3000/fluent'); 260 | }) 261 | .then(function (results) { 262 | assert.equal(results[0].statusCode, 200); 263 | done(); 264 | }) 265 | .done(); 266 | }); 267 | it('should create a GET endpoint which creates another GET endpoint which accepts a query string with a regex', function (done) { 268 | var first = interfake.get('/fluent'); 269 | first.creates.get('/fluent').query({ page : 2 }).status(300); 270 | first.creates.get('/fluent').query({ page : 2, name : /[a-z]+/ }).status(202); 271 | interfake.listen(3000); 272 | 273 | get('http://localhost:3000/fluent') 274 | .then(function (results) { 275 | assert.equal(results[0].statusCode, 200); 276 | return get('http://localhost:3000/fluent?page=2'); 277 | }) 278 | .then(function (results) { 279 | assert.equal(results[0].statusCode, 300); 280 | return get('http://localhost:3000/fluent?page=2&name=whatever'); 281 | }) 282 | .then(function (results) { 283 | assert.equal(results[0].statusCode, 202); 284 | return get('http://localhost:3000/fluent'); 285 | }) 286 | .then(function (results) { 287 | assert.equal(results[0].statusCode, 200); 288 | done(); 289 | }) 290 | .done(); 291 | }); 292 | }); 293 | }); 294 | 295 | describe('#status()', function () { 296 | it('should create one GET endpoint with a particular status code', function (done) { 297 | interfake.get('/fluent').status(300); 298 | interfake.listen(3000); 299 | 300 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response) { 301 | assert.equal(response.statusCode, 300); 302 | done(); 303 | }); 304 | }); 305 | 306 | it('should create a GET endpoint which accepts different querystrings', function (done) { 307 | interfake.get('/fluent?query=1').status(400); 308 | interfake.get('/fluent?query=2').status(500); 309 | interfake.listen(3000); 310 | 311 | 312 | Q.all([get({url:'http://localhost:3000/fluent?query=1',json:true}), get({url:'http://localhost:3000/fluent?query=2',json:true})]) 313 | .then(function (results) { 314 | assert.equal(results[0][0].statusCode, 400); 315 | assert.equal(results[1][0].statusCode, 500); 316 | done(); 317 | }); 318 | }); 319 | }); 320 | 321 | describe('#body()', function () { 322 | it('should create one GET endpoint with a particular body', function (done) { 323 | interfake.get('/fluent').body({ fluency : 'isgreat' }); 324 | interfake.listen(3000); 325 | 326 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 327 | assert.equal(response.statusCode, 200); 328 | assert.equal(body.fluency, 'isgreat'); 329 | done(); 330 | }); 331 | }); 332 | 333 | it('should create two similar GET endpoints with different querystrings and different bodies', function (done) { 334 | interfake.get('/fluent?query=1').body({ schfifty : 'five' }); 335 | interfake.get('/fluent?query=2').body({ gimme : 'shelter' }); 336 | interfake.listen(3000); 337 | 338 | 339 | Q.all([get({url:'http://localhost:3000/fluent?query=1',json:true}), get({url:'http://localhost:3000/fluent?query=2',json:true})]) 340 | .then(function (results) { 341 | assert.equal(results[0][1].schfifty, 'five'); 342 | assert.equal(results[1][1].gimme, 'shelter'); 343 | done(); 344 | }); 345 | }); 346 | 347 | describe('#status()', function () { 348 | it('should create one GET endpoint with a particular body and particular status', function (done) { 349 | interfake.get('/fluent').body({ fluency : 'isgreat' }).status(300); 350 | interfake.listen(3000); 351 | 352 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 353 | assert.equal(response.statusCode, 300); 354 | assert.equal(body.fluency, 'isgreat'); 355 | done(); 356 | }); 357 | }); 358 | 359 | it('should create two similar GET endpoints with different querystrings and different bodies and status codes', function (done) { 360 | interfake.get('/fluent?query=1&another=one').body({ schfifty : 'five' }).status(404); 361 | interfake.get('/fluent?query=2').body({ gimme : 'shelter' }).status(503); 362 | interfake.listen(3000); 363 | 364 | 365 | Q.all([get({url:'http://localhost:3000/fluent?query=1&another=one',json:true}), get({url:'http://localhost:3000/fluent?query=2',json:true})]) 366 | .then(function (results) { 367 | assert.equal(results[0][1].schfifty, 'five'); 368 | assert.equal(results[0][0].statusCode, 404); 369 | assert.equal(results[1][1].gimme, 'shelter'); 370 | assert.equal(results[1][0].statusCode, 503); 371 | done(); 372 | }); 373 | }); 374 | 375 | describe('#delay()', function() { 376 | it('should create one GET endpoint with a particular body, status and delay', function (done) { 377 | var enoughTimeHasPassed = false; 378 | var _this = this; 379 | this.slow(500); 380 | interfake.get('/fluent').body({ fluency : 'isgreat' }).status(300).delay(50); 381 | interfake.listen(3000); 382 | setTimeout(function() { 383 | enoughTimeHasPassed = true; 384 | }, 50); 385 | 386 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 387 | assert.equal(response.statusCode, 300); 388 | assert.equal(body.fluency, 'isgreat'); 389 | if(!enoughTimeHasPassed) { 390 | throw new Error('Response wasn\'t delay for long enough'); 391 | } 392 | done(); 393 | }); 394 | }); 395 | }); 396 | }); 397 | }); 398 | describe('#delay()', function() { 399 | it('should create one GET endpoint with a particular delay', function (done) { 400 | var enoughTimeHasPassed = false; 401 | this.slow(500); 402 | interfake.get('/fluent').delay(50); 403 | interfake.listen(3000); 404 | setTimeout(function() { 405 | enoughTimeHasPassed = true; 406 | }, 50); 407 | 408 | request({ url : 'http://localhost:3000/fluent', json : true }, function () { 409 | if(!enoughTimeHasPassed) { 410 | throw new Error('Response wasn\'t delay for long enough'); 411 | } 412 | done(); 413 | }); 414 | }); 415 | 416 | describe('#body()', function() { 417 | it('should create one GET endpoint with a particular delay and body', function (done) { 418 | var enoughTimeHasPassed = false; 419 | this.slow(500); 420 | interfake.get('/fluent').delay(50).body({ 421 | ok: 'yeah' 422 | }); 423 | interfake.listen(3000); 424 | setTimeout(function() { 425 | enoughTimeHasPassed = true; 426 | }, 50); 427 | 428 | request({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 429 | if(!enoughTimeHasPassed) { 430 | throw new Error('Response wasn\'t delay for long enough'); 431 | } 432 | assert.equal(body.ok, 'yeah'); 433 | done(); 434 | }); 435 | }); 436 | }); 437 | }); 438 | }); 439 | 440 | describe('#post()', function () { 441 | it('should create one POST endpoint', function (done) { 442 | interfake.post('/fluent'); 443 | interfake.listen(3000); 444 | 445 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 446 | assert.equal(response.statusCode, 200); 447 | done(); 448 | }); 449 | }); 450 | 451 | describe('#status()', function () { 452 | it('should create one POST endpoint with a particular status code', function (done) { 453 | interfake.post('/fluent').status(300); 454 | interfake.listen(3000); 455 | 456 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 457 | assert.equal(response.statusCode, 300); 458 | done(); 459 | }); 460 | }); 461 | }); 462 | 463 | describe('#echo()', function () { 464 | it('should return the request body', function (done) { 465 | interfake.post('/stuff').echo(); 466 | 467 | interfake.listen(3000); 468 | 469 | request.post({ 470 | url :'http://localhost:3000/stuff', 471 | json : true, 472 | body : { 473 | message : 'Echo!' 474 | }, 475 | }, 476 | function (error, response, body) { 477 | assert.equal(response.statusCode, 200); 478 | assert.equal(body.message, 'Echo!'); 479 | done(); 480 | }); 481 | }); 482 | }); 483 | 484 | describe('#body()', function () { 485 | it('should create one POST endpoint with a particular body', function (done) { 486 | interfake.post('/fluent').body({ fluency : 'isgreat' }); 487 | interfake.listen(3000); 488 | 489 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 490 | assert.equal(response.statusCode, 200); 491 | assert.equal(body.fluency, 'isgreat'); 492 | done(); 493 | }); 494 | }); 495 | 496 | describe('#status()', function () { 497 | it('should create one POST endpoint with a particular body and particular status', function (done) { 498 | interfake.post('/fluent').body({ fluency : 'isgreat' }).status(300); 499 | interfake.listen(3000); 500 | 501 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 502 | assert.equal(response.statusCode, 300); 503 | assert.equal(body.fluency, 'isgreat'); 504 | done(); 505 | }); 506 | }); 507 | describe('#delay()', function() { 508 | it('should create one POST endpoint with a particular body, status and delay', function (done) { 509 | var enoughTimeHasPassed = false; 510 | var _this = this; 511 | this.slow(500); 512 | interfake.post('/fluent').body({ fluency : 'isgreat' }).status(300).delay(50); 513 | interfake.listen(3000); 514 | setTimeout(function() { 515 | enoughTimeHasPassed = true; 516 | }, 50); 517 | 518 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 519 | assert.equal(response.statusCode, 300); 520 | assert.equal(body.fluency, 'isgreat'); 521 | if(!enoughTimeHasPassed) { 522 | throw new Error('Response wasn\'t delay for long enough'); 523 | } 524 | done(); 525 | }); 526 | }); 527 | }); 528 | }); 529 | }); 530 | 531 | describe('#delay()', function() { 532 | it('should create one POST endpoint with a particular delay', function (done) { 533 | var enoughTimeHasPassed = false; 534 | var tookTooLong = false; 535 | var _this = this; 536 | 537 | this.slow(500); 538 | 539 | interfake.post('/fluent').delay(50); 540 | interfake.listen(3000); 541 | 542 | setTimeout(function() { 543 | enoughTimeHasPassed = true; 544 | }, 50); 545 | 546 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 547 | if(!enoughTimeHasPassed) { 548 | throw new Error('Response wasn\'t delay for long enough'); 549 | } 550 | done(); 551 | }); 552 | }); 553 | 554 | it('should create one POST endpoint with a delay range', function (done) { 555 | var enoughTimeHasPassed = false; 556 | var _this = this; 557 | var tookTooLong = false; 558 | var timeout; 559 | this.slow(500); 560 | interfake.post('/fluent').delay('20..50'); 561 | interfake.listen(3000); 562 | 563 | setTimeout(function() { 564 | enoughTimeHasPassed = true; 565 | }, 20); 566 | 567 | timeout = setTimeout(function() { 568 | tookTooLong = true; 569 | }, 55); 570 | 571 | request.post({ url : 'http://localhost:3000/fluent', json : true }, function () { 572 | clearTimeout(timeout); 573 | if(!enoughTimeHasPassed) { 574 | throw new Error('Response wasn\'t delay for long enough'); 575 | } 576 | if(tookTooLong) { 577 | throw new Error('Response was delayed for too long'); 578 | } 579 | done(); 580 | }); 581 | }); 582 | }); 583 | 584 | describe('#creates', function () { 585 | describe('#get()', function () { 586 | it('should create one POST endpoint with a particular body and afterResponse endpoint', function (done) { 587 | interfake.post('/fluent').creates.get('/fluent/1'); 588 | interfake.listen(3000); 589 | 590 | get('http://localhost:3000/fluent/1') 591 | .then(function (results) { 592 | assert.equal(results[0].statusCode, 404); 593 | return post('http://localhost:3000/fluent'); 594 | }) 595 | .then(function (results) { 596 | assert.equal(results[0].statusCode, 200); 597 | return get('http://localhost:3000/fluent/1'); 598 | }) 599 | .then(function (results) { 600 | assert.equal(results[0].statusCode, 200); 601 | done(); 602 | }); 603 | }); 604 | 605 | it('should create one POST endpoint with two afterResponse endpoints', function (done) { 606 | var postEndpoint = interfake.post('/fluent'); 607 | postEndpoint.creates.get('/fluent/1'); 608 | postEndpoint.creates.put('/fluent/1'); 609 | interfake.listen(3000); 610 | 611 | get('http://localhost:3000/fluent/1') 612 | .then(function (results) { 613 | assert.equal(results[0].statusCode, 404); 614 | return post('http://localhost:3000/fluent'); 615 | }) 616 | .then(function (results) { 617 | assert.equal(results[0].statusCode, 200); 618 | return get('http://localhost:3000/fluent/1'); 619 | }) 620 | .then(function (results) { 621 | assert.equal(results[0].statusCode, 200); 622 | return put('http://localhost:3000/fluent/1'); 623 | }) 624 | .then(function (results) { 625 | assert.equal(results[0].statusCode, 200); 626 | done(); 627 | }); 628 | }); 629 | 630 | it('should create one POST endpoint with two afterResponse endpoints which accept querystrings', function (done) { 631 | var postEndpoint = interfake.post('/fluent'); 632 | postEndpoint.creates.get('/fluent?q=1'); 633 | postEndpoint.creates.put('/fluent?q=1'); 634 | interfake.listen(3000); 635 | 636 | get('http://localhost:3000/fluent?q=1') 637 | .then(function (results) { 638 | assert.equal(results[0].statusCode, 404); 639 | return post('http://localhost:3000/fluent'); 640 | }) 641 | .then(function (results) { 642 | assert.equal(results[0].statusCode, 200); 643 | return get('http://localhost:3000/fluent?q=1'); 644 | }) 645 | .then(function (results) { 646 | assert.equal(results[0].statusCode, 200); 647 | return put('http://localhost:3000/fluent?q=1'); 648 | }) 649 | .then(function (results) { 650 | assert.equal(results[0].statusCode, 200); 651 | done(); 652 | }); 653 | }); 654 | 655 | describe('#query()', function () { 656 | it('should create multiple post-response GET endpoints which accept querystrings and also create endpoints (CRUD)', function (done) { 657 | // CREATE 658 | var postEndpoint = interfake.post('/fluent').status(201); 659 | // READ 660 | postEndpoint.creates.get('/fluent?q=1').status(200).body({ title : 'Hello!' }); 661 | // UPDATE 662 | var putEndpoint = postEndpoint.creates.put('/fluent?q=1').status(200).body({ title : 'Hello again!' }); 663 | putEndpoint.creates.get('/fluent?q=1').status(200).body({ title : 'Hello again!' }); 664 | // DELETE 665 | var deleteEndpoint = postEndpoint.creates.delete('/fluent?q=1').status(200); 666 | deleteEndpoint.creates.get('/fluent?q=1').status(410); 667 | deleteEndpoint.creates.put('/fluent?q=1').status(410); 668 | interfake.listen(3000); 669 | 670 | get('http://localhost:3000/fluent?q=1') 671 | .then(function (results) { 672 | assert.equal(results[0].statusCode, 404, 'The GET should not exist yet'); 673 | return post('http://localhost:3000/fluent'); 674 | }) 675 | .then(function (results) { 676 | assert.equal(results[0].statusCode, 201, 'The POST should report creation'); 677 | return get({url: 'http://localhost:3000/fluent?q=1', json: true}); 678 | }) 679 | .then(function (results) { 680 | assert.equal(results[0].statusCode, 200, 'The GET should now exist'); 681 | assert.equal(results[1].title, 'Hello!'); 682 | return put({url: 'http://localhost:3000/fluent?q=1', json: true}); 683 | }) 684 | .then(function (results) { 685 | assert.equal(results[0].statusCode, 200, 'The PUT should have sucessfully updated'); 686 | assert.equal(results[1].title, 'Hello again!'); 687 | return get({url: 'http://localhost:3000/fluent?q=1', json: true}); 688 | }) 689 | .then(function (results) { 690 | assert.equal(results[0].statusCode, 200, 'The GET should still exist'); 691 | assert.equal(results[1].title, 'Hello again!'); 692 | return del({url: 'http://localhost:3000/fluent?q=1', json: true}); 693 | }) 694 | .then(function (results) { 695 | assert.equal(results[0].statusCode, 200, 'The DELETE should report successful deletion'); 696 | return get({url: 'http://localhost:3000/fluent?q=1', json: true}); 697 | }) 698 | .then(function (results) { 699 | assert.equal(results[0].statusCode, 410, 'The GET should no longer exist'); 700 | return put({url: 'http://localhost:3000/fluent?q=1', json: true}); 701 | }) 702 | .then(function (results) { 703 | assert.equal(results[0].statusCode, 410, 'The PUT should no longer exist'); 704 | done(); 705 | }) 706 | .fail(done); 707 | }); 708 | }); 709 | 710 | describe('#status()', function () { 711 | it('should create a post-response GET with a particular status', function (done) { 712 | interfake.post('/fluent').creates.get('/fluent/1').status(300); 713 | interfake.listen(3000); 714 | 715 | get('http://localhost:3000/fluent/1') 716 | .then(function (results) { 717 | assert.equal(results[0].statusCode, 404); 718 | return post('http://localhost:3000/fluent'); 719 | }) 720 | .then(function (results) { 721 | assert.equal(results[0].statusCode, 200); 722 | return get('http://localhost:3000/fluent/1'); 723 | }) 724 | .then(function (results) { 725 | assert.equal(results[0].statusCode, 300); 726 | done(); 727 | }); 728 | }); 729 | }); 730 | 731 | describe('#body()', function () { 732 | it('should create a post-response GET with a particular body', function (done) { 733 | interfake.post('/fluent').creates.get('/fluent/1').body({ fluency : 'is badass' }); 734 | interfake.listen(3000); 735 | 736 | get('http://localhost:3000/fluent/1') 737 | .then(function (results) { 738 | assert.equal(results[0].statusCode, 404); 739 | return post('http://localhost:3000/fluent'); 740 | }) 741 | .then(function (results) { 742 | assert.equal(results[0].statusCode, 200); 743 | return get({url:'http://localhost:3000/fluent/1', json:true}); 744 | }) 745 | .then(function (results) { 746 | assert.equal(results[1].fluency, 'is badass'); 747 | done(); 748 | }); 749 | }); 750 | 751 | describe('#status()', function() { 752 | it('should create a post-response GET with a particular and body and status', function (done) { 753 | interfake.post('/fluent').creates.get('/fluent/1').body({ fluency : 'is badass' }).status(500); 754 | interfake.listen(3000); 755 | 756 | get('http://localhost:3000/fluent/1') 757 | .then(function (results) { 758 | assert.equal(results[0].statusCode, 404); 759 | return post('http://localhost:3000/fluent'); 760 | }) 761 | .then(function (results) { 762 | assert.equal(results[0].statusCode, 200); 763 | return get({url:'http://localhost:3000/fluent/1', json:true}); 764 | }) 765 | .then(function (results) { 766 | assert.equal(results[0].statusCode, 500); 767 | assert.equal(results[1].fluency, 'is badass'); 768 | done(); 769 | }); 770 | }); 771 | }); 772 | }); 773 | 774 | describe('#creates', function () { 775 | it('should create a post-response GET with another post-response GET', function (done) { 776 | interfake.post('/fluent').creates.get('/fluent/1').creates.get('/fluent/2'); 777 | interfake.listen(3000); 778 | 779 | get('http://localhost:3000/fluent/1') 780 | .then(function (results) { 781 | assert.equal(results[0].statusCode, 404); 782 | return post('http://localhost:3000/fluent'); 783 | }) 784 | .then(function (results) { 785 | assert.equal(results[0].statusCode, 200); 786 | return get('http://localhost:3000/fluent/1'); 787 | }) 788 | .then(function (results) { 789 | assert.equal(results[0].statusCode, 200); 790 | return get('http://localhost:3000/fluent/2'); 791 | }) 792 | .then(function (results) { 793 | assert.equal(results[0].statusCode, 200); 794 | done(); 795 | }); 796 | }); 797 | }); 798 | }); 799 | }); 800 | }); 801 | 802 | describe('#patch()', function () { 803 | it('should create one PATCH endpoint', function (done) { 804 | interfake.patch('/fluent'); 805 | interfake.listen(3000); 806 | 807 | request.patch({ url : 'http://localhost:3000/fluent', json : true }, function (error, response, body) { 808 | assert.equal(response.statusCode, 200); 809 | done(); 810 | }); 811 | }); 812 | 813 | describe('#extends', function () { 814 | it('should create a PATCH endpoint which allows itself to be extended', function (done) { 815 | interfake.get('/users').body([ 816 | { 817 | name: 'Max Headroom' 818 | } 819 | ]); 820 | 821 | interfake.patch('/users').extends.get('/users').body([{ 822 | name: 'Min Headroom' 823 | } 824 | ]); 825 | 826 | interfake.listen(3000); 827 | 828 | get({ url : 'http://localhost:3000/users', json : true }) 829 | .then(function (results) { 830 | assert.equal(results[0].statusCode, 200); 831 | assert.equal(results[1].length, 1); 832 | return patch({ url : 'http://localhost:3000/users', json : true }); 833 | }) 834 | .then(function (results) { 835 | assert.equal(results[0].statusCode, 200); 836 | return get({ url : 'http://localhost:3000/users', json : true }); 837 | }) 838 | .then(function (results) { 839 | assert.equal(results[0].statusCode, 200); 840 | assert.equal(results[1].length, 2); 841 | done(); 842 | }) 843 | .catch(done); 844 | }); 845 | }); 846 | }); 847 | 848 | // Testing #extends stuff 849 | describe('#extends', function () { 850 | describe('#get()', function() { 851 | describe('#body()', function () { 852 | it('should create a GET endpoint which extends its own body when it gets called', function (done) { 853 | interfake.get('/fluent').body({ hello : 'there', goodbye: 'for now' }).extends.get('/fluent').body({ what: 'ever' }); 854 | interfake.listen(3000); 855 | 856 | get({url:'http://localhost:3000/fluent',json:true}) 857 | .then(function (results) { 858 | assert.equal(results[0].statusCode, 200); 859 | assert.equal(results[1].hello, 'there'); 860 | assert.equal(results[1].goodbye, 'for now'); 861 | assert.equal(results[1].what, undefined); 862 | return get({url:'http://localhost:3000/fluent',json:true}); 863 | }) 864 | .then(function (results) { 865 | assert.equal(results[0].statusCode, 200); 866 | assert.equal(results[1].hello, 'there'); 867 | assert.equal(results[1].goodbye, 'for now'); 868 | assert.equal(results[1].what, 'ever'); 869 | done(); 870 | }) 871 | .done(); 872 | }); 873 | 874 | it('should create a POST endpoint which adds to a GET endpoint with deep array when it gets called', function (done) { 875 | interfake.get('/items').body({ items : [ { id: 1 } ] }); 876 | interfake.post('/items').status(201).extends.get('/items').body({ items : [ { id : 2 } ] }); 877 | interfake.listen(3000); 878 | 879 | get({url:'http://localhost:3000/items',json:true}) 880 | .then(function (results) { 881 | assert.equal(results[0].statusCode, 200); 882 | assert.equal(results[1].items[0].id, 1); 883 | assert.equal(results[1].items.length, 1); 884 | return post({url:'http://localhost:3000/items',json:true}); 885 | }) 886 | .then(function (results) { 887 | assert.equal(results[0].statusCode, 201); 888 | return get({url:'http://localhost:3000/items',json:true}); 889 | }) 890 | .then(function (results) { 891 | assert.equal(results[0].statusCode, 200); 892 | assert.equal(results[1].items[0].id, 1); 893 | assert.equal(results[1].items[1].id, 2); 894 | assert.equal(results[1].items.length, 2); 895 | done(); 896 | }) 897 | .done(); 898 | }); 899 | 900 | it('should create a POST endpoint which adds to a GET endpoint with top-level array when it gets called', function (done) { 901 | interfake.get('/items').body([ { id: 1 } ]); 902 | interfake.post('/items').status(201).extends.get('/items').body([ { id : 2 } ]); 903 | interfake.listen(3000); 904 | 905 | get({url:'http://localhost:3000/items',json:true}) 906 | .then(function (results) { 907 | assert.equal(results[0].statusCode, 200); 908 | assert.equal(results[1][0].id, 1); 909 | assert.equal(results[1][1], undefined); 910 | return post({url:'http://localhost:3000/items',json:true}); 911 | }) 912 | .then(function (results) { 913 | assert.equal(results[0].statusCode, 201); 914 | return get({url:'http://localhost:3000/items',json:true}); 915 | }) 916 | .then(function (results) { 917 | assert.equal(results[0].statusCode, 200); 918 | assert.equal(results[1][0].id, 1); 919 | assert.equal(results[1][1].id, 2); 920 | done(); 921 | }) 922 | .done(); 923 | }); 924 | }); 925 | 926 | describe('#status()', function () { 927 | it('should create a GET endpoint which extends its own status when it gets called', function (done) { 928 | interfake.get('/fluent').body({ hello : 'there', goodbye: 'for now' }).extends.get('/fluent').status(401); 929 | interfake.listen(3000); 930 | 931 | get({url:'http://localhost:3000/fluent',json:true}) 932 | .then(function (results) { 933 | assert.equal(results[0].statusCode, 200); 934 | assert.equal(results[1].hello, 'there'); 935 | assert.equal(results[1].goodbye, 'for now'); 936 | assert.equal(results[1].what, undefined); 937 | return get({url:'http://localhost:3000/fluent',json:true}); 938 | }) 939 | .then(function (results) { 940 | assert.equal(results[0].statusCode, 401); 941 | assert.equal(results[1].hello, 'there'); 942 | assert.equal(results[1].goodbye, 'for now'); 943 | assert.equal(results[1].what, undefined); 944 | done(); 945 | }) 946 | .done(); 947 | }); 948 | }); 949 | 950 | describe('#responseHeaders()', function () { 951 | it('should create a GET endpoint which extends its own response headers when it gets called', function (done) { 952 | interfake.get('/fluent').responseHeaders({ 'Awesome-Header' : 'Awesome Value' }).extends.get('/fluent').responseHeaders({ 'Lame-Header' : 'Lame Value' }); 953 | interfake.listen(3000); 954 | 955 | get({url:'http://localhost:3000/fluent',json:true}) 956 | .then(function (results) { 957 | assert.equal(results[0].headers['awesome-header'], 'Awesome Value'); 958 | assert.equal(results[0].headers['lame-header'], undefined); 959 | return get({url:'http://localhost:3000/fluent',json:true}); 960 | }) 961 | .then(function (results) { 962 | assert.equal(results[0].headers['awesome-header'], 'Awesome Value'); 963 | assert.equal(results[0].headers['lame-header'], 'Lame Value'); 964 | done(); 965 | }) 966 | .done(); 967 | }); 968 | }); 969 | 970 | describe('#delay()', function () { 971 | it('should create a GET endpoint which adds a delay to itself when it gets called', function (done) { 972 | var enoughTimeHasPassed; 973 | interfake.get('/fluent').body({ hello : 'there', goodbye: 'for now' }).extends.get('/fluent').delay(50); 974 | interfake.listen(3000); 975 | 976 | setTimeout(function() { 977 | enoughTimeHasPassed = true; 978 | }, 50); 979 | 980 | get({url:'http://localhost:3000/fluent',json:true}) 981 | .then(function (results) { 982 | assert.equal(results[0].statusCode, 200); 983 | assert.equal(results[1].hello, 'there'); 984 | assert.equal(results[1].goodbye, 'for now'); 985 | assert.equal(results[1].what, undefined); 986 | return get({url:'http://localhost:3000/fluent',json:true}); 987 | }) 988 | .then(function (results) { 989 | if(!enoughTimeHasPassed) { 990 | throw new Error('Response wasn\'t delay for long enough'); 991 | } 992 | assert.equal(results[0].statusCode, 200); 993 | assert.equal(results[1].hello, 'there'); 994 | assert.equal(results[1].goodbye, 'for now'); 995 | assert.equal(results[1].what, undefined); 996 | done(); 997 | }) 998 | .done(); 999 | }); 1000 | 1001 | it('should create a GET endpoint which adds a delay to a different endpoint when it gets called', function (done) { 1002 | var enoughTimeHasPassed, tookTooLong; 1003 | interfake.get('/fluent').extends.get('/needs-delay').delay(50); 1004 | interfake.get('/needs-delay'); 1005 | interfake.listen(3000); 1006 | 1007 | setTimeout(function() { 1008 | tookTooLong = true; 1009 | }, 50); 1010 | 1011 | get({url:'http://localhost:3000/needs-delay', json:true}) 1012 | .then(function (results) { 1013 | assert.equal(results[0].statusCode, 200); 1014 | if (tookTooLong) { 1015 | throw new Error('The response took too long the first time'); 1016 | } 1017 | return get({url:'http://localhost:3000/fluent', json:true}); 1018 | }) 1019 | .then(function (results) { 1020 | assert.equal(results[0].statusCode, 200); 1021 | setTimeout(function() { 1022 | enoughTimeHasPassed = true; 1023 | }, 50); 1024 | return get({url:'http://localhost:3000/needs-delay', json:true}); 1025 | }) 1026 | .then(function (results) { 1027 | assert.equal(results[0].statusCode, 200); 1028 | if(!enoughTimeHasPassed) { 1029 | throw new Error('Response wasn\'t delay for long enough'); 1030 | } 1031 | done(); 1032 | }) 1033 | .done(); 1034 | }); 1035 | }); 1036 | 1037 | describe('#query()', function () { 1038 | it('should create a GET endpoint with a query which extends its own status', function (done) { 1039 | interfake.get('/fluent').query({ page : 2 }).status(200).extends.get('/fluent').query({ page : 2 }).status(300); 1040 | interfake.listen(3000); 1041 | 1042 | get('http://localhost:3000/fluent?page=2') 1043 | .then(function (results) { 1044 | assert.equal(results[0].statusCode, 200); 1045 | return get('http://localhost:3000/fluent?page=2'); 1046 | }) 1047 | .then(function (results) { 1048 | assert.equal(results[0].statusCode, 300); 1049 | done(); 1050 | }) 1051 | .done(); 1052 | }); 1053 | }); 1054 | 1055 | describe('#creates', function () { 1056 | describe('#get', function () { 1057 | it('should produce a useful error when trying to spawn a GET endpoint from a modified endpoint', function (done) { 1058 | assert.throws(function () { 1059 | interfake.get('/fluent').extends.get('/fluent').creates.get('/error'); 1060 | }, function (err) { 1061 | assert.equal(err.message, 'Sorry, but modified routes cannot yet create new routes after their response. This is planned for a future version of Interfake.'); 1062 | done(); 1063 | return true; 1064 | }); 1065 | }); 1066 | }); 1067 | describe('#post', function () { 1068 | it('should produce a useful error when trying to spawn a POST endpoint from a modified endpoint', function (done) { 1069 | assert.throws(function () { 1070 | interfake.get('/fluent').extends.get('/fluent').creates.post('/error'); 1071 | }, function (err) { 1072 | assert.equal(err.message, 'Sorry, but modified routes cannot yet create new routes after their response. This is planned for a future version of Interfake.'); 1073 | done(); 1074 | return true; 1075 | }); 1076 | }); 1077 | }); 1078 | describe('#put', function () { 1079 | it('should produce a useful error when trying to spawn a PUT endpoint from a modified endpoint', function (done) { 1080 | assert.throws(function () { 1081 | interfake.get('/fluent').extends.get('/fluent').creates.put('/error'); 1082 | }, function (err) { 1083 | assert.equal(err.message, 'Sorry, but modified routes cannot yet create new routes after their response. This is planned for a future version of Interfake.'); 1084 | done(); 1085 | return true; 1086 | }); 1087 | }); 1088 | }); 1089 | describe('#delete', function () { 1090 | it('should produce a useful error when trying to spawn a DELETE endpoint from a modified endpoint', function (done) { 1091 | assert.throws(function () { 1092 | interfake.get('/fluent').extends.get('/fluent').creates.delete('/error'); 1093 | }, function (err) { 1094 | assert.equal(err.message, 'Sorry, but modified routes cannot yet create new routes after their response. This is planned for a future version of Interfake.'); 1095 | done(); 1096 | return true; 1097 | }); 1098 | }); 1099 | }); 1100 | }); 1101 | 1102 | describe('#extends', function () { 1103 | describe('#get', function () { 1104 | it('should produce a useful error when trying to modify an existing GET endpoint from a modified endpoint', function (done) { 1105 | assert.throws(function () { 1106 | interfake.get('/fluent').extends.get('/fluent').extends.get('/error'); 1107 | }, function (err) { 1108 | assert.equal(err.message, 'Sorry, but modified routes cannot yet modify existing routes after their response. This is planned for a future version of Interfake.'); 1109 | done(); 1110 | return true; 1111 | }); 1112 | }); 1113 | }); 1114 | describe('#post', function () { 1115 | it('should produce a useful error when trying to modify an existing POST endpoint from a modified endpoint', function (done) { 1116 | assert.throws(function () { 1117 | interfake.get('/fluent').extends.get('/fluent').extends.post('/error'); 1118 | }, function (err) { 1119 | assert.equal(err.message, 'Sorry, but modified routes cannot yet modify existing routes after their response. This is planned for a future version of Interfake.'); 1120 | done(); 1121 | return true; 1122 | }); 1123 | }); 1124 | }); 1125 | describe('#put', function () { 1126 | it('should produce a useful error when trying to modify an existing PUT endpoint from a modified endpoint', function (done) { 1127 | assert.throws(function () { 1128 | interfake.get('/fluent').extends.get('/fluent').extends.put('/error'); 1129 | }, function (err) { 1130 | assert.equal(err.message, 'Sorry, but modified routes cannot yet modify existing routes after their response. This is planned for a future version of Interfake.'); 1131 | done(); 1132 | return true; 1133 | }); 1134 | }); 1135 | }); 1136 | describe('#delete', function () { 1137 | it('should produce a useful error when trying to modify an existing DELETE endpoint from a modified endpoint', function (done) { 1138 | assert.throws(function () { 1139 | interfake.get('/fluent').extends.get('/fluent').extends.delete('/error'); 1140 | }, function (err) { 1141 | assert.equal(err.message, 'Sorry, but modified routes cannot yet modify existing routes after their response. This is planned for a future version of Interfake.'); 1142 | done(); 1143 | return true; 1144 | }); 1145 | }); 1146 | }); 1147 | }); 1148 | }); 1149 | 1150 | describe('#put()', function () { 1151 | describe('#body()', function () { 1152 | it('should create a PUT endpoint which extends its own body when it gets called', function (done) { 1153 | interfake.put('/fluent').body({ version : 1 }).extends.put('/fluent').body({ version: 2 }); 1154 | interfake.listen(3000); 1155 | 1156 | put({url:'http://localhost:3000/fluent',json:true}) 1157 | .then(function (results) { 1158 | assert.equal(results[0].statusCode, 200); 1159 | assert.equal(results[1].version, 1); 1160 | return put({url:'http://localhost:3000/fluent',json:true}); 1161 | }) 1162 | .then(function (results) { 1163 | assert.equal(results[0].statusCode, 200); 1164 | assert.equal(results[1].version, 2); 1165 | done(); 1166 | }) 1167 | .done(); 1168 | }); 1169 | }); 1170 | }); 1171 | 1172 | describe('#post()', function () { 1173 | describe('#body()', function () { 1174 | it('should create a POST endpoint which extends its own body when it gets called', function (done) { 1175 | interfake.post('/fluent').body({ version : 1 }).extends.post('/fluent').body({ version: 2 }); 1176 | interfake.listen(3000); 1177 | 1178 | post({url:'http://localhost:3000/fluent',json:true}) 1179 | .then(function (results) { 1180 | assert.equal(results[0].statusCode, 200); 1181 | assert.equal(results[1].version, 1); 1182 | return post({url:'http://localhost:3000/fluent',json:true}); 1183 | }) 1184 | .then(function (results) { 1185 | assert.equal(results[0].statusCode, 200); 1186 | assert.equal(results[1].version, 2); 1187 | done(); 1188 | }) 1189 | .done(); 1190 | }); 1191 | }); 1192 | 1193 | describe('#echo()', function () { 1194 | it('should create a POST endpoint which becomes an echo endpoint when it gets called', function (done) { 1195 | interfake.post('/fluent').body({ version : 1 }).extends.post('/fluent').echo(); 1196 | interfake.listen(3000); 1197 | 1198 | post({url:'http://localhost:3000/fluent',json:true}) 1199 | .then(function (results) { 1200 | assert.equal(results[0].statusCode, 200); 1201 | assert.equal(results[1].version, 1); 1202 | return post({ url:'http://localhost:3000/fluent', json : true, body : { version : 5 } }); 1203 | }) 1204 | .then(function (results) { 1205 | assert.equal(results[0].statusCode, 200); 1206 | assert.equal(results[1].version, 5); 1207 | done(); 1208 | }) 1209 | .done(); 1210 | }); 1211 | 1212 | it('should create a POST endpoint which was an echo endpoint but becomes a non-echo endpoint when it gets called', function (done) { 1213 | interfake.post('/fluent').echo().extends.post('/fluent').body({ version : 2 }).echo(false); 1214 | interfake.listen(3000); 1215 | 1216 | post({ url:'http://localhost:3000/fluent', json : true, body : { version : 5 } }) 1217 | .then(function (results) { 1218 | assert.equal(results[0].statusCode, 200); 1219 | assert.equal(results[1].version, 5); 1220 | return post({ url:'http://localhost:3000/fluent', json : true, body : { version : 5 } }); 1221 | }) 1222 | .then(function (results) { 1223 | assert.equal(results[0].statusCode, 200); 1224 | assert.equal(results[1].version, 2); 1225 | done(); 1226 | }) 1227 | .done(); 1228 | }); 1229 | }); 1230 | }); 1231 | 1232 | describe('#delete()', function () { 1233 | describe('#body()', function () { 1234 | it('should create a DELETE endpoint which extends its own body when it gets called', function (done) { 1235 | interfake.delete('/fluent').body({ version : 1 }).extends.delete('/fluent').body({ version: 2 }); 1236 | interfake.listen(3000); 1237 | 1238 | del({url:'http://localhost:3000/fluent',json:true}) 1239 | .then(function (results) { 1240 | assert.equal(results[0].statusCode, 200); 1241 | assert.equal(results[1].version, 1); 1242 | return del({url:'http://localhost:3000/fluent',json:true}); 1243 | }) 1244 | .then(function (results) { 1245 | assert.equal(results[0].statusCode, 200); 1246 | assert.equal(results[1].version, 2); 1247 | done(); 1248 | }) 1249 | .done(); 1250 | }); 1251 | }); 1252 | }); 1253 | }); 1254 | }); -------------------------------------------------------------------------------- /tests/http.test.js: -------------------------------------------------------------------------------- 1 | /* globals describe, beforeEach, afterEach, it */ 2 | var assert = require('assert'); 3 | var request = require('request'); 4 | var Q = require('q'); 5 | 6 | request.defaults({ 7 | json:true 8 | }); 9 | 10 | var get = Q.denodeify(request.get); 11 | var post = Q.denodeify(request.post); 12 | var put = Q.denodeify(request.put); 13 | 14 | // The thing we're testing 15 | var Interfake = require('..'); 16 | 17 | describe('Interfake HTTP API', function () { 18 | describe('POST /_request', function () { 19 | it('should create one GET endpoint', function (done) { 20 | var interfake = new Interfake(/*{debug:true}*/); 21 | interfake.listen(3000); 22 | 23 | var endpoint = { 24 | request: { 25 | url: '/test', 26 | method: 'get' 27 | }, 28 | response: { 29 | code: 200, 30 | body: { 31 | hi: 'there' 32 | }, 33 | headers: { 34 | 'foo': 'bar' 35 | } 36 | } 37 | }; 38 | 39 | post({url: 'http://localhost:3000/_request', json: true, body: endpoint}) 40 | .then(function (results) { 41 | assert.equal(results[0].statusCode, 201); 42 | assert.equal(results[1].done, true); 43 | return get({url:'http://localhost:3000/test', json: true}); 44 | }) 45 | .then(function (results) { 46 | assert.equal(results[0].headers['foo'], 'bar'); 47 | // console.log(results[0].cookies) 48 | assert.equal(results[0].statusCode, 200); 49 | assert.equal(results[1].hi, 'there'); 50 | interfake.stop(); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | }); -------------------------------------------------------------------------------- /tests/javascript.test.js: -------------------------------------------------------------------------------- 1 | /* globals describe, beforeEach, afterEach, it */ 2 | var assert = require('assert'); 3 | var Q = require('q'); 4 | var request = require('request'); 5 | var path = require('path'); 6 | 7 | request = request.defaults({ 8 | json: true 9 | }); 10 | 11 | var get = Q.denodeify(request.get); 12 | var post = Q.denodeify(request.post); 13 | var put = Q.denodeify(request.put); 14 | var del = Q.denodeify(request.del); 15 | 16 | // The thing we're testing 17 | var Interfake = require('..'); 18 | var interfake; 19 | 20 | describe('Interfake JavaScript API', function() { 21 | beforeEach(function() { 22 | interfake = new Interfake(); 23 | }); 24 | afterEach(function() { 25 | if (interfake) { 26 | interfake.stop(); 27 | } 28 | }); 29 | describe('#listen', function() { 30 | it('should should support a callback', function(done) { 31 | interfake.listen(3000, done); 32 | }); 33 | }); 34 | describe('#createRoute()', function() { 35 | describe('when a GET endpoint is specified', function () { 36 | beforeEach(function (done) { 37 | interfake.createRoute({ 38 | request: { 39 | url: '/test/it/out', 40 | method: 'get' 41 | }, 42 | response: { 43 | code: 200, 44 | body: { 45 | hi: 'there' 46 | } 47 | } 48 | }); 49 | interfake.listen(3000, done); 50 | }); 51 | 52 | it('should not create unexpected endpoints', function(done) { 53 | request({ url : 'http://localhost:3000/donottest/it/out', json : false }, function(error, response, body) { 54 | assert.equal(response.statusCode, 404); 55 | assert(body.match('Cannot GET /donottest/it/out')); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should not advertise unexpected endpoints', function(done) { 61 | request({ method : 'options', url : 'http://localhost:3000/donottest/it/out', json : false }, function(error, response, body) { 62 | assert.equal(response.statusCode, 404); 63 | assert(body.match('Cannot OPTIONS /donottest/it/out')); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should create one GET endpoint', function(done) { 69 | request('http://localhost:3000/test/it/out', function(error, response, body) { 70 | assert.equal(response.statusCode, 200); 71 | assert.equal(body.hi, 'there'); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should advertise the GET in an OPTIONS request', function(done) { 77 | request({ method : 'options', url : 'http://localhost:3000/test/it/out' }, function(error, response, body) { 78 | assert.equal(response.statusCode, 200); 79 | assert.equal(response.headers['access-control-allow-methods'], 'GET, OPTIONS'); 80 | assert.equal(response.headers['access-control-allow-origin'], '*'); 81 | done(); 82 | }); 83 | }); 84 | 85 | describe('when a POST option is added to the same endpoint', function () { 86 | beforeEach(function () { 87 | interfake.createRoute({ 88 | request: { 89 | url: '/test/it/out', 90 | method: 'post' 91 | }, 92 | response: { 93 | code: 200, 94 | body: { 95 | hi: 'there' 96 | } 97 | } 98 | }); 99 | }); 100 | 101 | it('should advertise the GET and the POST in an OPTIONS request', function(done) { 102 | request({ method : 'options', url : 'http://localhost:3000/test/it/out' }, function(error, response, body) { 103 | assert.equal(response.statusCode, 200); 104 | assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, OPTIONS'); 105 | assert.equal(response.headers['access-control-allow-origin'], '*'); 106 | done(); 107 | }); 108 | }); 109 | 110 | describe('when a POST option is added to the same endpoint', function () { 111 | beforeEach(function () { 112 | interfake.createRoute({ 113 | request: { 114 | url: '/test/it/out', 115 | method: 'patch' 116 | }, 117 | response: { 118 | code: 200, 119 | body: { 120 | hi: 'there' 121 | } 122 | } 123 | }); 124 | }); 125 | 126 | it('should advertise the GET, POST and PATCH in an OPTIONS request', function(done) { 127 | request({ method : 'options', url : 'http://localhost:3000/test/it/out' }, function(error, response, body) { 128 | assert.equal(response.statusCode, 200); 129 | assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PATCH, OPTIONS'); 130 | assert.equal(response.headers['access-control-allow-origin'], '*'); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('when the URL is flagged as being a regular expression (in string format)', function () { 139 | beforeEach(function (done) { 140 | interfake.createRoute({ 141 | request: { 142 | url: { 143 | pattern : '/test/it/(up|out)', 144 | regexp : true 145 | }, 146 | method: 'get' 147 | }, 148 | response: { 149 | code: 200, 150 | body: { 151 | hi: 'there' 152 | } 153 | } 154 | }); 155 | interfake.listen(3000, done); 156 | }); 157 | 158 | it('should not create unexpected endpoints', function(done) { 159 | request({ url : 'http://localhost:3000/donottest/it/out', json : false }, function(error, response, body) { 160 | assert.equal(response.statusCode, 404); 161 | assert(body.match('Cannot GET /donottest/it/out')) 162 | done(); 163 | }); 164 | }); 165 | 166 | it('should not advertise unexpected endpoints', function(done) { 167 | request({ method : 'options', url : 'http://localhost:3000/donottest/it/out', json : false }, function(error, response, body) { 168 | assert.equal(response.statusCode, 404); 169 | assert(body.match('Cannot OPTIONS /donottest/it/out')) 170 | done(); 171 | }); 172 | }); 173 | 174 | it('should allow requests to one varition on the regular expression', function(done) { 175 | request('http://localhost:3000/test/it/out', function(error, response, body) { 176 | assert.equal(response.statusCode, 200); 177 | assert.equal(body.hi, 'there'); 178 | done(); 179 | }); 180 | }); 181 | 182 | it('should allow requests to another varition on the regular expression', function(done) { 183 | request('http://localhost:3000/test/it/up', function(error, response, body) { 184 | assert.equal(response.statusCode, 200); 185 | assert.equal(body.hi, 'there'); 186 | done(); 187 | }); 188 | }); 189 | 190 | it('should advertise the GET in an OPTIONS request for the first variation', function(done) { 191 | request({ method : 'options', url : 'http://localhost:3000/test/it/out' }, function(error, response, body) { 192 | assert.equal(response.statusCode, 200); 193 | assert.equal(response.headers['access-control-allow-methods'], 'GET, OPTIONS'); 194 | assert.equal(response.headers['access-control-allow-origin'], '*'); 195 | done(); 196 | }); 197 | }); 198 | 199 | it('should advertise the GET in an OPTIONS request for the second variation', function(done) { 200 | request({ method : 'options', url : 'http://localhost:3000/test/it/up' }, function(error, response, body) { 201 | assert.equal(response.statusCode, 200); 202 | assert.equal(response.headers['access-control-allow-methods'], 'GET, OPTIONS'); 203 | assert.equal(response.headers['access-control-allow-origin'], '*'); 204 | done(); 205 | }); 206 | }); 207 | }); 208 | 209 | describe('when the URL is already a regular expression', function () { 210 | beforeEach(function (done) { 211 | interfake.createRoute({ 212 | request: { 213 | url: /\/test\/it\/(up|out)/, 214 | method: 'get' 215 | }, 216 | response: { 217 | code: 200, 218 | body: { 219 | hi: 'there' 220 | } 221 | } 222 | }); 223 | interfake.listen(3000, done); 224 | }); 225 | 226 | it('should not create unexpected endpoints', function(done) { 227 | request({ url : 'http://localhost:3000/donottest/it/out', json : false }, function(error, response, body) { 228 | assert.equal(response.statusCode, 404); 229 | assert(body.match('Cannot GET /donottest/it/out')) 230 | done(); 231 | }); 232 | }); 233 | 234 | it('should not advertise unexpected endpoints', function(done) { 235 | request({ method : 'options', url : 'http://localhost:3000/donottest/it/out', json : false }, function(error, response, body) { 236 | assert.equal(response.statusCode, 404); 237 | assert(body.match('Cannot OPTIONS /donottest/it/out')) 238 | done(); 239 | }); 240 | }); 241 | 242 | it('should allow requests to one varition on the regular expression', function(done) { 243 | request('http://localhost:3000/test/it/out', function(error, response, body) { 244 | assert.equal(response.statusCode, 200); 245 | assert.equal(body.hi, 'there'); 246 | done(); 247 | }); 248 | }); 249 | 250 | it('should allow requests to another varition on the regular expression', function(done) { 251 | request('http://localhost:3000/test/it/up', function(error, response, body) { 252 | assert.equal(response.statusCode, 200); 253 | assert.equal(body.hi, 'there'); 254 | done(); 255 | }); 256 | }); 257 | 258 | it('should advertise the GET in an OPTIONS request for the first variation', function(done) { 259 | request({ method : 'options', url : 'http://localhost:3000/test/it/out' }, function(error, response, body) { 260 | assert.equal(response.statusCode, 200); 261 | assert.equal(response.headers['access-control-allow-methods'], 'GET, OPTIONS'); 262 | assert.equal(response.headers['access-control-allow-origin'], '*'); 263 | done(); 264 | }); 265 | }); 266 | 267 | it('should advertise the GET in an OPTIONS request for the second variation', function(done) { 268 | request({ method : 'options', url : 'http://localhost:3000/test/it/up' }, function(error, response, body) { 269 | assert.equal(response.statusCode, 200); 270 | assert.equal(response.headers['access-control-allow-methods'], 'GET, OPTIONS'); 271 | assert.equal(response.headers['access-control-allow-origin'], '*'); 272 | done(); 273 | }); 274 | }); 275 | }); 276 | 277 | it('should create one GET endpoint which returns custom headers', function(done) { 278 | interfake.createRoute({ 279 | request: { 280 | url: '/test', 281 | method: 'get' 282 | }, 283 | response: { 284 | code: 200, 285 | body: { 286 | hi: 'there' 287 | }, 288 | headers: { 289 | 'X-Request-Type': 'test', 290 | 'X-lol-TEST': 'bleep' 291 | } 292 | } 293 | }); 294 | interfake.listen(3000); 295 | 296 | request('http://localhost:3000/test', function(error, response, body) { 297 | assert.equal(response.statusCode, 200); 298 | assert.equal(response.headers['x-request-type'], 'test'); 299 | assert.equal(response.headers['x-lol-test'], 'bleep'); 300 | assert.equal(response.headers['x-undef'], undefined); 301 | assert.equal(body.hi, 'there'); 302 | done(); 303 | }); 304 | }); 305 | 306 | it('should create a GET endpoint that accepts a query parameter', function(done) { 307 | // interfake = new Interfake(); 308 | interfake.createRoute({ 309 | request: { 310 | url: '/wantsQueryParameter', 311 | query: { 312 | query: '1234' 313 | }, 314 | method: 'get' 315 | 316 | }, 317 | response: { 318 | code: 200, 319 | body: { 320 | high: 'hoe' 321 | } 322 | } 323 | }); 324 | interfake.listen(3000); 325 | 326 | request('http://localhost:3000/wantsQueryParameter?query=1234', function(error, response, body) { 327 | assert.equal(error, undefined); 328 | assert.equal(response.statusCode, 200); 329 | assert.equal(body.high, 'hoe'); 330 | done(); 331 | }); 332 | }); 333 | 334 | it('should create a GET endpoint that accepts a query array parameter', function(done) { 335 | interfake.createRoute({ 336 | request: { 337 | url: '/wantsQueryArrayParameter', 338 | query: { 339 | pages: ['1', '2'] 340 | }, 341 | method: 'get' 342 | }, 343 | response: { 344 | code: 200, 345 | body: { 346 | high: 'hoe' 347 | } 348 | } 349 | }); 350 | interfake.listen(3000); 351 | 352 | request('http://localhost:3000/wantsQueryArrayParameter?pages=1&pages=2', function(error, response, body) { 353 | assert.equal(error, undefined); 354 | assert.equal(response.statusCode, 200); 355 | assert.equal(body.high, 'hoe'); 356 | done(); 357 | }); 358 | }); 359 | 360 | it('should create one GET endpoint accepting query parameters with different responses', function() { 361 | interfake.createRoute({ 362 | request: { 363 | url: '/wantsQueryParameter', 364 | query: { 365 | query: '1234' 366 | }, 367 | method: 'get' 368 | 369 | }, 370 | response: { 371 | code: 200, 372 | body: { 373 | high: 'hoe' 374 | } 375 | } 376 | }); 377 | interfake.createRoute({ 378 | request: { 379 | url: '/wantsQueryParameter', 380 | query: { 381 | query: '5678', 382 | anotherQuery: '4321' 383 | }, 384 | method: 'get' 385 | }, 386 | response: { 387 | code: 200, 388 | body: { 389 | loan: 'shark' 390 | } 391 | } 392 | }); 393 | interfake.listen(3000); 394 | 395 | return Q.all([get('http://localhost:3000/wantsQueryParameter?query=1234'), 396 | get('http://localhost:3000/wantsQueryParameter?anotherQuery=4321&query=5678'), 397 | get('http://localhost:3000/wantsQueryParameter') 398 | ]).then(function(results) { 399 | assert.equal(results[0][0].statusCode, 200); 400 | assert.equal(results[0][1].high, 'hoe'); 401 | assert.equal(results[1][0].statusCode, 200); 402 | assert.equal(results[1][1].loan, 'shark'); 403 | assert.equal(results[2][0].statusCode, 404); 404 | }); 405 | }); 406 | 407 | it('should create one GET endpoint with a querystring in the url with different responses', function() { 408 | interfake.createRoute({ 409 | request: { 410 | url: '/wantsQueryParameter?query=1234', 411 | method: 'get' 412 | }, 413 | response: { 414 | code: 200, 415 | body: { 416 | high: 'hoe' 417 | } 418 | } 419 | }); 420 | interfake.createRoute({ 421 | request: { 422 | url: '/wantsQueryParameter?anotherQuery=5678', 423 | method: 'get' 424 | }, 425 | response: { 426 | code: 200, 427 | body: { 428 | loan: 'shark' 429 | } 430 | } 431 | }); 432 | interfake.listen(3000); 433 | 434 | return Q.all([get('http://localhost:3000/wantsQueryParameter?query=1234'), 435 | get('http://localhost:3000/wantsQueryParameter?anotherQuery=5678'), 436 | get('http://localhost:3000/wantsQueryParameter') 437 | ]).then(function(results) { 438 | assert.equal(results[0][0].statusCode, 200); 439 | assert.equal(results[0][1].high, 'hoe'); 440 | assert.equal(results[1][0].statusCode, 200); 441 | assert.equal(results[1][1].loan, 'shark'); 442 | assert.equal(results[2][0].statusCode, 404); 443 | }); 444 | }); 445 | 446 | it('should create one GET endpoint accepting query parameters using the url and options', function() { 447 | interfake.createRoute({ 448 | request: { 449 | url: '/wantsQueryParameter?query=1234', 450 | query: { 451 | page: 1 452 | }, 453 | method: 'get' 454 | }, 455 | response: { 456 | code: 200, 457 | body: { 458 | high: 'hoe' 459 | } 460 | } 461 | }); 462 | interfake.createRoute({ 463 | request: { 464 | url: '/wantsQueryParameter?query=1234', 465 | query: { 466 | page: 2 467 | }, 468 | method: 'get' 469 | }, 470 | response: { 471 | code: 200, 472 | body: { 473 | loan: 'shark' 474 | } 475 | } 476 | }); 477 | interfake.listen(3000); 478 | 479 | return Q.all([get('http://localhost:3000/wantsQueryParameter?query=1234&page=1'), 480 | get('http://localhost:3000/wantsQueryParameter?query=1234&page=2'), 481 | get('http://localhost:3000/wantsQueryParameter') 482 | ]).then(function(results) { 483 | assert.equal(results[0][0].statusCode, 200); 484 | assert.equal(results[0][1].high, 'hoe'); 485 | assert.equal(results[1][0].statusCode, 200); 486 | assert.equal(results[1][1].loan, 'shark'); 487 | assert.equal(results[2][0].statusCode, 404); 488 | }); 489 | }); 490 | 491 | it('should create three GET endpoints with different status codes', function(done) { 492 | interfake.createRoute({ 493 | request: { 494 | url: '/test1', 495 | method: 'get' 496 | }, 497 | response: { 498 | code: 200, 499 | body: { 500 | its: 'one' 501 | } 502 | } 503 | }); 504 | interfake.createRoute({ 505 | request: { 506 | url: '/test2', 507 | method: 'get' 508 | }, 509 | response: { 510 | code: 300, 511 | body: { 512 | its: 'two' 513 | } 514 | } 515 | }); 516 | interfake.createRoute({ 517 | request: { 518 | url: '/test3', 519 | method: 'get' 520 | }, 521 | response: { 522 | code: 500, 523 | body: { 524 | its: 'three' 525 | } 526 | } 527 | }); 528 | interfake.listen(3000); 529 | 530 | Q.all([get('http://localhost:3000/test1'), get('http://localhost:3000/test2'), get('http://localhost:3000/test3')]) 531 | .then(function(results) { 532 | assert.equal(results[0][0].statusCode, 200); 533 | assert.equal(results[0][1].its, 'one'); 534 | assert.equal(results[1][0].statusCode, 300); 535 | assert.equal(results[1][1].its, 'two'); 536 | assert.equal(results[2][0].statusCode, 500); 537 | assert.equal(results[2][1].its, 'three'); 538 | done(); 539 | }); 540 | }); 541 | 542 | it('should create a dynamic endpoint', function(done) { 543 | interfake.createRoute({ 544 | request: { 545 | url: '/dynamic', 546 | method: 'post' 547 | }, 548 | response: { 549 | code: 201, 550 | body: {} 551 | }, 552 | afterResponse: { 553 | endpoints: [{ 554 | request: { 555 | url: '/dynamic/1', 556 | method: 'get' 557 | }, 558 | response: { 559 | code: 200, 560 | body: {} 561 | } 562 | }] 563 | } 564 | }); 565 | interfake.listen(3000); 566 | 567 | get('http://localhost:3000/dynamic/1') 568 | .then(function(results) { 569 | assert.equal(results[0].statusCode, 404); 570 | return post('http://localhost:3000/dynamic'); 571 | }) 572 | .then(function(results) { 573 | assert.equal(results[0].statusCode, 201); 574 | return get('http://localhost:3000/dynamic/1'); 575 | }) 576 | .then(function(results) { 577 | assert.equal(results[0].statusCode, 200); 578 | done(); 579 | }) 580 | .done(); 581 | }); 582 | 583 | it('should create a dynamic endpoint within a dynamic endpoint', function(done) { 584 | interfake.createRoute({ 585 | request: { 586 | url: '/dynamic', 587 | method: 'post' 588 | }, 589 | response: { 590 | code: 201, 591 | body: { 592 | all: 'done' 593 | } 594 | }, 595 | afterResponse: { 596 | endpoints: [{ 597 | request: { 598 | url: '/dynamic/1', 599 | method: 'get' 600 | }, 601 | response: { 602 | code: 200, 603 | body: { 604 | yes: 'indeedy' 605 | } 606 | } 607 | }, { 608 | request: { 609 | url: '/dynamic/1', 610 | method: 'put' 611 | }, 612 | response: { 613 | code: 200, 614 | body: {} 615 | }, 616 | afterResponse: { 617 | endpoints: [{ 618 | request: { 619 | url: '/dynamic/1', 620 | method: 'get' 621 | }, 622 | response: { 623 | code: 200, 624 | body: { 625 | yes: 'indiddly' 626 | } 627 | } 628 | }] 629 | } 630 | }] 631 | } 632 | }); 633 | interfake.listen(3000); 634 | 635 | get('http://localhost:3000/dynamic/1') 636 | .then(function(results) { 637 | assert.equal(results[0].statusCode, 404); 638 | return post('http://localhost:3000/dynamic'); 639 | }) 640 | .then(function(results) { 641 | assert.equal(results[0].statusCode, 201); 642 | assert.equal(results[1].all, 'done'); 643 | return get('http://localhost:3000/dynamic/1'); 644 | }) 645 | .then(function(results) { 646 | assert.equal(results[0].statusCode, 200); 647 | assert.equal(results[1].yes, 'indeedy'); 648 | return put('http://localhost:3000/dynamic/1'); 649 | }) 650 | .then(function(results) { 651 | assert.equal(results[0].statusCode, 200); 652 | return get('http://localhost:3000/dynamic/1'); 653 | }) 654 | .then(function(results) { 655 | assert.equal(results[0].statusCode, 200); 656 | assert.equal(results[1].yes, 'indiddly'); 657 | done(); 658 | }); 659 | }); 660 | 661 | it('should return JSONP if requested', function(done) { 662 | interfake.createRoute({ 663 | request: { 664 | url: '/stuff', 665 | method: 'get' 666 | }, 667 | response: { 668 | code: 200, 669 | body: { 670 | stuff: 'hello' 671 | } 672 | } 673 | }); 674 | interfake.listen(3000); 675 | 676 | get('http://localhost:3000/stuff?callback=yo') 677 | .then(function(results) { 678 | assert.equal('hello', 'yo(' + JSON.stringify({ 679 | stuff: 'hello' 680 | }) + ');'); 681 | done(); 682 | }); 683 | 684 | request('http://localhost:3000/stuff?callback=yo', function(error, response, body) { 685 | assert.equal(body, 'yo(' + JSON.stringify({ 686 | stuff: 'hello' 687 | }) + ');'); 688 | done(); 689 | }); 690 | }); 691 | 692 | it('should create a proxy endpoint with a GET method', function(done) { 693 | var proxiedInterfake = new Interfake(); 694 | proxiedInterfake.get('/whatever').status(404).body({ 695 | message: 'This is something you proxied!' 696 | }).responseHeaders({ 697 | 'loving-you':'Isnt the right thing to do' 698 | }); 699 | proxiedInterfake.listen(3050); 700 | interfake.createRoute({ 701 | request: { 702 | url: '/stuff', 703 | method: 'get' 704 | }, 705 | response: { 706 | proxy: 'http://localhost:3050/whatever' 707 | } 708 | }); 709 | interfake.listen(3000); 710 | 711 | request('http://localhost:3000/stuff', function (error, response, body) { 712 | assert.equal(response.statusCode, 404); 713 | assert.equal(body.message, 'This is something you proxied!'); 714 | assert.equal(response.headers['loving-you'], 'Isnt the right thing to do'); 715 | done(); 716 | }); 717 | afterEach(function () { 718 | proxiedInterfake.stop(); 719 | }); 720 | }); 721 | 722 | it('should create a proxy endpoint with a GET method which sends the specified headers', function(done) { 723 | var proxiedInterfake = new Interfake({ 724 | onRequest: function (req) { 725 | assert.equal(req.get('Authorization'), 'Basic username:password'); 726 | done(); 727 | } 728 | }); 729 | proxiedInterfake.get('/whatever').status(404).body({ 730 | message: 'This is something you proxied!' 731 | }).responseHeaders({ 732 | 'loving-you':'Isnt the right thing to do' 733 | }); 734 | proxiedInterfake.listen(3050); 735 | interfake.createRoute({ 736 | request: { 737 | url: '/stuff', 738 | method: 'get' 739 | }, 740 | response: { 741 | proxy: { 742 | url:'http://localhost:3050/whatever', 743 | headers:{ 744 | 'Authorization': 'Basic username:password' 745 | } 746 | } 747 | } 748 | }); 749 | interfake.listen(3000); 750 | 751 | request('http://localhost:3000/stuff', function (error, response, body) { 752 | assert.equal(response.statusCode, 404); 753 | assert.equal(body.message, 'This is something you proxied!'); 754 | assert.equal(response.headers['loving-you'], 'Isnt the right thing to do'); 755 | }); 756 | 757 | afterEach(function () { 758 | proxiedInterfake.stop(); 759 | }); 760 | }); 761 | 762 | it('should create a proxy endpoint with a GET method and retain query parameters', function(done) { 763 | var proxiedInterfake = new Interfake(); 764 | proxiedInterfake.get('/whatever').query({ 765 | q: /\w+/, 766 | isit: /\w+/, 767 | youre: /\w+/ 768 | }).status(404).body({ 769 | message: 'I can see it in your eyes' 770 | }); 771 | proxiedInterfake.listen(3050); 772 | interfake.createRoute({ 773 | request: { 774 | url: '/stuff', 775 | method: 'get', 776 | query: { 777 | q: /\w+/, 778 | isit: /\w+/, 779 | youre: /\w+/ 780 | } 781 | }, 782 | response: { 783 | proxy: 'http://localhost:3050/whatever' 784 | } 785 | }); 786 | interfake.listen(3000); 787 | 788 | request('http://localhost:3000/stuff?q=hello&isit=me&youre=lookingfor', function (error, response, body) { 789 | assert.equal(response.statusCode, 404); 790 | assert.equal(body.message, 'I can see it in your eyes'); 791 | done(); 792 | }); 793 | afterEach(function () { 794 | proxiedInterfake.stop(); 795 | }); 796 | }); 797 | 798 | it('should create a proxy endpoint with a GET method and retain query parameters and their value', function(done) { 799 | var proxiedInterfake = new Interfake(); 800 | proxiedInterfake.get('/whatever').query({ 801 | q: 'hello', 802 | isit: 'me', 803 | youre: 'lookingfor' 804 | }).status(404).body({ 805 | message: 'I can see it in your eyes' 806 | }); 807 | proxiedInterfake.listen(3050); 808 | interfake.createRoute({ 809 | request: { 810 | url: '/stuff', 811 | method: 'get', 812 | query: { 813 | q: /\w+/, 814 | isit: /\w+/, 815 | youre: /\w+/ 816 | } 817 | }, 818 | response: { 819 | proxy: 'http://localhost:3050/whatever' 820 | } 821 | }); 822 | interfake.listen(3000); 823 | 824 | request('http://localhost:3000/stuff?q=hello&isit=me&youre=lookingfor', function (error, response, body) { 825 | assert.equal(response.statusCode, 404); 826 | assert.equal(body.message, 'I can see it in your eyes'); 827 | done(); 828 | }); 829 | afterEach(function () { 830 | proxiedInterfake.stop(); 831 | }); 832 | }); 833 | 834 | it('should create a proxy endpoint with a POST method', function(done) { 835 | var proxiedInterfake = new Interfake(); 836 | proxiedInterfake.post('/whatever').status(404).body({ 837 | message: 'This is something you proxied!' 838 | }).responseHeaders({ 839 | 'loving-you':'Isnt the right thing to do' 840 | }); 841 | proxiedInterfake.listen(3050); 842 | interfake.createRoute({ 843 | request: { 844 | url: '/stuff', 845 | method: 'post' 846 | }, 847 | response: { 848 | proxy: 'http://localhost:3050/whatever' 849 | } 850 | }); 851 | interfake.listen(3000); 852 | 853 | request.post('http://localhost:3000/stuff', function (error, response, body) { 854 | assert.equal(response.statusCode, 404); 855 | assert.equal(body.message, 'This is something you proxied!'); 856 | assert.equal(response.headers['loving-you'], 'Isnt the right thing to do'); 857 | done(); 858 | }); 859 | afterEach(function () { 860 | proxiedInterfake.stop(); 861 | }); 862 | }); 863 | 864 | it('should create a proxy endpoint which supports JSONP', function(done) { 865 | var proxiedInterfake = new Interfake(); 866 | proxiedInterfake.get('/whatever').status(404).body({ 867 | message: 'This is something you proxied!' 868 | }); 869 | proxiedInterfake.listen(3050); 870 | interfake.createRoute({ 871 | request: { 872 | url: '/stuff', 873 | method: 'get' 874 | }, 875 | response: { 876 | proxy: 'http://localhost:3050/whatever' 877 | } 878 | }); 879 | interfake.listen(3000); 880 | 881 | request('http://localhost:3000/stuff?callback=whatever', function (error, response, body) { 882 | assert.equal(response.statusCode, 404); 883 | assert.equal(body, 'whatever(' + JSON.stringify({ 884 | message: 'This is something you proxied!' 885 | }) + ');'); 886 | proxiedInterfake.stop(); 887 | done(); 888 | }); 889 | }); 890 | 891 | it('should create one GET endpoint with support for delaying the response', function(done) { 892 | var enoughTimeHasPassed = false; 893 | var _this = this; 894 | this.slow(500); 895 | interfake.createRoute({ 896 | request: { 897 | url: '/test', 898 | method: 'get' 899 | }, 900 | response: { 901 | code: 200, 902 | delay: 50, 903 | body: { 904 | hi: 'there' 905 | } 906 | } 907 | }); 908 | interfake.listen(3000); 909 | setTimeout(function() { 910 | enoughTimeHasPassed = true; 911 | }, 50); 912 | request('http://localhost:3000/test', function(error, response, body) { 913 | assert.equal(response.statusCode, 200); 914 | assert.equal(body.hi, 'there'); 915 | if (!enoughTimeHasPassed) { 916 | throw new Error('Response wasn\'t delay for long enough'); 917 | } 918 | done(); 919 | }); 920 | }); 921 | 922 | it('should create one GET endpoint with support for delaying the response with a delay range', function(done) { 923 | var enoughTimeHasPassed = false; 924 | var _this = this; 925 | var timeout; 926 | var tookTooLong = false; 927 | this.slow(500); 928 | interfake.createRoute({ 929 | request: { 930 | url: '/test', 931 | method: 'get' 932 | }, 933 | response: { 934 | code: 200, 935 | delay: '20..50', 936 | body: { 937 | hi: 'there' 938 | } 939 | } 940 | }); 941 | interfake.listen(3000); 942 | setTimeout(function() { 943 | enoughTimeHasPassed = true; 944 | }, 20); 945 | timeout = setTimeout(function() { 946 | tookTooLong = true; 947 | }, 60); 948 | request('http://localhost:3000/test', function(error, response, body) { 949 | clearTimeout(timeout); 950 | if (!enoughTimeHasPassed) { 951 | throw new Error('Response wasn\'t delay for long enough'); 952 | } 953 | if (tookTooLong) { 954 | throw new Error('Response was delayed for too long'); 955 | } 956 | done(); 957 | }); 958 | }); 959 | 960 | describe('response with { echo : true }', function () { 961 | it('should return the request body', function (done) { 962 | interfake.createRoute({ 963 | request: { 964 | url: '/stuff', 965 | method: 'post' 966 | }, 967 | response: { 968 | echo : true 969 | } 970 | }); 971 | 972 | interfake.listen(3000); 973 | 974 | request.post({ 975 | url :'http://localhost:3000/stuff', 976 | json : true, 977 | body : { 978 | message : 'Echo!' 979 | }, 980 | }, 981 | function (error, response, body) { 982 | assert.equal(response.statusCode, 200); 983 | assert.equal(body.message, 'Echo!'); 984 | done(); 985 | }); 986 | }); 987 | }); 988 | 989 | describe('#removeRoute', function () { 990 | it('should remove a created route based on a reference to the Route object', function (done) { 991 | var route = interfake.createRoute({ 992 | request: { 993 | url: '/remove/me', 994 | method: 'get' 995 | }, 996 | response: { 997 | code: 200, 998 | body: { 999 | please: 'remove' 1000 | } 1001 | } 1002 | }); 1003 | interfake.createRoute({ 1004 | request: { 1005 | url: '/keep/me', 1006 | method: 'get' 1007 | }, 1008 | response: { 1009 | code: 200, 1010 | body: { 1011 | please: 'keep' 1012 | } 1013 | } 1014 | }); 1015 | interfake.listen(3000); 1016 | 1017 | get('http://localhost:3000/remove/me') 1018 | .then(function(results) { 1019 | assert.equal(results[0].statusCode, 200); 1020 | assert.equal(results[1].please, 'remove'); 1021 | interfake.removeRoute(route); 1022 | return get('http://localhost:3000/remove/me'); 1023 | }) 1024 | .then(function (results) { 1025 | assert.equal(results[0].statusCode, 404); 1026 | assert(results[1].match('Cannot GET /remove/me')) 1027 | return get('http://localhost:3000/keep/me'); 1028 | }) 1029 | .then(function (results) { 1030 | assert.equal(results[0].statusCode, 200); 1031 | assert.equal(results[1].please, 'keep'); 1032 | done(); 1033 | }) 1034 | .done(); 1035 | }); 1036 | 1037 | it('should remove a created route based on the descriptor', function (done) { 1038 | var routeDescriptor = { 1039 | request: { 1040 | url: '/remove/me', 1041 | method: 'get' 1042 | }, 1043 | response: { 1044 | code: 200, 1045 | body: { 1046 | please: 'remove' 1047 | } 1048 | } 1049 | }; 1050 | 1051 | interfake.createRoute(routeDescriptor); 1052 | 1053 | interfake.createRoute({ 1054 | request: { 1055 | url: '/keep/me', 1056 | method: 'get' 1057 | }, 1058 | response: { 1059 | code: 200, 1060 | body: { 1061 | please: 'keep' 1062 | } 1063 | } 1064 | }); 1065 | 1066 | interfake.listen(3000); 1067 | 1068 | get('http://localhost:3000/remove/me') 1069 | .then(function(results) { 1070 | assert.equal(results[0].statusCode, 200); 1071 | assert.equal(results[1].please, 'remove'); 1072 | interfake.removeRoute(routeDescriptor); 1073 | return get('http://localhost:3000/remove/me'); 1074 | }) 1075 | .then(function (results) { 1076 | assert.equal(results[0].statusCode, 404); 1077 | assert(results[1].match('Cannot GET /remove/me')) 1078 | return get('http://localhost:3000/keep/me'); 1079 | }) 1080 | .then(function (results) { 1081 | assert.equal(results[0].statusCode, 200); 1082 | assert.equal(results[1].please, 'keep'); 1083 | done(); 1084 | }) 1085 | .done(); 1086 | }); 1087 | }); 1088 | }); 1089 | 1090 | // Testing the API root stuff 1091 | describe('#Interfake({ path: [String] })', function() { 1092 | it('should set the root path of the API', function(done) { 1093 | interfake = new Interfake({ 1094 | path: '/api' 1095 | }); 1096 | interfake.get('/endpoint').status(200).creates.get('/moar-endpoints'); 1097 | interfake.listen(3000); 1098 | 1099 | Q.all([get('http://localhost:3000/api/endpoint'), get('http://localhost:3000/endpoint')]) 1100 | .then(function(results) { 1101 | assert.equal(results[0][0].statusCode, 200); 1102 | assert.equal(results[1][0].statusCode, 404); 1103 | return get('http://localhost:3000/api/endpoint'); 1104 | }) 1105 | .then(function(results) { 1106 | assert.equal(results[0].statusCode, 200); 1107 | return get('http://localhost:3000/api/moar-endpoints'); 1108 | }) 1109 | .then(function(results) { 1110 | assert.equal(results[0].statusCode, 200); 1111 | done(); 1112 | }); 1113 | }); 1114 | }); 1115 | 1116 | describe('#loadFile()', function() { 1117 | it('should load a JSON file and the routes within it', function(done) { 1118 | interfake = new Interfake(); 1119 | interfake.loadFile('./tests/loadFileTest-1.json'); 1120 | interfake.listen(3000); 1121 | 1122 | get('http://localhost:3000/whattimeisit') 1123 | .then(function(results) { 1124 | assert.equal(results[0].statusCode, 200); 1125 | assert.equal(results[1].theTime, 'Adventure Time!'); 1126 | done(); 1127 | }) 1128 | .done(); 1129 | }); 1130 | 1131 | it('should load a JSON file and the with regex routes in it', function(done) { 1132 | interfake = new Interfake(); 1133 | interfake.loadFile('./tests/loadFileTest-3.json'); 1134 | interfake.listen(3000); 1135 | 1136 | get('http://localhost:3000/whostheboss') 1137 | .then(function(results) { 1138 | assert.equal(results[0].statusCode, 200); 1139 | assert.equal(results[1].theBoss, 'Angela'); 1140 | return get('http://localhost:3000/whostheleader'); 1141 | }) 1142 | .then(function(results) { 1143 | assert.equal(results[0].statusCode, 200); 1144 | assert.equal(results[1].theBoss, 'Angela'); 1145 | done(); 1146 | }) 1147 | .done(); 1148 | }); 1149 | 1150 | describe('#unload', function () { 1151 | it('should unload a single file', function (done) { 1152 | interfake = new Interfake(); 1153 | var firstFile = interfake.loadFile('./tests/loadFileTest-1.json'); 1154 | interfake.loadFile('./tests/loadFileTest-2.json'); 1155 | interfake.listen(3000); 1156 | 1157 | get('http://localhost:3000/whattimeisit') 1158 | .then(function(results) { 1159 | assert.equal(results[0].statusCode, 200); 1160 | assert.equal(results[1].theTime, 'Adventure Time!'); 1161 | firstFile.unload(); 1162 | return get('http://localhost:3000/whattimeisit'); 1163 | }) 1164 | .then(function (results) { 1165 | assert.equal(results[0].statusCode, 404); 1166 | assert(results[1].match('Cannot GET /whattimeisit')) 1167 | interfake.loadFile('./tests/loadFileTest-2.json'); 1168 | return get('http://localhost:3000/whostheboss'); 1169 | }) 1170 | .then(function (results) { 1171 | assert.equal(results[0].statusCode, 200); 1172 | assert.equal(results[1].theBoss, 'Angela'); 1173 | done(); 1174 | }) 1175 | .done(); 1176 | }); 1177 | }); 1178 | 1179 | describe('#unload', function () { 1180 | it('should be able to unload a single file (note, this cannot actually test functionality, just make sure nothing breaks)', function (done) { 1181 | interfake = new Interfake(); 1182 | var firstFile = interfake.loadFile('./tests/loadFileTest-1.json'); 1183 | interfake.loadFile('./tests/loadFileTest-2.json'); 1184 | interfake.listen(3000); 1185 | 1186 | get('http://localhost:3000/whattimeisit') 1187 | .then(function(results) { 1188 | assert.equal(results[0].statusCode, 200); 1189 | assert.equal(results[1].theTime, 'Adventure Time!'); 1190 | firstFile.reload(); 1191 | return get('http://localhost:3000/whattimeisit'); 1192 | }) 1193 | .then(function (results) { 1194 | assert.equal(results[0].statusCode, 200); 1195 | assert.equal(results[1].theTime, 'Adventure Time!'); 1196 | interfake.loadFile('./tests/loadFileTest-2.json'); 1197 | return get('http://localhost:3000/whostheboss'); 1198 | }) 1199 | .then(function (results) { 1200 | assert.equal(results[0].statusCode, 200); 1201 | assert.equal(results[1].theBoss, 'Angela'); 1202 | done(); 1203 | }) 1204 | .done(); 1205 | }); 1206 | }); 1207 | 1208 | describe('#clearAllRoutes()', function() { 1209 | it('should load a file and then clear the routes out', function(done) { 1210 | interfake = new Interfake(); 1211 | interfake.loadFile('./tests/loadFileTest-1.json'); 1212 | interfake.listen(3000); 1213 | 1214 | get('http://localhost:3000/whattimeisit') 1215 | .then(function(results) { 1216 | assert.equal(results[0].statusCode, 200); 1217 | assert.equal(results[1].theTime, 'Adventure Time!'); 1218 | interfake.clearAllRoutes(); 1219 | return get('http://localhost:3000/whattimeisit'); 1220 | }) 1221 | .then(function (results) { 1222 | assert.equal(results[0].statusCode, 404); 1223 | assert(results[1].match('Cannot GET /whattimeisit')) 1224 | done(); 1225 | }) 1226 | .done(); 1227 | }); 1228 | 1229 | it('should load a file and clear the routes out then load another one', function(done) { 1230 | interfake = new Interfake(); 1231 | interfake.loadFile('./tests/loadFileTest-1.json'); 1232 | interfake.listen(3000); 1233 | 1234 | get('http://localhost:3000/whattimeisit') 1235 | .then(function(results) { 1236 | assert.equal(results[0].statusCode, 200); 1237 | assert.equal(results[1].theTime, 'Adventure Time!'); 1238 | interfake.clearAllRoutes(); 1239 | return get('http://localhost:3000/whattimeisit'); 1240 | }) 1241 | .then(function (results) { 1242 | assert.equal(results[0].statusCode, 404); 1243 | assert(results[1].match('Cannot GET /whattimeisit')) 1244 | interfake.loadFile('./tests/loadFileTest-2.json'); 1245 | return get('http://localhost:3000/whostheboss'); 1246 | }) 1247 | .then(function (results) { 1248 | assert.equal(results[0].statusCode, 200); 1249 | assert.equal(results[1].theBoss, 'Angela'); 1250 | done(); 1251 | }) 1252 | .done(); 1253 | }); 1254 | }); 1255 | }); 1256 | 1257 | describe('#serveStatic()', function () { 1258 | it('should serve up a file in the given folder', function (done) { 1259 | var filePath = path.join(__dirname, './'); 1260 | interfake.serveStatic('/static', filePath); 1261 | interfake.listen(3000); 1262 | 1263 | request({ url : 'http://localhost:3000/static/static-test.txt', json : false }, function(error, response, body) { 1264 | assert.equal(response.statusCode, 200); 1265 | assert.equal(body, 'Testing'); 1266 | done(); 1267 | }); 1268 | }); 1269 | }); 1270 | }); 1271 | -------------------------------------------------------------------------------- /tests/loadFileTest-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/whattimeisit", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 200, 9 | "body": { 10 | "theTime": "Adventure Time!" 11 | }, 12 | "headers": { 13 | "X-Powered-By": "Interfake" 14 | } 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /tests/loadFileTest-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "/whostheboss", 5 | "method": "get" 6 | }, 7 | "response": { 8 | "code": 200, 9 | "body": { 10 | "theBoss": "Angela" 11 | }, 12 | "headers": { 13 | "X-Powered-By": "Interfake" 14 | } 15 | } 16 | } 17 | ] -------------------------------------------------------------------------------- /tests/loadFileTest-3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": { 5 | "pattern" : "/whosthe(boss|leader)", 6 | "regexp" : true 7 | }, 8 | "method": "get" 9 | }, 10 | "response": { 11 | "code": 200, 12 | "body": { 13 | "theBoss": "Angela" 14 | }, 15 | "headers": { 16 | "X-Powered-By": "Interfake" 17 | } 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /tests/static-test.txt: -------------------------------------------------------------------------------- 1 | Testing --------------------------------------------------------------------------------