Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore(site): Add unit tests, mocks #514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 23, 2022
76 changes: 76 additions & 0 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react"
import { Routes, Route } from "react-router-dom"
import { RequireAuth, AuthAndNav } from "./components"
import { IndexPage } from "./pages"
import { NotFoundPage } from "./pages/404"
import { CliAuthenticationPage } from "./pages/cli-auth"
import { HealthzPage } from "./pages/healthz"
import { SignInPage } from "./pages/login"
import { ProjectsPage } from "./pages/projects"
import { ProjectPage } from "./pages/projects/[organization]/[project]"
import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create"
import { WorkspacePage } from "./pages/workspaces/[workspace]"

export const AppRouter: React.FC = () => (
<Routes>
<Route path="/">
<Route
index
element={
<RequireAuth>
<IndexPage />
</RequireAuth>
}
/>

<Route path="login" element={<SignInPage />} />
<Route path="healthz" element={<HealthzPage />} />
<Route path="cli-auth" element={<CliAuthenticationPage />} />

<Route path="projects">
<Route
index
element={
<AuthAndNav>
<ProjectsPage />
</AuthAndNav>
}
/>
<Route path=":organization/:project">
<Route
index
element={
<AuthAndNav>
<ProjectPage />
</AuthAndNav>
}
/>
<Route
path="create"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>
</Route>

<Route path="workspaces">
<Route
path=":workspace"
element={
<AuthAndNav>
<WorkspacePage />
</AuthAndNav>
}
/>
</Route>

{/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
routes for. */}
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
)
76 changes: 3 additions & 73 deletions site/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,10 @@ import CssBaseline from "@material-ui/core/CssBaseline"
import ThemeProvider from "@material-ui/styles/ThemeProvider"
import { SWRConfig } from "swr"
import { light } from "./theme"
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
import { BrowserRouter as Router } from "react-router-dom"

import { CliAuthenticationPage } from "./pages/cli-auth"
import { NotFoundPage } from "./pages/404"
import { IndexPage } from "./pages/index"
import { SignInPage } from "./pages/login"
import { ProjectsPage } from "./pages/projects"
import { ProjectPage } from "./pages/projects/[organization]/[project]"
import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create"
import { WorkspacePage } from "./pages/workspaces/[workspace]"
import { HealthzPage } from "./pages/healthz"
import { AuthAndNav, RequireAuth } from "./components/Page"
import { XServiceProvider } from "./xServices/StateContext"
import { AppRouter } from "./AppRouter"

export const App: React.FC = () => {
return (
Expand All @@ -41,68 +32,7 @@ export const App: React.FC = () => {
<XServiceProvider>
<ThemeProvider theme={light}>
<CssBaseline />

<Routes>
<Route path="/">
<Route
index
element={
<RequireAuth>
<IndexPage />
</RequireAuth>
}
/>

<Route path="login" element={<SignInPage />} />
<Route path="healthz" element={<HealthzPage />} />
<Route path="cli-auth" element={<CliAuthenticationPage />} />

<Route path="projects">
<Route
index
element={
<AuthAndNav>
<ProjectsPage />
</AuthAndNav>
}
/>
<Route path=":organization/:project">
<Route
index
element={
<AuthAndNav>
<ProjectPage />
</AuthAndNav>
}
/>
<Route
path="create"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>
</Route>

<Route path="workspaces">
<Route
path=":workspace"
element={
<AuthAndNav>
<WorkspacePage />
</AuthAndNav>
}
/>
</Route>

{/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
routes for. */}
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
<AppRouter />
</ThemeProvider>
</XServiceProvider>
</SWRConfig>
Expand Down
4 changes: 3 additions & 1 deletion site/src/components/Page/RequireAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useActor } from "@xstate/react"
import React, { useContext } from "react"
import { Navigate, useLocation } from "react-router"
import { embedRedirect } from "../../util/redirect"
import { XServiceContext } from "../../xServices/StateContext"
import { FullScreenLoader } from "../Loader/FullScreenLoader"

Expand All @@ -12,9 +13,10 @@ export const RequireAuth: React.FC<RequireAuthProps> = ({ children }) => {
const xServices = useContext(XServiceContext)
const [userState] = useActor(xServices.userXService)
const location = useLocation()
const redirectTo = embedRedirect(location.pathname)

if (userState.matches("signedOut") || !userState.context.me) {
return <Navigate to={"/login?redirect=" + encodeURIComponent(location.pathname)} />
return <Navigate to={redirectTo} />
} else if (userState.hasTag("loading")) {
return <FullScreenLoader />
} else {
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/SignIn/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
<form onSubmit={form.handleSubmit}>
<div>
<FormTextField
label="Email"
autoComplete="email"
autoFocus
className={styles.loginTextField}
Expand All @@ -71,10 +72,10 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
inputProps={{
id: "signin-form-inpt-email",
}}
placeholder="Email"
variant="outlined"
/>
<FormTextField
label="Password"
autoComplete="current-password"
className={styles.loginTextField}
form={form}
Expand All @@ -84,7 +85,6 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
id: "signin-form-inpt-password",
}}
isPassword
placeholder="Password"
variant="outlined"
/>
{authErrorMessage && (
Expand Down
12 changes: 2 additions & 10 deletions site/src/pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { useActor } from "@xstate/react"
import React, { useContext } from "react"
import { SignInForm } from "./../components/SignIn"
import { Navigate, useLocation } from "react-router-dom"
import { Location } from "history"
import { XServiceContext } from "../xServices/StateContext"
import { retrieveRedirect } from "../util/redirect"

export const useStyles = makeStyles((theme) => ({
root: {
Expand All @@ -20,21 +20,13 @@ export const useStyles = makeStyles((theme) => ({
},
}))

const getRedirectFromLocation = (location: Location) => {
const defaultRedirect = "/"

const searchParams = new URLSearchParams(location.search)
const redirect = searchParams.get("redirect")
return redirect ? redirect : defaultRedirect
}

export const SignInPage: React.FC = () => {
const styles = useStyles()
const location = useLocation()
const xServices = useContext(XServiceContext)
const [userState, userSend] = useActor(xServices.userXService)
const isLoading = userState.hasTag("loading")
const redirectTo = getRedirectFromLocation(location)
const redirectTo = retrieveRedirect(location.search)
const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined

const onSubmit = async ({ email, password }: { email: string; password: string }) => {
Expand Down
24 changes: 12 additions & 12 deletions site/src/test_helpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,39 @@ export const MockSessionToken = { session_token: "my-session-token" }
export const MockAPIKey = { key: "my-api-key" }

export const MockUser: UserResponse = {
id: "test-user-id",
id: "test-user",
username: "TestUser",
email: "[email protected]",
created_at: "",
}

export const MockProject: Project = {
id: "project-id",
export const MockOrganization: Organization = {
id: "test-org",
name: "Test Organization",
created_at: "",
updated_at: "",
organization_id: "test-org",
name: "Test Project",
provisioner: "test-provisioner",
active_version_id: "",
}

export const MockProvisioner: Provisioner = {
id: "test-provisioner",
name: "Test Provisioner",
}

export const MockOrganization: Organization = {
id: "test-org",
name: "Test Organization",
export const MockProject: Project = {
id: "test-project",
created_at: "",
updated_at: "",
organization_id: MockOrganization.id,
name: "Test Project",
provisioner: MockProvisioner.id,
active_version_id: "",
}

export const MockWorkspace: Workspace = {
id: "test-workspace",
name: "Test-Workspace",
created_at: "",
updated_at: "",
project_id: "project-id",
owner_id: "test-user-id",
project_id: MockProject.id,
owner_id: MockUser.id
}
18 changes: 18 additions & 0 deletions site/src/test_helpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ export const handlers = [
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
rest.get("/api/v2/projects/:organizationId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockProject))
}),
rest.get("/api/v2/users/me/organizations/:organizationId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockProject))
}),
rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
rest.get("/api/v2/projects/:projectId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockProject))
}),
rest.get("/api/v2/organizations/:organizationId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockOrganization))
}),
rest.get("/api/v2/organizations/:organizationId/projects/:projectId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockOrganization))
}),
rest.post("/api/v2/users/login", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockSessionToken))
}),
Expand Down
16 changes: 16 additions & 0 deletions site/src/util/redirect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { embedRedirect, retrieveRedirect } from "./redirect"

describe("redirect helper functions", () => {
it("embeds the page to return to in the URL", () => {
const result = embedRedirect("/workspaces", "/page")
expect(result).toEqual("/page?redirect=%2Fworkspaces")
})
it("defaults to navigating to the login page", () => {
const result = embedRedirect("/workspaces")
expect(result).toEqual("/login?redirect=%2Fworkspaces")
})
it("retrieves the page to return to from the URL", () => {
const result = retrieveRedirect("?redirect=%2Fworkspaces")
expect(result).toEqual("/workspaces")
})
})
21 changes: 21 additions & 0 deletions site/src/util/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Creates a url containing a page to navigate to now, and embedding another
* URL in the query string so you can return to it later.
* @param navigateTo page to navigate to now (by default, /login)
* @param returnTo page to redirect to later (for instance, after logging in)
* @returns URL containing a redirect query parameter
*/
export const embedRedirect = (returnTo: string, navigateTo = "/login"): string =>
`${navigateTo}?redirect=${encodeURIComponent(returnTo)}`

/**
* Retrieves a url from the query string of the current URL
* @param search the query string in the current URL
* @returns the URL to redirect to
*/
export const retrieveRedirect = (search: string): string => {
const defaultRedirect = "/"
const searchParams = new URLSearchParams(search)
const redirect = searchParams.get("redirect")
return redirect ? redirect : defaultRedirect
}