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

Skip to content

Commit f2ac81c

Browse files
authored
chore(site): Add unit tests, mocks (#514)
* Extract and unit test redirect functions * Move router to make app more testable * Make mock entities more consistent * Use labels instead of placeholders * Fill out handlers * Lint * Reorganize App * Make mock entities reference each other * Add describes in tests * Clean up api and mocks
1 parent 3bf5ceb commit f2ac81c

File tree

10 files changed

+161
-118
lines changed

10 files changed

+161
-118
lines changed

site/src/AppRouter.tsx

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from "react"
2+
import { Routes, Route } from "react-router-dom"
3+
import { RequireAuth, AuthAndNav } from "./components"
4+
import { IndexPage } from "./pages"
5+
import { NotFoundPage } from "./pages/404"
6+
import { CliAuthenticationPage } from "./pages/cli-auth"
7+
import { HealthzPage } from "./pages/healthz"
8+
import { SignInPage } from "./pages/login"
9+
import { ProjectsPage } from "./pages/projects"
10+
import { ProjectPage } from "./pages/projects/[organization]/[project]"
11+
import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create"
12+
import { WorkspacePage } from "./pages/workspaces/[workspace]"
13+
14+
export const AppRouter: React.FC = () => (
15+
<Routes>
16+
<Route path="/">
17+
<Route
18+
index
19+
element={
20+
<RequireAuth>
21+
<IndexPage />
22+
</RequireAuth>
23+
}
24+
/>
25+
26+
<Route path="login" element={<SignInPage />} />
27+
<Route path="healthz" element={<HealthzPage />} />
28+
<Route path="cli-auth" element={<CliAuthenticationPage />} />
29+
30+
<Route path="projects">
31+
<Route
32+
index
33+
element={
34+
<AuthAndNav>
35+
<ProjectsPage />
36+
</AuthAndNav>
37+
}
38+
/>
39+
<Route path=":organization/:project">
40+
<Route
41+
index
42+
element={
43+
<AuthAndNav>
44+
<ProjectPage />
45+
</AuthAndNav>
46+
}
47+
/>
48+
<Route
49+
path="create"
50+
element={
51+
<RequireAuth>
52+
<CreateWorkspacePage />
53+
</RequireAuth>
54+
}
55+
/>
56+
</Route>
57+
</Route>
58+
59+
<Route path="workspaces">
60+
<Route
61+
path=":workspace"
62+
element={
63+
<AuthAndNav>
64+
<WorkspacePage />
65+
</AuthAndNav>
66+
}
67+
/>
68+
</Route>
69+
70+
{/* Using path="*"" means "match anything", so this route
71+
acts like a catch-all for URLs that we don't have explicit
72+
routes for. */}
73+
<Route path="*" element={<NotFoundPage />} />
74+
</Route>
75+
</Routes>
76+
)

site/src/api/index.ts

-20
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,6 @@ export const provisioners: Types.Provisioner[] = [
1717
},
1818
]
1919

20-
export namespace Project {
21-
export const create = async (request: Types.CreateProjectRequest): Promise<Types.Project> => {
22-
const response = await fetch(`/api/v2/projects/${request.organizationId}/`, {
23-
method: "POST",
24-
headers: {
25-
"Content-Type": "application/json",
26-
},
27-
body: JSON.stringify(request),
28-
})
29-
30-
const body = await response.json()
31-
await mutate("/api/v2/projects")
32-
if (!response.ok) {
33-
throw new Error(body.message)
34-
}
35-
36-
return body
37-
}
38-
}
39-
4020
export namespace Workspace {
4121
export const create = async (request: Types.CreateWorkspaceRequest): Promise<Types.Workspace> => {
4222
const response = await fetch(`/api/v2/users/me/workspaces`, {

site/src/app.tsx

+3-73
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,10 @@ import CssBaseline from "@material-ui/core/CssBaseline"
33
import ThemeProvider from "@material-ui/styles/ThemeProvider"
44
import { SWRConfig } from "swr"
55
import { light } from "./theme"
6-
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
6+
import { BrowserRouter as Router } from "react-router-dom"
77

8-
import { CliAuthenticationPage } from "./pages/cli-auth"
9-
import { NotFoundPage } from "./pages/404"
10-
import { IndexPage } from "./pages/index"
11-
import { SignInPage } from "./pages/login"
12-
import { ProjectsPage } from "./pages/projects"
13-
import { ProjectPage } from "./pages/projects/[organization]/[project]"
14-
import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create"
15-
import { WorkspacePage } from "./pages/workspaces/[workspace]"
16-
import { HealthzPage } from "./pages/healthz"
17-
import { AuthAndNav, RequireAuth } from "./components/Page"
188
import { XServiceProvider } from "./xServices/StateContext"
9+
import { AppRouter } from "./AppRouter"
1910
import "./theme/global-fonts"
2011

2112
export const App: React.FC = () => {
@@ -42,68 +33,7 @@ export const App: React.FC = () => {
4233
<XServiceProvider>
4334
<ThemeProvider theme={light}>
4435
<CssBaseline />
45-
46-
<Routes>
47-
<Route path="/">
48-
<Route
49-
index
50-
element={
51-
<RequireAuth>
52-
<IndexPage />
53-
</RequireAuth>
54-
}
55-
/>
56-
57-
<Route path="login" element={<SignInPage />} />
58-
<Route path="healthz" element={<HealthzPage />} />
59-
<Route path="cli-auth" element={<CliAuthenticationPage />} />
60-
61-
<Route path="projects">
62-
<Route
63-
index
64-
element={
65-
<AuthAndNav>
66-
<ProjectsPage />
67-
</AuthAndNav>
68-
}
69-
/>
70-
<Route path=":organization/:project">
71-
<Route
72-
index
73-
element={
74-
<AuthAndNav>
75-
<ProjectPage />
76-
</AuthAndNav>
77-
}
78-
/>
79-
<Route
80-
path="create"
81-
element={
82-
<RequireAuth>
83-
<CreateWorkspacePage />
84-
</RequireAuth>
85-
}
86-
/>
87-
</Route>
88-
</Route>
89-
90-
<Route path="workspaces">
91-
<Route
92-
path=":workspace"
93-
element={
94-
<AuthAndNav>
95-
<WorkspacePage />
96-
</AuthAndNav>
97-
}
98-
/>
99-
</Route>
100-
101-
{/* Using path="*"" means "match anything", so this route
102-
acts like a catch-all for URLs that we don't have explicit
103-
routes for. */}
104-
<Route path="*" element={<NotFoundPage />} />
105-
</Route>
106-
</Routes>
36+
<AppRouter />
10737
</ThemeProvider>
10838
</XServiceProvider>
10939
</SWRConfig>

site/src/components/Page/RequireAuth.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useActor } from "@xstate/react"
22
import React, { useContext } from "react"
33
import { Navigate, useLocation } from "react-router"
4+
import { embedRedirect } from "../../util/redirect"
45
import { XServiceContext } from "../../xServices/StateContext"
56
import { FullScreenLoader } from "../Loader/FullScreenLoader"
67

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

1618
if (userState.matches("signedOut") || !userState.context.me) {
17-
return <Navigate to={"/login?redirect=" + encodeURIComponent(location.pathname)} />
19+
return <Navigate to={redirectTo} />
1820
} else if (userState.hasTag("loading")) {
1921
return <FullScreenLoader />
2022
} else {

site/src/components/SignIn/SignInForm.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
6161
<form onSubmit={form.handleSubmit}>
6262
<div>
6363
<FormTextField
64+
label="Email"
6465
autoComplete="email"
6566
autoFocus
6667
className={styles.loginTextField}
@@ -71,10 +72,10 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
7172
inputProps={{
7273
id: "signin-form-inpt-email",
7374
}}
74-
placeholder="Email"
7575
variant="outlined"
7676
/>
7777
<FormTextField
78+
label="Password"
7879
autoComplete="current-password"
7980
className={styles.loginTextField}
8081
form={form}
@@ -84,7 +85,6 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
8485
id: "signin-form-inpt-password",
8586
}}
8687
isPassword
87-
placeholder="Password"
8888
variant="outlined"
8989
/>
9090
{authErrorMessage && (

site/src/pages/login.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { useActor } from "@xstate/react"
33
import React, { useContext } from "react"
44
import { SignInForm } from "./../components/SignIn"
55
import { Navigate, useLocation } from "react-router-dom"
6-
import { Location } from "history"
76
import { XServiceContext } from "../xServices/StateContext"
7+
import { retrieveRedirect } from "../util/redirect"
88

99
export const useStyles = makeStyles((theme) => ({
1010
root: {
@@ -20,21 +20,13 @@ export const useStyles = makeStyles((theme) => ({
2020
},
2121
}))
2222

23-
const getRedirectFromLocation = (location: Location) => {
24-
const defaultRedirect = "/"
25-
26-
const searchParams = new URLSearchParams(location.search)
27-
const redirect = searchParams.get("redirect")
28-
return redirect ? redirect : defaultRedirect
29-
}
30-
3123
export const SignInPage: React.FC = () => {
3224
const styles = useStyles()
3325
const location = useLocation()
3426
const xServices = useContext(XServiceContext)
3527
const [userState, userSend] = useActor(xServices.userXService)
3628
const isLoading = userState.hasTag("loading")
37-
const redirectTo = getRedirectFromLocation(location)
29+
const redirectTo = retrieveRedirect(location.search)
3830
const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined
3931

4032
const onSubmit = async ({ email, password }: { email: string; password: string }) => {

site/src/test_helpers/entities.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,39 @@ export const MockSessionToken = { session_token: "my-session-token" }
55
export const MockAPIKey = { key: "my-api-key" }
66

77
export const MockUser: UserResponse = {
8-
id: "test-user-id",
8+
id: "test-user",
99
username: "TestUser",
1010
1111
created_at: "",
1212
}
1313

14-
export const MockProject: Project = {
15-
id: "project-id",
14+
export const MockOrganization: Organization = {
15+
id: "test-org",
16+
name: "Test Organization",
1617
created_at: "",
1718
updated_at: "",
18-
organization_id: "test-org",
19-
name: "Test Project",
20-
provisioner: "test-provisioner",
21-
active_version_id: "",
2219
}
2320

2421
export const MockProvisioner: Provisioner = {
2522
id: "test-provisioner",
2623
name: "Test Provisioner",
2724
}
2825

29-
export const MockOrganization: Organization = {
30-
id: "test-org",
31-
name: "Test Organization",
26+
export const MockProject: Project = {
27+
id: "test-project",
3228
created_at: "",
3329
updated_at: "",
30+
organization_id: MockOrganization.id,
31+
name: "Test Project",
32+
provisioner: MockProvisioner.id,
33+
active_version_id: "",
3434
}
3535

3636
export const MockWorkspace: Workspace = {
3737
id: "test-workspace",
3838
name: "Test-Workspace",
3939
created_at: "",
4040
updated_at: "",
41-
project_id: "project-id",
42-
owner_id: "test-user-id",
41+
project_id: MockProject.id,
42+
owner_id: MockUser.id,
4343
}

site/src/test_helpers/handlers.ts

+22
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,26 @@ import { rest } from "msw"
22
import * as M from "./entities"
33

44
export const handlers = [
5+
// organizations
6+
rest.get("/api/v2/organizations/:organizationId", async (req, res, ctx) => {
7+
return res(ctx.status(200), ctx.json(M.MockOrganization))
8+
}),
9+
rest.get("/api/v2/organizations/:organizationId/projects/:projectId", async (req, res, ctx) => {
10+
return res(ctx.status(200), ctx.json(M.MockProject))
11+
}),
12+
13+
// projects
14+
rest.get("/api/v2/projects/:projectId", async (req, res, ctx) => {
15+
return res(ctx.status(200), ctx.json(M.MockProject))
16+
}),
17+
18+
// users
519
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
620
return res(ctx.status(200), ctx.json(M.MockWorkspace))
721
}),
22+
rest.get("/api/v2/users/me/organizations/:organizationId", async (req, res, ctx) => {
23+
return res(ctx.status(200), ctx.json(M.MockOrganization))
24+
}),
825
rest.post("/api/v2/users/login", async (req, res, ctx) => {
926
return res(ctx.status(200), ctx.json(M.MockSessionToken))
1027
}),
@@ -17,4 +34,9 @@ export const handlers = [
1734
rest.get("/api/v2/users/me/keys", async (req, res, ctx) => {
1835
return res(ctx.status(200), ctx.json(M.MockAPIKey))
1936
}),
37+
38+
// workspaces
39+
rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => {
40+
return res(ctx.status(200), ctx.json(M.MockWorkspace))
41+
}),
2042
]

site/src/util/redirect.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { embedRedirect, retrieveRedirect } from "./redirect"
2+
3+
describe("redirect helper functions", () => {
4+
describe("embedRedirect", () => {
5+
it("embeds the page to return to in the URL", () => {
6+
const result = embedRedirect("/workspaces", "/page")
7+
expect(result).toEqual("/page?redirect=%2Fworkspaces")
8+
})
9+
it("defaults to navigating to the login page", () => {
10+
const result = embedRedirect("/workspaces")
11+
expect(result).toEqual("/login?redirect=%2Fworkspaces")
12+
})
13+
})
14+
describe("retrieveRedirect", () => {
15+
it("retrieves the page to return to from the URL", () => {
16+
const result = retrieveRedirect("?redirect=%2Fworkspaces")
17+
expect(result).toEqual("/workspaces")
18+
})
19+
})
20+
})

0 commit comments

Comments
 (0)