From b81e52b03b2d3bcbdfb4f7d2226d8a5526e31230 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Thu, 22 Feb 2018 10:32:51 -0800 Subject: [PATCH 1/6] refactored storage test --- packages/auth/src/authstorage.js | 12 +- .../auth/src/iframeclient/iframewrapper.js | 15 +- packages/auth/src/storage/factory.js | 5 +- packages/auth/src/storage/inmemorystorage.js | 18 +- packages/auth/src/storage/mockstorage.js | 99 ++++++++ packages/auth/src/storageusermanager.js | 3 +- packages/auth/src/utils.js | 18 ++ packages/auth/test/auth_test.js | 151 +++++------ packages/auth/test/autheventmanager_test.js | 20 +- packages/auth/test/authstorage_test.js | 240 +++++++++++++++--- packages/auth/test/cordovahandler_test.js | 22 +- packages/auth/test/storage/factory_test.js | 28 +- .../auth/test/storage/mockstorage_test.js | 126 +++++++++ .../auth/test/storageautheventmanager_test.js | 108 +++++--- .../test/storageoauthhandlermanager_test.js | 57 +++-- .../storagependingredirectmanager_test.js | 28 +- .../test/storageredirectusermanager_test.js | 30 ++- packages/auth/test/storageusermanager_test.js | 156 ++++++------ packages/auth/test/testhelper.js | 36 ++- packages/auth/test/utils_test.js | 33 +++ 20 files changed, 869 insertions(+), 336 deletions(-) create mode 100644 packages/auth/src/storage/mockstorage.js create mode 100644 packages/auth/test/storage/mockstorage_test.js diff --git a/packages/auth/src/authstorage.js b/packages/auth/src/authstorage.js index 405b7b8e276..5f4033b8c54 100644 --- a/packages/auth/src/authstorage.js +++ b/packages/auth/src/authstorage.js @@ -235,6 +235,12 @@ fireauth.authStorage.Manager.getInstance = function() { }; +/** Clears storage manager instances. This is used for testing. */ +fireauth.authStorage.Manager.clear = function() { + fireauth.authStorage.Manager.instance_ = null; +}; + + /** * Returns the storage corresponding to the specified persistence. * @param {!fireauth.authStorage.Persistence} persistent The type of storage @@ -404,9 +410,9 @@ fireauth.authStorage.Manager.prototype.startListeners_ = function() { // TODO: refactor this implementation to be handled by the underlying // storage mechanism. if (!this.runsInBackground_ && - // Add an exception for IE11 and Edge browsers, we should stick to - // indexedDB in that case. - !fireauth.util.isLocalStorageNotSynchronized() && + // Add an exception for browsers that persist storage with indexedDB, we + // should stick with indexedDB listener implementation in that case. + !fireauth.util.persistsStorageWithIndexedDB() && // Confirm browser web storage is supported as polling relies on it. this.webStorageSupported_) { this.startManualListeners_(); diff --git a/packages/auth/src/iframeclient/iframewrapper.js b/packages/auth/src/iframeclient/iframewrapper.js index c92d289b617..25298bff318 100644 --- a/packages/auth/src/iframeclient/iframewrapper.js +++ b/packages/auth/src/iframeclient/iframewrapper.js @@ -174,10 +174,12 @@ fireauth.iframeclient.IframeWrapper.prototype.registerEvent = this.onIframeOpen_.then(function() { self.iframe_.register( eventName, - handler, + /** @type {function(this:gapi.iframes.Iframe, + * *, gapi.iframes.Iframe): *} + */ (handler), /** @type {!gapi.iframes.IframesFilter} */ ( - fireauth.util.getObjectRef( - 'gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER'))); + fireauth.util.getObjectRef( + 'gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER'))); }); }; @@ -191,12 +193,15 @@ fireauth.iframeclient.IframeWrapper.prototype.unregisterEvent = function(eventName, handler) { var self = this; this.onIframeOpen_.then(function() { - self.iframe_.unregister(eventName, handler); + self.iframe_.unregister( + eventName, + /** @type {(function(this:gapi.iframes.Iframe, + * *, gapi.iframes.Iframe): *|undefined)} + */ (handler)); }); }; - /** @private @const {!goog.string.Const} The GApi loader URL. */ fireauth.iframeclient.IframeWrapper.GAPI_LOADER_SRC_ = goog.string.Const.from( 'https://apis.google.com/js/api.js?onload=%{onload}'); diff --git a/packages/auth/src/storage/factory.js b/packages/auth/src/storage/factory.js index bc5e9884c92..d6ab01b5844 100644 --- a/packages/auth/src/storage/factory.js +++ b/packages/auth/src/storage/factory.js @@ -103,9 +103,8 @@ fireauth.storage.Factory.getEnvConfig = function() { * @return {!fireauth.storage.Storage} The persistent storage instance. */ fireauth.storage.Factory.prototype.makePersistentStorage = function() { - if (fireauth.util.isLocalStorageNotSynchronized()) { - // In a browser environment, when an iframe and a popup web storage are not - // synchronized, use the indexedDB fireauth.storage.Storage implementation. + if (fireauth.util.persistsStorageWithIndexedDB()) { + // If persistent storage is implemented using indexedDB, use indexedDB. return fireauth.storage.IndexedDB.getFireauthManager(); } return new this.env_.persistent(); diff --git a/packages/auth/src/storage/inmemorystorage.js b/packages/auth/src/storage/inmemorystorage.js index 5b751576dac..c63da740d49 100644 --- a/packages/auth/src/storage/inmemorystorage.js +++ b/packages/auth/src/storage/inmemorystorage.js @@ -31,8 +31,8 @@ goog.require('goog.Promise'); * @implements {fireauth.storage.Storage} */ fireauth.storage.InMemoryStorage = function() { - /** @private {!Object} The object where we store values. */ - this.storage_ = {}; + /** @protected {!Object} The object where we store values. */ + this.storage = {}; }; @@ -42,7 +42,7 @@ fireauth.storage.InMemoryStorage = function() { * @override */ fireauth.storage.InMemoryStorage.prototype.get = function(key) { - return goog.Promise.resolve(/** @type {*} */ (this.storage_[key])); + return goog.Promise.resolve(/** @type {*} */ (this.storage[key])); }; @@ -53,7 +53,7 @@ fireauth.storage.InMemoryStorage.prototype.get = function(key) { * @override */ fireauth.storage.InMemoryStorage.prototype.set = function(key, value) { - this.storage_[key] = value; + this.storage[key] = value; return goog.Promise.resolve(); }; @@ -64,14 +64,14 @@ fireauth.storage.InMemoryStorage.prototype.set = function(key, value) { * @override */ fireauth.storage.InMemoryStorage.prototype.remove = function(key) { - delete this.storage_[key]; + delete this.storage[key]; return goog.Promise.resolve(); }; /** - * @param {function(!goog.events.BrowserEvent)} listener The storage event - * listener. + * @param {function((!goog.events.BrowserEvent|!Array))} listener The + * storage event listener. * @override */ fireauth.storage.InMemoryStorage.prototype.addStorageListener = @@ -80,8 +80,8 @@ fireauth.storage.InMemoryStorage.prototype.addStorageListener = /** - * @param {function(!goog.events.BrowserEvent)} listener The storage event - * listener. + * @param {function((!goog.events.BrowserEvent|!Array))} listener The + * storage event listener. * @override */ fireauth.storage.InMemoryStorage.prototype.removeStorageListener = function( diff --git a/packages/auth/src/storage/mockstorage.js b/packages/auth/src/storage/mockstorage.js new file mode 100644 index 00000000000..d118fda4f3d --- /dev/null +++ b/packages/auth/src/storage/mockstorage.js @@ -0,0 +1,99 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.MockStorage'); + +goog.require('fireauth.storage.InMemoryStorage'); +goog.require('fireauth.storage.Storage'); +goog.require('fireauth.util'); +goog.require('goog.array'); + + +/** + * Mock storage structure useful for testing and mocking local storage and other + * types of storage without depending on any native type of storage. + * @constructor + * @implements {fireauth.storage.Storage} + * @extends {fireauth.storage.InMemoryStorage} + */ +fireauth.storage.MockStorage = function() { + /** + * @private {!Array))>} The + * storage listeners. + */ + this.storageListeners_ = []; + fireauth.storage.MockStorage.base(this, 'constructor'); +}; +goog.inherits(fireauth.storage.MockStorage, fireauth.storage.InMemoryStorage); + + +/** + * @param {function((!goog.events.BrowserEvent|!Array))} listener The + * storage event listener. + * @override + */ +fireauth.storage.MockStorage.prototype.addStorageListener = function(listener) { + this.storageListeners_.push(listener); +}; + + +/** + * @param {function((!goog.events.BrowserEvent|!Array))} listener The + * storage event listener. + * @override + */ +fireauth.storage.MockStorage.prototype.removeStorageListener = function( + listener) { + goog.array.removeAllIf(this.storageListeners_, function(ele) { + return ele == listener; + }); +}; + + +/** + * Simulates a storage event getting triggered which would trigger any attached + * listener. Any fired event would also update the underlying storage map. + * @param {!Event} storageEvent The storage event triggered. + */ +fireauth.storage.MockStorage.prototype.fireBrowserEvent = + function(storageEvent) { + // Get key of storage event. + var key = storageEvent.key; + if (key != null) { + // If key available, get newValue. + var newValue = storageEvent.newValue; + if (newValue != null) { + // newValue available, update corresponding value. + this.storage[key] = fireauth.util.parseJSON(newValue); + } else { + // newValue not available, delete the corresponding key's entry. + delete this.storage[key]; + } + } else { + // If key not available, clear storage. + this.clear(); + } + // Trigger all attached storage listeners. + goog.array.forEach(this.storageListeners_, function(listener) { + listener([key]); + }); +}; + + +/** Clears all stored data. */ +fireauth.storage.MockStorage.prototype.clear = function() { + this.storage = {}; +}; diff --git a/packages/auth/src/storageusermanager.js b/packages/auth/src/storageusermanager.js index bb05eee0612..44eadac0d07 100644 --- a/packages/auth/src/storageusermanager.js +++ b/packages/auth/src/storageusermanager.js @@ -66,6 +66,7 @@ goog.provide('fireauth.storage.UserManager'); goog.require('fireauth.AuthUser'); goog.require('fireauth.authStorage'); +goog.require('goog.Promise'); /** @@ -127,7 +128,7 @@ fireauth.storage.UserManager.prototype.switchToLocalOnExternalEvent_ = // local. this.waitForReady_(function() { return goog.Promise.resolve().then(function() { - // In current persistence is not already local. + // If current persistence is not already local. if (self.currentAuthUserKey_ && self.currentAuthUserKey_.persistent != fireauth.authStorage.Persistence.LOCAL) { diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index c311559a0d8..f5def3ed324 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -1335,3 +1335,21 @@ fireauth.util.utcTimestampToDateString = function(utcTimestamp) { } return null; }; + + +/** @return {boolean} Whether indexedDB is available. */ +fireauth.util.isIndexedDBAvailable = function() { + return !!goog.global['indexedDB']; +}; + + +/** @return {boolean} Whether indexedDB is used to persist storage. */ +fireauth.util.persistsStorageWithIndexedDB = function() { + // This will cover: + // IE11, Edge when indexedDB is available (this is unavailable in InPrivate + // mode). + // In a browser environment, when an iframe and a popup web storage are not + // synchronized, use the indexedDB fireauth.storage.Storage implementation. + return fireauth.util.isLocalStorageNotSynchronized() && + fireauth.util.isIndexedDBAvailable(); +}; diff --git a/packages/auth/test/auth_test.js b/packages/auth/test/auth_test.js index 516d09ea30a..d8a51e384d5 100644 --- a/packages/auth/test/auth_test.js +++ b/packages/auth/test/auth_test.js @@ -43,6 +43,7 @@ goog.require('fireauth.exports'); goog.require('fireauth.idp'); goog.require('fireauth.iframeclient.IfcHandler'); goog.require('fireauth.object'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.PendingRedirectManager'); goog.require('fireauth.storage.RedirectUserManager'); goog.require('fireauth.storage.UserManager'); @@ -147,9 +148,15 @@ var actionCodeSettings = { }, 'handleCodeInApp': true }; +var mockLocalStorage; +var mockSessionStorage; function setUp() { + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); // Disable Auth event manager for testing unless needed. fireauth.AuthEventManager.ENABLED = false; // Assume origin is a valid one. @@ -185,8 +192,7 @@ function setUp() { fireauth.util, 'getCurrentUrl', function() {return 'http://localhost';}); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Initialize App and Auth instances. config1 = { apiKey: 'apiKey1' @@ -323,7 +329,7 @@ function tearDown() { } finally { mockControl.$tearDown(); } - delete fireauth.authStorage.Manager.instance_; + fireauth.authStorage.Manager.clear(); currentUserStorageManager = null; redirectUserStorageManager = null; } @@ -361,16 +367,8 @@ function assertAuthTokenListenerCalledOnce(auth) { } -/** Simulates that local storage synchronizes across tabs. */ -function simulateLocalStorageSynchronized() { - stubs.replace( - fireauth.util, - 'isIe11', - function() {return false;}); - stubs.replace( - fireauth.util, - 'isEdge', - function() {return false;}); +/** Initializes mock storages. */ +function initializeMockStorage() { // Simulate tab can run in background. stubs.replace( fireauth.util, @@ -378,6 +376,8 @@ function simulateLocalStorageSynchronized() { function() { return true; }); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); } @@ -452,8 +452,7 @@ function testToJson_withUser() { // Test toJSON with a user signed in. stubs.reset(); fireauth.AuthEventManager.ENABLED = false; - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate available token. stubs.replace( fireauth.AuthUser.prototype, @@ -755,8 +754,7 @@ function testAddAuthTokenListener_initialNullState() { function() { return goog.Promise.resolve(null); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Suppress addStateChangeListener. stubs.replace( fireauth.storage.UserManager.prototype, @@ -829,8 +827,7 @@ function testAddAuthTokenListener_initialValidState() { function() { return goog.Promise.resolve(user); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Suppress addStateChangeListener. stubs.replace( fireauth.storage.UserManager.prototype, @@ -1126,8 +1123,7 @@ function testNotifyAuthStateObservers() { function() { return goog.Promise.resolve(user); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Suppress addStateChangeListener. stubs.replace( fireauth.storage.UserManager.prototype, @@ -1219,8 +1215,7 @@ function testAuth_onAuthStateChanged() { function() { return goog.Promise.resolve(user); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Suppress addStateChangeListener. stubs.replace( fireauth.storage.UserManager.prototype, @@ -1762,8 +1757,7 @@ function testAuth_authEventManager() { // Test Auth event manager. fireauth.AuthEventManager.ENABLED = true; stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); var expectedManager = { 'subscribe': goog.testing.recordFunction(), 'unsubscribe': goog.testing.recordFunction() @@ -1900,8 +1894,7 @@ function testAuth_initState_signedInStatus() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // New loaded user should be reloaded before being set as current user. @@ -1999,8 +1992,7 @@ function testAuth_initState_signedInStatus() { function testAuth_initState_reloadUpdate_previousSignedInUser() { asyncTestCase.waitForSignals(2); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate reload introduced external changes to user. stubs.replace( fireauth.AuthUser.prototype, @@ -2063,8 +2055,7 @@ function testAuth_initState_signedInStatus_differentAuthDomain() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // New loaded user should be reloaded before being set as current user. @@ -2128,8 +2119,7 @@ function testAuth_initState_signedInStatus_withRedirectUser() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Return new token on each request. @@ -2260,8 +2250,7 @@ function testAuth_initState_signedInStatus_withRedirectUser_sameEventId() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Return new token on each request. @@ -2392,8 +2381,7 @@ function testAuth_initState_signedInStatus_deletedUser() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // New loaded user should be reloaded. In this case throw an error. @@ -2465,8 +2453,7 @@ function testAuth_initState_signedInStatus_offline() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // New loaded user should be reloaded. In this case throw an error. @@ -2557,8 +2544,7 @@ function testAuth_initState_signedOutStatus() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Current user change listener should be added. @@ -2619,8 +2605,7 @@ function testAuth_syncAuthChanges_sameUser() { function() { return goog.Promise.resolve(); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Save sync listener. @@ -2756,8 +2741,7 @@ function testAuth_syncAuthChanges_newSignIn() { function() { return goog.Promise.resolve(); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Save sync listener. @@ -2877,8 +2861,7 @@ function testAuth_syncAuthChanges_newSignIn_differentAuthDomain() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); asyncTestCase.waitForSignals(1); @@ -2928,7 +2911,7 @@ function testAuth_syncAuthChanges_newSignIn_differentAuthDomain() { asyncTestCase.signal(); }); // This should force localstorage sync. - goog.testing.events.fireBrowserEvent(storageEvent); + mockLocalStorage.fireBrowserEvent(storageEvent); }); }); } @@ -2952,8 +2935,7 @@ function testAuth_syncAuthChanges_newSignOut() { function() { return goog.Promise.resolve(); }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Save sync listener. @@ -3061,8 +3043,7 @@ function testAuth_signInWithIdTokenResponse_newUser() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Initialize from ID token response should be called and resolved with the @@ -3151,8 +3132,7 @@ function testAuth_signInWithIdTokenResponse_sameUser() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Initialize from ID token response should be called and resolved with the @@ -3285,8 +3265,7 @@ function testAuth_signInWithIdTokenResponse_newUserDifferentFromCurrent() { function() { return now; }); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Stub OAuth sign in handler. fakeOAuthSignInHandler(); // Initialize from ID token response should be called and resolved with the @@ -6565,8 +6544,7 @@ function testAuth_returnFromSignInWithRedirect_withExistingUser() { null, 'http://www.example.com/#response', 'SESSION_ID'); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate user loaded from storage. stubs.replace( fireauth.AuthUser.prototype, @@ -6879,8 +6857,7 @@ function testAuth_returnFromLinkWithRedirect_success() { var expectedCred = fireauth.GoogleAuthProvider.credential(null, 'ACCESS_TOKEN'); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Expected link via redirect successful Auth event. var expectedAuthEvent = new fireauth.AuthEvent( fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, @@ -6987,8 +6964,7 @@ function testAuth_returnFromLinkWithRedirect_redirectedLoggedOutUser_success() { 'http://www.example.com/#response', 'SESSION_ID'); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate user loaded from storage. stubs.replace( fireauth.AuthUser.prototype, @@ -7123,8 +7099,7 @@ function testAuth_redirectedLoggedOutUser_differentAuthDomain() { stubs.reset(); // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); stubs.replace( goog, 'now', @@ -7242,8 +7217,7 @@ function testAuth_returnFromLinkWithRedirect_noCurrentUser_redirectUser() { 'http://www.example.com/#response', 'SESSION_ID'); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate redirect user loaded from storage. stubs.replace( fireauth.storage.RedirectUserManager.prototype, @@ -7440,8 +7414,7 @@ function testAuth_returnFromLinkWithRedirect_redirectedLoggedInUser_success() { 'http://www.example.com/#response', 'SESSION_ID'); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate user loaded from storage. stubs.replace( fireauth.AuthUser.prototype, @@ -7568,8 +7541,7 @@ function testAuth_returnFromLinkWithRedirect_invalidUser() { 'http://www.example.com/#response', 'SESSION_ID'); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate user loaded from storage. stubs.replace( fireauth.AuthUser.prototype, @@ -7663,8 +7635,7 @@ function testAuth_returnFromLinkWithRedirect_error() { null, expectedError); stubs.reset(); - // Simulate local storage change synchronized. - simulateLocalStorageSynchronized(); + initializeMockStorage(); // Simulate user loaded from storage. stubs.replace( fireauth.AuthUser.prototype, @@ -8572,7 +8543,8 @@ function testAuth_setPersistence_noExistingAuthState() { // Confirm first user saved in session storage. assertUserEquals(user1, user); return fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'session', user1); + auth1.getStorageKey(), 'session', user1, + fireauth.authStorage.Manager.getInstance()); }).then(function() { // Sign in with custom token. return auth1.signInWithCustomToken('CUSTOM_TOKEN'); @@ -8580,7 +8552,8 @@ function testAuth_setPersistence_noExistingAuthState() { // Confirm second user saved in session storage. clock.tick(1); return fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'session', user2); + auth1.getStorageKey(), 'session', user2, + fireauth.authStorage.Manager.getInstance()); }).then(function() { asyncTestCase.signal(); }); @@ -8622,13 +8595,15 @@ function testAuth_setPersistence_existingAuthState() { // When this is first triggered, the previously signed in user should be // switched to session storage. fireauth.common.testHelper.assertUserStorage( - config3['apiKey'] + ':' + appId1, 'session', user1).then(function() { + config3['apiKey'] + ':' + appId1, 'session', user1, + fireauth.authStorage.Manager.getInstance()).then(function() { // Sign in a new user. return auth1.signInAnonymously(); }).then(function() { // Second user should be also persistence in session storage. return fireauth.common.testHelper.assertUserStorage( - config3['apiKey'] + ':' + appId1, 'session', user2); + config3['apiKey'] + ':' + appId1, 'session', user2, + fireauth.authStorage.Manager.getInstance()); }).then(function() { asyncTestCase.signal(); }); @@ -8676,8 +8651,7 @@ function testAuth_temporaryPersistence_externalChange() { var storageEvent = new goog.testing.events.Event( goog.events.EventType.STORAGE, window); // Simulate existing user stored in session storage. - window.sessionStorage.setItem( - storageKey, JSON.stringify(user1.toPlainObject())); + mockSessionStorage.set(storageKey, user1.toPlainObject()); app1 = firebase.initializeApp(config3, appId1); auth1 = app1.auth(); storageEvent.key = 'firebase:authUser:' + auth1.getStorageKey(); @@ -8691,18 +8665,20 @@ function testAuth_temporaryPersistence_externalChange() { // On first call, the first user should be stored in session storage. assertUserEquals(user1, user); fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'session', user1).then(function() { + auth1.getStorageKey(), 'session', user1, + fireauth.authStorage.Manager.getInstance()).then(function() { // Simulate external user signed in on another tab. - window.localStorage.setItem( - storageKey, JSON.stringify(user2.toPlainObject())); - goog.testing.events.fireBrowserEvent(storageEvent); + mockLocalStorage.set( + storageKey, user2.toPlainObject()); + mockLocalStorage.fireBrowserEvent(storageEvent); }); } else if (calls == 2) { // On second call, the second user detected from external event should // be detected and stored in local storage. assertUserEquals(user2, user); fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'local', user2).then(function() { + auth1.getStorageKey(), 'local', user2, + fireauth.authStorage.Manager.getInstance()).then(function() { // Sign in anonymously. auth1.signInAnonymously(); }); @@ -8710,7 +8686,8 @@ function testAuth_temporaryPersistence_externalChange() { // Third anonymous user detected and should be stored in local storage. assertUserEquals(user3, user); fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'local', user3).then(function() { + auth1.getStorageKey(), 'local', user3, + fireauth.authStorage.Manager.getInstance()).then(function() { asyncTestCase.signal(); }); } @@ -8792,7 +8769,8 @@ function testAuth_storedPersistence_returnFromRedirect() { // savePersistenceForRedirect was previously called with session // persistence. return fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'session', user1); + auth1.getStorageKey(), 'session', user1, + fireauth.authStorage.Manager.getInstance()); }).then(function() { asyncTestCase.signal(); }); @@ -8897,7 +8875,8 @@ function testAuth_changedPersistence_returnFromRedirect() { // was session, it will be overriddent by the local persistence // explicitly called after Auth instance is initialized. return fireauth.common.testHelper.assertUserStorage( - auth1.getStorageKey(), 'local', user1); + auth1.getStorageKey(), 'local', user1, + fireauth.authStorage.Manager.getInstance()); }).then(function() { asyncTestCase.signal(); }); diff --git a/packages/auth/test/autheventmanager_test.js b/packages/auth/test/autheventmanager_test.js index d68299608d0..2dce3d1d066 100644 --- a/packages/auth/test/autheventmanager_test.js +++ b/packages/auth/test/autheventmanager_test.js @@ -32,8 +32,10 @@ goog.require('fireauth.RedirectAuthEventProcessor'); goog.require('fireauth.RpcHandler'); goog.require('fireauth.UniversalLinkSubscriber'); goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); goog.require('fireauth.constants'); goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.OAuthHandlerManager'); goog.require('fireauth.storage.PendingRedirectManager'); goog.require('fireauth.util'); @@ -76,8 +78,14 @@ var savePartialEventManager; var timeoutDelay = 30000; var mockControl; var ignoreArgument; +var mockLocalStorage; +var mockSessionStorage; function setUp() { + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); mockControl = new goog.testing.MockControl(); ignoreArgument = goog.testing.mockmatchers.ignoreArgument; mockControl.$resetAll(); @@ -86,7 +94,8 @@ function setUp() { 'resolvePendingPopupEvent': goog.testing.recordFunction(), 'getAuthEventHandlerFinisher': goog.testing.recordFunction() }; - simulateLocalStorageSynchronized(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); // Default OAuth sign in handler is IfcHandler. setOAuthSignInHandlerEnvironment(false); } @@ -162,15 +171,6 @@ function initializePlugins( } -/** Simulates that local storage synchronizes across tabs. */ -function simulateLocalStorageSynchronized() { - stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', - function() {return false;}); -} - - /** * Helper function to set the current OAuth sign in handler. * @param {boolean} isCordova Whether to simulate a Cordova environment. diff --git a/packages/auth/test/authstorage_test.js b/packages/auth/test/authstorage_test.js index 683a6f1ae84..441cecae531 100644 --- a/packages/auth/test/authstorage_test.js +++ b/packages/auth/test/authstorage_test.js @@ -28,6 +28,7 @@ goog.require('fireauth.common.testHelper'); goog.require('fireauth.exports'); goog.require('fireauth.storage.IndexedDB'); goog.require('fireauth.storage.LocalStorage'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.SessionStorage'); goog.require('fireauth.util'); goog.require('goog.Promise'); @@ -50,16 +51,17 @@ var config = { var stubs = new goog.testing.PropertyReplacer(); var appId = 'appId1'; var clock; +var mockLocalStorage; +var mockSessionStorage; function setUp() { - // Simulate browser that synchronizes between and iframe and a popup. - stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', - function() { - return false; - }); + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); clock = new goog.testing.MockClock(true); window.localStorage.clear(); window.sessionStorage.clear(); @@ -209,6 +211,13 @@ function testValidatePersistenceArgument_browser() { function testWebStorageNotSupported() { // Test when web storage not supported. In memory storage should be used // instead. + stubs.reset(); + stubs.replace( + fireauth.storage.IndexedDB, + 'isAvailable', + function() { + return false; + }); stubs.replace( fireauth.storage.LocalStorage, 'isAvailable', @@ -271,7 +280,6 @@ function testWebStorageNotSupported() { .then(function(value) { assertUndefined(value); }); - } @@ -288,10 +296,14 @@ function testGetSet_temporaryStorage() { return manager.get(key, appId); }) .then(function(value) { - assertNull(window.localStorage.getItem(storageKey)); - assertEquals( - window.sessionStorage.getItem(storageKey), - JSON.stringify(expectedValue)); + assertObjectEquals(expectedValue, value); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { assertObjectEquals(expectedValue, value); }) .then(function() { @@ -301,8 +313,14 @@ function testGetSet_temporaryStorage() { return manager.get(key, appId); }) .then(function(value) { - assertNull(window.sessionStorage.getItem(storageKey)); - assertNull(window.localStorage.getItem(storageKey)); + assertUndefined(value); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { assertUndefined(value); }); } @@ -321,19 +339,29 @@ function testGetSet_inMemoryStorage() { return manager.get(key, appId); }) .then(function(value) { - assertNull(window.sessionStorage.getItem(storageKey)); - assertNull(window.localStorage.getItem(storageKey)); assertObjectEquals(expectedValue, value); + return mockLocalStorage.get(storageKey); }) - .then(function() { + .then(function(value) { + assertUndefined(value); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); return manager.remove(key, appId); }) .then(function() { return manager.get(key, appId); }) .then(function(value) { - assertNull(window.sessionStorage.getItem(storageKey)); - assertNull(window.localStorage.getItem(storageKey)); + assertUndefined(value); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { assertUndefined(value); }); } @@ -352,21 +380,29 @@ function testGetSet_persistentStorage() { return manager.get(key, appId); }) .then(function(value) { - assertEquals( - window.localStorage.getItem(storageKey), - JSON.stringify(expectedValue)); - assertNull(window.sessionStorage.getItem(storageKey)); assertObjectEquals(expectedValue, value); + return mockSessionStorage.get(storageKey); }) - .then(function() { + .then(function(value) { + assertUndefined(value); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { + assertObjectEquals(expectedValue, value); return manager.remove(key, appId); }) .then(function() { return manager.get(key, appId); }) .then(function(value) { - assertNull(window.localStorage.getItem(storageKey)); - assertNull(window.sessionStorage.getItem(storageKey)); + assertUndefined(value); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { assertUndefined(value); }); } @@ -385,25 +421,103 @@ function testGetSet_persistentStorage_noId() { return manager.get(key); }) .then(function(value) { - assertEquals( - window.localStorage.getItem(storageKey), - JSON.stringify(expectedValue)); assertObjectEquals(expectedValue, value); + return mockLocalStorage.get(storageKey); }) - .then(function() { + .then(function(value) { + assertObjectEquals(expectedValue, value); return manager.remove(key); }) .then(function() { return manager.get(key); }) .then(function(value) { - assertNull(window.localStorage.getItem(storageKey)); + assertUndefined(value); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { assertUndefined(value); }); } +function testAddRemoveListeners_persistentStorage() { + var manager = + new fireauth.authStorage.Manager('name', ':', false, true, true); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + var key1 = {'name': 'authUser', 'persistent': true}; + var key2 = {'name': 'authEvent', 'persistent': true}; + return goog.Promise.resolve() + .then(function() { + return mockLocalStorage.set('name:authUser:appId1', {'foo': 'bar'}); + }) + .then(function() { + return mockLocalStorage.set('name:authEvent:appId1', {'foo': 'bar'}); + }) + .then(function() { + // Add listeners for 2 events. + manager.addListener(key1, 'appId1', listener1); + manager.addListener(key2, 'appId1', listener2); + manager.addListener(key1, 'appId1', listener3); + var storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + // Trigger user event. + storageEvent.key = 'name:authUser:appId1'; + storageEvent.newValue = null; + mockLocalStorage.fireBrowserEvent(storageEvent); + // Listener 1 and 3 should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + // Trigger second event. + storageEvent.key = 'name:authEvent:appId1'; + storageEvent.newValue = null; + mockLocalStorage.fireBrowserEvent(storageEvent); + // Only second listener should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + // Some unknown event. + storageEvent.key = 'key3'; + storageEvent.newValue = null; + mockLocalStorage.fireBrowserEvent(storageEvent); + // No listeners should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Remove all listeners. + manager.removeListener(key1, 'appId1', listener1); + manager.removeListener(key2, 'appId1', listener2); + manager.removeListener(key1, 'appId1', listener3); + // Trigger first event. + storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + storageEvent.key = 'name:authUser:appId1'; + storageEvent.newValue = JSON.stringify({'foo': 'bar'}); + mockLocalStorage.fireBrowserEvent(storageEvent); + // No change. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + }); +} + + function testAddRemoveListeners_localStorage() { + stubs.reset(); + // localStorage is used when service workers are not supported. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() { + return false; + }); var manager = new fireauth.authStorage.Manager('name', ':', false, true, true); var listener1 = goog.testing.recordFunction(); @@ -471,6 +585,12 @@ function testAddRemoveListeners_localStorage() { function testAddRemoveListeners_localStorage_nullKey() { + stubs.reset(); + // Simulate localStorage is used for persistent storage. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); var manager = new fireauth.authStorage.Manager('name', ':', false, true, true); var listener1 = goog.testing.recordFunction(); @@ -515,6 +635,14 @@ function testAddRemoveListeners_localStorage_nullKey() { function testAddRemoveListeners_localStorage_ie10() { + stubs.reset(); + // Simulate localStorage is used for persistent storage. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() { + return false; + }); // Simulate IE 10 with localStorage events not synchronized. // event.newValue will not be immediately equal to // localStorage.getItem(event.key). @@ -609,6 +737,7 @@ function testAddRemoveListeners_localStorage_ie10() { function testAddRemoveListeners_indexeddb() { + stubs.reset(); // Mock indexedDB local storage manager. var mockIndexeddb = { handlers: [], @@ -626,10 +755,10 @@ function testAddRemoveListeners_indexeddb() { } } }; - // Simulate browser that does not synchronize between and iframe and a popup. + // Simulate indexedDB is used for persistent storage. stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', + fireauth.util, + 'persistsStorageWithIndexedDB', function() { return true; }); @@ -687,6 +816,7 @@ function testAddRemoveListeners_indexeddb_cannotRunInBackground() { // between iframe and popup, polling web storage function should not be used. // indexedDB should be used. // Mock indexedDB local storage manager. + stubs.reset(); var mockIndexeddb = { handlers: [], addStorageListener: function(indexeddbHandler) { @@ -703,10 +833,10 @@ function testAddRemoveListeners_indexeddb_cannotRunInBackground() { } } }; - // Simulate browser that does not synchronize between and iframe and a popup. + // Simulate indexedDB is used for persistent storage. stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', + fireauth.util, + 'persistsStorageWithIndexedDB', function() { return true; }); @@ -761,6 +891,12 @@ function testAddRemoveListeners_indexeddb_cannotRunInBackground() { function testSafariLocalStorageSync_newEvent() { + stubs.reset(); + // Simulate persistent state implemented through localStorage for Safari. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); var manager = new fireauth.authStorage.Manager('firebase', ':', true, true, true); // Simulate Safari bug. @@ -798,6 +934,12 @@ function testSafariLocalStorageSync_newEvent() { function testSafariLocalStorageSync_cannotRunInBackground() { + stubs.reset(); + // Simulate persistent state implemented through localStorage for Safari. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); // This simulates iframe embedded in a cross origin domain. // Realistically only storage event should trigger here. // Test when new data is added to storage. @@ -838,6 +980,12 @@ function testSafariLocalStorageSync_cannotRunInBackground() { function testSafariLocalStorageSync_deletedEvent() { + stubs.reset(); + // Simulate persistent state implemented through localStorage for Safari. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); // This simulates iframe embedded in a cross origin domain. // Realistically only storage event should trigger here. // Test when old data is deleted from storage. @@ -881,6 +1029,12 @@ function testRunsInBackground_storageEventMode() { // foreground. // Test when storage event is first detected. Polling should be disabled to // prevent duplicate storage detection. + stubs.reset(); + // Simulate localStorage is used for persistent storage. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); var key = {name: 'authEvent', persistent: 'local'}; var storageKey = 'firebase:authEvent:appId1'; var manager = new fireauth.authStorage.Manager( @@ -972,6 +1126,12 @@ function testRunsInBackground_webStorageNotSupported() { function testRunsInBackground_pollingMode() { + stubs.reset(); + // Simulate localStorage is used for persistent storage. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); // Test when browser does not run in the background while another tab is in // foreground. // Test when storage polling first detects a storage notification. @@ -1025,6 +1185,12 @@ function testRunsInBackground_pollingMode() { function testRunsInBackground_currentTabChangesIgnored() { + stubs.reset(); + // Simulate localStorage is used for persistent storage. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() {return false;}); // Test when browser does not run in the background while another tab is in // foreground. // This tests that only other tab changes are detected and current tab changes diff --git a/packages/auth/test/cordovahandler_test.js b/packages/auth/test/cordovahandler_test.js index a2b17f0c79c..0dd935c6ec1 100644 --- a/packages/auth/test/cordovahandler_test.js +++ b/packages/auth/test/cordovahandler_test.js @@ -27,9 +27,11 @@ goog.require('fireauth.EmailAuthProvider'); goog.require('fireauth.GoogleAuthProvider'); goog.require('fireauth.UniversalLinkSubscriber'); goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); goog.require('fireauth.constants'); goog.require('fireauth.iframeclient.IfcHandler'); goog.require('fireauth.storage.AuthEventManager'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.OAuthHandlerManager'); goog.require('fireauth.util'); goog.require('goog.Promise'); @@ -66,6 +68,8 @@ var iOS8iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) A' + var iOS9iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) A' + 'ppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13C75 Safar' + 'i/601.1'; +var mockLocalStorage; +var mockSessionStorage; /** @@ -140,9 +144,12 @@ function initializePlugins( function setUp() { - // This would never run in IE environment anyway. - // Simulate localStorage synchronized. - simulateLocalStorageSynchronized(); + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); // Initialize plugins. initializePlugins( function(eventName, cb) { @@ -188,15 +195,6 @@ function tearDown() { } -/** Simulates that local storage synchronizes across tabs. */ -function simulateLocalStorageSynchronized() { - stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', - function() {return false;}); -} - - /** * Install the test to run and runs it. * @param {string} id The test identifier. diff --git a/packages/auth/test/storage/factory_test.js b/packages/auth/test/storage/factory_test.js index 2a441a12232..8102421d9c7 100644 --- a/packages/auth/test/storage/factory_test.js +++ b/packages/auth/test/storage/factory_test.js @@ -36,10 +36,10 @@ var stubs = new goog.testing.PropertyReplacer(); function setUp() { - // Simulate browser that synchronizes between and iframe and a popup. + // Simulate storage not persisted with indexedDB. stubs.replace( fireauth.util, - 'isLocalStorageNotSynchronized', + 'persistsStorageWithIndexedDB', function() { return false; }); @@ -59,7 +59,13 @@ function testGetStorage_browser_temporary() { } -function testGetStorage_browser_persistent() { +function testGetStorage_browser_persistent_localStorage() { + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() { + return false; + }); var factory = new fireauth.storage.Factory( fireauth.storage.Factory.EnvConfig.BROWSER); assertTrue(factory.makePersistentStorage() instanceof @@ -67,14 +73,14 @@ function testGetStorage_browser_persistent() { } -function testGetStorage_browser_persistent_isLocalStorageNotSynchronized() { +function testGetStorage_browser_persistent_indexedDB() { // Simulate browser to force usage of indexedDB storage. var mock = { type: 'indexedDB' }; stubs.replace( fireauth.util, - 'isLocalStorageNotSynchronized', + 'persistsStorageWithIndexedDB', function() { return true; }); @@ -99,6 +105,12 @@ function testGetStorage_node_temporary() { function testGetStorage_node_persistent() { + stubs.replace( + fireauth.storage.IndexedDB, + 'isAvailable', + function() { + return false; + }); var factory = new fireauth.storage.Factory( fireauth.storage.Factory.EnvConfig.NODE); assertTrue(factory.makePersistentStorage() instanceof @@ -115,6 +127,12 @@ function testGetStorage_reactnative_temporary() { function testGetStorage_reactnative_persistent() { + stubs.replace( + fireauth.storage.IndexedDB, + 'isAvailable', + function() { + return false; + }); var factory = new fireauth.storage.Factory( fireauth.storage.Factory.EnvConfig.REACT_NATIVE); assertTrue(factory.makePersistentStorage() instanceof diff --git a/packages/auth/test/storage/mockstorage_test.js b/packages/auth/test/storage/mockstorage_test.js new file mode 100644 index 00000000000..852f27290dc --- /dev/null +++ b/packages/auth/test/storage/mockstorage_test.js @@ -0,0 +1,126 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.MockStorageTest'); + +goog.require('fireauth.storage.MockStorage'); +/** @suppress {extraRequire} */ +goog.require('fireauth.storage.testHelper'); +goog.require('goog.events.EventType'); +goog.require('goog.testing.events'); +goog.require('goog.testing.events.Event'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.storage.MockStorageTest'); + + +var storage; + + +function setUp() { + storage = new fireauth.storage.MockStorage(); +} + + +function tearDown() { + storage = null; +} + + +function testBasicStorageOperations() { + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes() { + return assertDifferentTypes(storage); +} + + +function testListeners() { + var storageEvent; + + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + + storage.addStorageListener(listener1); + storage.addStorageListener(listener3); + + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'myKey1'; + storageEvent.newValue = JSON.stringify('value1'); + storage.fireBrowserEvent(storageEvent); + + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + return storage.get('myKey1').then(function(value) { + assertEquals('value1', value); + + storage.removeStorageListener(listener3); + + storageEvent.key = 'myKey2'; + storageEvent.newValue = JSON.stringify('value2'); + storage.fireBrowserEvent(storageEvent); + + assertEquals(2, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + return storage.get('myKey2'); + }).then(function(value) { + assertEquals('value2', value); + storageEvent.key = 'myKey1'; + storageEvent.newValue = null; + storage.fireBrowserEvent(storageEvent); + + assertEquals(3, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + + return storage.get('myKey1'); + }).then(function(value) { + assertUndefined(value); + + storage.removeStorageListener(listener1); + + storageEvent.key = null; + storage.fireBrowserEvent(storageEvent); + + assertEquals(3, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + + return storage.get('myKey2'); + }).then(function(value) { + assertUndefined(value); + }); +} + + +function testClear() { + storage.set('myKey1', 'value1'); + storage.set('myKey2', 'value2'); + storage.clear(); + return storage.get('myKey1').then(function(value) { + assertUndefined(value); + return storage.get('myKey2'); + }).then(function(value) { + assertUndefined(value); + }); +} diff --git a/packages/auth/test/storageautheventmanager_test.js b/packages/auth/test/storageautheventmanager_test.js index 323b9b1b7c9..39bdab127d7 100644 --- a/packages/auth/test/storageautheventmanager_test.js +++ b/packages/auth/test/storageautheventmanager_test.js @@ -22,7 +22,9 @@ goog.provide('fireauth.storage.AuthEventManagerTest'); goog.require('fireauth.AuthEvent'); goog.require('fireauth.authStorage'); +goog.require('fireauth.common.testHelper'); goog.require('fireauth.storage.AuthEventManager'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.events'); @@ -38,9 +40,17 @@ goog.setTestOnly('fireauth.storage.AuthEventManagerTest'); var appId = 'appId1'; var stubs = new goog.testing.PropertyReplacer(); +var mockLocalStorage; +var mockSessionStorage; function setUp() { + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); // Simulate browser that synchronizes between and iframe and a popup. stubs.replace( fireauth.util, @@ -53,6 +63,11 @@ function setUp() { } +function tearDown() { + stubs.reset(); +} + + /** * @return {!fireauth.authStorage.Manager} The default local storage * synchronized manager instance used for testing. @@ -71,12 +86,13 @@ function testGetSetRemoveAuthEvent() { '1234', 'http://www.example.com/#oauthResponse', 'SESSION_ID'); - // Set expected Auth event in localStorage. - window.localStorage.setItem( - 'firebase:authEvent:appId1', - JSON.stringify(expectedAuthEvent.toPlainObject())); var storageKey = 'firebase:authEvent:appId1'; return goog.Promise.resolve() + .then(function() { + // Set expected Auth event in localStorage. + return mockLocalStorage.set( + storageKey, expectedAuthEvent.toPlainObject()); + }) .then(function() { return authEventManager.getAuthEvent(); }) @@ -87,7 +103,10 @@ function testGetSetRemoveAuthEvent() { return authEventManager.removeAuthEvent(); }) .then(function() { - assertNull(window.localStorage.getItem(storageKey)); + return mockLocalStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); return authEventManager.getAuthEvent(); }) .then(function(authEvent) { @@ -106,9 +125,9 @@ function testGetSetRemoveRedirectEvent() { 'http://www.example.com/#oauthResponse', 'SESSION_ID'); // Set expected Auth event in sessionStorage. - window.sessionStorage.setItem( + mockSessionStorage.set( 'firebase:redirectEvent:appId1', - JSON.stringify(expectedAuthEvent.toPlainObject())); + expectedAuthEvent.toPlainObject()); var storageKey = 'firebase:redirectEvent:appId1'; return goog.Promise.resolve() .then(function() { @@ -121,7 +140,10 @@ function testGetSetRemoveRedirectEvent() { return authEventManager.removeRedirectEvent(); }) .then(function() { - assertNull(window.sessionStorage.getItem(storageKey)); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); return authEventManager.getRedirectEvent(); }) .then(function(authEvent) { @@ -141,38 +163,40 @@ function testAddRemoveAuthEventListener() { new fireauth.storage.AuthEventManager('appId1', storageManager); var listener = goog.testing.recordFunction(); // Save existing Auth events for appId1 and appId2. - window.localStorage.setItem( - 'firebase:authEvent:appId1', - JSON.stringify(expectedAuthEvent.toPlainObject())); - window.localStorage.setItem( - 'firebase:authEvent:appId2', - JSON.stringify(expectedAuthEvent.toPlainObject())); - authEventManager.addAuthEventListener(listener); - // Simulate appId1 event deletion. - storageEvent = - new goog.testing.events.Event(goog.events.EventType.STORAGE, window); - storageEvent.key = 'firebase:authEvent:appId1'; - storageEvent.newValue = null; - window.localStorage.removeItem('firebase:authEvent:appId1'); - // This should trigger listener. - goog.testing.events.fireBrowserEvent(storageEvent); - assertEquals(1, listener.getCallCount()); - // Simulate appId2 event deletion. - storageEvent.key = 'firebase:authEvent:appId2'; - window.localStorage.removeItem('firebase:authEvent:appId2'); - // This should not trigger listener. - goog.testing.events.fireBrowserEvent(storageEvent); - assertEquals(1, listener.getCallCount()); - // Remove listener. - authEventManager.removeAuthEventListener(listener); - // Simulate new event saved for appId1. - // This should not trigger listener anymore. - storageEvent.key = 'firebase:authEvent:appId1'; - storageEvent.oldValue = null; - storageEvent.newValue = JSON.stringify(expectedAuthEvent.toPlainObject()); - window.localStorage.setItem( - 'firebase:authEvent:appId1', - JSON.stringify(expectedAuthEvent.toPlainObject())); - goog.testing.events.fireBrowserEvent(storageEvent); - assertEquals(1, listener.getCallCount()); + return goog.Promise.resolve() + .then(function() { + return mockLocalStorage.set( + 'firebase:authEvent:appId1', expectedAuthEvent.toPlainObject()); + }) + .then(function() { + // Set expected Auth event in localStorage. + return mockLocalStorage.set( + 'firebase:authEvent:appId2', expectedAuthEvent.toPlainObject()); + }) + .then(function() { + authEventManager.addAuthEventListener(listener); + // Simulate appId1 event deletion. + storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + storageEvent.newValue = null; + // This should trigger listener. + mockLocalStorage.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + // Simulate appId2 event deletion. + storageEvent.key = 'firebase:authEvent:appId2'; + // This should not trigger listener. + mockLocalStorage.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + // Remove listener. + authEventManager.removeAuthEventListener(listener); + // Simulate new event saved for appId1. + // This should not trigger listener anymore. + storageEvent.key = 'firebase:authEvent:appId1'; + storageEvent.oldValue = null; + storageEvent.newValue = + JSON.stringify(expectedAuthEvent.toPlainObject()); + mockLocalStorage.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + }); } diff --git a/packages/auth/test/storageoauthhandlermanager_test.js b/packages/auth/test/storageoauthhandlermanager_test.js index d8552c19077..1559d0bd2f0 100644 --- a/packages/auth/test/storageoauthhandlermanager_test.js +++ b/packages/auth/test/storageoauthhandlermanager_test.js @@ -23,6 +23,8 @@ goog.provide('fireauth.storage.OAuthHandlerManagerTest'); goog.require('fireauth.AuthEvent'); goog.require('fireauth.OAuthHelperState'); goog.require('fireauth.authStorage'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.OAuthHandlerManager'); goog.require('fireauth.util'); goog.require('goog.Promise'); @@ -34,9 +36,17 @@ goog.setTestOnly('fireauth.storage.OAuthHandlerManagerTest'); var appId = 'appId1'; var stubs = new goog.testing.PropertyReplacer(); +var mockLocalStorage; +var mockSessionStorage; function setUp() { + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); // Simulate browser that synchronizes between and iframe and a popup. stubs.replace( fireauth.util, @@ -49,6 +59,11 @@ function setUp() { } +function tearDown() { + stubs.reset(); +} + + /** * @return {!fireauth.authStorage.Manager} The default local storage * synchronized manager instance used for testing. @@ -72,16 +87,17 @@ function testGetSetRemoveSessionId() { return oauthHandlerManager.getSessionId(appId); }) .then(function(sessionId) { - assertEquals( - window.sessionStorage.getItem(storageKey), - JSON.stringify(expectedSessionId)); assertObjectEquals(expectedSessionId, sessionId); + return mockSessionStorage.get(storageKey); }) - .then(function() { + .then(function(value) { + assertObjectEquals(expectedSessionId, value); return oauthHandlerManager.removeSessionId(appId); }) .then(function() { - assertNull(window.sessionStorage.getItem(storageKey)); + return mockSessionStorage.get(storageKey); + }).then(function(value) { + assertUndefined(value); return oauthHandlerManager.getSessionId(appId); }) .then(function(sessionId) { @@ -104,10 +120,12 @@ function testSetAuthEvent() { return oauthHandlerManager.setAuthEvent(appId, expectedAuthEvent); }) .then(function() { - assertEquals( - JSON.stringify(expectedAuthEvent.toPlainObject()), - window.localStorage.getItem( - 'firebase:authEvent:appId1')); + return mockLocalStorage.get('firebase:authEvent:appId1'); + }) + .then(function(value) { + assertObjectEquals( + expectedAuthEvent.toPlainObject(), + value); }); } @@ -126,10 +144,11 @@ function testSetRedirectEvent() { return oauthHandlerManager.setRedirectEvent(appId, expectedAuthEvent); }) .then(function() { - assertEquals( - JSON.stringify(expectedAuthEvent.toPlainObject()), - window.sessionStorage.getItem( - 'firebase:redirectEvent:appId1')); + return mockSessionStorage.get('firebase:redirectEvent:appId1'); + }).then(function(value) { + assertObjectEquals( + expectedAuthEvent.toPlainObject(), + value); }); } @@ -152,16 +171,18 @@ function testGetSetRemoveOAuthHelperState() { return oauthHandlerManager.getOAuthHelperState(); }) .then(function(state) { - assertEquals( - window.sessionStorage.getItem(storageKey), - JSON.stringify(expectedState.toPlainObject())); assertObjectEquals(expectedState, state); + return mockSessionStorage.get(storageKey); }) - .then(function() { + .then(function(value) { + assertObjectEquals(expectedState.toPlainObject(), value); return oauthHandlerManager.removeOAuthHelperState(); }) .then(function() { - assertNull(window.sessionStorage.getItem(storageKey)); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); return oauthHandlerManager.getOAuthHelperState(); }) .then(function(state) { diff --git a/packages/auth/test/storagependingredirectmanager_test.js b/packages/auth/test/storagependingredirectmanager_test.js index 8ef41d4dbf5..d2ceb2134e1 100644 --- a/packages/auth/test/storagependingredirectmanager_test.js +++ b/packages/auth/test/storagependingredirectmanager_test.js @@ -21,8 +21,9 @@ goog.provide('fireauth.storage.PendingRedirectManagerTest'); goog.require('fireauth.authStorage'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.PendingRedirectManager'); -goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.jsunit'); @@ -32,18 +33,19 @@ goog.setTestOnly('fireauth.storage.PendingRedirectManagerTest'); var appId = 'appId1'; var stubs = new goog.testing.PropertyReplacer(); +var mockLocalStorage; +var mockSessionStorage; function setUp() { - // Simulate browser that synchronizes between and iframe and a popup. - stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', - function() { - return false; - }); + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); window.localStorage.clear(); window.sessionStorage.clear(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); } @@ -69,14 +71,16 @@ function testGetSetPendingStatus() { return pendingRedirectManager.getPendingStatus(); }) .then(function(status) { - assertEquals( - window.sessionStorage.getItem(storageKey), - JSON.stringify('pending')); assertTrue(status); + return mockSessionStorage.get(storageKey); + }).then(function(value) { + assertEquals('pending', value); return pendingRedirectManager.removePendingStatus(); }) .then(function() { - assertNull(window.sessionStorage.getItem(storageKey)); + return mockSessionStorage.get(storageKey); + }).then(function(value) { + assertUndefined(value); return pendingRedirectManager.getPendingStatus(); }) .then(function(status) { diff --git a/packages/auth/test/storageredirectusermanager_test.js b/packages/auth/test/storageredirectusermanager_test.js index 40c0242cd95..5350a829e4d 100644 --- a/packages/auth/test/storageredirectusermanager_test.js +++ b/packages/auth/test/storageredirectusermanager_test.js @@ -22,8 +22,9 @@ goog.provide('fireauth.storage.RedirectUserManagerTest'); goog.require('fireauth.AuthUser'); goog.require('fireauth.authStorage'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.RedirectUserManager'); -goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.PropertyReplacer'); @@ -40,19 +41,20 @@ var clock; var expectedUser; var expectedUserWithAuthDomain; var stubs = new goog.testing.PropertyReplacer(); +var mockLocalStorage; +var mockSessionStorage; function setUp() { - // Simulate browser that synchronizes between and iframe and a popup. - stubs.replace( - fireauth.util, - 'isLocalStorageNotSynchronized', - function() { - return false; - }); + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); clock = new goog.testing.MockClock(true); window.localStorage.clear(); window.sessionStorage.clear(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); } @@ -116,10 +118,11 @@ function testGetSetRemoveRedirectUser() { return redirectUserManager.getRedirectUser(); }) .then(function(user) { - assertEquals( - window.sessionStorage.getItem(storageKey), - JSON.stringify(expectedUser.toPlainObject())); assertObjectEquals(expectedUser, user); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + assertObjectEquals(expectedUser.toPlainObject(), value); // Get user with authDomain. return redirectUserManager.getRedirectUser('project.firebaseapp.com'); }) @@ -128,7 +131,10 @@ function testGetSetRemoveRedirectUser() { return redirectUserManager.removeRedirectUser(); }) .then(function() { - assertNull(window.sessionStorage.getItem(storageKey)); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + assertUndefined(value); return redirectUserManager.getRedirectUser(); }) .then(function(user) { diff --git a/packages/auth/test/storageusermanager_test.js b/packages/auth/test/storageusermanager_test.js index 9d9998bd692..492517ebfcb 100644 --- a/packages/auth/test/storageusermanager_test.js +++ b/packages/auth/test/storageusermanager_test.js @@ -23,6 +23,7 @@ goog.provide('fireauth.storage.UserManagerTest'); goog.require('fireauth.AuthUser'); goog.require('fireauth.authStorage'); goog.require('fireauth.common.testHelper'); +goog.require('fireauth.storage.MockStorage'); goog.require('fireauth.storage.UserManager'); goog.require('fireauth.util'); goog.require('goog.Promise'); @@ -48,9 +49,17 @@ var expectedUserWithAuthDomain; var stubs = new goog.testing.PropertyReplacer(); var testUser; var testUser2; +var mockLocalStorage; +var mockSessionStorage; function setUp() { + // Create new mock storages for persistent and temporary storage before each + // test. + mockLocalStorage = new fireauth.storage.MockStorage(); + mockSessionStorage = new fireauth.storage.MockStorage(); + fireauth.common.testHelper.installMockStorages( + stubs, mockLocalStorage, mockSessionStorage); // Simulate browser that synchronizes between and iframe and a popup. stubs.replace( fireauth.util, @@ -154,10 +163,11 @@ function testGetSetRemoveCurrentUser() { return userManager.getCurrentUser(); }) .then(function(user) { - assertEquals( - window.localStorage.getItem(storageKey), - JSON.stringify(expectedUser.toPlainObject())); - assertObjectEquals(expectedUser, user); + assertObjectEquals(expectedUser.toPlainObject(), user.toPlainObject()); + return mockLocalStorage.get(storageKey); + }) + .then(function(user) { + assertObjectEquals(expectedUser.toPlainObject(), user); // Get user with authDomain. return userManager.getCurrentUser('project.firebaseapp.com'); }) @@ -166,7 +176,10 @@ function testGetSetRemoveCurrentUser() { return userManager.removeCurrentUser(); }) .then(function() { - assertNull(window.localStorage.getItem(storageKey)); + return mockLocalStorage.get(storageKey); + }) + .then(function(user) { + assertUndefined(user); return userManager.getCurrentUser(); }) .then(function(user) { @@ -186,51 +199,51 @@ function testAddRemoveCurrentUserChangeListener() { } }; // Save existing Auth users for appId1 and appId2. - window.localStorage.setItem( - 'firebase:authUser:appId1', - JSON.stringify(testUser.toPlainObject())); - window.localStorage.setItem( - 'firebase:authUser:appId2', - JSON.stringify(testUser.toPlainObject())); - userManager.addCurrentUserChangeListener(listener); - // Simulate appId1 user deletion. - var storageEvent = - new goog.testing.events.Event(goog.events.EventType.STORAGE, window); - storageEvent.key = 'firebase:authUser:appId1'; - storageEvent.oldValue = JSON.stringify(testUser.toPlainObject()); - storageEvent.newValue = null; - window.localStorage.removeItem('firebase:authUser:appId1'); - // This should trigger listener. - goog.testing.events.fireBrowserEvent(storageEvent); - assertEquals(1, calls); - // Simulate appId2 user deletion. - storageEvent.key = 'firebase:authUser:appId2'; - storageEvent.oldValue = JSON.stringify(testUser.toPlainObject()); - storageEvent.newValue = null; - window.localStorage.removeItem('firebase:authUser:appId2'); - // This should not trigger listener. - goog.testing.events.fireBrowserEvent(storageEvent); - assertEquals(1, calls); - // Remove listener. - userManager.removeCurrentUserChangeListener(listener); - // Simulate new user saved for appId1. - // This should not trigger listener. - storageEvent.key = 'firebase:authUser:appId1'; - storageEvent.newValue = JSON.stringify(testUser.toPlainObject()); - storageEvent.oldValue = null; - window.localStorage.setItem( - 'firebase:authUser:appId1', - JSON.stringify(testUser.toPlainObject())); - goog.testing.events.fireBrowserEvent(storageEvent); - assertEquals(1, calls); + mockLocalStorage.set('firebase:authUser:appId1', testUser.toPlainObject()); + mockLocalStorage.set('firebase:authUser:appId2', testUser.toPlainObject()); + return goog.Promise.resolve().then(function() { + return mockLocalStorage.set( + 'firebase:authUser:appId1', testUser.toPlainObject()); + }) + .then(function() { + return mockLocalStorage.set( + 'firebase:authUser:appId2', testUser.toPlainObject()); + }) + .then(function() { + userManager.addCurrentUserChangeListener(listener); + // Simulate appId1 user deletion. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authUser:appId1'; + storageEvent.oldValue = JSON.stringify(testUser.toPlainObject()); + storageEvent.newValue = null; + // This should trigger listener. + mockLocalStorage.fireBrowserEvent(storageEvent); + assertEquals(1, calls); + // Simulate appId2 user deletion. + storageEvent.key = 'firebase:authUser:appId2'; + storageEvent.oldValue = JSON.stringify(testUser.toPlainObject()); + storageEvent.newValue = null; + // This should not trigger listener. + mockLocalStorage.fireBrowserEvent(storageEvent); + assertEquals(1, calls); + // Remove listener. + userManager.removeCurrentUserChangeListener(listener); + // Simulate new user saved for appId1. + // This should not trigger listener. + storageEvent.key = 'firebase:authUser:appId1'; + storageEvent.newValue = JSON.stringify(testUser.toPlainObject()); + storageEvent.oldValue = null; + mockLocalStorage.fireBrowserEvent(storageEvent); + assertEquals(1, calls); + }); } function testUserManager_initializedWithSession() { // Save state in session storage. var storageKey = 'firebase:authUser:appId1'; - window.sessionStorage.setItem( - storageKey, JSON.stringify(testUser.toPlainObject())); + mockSessionStorage.set(storageKey, testUser.toPlainObject()); var storageManager = getDefaultStorageManagerInstance(); var userManager = new fireauth.storage.UserManager('appId1', storageManager); return userManager.getCurrentUser() @@ -261,14 +274,12 @@ function testUserManager_initializedWithSession_duplicateStorage() { var userManager; // Save state in session storage. var storageKey = 'firebase:authUser:appId1'; - window.sessionStorage.setItem( - storageKey, JSON.stringify(testUser.toPlainObject())); + mockSessionStorage.set(storageKey, testUser.toPlainObject()); // Add state to other types of storage. - window.localStorage.setItem( - storageKey, JSON.stringify(testUser2.toPlainObject())); + mockLocalStorage.set(storageKey, testUser2.toPlainObject()); // Set redirect persistence to none. - window.sessionStorage.setItem( - 'firebase:persistence:appId1', JSON.stringify('none')); + mockSessionStorage.set( + 'firebase:persistence:appId1', 'none'); // Save state using in memory storage. return storageManager.set( {name: 'authUser', persistent: 'none'}, @@ -336,8 +347,7 @@ function testUserManager_initializedWithInMemory() { function testUserManager_initializedWithLocal() { // Save state in local storage. var storageKey = 'firebase:authUser:appId1'; - window.localStorage.setItem( - storageKey, JSON.stringify(testUser.toPlainObject())); + mockLocalStorage.set(storageKey, testUser.toPlainObject()); var storageManager = getDefaultStorageManagerInstance(); var userManager = new fireauth.storage.UserManager('appId1', storageManager); return userManager.getCurrentUser() @@ -384,8 +394,7 @@ function testUserManager_initializedWithDefault() { function testUserManager_initializedWithSavedPersistence() { // Save redirect persistence. - window.sessionStorage.setItem( - 'firebase:persistence:appId1', JSON.stringify('session')); + mockSessionStorage.set('firebase:persistence:appId1', 'session'); var storageManager = getDefaultStorageManagerInstance(); var userManager = new fireauth.storage.UserManager('appId1', storageManager); return userManager.getCurrentUser() @@ -414,10 +423,11 @@ function testUserManager_savePersistenceForRedirect_default() { return userManager.savePersistenceForRedirect() .then(function() { // Should store persistence value in session storage. - assertEquals( - window.sessionStorage.getItem(storageKey), - // Should apply the current default persistence. - JSON.stringify('local')); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + // Should apply the current default persistence. + assertEquals('local', value); }); } @@ -431,10 +441,11 @@ function testUserManager_savePersistenceForRedirect_modifed() { return userManager.savePersistenceForRedirect() .then(function() { // Should store persistence value in session storage. - assertEquals( - window.sessionStorage.getItem(storageKey), - // The latest modified persistence value should be used. - JSON.stringify('session')); + return mockSessionStorage.get(storageKey); + }) + .then(function(value) { + // The latest modified persistence value should be used. + assertEquals('session', value); }); } @@ -498,8 +509,7 @@ function testUserManager_existingState_setPersistence() { // Test setPersistence behavior with some initial saved persistence state. var storageKey = 'firebase:authUser:appId1'; // Save initial data in local storage. - window.localStorage.setItem( - storageKey, JSON.stringify(testUser2.toPlainObject())); + mockLocalStorage.set(storageKey, testUser2.toPlainObject()); var storageManager = getDefaultStorageManagerInstance(); // As no existing state, the default is local. var userManager = new fireauth.storage.UserManager('appId1', storageManager); @@ -514,8 +524,7 @@ function testUserManager_existingState_setPersistence() { }) .then(function() { // Simulate some state duplication due to some unexpected error. - window.localStorage.setItem( - storageKey, JSON.stringify(testUser.toPlainObject())); + mockLocalStorage.set(storageKey, testUser.toPlainObject()); // Should switch state from session to none and clear everything else. userManager.setPersistence('none'); return userManager.getCurrentUser(); @@ -546,12 +555,10 @@ function testUserManager_switchToLocalOnExternalEvents_noExistingUser() { return userManager.setPersistence('session') .then(function() { // Simulate user signed in in another tab. - window.localStorage.setItem( - storageKey, JSON.stringify(testUser2.toPlainObject())); storageEvent.newValue = JSON.stringify(testUser2.toPlainObject()); // This should trigger listener and switch storage from session to // local. - goog.testing.events.fireBrowserEvent(storageEvent); + mockLocalStorage.fireBrowserEvent(storageEvent); // Listener should be called. assertEquals(1, listener.getCallCount()); return userManager.getCurrentUser(); @@ -565,7 +572,7 @@ function testUserManager_switchToLocalOnExternalEvents_noExistingUser() { .then(function() { userManager.removeCurrentUserChangeListener(listener); // This should not trigger listener. - goog.testing.events.fireBrowserEvent(storageEvent); + mockLocalStorage.fireBrowserEvent(storageEvent); assertEquals(1, listener.getCallCount()); }); } @@ -582,8 +589,7 @@ function testUserManager_switchToLocalOnExternalEvents_existingUser() { storageEvent.key = storageKey; storageEvent.newValue = null; // Existing user in session storage. - window.sessionStorage.setItem( - storageKey, JSON.stringify(testUser.toPlainObject())); + mockSessionStorage.set(storageKey, testUser.toPlainObject()); var storageManager = getDefaultStorageManagerInstance(); // Due to existing state in session storage, the initial state is session. @@ -599,11 +605,9 @@ function testUserManager_switchToLocalOnExternalEvents_existingUser() { }) .then(function(user) { // Simulate user signed in in another tab. - window.localStorage.setItem( - storageKey, JSON.stringify(testUser2.toPlainObject())); storageEvent.newValue = JSON.stringify(testUser2.toPlainObject()); // This should trigger listener and switch storage to local. - goog.testing.events.fireBrowserEvent(storageEvent); + mockLocalStorage.fireBrowserEvent(storageEvent); assertEquals(1, listener.getCallCount()); return userManager.getCurrentUser(); }) @@ -616,7 +620,7 @@ function testUserManager_switchToLocalOnExternalEvents_existingUser() { .then(function() { userManager.removeCurrentUserChangeListener(listener); // This should not trigger listener. - goog.testing.events.fireBrowserEvent(storageEvent); + mockLocalStorage.fireBrowserEvent(storageEvent); assertEquals(1, listener.getCallCount()); }); } diff --git a/packages/auth/test/testhelper.js b/packages/auth/test/testhelper.js index 163f005a916..b8de22915b3 100644 --- a/packages/auth/test/testhelper.js +++ b/packages/auth/test/testhelper.js @@ -21,6 +21,9 @@ goog.provide('fireauth.common.testHelper'); +goog.require('fireauth.storage.Factory'); +goog.require('goog.Promise'); + goog.setTestOnly('fireauth.common.testHelper'); @@ -121,21 +124,19 @@ fireauth.common.testHelper.assertDeprecatedUserCredentialResponse = function( * check for existence. If null is passed, the check will ensure no user is * saved in storage. * @param {?fireauth.AuthUser} expectedUser The expected Auth user to test for. - * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * @param {?fireauth.authStorage.Manager} manager The underlying storage * manager to use. If none is provided, the default global instance is used. * @return {!goog.Promise} A promise that resolves when the check completes. */ fireauth.common.testHelper.assertUserStorage = - function(appId, persistence, expectedUser, opt_manager) { - // Get storage manager. - var storage = opt_manager || fireauth.authStorage.Manager.getInstance(); + function(appId, persistence, expectedUser, manager) { var promises = []; // All supported persistence types. var types = ['local', 'session', 'none']; // For each persistence type. for (var i = 0; i < types.length; i++) { // Get the current user if stored in current persistence. - var p = storage.get({name: 'authUser', persistent: types[i]}, appId); + var p = manager.get({name: 'authUser', persistent: types[i]}, appId); if (persistence === types[i]) { // If matching specified persistence, ensure value matches the specified // user. @@ -154,3 +155,28 @@ fireauth.common.testHelper.assertUserStorage = // Wait for all checks to complete before resolving. return goog.Promise.all(promises); }; + + +/** + * Installs different persistent/temporary storage using the provided mocks. + * @param {!goog.testing.PropertyReplacer} stub The property replacer. + * @param {!fireauth.storage.Storage} mockLocalStorage The mock storage + * instance for persistent storage. + * @param {!fireauth.storage.Storage} mockSessionStorage The mock storage + * instance for temporary storage. + */ +fireauth.common.testHelper.installMockStorages = + function(stub, mockLocalStorage, mockSessionStorage) { + stub.replace( + fireauth.storage.Factory.prototype, + 'makePersistentStorage', + function() { + return mockLocalStorage; + }); + stub.replace( + fireauth.storage.Factory.prototype, + 'makeTemporaryStorage', + function() { + return mockSessionStorage; + }); +}; diff --git a/packages/auth/test/utils_test.js b/packages/auth/test/utils_test.js index 0d92f0f6e7d..cd2a140297f 100644 --- a/packages/auth/test/utils_test.js +++ b/packages/auth/test/utils_test.js @@ -1610,3 +1610,36 @@ function testUtcTimestampToDateString() { new Date(1506046529000).toUTCString(), fireauth.util.utcTimestampToDateString(1506046529000)); } + + +function testPersistsStorageWithIndexedDB() { + // localStorage not synchronized and indexedDB available. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() {return true;}); + stubs.replace( + fireauth.util, + 'isIndexedDBAvailable', + function() {return true;}); + assertTrue(fireauth.util.persistsStorageWithIndexedDB()); + + // localStorage synchronized and indexedDB available. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() {return false;}); + stubs.replace( + fireauth.util, + 'isIndexedDBAvailable', + function() {return true;}); + assertFalse(fireauth.util.persistsStorageWithIndexedDB()); + + // indexedDB not available. + stubs.reset(); + stubs.replace( + fireauth.util, + 'isIndexedDBAvailable', + function() {return false;}); + assertFalse(fireauth.util.persistsStorageWithIndexedDB()); +} From b1eeee7a1813fd3439f8f067013a0a310c56654d Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Thu, 22 Feb 2018 10:34:44 -0800 Subject: [PATCH 2/6] [AUTOMATED]: Prettier Code Styling --- packages/database/test/order_by.test.ts | 4 +- packages/database/test/query.test.ts | 28 ++++++++--- packages/database/test/transaction.test.ts | 48 ++++++++++++++----- .../src/platform_node/grpc_connection.ts | 6 +-- .../firestore/src/remote/persistent_stream.ts | 4 +- .../firestore/src/util/input_validation.ts | 8 ++-- .../test/integration/api/validation.test.ts | 4 +- 7 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/database/test/order_by.test.ts b/packages/database/test/order_by.test.ts index b821de62138..70495f2fd60 100644 --- a/packages/database/test/order_by.test.ts +++ b/packages/database/test/order_by.test.ts @@ -363,7 +363,9 @@ describe('.orderBy tests', function() { expect(addedPrevNames).to.deep.equal(expectedPrevNames); }); - it('Removing default listener removes non-default listener that loads all data', function(done) { + it('Removing default listener removes non-default listener that loads all data', function( + done + ) { const ref = getRandomNode() as Reference; const initial = { key: 'value' }; diff --git a/packages/database/test/query.test.ts b/packages/database/test/query.test.ts index bda21cdbb0b..499748d3edd 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database/test/query.test.ts @@ -1971,7 +1971,9 @@ describe('Query Tests', function() { expect(val).to.equal(2); }); - it('.startAt() with two arguments works properly (case 1169).', function(done) { + it('.startAt() with two arguments works properly (case 1169).', function( + done + ) { const ref = getRandomNode() as Reference; const data = { Walker: { @@ -2108,7 +2110,9 @@ describe('Query Tests', function() { }); }); - it(".endAt(null, 'f').limitToLast(5) returns the right set of children.", function(done) { + it(".endAt(null, 'f').limitToLast(5) returns the right set of children.", function( + done + ) { const ref = getRandomNode() as Reference; ref.set( { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g', h: 'h' }, @@ -2130,7 +2134,9 @@ describe('Query Tests', function() { ); }); - it('complex update() at query root raises correct value event', function(done) { + it('complex update() at query root raises correct value event', function( + done + ) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2235,7 +2241,9 @@ describe('Query Tests', function() { }); }); - it('listen for child_added events with limit and different types fires properly', function(done) { + it('listen for child_added events with limit and different types fires properly', function( + done + ) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2277,7 +2285,9 @@ describe('Query Tests', function() { }); }); - it('listen for child_changed events with limit and different types fires properly', function(done) { + it('listen for child_changed events with limit and different types fires properly', function( + done + ) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2328,7 +2338,9 @@ describe('Query Tests', function() { }); }); - it('listen for child_remove events with limit and different types fires properly', function(done) { + it('listen for child_remove events with limit and different types fires properly', function( + done + ) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2430,7 +2442,9 @@ describe('Query Tests', function() { ); }); - it('listen for child_remove events when parent set to scalar', function(done) { + it('listen for child_remove events when parent set to scalar', function( + done + ) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; diff --git a/packages/database/test/transaction.test.ts b/packages/database/test/transaction.test.ts index d0f44f5accc..e603105ebc0 100644 --- a/packages/database/test/transaction.test.ts +++ b/packages/database/test/transaction.test.ts @@ -86,7 +86,9 @@ describe('Transaction Tests', function() { }); }); - it('Non-aborted transaction sets committed to true in callback.', function(done) { + it('Non-aborted transaction sets committed to true in callback.', function( + done + ) { const node = getRandomNode() as Reference; node.transaction( @@ -102,7 +104,9 @@ describe('Transaction Tests', function() { ); }); - it('Aborted transaction sets committed to false in callback.', function(done) { + it('Aborted transaction sets committed to false in callback.', function( + done + ) { const node = getRandomNode() as Reference; node.transaction( @@ -232,7 +236,9 @@ describe('Transaction Tests', function() { return ea.promise; }); - it('Second transaction gets run immediately on previous output and only runs once.', function(done) { + it('Second transaction gets run immediately on previous output and only runs once.', function( + done + ) { const nodePair = getRandomNode(2) as Reference[]; let firstRun = false, firstDone = false, @@ -506,7 +512,9 @@ describe('Transaction Tests', function() { ); }); - it('Set should cancel already sent transactions that come back as datastale.', function(done) { + it('Set should cancel already sent transactions that come back as datastale.', function( + done + ) { const nodePair = getRandomNode(2) as Reference[]; let transactionCalls = 0; nodePair[0].set(5, function() { @@ -680,7 +688,9 @@ describe('Transaction Tests', function() { return Promise.all([tx1, tx2]); }); - it('Doing set() in successful transaction callback works. Case 870.', function(done) { + it('Doing set() in successful transaction callback works. Case 870.', function( + done + ) { const node = getRandomNode() as Reference; let transactionCalled = false; let callbackCalled = false; @@ -700,7 +710,9 @@ describe('Transaction Tests', function() { ); }); - it('Doing set() in aborted transaction callback works. Case 870.', function(done) { + it('Doing set() in aborted transaction callback works. Case 870.', function( + done + ) { const nodePair = getRandomNode(2) as Reference[], node1 = nodePair[0], node2 = nodePair[1]; @@ -1016,7 +1028,9 @@ describe('Transaction Tests', function() { ); }); - it('Transaction properly reverts data when you add a deeper listen.', function(done) { + it('Transaction properly reverts data when you add a deeper listen.', function( + done + ) { const refPair = getRandomNode(2) as Reference[], ref1 = refPair[0], ref2 = refPair[1]; @@ -1186,7 +1200,9 @@ describe('Transaction Tests', function() { }); }); - it("transaction() doesn't pick up cached data from previous once().", function(done) { + it("transaction() doesn't pick up cached data from previous once().", function( + done + ) { const refPair = getRandomNode(2) as Reference[]; const me = refPair[0], other = refPair[1]; @@ -1213,7 +1229,9 @@ describe('Transaction Tests', function() { }); }); - it("transaction() doesn't pick up cached data from previous transaction.", function(done) { + it("transaction() doesn't pick up cached data from previous transaction.", function( + done + ) { const refPair = getRandomNode(2) as Reference[]; const me = refPair[0], other = refPair[1]; @@ -1245,7 +1263,9 @@ describe('Transaction Tests', function() { ); }); - it('server values: local timestamp should eventually (but not immediately) match the server with txns', function(done) { + it('server values: local timestamp should eventually (but not immediately) match the server with txns', function( + done + ) { const refPair = getRandomNode(2) as Reference[], writer = refPair[0], reader = refPair[1], @@ -1337,7 +1357,9 @@ describe('Transaction Tests', function() { ); }); - it("transaction() on queried location doesn't run initially on null (firebase-worker-queue depends on this).", function(done) { + it("transaction() on queried location doesn't run initially on null (firebase-worker-queue depends on this).", function( + done + ) { const ref = getRandomNode() as Reference; ref.push({ a: 1, b: 2 }, function() { ref @@ -1415,7 +1437,9 @@ describe('Transaction Tests', function() { ); }); - it('transactions works with merges without the transaction path', function(done) { + it('transactions works with merges without the transaction path', function( + done + ) { const ref = getRandomNode() as Reference; ref.update({ foo: 'bar' }); diff --git a/packages/firestore/src/platform_node/grpc_connection.ts b/packages/firestore/src/platform_node/grpc_connection.ts index 639c8c3b831..f6780ead440 100644 --- a/packages/firestore/src/platform_node/grpc_connection.ts +++ b/packages/firestore/src/platform_node/grpc_connection.ts @@ -37,9 +37,9 @@ const LOG_TAG = 'Connection'; // TODO(b/38203344): The SDK_VERSION is set independently from Firebase because // we are doing out-of-band releases. Once we release as part of Firebase, we // should use the Firebase version instead. -const X_GOOG_API_CLIENT_VALUE = `gl-node/${ - process.versions.node -} fire/${SDK_VERSION} grpc/${grpcVersion}`; +const X_GOOG_API_CLIENT_VALUE = `gl-node/${process.versions.node} fire/${ + SDK_VERSION +} grpc/${grpcVersion}`; type DuplexRpc = () => grpc.ClientDuplexStream; type ReadableRpc = (req: Req) => grpc.ClientReadableStream; diff --git a/packages/firestore/src/remote/persistent_stream.ts b/packages/firestore/src/remote/persistent_stream.ts index 0fd3c6573fc..e563d084c7b 100644 --- a/packages/firestore/src/remote/persistent_stream.ts +++ b/packages/firestore/src/remote/persistent_stream.ts @@ -273,7 +273,9 @@ export abstract class PersistentStream< // rejections are not considered unhandled. assert( err.code === Code.CANCELLED, - `Received unexpected error in idle timeout closure. Expected CANCELLED, but was: ${err}` + `Received unexpected error in idle timeout closure. Expected CANCELLED, but was: ${ + err + }` ); }); } diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 7ad47b8b078..6d7701c760c 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -191,9 +191,11 @@ export function validateNamedPropertyEquals( const actualDescription = valueDescription(input); throw new FirestoreError( Code.INVALID_ARGUMENT, - `Invalid value ${actualDescription} provided to function ${functionName}() for option "${optionName}". Acceptable values: ${expectedDescription.join( - ', ' - )}` + `Invalid value ${actualDescription} provided to function ${ + functionName + }() for option "${ + optionName + }". Acceptable values: ${expectedDescription.join(', ')}` ); } diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index a9820691496..8bab7ff59fa 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -170,7 +170,9 @@ apiDescribe('Validation:', persistence => { const collection = db.collection('test-collection'); const doc = collection.doc('test-document'); for (const path of badPaths) { - const reason = `Invalid path (${path}). Paths must not contain // in them.`; + const reason = `Invalid path (${ + path + }). Paths must not contain // in them.`; expect(() => db.collection(path)).to.throw(reason); expect(() => db.doc(path)).to.throw(reason); expect(() => collection.doc(path)).to.throw(reason); From 52bfcfb3a0cd3e41681490e62481f3bb5af7140d Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Thu, 22 Feb 2018 12:35:37 -0800 Subject: [PATCH 3/6] [AUTOMATED]: Prettier Code Styling --- packages/database/test/order_by.test.ts | 4 +- packages/database/test/query.test.ts | 28 +++-------- packages/database/test/transaction.test.ts | 48 +++++-------------- .../src/platform_node/grpc_connection.ts | 6 +-- .../firestore/src/remote/persistent_stream.ts | 4 +- .../firestore/src/util/input_validation.ts | 8 ++-- .../test/integration/api/validation.test.ts | 4 +- 7 files changed, 28 insertions(+), 74 deletions(-) diff --git a/packages/database/test/order_by.test.ts b/packages/database/test/order_by.test.ts index 70495f2fd60..b821de62138 100644 --- a/packages/database/test/order_by.test.ts +++ b/packages/database/test/order_by.test.ts @@ -363,9 +363,7 @@ describe('.orderBy tests', function() { expect(addedPrevNames).to.deep.equal(expectedPrevNames); }); - it('Removing default listener removes non-default listener that loads all data', function( - done - ) { + it('Removing default listener removes non-default listener that loads all data', function(done) { const ref = getRandomNode() as Reference; const initial = { key: 'value' }; diff --git a/packages/database/test/query.test.ts b/packages/database/test/query.test.ts index 499748d3edd..bda21cdbb0b 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database/test/query.test.ts @@ -1971,9 +1971,7 @@ describe('Query Tests', function() { expect(val).to.equal(2); }); - it('.startAt() with two arguments works properly (case 1169).', function( - done - ) { + it('.startAt() with two arguments works properly (case 1169).', function(done) { const ref = getRandomNode() as Reference; const data = { Walker: { @@ -2110,9 +2108,7 @@ describe('Query Tests', function() { }); }); - it(".endAt(null, 'f').limitToLast(5) returns the right set of children.", function( - done - ) { + it(".endAt(null, 'f').limitToLast(5) returns the right set of children.", function(done) { const ref = getRandomNode() as Reference; ref.set( { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g', h: 'h' }, @@ -2134,9 +2130,7 @@ describe('Query Tests', function() { ); }); - it('complex update() at query root raises correct value event', function( - done - ) { + it('complex update() at query root raises correct value event', function(done) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2241,9 +2235,7 @@ describe('Query Tests', function() { }); }); - it('listen for child_added events with limit and different types fires properly', function( - done - ) { + it('listen for child_added events with limit and different types fires properly', function(done) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2285,9 +2277,7 @@ describe('Query Tests', function() { }); }); - it('listen for child_changed events with limit and different types fires properly', function( - done - ) { + it('listen for child_changed events with limit and different types fires properly', function(done) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2338,9 +2328,7 @@ describe('Query Tests', function() { }); }); - it('listen for child_remove events with limit and different types fires properly', function( - done - ) { + it('listen for child_remove events with limit and different types fires properly', function(done) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; @@ -2442,9 +2430,7 @@ describe('Query Tests', function() { ); }); - it('listen for child_remove events when parent set to scalar', function( - done - ) { + it('listen for child_remove events when parent set to scalar', function(done) { const nodePair = getRandomNode(2); const writer = nodePair[0]; const reader = nodePair[1]; diff --git a/packages/database/test/transaction.test.ts b/packages/database/test/transaction.test.ts index e603105ebc0..d0f44f5accc 100644 --- a/packages/database/test/transaction.test.ts +++ b/packages/database/test/transaction.test.ts @@ -86,9 +86,7 @@ describe('Transaction Tests', function() { }); }); - it('Non-aborted transaction sets committed to true in callback.', function( - done - ) { + it('Non-aborted transaction sets committed to true in callback.', function(done) { const node = getRandomNode() as Reference; node.transaction( @@ -104,9 +102,7 @@ describe('Transaction Tests', function() { ); }); - it('Aborted transaction sets committed to false in callback.', function( - done - ) { + it('Aborted transaction sets committed to false in callback.', function(done) { const node = getRandomNode() as Reference; node.transaction( @@ -236,9 +232,7 @@ describe('Transaction Tests', function() { return ea.promise; }); - it('Second transaction gets run immediately on previous output and only runs once.', function( - done - ) { + it('Second transaction gets run immediately on previous output and only runs once.', function(done) { const nodePair = getRandomNode(2) as Reference[]; let firstRun = false, firstDone = false, @@ -512,9 +506,7 @@ describe('Transaction Tests', function() { ); }); - it('Set should cancel already sent transactions that come back as datastale.', function( - done - ) { + it('Set should cancel already sent transactions that come back as datastale.', function(done) { const nodePair = getRandomNode(2) as Reference[]; let transactionCalls = 0; nodePair[0].set(5, function() { @@ -688,9 +680,7 @@ describe('Transaction Tests', function() { return Promise.all([tx1, tx2]); }); - it('Doing set() in successful transaction callback works. Case 870.', function( - done - ) { + it('Doing set() in successful transaction callback works. Case 870.', function(done) { const node = getRandomNode() as Reference; let transactionCalled = false; let callbackCalled = false; @@ -710,9 +700,7 @@ describe('Transaction Tests', function() { ); }); - it('Doing set() in aborted transaction callback works. Case 870.', function( - done - ) { + it('Doing set() in aborted transaction callback works. Case 870.', function(done) { const nodePair = getRandomNode(2) as Reference[], node1 = nodePair[0], node2 = nodePair[1]; @@ -1028,9 +1016,7 @@ describe('Transaction Tests', function() { ); }); - it('Transaction properly reverts data when you add a deeper listen.', function( - done - ) { + it('Transaction properly reverts data when you add a deeper listen.', function(done) { const refPair = getRandomNode(2) as Reference[], ref1 = refPair[0], ref2 = refPair[1]; @@ -1200,9 +1186,7 @@ describe('Transaction Tests', function() { }); }); - it("transaction() doesn't pick up cached data from previous once().", function( - done - ) { + it("transaction() doesn't pick up cached data from previous once().", function(done) { const refPair = getRandomNode(2) as Reference[]; const me = refPair[0], other = refPair[1]; @@ -1229,9 +1213,7 @@ describe('Transaction Tests', function() { }); }); - it("transaction() doesn't pick up cached data from previous transaction.", function( - done - ) { + it("transaction() doesn't pick up cached data from previous transaction.", function(done) { const refPair = getRandomNode(2) as Reference[]; const me = refPair[0], other = refPair[1]; @@ -1263,9 +1245,7 @@ describe('Transaction Tests', function() { ); }); - it('server values: local timestamp should eventually (but not immediately) match the server with txns', function( - done - ) { + it('server values: local timestamp should eventually (but not immediately) match the server with txns', function(done) { const refPair = getRandomNode(2) as Reference[], writer = refPair[0], reader = refPair[1], @@ -1357,9 +1337,7 @@ describe('Transaction Tests', function() { ); }); - it("transaction() on queried location doesn't run initially on null (firebase-worker-queue depends on this).", function( - done - ) { + it("transaction() on queried location doesn't run initially on null (firebase-worker-queue depends on this).", function(done) { const ref = getRandomNode() as Reference; ref.push({ a: 1, b: 2 }, function() { ref @@ -1437,9 +1415,7 @@ describe('Transaction Tests', function() { ); }); - it('transactions works with merges without the transaction path', function( - done - ) { + it('transactions works with merges without the transaction path', function(done) { const ref = getRandomNode() as Reference; ref.update({ foo: 'bar' }); diff --git a/packages/firestore/src/platform_node/grpc_connection.ts b/packages/firestore/src/platform_node/grpc_connection.ts index f6780ead440..639c8c3b831 100644 --- a/packages/firestore/src/platform_node/grpc_connection.ts +++ b/packages/firestore/src/platform_node/grpc_connection.ts @@ -37,9 +37,9 @@ const LOG_TAG = 'Connection'; // TODO(b/38203344): The SDK_VERSION is set independently from Firebase because // we are doing out-of-band releases. Once we release as part of Firebase, we // should use the Firebase version instead. -const X_GOOG_API_CLIENT_VALUE = `gl-node/${process.versions.node} fire/${ - SDK_VERSION -} grpc/${grpcVersion}`; +const X_GOOG_API_CLIENT_VALUE = `gl-node/${ + process.versions.node +} fire/${SDK_VERSION} grpc/${grpcVersion}`; type DuplexRpc = () => grpc.ClientDuplexStream; type ReadableRpc = (req: Req) => grpc.ClientReadableStream; diff --git a/packages/firestore/src/remote/persistent_stream.ts b/packages/firestore/src/remote/persistent_stream.ts index e563d084c7b..0fd3c6573fc 100644 --- a/packages/firestore/src/remote/persistent_stream.ts +++ b/packages/firestore/src/remote/persistent_stream.ts @@ -273,9 +273,7 @@ export abstract class PersistentStream< // rejections are not considered unhandled. assert( err.code === Code.CANCELLED, - `Received unexpected error in idle timeout closure. Expected CANCELLED, but was: ${ - err - }` + `Received unexpected error in idle timeout closure. Expected CANCELLED, but was: ${err}` ); }); } diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 6d7701c760c..7ad47b8b078 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -191,11 +191,9 @@ export function validateNamedPropertyEquals( const actualDescription = valueDescription(input); throw new FirestoreError( Code.INVALID_ARGUMENT, - `Invalid value ${actualDescription} provided to function ${ - functionName - }() for option "${ - optionName - }". Acceptable values: ${expectedDescription.join(', ')}` + `Invalid value ${actualDescription} provided to function ${functionName}() for option "${optionName}". Acceptable values: ${expectedDescription.join( + ', ' + )}` ); } diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index 8bab7ff59fa..a9820691496 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -170,9 +170,7 @@ apiDescribe('Validation:', persistence => { const collection = db.collection('test-collection'); const doc = collection.doc('test-document'); for (const path of badPaths) { - const reason = `Invalid path (${ - path - }). Paths must not contain // in them.`; + const reason = `Invalid path (${path}). Paths must not contain // in them.`; expect(() => db.collection(path)).to.throw(reason); expect(() => db.doc(path)).to.throw(reason); expect(() => collection.doc(path)).to.throw(reason); From 2000b13ed88dc7097b82b44e164cd395715bc2d9 Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Mon, 12 Mar 2018 15:36:39 -0700 Subject: [PATCH 4/6] migrate storage from localstorage to indexedDb --- packages/auth/src/authstorage.js | 50 ++++++ packages/auth/src/storage/asyncstorage.js | 2 + packages/auth/src/storage/factory.js | 6 + packages/auth/src/storage/indexeddb.js | 2 + packages/auth/src/storage/inmemorystorage.js | 2 + packages/auth/src/storage/localstorage.js | 2 + packages/auth/src/storage/mockstorage.js | 2 + packages/auth/src/storage/nullstorage.js | 3 + packages/auth/src/storage/sessionstorage.js | 2 + packages/auth/src/storage/storage.js | 19 +++ packages/auth/src/storageusermanager.js | 10 +- packages/auth/src/utils.js | 48 +++++- packages/auth/test/authstorage_test.js | 41 +++++ .../auth/test/storage/asyncstorage_test.js | 2 + packages/auth/test/storage/factory_test.js | 33 ++++ packages/auth/test/storage/indexeddb_test.js | 2 + .../auth/test/storage/inmemorystorage_test.js | 2 + .../auth/test/storage/localstorage_test.js | 2 + .../auth/test/storage/mockstorage_test.js | 2 + .../auth/test/storage/nullstorage_test.js | 2 + .../auth/test/storage/sessionstorage_test.js | 2 + packages/auth/test/storageusermanager_test.js | 66 ++++++++ packages/auth/test/utils_test.js | 158 +++++++++++++++++- 23 files changed, 450 insertions(+), 10 deletions(-) diff --git a/packages/auth/src/authstorage.js b/packages/auth/src/authstorage.js index 5f4033b8c54..ef187dcce22 100644 --- a/packages/auth/src/authstorage.js +++ b/packages/auth/src/authstorage.js @@ -26,6 +26,7 @@ goog.provide('fireauth.authStorage.Persistence'); goog.require('fireauth.AuthError'); goog.require('fireauth.authenum.Error'); goog.require('fireauth.storage.Factory'); +goog.require('fireauth.storage.Storage'); goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.array'); @@ -276,6 +277,55 @@ fireauth.authStorage.Manager.prototype.getKeyName_ = function(dataKey, opt_id) { }; +/** + * Migrates window.localStorage to the provided persistent storage. + * @param {fireauth.authStorage.Key} dataKey The key under which the persistent + * value is supposed to be stored. + * @param {?string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + * @return {!goog.Promise} A promise that resolves when the data stored + * in window.localStorage is migrated to the provided persistent storage + * identified by the provided data key. + */ +fireauth.authStorage.Manager.prototype.migrateFromLocalStorage = + function(dataKey, opt_id) { + var self = this; + var key = this.getKeyName_(dataKey, opt_id); + var storage = this.getStorage_(dataKey.persistent); + // Get data stored in the default persistent storage identified by dataKey. + return this.get(dataKey, opt_id).then(function(response) { + // Get the stored value in window.localStorage if available. + var oldStorageValue = null; + try { + oldStorageValue = fireauth.util.parseJSON( + goog.global['localStorage']['getItem'](key)); + } catch (e) { + // Set value as null. This will resolve the promise immediately. + } + // If data is stored in window.localStorage but no data is available in + // default persistent storage, migrate data from window.localStorage to + // default persistent storage. + if (oldStorageValue && !response) { + // This condition may fail in situations where a user opens a tab with + // an old version while using a tab with a new version, or when a + // developer switches back and forth between and old and new version of + // the library. + goog.global['localStorage']['removeItem'](key); + // Migrate the value to new default persistent storage. + return self.set(dataKey, oldStorageValue, opt_id); + } else if (oldStorageValue && + response && + storage.type != fireauth.storage.Storage.Type.LOCAL_STORAGE) { + // Data stored in both localStorage and new persistent storage (eg. + // indexedDB) for some reason. + // This could happen if the developer is migrating back and forth. + // The new default persistent storage (eg. indexedDB) takes precedence. + goog.global['localStorage']['removeItem'](key); + } + }); +}; + + /** * Gets the stored value from the corresponding storage. * @param {fireauth.authStorage.Key} dataKey The key under which the value is diff --git a/packages/auth/src/storage/asyncstorage.js b/packages/auth/src/storage/asyncstorage.js index e5cb678e20e..fc01b141b59 100644 --- a/packages/auth/src/storage/asyncstorage.js +++ b/packages/auth/src/storage/asyncstorage.js @@ -45,6 +45,8 @@ fireauth.storage.AsyncStorage = function(opt_asyncStorage) { throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, 'The React Native compatibility library was not found.'); } + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.ASYNC_STORAGE; }; diff --git a/packages/auth/src/storage/factory.js b/packages/auth/src/storage/factory.js index d6ab01b5844..a8ee5b4996e 100644 --- a/packages/auth/src/storage/factory.js +++ b/packages/auth/src/storage/factory.js @@ -78,6 +78,10 @@ fireauth.storage.Factory.EnvConfig = { REACT_NATIVE: { persistent: fireauth.storage.AsyncStorage, temporary: fireauth.storage.NullStorage + }, + WORKER: { + persistent: fireauth.storage.LocalStorage, + temporary: fireauth.storage.NullStorage } }; @@ -95,6 +99,8 @@ fireauth.storage.Factory.getEnvConfig = function() { fireauth.storage.Factory.EnvConfig.NODE; envMap[fireauth.util.Env.REACT_NATIVE] = fireauth.storage.Factory.EnvConfig.REACT_NATIVE; + envMap[fireauth.util.Env.WORKER] = + fireauth.storage.Factory.EnvConfig.WORKER; return envMap[fireauth.util.getEnvironment()]; }; diff --git a/packages/auth/src/storage/indexeddb.js b/packages/auth/src/storage/indexeddb.js index cd5176ef171..d333e79dd12 100644 --- a/packages/auth/src/storage/indexeddb.js +++ b/packages/auth/src/storage/indexeddb.js @@ -94,6 +94,8 @@ fireauth.storage.IndexedDB = function( /** @private {!IDBFactory} The indexedDB factory object. */ this.indexedDB_ = /** @type {!IDBFactory} */ ( opt_indexedDB || goog.global.indexedDB); + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.INDEXEDDB; }; diff --git a/packages/auth/src/storage/inmemorystorage.js b/packages/auth/src/storage/inmemorystorage.js index c63da740d49..58d9f3c1b39 100644 --- a/packages/auth/src/storage/inmemorystorage.js +++ b/packages/auth/src/storage/inmemorystorage.js @@ -33,6 +33,8 @@ goog.require('goog.Promise'); fireauth.storage.InMemoryStorage = function() { /** @protected {!Object} The object where we store values. */ this.storage = {}; + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.IN_MEMORY; }; diff --git a/packages/auth/src/storage/localstorage.js b/packages/auth/src/storage/localstorage.js index 61bf55cea12..ddfca2cafe1 100644 --- a/packages/auth/src/storage/localstorage.js +++ b/packages/auth/src/storage/localstorage.js @@ -50,6 +50,8 @@ fireauth.storage.LocalStorage = function() { this.storage_ = /** @type {!Storage} */ ( fireauth.storage.LocalStorage.getGlobalStorage() || firebase.INTERNAL['node']['localStorage']); + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.LOCAL_STORAGE; }; diff --git a/packages/auth/src/storage/mockstorage.js b/packages/auth/src/storage/mockstorage.js index d118fda4f3d..8d6c46dafc3 100644 --- a/packages/auth/src/storage/mockstorage.js +++ b/packages/auth/src/storage/mockstorage.js @@ -36,6 +36,8 @@ fireauth.storage.MockStorage = function() { */ this.storageListeners_ = []; fireauth.storage.MockStorage.base(this, 'constructor'); + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.MOCK_STORAGE; }; goog.inherits(fireauth.storage.MockStorage, fireauth.storage.InMemoryStorage); diff --git a/packages/auth/src/storage/nullstorage.js b/packages/auth/src/storage/nullstorage.js index 9070dbc0cc7..c3b9cdfc789 100644 --- a/packages/auth/src/storage/nullstorage.js +++ b/packages/auth/src/storage/nullstorage.js @@ -16,6 +16,7 @@ goog.provide('fireauth.storage.NullStorage'); +goog.require('fireauth.storage.Storage'); goog.require('goog.Promise'); @@ -29,6 +30,8 @@ goog.require('goog.Promise'); fireauth.storage.NullStorage = function() { /** @private {!Object} The object where we store values. */ this.storage_ = {}; + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.NULL_STORAGE; }; diff --git a/packages/auth/src/storage/sessionstorage.js b/packages/auth/src/storage/sessionstorage.js index c566e55c5ae..af770bac338 100644 --- a/packages/auth/src/storage/sessionstorage.js +++ b/packages/auth/src/storage/sessionstorage.js @@ -49,6 +49,8 @@ fireauth.storage.SessionStorage = function() { this.storage_ = /** @type {!Storage} */ ( fireauth.storage.SessionStorage.getGlobalStorage() || firebase.INTERNAL['node']['sessionStorage']); + /** @protected {string} The storage type identifier. */ + this.type = fireauth.storage.Storage.Type.SESSION_STORAGE; }; diff --git a/packages/auth/src/storage/storage.js b/packages/auth/src/storage/storage.js index 3a61342e647..d59c0ac7f89 100644 --- a/packages/auth/src/storage/storage.js +++ b/packages/auth/src/storage/storage.js @@ -65,3 +65,22 @@ fireauth.storage.Storage.prototype.addStorageListener = function(listener) {}; */ fireauth.storage.Storage.prototype.removeStorageListener = function(listener) { }; + + +/** @type {string} The storage type identifier. */ +fireauth.storage.Storage.prototype.type; + + +/** + * Enum for the identifier of the type of underlying storage. + * @enum {string} + */ +fireauth.storage.Storage.Type = { + ASYNC_STORAGE: 'asyncStorage', + IN_MEMORY: 'inMemory', + INDEXEDDB: 'indexedDB', + LOCAL_STORAGE: 'localStorage', + MOCK_STORAGE: 'mockStorage', + NULL_STORAGE: 'nullStorage', + SESSION_STORAGE: 'sessionStorage' +}; diff --git a/packages/auth/src/storageusermanager.js b/packages/auth/src/storageusermanager.js index 44eadac0d07..8c0bd52003d 100644 --- a/packages/auth/src/storageusermanager.js +++ b/packages/auth/src/storageusermanager.js @@ -206,8 +206,14 @@ fireauth.storage.UserManager.prototype.initialize_ = function() { // In memory key. This is unlikely to contain anything on load. var inMemoryKey = fireauth.storage.UserManager.getAuthUserKey_( fireauth.authStorage.Persistence.NONE); - // Check if state is stored in session storage. - return this.manager_.get(sessionKey, this.appId_).then(function(response) { + // Migrate any old currentUser from localStorage to indexedDB. + // This keeps any user signed in without the need for reauthentication and + // minimizes risks of dangling Auth states. + return this.manager_.migrateFromLocalStorage( + localKey, this.appId_).then(function() { + // Check if state is stored in session storage. + return self.manager_.get(sessionKey, self.appId_); + }).then(function(response) { if (response) { // Session storage is being used. return sessionKey; diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index f5def3ed324..b1e67abe2e1 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -610,6 +610,17 @@ fireauth.util.isOpenerAnIframe = function(opt_win) { }; +/** + * @param {?Object=} opt_global The optional global scope. + * @return {boolean} Whether current environment is a worker. + */ +fireauth.util.isWorker = function(opt_global) { + var scope = opt_global || goog.global; + return typeof scope['window'] !== 'object' && + typeof scope['importScripts'] === 'function'; +}; + + /** * Enum for the runtime environment. * @enum {string} @@ -617,7 +628,8 @@ fireauth.util.isOpenerAnIframe = function(opt_win) { fireauth.util.Env = { BROWSER: 'Browser', NODE: 'Node', - REACT_NATIVE: 'ReactNative' + REACT_NATIVE: 'ReactNative', + WORKER: 'Worker' }; @@ -632,6 +644,9 @@ fireauth.util.getEnvironment = function() { // the library is browser only. Use this check instead to reliably detect // a Node.js environment. return fireauth.util.Env.NODE; + } else if (fireauth.util.isWorker()) { + // Worker environment. + return fireauth.util.Env.WORKER; } // The default is a browser environment. return fireauth.util.Env.BROWSER; @@ -815,6 +830,13 @@ fireauth.util.getClientVersion = function(clientImplementation, clientVersion, // In a browser environment, report the browser name. var userAgent = opt_userAgent || fireauth.util.getUserAgentString(); reportedEnvironment = fireauth.util.getBrowserName(userAgent); + } else if (environment === fireauth.util.Env.WORKER) { + // Technically a worker runs from a browser but we need to differentiate a + // worker from a browser. + // For example: Chrome-Worker/JsCore/4.9.1/FirebaseCore-web. + var userAgent = opt_userAgent || fireauth.util.getUserAgentString(); + reportedEnvironment = fireauth.util.getBrowserName(userAgent) + '-' + + environment; } else { // Otherwise, just report the environment name. reportedEnvironment = environment; @@ -882,7 +904,11 @@ fireauth.util.isWebStorageSupported = function() { } return true; } - } catch (e) {} + } catch (e) { + // localStorage is not available from a worker. Test availability of + // indexedDB. + return fireauth.util.isWorker() && !!goog.global['indexedDB']; + } return false; }; @@ -913,7 +939,9 @@ fireauth.util.isPopupRedirectSupported = function() { !fireauth.util.isNativeEnvironment() && // Local storage has to be supported for browser popup and redirect // operations to work. - fireauth.util.isWebStorageSupported(); + fireauth.util.isWebStorageSupported() && + // DOM, popups and redirects are not supported within a worker. + !fireauth.util.isWorker(); }; @@ -1343,13 +1371,23 @@ fireauth.util.isIndexedDBAvailable = function() { }; +/** @return {boolean} Whether current mode is Auth handler or iframe. */ +fireauth.util.isAuthHandlerOrIframe = function() { + return !!(fireauth.util.getObjectRef('fireauth.oauthhelper', goog.global) || + fireauth.util.getObjectRef('fireauth.iframe', goog.global)); +}; + + /** @return {boolean} Whether indexedDB is used to persist storage. */ fireauth.util.persistsStorageWithIndexedDB = function() { // This will cover: // IE11, Edge when indexedDB is available (this is unavailable in InPrivate - // mode). + // mode). (SDK, OAuth handler and iframe) + // Any environment where indexedDB is available (SDK only). + // In a browser environment, when an iframe and a popup web storage are not // synchronized, use the indexedDB fireauth.storage.Storage implementation. - return fireauth.util.isLocalStorageNotSynchronized() && + return (fireauth.util.isLocalStorageNotSynchronized() || + !fireauth.util.isAuthHandlerOrIframe()) && fireauth.util.isIndexedDBAvailable(); }; diff --git a/packages/auth/test/authstorage_test.js b/packages/auth/test/authstorage_test.js index 441cecae531..3739a7e70f8 100644 --- a/packages/auth/test/authstorage_test.js +++ b/packages/auth/test/authstorage_test.js @@ -408,6 +408,47 @@ function testGetSet_persistentStorage() { } +function testMigrateFromLocalStorage_previouslyPersistedWithLocalStorage() { + var manager = getDefaultManagerInstance(); + var key = {name: 'persistent', persistent: 'local'}; + var expectedValue = 'something'; + var storageKey = 'firebase:persistent:appId1'; + // Save expected value to window.localStorage initially. + window.localStorage.setItem(storageKey, JSON.stringify(expectedValue)); + return manager.migrateFromLocalStorage(key, appId) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + // Data should be migrated from window.localStorage to mockLocalStorage. + assertEquals(expectedValue, value); + assertNull(window.localStorage.getItem(storageKey)); + }); +} + + +function testMigrateFromLocalStorage_multiplePersistentStorage() { + var manager = getDefaultManagerInstance(); + var key = {name: 'persistent', persistent: 'local'}; + var expectedValue = 'something'; + var expectedValue2 = 'somethingElse'; + var storageKey = 'firebase:persistent:appId1'; + // Save expected value to mockLocalStorage. + mockLocalStorage.set(storageKey, expectedValue); + // Save second expected value to window.localStorage. + window.localStorage.setItem(storageKey, JSON.stringify(expectedValue2)); + return manager.migrateFromLocalStorage(key, appId) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + // mockLocalStorage will take precedence over window.localStorage. + assertEquals(expectedValue, value); + assertNull(window.localStorage.getItem(storageKey)); + }); +} + + function testGetSet_persistentStorage_noId() { var manager = getDefaultManagerInstance(); var key = {name: 'persistent', persistent: 'local'}; diff --git a/packages/auth/test/storage/asyncstorage_test.js b/packages/auth/test/storage/asyncstorage_test.js index ac3f8d200dd..9e8afb72dd3 100644 --- a/packages/auth/test/storage/asyncstorage_test.js +++ b/packages/auth/test/storage/asyncstorage_test.js @@ -17,6 +17,7 @@ goog.provide('fireauth.storage.AsyncStorageTest'); goog.require('fireauth.storage.AsyncStorage'); +goog.require('fireauth.storage.Storage'); /** @suppress {extraRequire} */ goog.require('fireauth.storage.testHelper'); goog.require('fireauth.storage.testHelper.FakeAsyncStorage'); @@ -43,6 +44,7 @@ function tearDown() { function testBasicStorageOperations() { + assertEquals(fireauth.storage.Storage.Type.ASYNC_STORAGE, storage.type); return assertBasicStorageOperations(storage); } diff --git a/packages/auth/test/storage/factory_test.js b/packages/auth/test/storage/factory_test.js index 8102421d9c7..69ce48835ae 100644 --- a/packages/auth/test/storage/factory_test.js +++ b/packages/auth/test/storage/factory_test.js @@ -140,6 +140,39 @@ function testGetStorage_reactnative_persistent() { } +function testGetStorage_worker_persistent() { + var mock = { + type: 'indexedDB' + }; + // persistsStorageWithIndexedDB is true in a worker environment. + stubs.replace( + fireauth.util, + 'persistsStorageWithIndexedDB', + function() { + return true; + }); + // Return a mock indexeDB instance to assert the expected result of the test + // below. + stubs.replace( + fireauth.storage.IndexedDB, + 'getFireauthManager', + function() { + return mock; + }); + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.WORKER); + assertEquals('indexedDB', factory.makePersistentStorage().type); +} + + +function testGetStorage_worker_temporary() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.WORKER); + assertTrue(factory.makeTemporaryStorage() instanceof + fireauth.storage.NullStorage); +} + + function testGetStorage_inMemory() { var factory = new fireauth.storage.Factory( fireauth.storage.Factory.EnvConfig.BROWSER); diff --git a/packages/auth/test/storage/indexeddb_test.js b/packages/auth/test/storage/indexeddb_test.js index 9f0e1958f2f..863d9b7a571 100644 --- a/packages/auth/test/storage/indexeddb_test.js +++ b/packages/auth/test/storage/indexeddb_test.js @@ -19,6 +19,7 @@ goog.provide('fireauth.storage.IndexedDBTest'); goog.require('fireauth.AuthError'); goog.require('fireauth.authenum.Error'); goog.require('fireauth.storage.IndexedDB'); +goog.require('fireauth.storage.Storage'); goog.require('goog.Promise'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.PropertyReplacer'); @@ -240,6 +241,7 @@ function testIndexedDb_notSupported() { function testIndexedDb_null() { manager = getDefaultFireauthManager(); + assertEquals(fireauth.storage.Storage.Type.INDEXEDDB, manager.type); return manager.get('key1') .then(function(data) { assertNull(data); diff --git a/packages/auth/test/storage/inmemorystorage_test.js b/packages/auth/test/storage/inmemorystorage_test.js index 348f678dd6a..a18b7ae91b3 100644 --- a/packages/auth/test/storage/inmemorystorage_test.js +++ b/packages/auth/test/storage/inmemorystorage_test.js @@ -17,6 +17,7 @@ goog.provide('fireauth.storage.InMemoryStorageTest'); goog.require('fireauth.storage.InMemoryStorage'); +goog.require('fireauth.storage.Storage'); /** @suppress {extraRequire} */ goog.require('fireauth.storage.testHelper'); goog.require('goog.testing.jsunit'); @@ -38,6 +39,7 @@ function tearDown() { function testBasicStorageOperations() { + assertEquals(fireauth.storage.Storage.Type.IN_MEMORY, storage.type); return assertBasicStorageOperations(storage); } diff --git a/packages/auth/test/storage/localstorage_test.js b/packages/auth/test/storage/localstorage_test.js index c050117bb56..a50820187a9 100644 --- a/packages/auth/test/storage/localstorage_test.js +++ b/packages/auth/test/storage/localstorage_test.js @@ -19,6 +19,7 @@ goog.provide('fireauth.storage.LocalStorageTest'); goog.require('fireauth.AuthError'); goog.require('fireauth.authenum.Error'); goog.require('fireauth.storage.LocalStorage'); +goog.require('fireauth.storage.Storage'); /** @suppress {extraRequire} */ goog.require('fireauth.storage.testHelper'); goog.require('fireauth.util'); @@ -64,6 +65,7 @@ function simulateNodeEnvironment() { function testBasicStorageOperations() { + assertEquals(fireauth.storage.Storage.Type.LOCAL_STORAGE, storage.type); return assertBasicStorageOperations(storage); } diff --git a/packages/auth/test/storage/mockstorage_test.js b/packages/auth/test/storage/mockstorage_test.js index 852f27290dc..e959239b965 100644 --- a/packages/auth/test/storage/mockstorage_test.js +++ b/packages/auth/test/storage/mockstorage_test.js @@ -17,6 +17,7 @@ goog.provide('fireauth.storage.MockStorageTest'); goog.require('fireauth.storage.MockStorage'); +goog.require('fireauth.storage.Storage'); /** @suppress {extraRequire} */ goog.require('fireauth.storage.testHelper'); goog.require('goog.events.EventType'); @@ -42,6 +43,7 @@ function tearDown() { function testBasicStorageOperations() { + assertEquals(fireauth.storage.Storage.Type.MOCK_STORAGE, storage.type); return assertBasicStorageOperations(storage); } diff --git a/packages/auth/test/storage/nullstorage_test.js b/packages/auth/test/storage/nullstorage_test.js index 84f1bd3486c..d47e2f71d7d 100644 --- a/packages/auth/test/storage/nullstorage_test.js +++ b/packages/auth/test/storage/nullstorage_test.js @@ -17,6 +17,7 @@ goog.provide('fireauth.storage.NullStorageTest'); goog.require('fireauth.storage.NullStorage'); +goog.require('fireauth.storage.Storage'); goog.require('goog.Promise'); goog.require('goog.testing.jsunit'); @@ -27,6 +28,7 @@ goog.setTestOnly('fireauth.storage.NullStorageTest'); function testNullStorage() { var storage = new fireauth.storage.NullStorage(); var listener = function() {}; + assertEquals(fireauth.storage.Storage.Type.NULL_STORAGE, storage.type); storage.addStorageListener(listener); storage.removeStorageListener(listener); return goog.Promise.resolve() diff --git a/packages/auth/test/storage/sessionstorage_test.js b/packages/auth/test/storage/sessionstorage_test.js index 061c39d02b2..5660f5f2c09 100644 --- a/packages/auth/test/storage/sessionstorage_test.js +++ b/packages/auth/test/storage/sessionstorage_test.js @@ -19,6 +19,7 @@ goog.provide('fireauth.storage.SessionStorageTest'); goog.require('fireauth.AuthError'); goog.require('fireauth.authenum.Error'); goog.require('fireauth.storage.SessionStorage'); +goog.require('fireauth.storage.Storage'); /** @suppress {extraRequire} */ goog.require('fireauth.storage.testHelper'); goog.require('fireauth.util'); @@ -60,6 +61,7 @@ function simulateNodeEnvironment() { function testBasicStorageOperations() { + assertEquals(fireauth.storage.Storage.Type.SESSION_STORAGE, storage.type); return assertBasicStorageOperations(storage); } diff --git a/packages/auth/test/storageusermanager_test.js b/packages/auth/test/storageusermanager_test.js index 492517ebfcb..9e80065bab6 100644 --- a/packages/auth/test/storageusermanager_test.js +++ b/packages/auth/test/storageusermanager_test.js @@ -372,6 +372,72 @@ function testUserManager_initializedWithLocal() { } +function testUserManager_initializedWithLocal_migratedFromLocalStorage() { + var storageKey = 'firebase:authUser:appId1'; + // Save Auth state to localStorage. This will be migrated to mockLocalStorage. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.getCurrentUser() + .then(function(user) { + assertObjectEquals(testUser, user); + // User should be cleared from window.localStorage. + assertNull(window.localStorage.getItem(storageKey)); + // User should be saved in mock local storage only with everything else + // cleared. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser, storageManager); + }) + .then(function() { + // Should be saved in local storage only. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser2, storageManager); + }); +} + + +function testUserManager_initializedWithLocal_multiplePersistentStorage() { + var storageKey = 'firebase:authUser:appId1'; + // Save Auth state to window.localStorage. This will be cleared. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + // Save another Auth state in mockLocalStorage. This will have precedence over + // window.localStorage. + mockLocalStorage.set(storageKey, testUser2.toPlainObject()); + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.getCurrentUser() + .then(function(user) { + assertObjectEquals(testUser2, user); + // User should be ignored and cleared from window.localStorage. + assertNull(window.localStorage.getItem(storageKey)); + // Existing user saved in mock local storage persisted with everything + // else cleared. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser2, storageManager); + }) + .then(function() { + return userManager.setCurrentUser(testUser); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser, storageManager); + }); +} + + function testUserManager_initializedWithDefault() { var storageManager = getDefaultStorageManagerInstance(); var userManager = new fireauth.storage.UserManager('appId1', storageManager); diff --git a/packages/auth/test/utils_test.js b/packages/auth/test/utils_test.js index cd2a140297f..2867b3c0df2 100644 --- a/packages/auth/test/utils_test.js +++ b/packages/auth/test/utils_test.js @@ -494,6 +494,26 @@ function testGetEnvironment_node() { } +function testGetEnvironment_worker() { + // Simulate worker environment. + stubs.replace( + fireauth.util, + 'isWorker', + function() { + return true; + }); + assertEquals(fireauth.util.Env.WORKER, fireauth.util.getEnvironment()); +} + + +function testIsWorker() { + assertFalse(fireauth.util.isWorker({'window': {}})); + assertTrue(fireauth.util.isWorker({ + 'importScripts': function() {} + })); +} + + function testGetBrowserName_opera() { assertEquals('Opera', fireauth.util.getBrowserName(operaUA)); } @@ -601,6 +621,25 @@ function testGetClientVersion_node() { } +function testGetClientVersion_worker() { + var ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + + 'Gecko) Chrome/50.0.2661.94 Safari/537.36'; + var firebaseSdkVersion = '4.9.1'; + // Simulate worker environment. + stubs.replace( + fireauth.util, + 'isWorker', + function() { + return true; + }); + assertEquals( + 'Chrome-Worker/JsCore/4.9.1/FirebaseCore-web', + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebaseSdkVersion, + null, ua)); +} + + function testGetFrameworkIds() { assertArrayEquals([], fireauth.util.getFrameworkIds([])); assertArrayEquals([], fireauth.util.getFrameworkIds(['bla'])); @@ -902,6 +941,35 @@ function testIsPopupRedirectSupported_unsupportedNativeEnvironment() { } +function testIsPopupRedirectSupported_workerEnvironment() { + fireauth.util.isCordovaEnabled = false; + // Web storage supported via indexedDB within worker. + stubs.replace(fireauth.util, 'isWebStorageSupported', function() { + return true; + }); + // HTTPS scheme. + stubs.replace(fireauth.util, 'getCurrentScheme', function() { + return 'https:'; + }); + // Neither iOS, nor Android file environment. + stubs.replace(fireauth.util, 'isAndroidOrIosFileEnvironment', function() { + return false; + }); + // Non-native environment environment. + stubs.replace(fireauth.util, 'isNativeEnvironment', function() { + return false; + }); + // Popup/redirect should be supported with above conditions (minus worker). + assertTrue(fireauth.util.isPopupRedirectSupported()); + // Simulate worker environment. + stubs.replace(fireauth.util, 'isWorker', function() { + return true; + }); + // Popup/redirect no longer supported. + assertFalse(fireauth.util.isPopupRedirectSupported()); +} + + function testIsChromeExtension() { // Test https environment. stubs.replace( @@ -1196,6 +1264,12 @@ function testIsMobileDevice() { fireauth.util.isMobileDevice(chromeUA, fireauth.util.Env.BROWSER)); assertFalse( fireauth.util.isMobileDevice(null, fireauth.util.Env.NODE)); + // For worker environments, the userAgent is still accessible and should be + // used to determine if the current device is a mobile device. + assertTrue( + fireauth.util.isMobileDevice(chriosUA, fireauth.util.Env.WORKER)); + assertFalse( + fireauth.util.isMobileDevice(chromeUA, fireauth.util.Env.WORKER)); } @@ -1247,6 +1321,32 @@ function testIsMobileDevice_mobileEnv_default() { } +function testIsMobileDevice_mobileWorker_default() { + // Simulate mobile browser. + stubs.replace(fireauth.util, 'isMobileBrowser', function(ua) { + return true; + }); + // Whether this is a worker or a non-worker shouldn't matter. + stubs.replace(fireauth.util, 'getEnvironment', function() { + return fireauth.util.Env.WORKER; + }); + assertTrue(fireauth.util.isMobileDevice()); +} + + +function testIsMobileDevice_desktopWorker_default() { + // Simulate desktop browser. + stubs.replace(fireauth.util, 'isMobileBrowser', function(ua) { + return false; + }); + // Whether this is a worker or a non-worker shouldn't matter. + stubs.replace(fireauth.util, 'getEnvironment', function() { + return fireauth.util.Env.WORKER; + }); + assertFalse(fireauth.util.isMobileDevice()); +} + + function testIsOnline_httpOrHttps_online() { // HTTP/HTTPS environment. stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { @@ -1394,6 +1494,24 @@ function testDelay_mobileBrowser() { } +function testDelay_desktopBrowser() { + // Whether this is a worker or a non-worker shouldn't matter. + // The userAgent is the authority on how the delay is determined. + var delay = + new fireauth.util.Delay(10, 50, chromeUA, fireauth.util.Env.WORKER); + assertEquals(10, delay.get()); +} + + +function testDelay_mobileBrowser() { + // Whether this is a worker or a non-worker shouldn't matter. + // The userAgent is the authority on how the delay is determined. + var delay = + new fireauth.util.Delay(10, 50, chriosUA, fireauth.util.Env.WORKER); + assertEquals(50, delay.get()); +} + + function testDelay_node() { var delay = new fireauth.util.Delay(10, 50, null, fireauth.util.Env.NODE); assertEquals(10, delay.get()); @@ -1613,7 +1731,11 @@ function testUtcTimestampToDateString() { function testPersistsStorageWithIndexedDB() { - // localStorage not synchronized and indexedDB available. + // SDK only: localStorage not synchronized and indexedDB available. + stubs.replace( + fireauth.util, + 'isAuthHandlerOrIframe', + function() {return false;}); stubs.replace( fireauth.util, 'isLocalStorageNotSynchronized', @@ -1624,7 +1746,7 @@ function testPersistsStorageWithIndexedDB() { function() {return true;}); assertTrue(fireauth.util.persistsStorageWithIndexedDB()); - // localStorage synchronized and indexedDB available. + // SDK only: localStorage synchronized and indexedDB available. stubs.replace( fireauth.util, 'isLocalStorageNotSynchronized', @@ -1633,7 +1755,7 @@ function testPersistsStorageWithIndexedDB() { fireauth.util, 'isIndexedDBAvailable', function() {return true;}); - assertFalse(fireauth.util.persistsStorageWithIndexedDB()); + assertTrue(fireauth.util.persistsStorageWithIndexedDB()); // indexedDB not available. stubs.reset(); @@ -1642,4 +1764,34 @@ function testPersistsStorageWithIndexedDB() { 'isIndexedDBAvailable', function() {return false;}); assertFalse(fireauth.util.persistsStorageWithIndexedDB()); + + // Auth handler/iframe: localStorage not synchronized and indexedDB available. + stubs.replace( + fireauth.util, + 'isAuthHandlerOrIframe', + function() {return true;}); + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() {return true;}); + stubs.replace( + fireauth.util, + 'isIndexedDBAvailable', + function() {return true;}); + assertTrue(fireauth.util.persistsStorageWithIndexedDB()); + + // Auth handler/iframe: localStorage synchronized and indexedDB available. + stubs.replace( + fireauth.util, + 'isAuthHandlerOrIframe', + function() {return true;}); + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() {return false;}); + stubs.replace( + fireauth.util, + 'isIndexedDBAvailable', + function() {return true;}); + assertFalse(fireauth.util.persistsStorageWithIndexedDB()); } From 511ed03611dd8a9815dc34f5197e99a8e68e8c8e Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Mon, 12 Mar 2018 16:55:49 -0700 Subject: [PATCH 5/6] added worker compatibility, exposed finally, updated error message --- packages/auth/src/error_auth.js | 4 +- packages/auth/src/exports_auth.js | 3 + .../recaptchaverifier/recaptchaverifier.js | 12 +- packages/auth/src/rpchandler.js | 62 +++++--- packages/auth/src/utils.js | 46 ++++-- .../recaptchaverifier_test.js | 45 ++++++ packages/auth/test/rpchandler_test.js | 137 +++++++++++++++++- packages/auth/test/utils_test.js | 34 +++++ 8 files changed, 302 insertions(+), 41 deletions(-) diff --git a/packages/auth/src/error_auth.js b/packages/auth/src/error_auth.js index 0af4a05abf9..25dc4f096e3 100644 --- a/packages/auth/src/error_auth.js +++ b/packages/auth/src/error_auth.js @@ -240,7 +240,9 @@ fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_APP_CREDENTIAL] = fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_APP_ID] = 'The mobile app identifier is not registed for the current project.'; fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_AUTH] = - 'The user\'s credential is no longer valid. The user must sign in again.'; + 'This user\'s credential isn\'t valid for this project. This can happen ' + + 'if the user\'s token has been tampered with, or if the user isn\'t for ' + + 'the project associated with this API key.'; fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_AUTH_EVENT] = 'An internal error has occurred.'; fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_CODE] = diff --git a/packages/auth/src/exports_auth.js b/packages/auth/src/exports_auth.js index 8f93162e404..aefb74436aa 100644 --- a/packages/auth/src/exports_auth.js +++ b/packages/auth/src/exports_auth.js @@ -299,6 +299,9 @@ fireauth.exportlib.exportPrototypeMethods( fireauth.exportlib.exportPrototypeMethods( goog.Promise.prototype, { + thenAlways: { + name: 'finally' + }, thenCatch: { name: 'catch' }, diff --git a/packages/auth/src/recaptchaverifier/recaptchaverifier.js b/packages/auth/src/recaptchaverifier/recaptchaverifier.js index 058d569a490..5ccd1b65a48 100644 --- a/packages/auth/src/recaptchaverifier/recaptchaverifier.js +++ b/packages/auth/src/recaptchaverifier/recaptchaverifier.js @@ -94,6 +94,13 @@ fireauth.BaseRecaptchaVerifier = function(apiKey, container, opt_parameters, this.isInvisible_ = this.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.SIZE] === 'invisible'; + // Check if DOM is supported. + if (!fireauth.util.isDOMSupported()) { + throw new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, + 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + + 'environment with DOM support.'); + } // reCAPTCHA container must be valid and if visible, not empty. // An invisible reCAPTCHA will not render in its container. That container // will execute the reCAPTCHA when it is clicked. @@ -257,9 +264,8 @@ fireauth.BaseRecaptchaVerifier.prototype.isReady_ = function() { this.cachedReadyPromise_ = this.registerPendingPromise_(goog.Promise.resolve() .then(function() { // Verify environment first. - // This is actually not enough as this could be triggered from a worker - // environment, but DOM ready should theoretically not resolve. - if (fireauth.util.isHttpOrHttps()) { + // Fail quickly from a worker environment or non-HTTP/HTTPS environment. + if (fireauth.util.isHttpOrHttps() && !fireauth.util.isWorker()) { // Wait for DOM to be ready as this feature depends on that. return fireauth.util.onDomReady(); } else { diff --git a/packages/auth/src/rpchandler.js b/packages/auth/src/rpchandler.js index a127553d08b..389456e9336 100644 --- a/packages/auth/src/rpchandler.js +++ b/packages/auth/src/rpchandler.js @@ -35,6 +35,7 @@ goog.require('goog.html.TrustedResourceUrl'); goog.require('goog.json'); goog.require('goog.net.CorsXmlHttpFactory'); goog.require('goog.net.EventType'); +goog.require('goog.net.FetchXmlHttpFactory'); goog.require('goog.net.XhrIo'); goog.require('goog.net.XmlHttpFactory'); goog.require('goog.net.jsloader'); @@ -93,13 +94,6 @@ fireauth.XmlHttpFactory.prototype.internalGetOptions = function() { * @constructor */ fireauth.RpcHandler = function(apiKey, opt_config, opt_firebaseClientVersion) { - // Get XMLHttpRequest reference. - var XMLHttpRequest = fireauth.util.getXMLHttpRequest(); - if (!XMLHttpRequest) { - // In a Node.js environment, xmlhttprequest module needs to be required. - throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, - 'The XMLHttpRequest compatibility library was not found.'); - } this.apiKey_ = apiKey; var config = opt_config || {}; this.secureTokenEndpoint_ = config['secureTokenEndpoint'] || @@ -131,10 +125,46 @@ fireauth.RpcHandler = function(apiKey, opt_config, opt_firebaseClientVersion) { // Log client version for securetoken server. this.secureTokenHeaders_['X-Client-Version'] = opt_firebaseClientVersion; } - /** @const @private {!goog.net.CorsXmlHttpFactory} The CORS XHR factory. */ - this.corsXhrFactory_ = new goog.net.CorsXmlHttpFactory(); - /** @const @private {!goog.net.XmlHttpFactory} The XHR factory. */ - this.xhrFactory_ = new fireauth.XmlHttpFactory(XMLHttpRequest); + + // Get XMLHttpRequest reference. + var XMLHttpRequest = fireauth.RpcHandler.getXMLHttpRequest(); + if (!XMLHttpRequest && !fireauth.util.isWorker()) { + // In a Node.js environment, xmlhttprequest module needs to be required. + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'The XMLHttpRequest compatibility library was not found.'); + } + /** @private {!goog.net.XmlHttpFactory|undefined} The XHR factory. */ + this.rpcHandlerXhrFactory_ = undefined; + // Initialize XHR factory. CORS does not apply in native environments or + // workers so don't use CorsXmlHttpFactory in those cases. + if (fireauth.util.isWorker()) { + // For worker environment use FetchXmlHttpFactory. + this.rpcHandlerXhrFactory_ = new goog.net.FetchXmlHttpFactory( + /** @type {!WorkerGlobalScope} */ (self)); + } else if (fireauth.util.isNativeEnvironment()) { + // For Node.js, this is the polyfill library. For other environments, + // this is the native global XMLHttpRequest. + this.rpcHandlerXhrFactory_ = new fireauth.XmlHttpFactory( + /** @type {function(new:XMLHttpRequest)} */ (XMLHttpRequest)); + } else { + // CORS Browser environment. + this.rpcHandlerXhrFactory_ = new goog.net.CorsXmlHttpFactory(); + } +}; + + +/** + * @return {?function(new:XMLHttpRequest)|undefined} The current environment + * XMLHttpRequest. This is undefined for worker environment. + */ +fireauth.RpcHandler.getXMLHttpRequest = function() { + // In Node.js XMLHttpRequest is polyfilled. + var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; + var XMLHttpRequest = goog.global['XMLHttpRequest'] || + (isNode && + firebase.INTERNAL['node'] && + firebase.INTERNAL['node']['XMLHttpRequest']); + return XMLHttpRequest; }; @@ -392,7 +422,7 @@ fireauth.RpcHandler.prototype.sendXhr_ = function( return; } var sendXhr; - if (fireauth.util.supportsCors()) { + if (fireauth.util.supportsCors() || fireauth.util.isWorker()) { // If supports CORS use goog.net.XhrIo. sendXhr = goog.bind(this.sendXhrUsingXhrIo_, this); } else { @@ -430,13 +460,7 @@ fireauth.RpcHandler.prototype.sendXhrUsingXhrIo_ = function( opt_data, opt_headers, opt_timeout) { - // Send XHR request. CORS does not apply in native environments so don't use - // CorsXmlHttpFactory in those cases. - // For a Node.js environment use the fireauth.XmlHttpFactory instance. - var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; - var xhrIo = fireauth.util.isNativeEnvironment() ? - (isNode ? new goog.net.XhrIo(this.xhrFactory_) : new goog.net.XhrIo()) : - new goog.net.XhrIo(this.corsXhrFactory_); + var xhrIo = new goog.net.XhrIo(this.rpcHandlerXhrFactory_); // xhrIo.setTimeoutInterval not working in IE10 and IE11, handle manually. var requestTimeout; diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index b1e67abe2e1..c1a388a1542 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -23,6 +23,7 @@ goog.provide('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.Timer'); goog.require('goog.Uri'); +goog.require('goog.dom'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.html.SafeUrl'); @@ -82,7 +83,8 @@ fireauth.util.isLocalStorageNotSynchronized = function(opt_userAgent) { /** @return {string} The current URL. */ fireauth.util.getCurrentUrl = function() { return (goog.global['window'] && goog.global['window']['location']['href']) || - ''; + // Check for worker environments. + (self && self['location'] && self['location']['href']) || ''; }; @@ -495,6 +497,12 @@ fireauth.util.onDomReady = function() { }; +/** @return {boolean} Whether environment supports DOM. */ +fireauth.util.isDOMSupported = function() { + return !!goog.global.document; +}; + + /** * The default ondeviceready Cordova timeout in ms. * @const {number} @@ -1094,21 +1102,6 @@ fireauth.util.parseJSON = function(json) { }; -/** - * @return {?function(new:XMLHttpRequest)|undefined} The current environment - * XMLHttpRequest. - */ -fireauth.util.getXMLHttpRequest = function() { - // In Node.js XMLHttpRequest is polyfilled. - var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; - var XMLHttpRequest = goog.global['XMLHttpRequest'] || - (isNode && - firebase.INTERNAL['node'] && - firebase.INTERNAL['node']['XMLHttpRequest']); - return XMLHttpRequest; -}; - - /** * @param {?string=} opt_prefix An optional prefix string to prepend to ID. * @return {string} The generated event ID used to identify a generic event. @@ -1391,3 +1384,24 @@ fireauth.util.persistsStorageWithIndexedDB = function() { !fireauth.util.isAuthHandlerOrIframe()) && fireauth.util.isIndexedDBAvailable(); }; + + +/** Sets the no-referrer meta tag in the document head if applicable. */ +fireauth.util.setNoReferrer = function() { + var doc = goog.global.document; + if (doc) { + try { + var meta = goog.dom.createDom(goog.dom.TagName.META, { + 'name': 'referrer', + 'content': 'no-referrer' + }); + var headCollection = goog.dom.getElementsByTagName(goog.dom.TagName.HEAD); + // Append meta tag to head. + if (headCollection.length) { + headCollection[0].appendChild(meta); + } + } catch (e) { + // Best effort approach. + } + } +}; diff --git a/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js b/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js index 82d1bc1c82d..381a09a1314 100644 --- a/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js +++ b/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js @@ -349,6 +349,24 @@ function installAndRunTest(id, func) { } +function testBaseRecaptchaVerifier_noDOM() { + return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { + var isDOMSupported = mockControl.createMethodMock( + fireauth.util, 'isDOMSupported'); + isDOMSupported().$returns(false).$once(); + mockControl.$replayAll(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, + 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + + 'environment with DOM support.'); + var error = assertThrows(function() { + new fireauth.BaseRecaptchaVerifier('API_KEY', 'id'); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + function testBaseRecaptchaVerifier_noHttpOrHttps() { return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { var isHttpOrHttps = mockControl.createMethodMock( @@ -368,6 +386,33 @@ function testBaseRecaptchaVerifier_noHttpOrHttps() { } +function testBaseRecaptchaVerifier_worker() { + return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { + // This gets called in some underlying dependencies at various points. + // It is not feasible counting the exact number of calls and the sequence + // they get called. It is better to use property replacer to stub this + // utility. + stubs.replace( + fireauth.util, + 'isWorker', + function() {return true;}); + var isHttpOrHttps = mockControl.createMethodMock( + fireauth.util, 'isHttpOrHttps'); + isHttpOrHttps().$returns(true).$once(); + mockControl.$replayAll(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, + 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + + 'environment.'); + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + return recaptchaVerifier.render().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); + }); +} + + function testBaseRecaptchaVerifier_withSitekey() { return installAndRunTest('testBaseAppVerifier_withSitekey', function() { var options = { diff --git a/packages/auth/test/rpchandler_test.js b/packages/auth/test/rpchandler_test.js index 61683157519..222000cd70e 100644 --- a/packages/auth/test/rpchandler_test.js +++ b/packages/auth/test/rpchandler_test.js @@ -31,6 +31,8 @@ goog.require('fireauth.common.testHelper'); goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.json'); +goog.require('goog.net.CorsXmlHttpFactory'); +goog.require('goog.net.FetchXmlHttpFactory'); goog.require('goog.net.XhrIo'); goog.require('goog.net.XhrLike'); goog.require('goog.object'); @@ -169,6 +171,7 @@ function tearDown() { } finally { mockControl.$tearDown(); } + delete goog.global['self']; } @@ -179,7 +182,7 @@ function testGetApiKey() { function testRpcHandler_XMLHttpRequest_notSupported() { stubs.replace( - fireauth.util, + fireauth.RpcHandler, 'getXMLHttpRequest', function() {return undefined;}); var expectedError = new fireauth.AuthError( @@ -190,6 +193,136 @@ function testRpcHandler_XMLHttpRequest_notSupported() { } +function testRpcHandler_XMLHttpRequest_worker() { + // Test worker environment that FetchXmlHttpFactory is used in initialization + // of goog.net.XhrIo. + // Install mock clock. + clock = new goog.testing.MockClock(true); + // Simulates global self in a worker environment. + goog.global['self'] = {}; + var xhrInstance = mockControl.createStrictMock(goog.net.XhrLike); + var createInstance = mockControl.createMethodMock( + goog.net.FetchXmlHttpFactory.prototype, 'createInstance'); + stubs.reset(); + // Simulate worker environment. + stubs.replace( + fireauth.util, + 'isWorker', + function() {return true;}); + // No XMLHttpRequest available. + stubs.replace( + fireauth.RpcHandler, + 'getXMLHttpRequest', + function() {return undefined;}); + // Confirm RPC handler calls XHR instance from FetchXmlHttpFactory XHR. + createInstance().$returns(xhrInstance); + xhrInstance.open(ignoreArgument, ignoreArgument, ignoreArgument).$once(); + xhrInstance.setRequestHeader(ignoreArgument, ignoreArgument).$once(); + xhrInstance.send(ignoreArgument).$once(); + xhrInstance.abort().$once(); + asyncTestCase.waitForSignals(1); + mockControl.$replayAll(); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Simulate RPC and then timeout. + rpcHandler.fetchProvidersForIdentifier('user@example.com') + .thenCatch(function(error) { + asyncTestCase.signal(); + }); + // Timeout XHR request. + clock.tick(delay * 2); +} + + +function testRpcHandler_XMLHttpRequest_corsBrowser() { + // Test CORS browser environment that CorsXmlHttpFactory is used in + // initialization of goog.net.XhrIo. + // Install mock clock. + clock = new goog.testing.MockClock(true); + var xhrInstance = mockControl.createStrictMock(goog.net.XhrLike); + var createInstance = mockControl.createMethodMock( + goog.net.CorsXmlHttpFactory.prototype, 'createInstance'); + stubs.reset(); + // Non-worker environment. + stubs.replace( + fireauth.util, + 'isWorker', + function() {return false;}); + // CORS supporting browser. + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return true;}); + // Non-native environment. + stubs.replace( + fireauth.util, + 'isNativeEnvironment', + function() {return false;}); + // Confirm RPC handler calls XHR instance from CorsXmlHttpFactory XHR. + createInstance().$returns(xhrInstance); + xhrInstance.open(ignoreArgument, ignoreArgument, ignoreArgument).$once(); + xhrInstance.setRequestHeader(ignoreArgument, ignoreArgument).$once(); + xhrInstance.send(ignoreArgument).$once(); + xhrInstance.abort().$once(); + asyncTestCase.waitForSignals(1); + mockControl.$replayAll(); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Simulate RPC and then timeout. + rpcHandler.fetchProvidersForIdentifier('user@example.com') + .thenCatch(function(error) { + asyncTestCase.signal(); + }); + // Timeout XHR request. + clock.tick(delay * 2); +} + + +function testRpcHandler_XMLHttpRequest_reactNative() { + // Test react-native environment that built-in XMLHttpRequest is used in + // xhrFactory. + // Install mock clock. + clock = new goog.testing.MockClock(true); + var xhrInstance = mockControl.createStrictMock(goog.net.XhrLike); + var xhrConstructor = mockControl.createConstructorMock( + goog.net, 'XhrLike'); + stubs.reset(); + // CORS supporting environment. + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return true;}); + // Return native XMLHttpRequest.. + stubs.replace( + fireauth.RpcHandler, + 'getXMLHttpRequest', + function() {return xhrConstructor;}); + // React-native environment. + stubs.replace( + fireauth.util, + 'isNativeEnvironment', + function() {return true;}); + stubs.replace( + fireauth.util, + 'getEnvironment', + function() {return fireauth.util.Env.REACT_NATIVE;}); + // Confirm RPC handler calls XHR instance from factory XHR. + xhrConstructor().$returns(xhrInstance); + xhrInstance.open(ignoreArgument, ignoreArgument, ignoreArgument).$once(); + xhrInstance.setRequestHeader(ignoreArgument, ignoreArgument).$once(); + xhrInstance.send(ignoreArgument).$once(); + xhrInstance.abort().$once(); + asyncTestCase.waitForSignals(1); + mockControl.$replayAll(); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Simulate RPC and then timeout. + rpcHandler.fetchProvidersForIdentifier('user@example.com') + .thenCatch(function(error) { + asyncTestCase.signal(); + }); + // Timeout XHR request. + clock.tick(delay * 2); +} + + function testRpcHandler_XMLHttpRequest_node() { // Test node environment that Node.js implementation is used in xhrfactory. // Install mock clock. @@ -205,7 +338,7 @@ function testRpcHandler_XMLHttpRequest_node() { // Return mock XHR constructor. In a Node.js environment the polyfill library // would be used. stubs.replace( - fireauth.util, + fireauth.RpcHandler, 'getXMLHttpRequest', function() {return xhrConstructor;}); // Node.js environment. diff --git a/packages/auth/test/utils_test.js b/packages/auth/test/utils_test.js index 2867b3c0df2..dc8d1a11419 100644 --- a/packages/auth/test/utils_test.js +++ b/packages/auth/test/utils_test.js @@ -22,6 +22,7 @@ goog.provide('fireauth.utilTest'); goog.require('fireauth.util'); goog.require('goog.Timer'); +goog.require('goog.dom'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.TestCase'); @@ -94,6 +95,7 @@ var parsedJSON = { 'someOtherKeyName': false } }; +var lastMetaTag; function setUp() { @@ -105,6 +107,10 @@ function tearDown() { mockControl.$tearDown(); angular = undefined; stubs.reset(); + if (lastMetaTag) { + goog.dom.removeNode(lastMetaTag); + lastMetaTag = null; + } } @@ -1795,3 +1801,31 @@ function testPersistsStorageWithIndexedDB() { function() {return true;}); assertFalse(fireauth.util.persistsStorageWithIndexedDB()); } + + +/** + * @return {?HTMLElement} The last meta element if available in the head + * element. + */ +function getLastMetaTag() { + var collection = goog.dom.getElementsByTagName('head'); + if (collection.length) { + var metaTags = goog.dom.getElementsByTagName('meta', collection[0]); + if (metaTags.length > 0) { + return metaTags[metaTags.length - 1]; + } + } + return null; +} + + +function testSetNoReferrer() { + lastMetaTag = getLastMetaTag(); + if (lastMetaTag) { + assertNotEquals('referrer', lastMetaTag.getAttribute('name')); + } + fireauth.util.setNoReferrer(); + lastMetaTag = getLastMetaTag(); + assertEquals('referrer', lastMetaTag.getAttribute('name')); + assertEquals('no-referrer', lastMetaTag.getAttribute('content')); +} From ae564a796732023f85b099bf8aa64bc54e656e3f Mon Sep 17 00:00:00 2001 From: Ti Wang Date: Mon, 12 Mar 2018 17:20:33 -0700 Subject: [PATCH 6/6] appended photo size for google hosted image --- packages/auth/demo/public/script.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/auth/demo/public/script.js b/packages/auth/demo/public/script.js index 55c7bea41e0..c434d29f188 100644 --- a/packages/auth/demo/public/script.js +++ b/packages/auth/demo/public/script.js @@ -153,7 +153,15 @@ function refreshUserData() { $('input.profile-name').val(user.displayName); $('input.photo-url').val(user.photoURL); if (user.photoURL != null) { - $('img.profile-image').attr('src', user.photoURL).show(); + var photoURL = user.photoURL; + // Append size to the photo URL for Google hosted images to avoid requesting + // the image with its original resolution (using more bandwidth than needed) + // when it is going to be presented in smaller size. + if ((photoURL.indexOf('googleusercontent.com') != -1) || + (photoURL.indexOf('ggpht.com') != -1)) { + photoURL = photoURL + '?sz=' + $('img.profile-image').height(); + } + $('img.profile-image').attr('src', photoURL).show(); } else { $('img.profile-image').hide(); }