Skip to content

Commit

Permalink
chore(ui): fix defaultAuthHubListener unit tests, remove asynchronous…
Browse files Browse the repository at this point in the history
… declaration (#4777)

* chore(ui): fix defaultAuthHubListener unit tests, remove asynchronous declaration

* Create light-zoos-raise.md
  • Loading branch information
calebpollman authored Dec 13, 2023
1 parent 8a564b8 commit 4f643b0
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 154 deletions.
8 changes: 8 additions & 0 deletions .changeset/light-zoos-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@aws-amplify/ui-react-core": patch
"@aws-amplify/ui": patch
"@aws-amplify/ui-vue": patch
"@aws-amplify/ui-angular": patch
---

chore(ui): fix defaultAuthHubListener unit tests, remove asynchronous declaration
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ export class AuthenticatorService implements OnDestroy {

this._unsubscribeHub = listenToAuthHub(
this._authService,
async (data, service) => {
await defaultAuthHubHandler(data, service, { onSignIn, onSignOut });
(data, service) => {
defaultAuthHubHandler(data, service, { onSignIn, onSignOut });
this._hubSubject.next();
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ type Options = Parameters<AuthMachineHubHandler>[2];

const createHubHandler =
(options: Options): AuthMachineHubHandler =>
async (data, service) => {
await defaultAuthHubHandler(data, service, options);
(data, service) => {
defaultAuthHubHandler(data, service, options);
};

export default function AuthenticatorProvider({
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const config: Config = {
],
coverageThreshold: {
global: {
branches: 50,
functions: 50,
lines: 50,
statements: 50,
branches: 77,
functions: 70,
lines: 87,
statements: 88,
// @todo-migration: put back after fixing tests
// branches: 80,
// functions: 85,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,155 +1,93 @@
import { defaultAuthHubHandler } from '../defaultAuthHubHandler';
import { Hub } from 'aws-amplify/utils';
import {
defaultAuthHubHandler,
listenToAuthHub,
} from '../defaultAuthHubHandler';
import { AuthInterpreter } from '../types';

jest.mock('xstate/lib/waitFor', () => ({
waitFor: jest.fn(),
}));

jest.mock('../actor', () => ({
getActorState: () => ({ matches: () => true }),
}));

const authenticatedStateMachine = {
getSnapshot: () => ({
// this is the state.matches function
matches: (state: string) => state === 'authenticated.idle',
}),
send: jest.fn(),
} as unknown as AuthInterpreter;

const unauthenticatedStateMachine = {
getSnapshot: () => ({
// this is the state.matches function
matches: (state: string) => state === 'signIn',
}),
send: jest.fn(),
} as unknown as AuthInterpreter;

const authSendSpy = jest.spyOn(authenticatedStateMachine, 'send');
const unauthSendSpy = jest.spyOn(unauthenticatedStateMachine, 'send');

const onSignIn = jest.fn();
const onSignOut = jest.fn();
const service = { send: jest.fn() } as unknown as AuthInterpreter;

describe('defaultAuthHubHandler', () => {
beforeEach(() => {
authSendSpy.mockClear();
unauthSendSpy.mockClear();
onSignIn.mockClear();
onSignOut.mockClear();
jest.clearAllMocks();
});

// @todo-migration probably remove token refresh event handling
it.skip('responds to token refresh event when state is authenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'tokenRefresh' } },
authenticatedStateMachine
);
expect(authSendSpy).toHaveBeenCalledWith('TOKEN_REFRESH');
});
it.each(['tokenRefresh_failure', 'signedOut'])(
'handles a %s event as expected',
(event) => {
defaultAuthHubHandler({ channel: 'auth', payload: { event } }, service);
expect(service.send).toHaveBeenCalledTimes(1);
expect(service.send).toHaveBeenCalledWith('SIGN_OUT');
}
);

it('ignores token refresh event when state is unauthenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'tokenRefresh' } },
unauthenticatedStateMachine
it('handles a signInWithRedirect event as expected', () => {
defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'signInWithRedirect' } },
service
);
expect(unauthSendSpy).not.toHaveBeenCalled();
expect(service.send).toHaveBeenCalledTimes(1);
expect(service.send).toHaveBeenCalledWith('SIGN_IN_WITH_REDIRECT');
});

// @todo-migration
// expect(jest.fn()).toHaveBeenCalledWith(...expected)
// Expected: "SIGN_OUT"
// Number of calls: 0
it.skip('responds to signOut event when state is authenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'signOut' } },
authenticatedStateMachine
it('calls onSignOut callabck on signedOut event if provided', () => {
defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'signedOut' } },
service,
{ onSignOut }
);
expect(authSendSpy).toHaveBeenCalledWith('SIGN_OUT');
expect(onSignOut).toHaveBeenCalledTimes(1);
});

it('ignores signOut event when state is unauthenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'signOut' } },
unauthenticatedStateMachine
it('does not call onSignOut callback on tokenRefreh_failure event if provided', () => {
defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'tokenRefreh_failure' } },
service,
{ onSignOut }
);
expect(unauthSendSpy).not.toHaveBeenCalled();
expect(onSignOut).not.toHaveBeenCalled();
});

it('signs user out when token refresh failed in authenticated state', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'tokenRefresh_failure' } },
authenticatedStateMachine
it('calls onSignIn callabck on signedIn event if provided', () => {
defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'signedIn' } },
service,
{ onSignIn }
);
expect(authSendSpy).toHaveBeenCalledWith('SIGN_OUT');
expect(onSignIn).toHaveBeenCalledTimes(1);
});
});

// @todo-migration potentially remove
it.skip('ignores token refresh failure event when state is unauthenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'tokenRefresh_failure' } },
unauthenticatedStateMachine
);
expect(unauthSendSpy).not.toHaveBeenCalled();
});
describe('listenToAuthHub', () => {
it('creates a Hub listener', () => {
// add empty mockImplementation to prevent logging "auth channel" warning output to the console
jest.spyOn(console, 'warn').mockImplementation();

// @todo-migration
// expect(jest.fn()).toHaveBeenCalledWith(...expected)
// Expected: {"type": "AUTO_SIGN_IN"}
// Number of calls: 0
it.skip('responds to autoSignIn event when state is unauthenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'autoSignIn' } },
unauthenticatedStateMachine
);
expect(unauthSendSpy).toHaveBeenCalledWith({ type: 'AUTO_SIGN_IN' });
});
const hubListenSpy = jest.spyOn(Hub, 'listen');
const handler = jest.fn();

it('ignores autoSignIn event when state is authenticated', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'autoSignIn' } },
authenticatedStateMachine
);
expect(unauthSendSpy).not.toHaveBeenCalled();
});
listenToAuthHub(service, handler);

// @todo-migration
// expect(jest.fn()).toHaveBeenCalledWith(...expected)
// Expected: {"type": "AUTO_SIGN_IN_FAILURE"}
// Number of calls: 0
it.skip('responds to autoSignIn_failure event', async () => {
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'autoSignIn_failure' } },
unauthenticatedStateMachine
expect(hubListenSpy).toHaveBeenCalledTimes(1);
expect(hubListenSpy).toHaveBeenCalledWith(
'auth',
expect.any(Function),
'authenticator-hub-handler'
);
expect(unauthSendSpy).toHaveBeenCalledWith({
type: 'AUTO_SIGN_IN_FAILURE',
});
});

// @todo-migration
// Expected number of calls: 1
// Received number of calls: 0
it.skip.each(['signIn', 'signOut'])(
'calls the %s event handler as expected',
async (event) => {
const handler = event === 'signIn' ? onSignIn : onSignOut;
const handlerKey = event === 'signIn' ? 'onSignIn' : 'onSignOut';
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event } },
authenticatedStateMachine,
{ [handlerKey]: handler }
);
expect(handler).toHaveBeenCalledTimes(1);
}
);

it("doesn't break when unsupported event is passed", async () => {
const spyError = jest.spyOn(console, 'error');
await defaultAuthHubHandler(
{ channel: 'auth', payload: { event: 'unsupported' } },
unauthenticatedStateMachine
Hub.dispatch('auth', { event: 'signedIn' }, 'authenticator-hub-handler');

expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(
{
channel: 'auth',
patternInfo: [],
payload: { event: 'signedIn' },
source: 'authenticator-hub-handler',
},
{ send: expect.any(Function) }
);
expect(spyError).not.toHaveBeenCalled();
});
});
24 changes: 7 additions & 17 deletions packages/ui/src/helpers/authenticator/defaultAuthHubHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AuthInterpreter, AuthMachineHubHandler } from './types';
* Handles Amplify JS Auth hub events, by forwarding hub events as appropriate
* xstate events.
*/
export const defaultAuthHubHandler: AuthMachineHubHandler = async (
export const defaultAuthHubHandler: AuthMachineHubHandler = (
{ payload },
service,
options
Expand Down Expand Up @@ -42,29 +42,19 @@ export const defaultAuthHubHandler: AuthMachineHubHandler = async (
}
};

type HubHandler = Parameters<typeof Hub.listen>[1];
const getHubEventHandler =
(service: AuthInterpreter, handler: AuthMachineHubHandler): HubHandler =>
(data) => {
handler(data, service);
};

/**
* Listens to external auth Hub events and sends corresponding event to
* the `authService` of interest
*
* @param send - `send` function associated with the `authService` of interest
* the `service.send` of interest
*
* @param service - contains state machine `send` function
* @param handler - auth event handler
* @returns function that unsubscribes to the hub evenmt
*/
export const listenToAuthHub = (
service: AuthInterpreter,
// angular passes its own `handler` param
handler: AuthMachineHubHandler = defaultAuthHubHandler
) => {
return Hub.listen(
'auth',
getHubEventHandler(service, handler),
'authenticator-hub-handler'
);
const eventHandler: Parameters<typeof Hub.listen>[1] = (data) =>
handler(data, service);
return Hub.listen('auth', eventHandler, 'authenticator-hub-handler');
};
2 changes: 1 addition & 1 deletion packages/ui/src/helpers/authenticator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export type AuthMachineHubHandler = (
onSignIn?: (user: AuthUser) => void;
onSignOut?: () => void;
}
) => Promise<void>;
) => void;
6 changes: 3 additions & 3 deletions packages/vue/src/composables/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export const useAuth = createSharedComposable((): UseAuth => {
authStatus.value = 'unauthenticated';
};

const unsubscribeHub = listenToAuthHub(service, async (data, service) => {
await defaultAuthHubHandler(data, service, { onSignIn, onSignOut });
});
const unsubscribeHub = listenToAuthHub(service, (data, service) =>
defaultAuthHubHandler(data, service, { onSignIn, onSignOut })
);

getCurrentUser()
.then(() => {
Expand Down

0 comments on commit 4f643b0

Please sign in to comment.