From 3f49fd796060b2f3defdb234357b9c4181398af1 Mon Sep 17 00:00:00 2001 From: Savas Vedova Date: Mon, 6 May 2024 12:20:09 +0300 Subject: [PATCH] feat: redirects playground --- .../RedirectsPlaygroundModal.spec.tsx | 87 ++++++++ .../_components/RedirectsPlaygroundModal.tsx | 198 ++++++++++++++++++ .../config/_components/TabConfigRedirects.tsx | 21 ++ testing/nocks/nock_redirects_playground.ts | 30 +++ 4 files changed, 336 insertions(+) create mode 100644 src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.spec.tsx create mode 100644 src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.tsx create mode 100644 testing/nocks/nock_redirects_playground.ts diff --git a/src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.spec.tsx b/src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.spec.tsx new file mode 100644 index 00000000..696e3fa2 --- /dev/null +++ b/src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.spec.tsx @@ -0,0 +1,87 @@ +import { RenderResult, waitFor } from "@testing-library/react"; +import { fireEvent, render } from "@testing-library/react"; +import mockApp from "~/testing/data/mock_app"; +import mockEnvironments from "~/testing/data/mock_environments"; +import { mockPlayground } from "~/testing/nocks/nock_redirects_playground"; +import RedirectsPlaygroundModal from "./RedirectsPlaygroundModal"; + +interface WrapperProps { + app?: App; + environment?: Environment; + setRefreshToken?: () => void; +} + +jest.mock("@codemirror/lang-json", () => ({ json: jest.fn() })); +jest.mock("@uiw/react-codemirror", () => ({ value }: { value: string }) => ( + <>{value} +)); + +describe("~/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.tsx", () => { + let wrapper: RenderResult; + let currentApp: App; + let currentEnv: Environment; + let closeSpy: jest.Func; + + const createWrapper = ({ app, environment }: WrapperProps) => { + closeSpy = jest.fn(); + currentApp = app || mockApp(); + currentEnv = environment || mockEnvironments({ app: currentApp })[0]; + + wrapper = render( + + ); + }; + + test("should have a form", () => { + createWrapper({}); + + expect(wrapper.getByText("Redirects Playground")).toBeTruthy(); + + const requestUrlInput = wrapper.getByLabelText( + "Request URL" + ) as HTMLInputElement; + + expect(requestUrlInput.value).toBe("https://app.stormkit.io"); + + expect( + wrapper.getByText("Provide a URL to test against the redirects.") + ).toBeTruthy(); + + expect(wrapper.getByText("docs").getAttribute("href")).toBe( + "https://stormkit.io/docs/features/redirects-and-path-rewrites" + ); + }); + + test("should submit the form", async () => { + createWrapper({}); + + const scope = mockPlayground({ + appId: currentApp.id, + envId: currentEnv.id!, + address: "https://app.stormkit.io", + redirects: [{ from: "/my-path", to: "/my-new-path", status: 302 }], + response: { + match: true, + proxy: false, + status: 302, + redirect: "https://app.stormkit.io/new-url", + rewrite: "", + }, + }); + + fireEvent.submit(wrapper.getByTestId("redirects-playground-form")); + + await waitFor(() => { + expect(scope.isDone()).toBe(true); + expect(wrapper.getByText("It's a match!")); + expect(wrapper.getByText("Redirect 302")); + expect(wrapper.getByText("https://app.stormkit.io/new-url")); + expect(wrapper.getByText("Should proxy?")); + expect(wrapper.getByText("No")); + }); + }); +}); diff --git a/src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.tsx b/src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.tsx new file mode 100644 index 00000000..62b8d6b9 --- /dev/null +++ b/src/pages/apps/[id]/environments/[env-id]/config/_components/RedirectsPlaygroundModal.tsx @@ -0,0 +1,198 @@ +import { FormEventHandler, useState } from "react"; +import CodeMirror from "@uiw/react-codemirror"; +import { json } from "@codemirror/lang-json"; +import Table from "@mui/material/Table"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import TableBody from "@mui/material/TableBody"; +import Alert from "@mui/material/Alert"; +import AlertTitle from "@mui/material/AlertTitle"; +import Button from "@mui/lab/LoadingButton"; +import TextInput from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import Link from "@mui/material/Link"; +import Modal from "~/components/Modal"; +import Card from "~/components/Card"; +import CardHeader from "~/components/CardHeader"; +import CardFooter from "~/components/CardFooter"; +import api from "~/utils/api/Api"; +import { grey } from "@mui/material/colors"; + +interface Props { + env: Environment; + appId: string; + onClose: () => void; +} + +interface SubmitReturn { + match: boolean; + against: string; + pattern: string; + rewrite: string; + redirect: string; + proxy: boolean; + status: number; +} + +const exampleRedirects = `[ + { "from": "/my-path", "to": "/my-new-path", "status": 302 } +]`; + +export default function RedirectsPlaygroundModal({ + onClose, + appId, + env, +}: Props) { + const [address, setAddress] = useState(env.preview); + const [redirects, setRedirects] = useState(exampleRedirects); + const [result, setResult] = useState(); + const [error, setError] = useState(); + const [loading, setLoading] = useState(false); + + const submitHandler: FormEventHandler = e => { + e.preventDefault(); + e.stopPropagation(); + + let parsed: any; + + try { + parsed = JSON.parse(redirects); + } catch { + setError("Invalid JSON format."); + return; + } + + if (!address.trim()) { + setError("Request URL cannot be empty."); + return; + } + + setLoading(true); + + api + .post("/redirects/playground", { + appId: appId, + envId: env.id, + address, + redirects: parsed, + }) + .then(match => { + setResult(match); + }) + .finally(() => { + setError(undefined); + setLoading(false); + }); + }; + + return ( + + + + + setAddress(e.target.value)} + helperText={ + + Provide a URL to test against the redirects. + + } + /> + + + Redirects + setRedirects(value)} + theme="dark" + /> + + Check the{" "} + + docs + {" "} + for more information. + + + {result && + (result.match ? ( + <> + + It's a match! + + + + + Status + + {result.status + ? `Redirect ${result.status}` + : "Path Rewrite"} + + + + To + {result.redirect || result.rewrite} + + + Should proxy? + {result.proxy ? "Yes" : "No"} + + +
+ + ) : ( + + Not a match + + Provided domain did not match any redirect rule. + + + ))} + + + +
+
+ ); +} diff --git a/src/pages/apps/[id]/environments/[env-id]/config/_components/TabConfigRedirects.tsx b/src/pages/apps/[id]/environments/[env-id]/config/_components/TabConfigRedirects.tsx index 59827351..0a85db44 100644 --- a/src/pages/apps/[id]/environments/[env-id]/config/_components/TabConfigRedirects.tsx +++ b/src/pages/apps/[id]/environments/[env-id]/config/_components/TabConfigRedirects.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; import Button from "@mui/lab/LoadingButton"; @@ -5,6 +6,7 @@ import Card from "~/components/Card"; import CardHeader from "~/components/CardHeader"; import CardFooter from "~/components/CardFooter"; import { useSubmitHandler } from "../actions"; +import RedirectsPlaygroundModal from "./RedirectsPlaygroundModal"; interface Props { app: App; @@ -17,6 +19,7 @@ export default function TabConfigGeneral({ app, setRefreshToken, }: Props) { + const [showModal, setShowModal] = useState(false); const { submitHandler, error, success, isLoading } = useSubmitHandler({ app, env, @@ -39,6 +42,15 @@ export default function TabConfigGeneral({ setShowModal(true)} + > + Playground + + } /> + {showModal && ( + { + setShowModal(false); + }} + /> + )} ); } diff --git a/testing/nocks/nock_redirects_playground.ts b/testing/nocks/nock_redirects_playground.ts new file mode 100644 index 00000000..34e171aa --- /dev/null +++ b/testing/nocks/nock_redirects_playground.ts @@ -0,0 +1,30 @@ +import nock from "nock"; + +const endpoint = process.env.API_DOMAIN || ""; + +interface PlaygroundProps { + appId: string; + envId: string; + redirects: any; + address: string; + status?: number; + response: { + match: boolean; + status: number; + redirect: string; + rewrite: string; + proxy: boolean; + }; +} + +export const mockPlayground = ({ + appId, + envId, + address, + redirects, + response, + status = 200, +}: PlaygroundProps) => + nock(endpoint) + .post("/redirects/playground", { appId, envId, redirects, address }) + .reply(status, response);