I have a function in my React app's App component that refreshes the user's access token when it's first rendered (useEffect hook). At the moment, unit tests are checking to see how the state has changed at the end of the component's rendering. How can I make the function itself more testable?
I've considered refactoring to have the dispatch() hook, logout() reducer, and local setLoading() state function passed into the function as arguments so they can be mocked/so the function can be externalized from the component itself, but I'm not sure what value this would bring.
I understand that 100% test coverage is not necessary, but I'm learning and want to do the best I can while I do so.
A little context:
The app uses a ReduxToolkit slice for authentication state, including the user object and access token for the currently authenticated user, or nulls for guest users.
Auto refresh logic is implemented into a custom fetchBaseQuery.
The code below describes refreshing the access token for a user who's logged in and has a refresh token in localStorage, but has refreshed the page, clearing the redux state. It refreshes the accessToken before rendering any routes/views to avoid the user having to enter credentials every time the page refreshes.
Here's the current implementation:
//imports
...
const App = () => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
useEffect(() => {
const refresh = async () => {
const token = localStorage.getItem("refreshToken");
if (token) {
const refreshRequest = {
refresh: token,
};
const response = await fetch(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
}
);
if (response.status === 200) { // This branch gets no test coverage and I can't figure out how to fix that.
const data: RefreshResponse = await response.json();
// Should this be passed into the function to make it more reusable/testable?
dispatch(
setCredentials({ user: data.user, token: data.accessToken })
);
}
}
// Should this be passed into the function to make it more reusable/testable?
setLoading(false);
};
refresh();
}, [dispatch]);
if (loading) return (
<div className="h-100 d-flex justify-content-center align-items-center bg-dark">
<Spinner animation="border" />
</div>
);
return (
<>
<Routes>
// Routes
</Routes>
</>
);
}
export default App;
and here are the relevant test cases:
it("should successfully request refresh access token on render", async () => {
// refresh() expects a refreshToken item in localStorage
localStorage.setItem("refreshToken", "testRefreshToken");
// I can't use enzyme because I'm on react 18, so no shallow rendering afaik :/
// renderWithProviders renders including a redux store with auth/api reducers
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBe("testAccessToken");
});
localStorage.removeItem("refreshToken");
});
it("should fail to request refresh access token on render", async () => {
localStorage.setItem("refreshToken", "testRefreshToken");
// msn api route mocking, force a 401 error rather than the default HTTP 200 impl
server.use(
rest.post(
`${process.env.REACT_APP_API_URL}/auth/refresh/`,
(req, res, ctx) => {
return res(ctx.status(401));
}
)
);
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBeNull();
});
localStorage.removeItem("refreshToken");
});
it("should not successfully request refresh access token on render", async () => {
const { store } = renderWithProviders(
<MemoryRouter>
<App />
</MemoryRouter>
);
await waitFor(() => {
expect(store.getState().auth.token).toBe(null);
});
});