Skip to content

Instantly share code, notes, and snippets.

@kalda341
Created November 20, 2019 06:13
Show Gist options
  • Save kalda341/4d7d79d87d8982b2bfd13f9814505231 to your computer and use it in GitHub Desktop.
Save kalda341/4d7d79d87d8982b2bfd13f9814505231 to your computer and use it in GitHub Desktop.
import React, { useContext } from 'react';
import TestRenderer from 'react-test-renderer';
import { AuthProvider, AuthContext } from './AuthContext';
import { UserReadableError } from 'func-utils';
const token = expiry => '.' + btoa(JSON.stringify({ exp: expiry / 1000 }));
describe('AuthProvider', () => {
let providerContext = null;
let mount = null;
let unmount = null;
let mockLoadToken = null;
let mockSaveToken = null;
let mockRequestToken = null;
let mockRefreshToken = null;
beforeEach(() => {
jest.useFakeTimers();
const ProviderChild = function() {
providerContext = useContext(AuthContext);
return null;
};
mockLoadToken = jest.fn();
mockSaveToken = jest.fn();
mockRequestToken = jest.fn();
mockRefreshToken = jest.fn();
mount = () => {
const renderer = TestRenderer.create(
<AuthProvider
loadToken={mockLoadToken}
saveToken={mockSaveToken}
requestToken={mockRequestToken}
refreshToken={mockRefreshToken}
>
<ProviderChild />
</AuthProvider>
);
// So that we can clean up after ourselves
unmount = () => {
renderer.unmount();
};
return renderer;
};
});
afterEach(() => {
if (unmount) {
// Very important - otherwise timers will continue and influence other tests
unmount();
}
});
it('is in loading state on first render', () => {
mount();
expect(providerContext.isAuthenticated).toBe(false);
expect(providerContext.isLoading).toBe(true);
});
it('is not authenticated if no token is present', async () => {
mockLoadToken.mockImplementation(async () => null);
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(false);
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// No refresh if we don't have a valid token
expect(setTimeout).toHaveBeenCalledTimes(0);
});
it('uses local token if it is valid', async () => {
const accessToken = token(new Date().getTime() + 60 * 60 * 1000);
mockLoadToken.mockImplementation(async () => ({
access: accessToken,
refresh: token(new Date().getTime() + 60 * 60 * 1000)
}));
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(true);
expect(providerContext.authHeaders).toStrictEqual({
Authorization: `Bearer ${accessToken}`
});
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// Schedule token refresh
expect(setTimeout).toHaveBeenCalledTimes(1);
});
it('refreshes local access token if it has expired', async () => {
mockLoadToken.mockImplementation(async () => ({
access: token(new Date().getTime() - 1),
refresh: token(new Date().getTime() + 60 * 60 * 1000)
}));
const newAccessToken = token(new Date().getTime() + 60 * 60 * 1000);
mockRefreshToken.mockImplementation(async () => ({
access: newAccessToken
}));
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(true);
expect(providerContext.authHeaders).toStrictEqual({
Authorization: `Bearer ${newAccessToken}`
});
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(1);
// Schedule next token refresh
expect(setTimeout).toHaveBeenCalledTimes(1);
});
it('refreshes access token when it expires', async () => {
const originalAccessExpiry = new Date().getTime() + 40 * 60 * 1000;
const originalAccessToken = token(originalAccessExpiry);
mockLoadToken.mockImplementation(async () => ({
access: originalAccessToken,
refresh: token(new Date().getTime() + 60 * 60 * 1000)
}));
const newAccessExpiry = new Date().getTime() + 60 * 60 * 1000;
const newAccessToken = token(newAccessExpiry);
mockRefreshToken.mockImplementation(async () => ({
access: newAccessToken
}));
await TestRenderer.act(async () => {
mount();
});
// At this point we should have a valid token
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(true);
expect(providerContext.authHeaders).toStrictEqual({
Authorization: `Bearer ${originalAccessToken}`
});
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// We expect the timer to have been set
expect(setTimeout).toHaveBeenCalledTimes(1);
// Advance the timer
await TestRenderer.act(async () => {
jest.runOnlyPendingTimers();
});
// At this point we should have refreshed our token
// We shouldn't have loaded the token again
expect(mockLoadToken).toHaveBeenCalledTimes(1);
// We should have saved the new token
expect(mockSaveToken).toHaveBeenCalledTimes(2);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(1);
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(true);
expect(providerContext.authHeaders).toStrictEqual({
Authorization: `Bearer ${newAccessToken}`
});
// And if we advance the timer again we should refresh again
await TestRenderer.act(async () => {
jest.runOnlyPendingTimers();
});
expect(mockSaveToken).toHaveBeenCalledTimes(3);
});
it('should update the token after successful authentication', async () => {
mockLoadToken.mockImplementation(async () => null);
const accessToken = token(new Date().getTime() + 60 * 60 * 1000);
mockRequestToken.mockImplementation(async () => ({
access: accessToken,
refresh: token(new Date().getTime() + 60 * 60 * 1000)
}));
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(false);
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// Attempt authentication
await TestRenderer.act(async () => {
providerContext.authenticate('[email protected]', '12345678');
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(true);
expect(providerContext.authHeaders).toStrictEqual({
Authorization: `Bearer ${accessToken}`
});
expect(mockLoadToken).toHaveBeenCalledTimes(1);
// We should have saved the new token
expect(mockSaveToken).toHaveBeenCalledTimes(2);
expect(mockRequestToken).toHaveBeenCalledTimes(1);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
});
it('should be unauthenticated after unauthenticating', async () => {
const accessToken = token(new Date().getTime() + 60 * 60 * 1000);
mockLoadToken.mockImplementation(async () => ({
access: accessToken,
refresh: token(new Date().getTime() + 60 * 60 * 1000)
}));
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(true);
expect(providerContext.authHeaders).toStrictEqual({
Authorization: `Bearer ${accessToken}`
});
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// Unauthenticate
await TestRenderer.act(async () => {
providerContext.unauthenticate();
});
expect(mockLoadToken).toHaveBeenCalledTimes(1);
// We should have removed the token
expect(mockSaveToken).toHaveBeenCalledTimes(2);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// Test that the refresh has been cancelled
await TestRenderer.act(async () => {
jest.runOnlyPendingTimers();
});
// We shouldn't have refreshed, as we no longer have a token!
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
});
it('should stay unauthenticated after unsuccessful authentication', async () => {
mockLoadToken.mockImplementation(async () => null);
mockRequestToken.mockImplementation(() => Promise.reject({ status: 401 }));
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(false);
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
// Attempt authentication
let authPromise = null;
await TestRenderer.act(async () => {
authPromise = providerContext.authenticate('[email protected]', '12345678');
});
await expect(authPromise).rejects.toEqual(
new UserReadableError('Invalid email or password')
);
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(false);
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(2);
expect(mockRequestToken).toHaveBeenCalledTimes(1);
expect(mockRefreshToken).toHaveBeenCalledTimes(0);
});
it('is not authenticated after we fail to refresh the token', async () => {
mockLoadToken.mockImplementation(async () => ({
access: token(new Date().getTime() - 1),
refresh: token(new Date().getTime() - 1)
}));
mockRefreshToken.mockImplementation(() =>
Promise.reject(new Error('Failed to refresh access token'))
);
await TestRenderer.act(async () => {
mount();
});
expect(providerContext.isLoading).toBe(false);
expect(providerContext.isAuthenticated).toBe(false);
expect(mockLoadToken).toHaveBeenCalledTimes(1);
expect(mockSaveToken).toHaveBeenCalledTimes(1);
expect(mockRequestToken).toHaveBeenCalledTimes(0);
expect(mockRefreshToken).toHaveBeenCalledTimes(1);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment