feat(fe-navigationbar): add logout button to navigation + logout hook

This commit is contained in:
Marc 2024-01-03 13:20:16 -05:00
parent 6036c1e819
commit 3edc4b7ec6
Signed by: marc
GPG key ID: 048E042F22B5DC79
4 changed files with 170 additions and 10 deletions

View file

@ -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")
})
})
})

View file

@ -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>
)

View 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"))
})
})

View 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,
}