skip to Main Content

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


  1. Chosen as BEST ANSWER

    Chnaged my Main file PrivateRoute.tsx

    import React, { useContext, useEffect } from 'react';
    import { Outlet, RouteProps, useLocation } 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() {
      const { authorize, authenticated, authenticating, callbackPath } =
        useContext(AuthenticationContext);
      const location = useLocation();
    
      useEffect(() => {
        if (!authenticated && !authenticating && location.pathname !== callbackPath) {
          const authorizeAsync = async () => {
            authorize(location as unknown as URL);
          };
          authorizeAsync();
        }
      }, [authorize, location, authenticated, authenticating, callbackPath]);
    
      return authenticated ? <Outlet /> : null;
    }
    
    export default PrivateRoute;
    

    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"

      32 |
      33 |     return {
    > 34 |       ...render(content, { wrapper } as any),
         |                ^
      35 |       history,
      36 |     };
      37 |   }
    

    Requesting some clear solutions here


  2. There is no Route being rendered with a "/hello" path prop. The PrivateRoute component doesn’t pass the path prop through to the Route it is rendering. You are overcomplicating the PrivateRoute component though. It should simply render an Outlet for nested routes to render their element 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:

    const PrivateRoute = () => {
      const {
        authorize,
        authenticated,
        authenticating,
        callbackPath
      } = useContext(AuthenticationContext);
      const location = useLocation();
    
      useEffect(() => {
        if (!authenticated
          && !authenticating
          && location.pathname !== callbackPath
        ) {
          const authorizeAsync = async () => {
            authorize(location as unknown as URL);
          };
          authorizeAsync();
        }
      }, [authorize, location, authenticated, authenticating, callbackPath]);
    
      return authenticated ? <Outlet /> : null;
    }
    

    The unit tests should be refactored to render PrivateRoute as a layout route component.

    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(
            <Routes>
              <Route element={<PrivateRoute />}>
                <Route path="/hello" element={<h1>Hello</h1>} />
              </Route>
            </Routes>,
            {
              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(
            <Routes>
              <Route element={<PrivateRoute />}>
                <Route path="/hello" element={<MyComponent />} />
              </Route>
            </Routes>,
            {
              location: '/hello',
              context: { callbackPath: '/oauth', authenticated: true },
            }
          );
          expect(container.innerHTML).toBe('<h1>Hey</h1>');
        });
      });
    
      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(
              <Routes>
                <Route element={<PrivateRoute />}>
                  <Route path="/hello" element={<h1>Hello</h1>} />
                </Route>
              </Routes>,
              {
                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(
              <Routes>
                <Route path="/oauth" element={<h1>OAuth</h1>} />
                <Route element={<PrivateRoute />}>
                  <Route path="/hello" element={<h1>Hello</h1>} />
                </Route>
              </Routes>,
              {
                location: '/oauth?code=xyz&state=foo',
                context: { callbackPath: '/oauth', authenticated: false, authorize },
              },
            );
            expect(container.innerHTML).toBe('<h1>OAuth</h1>');
            expect(authorize).not.toHaveBeenCalled();
          });
        });
    
        describe('for a non-matching path', () => {
          it('does not check user authorization', () => {
            renderWithRouterAndContext(
              <Routes>
                <Route path="/hi" element={<h1>Hi</h1>} />
                <Route element={<PrivateRoute />}>
                  <Route path="/hello" element={<h1>Hello</h1>} />
                </Route>
              </Routes>,
              {
                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(
              <Routes>
                <Route path="/hi" element={<h1>Hi</h1>} />
                <Route element={<PrivateRoute />}>
                  <Route path="/hello" element={<h1>Hello</h1>} />
                </Route>
              </Routes>,
              {
                location: '/hi',
                context: {
                  callbackPath: '/hello',
                  authenticating: true,
                  authorize,
                },
              },
            );
            expect(authorize).not.toHaveBeenCalled();
          });
        });
      });
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search