Skip to content

Commit

Permalink
feat: redirects playground
Browse files Browse the repository at this point in the history
  • Loading branch information
svedova committed May 6, 2024
1 parent 0908485 commit 3f49fd7
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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(
<RedirectsPlaygroundModal
appId={currentApp.id}
env={currentEnv}
onClose={closeSpy}
/>
);
};

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"));
});
});
});
Original file line number Diff line number Diff line change
@@ -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<SubmitReturn>();
const [error, setError] = useState<string>();
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<SubmitReturn>("/redirects/playground", {
appId: appId,
envId: env.id,
address,
redirects: parsed,
})
.then(match => {
setResult(match);
})
.finally(() => {
setError(undefined);
setLoading(false);
});
};

return (
<Modal open onClose={onClose} maxWidth="lg">
<Card
data-testid="redirects-playground-form"
component="form"
onSubmit={submitHandler}
error={
error || !env.published
? "Redirects are tested against the published deployments. Please deploy and publish before testing redirects."
: undefined
}
>
<CardHeader title="Redirects Playground" />
<Box sx={{ mb: 4 }}>
<TextInput
variant="filled"
label="Request URL"
placeholder="e.g. https://www.stormkit.io/docs"
fullWidth
autoFocus
autoComplete="off"
value={address}
onChange={e => setAddress(e.target.value)}
helperText={
<Typography sx={{ mt: 1 }} component="span">
Provide a URL to test against the redirects.
</Typography>
}
/>
</Box>
<Box
sx={{
mb: 4,
p: 2,
bgcolor: "rgba(0,0,0,0.2)",
borderBottom: `1px solid ${grey[900]}`,
}}
>
<Typography sx={{ mb: 2, color: grey[400] }}>Redirects</Typography>
<CodeMirror
maxHeight="200px"
value={redirects}
extensions={[json()]}
onChange={value => setRedirects(value)}
theme="dark"
/>
<Typography sx={{ mt: 2, color: grey[400] }}>
Check the{" "}
<Link
href={
"https://stormkit.io/docs/features/redirects-and-path-rewrites"
}
target="_blank"
rel="noreferrer noopener"
>
docs
</Link>{" "}
for more information.
</Typography>
</Box>
{result &&
(result.match ? (
<>
<Alert color="success" sx={{ mb: 0 }}>
<Typography>It's a match!</Typography>
</Alert>
<Table sx={{ mb: 4 }}>
<TableBody>
<TableRow>
<TableCell>Status</TableCell>
<TableCell>
{result.status
? `Redirect ${result.status}`
: "Path Rewrite"}
</TableCell>
</TableRow>
<TableRow>
<TableCell>To</TableCell>
<TableCell>{result.redirect || result.rewrite}</TableCell>
</TableRow>
<TableRow>
<TableCell>Should proxy?</TableCell>
<TableCell>{result.proxy ? "Yes" : "No"}</TableCell>
</TableRow>
</TableBody>
</Table>
</>
) : (
<Alert color="error" sx={{ mb: 4 }}>
<AlertTitle>Not a match</AlertTitle>
<Typography>
Provided domain did not match any redirect rule.
</Typography>
</Alert>
))}
<CardFooter>
<Button
variant="contained"
color="secondary"
type="submit"
disabled={!env.published}
loading={loading}
>
Test
</Button>
</CardFooter>
</Card>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useState } from "react";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import Button from "@mui/lab/LoadingButton";
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;
Expand All @@ -17,6 +19,7 @@ export default function TabConfigGeneral({
app,
setRefreshToken,
}: Props) {
const [showModal, setShowModal] = useState(false);
const { submitHandler, error, success, isLoading } = useSubmitHandler({
app,
env,
Expand All @@ -39,6 +42,15 @@ export default function TabConfigGeneral({
<CardHeader
title="Redirects"
subtitle="Configure redirects and path rewrites for your application."
actions={
<Button
variant="text"
sx={{ color: "white" }}
onClick={() => setShowModal(true)}
>
Playground
</Button>
}
/>
<Box sx={{ mb: 4 }}>
<TextField
Expand Down Expand Up @@ -68,6 +80,15 @@ export default function TabConfigGeneral({
Save
</Button>
</CardFooter>
{showModal && (
<RedirectsPlaygroundModal
env={env}
appId={app.id}
onClose={() => {
setShowModal(false);
}}
/>
)}
</Card>
);
}
30 changes: 30 additions & 0 deletions testing/nocks/nock_redirects_playground.ts
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit 3f49fd7

Please sign in to comment.