Skip to content

Commit

Permalink
Merge branch 'beta' into split-config
Browse files Browse the repository at this point in the history
  • Loading branch information
Widcket committed Jan 29, 2021
2 parents 4d42311 + 022931e commit 8f2e6ab
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 36 deletions.
1 change: 1 addition & 0 deletions src/frontend/index.ts
@@ -1,2 +1,3 @@
export { default as ConfigProvider, ConfigProviderProps, useConfig } from './use-config';
export { default as UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './use-user';
export { default as withPageAuthRequired, WithPageAuthRequired } from './with-page-auth-required';
18 changes: 18 additions & 0 deletions src/frontend/use-config.tsx
@@ -0,0 +1,18 @@
import React, { ReactElement, useContext, createContext } from 'react';

export type ConfigContext = {
loginUrl?: string;
};

const Config = createContext<ConfigContext>({});

export type ConfigProviderProps = React.PropsWithChildren<ConfigContext>;
export type UseConfig = () => ConfigContext;
export const useConfig: UseConfig = () => useContext<ConfigContext>(Config);

export default ({
children,
loginUrl = process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login'
}: ConfigProviderProps): ReactElement<ConfigContext> => {
return <Config.Provider value={{ loginUrl }}>{children}</Config.Provider>;
};
46 changes: 31 additions & 15 deletions src/frontend/use-user.tsx
@@ -1,5 +1,7 @@
import React, { ReactElement, useState, useEffect, useCallback, useContext, createContext } from 'react';

import ConfigProvider, { ConfigContext } from './use-config';

/**
* The user claims returned from the {@link useUser} hook.
*
Expand All @@ -21,12 +23,12 @@ export interface UserProfile {
*
* @category Client
*/
export interface UserContext {
export type UserContext = {
user?: UserProfile;
error?: Error;
isLoading: boolean;
checkSession: () => Promise<void>;
}
};

/**
* Configure the {@link UserProvider} component.
Expand Down Expand Up @@ -59,7 +61,7 @@ export interface UserContext {
*
* @category Client
*/
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string }>;
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string } & ConfigContext>;

/**
* @ignore
Expand Down Expand Up @@ -119,33 +121,47 @@ export const useUser: UseUser = () => useContext<UserContext>(User);
*/
export type UserProvider = (props: UserProviderProps) => ReactElement<UserContext>;

/**
* @ignore
*/
type UserProviderState = {
user?: UserProfile;
error?: Error;
isLoading: boolean;
};

export default ({
children,
user: initialUser,
profileUrl = '/api/auth/me'
profileUrl = process.env.NEXT_PUBLIC_AUTH0_PROFILE || '/api/auth/me',
loginUrl
}: UserProviderProps): ReactElement<UserContext> => {
const [user, setUser] = useState<UserProfile | undefined>(() => initialUser);
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(() => !initialUser);
const [state, setState] = useState<UserProviderState>({ user: initialUser, isLoading: !initialUser });

const checkSession = useCallback(async (): Promise<void> => {
try {
const response = await fetch(profileUrl);
setUser(response.ok ? await response.json() : undefined);
setError(undefined);
const user = response.ok ? await response.json() : undefined;
setState((previous) => ({ ...previous, user, error: undefined }));
} catch (_e) {
setUser(undefined);
setError(new Error(`The request to ${profileUrl} failed`));
const error = new Error(`The request to ${profileUrl} failed`);
setState((previous) => ({ ...previous, user: undefined, error }));
}
}, [profileUrl]);

useEffect((): void => {
if (user) return;
if (state.user) return;
(async (): Promise<void> => {
await checkSession();
setIsLoading(false);
setState((previous) => ({ ...previous, isLoading: false }));
})();
}, [user]);
}, [state.user]);

const { user, error, isLoading } = state;

return <User.Provider value={{ user, error, isLoading, checkSession }}>{children}</User.Provider>;
return (
<ConfigProvider loginUrl={loginUrl}>
<User.Provider value={{ user, error, isLoading, checkSession }}>{children}</User.Provider>
</ConfigProvider>
);
};
18 changes: 3 additions & 15 deletions src/frontend/with-page-auth-required.tsx
@@ -1,6 +1,7 @@
import React, { ComponentType, useEffect } from 'react';
import { useRouter } from 'next/router';

import { useConfig } from './use-config';
import { useUser } from './use-user';

/**
Expand Down Expand Up @@ -29,15 +30,6 @@ export interface WithPageAuthRequiredOptions {
* Add a path to return the user to after login.
*/
returnTo?: string;
/**
* ```js
* withPageAuthRequired(Profile, {
* loginUrl: '/api/login'
* });
* ```
* The path of your custom login API route.
*/
loginUrl?: string;
/**
* ```js
* withPageAuthRequired(Profile, {
Expand Down Expand Up @@ -81,12 +73,8 @@ export type WithPageAuthRequired = <P extends object>(
const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => {
return function withPageAuthRequired(props): JSX.Element {
const router = useRouter();
const {
returnTo = router.asPath,
onRedirecting = defaultOnRedirecting,
onError = defaultOnError,
loginUrl = '/api/auth/login'
} = options;
const { returnTo = router.asPath, onRedirecting = defaultOnRedirecting, onError = defaultOnError } = options;
const { loginUrl } = useConfig();
const { user, error, isLoading } = useUser();

useEffect(() => {
Expand Down
12 changes: 10 additions & 2 deletions tests/fixtures/frontend.tsx
@@ -1,5 +1,7 @@
import React from 'react';

import { UserProvider, UserProviderProps, UserProfile } from '../../src';
import { ConfigProvider, ConfigProviderProps } from '../../src/frontend';

type FetchUserMock = {
ok: boolean;
Expand All @@ -16,8 +18,10 @@ export const user: UserProfile = {
updated_at: null
};

export const withUserProvider = ({ user, profileUrl }: UserProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => <UserProvider {...props} user={user} profileUrl={profileUrl} />;
export const withUserProvider = ({ user, profileUrl, loginUrl }: UserProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => (
<UserProvider {...props} user={user} profileUrl={profileUrl} loginUrl={loginUrl} />
);
};

export const fetchUserMock = (): Promise<FetchUserMock> => {
Expand All @@ -34,3 +38,7 @@ export const fetchUserUnsuccessfulMock = (): Promise<FetchUserMock> => {
};

export const fetchUserErrorMock = (): Promise<FetchUserMock> => Promise.reject(new Error('Error'));

export const withConfigProvider = ({ loginUrl }: ConfigProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => <ConfigProvider {...props} loginUrl={loginUrl} />;
};
36 changes: 36 additions & 0 deletions tests/frontend/use-config.test.ts
@@ -0,0 +1,36 @@
import { renderHook } from '@testing-library/react-hooks';

import { withConfigProvider } from '../fixtures/frontend';
import { useConfig } from '../../src/frontend/use-config';

jest.mock('next/router', () => ({
useRouter: (): any => ({ asPath: '/' })
}));

describe('context wrapper', () => {
test('should provide the default login url', async () => {
const { result } = renderHook(() => useConfig(), {
wrapper: withConfigProvider({ loginUrl: '/api/auth/login' })
});

expect(result.current.loginUrl).toEqual('/api/auth/login');
});

test('should provide a custom login url', async () => {
const { result } = renderHook(() => useConfig(), {
wrapper: withConfigProvider({ loginUrl: '/api/custom-url' })
});

expect(result.current.loginUrl).toEqual('/api/custom-url');
});

test('should provide a custom login url from an environment variable', async () => {
process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/custom-url';
const { result } = renderHook(() => useConfig(), {
wrapper: withConfigProvider()
});

expect(result.current.loginUrl).toEqual('/api/custom-url');
delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
});
});
Expand Up @@ -7,8 +7,13 @@ import {
withUserProvider,
user
} from '../fixtures/frontend';
import { useConfig } from '../../src/frontend';
import { useUser } from '../../src';

jest.mock('next/router', () => ({
useRouter: (): any => ({ asPath: '/' })
}));

describe('context wrapper', () => {
afterEach(() => delete (global as any).fetch);

Expand Down Expand Up @@ -57,15 +62,26 @@ describe('context wrapper', () => {
expect(result.current.isLoading).toEqual(false);
});

test('should use the existing user', async () => {
test('should provide the existing user', async () => {
const { result } = renderHook(() => useUser(), { wrapper: withUserProvider({ user }) });

expect(result.current.user).toEqual(user);
expect(result.current.error).toBeUndefined();
expect(result.current.isLoading).toEqual(false);
});

test('should use a custom profileUrl', async () => {
test('should use the default profile url', async () => {
const fetchSpy = jest.fn().mockReturnValue(Promise.resolve());
(global as any).fetch = fetchSpy;
const { result, waitForValueToChange } = renderHook(() => useUser(), {
wrapper: withUserProvider()
});

await waitForValueToChange(() => result.current.isLoading);
expect(fetchSpy).toHaveBeenCalledWith('/api/auth/me');
});

test('should accept a custom profile url', async () => {
const fetchSpy = jest.fn().mockReturnValue(Promise.resolve());
(global as any).fetch = fetchSpy;
const { result, waitForValueToChange } = renderHook(() => useUser(), {
Expand All @@ -76,12 +92,33 @@ describe('context wrapper', () => {
expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url');
});

test('should use a custom profile url from an environment variable', async () => {
process.env.NEXT_PUBLIC_AUTH0_PROFILE = '/api/custom-url';
const fetchSpy = jest.fn().mockReturnValue(Promise.resolve());
(global as any).fetch = fetchSpy;
const { result, waitForValueToChange } = renderHook(() => useUser(), {
wrapper: withUserProvider()
});

await waitForValueToChange(() => result.current.isLoading);
expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url');
delete process.env.NEXT_PUBLIC_AUTH0_PROFILE;
});

test('should accept a custom login url', async () => {
const { result } = renderHook(() => useConfig(), {
wrapper: withUserProvider({ user, loginUrl: '/api/custom-url' })
});

expect(result.current.loginUrl).toEqual('/api/custom-url');
});

test('should check the session when logged in', async () => {
(global as any).fetch = fetchUserUnsuccessfulMock;
const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() });

await waitForValueToChange(() => result.current.isLoading);
expect(result.current.user).toBeUndefined;
expect(result.current.user).toBeUndefined();

(global as any).fetch = fetchUserMock;

Expand Down
13 changes: 12 additions & 1 deletion tests/frontend/with-page-auth-required.test.tsx
Expand Up @@ -18,7 +18,7 @@ jest.mock('next/router', () => ({ useRouter: (): any => routerMock }));
describe('with-page-auth-required csr', () => {
afterEach(() => delete (global as any).fetch);

it('should block access to a CSR page when not authenticated', async () => {
it('should deny access to a CSR page when not authenticated', async () => {
(global as any).fetch = fetchUserUnsuccessfulMock;
const MyPage = (): JSX.Element => <>Private</>;
const ProtectedPage = withPageAuthRequired(MyPage);
Expand Down Expand Up @@ -83,4 +83,15 @@ describe('with-page-auth-required csr', () => {
render(<ProtectedPage />, { wrapper: withUserProvider() });
await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo')));
});

it('should use a custom login url', async () => {
process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo';
(global as any).fetch = fetchUserUnsuccessfulMock;
const MyPage = (): JSX.Element => <>Private</>;
const ProtectedPage = withPageAuthRequired(MyPage);

render(<ProtectedPage />, { wrapper: withUserProvider() });
await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('/api/foo')));
delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
});
});
1 change: 1 addition & 0 deletions typedoc.js
Expand Up @@ -4,6 +4,7 @@ module.exports = {
exclude: [
'./src/auth0-session/**',
'./src/session/cache.ts',
'./src/frontend/use-config.tsx',
'./src/utils/!(errors.ts)',
'./src/index.ts',
'./src/index.browser.ts'
Expand Down

0 comments on commit 8f2e6ab

Please sign in to comment.