diff --git a/components/Codemirror/index.js b/components/Codemirror/index.js index 54ffbbf..4bed7b7 100644 --- a/components/Codemirror/index.js +++ b/components/Codemirror/index.js @@ -17,7 +17,7 @@ import 'codemirror/addon/display/fullscreen.css'; import 'codemirror/addon/fold/foldgutter.css'; function CodeMirror({ - formKey, isEditView, readOnly, values, data, + formKey, isEditView, readOnly, values, data, id, }) { const codeRef = useRef(null); const [mounted, setMounted] = useState(false); @@ -111,7 +111,7 @@ function CodeMirror({ }, []); return ( -
+
{mounted ? ( { api.patch('/events/abort', { - bridge_id: id, + bridge_id: bridgeId, }) .then((res) => { if (res.status === 200) { @@ -51,8 +51,8 @@ function ActionsDialog({ }; const handleActivate = async () => { - await api.patch(`/bridges/${id}/${active ? 'deactivate' : 'activate'}`).then(() => { - router.push(`/bridge/${id}`); + await api.patch(`/bridges/${bridgeId}/${active ? 'deactivate' : 'activate'}`).then(() => { + router.push(`/bridge/${bridgeId}`); setSuccessOpen(true); setTimeout(() => { setSuccessOpen(false); @@ -66,7 +66,7 @@ function ActionsDialog({ }; const handleDelete = () => { - api.delete(`/bridges/${id}`).then(() => { + api.delete(`/bridges/${bridgeId}`).then(() => { router.push('/dashboard'); }).catch(() => { setErrorOpen(true); @@ -82,6 +82,7 @@ function ActionsDialog({ anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={successOpen} autoHideDuration={3000} + id="actions-success-message" > Success! Your bridge has been updated. @@ -91,14 +92,16 @@ function ActionsDialog({ anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={errorOpen} autoHideDuration={3000} + id="actions-error-message" > Some error occurred. Please try again. + Bridge Actions - + @@ -106,7 +109,8 @@ function ActionsDialog({ - + + @@ -114,7 +118,8 @@ function ActionsDialog({ - + + @@ -131,7 +136,7 @@ ActionsDialog.propTypes = { active: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, open: PropTypes.bool.isRequired, - id: PropTypes.number.isRequired, + bridgeId: PropTypes.number.isRequired, }; export default ActionsDialog; diff --git a/components/Editor/BridgeTestCard/index.js b/components/Editor/BridgeTestCard/index.js index 19b098d..0912ffe 100644 --- a/components/Editor/BridgeTestCard/index.js +++ b/components/Editor/BridgeTestCard/index.js @@ -67,6 +67,7 @@ function BridgeTestCard({ isEditView, values }) { formKey="testPayloadCode" isEditView={isEditView} values={values} + id="test-payload" /> diff --git a/components/Editor/EnvironmentVariablesCard/index.js b/components/Editor/EnvironmentVariablesCard/index.js index fc81038..081e590 100644 --- a/components/Editor/EnvironmentVariablesCard/index.js +++ b/components/Editor/EnvironmentVariablesCard/index.js @@ -89,6 +89,7 @@ function EnvironmentVariablesCard({ environmentVariables }) { name={`environmentVariables[${idx}].value`} value={envVar.value || ''} placeholder="Value" + id={`envVar-${idx}-value`} fullWidth validate={validateEnvVars} /> @@ -97,6 +98,7 @@ function EnvironmentVariablesCard({ environmentVariables }) { @@ -109,6 +111,7 @@ function EnvironmentVariablesCard({ environmentVariables }) { variant="outlined" className={classes.plusButton} onClick={() => arrayHelpers.push({ key: '', value: '' })} + id="envPlusBtn" > + diff --git a/components/Editor/HeadersCard/DelayDropdown.js b/components/Editor/HeadersCard/DelayDropdown.js index e0a864d..8a4a658 100644 --- a/components/Editor/HeadersCard/DelayDropdown.js +++ b/components/Editor/HeadersCard/DelayDropdown.js @@ -47,6 +47,7 @@ function MethodDropdown() { select validate={validateDelay} className={classes.dropDown} + id="delay" > Instant 15 Minutes diff --git a/components/Editor/HeadersCard/HeaderTextFields.js b/components/Editor/HeadersCard/HeaderTextFields.js index f09f711..b2848e9 100644 --- a/components/Editor/HeadersCard/HeaderTextFields.js +++ b/components/Editor/HeadersCard/HeaderTextFields.js @@ -90,6 +90,7 @@ function HeaderTextFields({ headers }) { name={`headers[${idx}].value`} value={header.value || ''} placeholder="Value" + id={`headers-${idx}-value`} validate={validateHeaders} fullWidth /> @@ -98,6 +99,7 @@ function HeaderTextFields({ headers }) { @@ -110,6 +112,7 @@ function HeaderTextFields({ headers }) { variant="outlined" className={classes.plusButton} onClick={() => arrayHelpers.push({ key: '', value: '' })} + id="headerPlusBtn" > + diff --git a/components/Editor/HeadersCard/MethodDropdown.js b/components/Editor/HeadersCard/MethodDropdown.js index cd905ad..34e08d2 100644 --- a/components/Editor/HeadersCard/MethodDropdown.js +++ b/components/Editor/HeadersCard/MethodDropdown.js @@ -42,11 +42,12 @@ function MethodDropdown() { {methods.map((method) => ( {method} diff --git a/components/Editor/HeadersCard/RetryDropdown.js b/components/Editor/HeadersCard/RetryDropdown.js index 6830861..ffdbc85 100644 --- a/components/Editor/HeadersCard/RetryDropdown.js +++ b/components/Editor/HeadersCard/RetryDropdown.js @@ -46,9 +46,10 @@ function MethodDropdown() { select validate={validateRetries} className={classes.dropDown} + id="retries" > {retries.map((retry) => ( - {`${retry} Retries`} + {`${retry}`} ))} diff --git a/components/Editor/HeadersCard/index.js b/components/Editor/HeadersCard/index.js index 3386bf8..aa1263f 100644 --- a/components/Editor/HeadersCard/index.js +++ b/components/Editor/HeadersCard/index.js @@ -46,7 +46,7 @@ function HeaderCard({ headers, outboundUrl, title }) { let error; if (!bridgeTitle) { error = 'Required'; - } else if (bridgeTitle.size < 3) { + } else if (bridgeTitle.length < 3) { error = 'Title must be at least 3 characters long'; } return error; diff --git a/components/Editor/PayloadCard/index.js b/components/Editor/PayloadCard/index.js index 5af8318..5fe238c 100644 --- a/components/Editor/PayloadCard/index.js +++ b/components/Editor/PayloadCard/index.js @@ -71,7 +71,7 @@ function PayloadCard({ isEditView, values }) { Edit outbound payload - + diff --git a/components/Editor/index.js b/components/Editor/index.js index fe5a874..69719f4 100644 --- a/components/Editor/index.js +++ b/components/Editor/index.js @@ -42,6 +42,8 @@ function Editor({ bridge, isEditView }) { const [open, setOpen] = useState(false); const [actionsDialogOpen, setActionsDialogOpen] = useState(false); const [errorOpen, setErrorOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState('Some error has occurred. Please try again.'); + // TODO: Custom error messages // const [errMsg, setErrMsg] = useState(''); @@ -49,7 +51,7 @@ function Editor({ bridge, isEditView }) { active, id, outboundUrl, - method, + httpMethod, headers, environmentVariables, data, @@ -62,7 +64,7 @@ function Editor({ bridge, isEditView }) { active, title, outboundUrl, - method, + httpMethod, retries, delay, headers, @@ -98,7 +100,7 @@ function Editor({ bridge, isEditView }) { const generatePayload = (values) => ({ active: values.active, title: values.title, - http_method: values.method, + http_method: values.httpMethod, outbound_url: values.outboundUrl, retries: values.retries, delay: values.delay, @@ -110,20 +112,54 @@ function Editor({ bridge, isEditView }) { }, }); + const validatePayloads = (payloadCode, testPayloadCode) => { + const erraneousPayloads = []; + const validatePayload = (payload, type) => { + try { JSON.parse(payload); } catch { erraneousPayloads.push(type); } + }; + const createErrorMessage = () => { + if (erraneousPayloads.length === 1) { + setErrorMessage(`Invalid JSON for ${erraneousPayloads[0]} editor`); + } else { + setErrorMessage('Invalid JSON for Payload and Test Payload editors'); + } + }; + + validatePayload(payloadCode, 'Payload'); + validatePayload(testPayloadCode, 'Test Payload'); + + if (erraneousPayloads.length !== 0) { + createErrorMessage(); + setErrorOpen(true); + return false; + } + + return true; + }; + const handleSubmit = async (values, setSubmitting) => { - if (id) { + if (validatePayloads(values.payloadCode, values.testPayloadCode)) { // POST if new bridge, otherwise PATCH - await api - .patch(`/bridges/${id}`, generatePayload(values)) - .catch(() => setErrorOpen(true)); - } else { - await api - .post('/bridges', generatePayload(values)) - .then((res) => router.push(`/bridge/${res.data.id}`)) - .catch(() => setErrorOpen(true)); + if (id) { + await api + .patch(`/bridges/${id}`, generatePayload(values)) + .then(() => setOpen(true)) + .catch(() => setErrorOpen(true)); + } else { + await api + .post('/bridges', generatePayload(values)) + .then((res) => { + setOpen(true); + // TODO: Should timeout so user gets a chance to read the snack + // but is 200ms the time we want? + setTimeout(() => { + router.push(`/bridge/${res.data.id}`); + }, 200); + }) + .catch(() => setErrorOpen(true)); + } } - setOpen(true); setSubmitting(false); }; @@ -149,6 +185,7 @@ function Editor({ bridge, isEditView }) { validateOnChange={false} validateOnBlur={false} className={classes.root} + id="form" > {({ values, submitForm }) => (
@@ -172,10 +209,18 @@ function Editor({ bridge, isEditView }) { active={active} open={actionsDialogOpen} onClose={() => setActionsDialogOpen(false)} - id={id} + bridgeId={id} /> - - + + @@ -195,8 +240,8 @@ function Editor({ bridge, isEditView }) { - - + + ); } @@ -209,7 +254,7 @@ Editor.defaultProps = { active: true, title: '', outboundUrl: '', - method: '', + httpMethod: '', retries: '', delay: '', headers: [ @@ -233,7 +278,7 @@ Editor.propTypes = { id: PropTypes.number, title: PropTypes.string, outboundUrl: PropTypes.string, - method: PropTypes.string, + httpMethod: PropTypes.string, retries: PropTypes.number, delay: PropTypes.number, headers: PropTypes.arrayOf( diff --git a/pages/bridge/new.js b/pages/bridge/new.js index 426940f..c5f0334 100644 --- a/pages/bridge/new.js +++ b/pages/bridge/new.js @@ -1,5 +1,6 @@ import Editor from '../../components/Editor'; import ProtectRoute from '../../utils/ProtectRoute'; +import { ssrRedirectUnlessToken } from '../../utils/ssrRedirect'; function New() { return ( @@ -10,3 +11,8 @@ function New() { } export default New; + +export async function getServerSideProps(context) { + ssrRedirectUnlessToken(context); + return { props: {} }; +} diff --git a/pages/users/signup.js b/pages/users/signup.js index f5c460f..a63add1 100644 --- a/pages/users/signup.js +++ b/pages/users/signup.js @@ -184,8 +184,20 @@ function Signup() { - - + + ); } diff --git a/specs/fixtures/bridge.json b/specs/fixtures/bridge.json new file mode 100644 index 0000000..f813c1c --- /dev/null +++ b/specs/fixtures/bridge.json @@ -0,0 +1,79 @@ +{ + "bridge": { + "id": 1, + "title": "title_1", + "inbound_url": "534a9856d7b2b9267609", + "outbound_url": "c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event", + "http_method": "POST", + "retries": 3, + "delay": 15, + "data": { + "payload": "{\n \"hello\": \"world\"\n}", + "test_payload": "{\n \"hello\": \"world\",\n \"acessEnvVars\": \"$env.MY_KEY\",\n \"accessPayload\": \"$payload.message\"\n}" + }, + "created_at": "2020-11-26T18:30:24.093Z", + "updated_at": "2020-11-26T18:30:24.093Z", + "user_id": 3, + "active": true, + "eventCount": 10, + "completedAt": "", + "environment_variables": [ + { + "id": 12, + "key": "API_KEY", + "value": "897342jhad891-adf" + } + ], + "headers": [ + { + "id": 57, + "key": "X_API_KEY", + "value": "$env.API_KEY" + } + ], + "events": [ + { + "id": 22, + "completed": true, + "data": "{\"inbound\":{\"payload\":{\"bridge_id\":\"1\",\"top_level_key\":\"present\",\"nested_key_1\":{\"nested_key_2\":\"present\"}},\"dateTime\":\"2020-11-25T15:33:31.372Z\",\"ip\":\"::1\",\"contentLength\":101},\"outbound\":[{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}},{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}},{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}}]}", + "inbound_url": "d51985cdb5139d8d7a76", + "outbound_url": "c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event", + "status_code": 201, + "completed_at": "2020-11-26T15:33:34.274Z", + "bridge_id": 3, + "created_at": "2020-11-25T15:33:31.383Z", + "updated_at": "2020-11-28T13:07:00.408Z", + "test": false, + "aborted": false + }, + { + "id": 24, + "completed": true, + "data": "{\"inbound\":{\"payload\":{\"bridge_id\":\"1\",\"top_level_key\":\"present\",\"nested_key_1\":{\"nested_key_2\":\"present\"}},\"dateTime\":\"2020-11-25T15:33:31.372Z\",\"ip\":\"::1\",\"contentLength\":101},\"outbound\":[{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}},{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}},{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}}]}", + "inbound_url": "d51985cdb5139d8d7a76", + "outbound_url": "c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event", + "status_code": 200, + "completed_at": "2020-11-25T15:33:34.274Z", + "bridge_id": 3, + "created_at": "2020-11-25T15:33:31.383Z", + "updated_at": "2020-11-28T13:07:00.408Z", + "test": false, + "aborted": false + }, + { + "id": 26, + "completed": true, + "data": "{\"inbound\":{\"payload\":{\"bridge_id\":\"1\",\"top_level_key\":\"present\",\"nested_key_1\":{\"nested_key_2\":\"present\"}},\"dateTime\":\"2020-11-25T15:33:31.372Z\",\"ip\":\"::1\",\"contentLength\":101},\"outbound\":[{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}},{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}},{\"request\":{\"payload\":{\"hello\":\"world\"},\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"contentLength\":\"17\",\"uri\":\"https://c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event\",\"headers\":[{\"key\":\"content-type\",\"value\":\"application/json\"},{\"key\":\"accept-encoding\",\"value\":\"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"},{\"key\":\"accept\",\"value\":\"*/*\"},{\"key\":\"user-agent\",\"value\":\"Ruby\"},{\"key\":\"host\",\"value\":\"c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io\"},{\"key\":\"connection\",\"value\":\"close\"},{\"key\":\"content-length\",\"value\":\"17\"}]},\"response\":{\"dateTime\":\"2020-11-25T15:33:34.273Z\",\"statusCode\":\"200\",\"message\":\"OK\",\"size\":13,\"payload\":{\"hello\":\"world\"}}}]}", + "inbound_url": "d51985cdb5139d8d7a76", + "outbound_url": "c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event", + "status_code": 200, + "completed_at": "2020-11-23T15:33:34.274Z", + "bridge_id": 3, + "created_at": "2020-11-23T15:33:31.383Z", + "updated_at": "2020-11-28T13:07:00.408Z", + "test": false, + "aborted": false + } + ] + } +} \ No newline at end of file diff --git a/specs/integration/bridgeapi/bridges/id.spec.js b/specs/integration/bridgeapi/bridges/id.spec.js new file mode 100644 index 0000000..0a6ddf9 --- /dev/null +++ b/specs/integration/bridgeapi/bridges/id.spec.js @@ -0,0 +1,173 @@ +/* eslint-disable cypress/no-unnecessary-waiting */ +describe('Show bridge', () => { + beforeEach(() => { + cy.clearCookies(); + }); + + afterEach(() => { + cy.clearCookies(); + }); + + it('redirects to login with bad token', () => { + cy.setBadToken(); + cy.visit('/bridge/1'); + + cy.location().should((location) => { + expect(location.pathname).to.eq('/users/login'); + }); + }); + + it('can display bridge values', () => { + cy.setToken(); + cy.visit('/bridge/1'); + + cy.get('#title').should('have.value', 'title_1'); + cy.get('#outboundUrl').should('have.value', 'c41a7126-a18c-4af6-880e-6857771a35c8.mock.pstmn.io/success_event'); + cy.get('#method').contains('POST').should('be.visible'); + cy.get('#retries').contains('3').should('be.visible'); + cy.get('#delay').contains('15 Minutes').should('be.visible'); + + cy.get('#headers-0').should('have.value', 'X_API_KEY'); + cy.get('#headers-0-value').should('have.value', '$env.API_KEY'); + + cy.get('#envVar-0').should('have.value', 'API_KEY'); + cy.get('#envVar-0-value').should('have.value', 'XXXX-XXX-XXXX'); + + // TODO: Find way to test editor values + }); + + it('can delete headers', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/headers/57', 'DELETE', 200, {}); + + cy.wait(100); + cy.get('#headers-trash-0').click(); + cy.wait(100); + + cy.get('#headers-0').should('not.exist'); + cy.get('#headers-0-value').should('not.exist'); + cy.get('#headers-trash-0').should('not.exist'); + }); + + it('can put headers back into dom if request fails', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/headers/57', 'DELETE', 400); + + cy.get('#headers-trash-0').click(); + + cy.get('#headers-0').should('exist'); + cy.get('#headers-0-value').should('exist'); + cy.get('#headers-trash-0').should('exist'); + cy.get('#headers-0').should('have.value', 'X_API_KEY'); + cy.get('#headers-0-value').should('have.value', '$env.API_KEY'); + }); + + it('can delete environment variables', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/environment_variables/12', 'DELETE', 200, {}); + + cy.wait(100); + cy.get('#envVar-trash-0').click(); + cy.wait(100); + + cy.get('#envVar-0').should('not.exist'); + cy.get('#envVar-0-value').should('not.exist'); + cy.get('#envVar-trash-0').should('not.exist'); + }); + + it('can put environment variables back into dom if request fails', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/environment_variables/12', 'DELETE', 400, {}); + + cy.get('#envVar-trash-0').click(); + + cy.get('#envVar-0').should('exist'); + cy.get('#envVar-0-value').should('exist'); + cy.get('#envVar-trash-0').should('exist'); + cy.get('#envVar-0').should('have.value', 'API_KEY'); + cy.get('#envVar-0-value').should('have.value', 'XXXX-XXX-XXXX'); + }); + + it('can save', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/bridges', 'PATCH', 201, {}); + cy.wait(100); + + cy.get('#save-btn').click(); + cy.get('#success-alert').contains('Bridge has been saved.'); + }); + + it('can deactive', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/bridges/1/deactivate', 'PATCH', 201, {}); + cy.wait(100); + + cy.get('#actions-button').click(); + cy.get('#action-deactive').click(); + cy.get('#actions-success-message').contains('Success! Your bridge has been updated.'); + }); + + it('can show error when deactiving fails', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/bridges/1/deactivate', 'PATCH', 400, {}); + cy.wait(100); + + cy.get('#actions-button').click(); + cy.get('#action-deactive').click(); + cy.get('#actions-error-message').contains('Some error occurred. Please try again.'); + }); + + it('can abort requests', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/events/abort', 'PATCH', 200, {}); + cy.wait(100); + + cy.get('#actions-button').click(); + cy.get('#action-abort').click(); + cy.get('#actions-success-message').contains('Success! Your bridge has been updated.'); + }); + + it('can show error when aborting fails', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/events/abort', 'PATCH', 400, {}); + cy.wait(100); + + cy.get('#actions-button').click(); + cy.get('#action-abort').click(); + cy.get('#actions-error-message').contains('Some error occurred. Please try again.'); + }); + + it('can delete', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/bridges/1', 'DELETE', 200, {}); + cy.wait(100); + + cy.get('#actions-button').click(); + cy.get('#action-delete').click(); + cy.wait(100); + cy.location().should((location) => { + expect(location.pathname).to.eq('/dashboard'); + }); + }); + + it('can show error when deleting fails', () => { + cy.setToken(); + cy.visit('/bridge/1'); + cy.stubRequest('/bridges/1', 'DELETE', 400, {}); + cy.wait(100); + + cy.get('#actions-button').click(); + cy.get('#action-delete').click(); + cy.get('#actions-error-message').contains('Some error occurred. Please try again.'); + }); +}); diff --git a/specs/integration/bridgeapi/bridges/new.spec.js b/specs/integration/bridgeapi/bridges/new.spec.js new file mode 100644 index 0000000..ef07597 --- /dev/null +++ b/specs/integration/bridgeapi/bridges/new.spec.js @@ -0,0 +1,111 @@ +/* eslint-disable cypress/no-unnecessary-waiting */ +import { stubSuccessfullCreateBridge, stubFailedCreateBridge } from '../../../support/utils/stubs'; + +import { + inputHeaderFields, + inputHeaderFieldsInvalidTitle, + inputHeaderFieldsInvalidUrl, + inputEnvFields, + inputInvalidPayload, + inputInvalidTestPayload, +} from '../../../support/utils/inputs'; + +describe('Create a new bridge', () => { + beforeEach(() => { + cy.clearCookies(); + cy.setToken(); + cy.visit('/bridge/new'); + }); + + afterEach(() => { + cy.clearCookies(); + }); + + it('displays all error messages upon submission of empty fields', () => { + cy.get('#save-btn').click(); + + cy.get('#title-helper-text').contains('Required'); + cy.get('#outboundUrl-helper-text').contains('Required'); + cy.get('#method-helper-text').contains('Required'); + cy.get('#retries-helper-text').contains('Required'); + cy.get('#delay-helper-text').contains('Required'); + }); + + it('returns an error message if an error submission occurred', () => { + stubFailedCreateBridge(); + cy.wait(100); + inputHeaderFields(); + inputEnvFields(); + cy.get('#save-btn').click(); + + cy.get('#error-alert').contains('Some error has occurred. Please try again.'); + }); + + it('can create a new bridge', () => { + stubSuccessfullCreateBridge(); + cy.wait(1000); // Sometimes editor takes a second to load.. + inputHeaderFields(); + inputEnvFields(); + cy.get('#save-btn').click(); + + cy.wait(100); + cy.get('#success-alert').contains('Bridge has been saved.'); + cy.location().should((location) => { + expect(location.pathname).to.eq('/bridge/1'); + }); + }); + + it('gives url validation error if invalid url', () => { + inputHeaderFieldsInvalidUrl(); + inputEnvFields(); + cy.get('#save-btn').click(); + cy.wait(100); + cy.get('#outboundUrl-helper-text') + .contains('Invalid URL'); + }); + + it('gives title validation error if too short title (< 2 chars)', () => { + inputHeaderFieldsInvalidTitle(); + inputEnvFields(); + cy.get('#save-btn').click(); + cy.get('#title-helper-text') + .contains('Title must be at least 3 characters long'); + }); + + it('gives validation error if invalid payload', () => { + cy.wait(100); + inputHeaderFields(); + inputEnvFields(); + inputInvalidPayload(); + cy.get('#save-btn').click(); + cy.wait(100); + cy.get('#error-alert') + .contains('Invalid JSON for Payload editor') + .should('be.visible'); + }); + + it('gives validation error if invalid testPayload', () => { + cy.wait(100); + inputHeaderFields(); + inputEnvFields(); + inputInvalidTestPayload(); + cy.get('#save-btn').click(); + cy.wait(100); + cy.get('#error-alert') + .contains('Invalid JSON for Test Payload editor') + .should('be.visible'); + }); + + it('gives validation errors for both payloads if both are invalid', () => { + cy.wait(100); + inputHeaderFields(); + inputEnvFields(); + inputInvalidPayload(); + inputInvalidTestPayload(); + cy.get('#save-btn').click(); + cy.wait(100); + cy.get('#error-alert') + .contains('Invalid JSON for Payload and Test Payload editors') + .should('be.visible'); + }); +}); diff --git a/specs/integration/bridgeapi/events/[slug].spec.js b/specs/integration/bridgeapi/events/[slug].spec.js index 2df38f5..6b8fa1b 100644 --- a/specs/integration/bridgeapi/events/[slug].spec.js +++ b/specs/integration/bridgeapi/events/[slug].spec.js @@ -28,8 +28,9 @@ describe('Events Show', () => { .children() .should('have.length', 4); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(100); cy.get('#failed-attempts') - .click() .click(); cy.get('#event-timeline') diff --git a/specs/integration/bridgeapi/users/sign_up.spec.js b/specs/integration/bridgeapi/users/sign_up.spec.js index e4e4f9c..89813ae 100644 --- a/specs/integration/bridgeapi/users/sign_up.spec.js +++ b/specs/integration/bridgeapi/users/sign_up.spec.js @@ -30,7 +30,7 @@ describe('Sign Up', () => { inputFields(); submit(); - cy.get('.MuiAlert-message').contains('Account has been created. Redirecting...'); + cy.get('#success-message').contains('Account has been created. Redirecting...'); cy.location().should((location) => { expect(location.pathname).to.eq('/bridge/new'); }); @@ -41,7 +41,7 @@ describe('Sign Up', () => { inputFields(); submit(); - cy.get('.MuiAlert-message').contains('Some error occurred. Please try again.'); + cy.get('#error-message').contains('Some error has occurred. Please try again.'); cy.location().should((location) => { expect(location.pathname).to.eq('/users/signup'); }); diff --git a/specs/support/mocks/handlers.js b/specs/support/mocks/handlers.js index a176d07..cddf58e 100644 --- a/specs/support/mocks/handlers.js +++ b/specs/support/mocks/handlers.js @@ -2,6 +2,7 @@ import { rest } from 'msw'; const bridges = require('../../fixtures/bridges.json'); +const bridge = require('../../fixtures/bridge.json'); const event = require('../../fixtures/event.json'); const user = require('../../fixtures/user.json'); @@ -27,6 +28,19 @@ const handlers = [ return res(ctx.json(bridges)); }), + rest.get('http://localhost/bridges/1', (req, res, ctx) => { + if (invalidToken(req)) { + return res( + ctx.status(401), + ctx.json( + {}, + ), + ); + } + + return res(ctx.json(bridge)); + }), + rest.get('http://localhost/events/26', (req, res, ctx) => { if (invalidToken(req)) { return res( diff --git a/specs/support/utils/inputs.js b/specs/support/utils/inputs.js index 8baf6b0..f4f4fff 100644 --- a/specs/support/utils/inputs.js +++ b/specs/support/utils/inputs.js @@ -33,3 +33,125 @@ export const inputLoginFields = (email, pw) => { export const submit = () => { cy.get('form').submit(); }; + +export const inputTitle = (title) => { + const input = title || 'Bridge Title'; + cy.get('#title').type(input).should('have.value', input); +}; + +export const inputOutboundUrl = (url) => { + const input = url || 'www.outboundurl.com'; + cy.get('#outboundUrl').type(input).should('have.value', input); +}; + +export const inputMethod = (methodName) => { + const input = methodName || 'GET'; + cy.get('#method').click().then(() => { + cy.get(`[data-value="${input}"]`).click().then(() => { + cy.get('#method').invoke('text').should('eq', input); + }); + }); +}; + +export const inputRetries = (retries) => { + const input = retries || '3'; + cy.get('#retries').click().then(() => { + cy.get(`[data-value="${3}"]`).click().then(() => { + cy.get('#retries').invoke('text').should('eq', input); + }); + }); +}; + +export const inputDelay = (delay) => { + const input = delay || '30'; + cy.get('#delay').click().then(() => { + cy.get(`[data-value="${input}"]`).click().then(() => { + cy.get('#delay').invoke('text').should('eq', `${input} Minutes`); + }); + }); +}; + +export const inputHeaderKey = (headerKey) => { + const input = headerKey || 'Key'; + // cy.get('#headerPlusBtn').click().then(() => { + cy.get('#headers-0').type(input).should('have.value', input); + // }); +}; + +export const inputHeaderValue = (headerValue) => { + const input = headerValue || 'Value'; + // cy.get('#headerPlusBtn').click().then(() => { + cy.get('#headers-0-value').type(input).should('have.value', input); + // }); +}; + +export const inputHeaderPairs = () => { + cy.get('#headerPlusBtn').click(); + inputHeaderKey(); + inputHeaderValue(); +}; + +export const inputEnvVarKey = (envKey) => { + const input = envKey || 'Key'; + cy.get('#envVar-0').type(input).should('have.value', input); +}; + +export const inputEnvVarValue = (envValue) => { + const input = envValue || 'Value'; + cy.get('#envVar-0-value').type(input).should('have.value', input); +}; + +// export const inputPayload = (payload) => { +// const input = payload || "{ 'hello': 'world' }"; +// // const input = payload || defaultPayload; +// cy.get('#payload').type(input).should('have.value', input); +// }; + +// export const inputTestPayload = (testPayload) => { +// const input = testPayload || "{ 'hello': 'world' }"; +// // const input = testPayload || defaultPayload; +// cy.get('#test-payload').type(input).should('have.value', input); +// }; + +export const inputHeaderFields = () => { + inputTitle(); + inputOutboundUrl(); + inputMethod(); + inputRetries(); + inputDelay(); + inputHeaderPairs(); +}; + +export const inputEnvFields = () => { + cy.get('#envPlusBtn').click(); + inputEnvVarKey(); + inputEnvVarValue(); +}; + +export const inputHeaderFieldsInvalidUrl = () => { + inputTitle(); + inputOutboundUrl('invalidUrl'); + inputMethod(); + inputRetries(); + inputDelay(); + inputHeaderPairs(); +}; + +export const inputHeaderFieldsInvalidTitle = () => { + inputTitle('ab'); + inputOutboundUrl(); + inputMethod(); + inputRetries(); + inputDelay(); + inputHeaderPairs(); +}; + +export const inputInvalidPayload = () => { + cy.get('#payload textarea') + .type('hi', { force: true }); +}; + +export const inputInvalidTestPayload = () => { + cy.get('#test-payload textarea') + .type('hi', { force: true }); +}; diff --git a/specs/support/utils/stubs.js b/specs/support/utils/stubs.js index ce03c24..04a090d 100644 --- a/specs/support/utils/stubs.js +++ b/specs/support/utils/stubs.js @@ -28,3 +28,25 @@ export const stubFailLogin = () => { }; cy.stubRequest('/login', 'POST', 422, response); }; + +export const stubSuccessfullCreateBridge = () => { + const response = { + id: '1', + }; + cy.stubRequest('/bridges', 'POST', 201, response); +}; + +export const stubFailedCreateBridge = () => { + const response = { + error: 'Some error has occurred. Please try again.', + }; + cy.stubRequest('/bridges', 'POST', 400, response); +}; + +export const stubUpdateSuccessBridge = () => { + cy.stubRequest('/bridges/1', 'PATCH', 200); +}; + +export const stubFailUpdateBridge = () => { + cy.stubRequest('/bridges/1', 'PATCH', 400); +}; diff --git a/utils/ssrRedirect.js b/utils/ssrRedirect.js index 4f33659..d087fb9 100644 --- a/utils/ssrRedirect.js +++ b/utils/ssrRedirect.js @@ -11,7 +11,7 @@ const ssrRedirect = (context) => { // Called during `getServerSideProps` to ensure // a user is authenticated otherwise they will be redirected // to the login page. Returns `true` if redirect will be occuring. -const ssrRedirectUnlessToken = (context) => { +export const ssrRedirectUnlessToken = (context) => { if (!nookies.get(context).token) { ssrRedirect(context); return true;