Created
November 20, 2019 06:13
-
-
Save kalda341/4d7d79d87d8982b2bfd13f9814505231 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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