Mocking custom callback hook react-testing-library - reactjs

I have created the following custom hook, and I'm having trouble mocking the hook in a way that the returned data would be updated when the callback is called.
export const useLazyFetch = ({ method, url, data, config, withAuth = true }: UseFetchArgs): LazyFetchResponse => {
const [res, setRes] = useState({ data: null, error: null, loading: false});
const callFetch = useCallback(() => {
setRes({ data: null, error: null, loading: true});
const jwtToken = loadItemFromLocalStorage('accessToken');
const authConfig = {
headers: {
Authorization: `Bearer ${jwtToken}`
}
};
const combinedConfig = Object.assign(withAuth ? authConfig : {}, config);
axios[method](url, data, combinedConfig)
.then(res => setRes({ data: res.data, loading: false, error: null}))
.catch(error => setRes({ data: null, loading: false, error}))
}, [method, url, data, config, withAuth])
return { res, callFetch };
};
The test is pretty simple, when a user clicks a button to perform the callback I want to ensure that the appropriate elements appear, right now I'm mocking axios which works but I was wondering if there is a way to mock the useLazyFetch method in a way that res is updated when the callback is called. This is the current test
it('does some stuff', async () => {
(axios.post as jest.Mock).mockReturnValue({ status: 200, data: { foo: 'bar' } });
const { getByRole, getByText, user } = renderComponent();
user.click(getByRole('button', { name: 'button text' }));
await waitFor(() => expect(getByText('success message')).toBeInTheDocument());
});
Here's an example of how I'm using useLazyFetch
const Component = ({ props }: Props) => {
const { res, callFetch } = useLazyFetch({
method: 'post',
url: `${BASE_URL}/some/endpoint`,
data: requestBody
});
const { data: postResponse, loading: postLoading, error: postError } = res;
return (
<Element
header={header}
subHeader={subHeader}
>
<Button
disabled={postLoading}
onClick={callFetch}
>
Submit Post Request
</Button>
</Element>
);
}

axios is already tested so there's no point in writing tests for that. We should be testing useLazyFetch itself. However, I might suggest abstracting away the axios choice and writing a more generic useAsync hook.
// hooks.js
import { useState, useEffect } from "react"
function useAsync(func, deps = []) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [data, setData] = useState(null)
useEffect(_ => {
let mounted = true
async function run() {
try { if (mounted) setData(await func(...deps)) }
catch (e) { if (mounted) setError(e) }
finally { if (mounted) setLoading(false) }
}
run()
return _ => { mounted = false }
}, deps)
return { loading, error, data }
}
export { useAsync }
But we can't stop there. Other improvements will help too, like a better API abstraction -
// api.js
import axios from "axios"
import { createContext, useContext, useMemo } from "react"
import { useLocalStorage } from "./hooks.js"
function client(jwt) {
// https://axios-http.com/docs/instance
return axios.create(Object.assign(
{},
jwt && { headers: { Authorization: `Bearer ${jwt}` } }
))
}
function APIRoot({ children }) {
const jwt = useLocalStorage("accessToken")
const context = useMemo(_ => client(jwt), [jwt])
return <ClientContext.Provider value={context}>
{children}
</ClientContext.Provider>
}
function useClient() {
return useContext(ClientContext)
}
const ClientContext = createContext(null)
export { APIRoot, useClient }
When a component is a child of APIRoot, it has access to the axios client instance -
<APIRoot>
<User id={4} /> {/* access to api client inside APIRoot */}
</APIRoot>
// User.js
import { useClient } from "./api.js"
import { useAsync } from "./hooks.js"
function User({ userId }) {
const client = useClient() // <- access the client
const {data, error, loading} = useAsync(id => { // <- generic hook
return client.get(`/users/${id}`).then(r => r.data) // <- async
}, [userId]) // <- dependencies
if (error) return <p>{error.message}</p>
if (loading) return <p>Loading...</p>
return <div data-user-id={userId}>
{data.username}
{data.avatar}
</div>
}
export default User
That's helpful, but the component is still concerned with API logic of constructing User URLs and things like accessing the .data property of the axios response. Let's push all of that into the API module -
// api.js
import axios from "axios"
import { createContext, useContext, useMemo } from "react"
import { useLocalStorage } from "./hooks.js"
function client(jwt) {
return axios.create(Object.assign(
{ transformResponse: res => res.data }, // <- auto return res.data
jwt && { headers: { Authorization: `Bearer ${jwt}` } }
))
}
function api(client) {
return {
getUser: (id) => // <- user-friendly functions
client.get(`/users/${id}`), // <- url logic encapsulated
createUser: (data) =>
client.post(`/users`, data),
loginUser: (email, password) =>
client.post(`/login`, {email,password}),
// ...
}
}
function APIRoot({ children }) {
const jwt = useLocalStorage("accessToken")
const context = useMemo(_ => api(client(jwt)), [jwt]) // <- api()
return <APIContext.Provider value={context}>
{children}
</APIContext.Provider>
}
const APIContext = createContext({})
const useAPI = _ => useContext(APIContext)
export { APIRoot, useAPI }
The pattern above is not sophisticated. It could be easily modularized for more complex API designs. Some segments of the API may require authorization, others are public. The API module gives you a well-defined area for all of this. The components are now freed from this complexity -
// User.js
import { useAPI } from "./api.js"
import { useAsync } from "./hooks.js"
function User({ userId }) {
const { getUser } = useAPI()
const {data, error, loading} = useAsync(getUser, [userId]) // <- ez
if (error) return <p>{error.message}</p>
if (loading) return <p>Loading...</p>
return <div data-user-id={userId}>
{data.username}
{data.avatar}
</div>
}
export default User
As for testing, now mocking any component or function is easy because everything has been isolated. You could also create a <TestingAPIRoot> in the API module that creates a specialized context for use in testing.
See also -
react-query
useSWR
useLocalStorage

Related

Unable to test-cover the catch block in my react code

I'm writing test cases using react testing library. Having issues covering the catch block which is failing my build.
Below is my component code with a simple button click handler calling the api.
TestComponent.js
import Button from '#material-ui/core/Button';
import MyApiFile from '../api/MyApiFile';
function someFunction(setIsSuccess, setIsError) {
setIsSuccess(false);
setIsError(false);
const { request } = MyApiFile.getThisOrThat();
request.then(({ status, response }) => {
someOtherFunction();
})
.catch((err) => {
setIsSuccess(true);
setIsError(true);
});
}
function TestComponent() {
const [isSuccess, setIsSuccess] = useState(false);
const [isFail, setIsFail] = useState(false);
const handleSave = () => {
someFunction(setIsSuccess, setIsFail)
};
return(
<Button data-testid="save-btn" onClick={ handleSave }>
Save
</Button>
)
}
export default TestComponent;
The API urls are generated and returned by the respective static functions of the MyApiFile class
MyApiFile.js
import axios, { CancelToken } from 'axios';
import urljoin from 'url-join';
import URI from 'urijs';
export default class MyApiFile {
static getURI(path){
const { config = {} } = window;
const { apiBasePath = '/' } = config;
const apiUri = new URI(urljoin(apiBasePath, path));
return apiUri;
}
static apiRequest(config){
const source = CancelToken.source();
config.cancelToken = source.token;
const request = axios.request(config)
return { request, source };
}
static getThisOrThat(param, blah) {
const apiUrl = MyApiFile.getURI(`/some/path/${param}`);
return MyApiFile.apiRequest({
method: 'post',
url: String(apiUrl),
data: blah
});
}
}
Below are the three approaches (marked by -->) that I tried after researching on internet,
TestComponent.test.js
beforeEach(()=> {
act(() => render(<TestComponent />));
})
it('should do so n so' , () => {
const mockError = { error: 'something went wrong' };
--> jest.mock('../MyApiFile', ()=>({
getThisOrThat: jest.fn( Promise.reject(mockError))
}))
--> jest.spyOn(MyApiFile, 'getThisOrThat').mockRejectedValue(mockError)
const save = screen.getByTestId('save-button');
await act(() => {
fireEvent.click(save);
})
expect(..........).(......);
});
import axios from 'axios'
--> import MockAdapter from 'axios-mock-adapter'
const mock = new MockAdapter(axios);
mock.onPost(/]/path\/some\/?/g).reply(config=>{
const {data} = config;
try{
return [200, successes: data, errors: []},{}];
}catch(err){
return [500, { error: 'sometin wrong' }, {}]
}
});
None of the above worked to get me coverage for the catch block that sets the error state which when set, would increase branch, line coverage. I need help to cover the catch block. Thanks in advance.

Avoid login form flashing in React

I have a signup/login workflow in React (NextJS), and everything is working correctly; i made a custom hook to remotely check if the user is authenticated based on localStorage jwt:
import React, { useState, useEffect } from 'react';
import axios from '../lib/api';
const useUser = () => {
const [logged, setIsLogged] = useState(false);
const [user, setUser] = useState('');
useEffect(async () => {
const jwt = localStorage.getItem('jwt');
if (!jwt) return;
await axios
.get('/api/users/me', {
headers: {
Authorization: `Bearer ${jwt}`,
},
})
.then((response) => {
setUser(response.data);
setIsLogged(true);
})
.catch((error) => {});
}, []);
return [logged, user, setIsLogged];
};
export default useUser;
This hooks works corectly in 99% of cases, but when i go on login form page, the login form flashes for a sec to logged in users, since the logged status is false before the check is initialized
import React, { useEffect, useState } from 'react';
import useUser from '../../lib/useUser';
import { useRouter } from 'next/router';
import LoginForm from '../../components/LoginForm';
function Login() {
const { push } = useRouter();
const [logged] = useUser();
console.log(ljwt, logged);
if (logged) {
//push('/');
return <p>nored</p>;
}
if (!logged) {
return <LoginForm />;
}
}
export default Login;
how can i avoid this? i tried to pass to useUser the jwt, so it assume the user is logged in while performing the remote check, but it is not really working as expected.
any suggestion?
Don't render the login form when the login state is still indeterminate.
By the way, useEffect functions can't be async in themselves, since they need to either return nothing or a cleanup function; async functions always return a promise.
async function getLoginState() {
const jwt = localStorage.getItem("jwt");
if (!jwt) return [false, null];
const resp = await axios.get("/api/users/me", {
headers: {
Authorization: `Bearer ${jwt}`,
},
});
return [true, response.data];
}
/**
* Get user login state.
*
* Returns undefined if the login state is not yet known.
* Returns a 2-item array [loginState, user] otherwise.
* `user` can be null when `loginState` is false.
*/
function useLoginState() {
const [loginState, setLoginState] = useState(undefined);
useEffect(() => {
getLoginState().then(setLoginState);
}, []);
return loginState;
}
function Login() {
const { push } = useRouter();
const loginState = useLoginState();
if (loginState === undefined) {
return <>Loading...</>;
}
const [logged, user] = loginState;
if (logged) {
return <p>Hi, {JSON.stringify(user)}</p>;
} else {
return <LoginForm />;
}
}

Why does the following HOC keep refreshing even though it is returning a memoized component?

For context, I am working with integrating a Django Rest Framework backend with Next.js + Next-Auth. I have most of the integration down, except one part. The requirement is to have a refresh token system that will try to refresh the access token when it is almost expired. Here is the logic that I have:
/api/auth/[...nextauth].ts
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import { NextAuthOptions } from "next-auth";
import Providers from "next-auth/providers";
import axios from "axios";
import { AuthenticatedUser } from "../../../types";
import { JwtUtils, UrlUtils } from "../../../constants/Utils";
namespace NextAuthUtils {
export const refreshToken = async function (refreshToken) {
try {
const response = await axios.post(
// "http://localhost:8000/api/auth/token/refresh/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"auth",
"token",
"refresh",
),
{
refresh: refreshToken,
},
);
const { access, refresh } = response.data;
// still within this block, return true
return [access, refresh];
} catch {
return [null, null];
}
};
}
const settings: NextAuthOptions = {
secret: process.env.SESSION_SECRET,
session: {
jwt: true,
maxAge: 24 * 60 * 60, // 24 hours
},
jwt: {
secret: process.env.JWT_SECRET,
},
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async signIn(user: AuthenticatedUser, account, profile) {
// may have to switch it up a bit for other providers
if (account.provider === "google") {
// extract these two tokens
const { accessToken, idToken } = account;
// make a POST request to the DRF backend
try {
const response = await axios.post(
// tip: use a seperate .ts file or json file to store such URL endpoints
// "http://127.0.0.1:8000/api/social/login/google/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"social",
"login",
account.provider,
),
{
access_token: accessToken, // note the differences in key and value variable names
id_token: idToken,
},
);
// extract the returned token from the DRF backend and add it to the `user` object
const { access_token, refresh_token } = response.data;
user.accessToken = access_token;
user.refreshToken = refresh_token;
return true; // return true if everything went well
} catch (error) {
return false;
}
}
return false;
},
async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
if (user) {
const { accessToken, refreshToken } = user;
// reform the `token` object from the access token we appended to the `user` object
token = {
...token,
accessToken,
refreshToken,
};
// remove the tokens from the user objects just so that we don't leak it somehow
delete user.accessToken;
delete user.refreshToken;
return token;
}
// token has been invalidated, try refreshing it
if (JwtUtils.isJwtExpired(token.accessToken as string)) {
const [
newAccessToken,
newRefreshToken,
] = await NextAuthUtils.refreshToken(token.refreshToken);
if (newAccessToken && newRefreshToken) {
token = {
...token,
accessToken: newAccessToken,
refreshToken: newRefreshToken,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60),
};
return token;
}
// unable to refresh tokens from DRF backend, invalidate the token
return {
...token,
exp: 0,
};
}
// token valid
return token;
},
async session(session, userOrToken) {
session.accessToken = userOrToken.accessToken;
return session;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, settings);
Next, the example in the Next-Auth documentation shows the use of useSession() hook. But I am not a fan of it because:
It does not update the state of the session once the access token is refreshed unless the window itself is refreshed (it is an open issue)
It feels like a lot of code repetition on every component that wants to use the session, with the guards that check the existence of session object, whether the session is loading etc. So I wanted to use a HOC.
As such, I came up with the following solutions:
constants/Hooks.tsx
import { Session } from "next-auth";
import { useEffect, useMemo, useState } from "react";
export function useAuth(refreshInterval?: number): [Session, boolean] {
/*
custom hook that keeps the session up-to-date by refreshing it
#param {number} refreshInterval: The refresh/polling interval in seconds. default is 10.
#return {tuple} A tuple of the Session and boolean
*/
const [session, setSession] = useState<Session>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
async function fetchSession() {
let sessionData: Session = null;
setLoading(true);
const response = await fetch("/api/auth/session");
if (response.ok) {
const data: Session = await response.json();
if (Object.keys(data).length > 0) {
sessionData = data;
}
}
setSession(sessionData);
setLoading(false);
}
refreshInterval = refreshInterval || 10;
fetchSession();
const interval = setInterval(() => fetchSession(), refreshInterval * 1000);
return () => clearInterval(interval);
}, []);
return [session, loading];
}
constants/HOCs.tsx
import { Session } from "next-auth";
import { signIn } from "next-auth/client";
import React from "react";
import { useAuth } from "./Hooks";
type TSessionProps = {
session: Session;
};
export function withAuth<P extends object>(Component: React.ComponentType<P>) {
return React.memo(function (props: Exclude<P, TSessionProps>) {
const [session, loading] = useAuth(); // custom hook call
if (loading) {
return <h2>Loading...</h2>;
}
if (!loading && !session) {
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
<pre>{!session && "User is not logged in"}</pre>
</>
);
}
return <Component {...props} session={session} />;
});
}
Then, in a component where I have periodic data fetching requirements (I know this could be achieved in a much better way, this is just a contrived example where I am trying to simulate user inactivity but the app can still work in the background if needed), I am using the HOC:
pages/posts.tsx
import React, { useEffect, useState } from "react";
import Post from "../components/Post";
import { withAuth } from "../constants/HOCs";
import { TPost } from "../constants/Types";
import Link from "next/link";
function Posts(props) {
const { session } = props;
// const [session, loading] = useAuth();
const [posts, setPosts] = useState<TPost[]>([]);
const [fetchingPosts, setFetchingPosts] = useState<boolean>(false);
useEffect(() => {
if (!session) {
return;
}
async function getPosts() {
setFetchingPosts(true);
const response = await fetch("http://127.0.0.1:8000/api/posts", {
method: "get",
headers: new Headers({
Authorization: `Bearer ${session?.accessToken}`,
}),
});
if (response.ok) {
const posts: TPost[] = await response.json();
setPosts(posts);
}
setFetchingPosts(false);
}
// initiate the post fetching mechanism once
getPosts();
const intervalId = setInterval(() => getPosts(), 10 * 1000);
// useEffect cleanup
return () => clearInterval(intervalId);
}, [JSON.stringify(session)]);
// {
// loading && <h2>Loading...</h2>;
// }
// {
// !loading && !session && (
// <>
// Not signed in <br />
// <button onClick={() => signIn()}>Sign in</button>
// <pre>{!session && "User is not logged in"}</pre>
// </>
// );
// }
return (
<div>
<h2>Fetched at {JSON.stringify(new Date())}</h2>
<Link href="/">Back to homepage</Link>
{posts.map((post) => (
<Post key={post.title} post={post} />
))}
</div>
);
}
export default withAuth(Posts);
The problem is that the entire page gets re-rendered due to the withAuth HOC and possibly due to the useAuth hook every 10 seconds. However, I have had no luck trying to debug it. Maybe I am missing something key in my React concepts. I appreciate any and all suggestions/help possible. Thanks in advance.
PS. I am aware of a solution that uses SWR library, but I would like to avoid using that library if at all possible.
I ended up using the useSwr() hook after spending an unworldly amount of time trying to fix this issue. Also ended up writing this article for those who are interested.

access token from auth0provider outside of react components

I'm using the auth0 token provided by the user on login to make api calls via useAuth0.getTokenSilently.
In this example, fetchTodoList, addTodoItem, and updateTodoItem all require a token for authorization. I'd like to be able to extract these functions out in to a separate file (like utils/api-client.js and import them without having to explicitly pass in the token.
import React, { useContext } from 'react'
import { Link, useParams } from 'react-router-dom'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faCircle, faList } from '#fortawesome/free-solid-svg-icons'
import axios from 'axios'
import { queryCache, useMutation, useQuery } from 'react-query'
import { TodoItem } from '../models/TodoItem'
import { TodoInput } from './TodoInput'
import { TodoList as TodoListComponent } from './TodoList'
import { TodoListsContext } from '../store/todolists'
import { TodoListName } from './TodoListName'
import { TodoList } from '../models/TodoList'
import { useAuth0 } from '../utils/react-auth0-wrapper'
export const EditTodoList = () => {
const { getTokenSilently } = useAuth0()
const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
try {
const token = await getTokenSilently!()
const { data } = await axios.get(
`/api/TodoLists/${todoListId}`,
{
headers: {
Authorization: `Bearer ${token}`
}
}
)
return data
} catch (error) {
return error
}
}
const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
try {
const token = await getTokenSilently!()
const { data } = await axios.post(
'/api/TodoItems',
todoItem,
{
headers: {
Authorization: `Bearer ${token}`,
}
}
)
return data
} catch (addTodoListError) {
return addTodoListError
}
}
const updateTodoItem = async (todoItem: TodoItem) => {
try {
const token = await getTokenSilently!()
const { data } = await axios.put(
'/api/TodoItems',
todoItem,
{
headers: {
Authorization: `Bearer ${token}`,
}
}
)
return data
} catch (addTodoListError) {
return addTodoListError
}
}
const [updateTodoItemMutation] = useMutation(updateTodoItem, {
onSuccess: () => {
queryCache.refetchQueries(['todoList', todoListId])
}
})
const [addTodoItemMutation] = useMutation(addTodoItem, {
onSuccess: () => {
console.log('success')
queryCache.refetchQueries(['todoList', todoListId])
}
})
const onAddTodoItem = async (todoItem: TodoItem) => {
try {
await addTodoItemMutation({
...todoItem,
todoListId: parseInt(todoListId, 10)
})
} catch (error) {
// Uh oh, something went wrong
}
}
const { todoListId } = useParams()
const { status, data: todoList, error } = useQuery(['todoList', todoListId], () => fetchTodoList(todoListId))
const { todoLists, setTodoList } = useContext(TodoListsContext)
const todoListIndex = todoLists.findIndex(
list => todoListId === list.id.toString()
)
const setTodoItems = (todoItems: TodoItem[]) => {
// if(todoList) {
// const list = { ...todoList, todoItems }
// setTodoList(todoListIndex, list)
// }
}
const setTodoListName = (name: string) => {
// setTodoList(todoListIndex, { ...todoList, name })
}
return (
<>
<Link className="block flex align-items-center mt-8" to="/">
<span className="fa-layers fa-fw fa-3x block m-auto group">
<FontAwesomeIcon
icon={faCircle}
className="text-teal-500 transition-all duration-200 ease-in-out group-hover:text-teal-600"
/>
<FontAwesomeIcon icon={faList} inverse transform="shrink-8" />
</span>
</Link>
{status === 'success' && !!todoList && (
<>
<TodoListName
todoListName={todoList.name}
setTodoListName={setTodoListName}
/>
<TodoInput
onAddTodoItem={onAddTodoItem}
/>
<TodoListComponent
todoItems={todoList.todoItems}
setTodoItems={setTodoItems}
updateTodo={updateTodoItemMutation}
/>
</>
)}
</>
)
}
Here's a link to the repo: https://github.com/gpspake/todo-client
I was having a similar issue on how to use getAccessTokenSilently outside of a React component, what I ended up with was this:
My HTTP client wrapper
export class HttpClient {
constructor() {
HttpClient.instance = axios.create({ baseURL: process.env.API_BASE_URL });
HttpClient.instance.interceptors.request.use(
async config => {
const token = await this.getToken();
return {
...config,
headers: { ...config.headers, Authorization: `Bearer ${token}` },
};
},
error => {
Promise.reject(error);
},
);
return this;
}
setTokenGenerator(tokenGenerator) {
this.tokenGenerator = tokenGenerator;
return this;
}
getToken() {
return this.tokenGenerator();
}
}
On my App root, I pass the getAccessTokenSilently from auth0
useEffect(() => {
httpClient.setTokenGenerator(getAccessTokenSilently);
}, [getAccessTokenSilently]);
And that's it!
You now have an axios instance ready to do authenticated requests with
This is a variant of the answer by #james-quick, where I am using a "RequestFactory" to generate requests in the axios format, and then just adding the auth header from Auth0
I was facing the same issue, and I got around this limitation by moving all my API call logic into a custom hook that I created:
import { useAuth0 } from '#auth0/auth0-react';
import { useCallback } from 'react';
import makeRequest from './axios';
export const useRequest = () => {
const { getAccessTokenSilently } = useAuth0();
// memoized the function, as otherwise if the hook is used inside a useEffect, it will lead to an infinite loop
const memoizedFn = useCallback(
async (request) => {
const accessToken = await getAccessTokenSilently({ audience: AUDIANCE })
return makeRequest({
...request,
headers: {
...request.headers,
// Add the Authorization header to the existing headers
Authorization: `Bearer ${accessToken}`,
},
});
},
[isAuthenticated, getAccessTokenSilently]
);
return {
requestMaker: memoizedFn,
};
};
export default useRequest;
Usage Example:
import { RequestFactory } from 'api/requestFactory';
const MyAwesomeComponent = () => {
const { requestMaker } = useRequest(); // Custom Hook
...
requestMaker(QueueRequestFactory.create(queueName))
.then((response) => {
// Handle response here
...
});
}
RequestFactory defines and generates the request payload for my different API calls, for example:
export const create = (queueName) => ({ method: 'post', url: '/queue', data: { queueName } });
Here is a full Auth0 integration PR for reference.
I'm not exactly sure why you couldn't access the token inside of your individual functions? Is it because they wouldn't be React function components but just regular functions?
One of the things I have done is create a useFetch hook that can get the user token and attach it to a request itself. Then, instead of exporting those functions specifically,I can just call this new fetch hook. Here's an example of what I mean.
import React from "react"
import { useAuth0 } from "../utils/auth"
const useFetch = () => {
const [response, setResponse] = React.useState(null)
const [error, setError] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const { getTokenSilently } = useAuth0()
const fetchData = async (url, method, body, authenticated, options = {}) => {
setIsLoading(true)
try {
if (authenticated) {
const token = await getTokenSilently()
if (!options.headers) {
options.headers = {}
}
options.headers["Authorization"] = `Bearer ${token}`
}
options.method = method
if (method !== "GET") {
options.body = JSON.stringify(body)
}
const res = await fetch(url, options)
const json = await res.json()
setResponse(json)
setIsLoading(false)
if (res.status === 200) {
return json
}
throw { msg: json.msg }
} catch (error) {
console.error(error)
setError(error)
throw error
}
}
return { response, error, isLoading, fetchData }
}
export default useFetch
Ok, got it!
Now that I understand better, my real question was how to provide an auth0 token to axios requests such that they don't need to be declared in components.
The short answer:
Get the token when auth0 is initialized and register an axios interceptor to set that token as a header value for all axios requests.
The long answer (examples in typescript):
Declare a function that takes a token and registers an axios interceptor
const setAxiosTokenInterceptor = async (accessToken: string): Promise<void> => {
axios.interceptors.request.use(async config => {
const requestConfig = config
if (accessToken) {
requestConfig.headers.common.Authorization = `Bearer ${accessToken}`
}
return requestConfig
})
}
In the auth0provider wrapper, when the auth0 client is initialized and authenticated, get the token with setAxiosTokenInterceptor and pass it to the function that registers the interceptor (Modified example from the Auth0 React SDK Quickstart):
useEffect(() => {
const initAuth0 = async () => {
const auth0FromHook = await createAuth0Client(initOptions)
setAuth0(auth0FromHook)
if (window.location.search.includes('code=')) {
const { appState } = await auth0FromHook.handleRedirectCallback()
onRedirectCallback(appState)
}
auth0FromHook.isAuthenticated().then(
async authenticated => {
setIsAuthenticated(authenticated)
if (authenticated) {
auth0FromHook.getUser().then(
auth0User => {
setUser(auth0User)
}
)
// get token and register interceptor
const token = await auth0FromHook.getTokenSilently()
setAxiosTokenInterceptor(token).then(
() => {setLoading(false)}
)
}
}
)
}
initAuth0().catch()
}, [])
Calling setLoading(false) when the promise is resolved ensures that, if auth0 is finished loading, the interceptor has been registered. Since none of the components that make requests are rendered until auth0 is finished loading, this prevents any calls from being made without the token.
This allowed me to move all of my axios functions in to a separate file and import them in to the components need them. When any of these functions are called, the interceptor will add the token to the header
utils/todo-client.ts
import axios from 'axios'
import { TodoList } from '../models/TodoList'
import { TodoItem } from '../models/TodoItem'
export const fetchTodoLists = async (): Promise<TodoList[]> => {
try {
const { data } = await axios.get(
'/api/TodoLists'
)
return data
} catch (error) {
return error
}
}
export const fetchTodoList = async (todoListId: number): Promise<TodoList> => {
try {
const { data } = await axios.get(
`/api/TodoLists/${todoListId}`
)
return data
} catch (error) {
return error
}
}
export const addTodoItem = async (todoItem: TodoItem): Promise<TodoItem> => {
try {
const { data } = await axios.post(
'/api/TodoItems',
todoItem
)
return data
} catch (addTodoListError) {
return addTodoListError
}
}
...
Full source on github
I like to have my API calls in their own directory (under /api for example) and have the code to call to the API be as small as possible. I took a similar approach to others on here, using Auth0, TypeScript, Axios (including an interceptor) and React hooks.
The TLDR answer
Place your Axios interceptor within a hook, and then use that hook within segmented API hooks (ie, useUserApi, useArticleApi, useCommentApi and so on). You can then cleanly call your API using Auth0.
The long answer
Define your Axios hook, I've only covered the HTTP methods I'm currently using:
# src/api/useAxios.ts
import { useAuth0 } from '#auth0/auth0-react';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
// We wrap Axios methods in a hook, so we can centrally handle adding auth tokens.
const useAxios = () => {
const { getAccessTokenSilently } = useAuth0();
axios.interceptors.request.use(async (config: any) => {
if (config.url.indexOf('http') === -1) {
config.url = `${process.env.REACT_APP_API_ENDPOINT}/${config.url}`;
}
if (typeof config.headers.Authorization === 'undefined') {
config.headers.Authorization = `Bearer ${await getAccessTokenSilently()}`;
}
return config;
});
return {
get: async (url: string, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.get(url, config),
delete: async (url: string, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.delete(url, config),
post: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.post(url, data, config),
put: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.put(url, data, config),
patch: async (url: string, data?: any, config?: AxiosRequestConfig<any> | undefined): Promise<AxiosResponse> => axios.patch(url, data, config),
}
};
export default useAxios;
What I'm doing here is adding a bearer token by calling getAccessTokensSilently() if one is not already defined. Additionally, if HTTP isn't present in my URL then I append the default API URL from my environment variables - this means I can keep my request short and not have to use the full URL every time.
Now I define a hook based on my user API as following:
# src/api/useUserApi.ts
import { UserInterface } from '[REDACTED]/types';
import { AxiosResponse } from 'axios';
import useAxios from './useAxios';
const useUserApi = () => {
const { get, post, put } = useAxios();
return {
getUser: (id: string): Promise<AxiosResponse<UserInterface>> => get(`user/${id}`),
putUser: (user: UserInterface) => put('user', user),
postUser: (user: UserInterface) => post('user', user),
}
};
export default useUserApi;
You can see I expose the underlying HTTP methods from Axios, and then use them in API specific scenarios, getUser, putUser, and postUser.
Now I can go ahead and call my API in some application logic, keeping the API code to the absolute minimum but still allowing full pass through and typing of the Axios objects.
import { useAuth0 } from '#auth0/auth0-react';
import { useNavigate } from 'react-router';
import useUserApi from '../../../api/useUserApi';
const LoginCallback = (): JSX.Element => {
const navigate = useNavigate()
const { user, isAuthenticated, isLoading } = useAuth0();
const { getUser, putUser, postUser} = useUserApi();
const saveUserToApi = async () => {
if (!user?.sub) {
throw new Error ('User does not have a sub');
}
// Try and find the user, if 404 then create a new one for this Auth0 sub
try {
const userResult = await getUser(user.sub);
putUser(Object.assign(user, userResult.data));
navigate('/');
} catch (e: any) {
if (e.response.status === 404) {
postUser({
id: user.sub,
email: user.email,
name: user.name,
givenName: user.givenName,
familyName: user.familyName,
locale: user.locale,
});
navigate('/');
}
}
}
if (isLoading) {
return <div>Logging you in...</div>;
}
if (isAuthenticated && user?.sub) {
saveUserToApi();
return <p>Saved</p>
} else {
return <p>An error occured whilst logging in.</p>;
}
};
export default LoginCallback;
You can note the above postUser, putUser and getUser API requests are all one liners, with only the declaration (const { getUser, putUser, postUser} = useUserApi();) and import being required otherwise.
This answer is definitely standing on the shoulders of giants, but I thought it would be useful all the same to someone who likes to keep their API calls as clean as possible.
There are different ways to solve this.
To not change your code base too much. I would go with a store with a provider and a hook. There are many store libraries out there.
Here is a tiny version which also can be used outside React rendering.
https://github.com/storeon/storeon
This was just one example of a very small store I could find that might fit the bill.
Using a store library outside React could look like:
import store from './path/to/my/store.js;'
// Read data
const state = store.get();
// Save data in the store
store.dispatch('foo/bar', myToken);

How can I test custom callbacks passed to Axios API using jest + enzyme

In my react application I have an async api call done with axios. And that api call does accept a custom callback.
I am able to test the axios api call using Jest + Enzyme. But not able to test the custom callback method.
Note: I have mocked my axios module.
src/mocks/axios.js
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} }))
}
auth.api.js
import Axios from 'axios';
import { AUTH_SERVER_URL } from './../../settings';
import { setAuthToken } from '../actions/auth/auth.action';
export const saveUsers = (user, dispatch) => {
const URL = `${AUTH_SERVER_URL}/auth/register`;
Axios.post(URL, user)
.then(response => {
const { data } = response;
const token = {
accessToken: data.access_token,
};
return token;
})
.then(token => dispatch(setAuthToken(token)))
.catch(error => {
if (error.response) {
console.error(error.response.data.message);
}
})
}
And here is my test code.
spec.js
import mockAxios from 'axios';
import { AUTH_SERVER_URL } from './../../settings';
import { saveUsers } from './auth.api';
import { setAuthToken } from '../actions/auth/auth.action';
describe('Authentication API', () => {
it('saveUsers', () => {
const user = { x: 'test' }
const dispatch = jest.fn(); // need to test this dispatch function gets called or not
const response = {
data: {
access_token: 'access_token',
}
};
const expectedToken = {
accessToken: 'access_token',
};
mockAxios.post.mockImplementationOnce(() => Promise.resolve(response));
saveUsers(user, dispatch);
const url = `${AUTH_SERVER_URL}/auth/register`;
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(url, user);
console.log(dispatch.mock.calls);
expect(dispatch).toHaveBeenCalledTimes(1); // failed
expect(dispatch).toHaveBeenCalledWith(setAuthToken(expectedToken)); // failed
});
})
Please help me in this
Try to install this package flush-promises.
Then import it in your test file
import flushPromises from 'flush-promises';
And add it before your assertions.
...
await flushPromises();
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(setAuthToken(expectedToken));
And here add async.
it('saveUsers', async () => {
But I'm not sure if it will help.
Thanks to #Jakub Janik for his answer.
Bellow is my answer without using flush-promise package. But it is using the concept behind flush-promise.
import mockAxios from 'axios';
import { AUTH_SERVER_URL } from './../../settings';
import { saveUsers } from './auth.api';
import { setAuthToken } from '../actions/auth/auth.action';
// A helper function can turn that into a promise itself so you don't need to deal with the done callback.
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
describe('Authentication API', () => {
it('saveUsers', async () => {
const user = { x: 'test' }
const dispatch = jest.fn(); // need to test this dispatch function gets called or not
const response = {
data: {
access_token: 'access_token',
}
};
const expectedToken = {
accessToken: 'access_token',
};
mockAxios.post.mockImplementationOnce(() => Promise.resolve(response));
saveUsers(user, dispatch);
const url = `${AUTH_SERVER_URL}/auth/register`;
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(url, user);
await flushPromises(); // Magic happens here
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(setAuthToken(expectedToken));
});
})

Resources