feat(fe-login): redirect on successful login + provide error feedback on error
This commit is contained in:
parent
53d43ff070
commit
8a70c144d5
5 changed files with 107 additions and 9 deletions
|
@ -27,6 +27,7 @@
|
|||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.18",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"axios-mock-adapter": "^1.21.5",
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
|
||||
import AxiosMockAdapter from "axios-mock-adapter"
|
||||
|
||||
import axios from "../../axios"
|
||||
import * as locationHook from "../../contexts/LocationContext"
|
||||
import LoginView from "."
|
||||
|
||||
function renderComponent() {
|
||||
|
@ -54,7 +55,7 @@ describe("LoginView", () => {
|
|||
it("sends a request to the authentication API on submit", async () => {
|
||||
const axiosMockAdapter = new AxiosMockAdapter(axios)
|
||||
|
||||
axiosMockAdapter.onPost("/auth/session/").reply(201, { token: "testtoken" })
|
||||
axiosMockAdapter.onPost("/auth/session/").reply(201)
|
||||
|
||||
const { user } = renderComponent()
|
||||
|
||||
|
@ -79,4 +80,72 @@ describe("LoginView", () => {
|
|||
|
||||
expect(requestBody).toEqual(testInput)
|
||||
})
|
||||
|
||||
it("displays error messaging if the login attempt is not successful", async () => {
|
||||
const axiosMockAdapter = new AxiosMockAdapter(axios)
|
||||
|
||||
axiosMockAdapter.onPost("/auth/session/").reply(400)
|
||||
|
||||
const { user } = renderComponent()
|
||||
|
||||
const testInput = {
|
||||
username: "test@domain.com",
|
||||
password: "password",
|
||||
}
|
||||
|
||||
const emailInput = screen.getByLabelText(/email address login input/i)
|
||||
await user.type(emailInput, testInput.username)
|
||||
|
||||
const passwordInput = screen.getByLabelText(/password login input/i)
|
||||
await user.type(passwordInput, testInput.password)
|
||||
|
||||
const submitButton = screen.getByText("Log in", { selector: "button" })
|
||||
|
||||
await user.click(submitButton)
|
||||
|
||||
await waitFor(() => expect(screen.getByRole("alert")).toBeInTheDocument())
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/this combination of email and password does not match our records\. verify if the email or password are incorrect\./i,
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("redirects the user on success", async () => {
|
||||
const mockNavigate = jest.fn()
|
||||
const mockLocationHook = jest
|
||||
.spyOn(locationHook, "useLocationContext")
|
||||
.mockImplementation(() => ({
|
||||
location: {
|
||||
path: "",
|
||||
label: "",
|
||||
params: {},
|
||||
pattern: "",
|
||||
},
|
||||
navigate: mockNavigate,
|
||||
}))
|
||||
const axiosMockAdapter = new AxiosMockAdapter(axios)
|
||||
|
||||
axiosMockAdapter.onPost("/auth/session/").reply(201)
|
||||
|
||||
const { user } = renderComponent()
|
||||
|
||||
const testInput = {
|
||||
username: "test@domain.com",
|
||||
password: "password",
|
||||
}
|
||||
|
||||
const emailInput = screen.getByLabelText(/email address login input/i)
|
||||
await user.type(emailInput, testInput.username)
|
||||
|
||||
const passwordInput = screen.getByLabelText(/password login input/i)
|
||||
await user.type(passwordInput, testInput.password)
|
||||
|
||||
const submitButton = screen.getByText("Log in", { selector: "button" })
|
||||
|
||||
await user.click(submitButton)
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -9,25 +9,29 @@ import TextField from "@mui/material/TextField"
|
|||
import InputLabel from "@mui/material/InputLabel"
|
||||
import Button from "@mui/material/Button"
|
||||
import Link from "@mui/material/Link"
|
||||
import Alert from "@mui/material/Alert"
|
||||
|
||||
import axiosWithDefaults from "../../axios"
|
||||
import TextInput from "../TextInput"
|
||||
import { useLocationContext } from "../../contexts/LocationContext"
|
||||
|
||||
function LoginView() {
|
||||
const [emailAddress, setEmailAddress] = React.useState<string>("")
|
||||
const [password, setPassword] = React.useState<string>("")
|
||||
const { navigate } = useLocationContext()
|
||||
|
||||
const { mutate } = useMutation({
|
||||
const { mutate, isError, isLoading } = useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
password,
|
||||
}: { email: string; password: string }) => {
|
||||
const response = await axiosWithDefaults.post("/auth/session/", {
|
||||
return axiosWithDefaults.post("/auth/session/", {
|
||||
username: email,
|
||||
password,
|
||||
})
|
||||
|
||||
return response
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
navigate("/")
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -71,22 +75,29 @@ function LoginView() {
|
|||
<Box sx={{ display: "flex", justifyContent: "center", width: "100%" }}>
|
||||
<FormGroup
|
||||
sx={{
|
||||
flexGrow: 0.1,
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
textAlign: "center",
|
||||
width: "600px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h1" sx={{ fontSize: "2rem" }}>
|
||||
Log in
|
||||
</Typography>
|
||||
{isError ? (
|
||||
<Alert severity="error">
|
||||
{
|
||||
"This combination of email and password does not match our records. Verify if the email or password are incorrect."
|
||||
}
|
||||
</Alert>
|
||||
) : null}
|
||||
{emailField}
|
||||
{passwordField}
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onLoginClick}
|
||||
aria-label="submit login"
|
||||
disabled={!isFormValid}
|
||||
disabled={!isFormValid || isLoading}
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["jest"]
|
||||
"types": ["jest", "node"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2440,6 +2440,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^20.10.6":
|
||||
version: 20.10.6
|
||||
resolution: "@types/node@npm:20.10.6"
|
||||
dependencies:
|
||||
undici-types: "npm:~5.26.4"
|
||||
checksum: 08471220d3cbbb6669835c4b78541edf5eface8f2c2e36c550cfa4ff73da73071c90e200a06359fac25d6564127597c23e178128058fb676824ec23d5178a017
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/parse-json@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@types/parse-json@npm:4.0.0"
|
||||
|
@ -6453,6 +6462,7 @@ __metadata:
|
|||
"@testing-library/react": "npm:^14.1.2"
|
||||
"@testing-library/user-event": "npm:^14.5.1"
|
||||
"@types/jest": "npm:^29.5.3"
|
||||
"@types/node": "npm:^20.10.6"
|
||||
"@types/react": "npm:^18.2.18"
|
||||
"@types/react-dom": "npm:^18.2.7"
|
||||
axios: "npm:^1.4.0"
|
||||
|
@ -7001,6 +7011,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~5.26.4":
|
||||
version: 5.26.5
|
||||
resolution: "undici-types@npm:5.26.5"
|
||||
checksum: 0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "unique-filename@npm:3.0.0"
|
||||
|
|
Reference in a new issue