├── .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 | [](https://travis-ci.org/boyney123/mockit)
9 | [](https://codecov.io/gh/boyney123/mockit)
10 | [](https://app.netlify.com/sites/mockit/deploys)
11 | [![MIT License][license-badge]][license]
12 | [![PRs Welcome][prs-badge]][prs]
13 | [](#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 |
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 |
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 | You need to enable JavaScript to run this app.
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 |
34 |
35 |
MockIt
36 |
37 |
53 |
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 |
21 |
22 |
23 |
28 | Delete
29 |
30 |
31 | Cancel
32 |
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 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 | Edit
55 |
56 |
61 | Delete
62 |
63 |
64 |
65 |
66 |
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 |
114 |
115 |
116 |
117 |
118 | Route
119 |
120 |
121 | updateRoute(`/${e.currentTarget.value}`)}
128 | />
129 | /
130 |
131 |
132 |
133 |
134 |
135 | HTTP Method
136 |
137 |
138 |
139 | updateHttpMethod(e.currentTarget.value)}
143 | >
144 | {HTTP_METHOD_LIST.map((method) => (
145 |
146 | {method}
147 |
148 | ))}
149 |
150 |
151 |
152 |
153 |
154 |
Status Code
155 |
156 |
157 | updateStatusCode(e.currentTarget.value)}
161 | >
162 |
163 | {statusCodeStartingWith('2').map((routeStatusCode) => (
164 |
165 | {routeStatusCode}
166 |
167 | ))}
168 |
169 |
170 | {statusCodeStartingWith('4').map((routeStatusCode) => (
171 |
172 | {routeStatusCode}
173 |
174 | ))}
175 |
176 |
177 | {statusCodeStartingWith('5').map((routeStatusCode) => (
178 |
179 | {routeStatusCode}
180 |
181 | ))}
182 |
183 |
184 |
185 |
186 |
187 |
188 |
Delay
189 |
190 |
191 | updateDelay(e.currentTarget.value)}
195 | >
196 | 0
197 | 250
198 | 500
199 | 1000
200 | 1500
201 | 2000
202 | 5000
203 |
204 |
205 |
206 |
207 |
208 |
209 |
Response
210 |
226 |
227 |
228 |
229 |
230 | Response Headers (optional)
231 |
232 | {headers.length === 0 && No headers added. }
233 | {headers.map((header, index) => {
234 | return (
235 |
240 | );
241 | })}
242 |
247 | Add Header
248 |
249 |
250 |
251 |
252 | Settings
253 |
254 | updateDisabled(e.target.checked)}
260 | />
261 | Disable Route
262 |
263 |
264 |
265 |
266 |
267 |
272 | Save changes
273 |
274 |
279 | Cancel
280 |
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 |
44 |
45 |
46 |
Basic Authentication
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 |
65 | Enable Basic Authentication
66 |
67 |
68 |
69 |
70 |
71 |
CORS
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 | Enable CORS
89 |
90 |
91 |
92 |
93 |
Chaos Monkey
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 | Enable Monkey 🙊
109 |
110 |
111 |
112 |
113 |
Group routes
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 |
131 | Enable Grouped Routes
132 |
133 |
134 |
135 |
136 |
137 |
138 |
143 | Save
144 |
145 |
146 | Cancel
147 |
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 |
--------------------------------------------------------------------------------