From 8b26c4be9443614407fca1d9f20a9ea69a83c8d9 Mon Sep 17 00:00:00 2001 From: David Boyne Date: Thu, 23 May 2019 14:40:05 +0100 Subject: [PATCH] #4 - Added the ability to add headers in the routes --- client/src/components/HeaderInput/index.js | 31 ++++ client/src/components/HeaderInput/spec.js | 55 ++++++ client/src/components/RouteModal/index.js | 68 +++---- client/src/components/RouteModal/spec.js | 54 +++++- client/src/config/routes.json | 2 + configuration/routes.json | 204 +++++++++++---------- mockit-routes/src/index.js | 5 +- mockit-routes/src/spec.js | 21 +++ 8 files changed, 299 insertions(+), 141 deletions(-) create mode 100644 client/src/components/HeaderInput/index.js create mode 100644 client/src/components/HeaderInput/spec.js diff --git a/client/src/components/HeaderInput/index.js b/client/src/components/HeaderInput/index.js new file mode 100644 index 0000000..6a37ae2 --- /dev/null +++ b/client/src/components/HeaderInput/index.js @@ -0,0 +1,31 @@ +import React, { useState, useEffect } from "react"; +import uuid from "uuid/v4"; + +export default function({ index, data = {}, onBlur = () => {}, onRemove = () => {} } = {}) { + const { id = uuid(), header: initialHeader, value: initialValue } = data; + + const [header, setHeader] = useState(initialHeader); + const [value, setValue] = useState(initialValue); + + const update = (field, inputValue) => { + field === "header" ? setHeader(inputValue) : setValue(inputValue); + }; + + useEffect(() => { + if (header && value) onBlur({ id, header, value }); + }, [header, value]); + + return ( +
+
+ update("header", e.target.value)} /> +
+
+ update("value", e.target.value)} /> +
+
onRemove(id)}> + +
+
+ ); +} diff --git a/client/src/components/HeaderInput/spec.js b/client/src/components/HeaderInput/spec.js new file mode 100644 index 0000000..ef53920 --- /dev/null +++ b/client/src/components/HeaderInput/spec.js @@ -0,0 +1,55 @@ +// __tests__/fetch.test.js +import React from "react"; +import { render, fireEvent, cleanup } from "react-testing-library"; +import "jest-dom/extend-expect"; +import HeaderInput from "./"; + +afterEach(cleanup); + +describe("DoubleInput", () => { + describe("renders", () => { + it("two inputs one for the header and one for the value", () => { + const { getByPlaceholderText } = render(); + expect(getByPlaceholderText("header")).toBeVisible(); + expect(getByPlaceholderText("value")).toBeVisible(); + }); + + it('two inputs are rendered with the given "header" and "value" data when given to the component', () => { + const { getByPlaceholderText } = render(); + expect(getByPlaceholderText("header").value).toEqual("Content-Type"); + expect(getByPlaceholderText("value").value).toEqual("application/json"); + }); + }); + + describe("props: events", () => { + it('onBlur is called when both "header" and "value" have been entered', () => { + const spy = jest.fn(); + const { getByPlaceholderText } = render(); + fireEvent.change(getByPlaceholderText("header"), { target: { value: "Content-Type" } }); + fireEvent.change(getByPlaceholderText("value"), { target: { value: "application/json" } }); + expect(spy).toHaveBeenCalled(); + }); + + it('onBlur is not called when "header" value is set but "value" is missing', () => { + const spy = jest.fn(); + const { getByPlaceholderText } = render(); + fireEvent.change(getByPlaceholderText("header"), { target: { value: "Content-Type" } }); + expect(spy).not.toHaveBeenCalled(); + }); + + it('onBlur is not called when "value" is set but the "header" value is not', () => { + const spy = jest.fn(); + const { getByPlaceholderText } = render(); + fireEvent.change(getByPlaceholderText("value"), { target: { value: "application/json" } }); + expect(spy).not.toHaveBeenCalled(); + }); + + it("onRemove is called with the headers id when the user clicks on the remove icon", () => { + const spy = jest.fn(); + const data = { id: 1, header: "Content-Type", value: "application/json" }; + const { getByLabelText } = render(); + fireEvent.click(getByLabelText("remove-header")); + expect(spy).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/client/src/components/RouteModal/index.js b/client/src/components/RouteModal/index.js index dc3adfa..c67cda7 100644 --- a/client/src/components/RouteModal/index.js +++ b/client/src/components/RouteModal/index.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import JSONInput from "react-json-editor-ajrm"; +import HeaderInput from "../HeaderInput"; import { HttpMethods, StatusCodes } from "../../utils/consts"; import { updateRoute as updateRouteRequest, createNewRoute } from "../../utils/routes-api"; import faker from "faker"; @@ -8,35 +9,9 @@ import uuid from "uuid/v4"; const HTTP_METHOD_LIST = [HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT, HttpMethods.DELETE]; const STATUS_CODES = [StatusCodes.OK, StatusCodes.CREATED, StatusCodes.NO_CONTENT, StatusCodes.BAD_REQUEST, StatusCodes.FORBIDDEN, StatusCodes.INTERNAL_SERVER_ERROR]; -const HeaderInput = function({ index, header: initialHeader, value: initialValue, onBlur = () => {}, onChange = () => {}, onRemove = () => {} } = {}) { - const [header, setHeader] = useState(initialHeader); - const [value, setValue] = useState(initialValue); - - const update = (field, inputValue) => { - field === "header" ? setHeader(inputValue) : setValue(inputValue); - }; - - useEffect(() => { - if (header && value) onBlur({ index, header, value }); - }, [header, value]); - - return ( -
-
- update("header", e.target.value)} /> -
-
- update("value", e.target.value)} /> -
-
- -
-
- ); -}; - /** * add header adds some input fields + * * Clicking X removes the field from the array.... */ @@ -55,16 +30,30 @@ const Modal = function(props) { const modalTitle = isNewRoute ? "Add Route" : "Edit Route"; - const setHeader = header => { - const { index } = header; - const newHeaders = headers.concat([]); - newHeaders[index] = header; - console.log("header", headers); - updateHeaders(newHeaders); + const setHeader = updatedHeader => { + const { id } = updatedHeader; + + const updatedHeaders = headers.map((header, index) => { + if (id !== header.id) return header; + return { + ...header, + ...updatedHeader + }; + }); + + updateHeaders(updatedHeaders); + }; + + const addNewHeader = () => updateHeaders(headers.concat([{ id: uuid(), header: "", value: "" }])); + const removeHeader = route => { + const filteredHeaders = headers.filter(({ id } = {}) => id !== route); + updateHeaders(filteredHeaders); }; const saveChanges = async () => { try { + const cleanedHeaders = headers.filter(({ header, value }) => header !== "" && value !== ""); + const data = { ...editedRoute, route, @@ -73,7 +62,7 @@ const Modal = function(props) { delay, payload, disabled, - headers + headers: cleanedHeaders }; isNewRoute ? await createNewRoute(data) : await updateRouteRequest(data); @@ -82,8 +71,6 @@ const Modal = function(props) { } }; - console.log("headers", headers); - return ( <>
@@ -162,11 +149,14 @@ const Modal = function(props) {

- + + {headers.length === 0 && No headers added.} {headers.map((header, index) => { - return ; + return ; })} -
diff --git a/client/src/components/RouteModal/spec.js b/client/src/components/RouteModal/spec.js index ea2cb6a..1f264b3 100644 --- a/client/src/components/RouteModal/spec.js +++ b/client/src/components/RouteModal/spec.js @@ -14,7 +14,8 @@ const buildRoute = () => { delay: "0", disabled: false, payload: { test: true }, - statusCode: "200" + statusCode: "200", + headers: [{ id: 1, header: "Content-Type", value: "application/json" }, { id: 2, header: "x-api-key", value: "test" }] }; }; @@ -97,6 +98,20 @@ describe("Route Modal", () => { expect(getByLabelText("route-randomly-generate-data")).toBeVisible(); }); + it("with a list of headers when the route has headers", () => { + const route = buildRoute(); + const { getAllByLabelText } = render(); + const headers = getAllByLabelText("header"); + expect(headers).toHaveLength(2); + }); + + it('a message saying "No headers added" when no headers are in the route', () => { + const route = buildRoute(); + delete route["headers"]; + const { getByLabelText } = render(); + expect(getByLabelText("no-headers-message")).toBeVisible(); + }); + it("with a checkbox to disable the route", () => { const route = buildRoute(); const { getByLabelText } = render(); @@ -111,6 +126,43 @@ describe("Route Modal", () => { }); }); + describe("headers", () => { + it("when clicking `Add Header` two new inputs are shown", () => { + const route = buildRoute(); + delete route["headers"]; + const { getByLabelText } = render(); + fireEvent.click(getByLabelText("add-header")); + + expect(getByLabelText("header")).toBeVisible(); + expect(getByLabelText("header-key").value).toBe(""); + expect(getByLabelText("header-value").value).toBe(""); + }); + + it("when clicking the remove button on the header the header is removed", () => { + const route = buildRoute(); + delete route["headers"]; + route["headers"] = [{ id: "1", header: "x-api-key", value: "test" }]; + + const { getByLabelText, queryByLabelText } = render(); + fireEvent.click(getByLabelText("remove-header")); + + expect(queryByLabelText("header")).toBeNull(); + }); + + it("when clicking `Save Route` with empty headers on the screen they are removed before sending the data", () => { + const route = buildRoute(); + route.headers.push({ id: 99, header: "", value: "" }); + + const { getByLabelText } = render(); + fireEvent.click(getByLabelText("route-save")); + + const expectedResult = Object.assign({}, route); + expectedResult.routes = expectedResult.headers.pop(); + + expect(utils.updateRoute).toHaveBeenCalledWith(route); + }); + }); + describe("modal", () => { it("when entering a value into the route input field the value is updated", () => { const { getByLabelText } = render(); diff --git a/client/src/config/routes.json b/client/src/config/routes.json index ed557dc..949db6d 100644 --- a/client/src/config/routes.json +++ b/client/src/config/routes.json @@ -22,10 +22,12 @@ }, "headers": [ { + "id": "1", "header": "Accept", "value": "application/json" }, { + "id": "2", "header": "x-api-key", "value": "abcdef12345" } diff --git a/configuration/routes.json b/configuration/routes.json index 09eaf82..947ba5f 100644 --- a/configuration/routes.json +++ b/configuration/routes.json @@ -1,108 +1,116 @@ { - "settings": { - "features": { - "chaosMonkey": false, - "cors": true, - "authentication": false + "settings": { + "features": { + "chaosMonkey": false, + "cors": true, + "authentication": false, + "groupedRoutes": false + }, + "authentication": { + "username": "test", + "password": "test" + } + }, + "routes": [ + { + "id": "6f935aa8-b629-4247-ba51-f02347b06e95", + "route": "/user", + "httpMethod": "GET", + "statusCode": "200", + "delay": "0", + "payload": { + "name": "Jermain Spinka", + "username": "Agnes71", + "email": "Taurean.Huels55@hotmail.com", + "address": { + "street": "Kihn Inlet", + "suite": "Apt. 784", + "city": "South Doraside", + "zipcode": "91828", + "geo": { + "lat": "-56.8426", + "lng": "-122.9710" + } }, - "authentication": { - "username": "test", - "password": "test" + "phone": "810-578-0341", + "website": "jewell.name", + "company": { + "name": "Quitzon, Wilderman and VonRueden", + "catchPhrase": "Realigned zero administration encoding", + "bs": "robust incubate synergies" } - }, - "routes": [ + }, + "disabled": false, + "headers": [ { - "id": "6f935aa8-b629-4247-ba51-f02347b06e95", - "route": "/user", - "httpMethod": "GET", - "statusCode": "200", - "delay": "0", - "payload": { - "name": "Jermain Spinka", - "username": "Agnes71", - "email": "Taurean.Huels55@hotmail.com", - "address": { - "street": "Kihn Inlet", - "suite": "Apt. 784", - "city": "South Doraside", - "zipcode": "91828", - "geo": { - "lat": "-56.8426", - "lng": "-122.9710" - } - }, - "phone": "810-578-0341", - "website": "jewell.name", - "company": { - "name": "Quitzon, Wilderman and VonRueden", - "catchPhrase": "Realigned zero administration encoding", - "bs": "robust incubate synergies" - } - }, - "disabled": false - }, + "id": "b3f30451-3e1c-444f-9bfc-cb42441e9824", + "header": "x-api-key", + "value": "testing123" + } + ] + }, + { + "id": "ec6166c8-ac50-4f2c-9d6f-b26ed677fe72", + "route": "/users", + "httpMethod": "GET", + "statusCode": "200", + "delay": "0", + "payload": [ { - "id": "ec6166c8-ac50-4f2c-9d6f-b26ed677fe72", - "route": "/users", - "httpMethod": "GET", - "statusCode": "200", - "delay": "0", - "payload": [ - { - "name": "Edison Littel", - "username": "Tyrel.Funk24", - "email": "Burnice38@yahoo.com", - "address": { - "street": "Christelle Drive", - "suite": "Apt. 884", - "city": "Hoegerport", - "zipcode": "59938", - "geo": { - "lat": "-49.1240", - "lng": "148.9288" - } - }, - "phone": "(030) 225-3359 x582", - "website": "priscilla.com", - "company": { - "name": "Reynolds Inc", - "catchPhrase": "User-centric value-added productivity", - "bs": "ubiquitous leverage paradigms" - } - }, - { - "name": "Garth Corwin", - "username": "Hilton33", - "email": "Alvera_Runte78@yahoo.com", - "address": { - "street": "Ervin Gardens", - "suite": "Apt. 778", - "city": "Lake Laurynburgh", - "zipcode": "39378", - "geo": { - "lat": "77.6111", - "lng": "124.8899" - } - }, - "phone": "(477) 397-0658", - "website": "selena.net", - "company": { - "name": "Schmeler LLC", - "catchPhrase": "Adaptive global middleware", - "bs": "interactive deploy bandwidth" - } - } - ] + "name": "Edison Littel", + "username": "Tyrel.Funk24", + "email": "Burnice38@yahoo.com", + "address": { + "street": "Christelle Drive", + "suite": "Apt. 884", + "city": "Hoegerport", + "zipcode": "59938", + "geo": { + "lat": "-49.1240", + "lng": "148.9288" + } + }, + "phone": "(030) 225-3359 x582", + "website": "priscilla.com", + "company": { + "name": "Reynolds Inc", + "catchPhrase": "User-centric value-added productivity", + "bs": "ubiquitous leverage paradigms" + } }, { - "id": "61b4aaa9-bbe9-4cfc-80ee-3d8528a218e6", - "route": "/demo", - "httpMethod": "GET", - "statusCode": "200", - "delay": "0", - "payload": { - "message": "hello" + "name": "Garth Corwin", + "username": "Hilton33", + "email": "Alvera_Runte78@yahoo.com", + "address": { + "street": "Ervin Gardens", + "suite": "Apt. 778", + "city": "Lake Laurynburgh", + "zipcode": "39378", + "geo": { + "lat": "77.6111", + "lng": "124.8899" } + }, + "phone": "(477) 397-0658", + "website": "selena.net", + "company": { + "name": "Schmeler LLC", + "catchPhrase": "Adaptive global middleware", + "bs": "interactive deploy bandwidth" + } } - ] + ] + }, + { + "id": "61b4aaa9-bbe9-4cfc-80ee-3d8528a218e6", + "route": "/demo", + "httpMethod": "GET", + "statusCode": "200", + "delay": "0", + "payload": { + "message": "hello" + } + } + ] } diff --git a/mockit-routes/src/index.js b/mockit-routes/src/index.js index 1b9b866..96f43bf 100644 --- a/mockit-routes/src/index.js +++ b/mockit-routes/src/index.js @@ -28,10 +28,9 @@ routes.forEach(route => { if (!disabled) { app[method](path, (req, res) => { - Object.keys(headers).forEach(key => { - res.set(key, headers[key]); + headers.forEach(({ header, value } = {}) => { + res.set(header, value); }); - res.status(statusCode).send(payload); }); } diff --git a/mockit-routes/src/spec.js b/mockit-routes/src/spec.js index 1f22af0..18d9c80 100644 --- a/mockit-routes/src/spec.js +++ b/mockit-routes/src/spec.js @@ -110,6 +110,19 @@ const exampleConfig = { test: true }, disabled: false + }, + { + route: "/headersExample", + httpMethod: "GET", + statusCode: "200", + delay: "0", + headers: [ + { + header: "Content-Type", + value: "application/json" + } + ], + disabled: false } ] }; @@ -150,6 +163,14 @@ describe("Mockit Routes", () => { }); }); + describe("headers", () => { + it("when a route is configured with headers the headers are sent back in the response", async () => { + await request(app) + .get("/headersExample") + .expect("Content-Type", "application/json; charset=utf-8"); + }); + }); + describe("disabled routes", () => { it("when a route is marked as disabled in the configuration file, the route cannot be accessed and returns a 404", async () => { await request(app)