=> {
- const response = await fetch(profileUrl);
- const result = response.ok ? await response.json() : undefined;
-
- setUser(result);
- setIsLoading(false);
+ try {
+ const response = await fetch(profileUrl);
+ setUser(response.ok ? await response.json() : undefined);
+ setError(undefined);
+ } catch (_e) {
+ setError(new Error(`The request to ${profileUrl} failed`));
+ } finally {
+ setIsLoading(false);
+ }
})();
}, [user]);
- return {children};
+ return {children};
};
diff --git a/src/frontend/with-page-auth-required.tsx b/src/frontend/with-page-auth-required.tsx
index e2b9b6052..3b20ab301 100644
--- a/src/frontend/with-page-auth-required.tsx
+++ b/src/frontend/with-page-auth-required.tsx
@@ -8,6 +8,11 @@ import { useUser } from './use-user';
*/
const defaultOnRedirecting = (): JSX.Element => <>>;
+/**
+ * @ignore
+ */
+const defaultOnError = (): JSX.Element => <>>;
+
/**
* Options for the withPageAuthRequired Higher Order Component
*
@@ -43,6 +48,16 @@ export interface WithPageAuthRequiredOptions {
* Render a message to show that the user is being redirected to the login.
*/
onRedirecting?: () => JSX.Element;
+ /**
+ * ```js
+ * withPageAuthRequired(Profile, {
+ * onError: error => Error: {error.message}
+ * });
+ * ```
+ *
+ * Render a fallback in case of error fetching the user from the profile API route.
+ */
+ onError?: (error: Error) => JSX.Element;
}
/**
@@ -66,18 +81,26 @@ export type WithPageAuthRequired = (
const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => {
return function withPageAuthRequired(props): JSX.Element {
const router = useRouter();
- const { returnTo = router.asPath, onRedirecting = defaultOnRedirecting, loginUrl = '/api/auth/login' } = options;
- const { user, isLoading } = useUser();
+ const {
+ returnTo = router.asPath,
+ onRedirecting = defaultOnRedirecting,
+ onError = defaultOnError,
+ loginUrl = '/api/auth/login'
+ } = options;
+ const { user, error, isLoading } = useUser();
useEffect(() => {
- if (user || isLoading) return;
+ if ((user && !error) || isLoading) return;
(async (): Promise => {
await router.push(`${loginUrl}?returnTo=${returnTo}`);
})();
- }, [user, isLoading, router, loginUrl, returnTo]);
+ }, [user, error, isLoading]);
+
+ if (error) return onError(error);
+ if (user) return ;
- return user ? : onRedirecting();
+ return onRedirecting();
};
};
diff --git a/src/index.browser.ts b/src/index.browser.ts
index 04eb734bc..d8de45732 100644
--- a/src/index.browser.ts
+++ b/src/index.browser.ts
@@ -4,6 +4,7 @@ import { WithApiAuthRequired } from './helpers';
import { HandleAuth, HandleCallback, HandleLogin, HandleLogout, HandleProfile } from './handlers';
export {
UserProvider,
+ UserProviderProps,
UserProfile,
UserContext,
useUser,
diff --git a/src/index.ts b/src/index.ts
index 30bb77987..106f3fd6e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -92,7 +92,7 @@ export const handleCallback: HandleCallback = (...args) => getInstance().handleC
export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args);
export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args);
-export { UserProvider, UserProfile, UserContext, useUser } from './frontend';
+export { UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './frontend';
export {
Config,
diff --git a/tests/fixtures/frontend.tsx b/tests/fixtures/frontend.tsx
index 617bcc6db..688771a71 100644
--- a/tests/fixtures/frontend.tsx
+++ b/tests/fixtures/frontend.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { UserProfile, UserProvider } from '../../src';
+import { UserProvider, UserProviderProps, UserProfile } from '../../src';
type FetchUserMock = {
ok: boolean;
@@ -16,16 +16,21 @@ export const user: UserProfile = {
updated_at: null
};
-export const withUser = (user?: UserProfile): React.ComponentType => {
- return (props: any): React.ReactElement => ;
+export const withUserProvider = ({ user, profileUrl }: UserProviderProps = {}): React.ComponentType => {
+ return (props: any): React.ReactElement => ;
};
-export const fetchUserMock = (): FetchUserMock => ({
- ok: true,
- json: (): Promise => Promise.resolve(user)
-});
+export const fetchUserMock = (): Promise => {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve(user)
+ });
+};
+
+export const fetchUserUnsuccessfulMock = (): Promise => {
+ return Promise.resolve({
+ ok: false
+ });
+};
-export const fetchUserFailureMock = (): FetchUserMock => ({
- ok: false,
- json: undefined
-});
+export const fetchUserErrorMock = (): Promise => Promise.reject(new Error('Error'));
diff --git a/tests/frontend/use-user.test.tsx b/tests/frontend/use-user.test.tsx
index 618170394..51e1fe8ea 100644
--- a/tests/frontend/use-user.test.tsx
+++ b/tests/frontend/use-user.test.tsx
@@ -1,44 +1,79 @@
import { renderHook } from '@testing-library/react-hooks';
import { useUser } from '../../src';
-import { fetchUserMock, fetchUserFailureMock, withUser, user } from '../fixtures/frontend';
+import {
+ fetchUserMock,
+ fetchUserUnsuccessfulMock,
+ fetchUserErrorMock,
+ withUserProvider,
+ user
+} from '../fixtures/frontend';
describe('context wrapper', () => {
- test('should use the initial user', async () => {
- const { result } = renderHook(() => useUser(), { wrapper: withUser(user) });
+ test('should fetch the user', async () => {
+ (global as any).fetch = fetchUserMock;
+ const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() });
+
+ expect(result.current.user).toEqual(undefined);
+ expect(result.current.error).toEqual(undefined);
+ expect(result.current.isLoading).toEqual(true);
+
+ await waitForValueToChange(() => result.current.isLoading);
expect(result.current.user).toEqual(user);
+ expect(result.current.error).toEqual(undefined);
expect(result.current.isLoading).toEqual(false);
});
- test('should fetch the user', async () => {
- (global as any).fetch = fetchUserMock;
-
- const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUser() });
+ test('should discard the response when the status code is not successful', async () => {
+ (global as any).fetch = fetchUserUnsuccessfulMock;
+ const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() });
expect(result.current.user).toEqual(undefined);
+ expect(result.current.error).toEqual(undefined);
expect(result.current.isLoading).toEqual(true);
await waitForValueToChange(() => result.current.isLoading);
- expect(result.current.user).toEqual(user);
+ expect(result.current.user).toEqual(undefined);
+ expect(result.current.error).toEqual(undefined);
expect(result.current.isLoading).toEqual(false);
});
test('should fail to fetch the user', async () => {
- (global as any).fetch = fetchUserFailureMock;
-
- const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUser() });
+ (global as any).fetch = fetchUserErrorMock;
+ const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() });
expect(result.current.user).toEqual(undefined);
+ expect(result.current.error).toEqual(undefined);
expect(result.current.isLoading).toEqual(true);
await waitForValueToChange(() => result.current.isLoading);
expect(result.current.user).toEqual(undefined);
+ expect(result.current.error).toEqual(new Error('The request to /api/auth/me failed'));
expect(result.current.isLoading).toEqual(false);
});
+ test('should use the existing user', async () => {
+ const { result } = renderHook(() => useUser(), { wrapper: withUserProvider({ user }) });
+
+ expect(result.current.user).toEqual(user);
+ expect(result.current.error).toEqual(undefined);
+ expect(result.current.isLoading).toEqual(false);
+ });
+
+ test('should use a custom profileUrl', async () => {
+ const fetchSpy = jest.fn();
+ (global as any).fetch = fetchSpy;
+ const { result, waitForValueToChange } = renderHook(() => useUser(), {
+ wrapper: withUserProvider({ profileUrl: '/api/custom-url' })
+ });
+
+ await waitForValueToChange(() => result.current.isLoading);
+ expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url');
+ });
+
afterAll(() => {
delete (global as any).fetch;
});
diff --git a/tests/frontend/with-page-auth-required.test.tsx b/tests/frontend/with-page-auth-required.test.tsx
index d1b135430..7c1a4f8b7 100644
--- a/tests/frontend/with-page-auth-required.test.tsx
+++ b/tests/frontend/with-page-auth-required.test.tsx
@@ -5,7 +5,7 @@ import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
-import { fetchUserFailureMock, withUser, user } from '../fixtures/frontend';
+import { fetchUserUnsuccessfulMock, fetchUserErrorMock, withUserProvider, user } from '../fixtures/frontend';
import { withPageAuthRequired } from '../../src/frontend';
const routerMock = {
@@ -17,11 +17,11 @@ jest.mock('next/router', () => ({ useRouter: (): any => routerMock }));
describe('with-page-auth-required csr', () => {
it('should block access to a CSR page when not authenticated', async () => {
- (global as any).fetch = fetchUserFailureMock;
+ (global as any).fetch = fetchUserUnsuccessfulMock;
const MyPage = (): JSX.Element => <>Private>;
const ProtectedPage = withPageAuthRequired(MyPage);
- render(, { wrapper: withUser() });
+ render(, { wrapper: withUserProvider() });
await waitFor(() => expect(routerMock.push).toHaveBeenCalledTimes(1));
await waitFor(() => expect(screen.queryByText('Private')).not.toBeInTheDocument());
});
@@ -30,7 +30,7 @@ describe('with-page-auth-required csr', () => {
const MyPage = (): JSX.Element => <>Private>;
const ProtectedPage = withPageAuthRequired(MyPage);
- render(, { wrapper: withUser(user) });
+ render(, { wrapper: withUserProvider({ user }) });
await waitFor(() => expect(routerMock.push).not.toHaveBeenCalled());
await waitFor(() => expect(screen.getByText('Private')).toBeInTheDocument());
});
@@ -40,15 +40,25 @@ describe('with-page-auth-required csr', () => {
const OnRedirecting = (): JSX.Element => <>Redirecting>;
const ProtectedPage = withPageAuthRequired(MyPage, { onRedirecting: OnRedirecting });
- render(, { wrapper: withUser() });
+ render(, { wrapper: withUserProvider() });
await waitFor(() => expect(screen.getByText('Redirecting')).toBeInTheDocument());
});
+ it('should show a fallback in case of error', async () => {
+ (global as any).fetch = fetchUserErrorMock;
+ const MyPage = (): JSX.Element => <>Private>;
+ const OnError = (): JSX.Element => <>Error>;
+ const ProtectedPage = withPageAuthRequired(MyPage, { onError: OnError });
+
+ render(, { wrapper: withUserProvider() });
+ await waitFor(() => expect(screen.getByText('Error')).toBeInTheDocument());
+ });
+
it('should accept a returnTo url', async () => {
const MyPage = (): JSX.Element => <>Private>;
const ProtectedPage = withPageAuthRequired(MyPage, { returnTo: '/foo' });
- render(, { wrapper: withUser() });
+ render(, { wrapper: withUserProvider() });
await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo')));
});