feat(fe-navigationbar): add logout button to navigation + logout hook
This commit is contained in:
parent
6036c1e819
commit
3edc4b7ec6
4 changed files with 170 additions and 10 deletions
|
@ -1,20 +1,32 @@
|
|||
import { vi, it, describe, expect } from "vitest"
|
||||
import { within } from "@testing-library/dom"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { screen, render, waitFor } from "@testing-library/react"
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
|
||||
|
||||
import {
|
||||
renderWithContexts as render,
|
||||
getAxiosMockAdapter,
|
||||
} from "../../tests/helpers"
|
||||
import * as locationHook from "../../contexts/LocationContext"
|
||||
import { getAxiosMockAdapter } from "../../tests/helpers"
|
||||
import NavigationBar from "."
|
||||
|
||||
import { type FileData } from "../../types/files"
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
return {
|
||||
...render(<NavigationBar />, { wrapper }),
|
||||
user: userEvent.setup(),
|
||||
}
|
||||
}
|
||||
|
||||
describe("NavigationBar", () => {
|
||||
describe("Upload functionality", () => {
|
||||
it("Renders the upload button", () => {
|
||||
const { getByText } = render(<NavigationBar />)
|
||||
getByText("Upload file")
|
||||
renderComponent()
|
||||
expect(screen.queryByText("Upload file")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("Clicking the upload button and selecting a file POSTs the file", async () => {
|
||||
|
@ -27,10 +39,8 @@ describe("NavigationBar", () => {
|
|||
size: 1,
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
|
||||
const { getByText, container } = render(<NavigationBar />)
|
||||
const uploadButton = getByText("Upload file")
|
||||
const { user, container } = renderComponent()
|
||||
const uploadButton = screen.getByText("Upload file")
|
||||
const mockFile = new File(["test"], "test.txt", { type: "text/plain" })
|
||||
|
||||
const fileInput = container.querySelector('input[type="file"]')
|
||||
|
@ -48,4 +58,40 @@ describe("NavigationBar", () => {
|
|||
expect(postRequest.url).toMatch(expectedUrlPattern)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Log out", () => {
|
||||
it("renders a logout button", () => {
|
||||
renderComponent()
|
||||
|
||||
expect(screen.queryByText("Log out")).toBeInTheDocument()
|
||||
|
||||
const button = screen.getByText("Log out")
|
||||
})
|
||||
|
||||
it("sends a logout request and redirects to the login page when logging out", async () => {
|
||||
const axiosMock = getAxiosMockAdapter()
|
||||
axiosMock.onDelete("/auth/session/").reply(204)
|
||||
const mockNavigate = vi.fn()
|
||||
const mockLocationHook = vi
|
||||
.spyOn(locationHook, "useLocationContext")
|
||||
.mockImplementation(() => ({
|
||||
location: {
|
||||
path: "",
|
||||
label: "",
|
||||
params: {},
|
||||
pattern: "",
|
||||
},
|
||||
navigate: mockNavigate,
|
||||
}))
|
||||
|
||||
const { user } = renderComponent()
|
||||
|
||||
const logoutButton = screen.getByText("Log out")
|
||||
|
||||
await user.click(logoutButton)
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.delete.length).toEqual(1))
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/login")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -8,6 +8,7 @@ import Typography from "@mui/material/Typography"
|
|||
import UploadIcon from "@mui/icons-material/Upload"
|
||||
|
||||
import { useFileMutations } from "../../hooks/files"
|
||||
import { useLogout } from "../../queries/auth"
|
||||
|
||||
function UploadFileButton() {
|
||||
const fileRef = useRef(null)
|
||||
|
@ -45,6 +46,8 @@ function UploadFileButton() {
|
|||
}
|
||||
|
||||
function NavigationBar() {
|
||||
const { logout } = useLogout()
|
||||
|
||||
return (
|
||||
<AppBar position="sticky" sx={{ display: "flex" }}>
|
||||
<Toolbar>
|
||||
|
@ -52,6 +55,14 @@ function NavigationBar() {
|
|||
Rotini
|
||||
</Typography>
|
||||
<UploadFileButton />
|
||||
<Button
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
logout()
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
|
|
60
frontend/src/queries/auth.test.tsx
Normal file
60
frontend/src/queries/auth.test.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import React from "react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { renderHook, waitFor } from "@testing-library/react"
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
|
||||
import AxiosMockAdapter from "axios-mock-adapter"
|
||||
|
||||
import * as locationHook from "../contexts/LocationContext"
|
||||
import axiosWithDefaults from "../axios"
|
||||
import { useLogout } from "./auth"
|
||||
|
||||
function WithProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe("useLogout", () => {
|
||||
it("sends a request to the logout api", async () => {
|
||||
const axios = new AxiosMockAdapter(axiosWithDefaults)
|
||||
|
||||
axios.onDelete("/auth/session/").reply(204)
|
||||
|
||||
const { result } = renderHook(() => useLogout(), { wrapper: WithProviders })
|
||||
|
||||
result.current.logout()
|
||||
|
||||
await waitFor(() => expect(axios.history.delete.length).toEqual(1))
|
||||
|
||||
const deleteRequest = axios.history.delete[0]
|
||||
|
||||
expect(deleteRequest.url).toEqual("/auth/session/")
|
||||
})
|
||||
|
||||
it("navigates to the login page on success", async () => {
|
||||
const mockNavigate = vi.fn()
|
||||
const mockLocationHook = vi
|
||||
.spyOn(locationHook, "useLocationContext")
|
||||
.mockImplementation(() => ({
|
||||
location: {
|
||||
path: "",
|
||||
label: "",
|
||||
params: {},
|
||||
pattern: "",
|
||||
},
|
||||
navigate: mockNavigate,
|
||||
}))
|
||||
|
||||
const axios = new AxiosMockAdapter(axiosWithDefaults)
|
||||
|
||||
axios.onDelete("/auth/session/").reply(204)
|
||||
|
||||
const { result } = renderHook(() => useLogout(), { wrapper: WithProviders })
|
||||
|
||||
result.current.logout()
|
||||
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith("/login"))
|
||||
})
|
||||
})
|
43
frontend/src/queries/auth.ts
Normal file
43
frontend/src/queries/auth.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Authentication queries
|
||||
*
|
||||
* This module contains the set of queries (both fetches and mutations) that
|
||||
* affect the user's session.
|
||||
*
|
||||
*/
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useLocationContext } from "../contexts/LocationContext"
|
||||
import axiosWithDefaults from "../axios"
|
||||
|
||||
/*
|
||||
* Handles the log-out interaction.
|
||||
*
|
||||
* Using `logout` will instruct the application to invalidate
|
||||
* the current authentication token and will redirect the user
|
||||
* to the login page.
|
||||
*/
|
||||
function useLogout() {
|
||||
const { navigate } = useLocationContext()
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return axiosWithDefaults.delete("/auth/session/")
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate("/login")
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate, isError, isPending, ...rest } = logoutMutation
|
||||
|
||||
return {
|
||||
logout: mutate,
|
||||
isError,
|
||||
isPending,
|
||||
}
|
||||
}
|
||||
|
||||
export { useLogout }
|
||||
|
||||
export default {
|
||||
useLogout,
|
||||
}
|
Reference in a new issue