mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Initial Login flow (#42)
This just implements a basic sign-in flow, using the new endpoints in #29 :  This brings over several dependencies that are necessary: - `formik` - `yep` Ports over some v1 code to bootstrap it: - `FormTextField` - `PasswordField` - `CoderIcon` And implements basic sign-in: Fixes #37 Fixes #43 This does not implement it navbar integration (importantly - there is no way to sign out yet, unless you manually delete your `session_token`). I'll do that in the next PR - figured this was big enough to get reviewed.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import singletonRouter from "next/router"
|
||||
import mockRouter from "next-router-mock"
|
||||
import React from "react"
|
||||
import { SWRConfig } from "swr"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
|
||||
import { User, UserProvider, useUser } from "./UserContext"
|
||||
|
||||
namespace Helpers {
|
||||
// Helper component that renders out the state of the `useUser` hook.
|
||||
// It just renders simple text in the 'error', 'me', and 'loading' states,
|
||||
// so that the test can get a peak at the state of the hook.
|
||||
const TestComponent: React.FC<{ redirectOnFailure: boolean }> = ({ redirectOnFailure }) => {
|
||||
const { me, error } = useUser(redirectOnFailure)
|
||||
|
||||
if (error) {
|
||||
return <div>{`Error: ${error.toString()}`}</div>
|
||||
}
|
||||
if (me) {
|
||||
return <div>{`Me: ${me.toString()}`}</div>
|
||||
}
|
||||
|
||||
return <div>Loading</div>
|
||||
}
|
||||
|
||||
// Helper to render a userContext, and all the scaffolding needed
|
||||
// (an SWRConfig as well as a UserPRovider)
|
||||
export const renderUserContext = (
|
||||
simulatedRequest: () => Promise<User>,
|
||||
redirectOnFailure: boolean,
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
// Set up an SWRConfig that works for testing - we'll simulate a request,
|
||||
// and set up the cache to reset every test.
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: simulatedRequest,
|
||||
// Reset cache for every test. Without this, requests will be cached between test cases.
|
||||
provider: () => new Map(),
|
||||
}}
|
||||
>
|
||||
<UserProvider>
|
||||
<TestComponent redirectOnFailure={redirectOnFailure} />
|
||||
</UserProvider>
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
|
||||
export const mockUser: User = {
|
||||
id: "test-user-id",
|
||||
username: "TestUser",
|
||||
email: "test@coder.com",
|
||||
created_at: "",
|
||||
}
|
||||
}
|
||||
|
||||
describe("UserContext", () => {
|
||||
const failingRequest = () => Promise.reject("Failed to load user")
|
||||
const successfulRequest = () => Promise.resolve(Helpers.mockUser)
|
||||
|
||||
// Reset the router to '/' before every test
|
||||
beforeEach(() => {
|
||||
mockRouter.setCurrentUrl("/")
|
||||
})
|
||||
|
||||
it("shouldn't redirect if user fails to load and redirectOnFailure is false", async () => {
|
||||
// When
|
||||
render(Helpers.renderUserContext(failingRequest, false))
|
||||
|
||||
// Then
|
||||
// Verify we get an error message
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Error:", { exact: false })).toBeDefined()
|
||||
})
|
||||
// ...and the route should be unchanged
|
||||
expect(singletonRouter).toMatchObject({ asPath: "/" })
|
||||
})
|
||||
|
||||
it("should redirect if user fails to load and redirectOnFailure is true", async () => {
|
||||
// When
|
||||
render(Helpers.renderUserContext(failingRequest, true))
|
||||
|
||||
// Then
|
||||
// Verify we route to the login page
|
||||
await waitFor(() => expect(singletonRouter).toMatchObject({ asPath: "/login?redirect=%2F" }))
|
||||
})
|
||||
|
||||
it("should not redirect if user loads and redirectOnFailure is true", async () => {
|
||||
// When
|
||||
render(Helpers.renderUserContext(successfulRequest, true))
|
||||
|
||||
// Then
|
||||
// Verify the user is rendered
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Me:", { exact: false })).toBeDefined()
|
||||
})
|
||||
// ...and the route should be unchanged
|
||||
expect(singletonRouter).toMatchObject({ asPath: "/" })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useRouter } from "next/router"
|
||||
import React, { useContext, useEffect } from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
export interface User {
|
||||
readonly id: string
|
||||
readonly username: string
|
||||
readonly email: string
|
||||
readonly created_at: string
|
||||
}
|
||||
|
||||
export interface UserContext {
|
||||
readonly error?: Error
|
||||
readonly me?: User
|
||||
}
|
||||
|
||||
const UserContext = React.createContext<UserContext>({})
|
||||
|
||||
export const useUser = (redirectOnError = false): UserContext => {
|
||||
const ctx = useContext(UserContext)
|
||||
const router = useRouter()
|
||||
|
||||
const requestError = ctx.error
|
||||
useEffect(() => {
|
||||
if (redirectOnError && requestError) {
|
||||
router.push({
|
||||
pathname: "/login",
|
||||
query: {
|
||||
redirect: router.asPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [redirectOnError, requestError])
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
export const UserProvider: React.FC = (props) => {
|
||||
const { data, error } = useSWR("/api/v2/user")
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
error: error,
|
||||
me: data,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user