Skip to content

Commit

Permalink
Feature - Add support for adding headers onto responses (#46)
Browse files Browse the repository at this point in the history
* Started to add custom headers

* Tmp

* #4 - Added the ability to add headers in the routes
  • Loading branch information
boyney123 authored May 23, 2019
1 parent a927278 commit c33a38f
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 107 deletions.
31 changes: 31 additions & 0 deletions client/src/components/HeaderInput/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="columns Header" aria-label="header">
<div className="control column">
<input className="input" placeholder="header" value={header} aria-label="header-key" onChange={e => update("header", e.target.value)} />
</div>
<div className="control column">
<input className="input" placeholder="value" value={value} aria-label="header-value" onChange={e => update("value", e.target.value)} />
</div>
<div className="control column is-1 Header__Remove" onClick={() => onRemove(id)}>
<i className="far fa-times-circle" aria-label="remove-header" />
</div>
</div>
);
}
55 changes: 55 additions & 0 deletions client/src/components/HeaderInput/spec.js
Original file line number Diff line number Diff line change
@@ -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(<HeaderInput />);
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(<HeaderInput data={{ id: 1, header: "Content-Type", value: "application/json" }} />);
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(<HeaderInput onBlur={spy} />);
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(<HeaderInput onBlur={spy} />);
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(<HeaderInput onBlur={spy} />);
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(<HeaderInput data={data} onRemove={spy} />);
fireEvent.click(getByLabelText("remove-header"));
expect(spy).toHaveBeenCalledWith(1);
});
});
});
48 changes: 46 additions & 2 deletions client/src/components/RouteModal/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useState } from "react";
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";
import uuid from "uuid/v4";

const HTTP_METHOD_LIST = [HttpMethods.GET, HttpMethods.POST, HttpMethods.PUT, HttpMethods.PATCH, HttpMethods.DELETE];
const STATUS_CODES = [
Expand All @@ -19,6 +21,12 @@ const STATUS_CODES = [
StatusCodes.INTERNAL_SERVER_ERROR
];

/**
* add header adds some input fields
*
* Clicking X removes the field from the array....
*/

const Modal = function(props) {
const { onClose = () => {}, route: editedRoute } = props;

Expand All @@ -28,21 +36,45 @@ const Modal = function(props) {
const [delay, updateDelay] = useState(editedRoute.delay);
const [payload, updatePayload] = useState(editedRoute.payload);
const [disabled, updateDisabled] = useState(editedRoute.disabled);
const [headers, updateHeaders] = useState(editedRoute.headers || []);

const isNewRoute = editedRoute.id === undefined;

const modalTitle = isNewRoute ? "Add Route" : "Edit Route";

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,
httpMethod,
statusCode,
delay,
payload,
disabled
disabled,
headers: cleanedHeaders
};

isNewRoute ? await createNewRoute(data) : await updateRouteRequest(data);
Expand Down Expand Up @@ -128,6 +160,18 @@ const Modal = function(props) {
</div>
</div>
<hr />
<div className="field mt10">
<label className="label" aria-label="no-headers-message">
Response Headers (optional)
</label>
{headers.length === 0 && <i>No headers added.</i>}
{headers.map((header, index) => {
return <HeaderInput data={header} onBlur={setHeader} onRemove={removeHeader} />;
})}
<button aria-label="add-header" className="button is-primary is-small is-pulled-right" onClick={addNewHeader}>
Add Header
</button>
</div>
<div className="field">
<div className="control">
<label className="label">Settings</label>
Expand Down
54 changes: 53 additions & 1 deletion client/src/components/RouteModal/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]
};
};

Expand Down Expand Up @@ -103,6 +104,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(<RouteModal route={route} />);
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(<RouteModal route={route} />);
expect(getByLabelText("no-headers-message")).toBeVisible();
});

it("with a checkbox to disable the route", () => {
const route = buildRoute();
const { getByLabelText } = render(<RouteModal route={route} />);
Expand All @@ -117,6 +132,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(<RouteModal route={route} />);
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(<RouteModal route={route} />);
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(<RouteModal route={route} />);
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(<RouteModal route={buildRoute()} />);
Expand Down
18 changes: 18 additions & 0 deletions client/src/config/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@
"newRoute": true,
"Money": 100
},
"headers": [
{
"id": "1",
"header": "Accept",
"value": "application/json"
},
{
"id": "2",
"header": "x-api-key",
"value": "abcdef12345"
}
],
"httpMethod": "POST",
"statusCode": "201",
"delay": "500"
Expand All @@ -44,6 +56,12 @@
"newRoute": true,
"Money": 100
},
"headers": [
{
"header": "test",
"value": "test"
}
],
"httpMethod": "POST",
"statusCode": "201",
"delay": "0",
Expand Down
7 changes: 7 additions & 0 deletions client/src/scss/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,10 @@ nav {
-webkit-animation-timing-function: linear;
font-size: 3rem;
}

.Header__Remove {
margin: 3px 0px 0px;
font-size: 20px;
color: #d87171;
cursor: pointer;
}
Loading

0 comments on commit c33a38f

Please sign in to comment.