Main file:
import React, { useContext, useEffect } from 'react';
import { Route, RouteProps, Routes, useLocation, useMatch } from 'react-router-dom';
import AuthenticationContext from './AuthenticationContext';
export type PrivateRouteProps = RouteProps & {
Element?: React.ComponentType<any>;
};
/* eslint-disable react/jsx-props-no-spreading */
function PrivateRoute({ children, path, Element, ...routePropsWithoutChildrenAndComponent }: any) {
const { authorize, authenticated, authenticating, callbackPath } =
useContext(AuthenticationContext);
const location = useLocation();
const match = useMatch(path?.toString() || '*');
useEffect(() => {
if (!authenticated && !authenticating && match && location.pathname !== callbackPath) {
const authorizeAsync = async () => {
authorize(location as unknown as URL);
};
authorizeAsync();
}
}, [authorize, match, location, authenticated, authenticating, callbackPath]);
if (!authenticated) {
return null;
}
return (
<Routes>
<Route
{...routePropsWithoutChildrenAndComponent}
element={Element ? <Element /> : children}
/>
</Routes>
);
}
export default PrivateRoute;
Test file:
/* eslint-disable react/prop-types */
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { unstable_HistoryRouter as Router } from 'react-router-dom';
import { PrivateRoute } from '..';
import AuthenticationContext, { AuthenticationContextProps } from '../AuthenticationContext';
describe('PrivateRoute', () => {
function renderWithRouterAndContext(
content: JSX.Element,
// eslint-disable-next-line @typescript-eslint/ban-types
{ location, context } = {} as { location: string; context: object },
) {
const history = createMemoryHistory({ initialEntries: [location] });
const defaultContext: AuthenticationContextProps = {
fetchToken: () => 'xyz' as any,
callbackPath: '/oauth',
setError: () => {},
authenticated: false,
authenticating: false,
authorize: () => {},
logout: () => {},
getToken: () => 'xyz',
};
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthenticationContext.Provider value={{ ...defaultContext, ...context }}>
<Router history={history as any}>{children}</Router>
</AuthenticationContext.Provider>
);
return {
...render(content, { wrapper } as any),
history,
};
}
describe('when authenticated', () => {
it('can render children', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{ location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
);
expect(container.innerHTML).toBe('<h1>Hello</h1>');
});
it('can render a component', () => {
function MyComponent() {
return <h1>Hey</h1>;
}
// eslint-disable-next-line react/jsx-no-bind
const { container } = renderWithRouterAndContext(<PrivateRoute component={MyComponent} />, {
location: '/hello',
context: { callbackPath: '/oauth', authenticated: true },
});
expect(container.innerHTML).toBe('<h1>Hey</h1>');
});
it('can invoke a render prop function', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute
render={({ history, location }: any) => <p>{${history.length} ${location.pathname}}</p>}
/>,
{ location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
);
expect(container.innerHTML).toBe('<p>1 /hello</p>');
});
});
describe('when unauthenticated', () => {
const authorize = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('for a matching path', () => {
it('checks user authorization and does not render anything', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{
location: '/hello',
context: { callbackPath: '/oauth', authenticated: false, authorize },
},
);
expect(container.innerHTML).toBe('');
expect(authorize).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/hello' }));
});
});
describe('for an OAuth callback path', () => {
it('does not check user authorization and does not render anything', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute path="/">
<h1>Hello</h1>
</PrivateRoute>,
{
location: '/oauth?code=xyz&state=foo',
context: { callbackPath: '/oauth', authenticated: false, authorize },
},
);
expect(container.innerHTML).toBe('');
expect(authorize).not.toHaveBeenCalled();
});
});
describe('for a non-matching path', () => {
it('does not check user authorization', () => {
renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{ location: '/hi', context: { callbackPath: '/oauth', authenticated: false, authorize } },
);
expect(authorize).not.toHaveBeenCalled();
});
});
describe('when authentication is in progress', () => {
it('does not check user authorization', () => {
renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{
location: '/hi',
context: {
callbackPath: '/hello',
authenticating: true,
authorize,
},
},
);
expect(authorize).not.toHaveBeenCalled();
});
});
});
});
With the new version of react-router-dom v6 and @types/react-router-dom v5.3.3, after resolving some errors with the code changes it was giving the "hello" path not found error.
Error:
console.warn
No routes matched location "/hello"
31 | );
32 | return {
> 33 | ...render(content, { wrapper } as any),
| ^
34 | history,
35 | };
36 | }
● PrivateRoute › when authenticated › can render children
expect(received).toBe(expected) // Object.is equality
Expected: "<h1>Hello</h1>"
Received: ""
44 | { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
45 | );
> 46 | expect(container.innerHTML).toBe('<h1>Hello</h1>');
| ^
47 | });
48 |
49 | it('can render a component', () => {
at Object.<anonymous> (packages/auth/src/_tests_/PrivateRoute.test.tsx:46:35)
● PrivateRoute › when authenticated › can render a component
expect(received).toBe(expected) // Object.is equality
Expected: "<h1>Hey</h1>"
Received: ""
56 | context: { callbackPath: '/oauth', authenticated: true },
57 | });
> 58 | expect(container.innerHTML).toBe('<h1>Hey</h1>');
| ^
59 | });
60 |
61 | it('can invoke a render prop function', () => {
at Object.<anonymous> (packages/auth/src/_tests_/PrivateRoute.test.tsx:58:35)
● PrivateRoute › when authenticated › can invoke a render prop function
expect(received).toBe(expected) // Object.is equality
Expected: "<p>1 /hello</p>"
Received: ""
66 | { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
67 | );
> 68 | expect(container.innerHTML).toBe('<p>1 /hello</p>');
| ^
69 | });
70 | });
71 |
Appriciate some suggessions, Thanks in advance
2
Answers
Chnaged my Main file PrivateRoute.tsx
Keep test cases same as you provided in the above solutions Getting No match path /oauth error
console.warn No routes matched location "/oauth?code=xyz&state=foo"
Requesting some clear solutions here
There is no
Route
being rendered with a"/hello"
path
prop. ThePrivateRoute
component doesn’t pass thepath
prop through to theRoute
it is rendering. You are overcomplicating thePrivateRoute
component though. It should simply render anOutlet
for nested routes to render theirelement
content, or in the case of unauthenticated users, generally redirect them to a safe unprotected route.Keeping your logic of rendering
null
for unauthenticated users though:The unit tests should be refactored to render
PrivateRoute
as a layout route component.