Skip to content

Commit

Permalink
feat(hooks): update useAuth to handle more auth hub events (#2795)
Browse files Browse the repository at this point in the history
* handle additional auth hub events

* Update packages/react/src/hooks/__tests__/useAuth.test.ts

* Update packages/react/src/hooks/useAuth.ts

* Remove extraneous dependency

* add default case + wrap switch blocks in curly brackets

* Use scoped switch body

* Defer mockImplementation to test cases
  • Loading branch information
wlee221 authored Nov 1, 2022
1 parent ea95272 commit 035ab96
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 92 deletions.
153 changes: 91 additions & 62 deletions packages/react/src/hooks/__tests__/useAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,30 @@ import { Hub } from '@aws-amplify/core';
import { act, renderHook } from '@testing-library/react-hooks';
import { useAuth } from '../useAuth';

jest.mock('@aws-amplify/auth');
const currentAuthenticatedUserSpy = jest.spyOn(
Auth,
'currentAuthenticatedUser'
);

// hub events that return valid user object
const SUCCESS_EVENTS_WITH_USER = ['signIn', 'signUp', 'autoSignIn'];

// hub events that return error object
const FAILURE_EVENTS_WITH_ERROR = ['tokenRefresh_failure', 'signIn_failure'];

const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};

describe('useAuth', () => {
afterEach(() => jest.clearAllMocks());

it('should return default values when initialized', async () => {
(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(undefined);
currentAuthenticatedUserSpy.mockResolvedValue(undefined);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

Expand All @@ -21,23 +38,17 @@ describe('useAuth', () => {
});

it('should invoke Auth.currentAuthenticatedUser function', async () => {
const mockCurrentAuthenticatedUser = jest.fn(() => Promise.resolve());

(Auth.currentAuthenticatedUser as jest.Mock).mockImplementation(
mockCurrentAuthenticatedUser
);
currentAuthenticatedUserSpy.mockResolvedValue(mockCognitoUser);

const { waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(mockCurrentAuthenticatedUser).toHaveBeenCalled();
expect(currentAuthenticatedUserSpy).toHaveBeenCalled();
});

it('should set an error when something unexpected happen', async () => {
(Auth.currentAuthenticatedUser as jest.Mock).mockRejectedValue(
new Error('Unknown error')
);
currentAuthenticatedUserSpy.mockRejectedValue(new Error('Unknown error'));

const { result, waitForNextUpdate } = renderHook(() => useAuth());

Expand All @@ -47,17 +58,7 @@ describe('useAuth', () => {
});

it('should retrieve a Cognito user', async () => {
const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};

(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(
mockCognitoUser
);
currentAuthenticatedUserSpy.mockResolvedValue(mockCognitoUser);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

Expand All @@ -67,65 +68,93 @@ describe('useAuth', () => {
expect(result.current.user).toBe(mockCognitoUser);
});

it('should receive a Cognito user on Auth.signIn Hub event', async () => {
const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};
it.each(SUCCESS_EVENTS_WITH_USER)(
'should receive a Cognito user on %s Hub event',
async () => {
currentAuthenticatedUserSpy.mockResolvedValue(undefined);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(undefined);
await waitForNextUpdate();

expect(result.current.user).toBe(undefined);

// Simulate Auth signIn Hub action
act(() => {
Hub.dispatch('auth', { event: 'signIn', data: mockCognitoUser });
});

expect(result.current.user).toBe(mockCognitoUser);
}
);

it('should should unset user on Auth.signOut Hub event', async () => {
currentAuthenticatedUserSpy.mockResolvedValue(mockCognitoUser);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(result.current.user).toBe(undefined);
expect(result.current.user).toBe(mockCognitoUser);

// Simulate Auth signIn Hub action
// Simulate Auth signOut Hub action
act(() => {
Hub.dispatch(
'auth',
{ event: 'signIn', data: mockCognitoUser },
'Auth',
Symbol.for('amplify_default')
);
Hub.dispatch('auth', { event: 'signOut' });
});

expect(result.current.user).toBe(mockCognitoUser);
expect(result.current.user).toBeUndefined();
});

it('should should unset user on Auth.signOut Hub event', async () => {
const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};

(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(
mockCognitoUser
);

const { result, waitForNextUpdate } = renderHook(() => useAuth());
it('invokes Auth.currentAuthenticatedUser on tokenRefresh event', async () => {
currentAuthenticatedUserSpy.mockResolvedValue(mockCognitoUser);

const { waitForNextUpdate } = renderHook(() => useAuth());
await waitForNextUpdate();

expect(result.current.user).toBe(mockCognitoUser);
// Simulate Auth tokenRefresh Hub action
act(() => {
Hub.dispatch('auth', { event: 'tokenRefresh' });
});

expect(currentAuthenticatedUserSpy).toHaveBeenCalled();
});

it.each(FAILURE_EVENTS_WITH_ERROR)(
'returns error on %s event',
async (event) => {
currentAuthenticatedUserSpy.mockResolvedValue(mockCognitoUser);

const { result, waitForNextUpdate } = renderHook(() => useAuth());
await waitForNextUpdate();

act(() => {
Hub.dispatch('auth', {
event,
data: new Error('mock auth error'),
});
});

expect(result.current.user).toBeUndefined();
expect(result.current.error?.message).toBe('mock auth error');
}
);

it('returns error on autoSignIn_failure event', async () => {
currentAuthenticatedUserSpy.mockResolvedValue(mockCognitoUser);

const { result, waitForNextUpdate } = renderHook(() => useAuth());
await waitForNextUpdate();

// Simulate Auth signOut Hub action
act(() => {
Hub.dispatch(
'auth',
{ event: 'signOut' },
'Auth',
Symbol.for('amplify_default')
);
// adapted from https://github.com/aws-amplify/amplify-js/blob/272c2c607cc4adb5ddc9421444887bdb382227a0/packages/auth/src/Auth.ts#L274-L278
Hub.dispatch('auth', {
event: 'autoSignIn_failure',
// autoSignIn_failure event only contains `message` but not `payload`.
message: 'autoSignInError',
});
});

expect(result.current.user).toBeUndefined();
expect(result.current.error?.message).toBe('autoSignInError');
});
});
99 changes: 69 additions & 30 deletions packages/react/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import * as React from 'react';

import Auth, { CognitoUser } from '@aws-amplify/auth';
import { Hub, HubCallback } from '@aws-amplify/core';

// Exposes relevant CognitoUser properties
interface AuthUser extends CognitoUser {
username: string;
attributes: Record<string, string>;
}
import { AmplifyUser } from '@aws-amplify/ui';
import { Auth } from 'aws-amplify';

export interface UseAuthResult {
user?: AuthUser;
user?: AmplifyUser;
isLoading: boolean;
error?: Error;
fetch?: () => void;
/** @deprecated Fetch is handled automatically, do not use this directly */
fetch?: () => Promise<void>;
}

/**
Expand All @@ -27,32 +23,75 @@ export const useAuth = (): UseAuthResult => {
user: undefined,
});

const handleAuth: HubCallback = ({ payload }) => {
switch (payload.event) {
case 'signIn':
return setResult({ user: payload.data as AuthUser, isLoading: false });
case 'signOut':
return setResult({ isLoading: false });
default:
break;
/**
* Hub events like `tokenRefresh` will not give back the user object.
* This util will be used to get current user after those events.
*/
const fetchCurrentUser = React.useCallback(async () => {
setResult({ user: undefined, isLoading: true, error: undefined });

try {
// casting the result because `Auth.currentAuthenticateduser` returns `any`
const user = (await Auth.currentAuthenticatedUser()) as AmplifyUser;
setResult({ user, isLoading: false });
} catch (e) {
const error = e as Error;
setResult({ error, isLoading: false });
}
};
}, []);

const fetch = () => {
setResult({ isLoading: true });
const handleAuth: HubCallback = React.useCallback(
({ payload }) => {
switch (payload.event) {
// success events
case 'signIn':
case 'signUp':
case 'autoSignIn': {
setResult({ user: payload.data as AmplifyUser, isLoading: false });
break;
}
case 'signOut': {
setResult({ user: undefined, isLoading: false });
break;
}

Auth.currentAuthenticatedUser()
.then((user: AuthUser) => setResult({ user, isLoading: false }))
.catch((error: Error) => setResult({ error, isLoading: false }));
// failure events
case 'tokenRefresh_failure':
case 'signIn_failure': {
setResult({ error: payload.data as Error, isLoading: false });
break;
}
case 'autoSignIn_failure': {
// autoSignIn just returns error message. Wrap it to an Error object
setResult({ error: new Error(payload.message), isLoading: false });
break;
}

// Handle Hub Auth events
Hub.listen('auth', handleAuth);
// events that need another fetch
case 'tokenRefresh': {
fetchCurrentUser();
break;
}

// Stop listening events on unmount
return () => Hub.remove('auth', handleAuth);
};
default: {
// we do not handle other hub events like `configured`.
break;
}
}
},
[fetchCurrentUser]
);

React.useEffect(() => {
const unsubscribe = Hub.listen('auth', handleAuth, 'useAuth');
fetchCurrentUser(); // on init, see if user is already logged in

React.useEffect(fetch, []);
return unsubscribe;
}, [handleAuth, fetchCurrentUser]);

return { ...result, fetch };
return {
...result,
/** @deprecated Fetch is handled automatically, do not use this directly */
fetch: fetchCurrentUser,
};
};

0 comments on commit 035ab96

Please sign in to comment.