├── .all-contributorsrc ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── client ├── .dockerignore ├── .gitignore ├── .vscode │ └── settings.json ├── Dockerfile ├── README.md ├── config-overrides.js ├── jest.config.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── components │ ├── ConfirmationDialog │ │ ├── index.js │ │ └── spec.js │ ├── HeaderInput │ │ ├── index.js │ │ └── spec.js │ ├── Logo │ │ └── index.js │ ├── Route │ │ ├── RouteItem.js │ │ ├── index.js │ │ └── spec.js │ ├── RouteListGroup │ │ ├── index.js │ │ └── spec.js │ ├── RouteListStack │ │ ├── index.js │ │ └── spec.js │ ├── RouteModal │ │ ├── index.js │ │ └── spec.js │ └── SettingsModal │ │ ├── index.js │ │ └── spec.js │ ├── config │ └── routes.json │ ├── hooks │ └── useScrollReveal │ │ └── index.js │ ├── index.js │ ├── logo.svg │ ├── scss │ ├── App.scss │ └── index.scss │ ├── serviceWorker.js │ ├── spec.js │ └── utils │ ├── consts │ └── index.js │ └── routes-api │ ├── index.js │ └── spec.js ├── codecov.yml ├── configuration └── routes.json ├── docker-compose.yml ├── images ├── demo.gif └── screenshot.png ├── install-and-test.sh ├── mockit-routes ├── .dockerignore ├── Dockerfile ├── configuration │ └── routes.json ├── jest.config.json ├── package-lock.json ├── package.json └── src │ ├── index.js │ ├── middlewares │ ├── basic-auth │ │ ├── index.js │ │ └── spec.js │ ├── chaos-monkey │ │ ├── index.js │ │ ├── spec.js │ │ ├── util.js │ │ └── util.spec.js │ └── delay │ │ └── index.js │ └── spec.js ├── package-lock.json └── server ├── .dockerignore ├── .gitignore ├── .secrets-baseline ├── .travis.yml ├── Dockerfile ├── codecov.yml ├── configuration └── routes.json ├── jest.config.json ├── license ├── package-lock.json ├── package.json └── src ├── index.js ├── spec.js └── utils └── config-helper.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "mockit", 3 | "projectOwner": "boyney123", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "boyney123", 15 | "name": "David Boyne", 16 | "avatar_url": "https://avatars1.githubusercontent.com/u/3268013?v=4", 17 | "profile": "https://medium.com/@boyney123", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "design", 22 | "ideas", 23 | "review", 24 | "tool" 25 | ] 26 | }, 27 | { 28 | "login": "lirantal", 29 | "name": "Liran Tal", 30 | "avatar_url": "https://avatars1.githubusercontent.com/u/316371?v=4", 31 | "profile": "https://medium.com/@liran.tal", 32 | "contributions": [ 33 | "security" 34 | ] 35 | }, 36 | { 37 | "login": "Hongarc", 38 | "name": "Hongarc", 39 | "avatar_url": "https://avatars1.githubusercontent.com/u/19208123?v=4", 40 | "profile": "https://fb.com/RemoveU", 41 | "contributions": [ 42 | "doc" 43 | ] 44 | }, 45 | { 46 | "login": "Calinou", 47 | "name": "Hugo Locurcio", 48 | "avatar_url": "https://avatars3.githubusercontent.com/u/180032?v=4", 49 | "profile": "https://hugo.pro", 50 | "contributions": [ 51 | "code" 52 | ] 53 | }, 54 | { 55 | "login": "FattusMannus", 56 | "name": "Andrew Hall", 57 | "avatar_url": "https://avatars1.githubusercontent.com/u/724328?v=4", 58 | "profile": "http://www.andrewroberthall.co.uk", 59 | "contributions": [ 60 | "doc" 61 | ] 62 | }, 63 | { 64 | "login": "peterjgrainger", 65 | "name": "Peter Grainger", 66 | "avatar_url": "https://avatars1.githubusercontent.com/u/1332395?v=4", 67 | "profile": "http://grainger.xyz", 68 | "contributions": [ 69 | "doc" 70 | ] 71 | }, 72 | { 73 | "login": "wohlben", 74 | "name": "Ben", 75 | "avatar_url": "https://avatars2.githubusercontent.com/u/9362553?v=4", 76 | "profile": "https://github.com/wohlben", 77 | "contributions": [ 78 | "code" 79 | ] 80 | }, 81 | { 82 | "login": "MCRayRay", 83 | "name": "MCRayRay", 84 | "avatar_url": "https://avatars1.githubusercontent.com/u/2843957?v=4", 85 | "profile": "https://github.com/MCRayRay", 86 | "contributions": [ 87 | "code" 88 | ] 89 | }, 90 | { 91 | "login": "fbricon", 92 | "name": "Fred Bricon", 93 | "avatar_url": "https://avatars3.githubusercontent.com/u/148698?v=4", 94 | "profile": "https://github.com/fbricon", 95 | "contributions": [ 96 | "code" 97 | ] 98 | }, 99 | { 100 | "login": "fliu2476", 101 | "name": "fliu2476", 102 | "avatar_url": "https://avatars1.githubusercontent.com/u/19582252?v=4", 103 | "profile": "https://blog.missj.club", 104 | "contributions": [ 105 | "bug" 106 | ] 107 | }, 108 | { 109 | "login": "de314", 110 | "name": "David Esposito", 111 | "avatar_url": "https://avatars1.githubusercontent.com/u/816693?v=4", 112 | "profile": "https://github.com/de314", 113 | "contributions": [ 114 | "doc" 115 | ] 116 | }, 117 | { 118 | "login": "mlaopane", 119 | "name": "Mickaël", 120 | "avatar_url": "https://avatars3.githubusercontent.com/u/23735276?v=4", 121 | "profile": "https://github.com/mlaopane", 122 | "contributions": [ 123 | "doc" 124 | ] 125 | }, 126 | { 127 | "login": "zecarrera", 128 | "name": "José Carréra Alvares Neto", 129 | "avatar_url": "https://avatars1.githubusercontent.com/u/4092515?v=4", 130 | "profile": "https://github.com/zecarrera", 131 | "contributions": [ 132 | "code" 133 | ] 134 | } 135 | ], 136 | "contributorsPerLine": 7 137 | } 138 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What you did: 2 | 3 | ## What happened: 4 | 5 | 6 | 7 | ## Problem description: 8 | 9 | 10 | 11 | ## Suggested solution: 12 | 13 | 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | **What**: 11 | 12 | 13 | 14 | **Why**: 15 | 16 | 17 | 18 | **How**: 19 | 20 | 21 | 22 | **Checklist**: 23 | 24 | 25 | 26 | 27 | 28 | - [ ] Tests 29 | - [ ] Ready to be merged 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Node ### 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # nyc test coverage 17 | .nyc_output 18 | 19 | # Compiled binary addons (https://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # dotenv environment variables file 26 | .env 27 | .env.test 28 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | install: 6 | - npm install -g codecov 7 | script: 8 | - bash install-and-test.sh 9 | - codecov 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.1 2 | 3 | - Added more status codes ([zecarrera](https://github.com/zecarrera) in [#65](https://github.com/boyney123/mockit/pull/65)) 4 | 5 | - Clean up markdown #64 ([lirantal](https://github.com/lirantal) in [#64](https://github.com/boyney123/mockit/pull/64)) 6 | 7 | ## 1.2.0 8 | 9 | - Prevent secrets from leaking to source control ([lirantal](https://github.com/lirantal) in [#59](https://github.com/boyney123/mockit/pull/59)) 10 | 11 | ## 1.1.1 12 | 13 | - Fix for header format in mockit-routes ([boyney123](https://github.com/boyney123) in [#57](https://github.com/boyney123/mockit/pull/57)) 14 | 15 | ## 1.1.0 16 | 17 | - Add the ability to group routes ([JDansercoer](https://github.com/JDansercoer) in [#42](https://github.com/boyney123/mockit/pull/42)) 18 | 19 | - Removed useless header x-powered-by ([webdevium](https://github.com/webdevium) in [#45](https://github.com/boyney123/mockit/pull/45)) 20 | 21 | - Improved methods and status codes ([webdevium](https://github.com/webdevium) in [#44](https://github.com/boyney123/mockit/pull/44)) 22 | 23 | - Add the ability to add custom headers ([boyney123](https://github.com/boyney123) in [#46](https://github.com/boyney123/mockit/pull/46)) 24 | 25 | ## 1.0.0 26 | 27 | Initial Release 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 4 | 5 | ## Issues 6 | 7 | We'd love you to open issues, if they're relevant to this repository: feature requests, bug reports, questions about our processes, declarations of gratefulness, etc. are all welcome. 8 | 9 | In particular, if you have a large PR you want to send our way, it may make sense to open an issue to discuss it with the maintainers first. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) David Boyne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

MockIt: A tool to quickly create mocked APIs.

4 |

Stop wasting time mocking APIs. MockIt gives you an interface to configure and create REAL mocked endpoints for your applications. Whilst you wait for APIS to be built use MockIt to talk to a real service. 5 | 6 |


7 | 8 | [![Travis](https://img.shields.io/travis/boyney123/mockit/master.svg)](https://travis-ci.org/boyney123/mockit) 9 | [![CodeCov](https://codecov.io/gh/boyney123/mockit/branch/master/graph/badge.svg?token=AoXW3EFgMP)](https://codecov.io/gh/boyney123/mockit) 10 | [![Netlify Status](https://api.netlify.com/api/v1/badges/6d5acca1-0959-4d92-a739-08f725fdc464/deploy-status)](https://app.netlify.com/sites/mockit/deploys) 11 | [![MIT License][license-badge]][license] 12 | [![PRs Welcome][prs-badge]][prs] 13 | [![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors-) 14 | 15 | [![Watch on GitHub][github-watch-badge]][github-watch] 16 | [![Star on GitHub][github-star-badge]][github-star] 17 | [![Tweet][twitter-badge]][twitter] 18 | 19 | [Donate ☕](https://www.paypal.me/boyney123/5) 20 | 21 |
22 | 23 | header 24 | 25 |

Features: Live Reload, Chaos Engineering, Authentication, CORS and more...

26 | 27 | [Read the Docs](https://mockit.netlify.com/) | [Edit the Docs](https://github.com/boyney123/mockit-docs) 28 | 29 |
30 | 31 |
32 | 33 | # The problem 34 | 35 | When building applications you often need to interact with services. When the services are not ready to be consumed you have a few options: 36 | 37 | 1. Mock out the response with a JSON file 38 | 2. Create a mock service yourself 39 | 3. Use MockIt. 40 | 41 | # This solution 42 | 43 | This tool was designed to help developers quickly create endpoints for their applications. No need to create a server, just use docker and run this project locally. You can create, edit and manage routes to your API. Every change to the API will be reflected on the server and updated straight away. 44 | 45 | This tool comes with a few features out the box: 46 | 47 | - CORS 48 | - Basic Authentication 49 | - Chaos Monkey (Unleash a monkey to take down your endpoints) 50 | 51 | More information about how it works, its features can be found on the docs. 52 | 53 | [Read the docs and get started](https://mockit.netlify.com/) 54 | 55 | # Getting Started 56 | 57 | _Make sure you have docker running_ 58 | 59 | ```sh 60 | git clone https://github.com/boyney123/mockit.git 61 | ``` 62 | 63 | ```sh 64 | cd mockit && docker-compose up --build -d 65 | ``` 66 | 67 | Once everything is up and running go to [http://localhost:5000](http://localhost:5000) to see MockIt. 68 | 69 | For instructions on how to use MockIt please see the [documentation](https://mockit.netlify.com/docs/getting-started/routes). 70 | 71 | ## Permissions 72 | 73 | _If you get error: `Couldn't connect to Docker daemon at http+docker://localhost - is it running?` you might need run_ with _sudo_ 74 | 75 | ``` 76 | sudo docker-compose up --build -d 77 | ``` 78 | 79 | ## Local install and running tests 80 | 81 | If you want to install and run the tests for all apps then you can run this script: 82 | 83 | ``` 84 | sh install-and-test.sh 85 | ``` 86 | 87 | _If you have any problems with permissions you might need to chmod the file_ 88 | 89 | ``` 90 | chmod +x install-and-test.sh && ./install-and-test.sh 91 | ``` 92 | 93 | # Viewing the dashboard, server and API 94 | 95 | Once Docker is running you have three applications running on the machine. 96 | 97 | 1. The client: [http://localhost:5000](http://localhost:5000) 98 | 2. The client-server: [http://localhost:4000](http://localhost:4000) 99 | 3. The MockIt API (this is the server that runs your API): [http://localhost:3000](http://localhost:3000) 100 | 101 | If you want to view the dashboard to get started go to [http://localhost:5000](http://localhost:5000). 102 | 103 | If you want to interact with your new API go to [http://localhost:3000](http://localhost:3000). 104 | 105 | For example, if you have a `/user` route setup, go to [http://localhost:3000/user](http://localhost:3000/user) to view the data. 106 | 107 | # Tools 108 | 109 | - [nodemon - Listening for changes](https://github.com/remy/nodemon) 110 | - [Express](https://expressjs.com/) 111 | - [React](https://reactjs.org/) 112 | - [Docker](https://www.docker.com/) 113 | 114 | ## Documentation 115 | 116 | - [Docusaurus](https://docusaurus.io/) 117 | 118 | ## Testing 119 | 120 | - [jest](https://jestjs.io/) 121 | - [react-testing-library](https://github.com/kentcdodds/react-testing-library) 122 | - [supertest](https://github.com/visionmedia/supertest) 123 | 124 | # Contributing 125 | 126 | If you have any questions, features or issues please raise any issue or pull requests you like. 127 | 128 | [spectrum-badge]: https://withspectrum.github.io/badge/badge.svg 129 | [spectrum]: https://spectrum.chat/explore-tech 130 | [license-badge]: https://img.shields.io/github/license/boyney123/mockit.svg?color=yellow 131 | [license]: https://github.com/boyney123/react.explore-tech.org/blob/master/LICENSE 132 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 133 | [prs]: http://makeapullrequest.com 134 | [github-watch-badge]: https://img.shields.io/github/watchers/boyney123/mockit.svg?style=social 135 | [github-watch]: https://github.com/boyney123/mockit/watchers 136 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20mockit%20by%20%40boyney123%20https%3A%2F%2Fgithub.com%2Fboyney123%2Fmockit%20%F0%9F%91%8D 137 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/boyney123/mockit.svg?style=social 138 | [github-star-badge]: https://img.shields.io/github/stars/boyney123/mockit.svg?style=social 139 | [github-star]: https://github.com/boyney123/mockit/stargazers 140 | 141 | # Donating 142 | 143 | If you find this tool useful, feel free to buy me a ☕ 👍 144 | 145 | [Buy a drink](https://www.paypal.me/boyney123/5) 146 | 147 | # License 148 | 149 | MIT. 150 | 151 | ## Contributors 152 | 153 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
David Boyne
David Boyne

💻 📖 🎨 🤔 👀 🔧
Liran Tal
Liran Tal

🛡️
Hongarc
Hongarc

📖
Hugo Locurcio
Hugo Locurcio

💻
Andrew Hall
Andrew Hall

📖
Peter Grainger
Peter Grainger

📖
Ben
Ben

💻
MCRayRay
MCRayRay

💻
Fred Bricon
Fred Bricon

💻
fliu2476
fliu2476

🐛
David Esposito
David Esposito

📖
Mickaël
Mickaël

📖
José Carréra Alvares Neto
José Carréra Alvares Neto

💻
177 | 178 | 179 | 180 | 181 | 182 | 183 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 184 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported versions 4 | 5 | The following table describes the versions of this project that are currently supported with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x | :white_check_mark: | 10 | 11 | ## Responsible disclosure security policy 12 | 13 | A responsible disclosure policy helps protect users of the project from publicly disclosed security vulnerabilities 14 | without a fix by employing a process where vulnerabilities are first triaged in a private manner, and only publicly 15 | disclosed after a reasonable time period that allows patching the vulnerability and provides an upgrade path for users. 16 | 17 | When contacting us directly via email, we will do our best efforts to respond in a reasonable time to resolve the issue. 18 | When contacting a security program their disclosure policy will provide details on timeframe, processes and paid bounties. 19 | 20 | We kindly ask you to refrain from malicious acts that put our users, the project, or any of the project’s team members at 21 | risk. 22 | 23 | ## Reporting a security issue 24 | 25 | At Mockit, we consider the security of our systems a top priority. But no matter how much effort we put into system 26 | security, there can still be vulnerabilities present. 27 | 28 | If you discover a security vulnerability, please use one of the following means of communications to report it to us: 29 | 30 | - Report the security issue to the Node.js Security WG through the 31 | [HackerOne program](https://hackerone.com/nodejs-ecosystem) for ecosystem modules on npm, or to 32 | [Snyk Security Team](https://snyk.io/vulnerability-disclosure). They will help triage the security issue and work with 33 | all involved parties to remediate and release a fix. 34 | 35 | Note that time-frame and processes are subject to each program’s own policy. 36 | 37 | - Report the security issue to the project maintainers directly at davidboyne123@hotmail.co.uk. If the report contains 38 | highly sensitive information, you should consider reporting to one of the above mentioned disclosure programs that allow 39 | sending the report over a secure medium. 40 | 41 | Your efforts to responsibly disclose your findings are sincerely appreciated and will be taken into account to acknowledge 42 | your contributions. 43 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11.4.0-alpine 2 | RUN mkdir -p /usr/src/mockit-client 3 | WORKDIR /usr/src/mockit-client 4 | 5 | COPY package.json . 6 | COPY package-lock.json . 7 | 8 | RUN npm ci 9 | 10 | COPY . ./ 11 | 12 | 13 | CMD ["npm", "start"] 14 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analysing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jest: function (config) { 3 | config.testMatch.push('/src/**/{spec,test}.{js,jsx,ts,tsx}'); 4 | return config; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /client/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bail": true, 3 | "verbose": true, 4 | "coveragePathIgnorePatterns": ["/node_modules/", "/serviceWorker.js"] 5 | } 6 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockit-ui", 3 | "version": "1.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bulma": "^0.7.4", 7 | "bulma-extensions": "^6.2.4", 8 | "faker": "^4.1.0", 9 | "lodash": "^4.17.11", 10 | "node-sass": "^4.11.0", 11 | "react": "v16.8.3", 12 | "react-dom": "v16.8.3", 13 | "react-json-editor-ajrm": "^2.5.9", 14 | "react-scripts": "3.0.0", 15 | "scrollreveal": "^4.0.5", 16 | "styled-components": "^4.2.0", 17 | "uuid": "^3.3.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-app-rewired test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "jest": { 26 | "collectCoverageFrom": [ 27 | "src/**/*.{js,jsx}", 28 | "!src/serviceWorker.js", 29 | "!src/index.js" 30 | ] 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ], 41 | "devDependencies": { 42 | "dom-testing-library": "^3.19.1", 43 | "jest-dom": "^3.1.3", 44 | "nock": "^10.0.6", 45 | "react-app-rewired": "^2.1.3", 46 | "react-testing-library": "^6.1.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyney123/mockit/e593e5d328ec8371c275e07d759cc81fe4e19ebb/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 23 | 27 | 36 | MockIt 37 | 38 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "MockIt", 3 | "name": "A tool to quickly create mocked APIs", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import useScrollReval from './hooks/useScrollReveal'; 3 | import RouteListStack from './components/RouteListStack'; 4 | import RouteListGroup from './components/RouteListGroup'; 5 | import Logo from './components/Logo'; 6 | import { version } from '../package.json'; 7 | 8 | import { buildRoute, deleteRoute } from './utils/routes-api'; 9 | 10 | import RouteModal from './components/RouteModal'; 11 | import SettingsModal from './components/SettingsModal'; 12 | import ConfirmationDialog from './components/ConfirmationDialog'; 13 | 14 | import { settings, routes as configRoutes } from './config/routes.json'; 15 | 16 | import './scss/index.scss'; 17 | 18 | export default function ({ settings: propSettings, customRoutes }) { 19 | useScrollReval([{ selector: '.hero .title, .card, .subtitle ' }]); 20 | 21 | const [selectedRoute, setSelectedRoute] = useState(); 22 | const [routeToBeRemoved, setRouteToBeRemoved] = useState(); 23 | const [settingsModalVisible, showSettingsModal] = useState(false); 24 | 25 | const routes = customRoutes || configRoutes; 26 | 27 | const { features: { chaosMonkey = false, groupedRoutes = false } = {} } = 28 | propSettings || settings; 29 | 30 | return ( 31 | <> 32 |
33 | 54 | 55 |
56 |
57 | 58 |

59 | A tool to quickly mock out end points, setup delays and more... 60 |

61 |
62 |
63 |
64 | 65 | {selectedRoute && ( 66 | setSelectedRoute(null)} 69 | /> 70 | )} 71 | 72 | {routeToBeRemoved && ( 73 | deleteRoute(routeToBeRemoved)} 76 | onClose={() => setRouteToBeRemoved(null)} 77 | > 78 |

79 | Are you sure you want to delete the route :{' '} 80 | {routeToBeRemoved.route} ? 81 |

82 |
83 | )} 84 | 85 | {settingsModalVisible && ( 86 | showSettingsModal(false)} /> 87 | )} 88 | 89 |
90 | {chaosMonkey && ( 91 | <> 92 |

96 | 97 | 🐒 98 | 99 |

100 |

101 | The chaos monkey is enabled and causing havoc on all APIs... 102 |

103 | 104 | )} 105 | {routes.length === 0 && ( 106 |

107 | No routes found. Click "Add Route" to get started. 108 |

109 | )} 110 | 111 | {groupedRoutes ? ( 112 | setSelectedRoute(route)} 115 | onRouteDelete={(route) => setRouteToBeRemoved(route)} 116 | /> 117 | ) : ( 118 | setSelectedRoute(route)} 121 | onRouteDelete={(route) => setRouteToBeRemoved(route)} 122 | /> 123 | )} 124 |
125 | 147 | 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /client/src/components/ConfirmationDialog/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ConfirmationDialog = function ({ 4 | onClose = () => {}, 5 | onConfirm = () => {}, 6 | heading, 7 | children 8 | } = {}) { 9 | return ( 10 | <> 11 |
12 |
13 |
14 |
18 |

{heading}

19 |
21 |
{children}
22 |
23 | 30 | 33 |
34 |
35 |
36 | 37 | ); 38 | }; 39 | 40 | export default ConfirmationDialog; 41 | -------------------------------------------------------------------------------- /client/src/components/ConfirmationDialog/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, fireEvent, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import ConfirmationDialog from './'; 6 | 7 | afterEach(cleanup); 8 | 9 | describe('Confirmation Dialog', () => { 10 | describe('renders', () => { 11 | it('a header with the given header value', () => { 12 | const { getByText } = render( 13 | 14 | ); 15 | expect(getByText('Test Header')).toBeVisible(); 16 | }); 17 | 18 | it('any given children inside the modal dialog', () => { 19 | const { getByTestId } = render( 20 | 21 |

test

22 |
23 | ); 24 | expect(getByTestId('my-child-example')).toBeVisible(); 25 | }); 26 | 27 | it('a delete and cancel button', () => { 28 | const { getByLabelText } = render( 29 | 30 | ); 31 | expect(getByLabelText('Delete')).toBeVisible(); 32 | expect(getByLabelText('Cancel')).toBeVisible(); 33 | }); 34 | }); 35 | 36 | describe('props', () => { 37 | describe('onClose', () => { 38 | it('is called when closing the modal dialog using the close button', () => { 39 | const spy = jest.fn(); 40 | const { getByLabelText } = render( 41 | 42 | ); 43 | fireEvent.click(getByLabelText('close')); 44 | expect(spy).toHaveBeenCalled(); 45 | }); 46 | it('is called when clicking the cancel button', () => { 47 | const spy = jest.fn(); 48 | const { getByLabelText } = render( 49 | 50 | ); 51 | fireEvent.click(getByLabelText('Cancel')); 52 | expect(spy).toHaveBeenCalled(); 53 | }); 54 | }); 55 | describe('onConfirm', () => { 56 | it('is called when clicking the delete button', () => { 57 | const spy = jest.fn(); 58 | const { getByLabelText } = render( 59 | 60 | ); 61 | fireEvent.click(getByLabelText('Delete')); 62 | expect(spy).toHaveBeenCalled(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /client/src/components/HeaderInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import uuid from 'uuid/v4'; 3 | 4 | export default function ({ 5 | index, 6 | data = {}, 7 | onBlur = () => {}, 8 | onRemove = () => {} 9 | } = {}) { 10 | const { id = uuid(), header: initialHeader, value: initialValue } = data; 11 | 12 | const [header, setHeader] = useState(initialHeader); 13 | const [value, setValue] = useState(initialValue); 14 | 15 | const update = (field, inputValue) => { 16 | field === 'header' ? setHeader(inputValue) : setValue(inputValue); 17 | }; 18 | 19 | useEffect(() => { 20 | if (header && value) onBlur({ id, header, value }); 21 | }, [header, value]); 22 | 23 | return ( 24 |
25 |
26 | update('header', e.target.value)} 32 | /> 33 |
34 |
35 | update('value', e.target.value)} 41 | /> 42 |
43 |
onRemove(id)} 46 | > 47 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /client/src/components/HeaderInput/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, fireEvent, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import HeaderInput from './'; 6 | 7 | afterEach(cleanup); 8 | 9 | describe('DoubleInput', () => { 10 | describe('renders', () => { 11 | it('two inputs one for the header and one for the value', () => { 12 | const { getByPlaceholderText } = render(); 13 | expect(getByPlaceholderText('header')).toBeVisible(); 14 | expect(getByPlaceholderText('value')).toBeVisible(); 15 | }); 16 | 17 | it('two inputs are rendered with the given "header" and "value" data when given to the component', () => { 18 | const { getByPlaceholderText } = render( 19 | 22 | ); 23 | expect(getByPlaceholderText('header').value).toEqual('Content-Type'); 24 | expect(getByPlaceholderText('value').value).toEqual('application/json'); 25 | }); 26 | }); 27 | 28 | describe('props: events', () => { 29 | it('onBlur is called when both "header" and "value" have been entered', () => { 30 | const spy = jest.fn(); 31 | const { getByPlaceholderText } = render(); 32 | fireEvent.change(getByPlaceholderText('header'), { 33 | target: { value: 'Content-Type' } 34 | }); 35 | fireEvent.change(getByPlaceholderText('value'), { 36 | target: { value: 'application/json' } 37 | }); 38 | expect(spy).toHaveBeenCalled(); 39 | }); 40 | 41 | it('onBlur is not called when "header" value is set but "value" is missing', () => { 42 | const spy = jest.fn(); 43 | const { getByPlaceholderText } = render(); 44 | fireEvent.change(getByPlaceholderText('header'), { 45 | target: { value: 'Content-Type' } 46 | }); 47 | expect(spy).not.toHaveBeenCalled(); 48 | }); 49 | 50 | it('onBlur is not called when "value" is set but the "header" value is not', () => { 51 | const spy = jest.fn(); 52 | const { getByPlaceholderText } = render(); 53 | fireEvent.change(getByPlaceholderText('value'), { 54 | target: { value: 'application/json' } 55 | }); 56 | expect(spy).not.toHaveBeenCalled(); 57 | }); 58 | 59 | it('onRemove is called with the headers id when the user clicks on the remove icon', () => { 60 | const spy = jest.fn(); 61 | const data = { id: 1, header: 'Content-Type', value: 'application/json' }; 62 | const { getByLabelText } = render( 63 | 64 | ); 65 | fireEvent.click(getByLabelText('remove-header')); 66 | expect(spy).toHaveBeenCalledWith(1); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /client/src/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Logo = () => { 4 | return ( 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 158 | 159 | 160 | 161 | ); 162 | }; 163 | 164 | export default Logo; 165 | -------------------------------------------------------------------------------- /client/src/components/Route/RouteItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const RouteItem = ({ title, value }) => { 4 | const buildAriaLabel = (label) => 5 | label 6 | ? `route-${title.toLowerCase()}-${label}` 7 | : `route-${title.toLowerCase()}`; 8 | 9 | return ( 10 |
11 |
12 |

13 | {title} 14 |

15 |

16 | {value} 17 |

18 |
19 |
20 | ); 21 | }; 22 | 23 | export default RouteItem; 24 | -------------------------------------------------------------------------------- /client/src/components/Route/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import url from 'url'; 3 | import RouteItem from './RouteItem'; 4 | 5 | import { MOCKIT_SERVER_URL } from '../../utils/consts'; 6 | 7 | const Route = ({ routeItem, onRouteDelete, onRouteEdit }) => { 8 | const { 9 | delay = 0, 10 | route, 11 | statusCode, 12 | httpMethod, 13 | disabled = false 14 | } = routeItem; 15 | const routeClassName = disabled ? 'disabled' : ''; 16 | 17 | const openRoute = (route) => { 18 | return () => { 19 | window.open(url.resolve(MOCKIT_SERVER_URL, route), '_blank'); 20 | }; 21 | }; 22 | 23 | const editRoute = (event) => { 24 | event.stopPropagation(); 25 | onRouteEdit(routeItem); 26 | }; 27 | 28 | const deleteRoute = (event) => { 29 | event.stopPropagation(); 30 | onRouteDelete(routeItem); 31 | }; 32 | 33 | return ( 34 |
35 |
40 | 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default Route; 73 | -------------------------------------------------------------------------------- /client/src/components/Route/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, fireEvent, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import Route from './'; 6 | import RouteItem from './RouteItem'; 7 | 8 | afterEach(cleanup); 9 | 10 | const buildRoute = () => ({ 11 | id: '1', 12 | route: '/test', 13 | delay: '500', 14 | statusCode: '200', 15 | httpMethod: 'GET' 16 | }); 17 | 18 | describe('Route', () => { 19 | describe('renders', () => { 20 | it('renders the route, delay, statuscode, http method rendered', () => { 21 | const { getByLabelText, getAllByLabelText, getByText } = render( 22 | 23 | ); 24 | const routes = getAllByLabelText('Route'); 25 | const route = routes[0]; 26 | 27 | expect(getByLabelText('route-route')).toBeVisible(); 28 | expect(getByText('/test', route)).toBeVisible(); 29 | expect(getByText('500 ms', route)).toBeVisible(); 30 | expect(getByText('200', route)).toBeVisible(); 31 | expect(getByText('GET', route)).toBeVisible(); 32 | }); 33 | 34 | it('the route renders with an edit and delete button', () => { 35 | const { getByLabelText } = render(); 36 | expect(getByLabelText('Edit Route')).toBeVisible(); 37 | expect(getByLabelText('Delete Route')).toBeVisible(); 38 | }); 39 | }); 40 | 41 | describe('props and actions', () => { 42 | describe('onRouteEdit', () => { 43 | it('is called when the edit route button is clicked', () => { 44 | const spy = jest.fn(); 45 | const { getByLabelText } = render( 46 | 47 | ); 48 | fireEvent.click(getByLabelText('Edit Route')); 49 | expect(spy).toHaveBeenCalled(); 50 | }); 51 | }); 52 | describe('onRouteDelete', () => { 53 | it('is called when the delete route button is clicked', () => { 54 | const spy = jest.fn(); 55 | const { getByLabelText } = render( 56 | 57 | ); 58 | fireEvent.click(getByLabelText('Delete Route')); 59 | expect(spy).toHaveBeenCalled(); 60 | }); 61 | }); 62 | 63 | it.only('when clicking on the route the user is navigated to that route in the browser', () => { 64 | const globalOpen = global.open; 65 | global.open = jest.fn(); 66 | const { getByLabelText } = render(); 67 | fireEvent.click(getByLabelText('Route')); 68 | expect(global.open).toBeCalledWith('localhost:/test', '_blank'); 69 | global.open = globalOpen; 70 | }); 71 | }); 72 | 73 | describe('RouteItem', () => { 74 | it('renders the given title and value', () => { 75 | const { getByLabelText } = render( 76 | 77 | ); 78 | expect(getByLabelText('route-testtitle-title')).toHaveTextContent( 79 | 'TestTitle' 80 | ); 81 | expect(getByLabelText('route-testtitle-value')).toHaveTextContent( 82 | 'TestValue' 83 | ); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /client/src/components/RouteListGroup/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import Route from '../Route'; 3 | import styled from 'styled-components'; 4 | import _ from 'lodash'; 5 | 6 | const Wrapper = styled.details` 7 | padding-left: 20px; 8 | `; 9 | 10 | const RouteTitle = styled.summary` 11 | color: #1d1d1d; 12 | font-weight: 600; 13 | cursor: pointer; 14 | font-size: 1.3em; 15 | padding-left: 50px; 16 | position: relative; 17 | 18 | &:before { 19 | position: absolute; 20 | content: ' '; 21 | height: 2px; 22 | background-color: #209cee; 23 | left: 0; 24 | right: 0; 25 | top: 50%; 26 | transform: translateY(-50%); 27 | z-index: -1; 28 | } 29 | 30 | &:focus { 31 | outline: none; 32 | } 33 | 34 | &::-webkit-details-marker { 35 | background: white; 36 | padding-right: 10px; 37 | padding-left: 10px; 38 | } 39 | `; 40 | 41 | const RouteTitleText = styled.span` 42 | background-color: white; 43 | padding-right: 10px; 44 | margin-left: -10px; 45 | `; 46 | 47 | const renderLevel = (routes, onRouteEdit, onRouteDelete, index = 0) => { 48 | return ( 49 | 50 | {_.map(routes, (item, key) => { 51 | const hasChildren = _.size(_.omit(item, ['children'])) > 0; 52 | const itemIndex = hasChildren ? index + 1 : index; 53 | 54 | return ( 55 | 56 | 57 | 58 | /{key} 59 | 60 | 61 | {hasChildren && ( 62 | 67 | )} 68 | {_.size(item.children) > 0 && 69 | renderLevel(item.children, onRouteEdit, onRouteDelete, itemIndex)} 70 | 71 | ); 72 | })} 73 | 74 | ); 75 | }; 76 | 77 | export default function ({ routes, onRouteDelete, onRouteEdit }) { 78 | const groupedRoutes = useMemo(() => { 79 | // We need to order routes by route length. Otherwise, if a deeper router would appear 80 | // before a a shallower route, the shallow route would overwrite the deep route 81 | // when using _.set 82 | const orderedRoutes = _.orderBy(routes, (route) => { 83 | return route.route.split('/').length; 84 | }); 85 | 86 | const routesObject = {}; 87 | orderedRoutes.forEach((route) => { 88 | const paths = route.route.split('/'); 89 | const routeDepth = paths.length; 90 | const parentPath = paths.slice(1, paths.length - 1); 91 | const lastLevel = paths[paths.length - 1]; 92 | 93 | // routes with a depth lower than three are the first-level routes since all routes 94 | // start with a slash, so their split path becomes ['', 'path'] 95 | if (routeDepth < 3) { 96 | routesObject[lastLevel] = { 97 | ...route, 98 | children: {} 99 | }; 100 | } else { 101 | let pathToChild = []; 102 | parentPath.forEach((parent, index) => { 103 | const currentLevelPath = parentPath.slice(0, index + 1); 104 | const currentLevelPathWithChildren = []; 105 | currentLevelPath.forEach((path) => { 106 | currentLevelPathWithChildren.push(path, 'children'); 107 | }); 108 | pathToChild = currentLevelPathWithChildren; 109 | }); 110 | _.set(routesObject, [...pathToChild, lastLevel], { ...route }); 111 | } 112 | }); 113 | 114 | return routesObject; 115 | }, [routes]); 116 | 117 | return ( 118 |
119 | {renderLevel(groupedRoutes, onRouteEdit, onRouteDelete)} 120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /client/src/components/RouteListGroup/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import RouteListGroup from './'; 6 | 7 | afterEach(cleanup); 8 | 9 | const buildRoutes = () => { 10 | return [ 11 | { 12 | id: '1', 13 | route: '/test', 14 | delay: '500', 15 | statusCode: '200', 16 | httpMethod: 'GET' 17 | }, 18 | { 19 | id: '2', 20 | route: '/test/test2', 21 | delay: '500', 22 | statusCode: '200', 23 | httpMethod: 'GET' 24 | }, 25 | { 26 | id: '1', 27 | route: '/test/test2/test3', 28 | delay: '500', 29 | statusCode: '200', 30 | httpMethod: 'GET' 31 | }, 32 | { 33 | id: '1', 34 | route: '/test/test2/test3/test4', 35 | delay: '500', 36 | statusCode: '200', 37 | httpMethod: 'GET' 38 | } 39 | ]; 40 | }; 41 | 42 | describe('RouteListGroup', () => { 43 | describe('renders', () => { 44 | it.only('the list of given routes with their own groups based of the route path', () => { 45 | const { getAllByLabelText, getByLabelText, getByText } = render( 46 | 47 | ); 48 | const routes = getAllByLabelText('Route'); 49 | 50 | expect(getByLabelText('route-group-test')).toBeVisible(); 51 | expect(getByLabelText('route-group-test2')).toBeVisible(); 52 | expect(getByLabelText('route-group-test3')).toBeVisible(); 53 | expect(getByLabelText('route-group-test4')).toBeVisible(); 54 | 55 | expect(getByText('/test')).toBeVisible(); 56 | expect(getByText('/test2')).toBeVisible(); 57 | expect(getByText('/test3')).toBeVisible(); 58 | expect(getByText('/test4')).toBeVisible(); 59 | 60 | expect(routes).toHaveLength(4); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /client/src/components/RouteListStack/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Route from '../Route'; 4 | 5 | const Routes = function ({ 6 | routes = [], 7 | onRouteEdit = () => {}, 8 | onRouteDelete = () => {} 9 | }) { 10 | return ( 11 |
12 | {routes.map((route, key) => ( 13 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default Routes; 25 | -------------------------------------------------------------------------------- /client/src/components/RouteListStack/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, fireEvent, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import RouteListStack from './'; 6 | 7 | afterEach(cleanup); 8 | 9 | const buildRoutes = () => { 10 | return [ 11 | { 12 | id: '1', 13 | route: '/test', 14 | delay: '500', 15 | statusCode: '200', 16 | httpMethod: 'GET' 17 | }, 18 | { 19 | id: '2', 20 | route: '/test2', 21 | delay: '0', 22 | statusCode: '200', 23 | httpMethod: 'GET' 24 | } 25 | ]; 26 | }; 27 | 28 | describe('RouteListStack', () => { 29 | describe('renders', () => { 30 | it('the list of given routes', () => { 31 | const { getAllByLabelText } = render( 32 | 33 | ); 34 | const routes = getAllByLabelText('Route'); 35 | expect(routes.length).toBe(2); 36 | }); 37 | }); 38 | describe('props', () => { 39 | it('when the user clicks on the edit button on the rendered route the given `onRouteEdit` callback is triggered', () => { 40 | const spy = jest.fn(); 41 | const { getByLabelText } = render( 42 | 43 | ); 44 | fireEvent.click(getByLabelText('Edit Route')); 45 | expect(spy).toHaveBeenCalled(); 46 | }); 47 | it('when the user clicks on the delete button on the rendered route the given `onRouteDelete` callback is triggered', () => { 48 | const spy = jest.fn(); 49 | const { getByLabelText } = render( 50 | 51 | ); 52 | fireEvent.click(getByLabelText('Delete Route')); 53 | expect(spy).toHaveBeenCalled(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /client/src/components/RouteModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import JSONInput from 'react-json-editor-ajrm'; 3 | import HeaderInput from '../HeaderInput'; 4 | import { HttpMethods, StatusCodes } from '../../utils/consts'; 5 | import { 6 | updateRoute as updateRouteRequest, 7 | createNewRoute 8 | } from '../../utils/routes-api'; 9 | import faker from 'faker'; 10 | import uuid from 'uuid/v4'; 11 | 12 | const HTTP_METHOD_LIST = [ 13 | HttpMethods.GET, 14 | HttpMethods.POST, 15 | HttpMethods.PUT, 16 | HttpMethods.PATCH, 17 | HttpMethods.DELETE 18 | ]; 19 | const STATUS_CODES = [ 20 | StatusCodes.OK, 21 | StatusCodes.CREATED, 22 | StatusCodes.ACCEPTED, 23 | StatusCodes.NO_CONTENT, 24 | StatusCodes.BAD_REQUEST, 25 | StatusCodes.UNAUTHORIZED, 26 | StatusCodes.FORBIDDEN, 27 | StatusCodes.NOT_FOUND, 28 | StatusCodes.CONFLICT, 29 | StatusCodes.UNPROCESSABLE_ENTITY, 30 | StatusCodes.INTERNAL_SERVER_ERROR, 31 | StatusCodes.SERVICE_UNAVAILABLE, 32 | StatusCodes.GATEWAY_TIMEOUT 33 | ]; 34 | 35 | /** 36 | * add header adds some input fields 37 | * 38 | * Clicking X removes the field from the array.... 39 | */ 40 | 41 | const Modal = function (props) { 42 | const { onClose = () => {}, route: editedRoute } = props; 43 | 44 | const [route, updateRoute] = useState(editedRoute.route); 45 | const [httpMethod, updateHttpMethod] = useState(editedRoute.httpMethod); 46 | const [statusCode, updateStatusCode] = useState(editedRoute.statusCode); 47 | const [delay, updateDelay] = useState(editedRoute.delay); 48 | const [payload, updatePayload] = useState(editedRoute.payload); 49 | const [disabled, updateDisabled] = useState(editedRoute.disabled); 50 | const [headers, updateHeaders] = useState(editedRoute.headers || []); 51 | 52 | const isNewRoute = editedRoute.id === undefined; 53 | 54 | const modalTitle = isNewRoute ? 'Add Route' : 'Edit Route'; 55 | 56 | const setHeader = (updatedHeader) => { 57 | const { id } = updatedHeader; 58 | 59 | const updatedHeaders = headers.map((header, index) => { 60 | if (id !== header.id) return header; 61 | return { 62 | ...header, 63 | ...updatedHeader 64 | }; 65 | }); 66 | 67 | updateHeaders(updatedHeaders); 68 | }; 69 | 70 | const addNewHeader = () => 71 | updateHeaders(headers.concat([{ id: uuid(), header: '', value: '' }])); 72 | const removeHeader = (route) => { 73 | const filteredHeaders = headers.filter(({ id } = {}) => id !== route); 74 | updateHeaders(filteredHeaders); 75 | }; 76 | 77 | const saveChanges = async () => { 78 | try { 79 | const cleanedHeaders = headers.filter( 80 | ({ header, value }) => header !== '' && value !== '' 81 | ); 82 | 83 | const data = { 84 | ...editedRoute, 85 | route, 86 | httpMethod, 87 | statusCode, 88 | delay, 89 | payload, 90 | disabled, 91 | headers: cleanedHeaders 92 | }; 93 | 94 | isNewRoute ? await createNewRoute(data) : await updateRouteRequest(data); 95 | } catch (error) { 96 | console.log('Error', error); 97 | } 98 | }; 99 | 100 | const statusCodeStartingWith = (startingNumber) => { 101 | return STATUS_CODES.filter((routeStatusCode) => 102 | routeStatusCode.startsWith(startingNumber) 103 | ); 104 | }; 105 | 106 | return ( 107 | <> 108 |
109 |
110 |
111 |
112 |

{modalTitle}

113 |
115 |
116 |
117 | 120 |
121 | updateRoute(`/${e.currentTarget.value}`)} 128 | /> 129 | / 130 |
131 |
132 |
133 |
134 | 137 |
138 |
139 | 150 |
151 |
152 |
153 |
154 | 155 |
156 |
157 | 184 |
185 |
186 |
187 |
188 | 189 |
190 |
191 | 204 |
205 |
206 |
207 |
208 |
209 | 210 |
211 | updatePayload(e.jsObject)} 214 | height="120px" 215 | width="100%" 216 | locale="en-gb" 217 | /> 218 | updatePayload(faker.helpers.userCard())} 221 | aria-label="route-randomly-generate-data" 222 | > 223 | Randomly Generate Data 224 | 225 |
226 |
227 |
228 |
229 | 232 | {headers.length === 0 && No headers added.} 233 | {headers.map((header, index) => { 234 | return ( 235 | 240 | ); 241 | })} 242 | 249 |
250 |
251 |
252 | 253 | 263 |
264 |
265 |
266 |
267 | 274 | 281 |
282 |
283 |
284 | 285 | ); 286 | }; 287 | 288 | export default Modal; 289 | -------------------------------------------------------------------------------- /client/src/components/RouteModal/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, fireEvent, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import RouteModal from './'; 6 | import * as utils from '../../utils/routes-api'; 7 | 8 | afterEach(cleanup); 9 | 10 | const buildRoute = () => { 11 | return { 12 | id: '1', 13 | route: '/newRoute', 14 | delay: '0', 15 | disabled: false, 16 | payload: { test: true }, 17 | statusCode: '200', 18 | headers: [ 19 | { id: 1, header: 'Content-Type', value: 'application/json' }, 20 | { id: 2, header: 'x-api-key', value: 'test' } 21 | ] 22 | }; 23 | }; 24 | 25 | jest.mock('../../utils/routes-api', () => { 26 | return { createNewRoute: jest.fn(), updateRoute: jest.fn() }; 27 | }); 28 | 29 | describe('Route Modal', () => { 30 | describe('renders', () => { 31 | it('with a header of `Add route` when the given route has no id', () => { 32 | const route = buildRoute(); 33 | delete route['id']; 34 | const { getByText } = render(); 35 | expect(getByText('Add Route')).toBeVisible(); 36 | }); 37 | 38 | it('with a header of `Edit route` when the given route has an id', () => { 39 | const route = buildRoute(); 40 | const { getByText } = render(); 41 | expect(getByText('Edit Route')).toBeVisible(); 42 | }); 43 | 44 | it('with an input for the route value, reads the route value and strips of any `/` values from it', () => { 45 | const route = buildRoute(); 46 | const { getByLabelText } = render(); 47 | 48 | expect(getByLabelText('route-name', { selector: 'input' }).value).toEqual( 49 | 'newRoute' 50 | ); 51 | }); 52 | 53 | it('with a dropdown list of all available http methods', () => { 54 | const route = buildRoute(); 55 | const { getByLabelText, getByValue } = render( 56 | 57 | ); 58 | const dropdown = getByLabelText('route-http'); 59 | const dropdownOptions = dropdown.children; 60 | 61 | expect(dropdownOptions.length).toEqual(5); 62 | 63 | expect(getByValue('GET', dropdownOptions)).toBeVisible(); 64 | expect(getByValue('POST', dropdownOptions)).toBeVisible(); 65 | expect(getByValue('PUT', dropdownOptions)).toBeVisible(); 66 | expect(getByValue('DELETE', dropdownOptions)).toBeVisible(); 67 | expect(getByValue('PATCH', dropdownOptions)).toBeVisible(); 68 | }); 69 | 70 | it('with a dropdown list with three groups', () => { 71 | const route = buildRoute(); 72 | const { getByLabelText } = render(); 73 | const dropdown = getByLabelText('route-statuscode'); 74 | const dropdownOptionGroups = dropdown.childNodes; 75 | 76 | expect(dropdownOptionGroups.length).toEqual(3); 77 | expect(getByLabelText('2xx', dropdownOptionGroups)).toBeVisible(); 78 | expect(getByLabelText('4xx', dropdownOptionGroups)).toBeVisible(); 79 | expect(getByLabelText('5xx', dropdownOptionGroups)).toBeVisible(); 80 | }); 81 | 82 | it('with a dropdown list of all available status code', () => { 83 | const route = buildRoute(); 84 | const { getByLabelText, getByValue } = render( 85 | 86 | ); 87 | const dropdown = getByLabelText('route-statuscode'); 88 | const dropdownOptionGroups = dropdown.childNodes; 89 | 90 | expect(getByValue('200', dropdownOptionGroups[0].children)).toBeVisible(); 91 | expect(getByValue('201', dropdownOptionGroups[0].children)).toBeVisible(); 92 | expect(getByValue('202', dropdownOptionGroups[0].children)).toBeVisible(); 93 | expect(getByValue('204', dropdownOptionGroups[0].children)).toBeVisible(); 94 | expect(getByValue('400', dropdownOptionGroups[1].children)).toBeVisible(); 95 | expect(getByValue('401', dropdownOptionGroups[1].children)).toBeVisible(); 96 | expect(getByValue('403', dropdownOptionGroups[1].children)).toBeVisible(); 97 | expect(getByValue('404', dropdownOptionGroups[1].children)).toBeVisible(); 98 | expect(getByValue('409', dropdownOptionGroups[1].children)).toBeVisible(); 99 | expect(getByValue('422', dropdownOptionGroups[1].children)).toBeVisible(); 100 | expect(getByValue('500', dropdownOptionGroups[2].children)).toBeVisible(); 101 | expect(getByValue('503', dropdownOptionGroups[2].children)).toBeVisible(); 102 | expect(getByValue('504', dropdownOptionGroups[2].children)).toBeVisible(); 103 | }); 104 | 105 | it('with a dropdown list of all available delay values', () => { 106 | const route = buildRoute(); 107 | const { getByLabelText, getByValue } = render( 108 | 109 | ); 110 | const dropdown = getByLabelText('route-delay'); 111 | const dropdownOptions = dropdown.children; 112 | 113 | expect(dropdownOptions.length).toEqual(7); 114 | 115 | expect(getByValue('0', dropdownOptions)).toBeVisible(); 116 | expect(getByValue('250', dropdownOptions)).toBeVisible(); 117 | expect(getByValue('500', dropdownOptions)).toBeVisible(); 118 | expect(getByValue('1000', dropdownOptions)).toBeVisible(); 119 | expect(getByValue('1500', dropdownOptions)).toBeVisible(); 120 | expect(getByValue('2000', dropdownOptions)).toBeVisible(); 121 | expect(getByValue('5000', dropdownOptions)).toBeVisible(); 122 | }); 123 | 124 | it('with a link that randomly generates data', () => { 125 | const route = buildRoute(); 126 | const { getByLabelText } = render(); 127 | expect(getByLabelText('route-randomly-generate-data')).toBeVisible(); 128 | }); 129 | 130 | it('with a list of headers when the route has headers', () => { 131 | const route = buildRoute(); 132 | const { getAllByLabelText } = render(); 133 | const headers = getAllByLabelText('header'); 134 | expect(headers).toHaveLength(2); 135 | }); 136 | 137 | it('a message saying "No headers added" when no headers are in the route', () => { 138 | const route = buildRoute(); 139 | delete route['headers']; 140 | const { getByLabelText } = render(); 141 | expect(getByLabelText('no-headers-message')).toBeVisible(); 142 | }); 143 | 144 | it('with a checkbox to disable the route', () => { 145 | const route = buildRoute(); 146 | const { getByLabelText } = render(); 147 | expect(getByLabelText('route-disable')).toBeVisible(); 148 | }); 149 | 150 | it('with a `Save Changes` button and `Cancel` button', () => { 151 | const route = buildRoute(); 152 | const { getByLabelText } = render(); 153 | expect(getByLabelText('route-save')).toBeVisible(); 154 | expect(getByLabelText('route-cancel')).toBeVisible(); 155 | }); 156 | }); 157 | 158 | describe('headers', () => { 159 | it('when clicking `Add Header` two new inputs are shown', () => { 160 | const route = buildRoute(); 161 | delete route['headers']; 162 | const { getByLabelText } = render(); 163 | fireEvent.click(getByLabelText('add-header')); 164 | 165 | expect(getByLabelText('header')).toBeVisible(); 166 | expect(getByLabelText('header-key').value).toBe(''); 167 | expect(getByLabelText('header-value').value).toBe(''); 168 | }); 169 | 170 | it('when clicking the remove button on the header the header is removed', () => { 171 | const route = buildRoute(); 172 | delete route['headers']; 173 | route['headers'] = [{ id: '1', header: 'x-api-key', value: 'test' }]; 174 | 175 | const { getByLabelText, queryByLabelText } = render( 176 | 177 | ); 178 | fireEvent.click(getByLabelText('remove-header')); 179 | 180 | expect(queryByLabelText('header')).toBeNull(); 181 | }); 182 | 183 | it('when clicking `Save Route` with empty headers on the screen they are removed before sending the data', () => { 184 | const route = buildRoute(); 185 | route.headers.push({ id: 99, header: '', value: '' }); 186 | 187 | const { getByLabelText } = render(); 188 | fireEvent.click(getByLabelText('route-save')); 189 | 190 | const expectedResult = Object.assign({}, route); 191 | expectedResult.routes = expectedResult.headers.pop(); 192 | 193 | expect(utils.updateRoute).toHaveBeenCalledWith(route); 194 | }); 195 | }); 196 | 197 | describe('modal', () => { 198 | it('when entering a value into the route input field the value is updated', () => { 199 | const { getByLabelText } = render(); 200 | expect(getByLabelText('route-name').value).toEqual('newRoute'); 201 | fireEvent.change(getByLabelText('route-name'), { 202 | target: { value: 'testRoute' } 203 | }); 204 | expect(getByLabelText('route-name').value).toEqual('testRoute'); 205 | }); 206 | 207 | it('when selecting a value from the dropdown list of HTTP methods the dropdown value is updated', () => { 208 | const { getByLabelText } = render(); 209 | expect(getByLabelText('route-http').value).toEqual('GET'); 210 | fireEvent.change(getByLabelText('route-http'), { 211 | target: { value: 'POST' } 212 | }); 213 | expect(getByLabelText('route-http').value).toEqual('POST'); 214 | }); 215 | 216 | it('when selecting a value from the dropdown list of Status codes methods the dropdown value is updated', () => { 217 | const { getByLabelText } = render(); 218 | expect(getByLabelText('route-statuscode').value).toEqual('200'); 219 | fireEvent.change(getByLabelText('route-statuscode'), { 220 | target: { value: '500' } 221 | }); 222 | expect(getByLabelText('route-statuscode').value).toEqual('500'); 223 | }); 224 | 225 | it('when selecting a value from the delay list the dropdown value is updated', () => { 226 | const { getByLabelText } = render(); 227 | expect(getByLabelText('route-delay').value).toEqual('0'); 228 | fireEvent.change(getByLabelText('route-delay'), { 229 | target: { value: '500' } 230 | }); 231 | expect(getByLabelText('route-delay').value).toEqual('500'); 232 | }); 233 | 234 | it('when disabling the route the checkbox value is updated', () => { 235 | const { getByLabelText } = render(); 236 | expect(getByLabelText('route-disable').checked).toEqual(false); 237 | fireEvent.change(getByLabelText('route-disable'), { 238 | target: { checked: true } 239 | }); 240 | expect(getByLabelText('route-disable').checked).toEqual(true); 241 | }); 242 | 243 | it('when the save button is clicked and the route is a new route a request to create a new route is made', () => { 244 | const route = buildRoute(); 245 | delete route['id']; 246 | const { getByLabelText } = render(); 247 | fireEvent.click(getByLabelText('route-save')); 248 | expect(utils.createNewRoute).toHaveBeenCalledWith(route); 249 | }); 250 | 251 | it('when the save button is clicked and the route is an existing route a request to edit the route', () => { 252 | const route = buildRoute(); 253 | const { getByLabelText } = render(); 254 | fireEvent.click(getByLabelText('route-save')); 255 | expect(utils.updateRoute).toHaveBeenCalledWith(route); 256 | }); 257 | }); 258 | 259 | describe('props', () => { 260 | describe('onClose', () => { 261 | it('is called when closing the modal dialog using the close button', () => { 262 | const spy = jest.fn(); 263 | const { getByLabelText } = render( 264 | 265 | ); 266 | fireEvent.click(getByLabelText('close')); 267 | expect(spy).toHaveBeenCalled(); 268 | }); 269 | it('is called when clicking the cancel button', () => { 270 | const spy = jest.fn(); 271 | const { getByLabelText } = render( 272 | 273 | ); 274 | fireEvent.click(getByLabelText('route-cancel')); 275 | expect(spy).toHaveBeenCalled(); 276 | }); 277 | }); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /client/src/components/SettingsModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { settings as applicationSettings } from '../../config/routes.json'; 3 | import { updateSettings } from '../../utils/routes-api'; 4 | 5 | const SettingsModal = function ({ 6 | onClose = () => {}, 7 | onConfirm = () => {}, 8 | heading, 9 | children 10 | } = {}) { 11 | const [settings, setSettings] = useState(applicationSettings); 12 | 13 | const { 14 | features: { chaosMonkey, cors, authentication, groupedRoutes } = {} 15 | } = settings; 16 | 17 | const setFeature = (feature, value) => { 18 | const newSettings = { 19 | ...settings 20 | }; 21 | 22 | newSettings['features'][feature] = value; 23 | 24 | setSettings(newSettings); 25 | }; 26 | 27 | const saveSettings = async () => { 28 | try { 29 | await updateSettings(settings); 30 | } catch (error) { 31 | console.log('Error'); 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 |
38 |
39 |
40 |
41 |

Settings

42 |
44 |
45 |
46 | 47 |

48 | Enable basic authentication on all routes. This can be 49 | configured with a username and password in the configuration 50 | file. 51 |

52 |
53 | 60 | setFeature('authentication', e.target.checked) 61 | } 62 | aria-label="feature-basic-auth-input" 63 | /> 64 | 67 |
68 |
69 |
70 |
71 | 72 |

73 | Cross-origin resource sharing (CORS) is a mechanism that allows 74 | restricted resources on a web page to be requested from another 75 | domain outside the domain from which the first resource was 76 | served. This feature enables CORS for all routes. 77 |

78 |
79 | setFeature('cors', e.target.checked)} 86 | aria-label="feature-cors-input" 87 | /> 88 | 89 |
90 |
91 |
92 |
93 | 94 |

95 | Unleash the monkey. The monkey will randomly take down end 96 | points and enforce failures on your routes. 97 |

98 |
99 | setFeature('chaosMonkey', e.target.checked)} 106 | aria-label="feature-chaos-monkey-input" 107 | /> 108 | 109 |
110 |
111 |
112 |
113 | 114 |

115 | With the ability to group routes, you can quickly hide a whole 116 | list of routes that share the same basepath. 117 |

118 |
119 | 126 | setFeature('groupedRoutes', e.target.checked) 127 | } 128 | aria-label="feature-grouped-routes-input" 129 | /> 130 | 133 |
134 |
135 |
136 | 137 |
138 | 145 | 148 |
149 |
150 |
151 | 152 | ); 153 | }; 154 | 155 | export default SettingsModal; 156 | -------------------------------------------------------------------------------- /client/src/components/SettingsModal/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { render, fireEvent, cleanup } from 'react-testing-library'; 4 | import 'jest-dom/extend-expect'; 5 | import SettingsDialog from './'; 6 | import * as utils from '../../utils/routes-api'; 7 | 8 | afterEach(cleanup); 9 | 10 | jest.mock('../../utils/routes-api', () => { 11 | return { updateSettings: jest.fn() }; 12 | }); 13 | 14 | describe('Settings Dialog', () => { 15 | describe('renders', () => { 16 | it('with a header', () => { 17 | const { getByText } = render(); 18 | expect(getByText('Settings')).toBeVisible(); 19 | }); 20 | }); 21 | 22 | describe('features', () => { 23 | describe('Basic Auth', () => { 24 | it('renders the feature with an input', () => { 25 | const { getByLabelText } = render(); 26 | expect(getByLabelText('feature-basic-auth')).toBeVisible(); 27 | expect(getByLabelText('feature-basic-auth-input')).toBeVisible(); 28 | }); 29 | 30 | it('clicking on the feature enables basic authenication and updates the input value', () => { 31 | const { getByLabelText } = render(); 32 | 33 | expect(getByLabelText('feature-basic-auth-input').checked).toEqual( 34 | false 35 | ); 36 | fireEvent.change(getByLabelText('feature-basic-auth-input'), { 37 | target: { checked: true } 38 | }); 39 | expect(getByLabelText('feature-basic-auth-input').checked).toEqual( 40 | true 41 | ); 42 | }); 43 | }); 44 | describe('CORS', () => { 45 | it('renders the feature with an input', () => { 46 | const { getByLabelText } = render(); 47 | expect(getByLabelText('feature-cors')).toBeVisible(); 48 | expect(getByLabelText('feature-cors-input')).toBeVisible(); 49 | }); 50 | 51 | it('clicking on the feature enables basic authenication and updates the input value', () => { 52 | const { getByLabelText } = render(); 53 | 54 | expect(getByLabelText('feature-cors-input').checked).toEqual(true); 55 | fireEvent.change(getByLabelText('feature-cors-input'), { 56 | target: { checked: false } 57 | }); 58 | expect(getByLabelText('feature-cors-input').checked).toEqual(false); 59 | }); 60 | }); 61 | describe('Chaos Monkey', () => { 62 | it('renders the feature with an input', () => { 63 | const { getByLabelText } = render(); 64 | expect(getByLabelText('feature-chaos-monkey')).toBeVisible(); 65 | expect(getByLabelText('feature-chaos-monkey-input')).toBeVisible(); 66 | }); 67 | 68 | it('clicking on the feature enables basic authenication and updates the input value', () => { 69 | const { getByLabelText } = render(); 70 | 71 | expect(getByLabelText('feature-chaos-monkey-input').checked).toEqual( 72 | false 73 | ); 74 | fireEvent.change(getByLabelText('feature-chaos-monkey-input'), { 75 | target: { checked: true } 76 | }); 77 | expect(getByLabelText('feature-chaos-monkey-input').checked).toEqual( 78 | true 79 | ); 80 | }); 81 | }); 82 | describe('Grouped Routes', () => { 83 | it('renders the feature with an input', () => { 84 | const { getByLabelText } = render(); 85 | expect(getByLabelText('feature-grouped-routes')).toBeVisible(); 86 | expect(getByLabelText('feature-grouped-routes-input')).toBeVisible(); 87 | }); 88 | 89 | it('clicking on the feature enables grouped routes and updates the input value', () => { 90 | const { getByLabelText } = render(); 91 | 92 | expect(getByLabelText('feature-grouped-routes-input').checked).toEqual( 93 | false 94 | ); 95 | fireEvent.change(getByLabelText('feature-grouped-routes-input'), { 96 | target: { checked: true } 97 | }); 98 | expect(getByLabelText('feature-grouped-routes-input').checked).toEqual( 99 | true 100 | ); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('saving settings', () => { 106 | it('clicking save settings makes a request to update the users settings', () => { 107 | const { getByLabelText } = render(); 108 | fireEvent.click(getByLabelText('save')); 109 | expect(utils.updateSettings).toHaveBeenCalled(); 110 | }); 111 | }); 112 | 113 | describe('props', () => { 114 | describe('onClose', () => { 115 | it('is called when closing the modal dialog using the close button', () => { 116 | const spy = jest.fn(); 117 | const { getByLabelText } = render(); 118 | fireEvent.click(getByLabelText('close')); 119 | expect(spy).toHaveBeenCalled(); 120 | }); 121 | it('is called when clicking the cancel button', () => { 122 | const spy = jest.fn(); 123 | const { getByLabelText } = render(); 124 | fireEvent.click(getByLabelText('cancel')); 125 | expect(spy).toHaveBeenCalled(); 126 | }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /client/src/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "features": { 4 | "chaosMonkey": false, 5 | "groupedRoutes": false, 6 | "cors": true, 7 | "authentication": false 8 | }, 9 | "authentication": { 10 | "username": "test", 11 | "password": "test" 12 | } 13 | }, 14 | "routes": [ 15 | { 16 | "id": "1f4dfdf3-5712-48b7-9310-6314f8ee2adc", 17 | "route": "/user", 18 | "payload": { 19 | "httpMethod": "get", 20 | "newRoute": true, 21 | "Money": 100 22 | }, 23 | "headers": [ 24 | { 25 | "id": "1", 26 | "header": "Accept", 27 | "value": "application/json" 28 | }, 29 | { 30 | "id": "2", 31 | "header": "x-api-key", 32 | "value": "abcdef12345" 33 | } 34 | ], 35 | "httpMethod": "POST", 36 | "statusCode": "201", 37 | "delay": "500" 38 | }, 39 | { 40 | "id": "1f4dfdf3-5712-48b7-9310-6314f8ee2adc", 41 | "route": "/user/boyney123", 42 | "payload": { 43 | "httpMethod": "get", 44 | "newRoute": true, 45 | "Money": 100 46 | }, 47 | "httpMethod": "POST", 48 | "statusCode": "201", 49 | "delay": "500" 50 | }, 51 | { 52 | "id": "1f4dfdf3-5712-48b7-9310-6314f8ee2add", 53 | "route": "/fruit", 54 | "payload": { 55 | "httpMethod": "get", 56 | "newRoute": true, 57 | "Money": 100 58 | }, 59 | "headers": [ 60 | { 61 | "header": "test", 62 | "value": "test" 63 | } 64 | ], 65 | "httpMethod": "POST", 66 | "statusCode": "201", 67 | "delay": "0", 68 | "disabled": true 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /client/src/hooks/useScrollReveal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import ScrollReveal from 'scrollreveal'; 3 | 4 | export default function (options) { 5 | useEffect(() => { 6 | const sr = (window.sr = ScrollReveal()); 7 | options.forEach(({ selector, options }) => { 8 | sr.reveal(selector, options); 9 | }); 10 | }, []); 11 | } 12 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: http://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /client/src/scss/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | main { 6 | flex: 1 0 auto; 7 | } 8 | 9 | footer { 10 | position: inherit !important; 11 | } 12 | 13 | main { 14 | padding: 3rem 5em; 15 | } 16 | 17 | .footer { 18 | padding: 3rem 5em; 19 | background: #1d1d1d; 20 | color: white; 21 | } 22 | 23 | .footer strong { 24 | color: white; 25 | } 26 | 27 | .App-logo { 28 | animation: App-logo-spin infinite 20s linear; 29 | height: 40vmin; 30 | } 31 | 32 | .App-header { 33 | background-color: #282c34; 34 | min-height: 100vh; 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | justify-content: center; 39 | font-size: calc(10px + 2vmin); 40 | color: white; 41 | } 42 | 43 | .App-link { 44 | color: #61dafb; 45 | } 46 | 47 | @keyframes App-logo-spin { 48 | from { 49 | transform: rotate(0deg); 50 | } 51 | to { 52 | transform: rotate(360deg); 53 | } 54 | } 55 | 56 | .hero .title, 57 | .subtitle { 58 | visibility: hidden; 59 | } 60 | 61 | .main-background { 62 | background: #0f8a9d; 63 | background: linear-gradient(57deg, #00c6a7 0%, #1e4d92 100%); 64 | } 65 | 66 | .card-header-title { 67 | background: #00c5a5; 68 | color: white !important; 69 | } 70 | 71 | #root { 72 | display: flex; 73 | min-height: 100vh; 74 | flex-direction: column; 75 | } 76 | 77 | /* Route */ 78 | 79 | .no-routes { 80 | color: #c4c4c4; 81 | } 82 | 83 | .route { 84 | /* background: #1f619517; */ 85 | border: 1px solid #b0d2e6; 86 | padding: 1em; 87 | box-shadow: 0 3px 6px #00000014; 88 | } 89 | 90 | .route.disabled { 91 | background: #ebebeb; 92 | opacity: 0.5 !important; 93 | } 94 | 95 | .route:hover { 96 | cursor: pointer; 97 | box-shadow: 0 10px 20px #00000014; 98 | /* box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.2); */ 99 | transform: scale(1.01) !important; 100 | 101 | /* box-shadow: 2px 4px 20px 0px #a6a9a9; */ 102 | } 103 | 104 | .route .fa-edit { 105 | margin-right: 10px; 106 | } 107 | 108 | .route .heading { 109 | color: black; 110 | } 111 | 112 | /* HTTP Methods */ 113 | .http-method { 114 | padding: 5px; 115 | border-radius: 5px; 116 | } 117 | 118 | .http-method__get { 119 | background: #29c711; 120 | } 121 | 122 | .level { 123 | color: white !important; 124 | padding: 10px 0; 125 | } 126 | 127 | footer { 128 | bottom: 0; 129 | left: 0; 130 | position: fixed; 131 | right: 0; 132 | } 133 | 134 | nav { 135 | padding: 1em; 136 | } 137 | 138 | .random-data { 139 | margin-top: 10px; 140 | } 141 | 142 | /* Helpers */ 143 | 144 | .mr10 { 145 | margin-right: 10px; 146 | } 147 | 148 | .mt10 { 149 | margin-top: 10px; 150 | } 151 | 152 | .mb10 { 153 | margin-bottom: 10px; 154 | } 155 | .mb20 { 156 | margin-bottom: 20px; 157 | } 158 | 159 | @-webkit-keyframes monkey { 160 | 0% { 161 | -webkit-transform: translate(0px, 1px) rotate(0deg); 162 | } 163 | 10% { 164 | -webkit-transform: translate(-1px, -2px) rotate(-1deg); 165 | } 166 | 20% { 167 | -webkit-transform: translate(-3px, 0px) rotate(1deg); 168 | } 169 | 30% { 170 | -webkit-transform: translate(0px, 2px) rotate(0deg); 171 | } 172 | 40% { 173 | -webkit-transform: translate(1px, -1px) rotate(1deg); 174 | } 175 | 50% { 176 | -webkit-transform: translate(-1px, 2px) rotate(-1deg); 177 | } 178 | 60% { 179 | -webkit-transform: translate(-3px, 1px) rotate(0deg); 180 | } 181 | 70% { 182 | -webkit-transform: translate(2px, 1px) rotate(-1deg); 183 | } 184 | 80% { 185 | -webkit-transform: translate(-1px, -1px) rotate(1deg); 186 | } 187 | 90% { 188 | -webkit-transform: translate(2px, 2px) rotate(0deg); 189 | } 190 | 100% { 191 | -webkit-transform: translate(1px, -2px) rotate(-1deg); 192 | } 193 | } 194 | 195 | .chaos-monkey { 196 | -webkit-animation-name: monkey; 197 | -webkit-animation-duration: 0.8s; 198 | -webkit-animation-iteration-count: infinite; 199 | -webkit-animation-timing-function: linear; 200 | font-size: 3rem; 201 | } 202 | 203 | .Header__Remove { 204 | margin: 3px 0px 0px; 205 | font-size: 20px; 206 | color: #d87171; 207 | cursor: pointer; 208 | } 209 | -------------------------------------------------------------------------------- /client/src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/bulma/bulma.sass'; 2 | @import '../../node_modules/bulma-extensions/bulma-switch/src/sass/index.sass'; 3 | 4 | @import './App.scss'; 5 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then((registration) => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /client/src/spec.js: -------------------------------------------------------------------------------- 1 | // __tests__/fetch.test.js 2 | import React from 'react'; 3 | import { 4 | render, 5 | fireEvent, 6 | cleanup, 7 | queryAllByLabelText 8 | } from 'react-testing-library'; 9 | import * as utils from './utils/routes-api'; 10 | import 'jest-dom/extend-expect'; 11 | import App from './App'; 12 | 13 | jest.mock('./utils/routes-api', () => { 14 | return { 15 | deleteRoute: jest.fn(), 16 | buildRoute: jest.fn(() => { 17 | return { route: '/test' }; 18 | }) 19 | }; 20 | }); 21 | 22 | afterEach(cleanup); 23 | 24 | describe('App', () => { 25 | describe('renders', () => { 26 | it('the add route and settings button', () => { 27 | const { getByLabelText } = render(); 28 | 29 | expect(getByLabelText('Add Route')).toBeVisible(); 30 | expect(getByLabelText('Settings')).toBeVisible(); 31 | }); 32 | 33 | it('renders a list of stacked routes based on the routes in the configuration', () => { 34 | const { container, getByLabelText } = render(); 35 | const routes = queryAllByLabelText(container, 'Route'); 36 | expect(routes).toHaveLength(3); 37 | expect(getByLabelText('routes-stacked')).toBeVisible(); 38 | }); 39 | 40 | it('renders a list of routes that are grouped in the grouped feature is set to true', () => { 41 | const settings = { features: { groupedRoutes: true } }; 42 | const { container, getByLabelText } = render(); 43 | const routes = queryAllByLabelText(container, 'Route'); 44 | expect(routes).toHaveLength(3); 45 | expect(getByLabelText('routes-grouped')).toBeVisible(); 46 | }); 47 | 48 | it('renders the no routes message when no routes have been added yet', () => { 49 | const { getByLabelText } = render(); 50 | expect(getByLabelText('no-routes')).toBeVisible(); 51 | }); 52 | 53 | it('renders a footer on the screen with a link to the github repo', () => { 54 | const { getByLabelText } = render(); 55 | expect(getByLabelText('Site footer')).toBeVisible(); 56 | expect(getByLabelText('Github Repo')).toBeVisible(); 57 | expect(getByLabelText('Github Repo').text).toBe('David Boyne'); 58 | }); 59 | }); 60 | 61 | describe('RouteListGrouped', () => { 62 | describe('edit route', () => { 63 | it('when edit is selected on the route the modal dialog is shown with that route', () => { 64 | const settings = { features: { groupedRoutes: true } }; 65 | const { container, getByTestId, getByLabelText } = render( 66 | 67 | ); 68 | const routes = queryAllByLabelText(container, 'Route'); 69 | const editButton = getByLabelText('Edit Route', { element: routes[0] }); 70 | 71 | fireEvent( 72 | editButton, 73 | new MouseEvent('click', { 74 | bubbles: true, 75 | cancelable: true 76 | }) 77 | ); 78 | 79 | expect(getByTestId('route-modal')).toBeVisible(); 80 | }); 81 | }); 82 | 83 | describe('delete route', () => { 84 | const clickDeleteRoute = () => { 85 | const settings = { features: { groupedRoutes: true } }; 86 | const { getByLabelText, container, getByText } = render( 87 | 88 | ); 89 | const routes = queryAllByLabelText(container, 'Route'); 90 | const deleteButton = getByLabelText('Delete Route', { 91 | element: routes[0] 92 | }); 93 | 94 | fireEvent.click(deleteButton); 95 | 96 | return { getByLabelText, getByText }; 97 | }; 98 | 99 | it('when delete is selected on the route the confirmation dialog is shown on that route', () => { 100 | const { getByLabelText } = clickDeleteRoute(); 101 | expect(getByLabelText('Confirmation Dialog')).toBeVisible(); 102 | }); 103 | 104 | it('when clicking confirm on the route deletion a request is made to delete that route', () => { 105 | const { getByLabelText, getByText } = clickDeleteRoute(); 106 | 107 | const modal = getByLabelText('Confirmation Dialog'); 108 | 109 | fireEvent.click(getByText('Delete', { element: modal })); 110 | 111 | expect(utils.deleteRoute).toHaveBeenCalled(); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('RouteListStacked', () => { 117 | describe('edit route', () => { 118 | it('when edit is selected on the route the modal dialog is shown with that route', () => { 119 | const { container, getByTestId, getByLabelText } = render(); 120 | const routes = queryAllByLabelText(container, 'Route'); 121 | const editButton = getByLabelText('Edit Route', { element: routes[0] }); 122 | 123 | fireEvent( 124 | editButton, 125 | new MouseEvent('click', { 126 | bubbles: true, 127 | cancelable: true 128 | }) 129 | ); 130 | 131 | expect(getByTestId('route-modal')).toBeVisible(); 132 | }); 133 | }); 134 | 135 | describe('delete route', () => { 136 | const clickDeleteRoute = () => { 137 | const { getByLabelText, container, getByText } = render(); 138 | const routes = queryAllByLabelText(container, 'Route'); 139 | const deleteButton = getByLabelText('Delete Route', { 140 | element: routes[0] 141 | }); 142 | 143 | fireEvent.click(deleteButton); 144 | 145 | return { getByLabelText, getByText }; 146 | }; 147 | 148 | it('when delete is selected on the route the confirmation dialog is shown on that route', () => { 149 | const { getByLabelText } = clickDeleteRoute(); 150 | expect(getByLabelText('Confirmation Dialog')).toBeVisible(); 151 | }); 152 | 153 | it('when clicking confirm on the route deletion a request is made to delete that route', () => { 154 | const { getByLabelText, getByText } = clickDeleteRoute(); 155 | 156 | const modal = getByLabelText('Confirmation Dialog'); 157 | 158 | fireEvent.click(getByText('Delete', { element: modal })); 159 | 160 | expect(utils.deleteRoute).toHaveBeenCalled(); 161 | }); 162 | }); 163 | }); 164 | 165 | describe('Features', () => { 166 | describe('Add route', () => { 167 | const showSettingsModal = () => { 168 | const { getByLabelText, getByTestId, queryByTestId } = render(); 169 | const addButton = getByLabelText('Add Route'); 170 | fireEvent.click(addButton); 171 | return { getByLabelText, getByTestId, queryByTestId }; 172 | }; 173 | 174 | it('when clicking on the Add Route button the modal is shown to the user', () => { 175 | const { getByTestId } = showSettingsModal(); 176 | expect(getByTestId('route-modal')).toBeVisible(); 177 | }); 178 | it('when the modal is visible and the user clicks the close button, the modal is closed', () => { 179 | const { getByLabelText, queryByTestId } = showSettingsModal(); 180 | 181 | fireEvent.click(getByLabelText('close')); 182 | expect(queryByTestId('route-modal')).toBeNull(); 183 | }); 184 | }); 185 | 186 | describe('Settings modal', () => { 187 | it('when clicking on the settings button the settings modal is shown to the user', () => { 188 | const { getByLabelText } = render(); 189 | const settingsButton = getByLabelText('Settings'); 190 | fireEvent.click(settingsButton); 191 | expect(getByLabelText('Settings Modal')).toBeVisible(); 192 | }); 193 | 194 | it('when the modal is visible and the user clicks on the close button, the modal is closed', () => { 195 | const { getByLabelText, queryByLabelText } = render(); 196 | const settingsButton = getByLabelText('Settings'); 197 | fireEvent.click(settingsButton); 198 | fireEvent.click(getByLabelText('close')); 199 | expect(queryByLabelText('Settings Modal')).toBeNull(); 200 | }); 201 | }); 202 | 203 | it('when the chaos monkey feature is enabled then the monkey is shown above the list of routes', () => { 204 | const settings = { features: { chaosMonkey: true } }; 205 | 206 | const { getByLabelText } = render(); 207 | 208 | expect(getByLabelText('Chaos Monkey Feature')).toBeVisible(); 209 | }); 210 | 211 | it('when the chaos monkey is not enabled the monkey is not shown above the list of routes', () => { 212 | const settings = { features: { chaosMonkey: false } }; 213 | 214 | const { queryByLabelText } = render(); 215 | 216 | expect(queryByLabelText('Chaos Monkey Feature')).toBeNull(); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /client/src/utils/consts/index.js: -------------------------------------------------------------------------------- 1 | export const HttpMethods = { 2 | GET: 'GET', 3 | PUT: 'PUT', 4 | PATCH: 'PATCH', 5 | POST: 'POST', 6 | DELETE: 'DELETE' 7 | }; 8 | 9 | export const StatusCodes = { 10 | OK: '200', 11 | CREATED: '201', 12 | ACCEPTED: '202', 13 | NO_CONTENT: '204', 14 | BAD_REQUEST: '400', 15 | UNAUTHORIZED: '401', 16 | FORBIDDEN: '403', 17 | NOT_FOUND: '404', 18 | CONFLICT: '409', 19 | UNPROCESSABLE_ENTITY: '422', 20 | INTERNAL_SERVER_ERROR: '500', 21 | SERVICE_UNAVAILABLE: '503', 22 | GATEWAY_TIMEOUT: '504' 23 | }; 24 | 25 | export const MOCKIT_SERVER_URL = 26 | process.env.REACT_APP_MOCKIT_SERVER_URL || 'localhost:3000'; 27 | -------------------------------------------------------------------------------- /client/src/utils/routes-api/index.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import { HttpMethods, StatusCodes } from '../consts'; 3 | 4 | const host = process.env.REACT_APP_MOCKIT_API_URL || 'localhost'; 5 | 6 | export const buildRoute = () => ({ 7 | route: '/newRoute', 8 | httpMethod: HttpMethods.GET, 9 | statusCode: StatusCodes.OK, 10 | delay: '0', 11 | payload: { test: true } 12 | }); 13 | 14 | export const createNewRoute = async (route) => { 15 | return await fetch(url.resolve(host, '/route'), { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json' 19 | }, 20 | body: JSON.stringify(route) 21 | }); 22 | }; 23 | 24 | export const updateRoute = async (data) => { 25 | return await fetch(url.resolve(host, '/route'), { 26 | method: 'PUT', 27 | headers: { 28 | 'Content-Type': 'application/json' 29 | }, 30 | body: JSON.stringify(data) 31 | }); 32 | }; 33 | 34 | export const deleteRoute = async (data) => { 35 | return await fetch(url.resolve(host, '/route'), { 36 | method: 'DELETE', 37 | headers: { 38 | 'Content-Type': 'application/json' 39 | }, 40 | body: JSON.stringify(data) 41 | }); 42 | }; 43 | 44 | export const updateSettings = async (settings) => { 45 | return await fetch(url.resolve(host, '/settings'), { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json' 49 | }, 50 | body: JSON.stringify(settings) 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /client/src/utils/routes-api/spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | createNewRoute, 3 | updateRoute, 4 | deleteRoute, 5 | buildRoute as buildRouteUtil, 6 | updateSettings 7 | } from './index'; 8 | 9 | const mockFetchPromise = Promise.resolve({ 10 | json: () => Promise.resolve({}) 11 | }); 12 | 13 | jest.spyOn(window, 'fetch').mockImplementation(() => mockFetchPromise); // 4 14 | 15 | const buildRoute = () => ({ 16 | route: '/test', 17 | httpMethod: 'GET', 18 | delay: '2000', 19 | payload: { test: true }, 20 | statusCode: '200' 21 | }); 22 | 23 | describe('routes-api', () => { 24 | beforeEach(() => { 25 | window.fetch.mockClear(); 26 | }); 27 | 28 | describe('buildRoute', () => { 29 | it('returns a new route object', () => { 30 | const route = buildRouteUtil(); 31 | expect(route).toEqual({ 32 | route: '/newRoute', 33 | httpMethod: 'GET', 34 | statusCode: '200', 35 | delay: '0', 36 | payload: { test: true } 37 | }); 38 | }); 39 | }); 40 | 41 | describe('createNewRoute', () => { 42 | it('makes a POST request with the given route to the /route end point', async () => { 43 | const route = buildRoute(); 44 | 45 | await createNewRoute(route); 46 | 47 | expect(window.fetch).toHaveBeenCalled(); 48 | expect(window.fetch.mock.calls[0][1]).toEqual({ 49 | method: 'POST', 50 | headers: { 'Content-Type': 'application/json' }, 51 | body: JSON.stringify(route) 52 | }); 53 | }); 54 | }); 55 | 56 | describe('updateRoute', () => { 57 | it('makes a POST request with the given route to the /route end point', async () => { 58 | const route = buildRoute(); 59 | 60 | await updateRoute(route); 61 | 62 | expect(window.fetch).toHaveBeenCalled(); 63 | expect(window.fetch.mock.calls[0][1]).toEqual({ 64 | method: 'PUT', 65 | headers: { 'Content-Type': 'application/json' }, 66 | body: JSON.stringify(route) 67 | }); 68 | }); 69 | }); 70 | 71 | describe('deleteRoute', () => { 72 | it('makes a DELETE request with the given route to the /route end point', async () => { 73 | const route = buildRoute(); 74 | 75 | await deleteRoute(route); 76 | 77 | expect(window.fetch).toHaveBeenCalled(); 78 | expect(window.fetch.mock.calls[0][1]).toEqual({ 79 | method: 'DELETE', 80 | headers: { 'Content-Type': 'application/json' }, 81 | body: JSON.stringify(route) 82 | }); 83 | }); 84 | }); 85 | 86 | describe('updateSettings', () => { 87 | it('makes a POST request with the given route to the /settings end point', async () => { 88 | const route = buildRoute(); 89 | 90 | await updateSettings(route); 91 | 92 | expect(window.fetch).toHaveBeenCalled(); 93 | expect(window.fetch.mock.calls[0][1]).toEqual({ 94 | method: 'POST', 95 | headers: { 'Content-Type': 'application/json' }, 96 | body: JSON.stringify(route) 97 | }); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: off 5 | client: 6 | target: 90% 7 | flags: client 8 | server: 9 | target: 90% 10 | flags: server 11 | mockit-routes: 12 | target: 90% 13 | flags: mockit-routes 14 | 15 | flags: 16 | server: 17 | paths: 18 | - server/ 19 | client: 20 | paths: 21 | - client/ 22 | mockit-routes: 23 | paths: 24 | - mockit-routes/ 25 | tests: 26 | paths: 27 | - tests/ 28 | 29 | comment: 30 | layout: diff 31 | require_changes: yes 32 | -------------------------------------------------------------------------------- /configuration/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "features": { 4 | "chaosMonkey": false, 5 | "cors": true, 6 | "authentication": false, 7 | "groupedRoutes": false 8 | }, 9 | "authentication": { 10 | "username": "test", 11 | "password": "test" 12 | } 13 | }, 14 | "routes": [ 15 | { 16 | "id": "6f935aa8-b629-4247-ba51-f02347b06e95", 17 | "route": "/user", 18 | "httpMethod": "GET", 19 | "statusCode": "200", 20 | "delay": "0", 21 | "payload": { 22 | "name": "Jermain Spinka", 23 | "username": "Agnes71", 24 | "email": "Taurean.Huels55@hotmail.com", 25 | "address": { 26 | "street": "Kihn Inlet", 27 | "suite": "Apt. 784", 28 | "city": "South Doraside", 29 | "zipcode": "91828", 30 | "geo": { 31 | "lat": "-56.8426", 32 | "lng": "-122.9710" 33 | } 34 | }, 35 | "phone": "810-578-0341", 36 | "website": "jewell.name", 37 | "company": { 38 | "name": "Quitzon, Wilderman and VonRueden", 39 | "catchPhrase": "Realigned zero administration encoding", 40 | "bs": "robust incubate synergies" 41 | } 42 | }, 43 | "disabled": false, 44 | "headers": [ 45 | { 46 | "id": "b3f30451-3e1c-444f-9bfc-cb42441e9824", 47 | "header": "x-api-key", 48 | "value": "testing123" 49 | } 50 | ] 51 | }, 52 | { 53 | "id": "ec6166c8-ac50-4f2c-9d6f-b26ed677fe72", 54 | "route": "/users", 55 | "httpMethod": "GET", 56 | "statusCode": "200", 57 | "delay": "0", 58 | "payload": [ 59 | { 60 | "name": "Edison Littel", 61 | "username": "Tyrel.Funk24", 62 | "email": "Burnice38@yahoo.com", 63 | "address": { 64 | "street": "Christelle Drive", 65 | "suite": "Apt. 884", 66 | "city": "Hoegerport", 67 | "zipcode": "59938", 68 | "geo": { 69 | "lat": "-49.1240", 70 | "lng": "148.9288" 71 | } 72 | }, 73 | "phone": "(030) 225-3359 x582", 74 | "website": "priscilla.com", 75 | "company": { 76 | "name": "Reynolds Inc", 77 | "catchPhrase": "User-centric value-added productivity", 78 | "bs": "ubiquitous leverage paradigms" 79 | } 80 | }, 81 | { 82 | "name": "Garth Corwin", 83 | "username": "Hilton33", 84 | "email": "Alvera_Runte78@yahoo.com", 85 | "address": { 86 | "street": "Ervin Gardens", 87 | "suite": "Apt. 778", 88 | "city": "Lake Laurynburgh", 89 | "zipcode": "39378", 90 | "geo": { 91 | "lat": "77.6111", 92 | "lng": "124.8899" 93 | } 94 | }, 95 | "phone": "(477) 397-0658", 96 | "website": "selena.net", 97 | "company": { 98 | "name": "Schmeler LLC", 99 | "catchPhrase": "Adaptive global middleware", 100 | "bs": "interactive deploy bandwidth" 101 | } 102 | } 103 | ] 104 | }, 105 | { 106 | "id": "61b4aaa9-bbe9-4cfc-80ee-3d8528a218e6", 107 | "route": "/demo", 108 | "httpMethod": "GET", 109 | "statusCode": "200", 110 | "delay": "0", 111 | "payload": { 112 | "message": "hello" 113 | } 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mockit-routes: 4 | image: 'mockit-routes' 5 | build: 'mockit-routes' 6 | ports: 7 | - 3000:3000 8 | volumes: 9 | - ./configuration/routes.json:/usr/src/mockit-routes/configuration/routes.json 10 | mockit-server: 11 | image: 'mockit-server' 12 | build: 'server' 13 | ports: 14 | - 4000:4000 15 | volumes: 16 | - ./configuration/routes.json:/usr/src/mockit-server/configuration/routes.json 17 | mockit-client: 18 | image: 'mockit-client' 19 | build: 'client' 20 | ports: 21 | - 5000:3000 22 | environment: 23 | - REACT_APP_MOCKIT_SERVER_URL=http://localhost:3000 24 | - REACT_APP_MOCKIT_API_URL=http://localhost:4000 25 | volumes: 26 | - ./configuration/routes.json:/usr/src/mockit-client/src/config/routes.json 27 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyney123/mockit/e593e5d328ec8371c275e07d759cc81fe4e19ebb/images/demo.gif -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyney123/mockit/e593e5d328ec8371c275e07d759cc81fe4e19ebb/images/screenshot.png -------------------------------------------------------------------------------- /install-and-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dir=$(pwd) 4 | cd "$dir/mockit-routes" && npm install && npm test . 5 | cd "$dir/client" && npm install && CI=true npm test -- --coverage 6 | cd "$dir/server" && npm install && npm test 7 | -------------------------------------------------------------------------------- /mockit-routes/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /mockit-routes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11.4.0-alpine 2 | 3 | RUN mkdir -p /usr/src/mockit-routes 4 | WORKDIR /usr/src/mockit-routes 5 | 6 | COPY package.json . 7 | COPY package-lock.json . 8 | 9 | RUN npm ci 10 | 11 | COPY . . 12 | 13 | EXPOSE 3000 14 | 15 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /mockit-routes/configuration/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "features": { 4 | "chaosMonkey": false, 5 | "cors": true, 6 | "authentication": false 7 | }, 8 | "authentication": { 9 | "username": "test", 10 | "password": "test" 11 | } 12 | }, 13 | "routes": [ 14 | { 15 | "id": "b0e91a33-787c-4d54-8c58-10ac970e2865", 16 | "route": "/user", 17 | "httpMethod": "GET", 18 | "statusCode": "200", 19 | "delay": "0", 20 | "payload": { 21 | "name": "Ellen Emard", 22 | "username": "Mohamed.Pfeffer84", 23 | "email": "Chauncey14@yahoo.com", 24 | "address": { 25 | "street": "Durgan Locks", 26 | "suite": "Apt. 331", 27 | "city": "West Sid", 28 | "zipcode": "16439", 29 | "geo": { 30 | "lat": "-65.7730", 31 | "lng": "-140.2122" 32 | } 33 | }, 34 | "phone": "024.912.5659 x5359", 35 | "website": "osborne.net", 36 | "company": { 37 | "name": "Barrows - Jakubowski", 38 | "catchPhrase": "Programmable client-server parallelism", 39 | "bs": "integrated whiteboard e-markets" 40 | } 41 | }, 42 | "disabled": true 43 | }, 44 | { 45 | "id": "bd8893f7-68d5-4090-8ae2-20529191cd88", 46 | "route": "/random", 47 | "httpMethod": "GET", 48 | "statusCode": "200", 49 | "delay": "0", 50 | "headers": [ 51 | { 52 | "header": "x-testing", 53 | "value": "tester" 54 | }, 55 | { 56 | "header": "x-testing2", 57 | "value": "tester2" 58 | }, 59 | { 60 | "header": "x-testing3", 61 | "value": "tester3" 62 | } 63 | ], 64 | "payload": { 65 | "name": "Fanny Rath", 66 | "username": "Adell.Adams", 67 | "email": "Clair30@yahoo.com", 68 | "address": { 69 | "street": "Carroll Meadow", 70 | "suite": "Suite 114", 71 | "city": "West Astrid", 72 | "zipcode": "02752", 73 | "geo": { 74 | "lat": "-3.4480", 75 | "lng": "86.2320" 76 | } 77 | }, 78 | "phone": "527.926.2249 x635", 79 | "website": "evans.biz", 80 | "company": { 81 | "name": "Mills - Schinner", 82 | "catchPhrase": "Profound clear-thinking array", 83 | "bs": "compelling disintermediate mindshare" 84 | } 85 | }, 86 | "disabled": false 87 | }, 88 | { 89 | "id": "56477240-796b-4fa5-bfa2-d09d804e3d24", 90 | "route": "/newRoute", 91 | "httpMethod": "GET", 92 | "statusCode": "200", 93 | "delay": "1000", 94 | "payload": { 95 | "test": true 96 | } 97 | }, 98 | { 99 | "id": "686819df-9cd4-4814-a75a-39f65ea76b8e", 100 | "route": "/newRoute", 101 | "httpMethod": "GET", 102 | "statusCode": "200", 103 | "delay": "0", 104 | "payload": { 105 | "test": true 106 | } 107 | } 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /mockit-routes/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bail": true, 3 | "verbose": true, 4 | "watchPathIgnorePatterns": ["/configuration"], 5 | "coveragePathIgnorePatterns": [ 6 | "/node_modules/", 7 | "/configuration/", 8 | "package.json", 9 | "jest.config.json", 10 | "package-lock.json" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /mockit-routes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockit-mock-server", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "./node_modules/.bin/nodemon src/index.js", 8 | "test": "ENV=test jest --coverage --config jest.config.json", 9 | "test:watch": "ENV=test jest --no-cache --watch --notify --notifyMode=change --coverage --config jest.config.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "basic-auth": "^2.0.1", 16 | "chokidar": "^2.1.5", 17 | "cors": "^2.8.5", 18 | "express": "^4.16.4", 19 | "fs-extra": "^7.0.1", 20 | "nodemon": "^1.18.11" 21 | }, 22 | "devDependencies": { 23 | "jest": "^24.7.1", 24 | "supertest": "^4.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mockit-routes/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const app = express(); 5 | const cors = require('cors'); 6 | 7 | const port = process.env.PORT || 3000; 8 | 9 | const delayMiddleware = require('./middlewares/delay'); 10 | const chaosMonkeyMiddleware = require('./middlewares/chaos-monkey'); 11 | const basicAuth = require('./middlewares/basic-auth'); 12 | 13 | const data = fs.readJsonSync( 14 | path.resolve(__dirname, '../configuration/routes.json') 15 | ); 16 | const { 17 | routes, 18 | settings: { features: { cors: corsFeature } = {} } = {} 19 | } = data; 20 | 21 | app.use(basicAuth); 22 | app.use(delayMiddleware); 23 | app.use(chaosMonkeyMiddleware); 24 | 25 | if (corsFeature) { 26 | app.use(cors()); 27 | } 28 | 29 | app.disable('x-powered-by'); 30 | 31 | routes.forEach((route) => { 32 | const { 33 | route: path, 34 | statusCode, 35 | payload, 36 | disabled = false, 37 | httpMethod = 'GET', 38 | headers = [] 39 | } = route; 40 | 41 | const method = httpMethod.toLowerCase(); 42 | 43 | if (!disabled) { 44 | app[method](path, (req, res) => { 45 | headers.forEach(({ header, value } = {}) => { 46 | res.set(header, value); 47 | }); 48 | res.status(statusCode).send(payload); 49 | }); 50 | } 51 | }); 52 | 53 | if (process.env.ENV !== 'test') { 54 | server = app.listen(port, () => 55 | console.log(`MockIt app listening on port ${port}!`) 56 | ); 57 | } 58 | 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/basic-auth/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const auth = require('basic-auth'); 4 | const data = fs.readJsonSync( 5 | path.resolve(__dirname, '../../../configuration/routes.json') 6 | ); 7 | const { settings } = data; 8 | const { 9 | features: { authentication } = {}, 10 | authentication: authenticationSettings = {} 11 | } = settings; 12 | 13 | const isAuthenticated = (name, pass) => { 14 | const { username, password } = authenticationSettings; 15 | 16 | return name === username && pass === password; 17 | }; 18 | 19 | module.exports = (req, res, next) => { 20 | if (!authentication) { 21 | return next(); 22 | } 23 | 24 | const { name, pass } = auth(req) || {}; 25 | 26 | if ((!name && !pass) || !isAuthenticated(name, pass)) { 27 | return res.status(401).send('Access denied'); 28 | } 29 | 30 | next(); 31 | }; 32 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/basic-auth/spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | const buildExampleConfig = () => { 6 | return { 7 | settings: { 8 | features: { 9 | chaosMonkey: false, 10 | cors: true, 11 | authentication: true 12 | }, 13 | authentication: { 14 | username: 'test', 15 | password: 'test' 16 | } 17 | }, 18 | routes: [ 19 | { 20 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2865', 21 | route: '/getExample', 22 | httpMethod: 'GET', 23 | statusCode: '200', 24 | delay: '0', 25 | payload: { 26 | test: true 27 | }, 28 | disabled: false 29 | }, 30 | { 31 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2865', 32 | route: '/postExample', 33 | httpMethod: 'POST', 34 | statusCode: '200', 35 | delay: '0', 36 | payload: { 37 | test: true 38 | }, 39 | disabled: false 40 | } 41 | ] 42 | }; 43 | }; 44 | 45 | const fsExtra = jest.mock( 46 | 'fs-extra', 47 | jest.fn(() => { 48 | return { 49 | readJsonSync: jest.fn(() => buildExampleConfig()) 50 | }; 51 | }) 52 | ); 53 | 54 | const app = require('../../'); 55 | 56 | describe('Basic Auth Middleware', () => { 57 | describe('routes', () => { 58 | it('returns 401 when no Authentication is sent', async () => { 59 | await request(app).get('/getExample').expect(401); 60 | }); 61 | it('returns 401 when Authentication fails', async () => { 62 | await request(app).get('/getExample').auth('test', 'random').expect(401); 63 | }); 64 | it('returns the expected response when the correct Authenication header is sent', async () => { 65 | await request(app).post('/postExample').auth('test', 'test').expect(200); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/chaos-monkey/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { hitByMonkey } = require('./util'); 4 | const data = fs.readJsonSync( 5 | path.resolve(__dirname, '../../../configuration/routes.json') 6 | ); 7 | const { settings } = data; 8 | const { features: { chaosMonkey } = {} } = settings; 9 | 10 | module.exports = (req, res, next) => { 11 | if (!chaosMonkey) { 12 | return next(); 13 | } 14 | 15 | // send random message back 16 | if (hitByMonkey()) { 17 | return res.send('Monkey left you a 🍌...'); 18 | } 19 | 20 | // send back server error 21 | if (hitByMonkey()) { 22 | return res.send(500); 23 | } 24 | 25 | // timeout the request, send nothing back 26 | if (hitByMonkey()) { 27 | return null; 28 | } 29 | 30 | // well done you passed. Continue 31 | next(); 32 | }; 33 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/chaos-monkey/spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | jest.mock('./util', () => { 6 | return { 7 | hitByMonkey: jest.fn(() => true) 8 | }; 9 | }); 10 | const utils = require('./util'); 11 | 12 | const buildExampleConfig = () => { 13 | return { 14 | settings: { 15 | features: { 16 | chaosMonkey: true, 17 | cors: true, 18 | authentication: false 19 | } 20 | }, 21 | routes: [ 22 | { 23 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2865', 24 | route: '/getExample', 25 | httpMethod: 'GET', 26 | statusCode: '200', 27 | delay: '0', 28 | payload: { 29 | test: true 30 | }, 31 | disabled: false 32 | }, 33 | { 34 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2865', 35 | route: '/postExample', 36 | httpMethod: 'POST', 37 | statusCode: '200', 38 | delay: '0', 39 | payload: { 40 | test: true 41 | }, 42 | disabled: false 43 | } 44 | ] 45 | }; 46 | }; 47 | 48 | const fsExtra = jest.mock( 49 | 'fs-extra', 50 | jest.fn(() => { 51 | return { 52 | readJsonSync: jest.fn(() => buildExampleConfig()) 53 | }; 54 | }) 55 | ); 56 | 57 | const app = require('../../'); 58 | 59 | describe('Chaos Monkey', () => { 60 | describe('routes', () => { 61 | it('monkey returns a banana if the first check in the middleware fails', async () => { 62 | utils.hitByMonkey.mockImplementationOnce(() => true); 63 | 64 | await request(app) 65 | .get('/getExample') 66 | .expect(200, 'Monkey left you a 🍌...'); 67 | }); 68 | 69 | it('monkey returns a 500 status code if the second check in the middleware fails', async () => { 70 | utils.hitByMonkey 71 | .mockImplementationOnce(() => false) 72 | .mockImplementationOnce(() => true); 73 | 74 | await request(app).get('/getExample').expect(500); 75 | }); 76 | 77 | it('monkey returns expected response if all checks pass', async () => { 78 | utils.hitByMonkey 79 | .mockImplementationOnce(() => false) 80 | .mockImplementationOnce(() => false) 81 | .mockImplementationOnce(() => false); 82 | 83 | await request(app).get('/getExample').expect(200); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/chaos-monkey/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hitByMonkey: () => Math.random() > 0.5 3 | }; 4 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/chaos-monkey/util.spec.js: -------------------------------------------------------------------------------- 1 | const { hitByMonkey } = require('./util'); 2 | 3 | describe('Utils', () => { 4 | describe('hitByMonkey', () => { 5 | it('returns true when Math.random returns greater than 0.5', async () => { 6 | const mockMath = Object.create(global.Math); 7 | mockMath.random = () => 0.6; 8 | global.Math = mockMath; 9 | const result = hitByMonkey(); 10 | expect(result).toEqual(true); 11 | }); 12 | it('returns false when Math.random returns less than 0.5', async () => { 13 | const mockMath = Object.create(global.Math); 14 | mockMath.random = () => 0.4; 15 | global.Math = mockMath; 16 | const result = hitByMonkey(); 17 | expect(result).toEqual(false); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /mockit-routes/src/middlewares/delay/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const data = fs.readJsonSync( 4 | path.resolve(__dirname, '../../../configuration/routes.json') 5 | ); 6 | const { routes } = data; 7 | 8 | const findRoute = (path) => { 9 | return routes.find(({ route } = {}) => { 10 | return route === path; 11 | }); 12 | }; 13 | 14 | module.exports = (req, res, next) => { 15 | const { delay = 0 } = findRoute(req.url) || {}; 16 | 17 | if (delay > 0) { 18 | return setTimeout(next, delay); 19 | } 20 | 21 | next(); 22 | }; 23 | -------------------------------------------------------------------------------- /mockit-routes/src/spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | const exampleConfig = { 6 | settings: { 7 | features: { 8 | chaosMonkey: false, 9 | cors: true, 10 | authentication: false 11 | }, 12 | authentication: { 13 | username: 'test', 14 | password: 'test' 15 | } 16 | }, 17 | routes: [ 18 | { 19 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2865', 20 | route: '/getExample', 21 | httpMethod: 'GET', 22 | statusCode: '200', 23 | delay: '0', 24 | payload: { 25 | test: true 26 | }, 27 | disabled: false 28 | }, 29 | { 30 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2861', 31 | route: '/postExample', 32 | httpMethod: 'POST', 33 | statusCode: '200', 34 | delay: '0', 35 | payload: { 36 | test: true 37 | }, 38 | disabled: false 39 | }, 40 | { 41 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2862', 42 | route: '/putExample', 43 | httpMethod: 'PUT', 44 | statusCode: '200', 45 | delay: '0', 46 | payload: { 47 | test: true 48 | }, 49 | disabled: false 50 | }, 51 | { 52 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2863', 53 | route: '/deleteExample', 54 | httpMethod: 'DELETE', 55 | statusCode: '200', 56 | delay: '0', 57 | payload: { 58 | test: true 59 | }, 60 | disabled: false 61 | }, 62 | { 63 | id: 'b0e91a33-787c-4d54-8c58-10ac970e2865', 64 | route: '/test1', 65 | httpMethod: 'GET', 66 | statusCode: '200', 67 | delay: '0', 68 | payload: { 69 | test: true 70 | }, 71 | disabled: false 72 | }, 73 | { 74 | id: 'bd8893f7-68d5-4090-8ae2-20529191cd88', 75 | route: '/test2', 76 | httpMethod: 'POST', 77 | statusCode: '200', 78 | delay: '0', 79 | payload: { 80 | test: true 81 | }, 82 | disabled: false 83 | }, 84 | { 85 | route: '/disabledExample', 86 | httpMethod: 'GET', 87 | statusCode: '200', 88 | delay: '0', 89 | payload: { 90 | test: true 91 | }, 92 | disabled: true 93 | }, 94 | { 95 | route: '/delayExample', 96 | httpMethod: 'GET', 97 | statusCode: '200', 98 | delay: '2000', 99 | payload: { 100 | test: true 101 | }, 102 | disabled: false 103 | }, 104 | { 105 | route: '/500Example', 106 | httpMethod: 'GET', 107 | statusCode: '500', 108 | delay: '0', 109 | payload: { 110 | test: true 111 | }, 112 | disabled: false 113 | }, 114 | { 115 | route: '/headersExample', 116 | httpMethod: 'GET', 117 | statusCode: '200', 118 | delay: '0', 119 | headers: [ 120 | { 121 | header: 'Content-Type', 122 | value: 'application/json' 123 | } 124 | ], 125 | disabled: false 126 | } 127 | ] 128 | }; 129 | 130 | const fsExtra = jest.mock( 131 | 'fs-extra', 132 | jest.fn(() => { 133 | return { 134 | readJsonSync: jest.fn(() => exampleConfig) 135 | }; 136 | }) 137 | ); 138 | 139 | const app = require('./'); 140 | 141 | describe('Mockit Routes', () => { 142 | describe('dynamic routes', () => { 143 | describe('http methods', () => { 144 | it('when a route is configured with `GET` set as the httpMethod then that route can only be accessed through GET', async () => { 145 | await request(app).get('/getExample').expect(200, { test: true }); 146 | }); 147 | it('when a route is configured with `POST` set as the httpMethod then that route can only be accessed through POST', async () => { 148 | await request(app).post('/postExample').expect(200, { test: true }); 149 | }); 150 | it('when a route is configured with `PUT` set as the httpMethod then that route can only be accessed through PUT', async () => { 151 | await request(app).put('/putExample').expect(200, { test: true }); 152 | }); 153 | it('when a route is configured with `DELETE` set as the httpMethod then that route can only be accessed through DELETE', async () => { 154 | await request(app).del('/deleteExample').expect(200, { test: true }); 155 | }); 156 | }); 157 | 158 | describe('headers', () => { 159 | it('when a route is configured with headers the headers are sent back in the response', async () => { 160 | await request(app) 161 | .get('/headersExample') 162 | .expect('Content-Type', 'application/json; charset=utf-8'); 163 | }); 164 | }); 165 | 166 | describe('disabled routes', () => { 167 | it('when a route is marked as disabled in the configuration file, the route cannot be accessed and returns a 404', async () => { 168 | await request(app).get('/disabledExample').expect(404); 169 | }); 170 | }); 171 | 172 | describe('delayed routes', () => { 173 | it('when a route has a delay configured on it, the response will come back after the delay has fulfilled', async () => { 174 | await request(app).get('/delayExample').expect(200, { test: true }); 175 | }); 176 | }); 177 | 178 | describe('status codes', () => { 179 | it('when a route has a status code configured on it, that status code is returned', async () => { 180 | await request(app).get('/500Example').expect(500); 181 | }); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "prettier": { 6 | "version": "2.2.0", 7 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.0.tgz", 8 | "integrity": "sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw==" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Directory for instrumented libs generated by jscoverage/JSCover 10 | lib-cov 11 | 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | 15 | # nyc test coverage 16 | .nyc_output 17 | 18 | # Compiled binary addons (https://nodejs.org/api/addons.html) 19 | build/Release 20 | 21 | # Dependency directories 22 | node_modules/ 23 | 24 | # dotenv environment variables file 25 | .env 26 | .env.test 27 | -------------------------------------------------------------------------------- /server/.secrets-baseline: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": { 3 | "files": null, 4 | "lines": null 5 | }, 6 | "generated_at": "2019-07-19T05:29:22Z", 7 | "plugins_used": [ 8 | { 9 | "name": "AWSKeyDetector" 10 | }, 11 | { 12 | "base64_limit": 4.5, 13 | "name": "Base64HighEntropyString" 14 | }, 15 | { 16 | "name": "BasicAuthDetector" 17 | }, 18 | { 19 | "hex_limit": 3, 20 | "name": "HexHighEntropyString" 21 | }, 22 | { 23 | "name": "KeywordDetector" 24 | }, 25 | { 26 | "name": "PrivateKeyDetector" 27 | }, 28 | { 29 | "name": "SlackDetector" 30 | } 31 | ], 32 | "results": {}, 33 | "version": "0.12.2" 34 | } 35 | -------------------------------------------------------------------------------- /server/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | install: 6 | - npm install -g codecov 7 | - npm install 8 | script: 9 | - npm test 10 | - codecov 11 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11.4.0-alpine 2 | 3 | RUN mkdir -p /usr/src/mockit-server 4 | WORKDIR /usr/src/mockit-server 5 | 6 | COPY package.json . 7 | COPY package-lock.json . 8 | 9 | RUN npm ci 10 | 11 | COPY . . 12 | 13 | EXPOSE 4000 14 | 15 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /server/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Fail the status if coverage drops by >= 5% 6 | threshold: 5 7 | patch: 8 | default: 9 | threshold: 5 10 | 11 | comment: 12 | layout: diff 13 | require_changes: yes 14 | -------------------------------------------------------------------------------- /server/configuration/routes.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyney123/mockit/e593e5d328ec8371c275e07d759cc81fe4e19ebb/server/configuration/routes.json -------------------------------------------------------------------------------- /server/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bail": true, 3 | "verbose": true, 4 | "watchPathIgnorePatterns": ["/configuration"], 5 | "coveragePathIgnorePatterns": [ 6 | "/node_modules/", 7 | "/configuration/", 8 | "package.json", 9 | "jest.config.json", 10 | "package-lock.json" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /server/license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) David Boyne 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockit-api", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index", 8 | "test": "ENV=test jest --coverage --config jest.config.json", 9 | "test:watch": "ENV=test jest --no-cache --watch --notify --notifyMode=change --coverage --config jest.config.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "body-parser": "^1.18.3", 16 | "cors": "^2.8.5", 17 | "detect-secrets": "^1.0.3", 18 | "express": "^4.16.4", 19 | "fs-extra": "^7.0.1", 20 | "husky": "^3.0.0", 21 | "lint-staged": "^9.2.0", 22 | "uuid": "^3.3.2" 23 | }, 24 | "devDependencies": { 25 | "jest": "^24.3.1", 26 | "supertest": "^3.4.2" 27 | }, 28 | "husky": { 29 | "hooks": { 30 | "pre-commit": "lint-staged --relative" 31 | } 32 | }, 33 | "lint-staged": { 34 | "**/*.js": [ 35 | "detect-secrets-launcher --baseline .secrets-baseline" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const pullAt = require('lodash/pullAt'); 3 | const app = express(); 4 | const cors = require('cors'); 5 | const bodyParser = require('body-parser'); 6 | const fs = require('fs-extra'); 7 | const path = require('path'); 8 | const uuid = require('uuid/v4'); 9 | 10 | const port = process.env.PORT || 4000; 11 | 12 | const { getConfig, writeConfig } = require('./utils/config-helper'); 13 | 14 | app.use(cors()); 15 | app.use(bodyParser.json()); 16 | 17 | const requiredPropertiesForRoute = [ 18 | 'route', 19 | 'httpMethod', 20 | 'statusCode', 21 | 'delay', 22 | 'payload' 23 | ]; 24 | const isRouteValid = (route, additionalRequiredProperties = []) => { 25 | const required = requiredPropertiesForRoute.concat( 26 | additionalRequiredProperties 27 | ); 28 | return required.every((key) => { 29 | return route[key] !== undefined; 30 | }); 31 | }; 32 | 33 | app.post('/route', async (req, res) => { 34 | const payload = req.body; 35 | const config = await getConfig(); 36 | 37 | const route = { 38 | id: uuid(), 39 | ...payload 40 | }; 41 | 42 | if (!isRouteValid(route)) { 43 | return res.sendStatus(400); 44 | } 45 | 46 | config.routes.push(route); 47 | 48 | await writeConfig(config); 49 | 50 | res.sendStatus(201); 51 | }); 52 | 53 | app.put('/route', async (req, res) => { 54 | const route = req.body; 55 | 56 | if (!isRouteValid(route, ['id'])) { 57 | return res.sendStatus(400); 58 | } 59 | 60 | const config = await getConfig(); 61 | 62 | const routeIndex = config.routes.findIndex(({ id } = {}) => id === route.id); 63 | config.routes[routeIndex] = route; 64 | 65 | await writeConfig(config); 66 | 67 | res.sendStatus(204); 68 | }); 69 | 70 | app.delete('/route', async (req, res) => { 71 | const payload = req.body; 72 | const config = await getConfig(); 73 | 74 | const { id } = payload; 75 | 76 | if (!id) { 77 | return res.sendStatus(400); 78 | } 79 | 80 | const routeIndex = config.routes.findIndex( 81 | ({ id: routeId } = {}) => routeId === id 82 | ); 83 | 84 | pullAt(config.routes, routeIndex); 85 | 86 | await writeConfig(config); 87 | return res.sendStatus(204); 88 | }); 89 | 90 | app.post('/settings', async (req, res) => { 91 | const payload = req.body; 92 | const config = await getConfig(); 93 | 94 | const { settings = {} } = config; 95 | 96 | const newSettings = { 97 | ...settings, 98 | ...payload 99 | }; 100 | 101 | config['settings'] = newSettings; 102 | 103 | await writeConfig(config); 104 | return res.sendStatus(204); 105 | }); 106 | 107 | if (process.env.ENV !== 'test') { 108 | server = app.listen(port, () => 109 | console.log(`Example app listening on port ${port}!`) 110 | ); 111 | } 112 | 113 | module.exports = app; 114 | -------------------------------------------------------------------------------- /server/src/spec.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | 5 | const app = require('./'); 6 | 7 | const generateRoute = (overrides) => ({ 8 | route: '/test', 9 | httpMethod: 'GET', 10 | statusCode: '200', 11 | delay: '0', 12 | payload: { 13 | test: true 14 | }, 15 | ...overrides 16 | }); 17 | 18 | const writeConfiguration = async (config = { routes: [] }) => { 19 | return await fs.writeJson( 20 | path.resolve(__dirname, '../configuration/routes.json'), 21 | config 22 | ); 23 | }; 24 | 25 | const getConfiguration = () => { 26 | return fs.readJsonSync( 27 | path.resolve(__dirname, '../configuration/routes.json') 28 | ); 29 | }; 30 | 31 | const clearConfiguration = async () => { 32 | await writeConfiguration(); 33 | }; 34 | 35 | describe('Route API', () => { 36 | beforeEach(() => { 37 | clearConfiguration(); 38 | }); 39 | 40 | describe('/ (POST)', () => { 41 | ['route', 'httpMethod', 'statusCode', 'delay', 'payload'].forEach( 42 | (requiredProperty) => { 43 | it(`returns a 400 status code when ${requiredProperty} property is missing from the payload`, async () => { 44 | const payload = generateRoute(); 45 | 46 | delete payload[requiredProperty]; 47 | 48 | await request(app) 49 | .post('/route') 50 | .send({ ...payload }) 51 | .set('Accept', 'application/json') 52 | .expect(400); 53 | }); 54 | } 55 | ); 56 | 57 | it('returns a 201 and writes the given route to the configuration file with a generated ID', async () => { 58 | const route = generateRoute(); 59 | 60 | await request(app) 61 | .post('/route') 62 | .send({ ...route }) 63 | .set('Accept', 'application/json') 64 | .expect(201); 65 | 66 | const config = getConfiguration(); 67 | 68 | expect(config.routes).toHaveLength(1); 69 | expect(config.routes[0].id).toBeDefined(); 70 | expect(config.routes[0]).toEqual(expect.objectContaining(route)); 71 | }); 72 | }); 73 | 74 | describe('/ (PUT)', () => { 75 | ['route', 'httpMethod', 'statusCode', 'delay', 'payload', 'id'].forEach( 76 | (requiredProperty) => { 77 | it(`returns a 400 status code when ${requiredProperty} property is missing from the payload`, async () => { 78 | const payload = generateRoute({ id: '1234' }); 79 | 80 | delete payload[requiredProperty]; 81 | 82 | await request(app) 83 | .put('/route') 84 | .send({ ...payload }) 85 | .set('Accept', 'application/json') 86 | .expect(400); 87 | }); 88 | } 89 | ); 90 | 91 | it('returns a 200 and updates the given route to the configuration file with a generated ID', async () => { 92 | const originalRoute = generateRoute({ id: '1234' }); 93 | 94 | await writeConfiguration({ 95 | routes: [generateRoute(), generateRoute(), originalRoute] 96 | }); 97 | 98 | // make changes to the route 99 | const updatedRoute = { 100 | ...originalRoute, 101 | route: '/change', 102 | payload: 'changeToPayload', 103 | delay: 5000 104 | }; 105 | 106 | await request(app) 107 | .put('/route') 108 | .send({ ...updatedRoute }) 109 | .set('Accept', 'application/json') 110 | .expect(204); 111 | 112 | const config = getConfiguration(); 113 | 114 | expect(config.routes).toHaveLength(3); 115 | expect(config.routes[2]).toEqual(expect.objectContaining(updatedRoute)); 116 | }); 117 | }); 118 | 119 | describe('/ (DELETE)', () => { 120 | ['id'].forEach((requiredProperty) => { 121 | it(`returns a 400 status code when ${requiredProperty} property is missing from the payload`, async () => { 122 | const payload = generateRoute({ id: '1234' }); 123 | 124 | delete payload[requiredProperty]; 125 | 126 | await request(app) 127 | .delete('/route') 128 | .send({ ...payload }) 129 | .set('Accept', 'application/json') 130 | .expect(400); 131 | }); 132 | }); 133 | 134 | it('returns a 200 and deletes the given route', async () => { 135 | const route = generateRoute({ id: '1234' }); 136 | 137 | await writeConfiguration({ 138 | routes: [generateRoute(), generateRoute(), route] 139 | }); 140 | 141 | await request(app) 142 | .delete('/route') 143 | .send({ id: '1234' }) 144 | .set('Accept', 'application/json') 145 | .expect(204); 146 | 147 | const config = getConfiguration(); 148 | 149 | expect(config.routes).toHaveLength(2); 150 | }); 151 | }); 152 | 153 | describe('/settings', () => { 154 | it('returns a 204 and updated the settings in the configuration with the settings provided in the payload', async () => { 155 | await writeConfiguration({ settings: { test: true } }); 156 | 157 | await request(app) 158 | .post('/settings') 159 | .send({ newSetting: true }) 160 | .set('Accept', 'application/json') 161 | .expect(204); 162 | 163 | const config = getConfiguration(); 164 | 165 | expect(config).toEqual({ 166 | settings: { 167 | newSetting: true, 168 | test: true 169 | } 170 | }); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /server/src/utils/config-helper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const getConfig = async () => { 5 | return await fs.readJson( 6 | path.resolve(__dirname, '../../configuration/routes.json') 7 | ); 8 | }; 9 | 10 | const writeConfig = async (config) => { 11 | fs.writeJson( 12 | path.resolve(__dirname, '../../configuration/routes.json'), 13 | config, 14 | { 15 | spaces: 4 16 | } 17 | ); 18 | }; 19 | 20 | module.exports = { 21 | getConfig, 22 | writeConfig 23 | }; 24 | --------------------------------------------------------------------------------