Skip to content

Commit

Permalink
refactor: perssistSession function
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelgozi committed Apr 19, 2020
1 parent 5ea30c6 commit 37f7af7
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 44 deletions.
47 changes: 23 additions & 24 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import humanReadableErrors from './errors.json';

// Generate a local storage adapter.
// Its a bit verbose, but takes less characters than writing it manually.
const localStorageAdapter = {};
['set', 'get', 'remove'].forEach(m => (localStorageAdapter[m] = async (k, v) => localStorage[m + 'Item'](k, v)));
const storageApi = {};
['set', 'get', 'remove'].forEach(m => (storageApi[m] = async (k, v) => localStorage[m + 'Item'](k, v)));

/**
* Encapsulates authentication flow logic.
Expand All @@ -41,7 +41,7 @@ const localStorageAdapter = {};
* @param {Array.<ProviderOptions|string>} options.providers Array of arguments that will be passed to the addProvider method.
*/
export default class Auth {
constructor({ name = 'default', apiKey, redirectUri, storage = localStorageAdapter } = {}) {
constructor({ name = 'default', apiKey, redirectUri, storage = storageApi } = {}) {
if (typeof apiKey !== 'string') throw Error('The argument "apiKey" is required');

Object.assign(this, {
Expand All @@ -54,7 +54,7 @@ export default class Auth {

this.storage.get(this.sKey('User')).then(user => {
this.user = JSON.parse(user);
if (user) this.refreshIdToken(false).then(tm => this.fetchProfile(tm));
if (user) this.refreshIdToken().then(() => this.fetchProfile());
else this.emit();
});

Expand All @@ -65,7 +65,7 @@ export default class Auth {
// This code will run if localStorage for this user
// data was updated from a different browser window.
if (e.key !== this.sKey('User')) return;
this.persistSession(JSON.parse(e.newValue), false);
this.setState(JSON.parse(e.newValue), false);
});
}

Expand Down Expand Up @@ -146,53 +146,51 @@ export default class Auth {
* @param {boolean} [updateStorage = true] Whether to update local storage or not.
* @private
*/
async persistSession(userData, updateStorage = true) {
if (updateStorage) await this.storage[userData ? 'set' : 'remove'](this.sKey('User'), JSON.stringify(userData));
async setState(userData, persist = true, emit = true) {
this.user = userData;
this.emit();
persist && (await this.storage[userData ? 'set' : 'remove'](this.sKey('User'), JSON.stringify(userData)));
emit && this.emit();
}

/**
* Sign out the currently signed in user.
* Removes all data stored in the storage that's associated with the user.
*/
signOut() {
return this.persistSession(null);
return this.setState(null);
}

/**
* Refreshes the idToken by using the locally stored refresh token
* only if the idToken has expired.
* @private
*/
async refreshIdToken(persist = true) {
async refreshIdToken() {
// If the idToken didn't expire, return.
if (Date.now() < this.user.tokenManager.expiresAt) return;

// If a request for a new token was already made, then wait for it and then return.
if (this.refreshRequest) {
return await this.refreshRequest;
if (this._ref) {
return void (await this._ref);
}

try {
// Save the promise so that if this function is called
// anywhere else we don't make more than one request.
this.refreshRequest = this.api('token', {
this._ref = this.api('token', {
grant_type: 'refresh_token',
refresh_token: this.user.tokenManager.refreshToken
}).then(async data => {
}).then(data => {
const tokenManager = {
idToken: data.id_token,
refreshToken: data.refresh_token,
expiresAt: data.expiresAt
};
if (persist) await this.persistSession({ ...this.user, tokenManager });
return tokenManager;
return this.setState({ ...this.user, tokenManager }, true, false);
});

return await this.refreshRequest;
await this._ref;
} finally {
this.refreshRequest = null;
this._ref = null;
}
}

Expand Down Expand Up @@ -237,17 +235,18 @@ export default class Auth {
throw Error('In order to use an Identity provider you should initiate the "Auth" instance with a "redirectUri".');

// The options can be a string, or an object, so here we make sure we extract the right data in each case.
const { provider, scope, context, linkAccount } = typeof options === 'string' ? { provider: options } : options;
const { provider, oauthScope, context, linkAccount } =
typeof options === 'string' ? { provider: options } : options;

// Make sure the user is logged in when an "account link" was requested.
if (linkAccount) await this.enforceAuth();

// Get the url and other data necessary for the authentication.
const { authUri, sessionId } = await this.api('createAuthUri', {
providerId: provider,
continueUri: this.redirectUri,
oauthScope: scope,
authFlowType: 'CODE_FLOW',
providerId: provider,
oauthScope,
context
});

Expand Down Expand Up @@ -407,7 +406,7 @@ export default class Auth {
delete userData.kind;
userData.tokenManager = tokenManager;

await this.persistSession(userData);
await this.setState(userData);
}

/**
Expand Down Expand Up @@ -437,7 +436,7 @@ export default class Auth {
delete updatedData.idToken;
delete updatedData.refreshToken;

await this.persistSession(updatedData);
await this.setState(updatedData);
}

/**
Expand Down
33 changes: 13 additions & 20 deletions test/main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ describe('localStorageAdapter()', () => {
test('get() returns an item from local storage', async () => {
expect(await auth.storage.get('testKey')).toEqual('testValue');
});

test('delete() removes an item from local storage', async () => {
await auth.storage.remove('testKey');
expect(localStorage.getItem('testKey')).toEqual(null);
});
});

describe('Auth', () => {
Expand Down Expand Up @@ -282,24 +277,27 @@ describe('Auth', () => {
});
});

describe('perssistSession()', () => {
describe('setState()', () => {
test('Stores the user data locally', async () => {
const auth = new Auth({ apiKey: 'key' });
await auth.persistSession({ test: 'working' });
await auth.setState({ test: 'working' });

expect(await auth.storage.get('Auth:User:key:default')).toEqual(JSON.stringify({ test: 'working' }));
});

test("Doens't update storage when second argument is false", async () => {
const auth = new Auth({ apiKey: 'key' });
await auth.persistSession({ test: 'working' }, false);
await auth.setState({ test: 'working' }, false);

expect(await auth.storage.get('Auth:User:key:default')).toEqual(null);
});

test('Updates the "user" property with the new data', async () => {
const auth = new Auth({ apiKey: 'key' });
await auth.persistSession({ test: 'working' });

// Wait instantiation to finish.
await new Promise(resolve => auth.listen(resolve));
await auth.setState({ test: 'working' });

expect(auth.user).toEqual({ test: 'working' });
});
Expand All @@ -310,7 +308,7 @@ describe('Auth', () => {
const callback = jest.fn(() => {});
auth.listen(callback);

await auth.persistSession();
await auth.setState();

// One time in instantiation, and one
// time for the called method.
Expand All @@ -322,10 +320,7 @@ describe('Auth', () => {
test('Deletes user data from storage', async () => {
const auth = new Auth({ apiKey: 'key' });

// Mock logged in user.
await auth.persistSession('test');

// sign out.
await mockLoggedIn(auth);
await auth.signOut();

expect(localStorage.getItem('Auth:User:key:default')).toEqual(null);
Expand Down Expand Up @@ -439,7 +434,7 @@ describe('Auth', () => {
expect(auth.user.tokenManager.idToken).toEqual('updated');
});

test("Doesn't update local storage or emits when persist is set to false", async () => {
test("Doesn't emit", async () => {
// The constructor makes some requests.
// We have to mock them for this not to throw
fetch.mockResponse('{"refresh_token": "updated", "id_token": "updated"}');
Expand All @@ -458,11 +453,9 @@ describe('Auth', () => {

const listener = jest.fn(() => {});
auth.listen(listener);
const tm = await auth.refreshIdToken(false);
await auth.refreshIdToken();

expect(listener).toHaveBeenCalledTimes(0);
expect(localStorage.getItem(auth.sKey('User'))).toEqual(null);
expect(tm).toEqual({ refreshToken: 'updated', idToken: 'updated', expiresAt: NaN });
});
});

Expand Down Expand Up @@ -556,9 +549,9 @@ describe('Auth', () => {

expect(body).toEqual(
JSON.stringify({
providerId: 'google.com',
continueUri: 'redirectHere',
authFlowType: 'CODE_FLOW'
authFlowType: 'CODE_FLOW',
providerId: 'google.com'
})
);
});
Expand Down

0 comments on commit 37f7af7

Please sign in to comment.