Skip to content

Commit f6d12d2

Browse files
committed
Add persistence tests and fix some issues.
1 parent 44759f7 commit f6d12d2

File tree

8 files changed

+286
-20
lines changed

8 files changed

+286
-20
lines changed

packages-exp/auth-exp/src/core/persistence/persistence_user_manager.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,53 @@ export class PersistenceUserManager {
113113
);
114114
}
115115

116-
const key = _persistenceKeyName(userKey, auth.config.apiKey, auth.name);
116+
// Use the first persistence that supports a full read-write roundtrip (._isAvailable()).
117+
let chosenPersistence: PersistenceInternal | null = null;
117118
for (const persistence of persistenceHierarchy) {
118-
if (await persistence._get(key)) {
119-
return new PersistenceUserManager(persistence, auth, userKey);
119+
if (await persistence._isAvailable()) {
120+
chosenPersistence = persistence;
121+
break;
120122
}
121123
}
124+
chosenPersistence =
125+
chosenPersistence ||
126+
_getInstance<PersistenceInternal>(inMemoryPersistence);
122127

123-
// Check all the available storage options.
124-
// TODO: Migrate from local storage to indexedDB
125-
// TODO: Clear other forms once one is found
128+
// However, attempt to migrate users stored in other persistences (in the hierarchy order).
129+
let userToMigrate: UserInternal | null = null;
130+
const key = _persistenceKeyName(userKey, auth.config.apiKey, auth.name);
131+
for (const persistence of persistenceHierarchy) {
132+
// We attempt to call _get without checking _isAvailable since here we don't care if the full
133+
// round-trip (read+write) is supported. We'll take the first one that we can read or give up.
134+
try {
135+
const blob = await persistence._get<PersistedBlob>(key); // throws if unsupported
136+
if (blob) {
137+
const user = UserImpl._fromJSON(auth, blob); // throws for unparsable blob (wrong format)
138+
if (persistence !== chosenPersistence) {
139+
userToMigrate = user;
140+
}
141+
break;
142+
}
143+
} catch {}
144+
}
126145

127-
// All else failed, fall back to zeroth persistence
128-
// TODO: Modify this to support non-browser devices
129-
return new PersistenceUserManager(persistenceHierarchy[0], auth, userKey);
146+
if (userToMigrate) {
147+
// This normally shouldn't throw since chosenPersistence.isAvailable() is true, but if it does
148+
// we'll just let it bubble to surface the error.
149+
await chosenPersistence._set(key, userToMigrate.toJSON());
150+
}
151+
152+
// Attempt to clear the key in other persistences but ignore errors. This helps prevent issues
153+
// such as users getting stuck with a previous account after signing out and refreshing the tab.
154+
await Promise.all(
155+
persistenceHierarchy.map(async persistence => {
156+
if (persistence !== chosenPersistence) {
157+
try {
158+
await persistence._remove(key);
159+
} catch {}
160+
}
161+
})
162+
);
163+
return new PersistenceUserManager(chosenPersistence, auth, userKey);
130164
}
131165
}

packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class BrowserLocalPersistence
4949
static type: 'LOCAL' = 'LOCAL';
5050

5151
constructor() {
52-
super(localStorage, PersistenceType.LOCAL);
52+
super(window.localStorage, PersistenceType.LOCAL);
5353
this.boundEventHandler = this.onStorageEvent.bind(this);
5454
}
5555

packages-exp/auth-exp/src/platform_browser/persistence/session_storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class BrowserSessionPersistence
3030
static type: 'SESSION' = 'SESSION';
3131

3232
constructor() {
33-
super(sessionStorage, PersistenceType.SESSION);
33+
super(window.sessionStorage, PersistenceType.SESSION);
3434
}
3535

3636
_addListener(_key: string, _listener: StorageEventListener): void {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// eslint-disable-next-line import/no-extraneous-dependencies
19+
import { UserCredential } from '@firebase/auth-exp';
20+
import { expect } from 'chai';
21+
import { API_KEY } from '../../helpers/integration/settings';
22+
import { AnonFunction, PersistenceFunction } from './util/functions';
23+
import { browserDescribe } from './util/test_runner';
24+
25+
browserDescribe('WebDriver persistence test', driver => {
26+
context('default persistence hierarchy (indexedDB > localStorage)', () => {
27+
it('stores user in indexedDB by default', async () => {
28+
const cred: UserCredential = await driver.call(
29+
AnonFunction.SIGN_IN_ANONYMOUSLY
30+
);
31+
const uid = cred.user.uid;
32+
33+
expect(await driver.getUserSnapshot()).to.eql(cred.user);
34+
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
35+
{}
36+
);
37+
expect(
38+
await driver.call(PersistenceFunction.SESSION_STORAGE_SNAP)
39+
).to.eql({});
40+
41+
const snap = await driver.call(PersistenceFunction.INDEXED_DB_SNAP);
42+
expect(snap)
43+
.to.have.property(`firebase:authUser:${API_KEY}:[DEFAULT]`)
44+
.that.contains({ uid });
45+
46+
// Persistence should survive a refresh:
47+
await driver.webDriver.navigate().refresh();
48+
await driver.injectConfigAndInitAuth();
49+
await driver.waitForAuthInit();
50+
expect(await driver.getUserSnapshot()).to.contain({ uid });
51+
});
52+
53+
it('should work fine if indexedDB is available while localStorage is not', async () => {
54+
await driver.webDriver.navigate().refresh();
55+
// Simulate browsers that do not support localStorage.
56+
await driver.webDriver.executeScript('delete window.localStorage;');
57+
await driver.injectConfigAndInitAuth();
58+
await driver.waitForAuthInit();
59+
60+
const cred: UserCredential = await driver.call(
61+
AnonFunction.SIGN_IN_ANONYMOUSLY
62+
);
63+
const uid = cred.user.uid;
64+
65+
expect(await driver.getUserSnapshot()).to.eql(cred.user);
66+
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
67+
{}
68+
);
69+
expect(
70+
await driver.call(PersistenceFunction.SESSION_STORAGE_SNAP)
71+
).to.eql({});
72+
73+
const snap = await driver.call(PersistenceFunction.INDEXED_DB_SNAP);
74+
expect(snap)
75+
.to.have.property(`firebase:authUser:${API_KEY}:[DEFAULT]`)
76+
.that.contains({ uid });
77+
78+
// Persistence should survive a refresh:
79+
await driver.webDriver.navigate().refresh();
80+
await driver.injectConfigAndInitAuth();
81+
await driver.waitForAuthInit();
82+
expect(await driver.getUserSnapshot()).to.contain({ uid });
83+
});
84+
85+
it('stores user in localStorage if indexedDB is not available', async () => {
86+
await driver.webDriver.navigate().refresh();
87+
// Simulate browsers that do not support indexedDB.
88+
await driver.webDriver.executeScript('delete window.indexedDB;');
89+
await driver.injectConfigAndInitAuth();
90+
await driver.waitForAuthInit();
91+
92+
const cred: UserCredential = await driver.call(
93+
AnonFunction.SIGN_IN_ANONYMOUSLY
94+
);
95+
const uid = cred.user.uid;
96+
97+
expect(await driver.getUserSnapshot()).to.eql(cred.user);
98+
expect(
99+
await driver.call(PersistenceFunction.SESSION_STORAGE_SNAP)
100+
).to.eql({});
101+
102+
const snap = await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP);
103+
expect(snap)
104+
.to.have.property(`firebase:authUser:${API_KEY}:[DEFAULT]`)
105+
.that.contains({ uid });
106+
107+
// Persistence should survive a refresh:
108+
await driver.webDriver.navigate().refresh();
109+
await driver.injectConfigAndInitAuth();
110+
await driver.waitForAuthInit();
111+
expect(await driver.getUserSnapshot()).to.contain({ uid });
112+
});
113+
114+
it('fall back to in-memory if neither indexedDB or localStorage is present', async () => {
115+
await driver.webDriver.navigate().refresh();
116+
// Simulate browsers that do not support indexedDB or localStorage.
117+
await driver.webDriver.executeScript(
118+
'delete window.indexedDB; delete window.localStorage;'
119+
);
120+
await driver.injectConfigAndInitAuth();
121+
await driver.waitForAuthInit();
122+
123+
const cred: UserCredential = await driver.call(
124+
AnonFunction.SIGN_IN_ANONYMOUSLY
125+
);
126+
127+
expect(await driver.getUserSnapshot()).to.eql(cred.user);
128+
expect(
129+
await driver.call(PersistenceFunction.SESSION_STORAGE_SNAP)
130+
).to.eql({});
131+
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
132+
{}
133+
);
134+
expect(await driver.call(PersistenceFunction.INDEXED_DB_SNAP)).to.eql({});
135+
136+
// User will be gone (a.k.a. logged out) after refresh.
137+
await driver.webDriver.navigate().refresh();
138+
await driver.injectConfigAndInitAuth();
139+
await driver.waitForAuthInit();
140+
expect(await driver.getUserSnapshot()).to.equal(null);
141+
});
142+
});
143+
144+
// TODO: Upgrade tests (e.g. migrate user from localStorage to indexedDB).
145+
146+
// TODO: Compatibility tests (e.g. sign in with JS SDK and should stay logged in with TS SDK).
147+
});

packages-exp/auth-exp/test/integration/webdriver/static/core.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,10 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
import { clearPersistence } from './persistence';
1718

1819
export function reset() {
19-
sessionStorage.clear();
20-
localStorage.clear();
21-
const del = indexedDB.deleteDatabase('firebaseLocalStorageDb');
22-
23-
return new Promise(resolve => {
24-
del.addEventListener('success', () => resolve());
25-
del.addEventListener('error', () => resolve());
26-
del.addEventListener('blocked', () => resolve());
27-
});
20+
return clearPersistence();
2821
}
2922

3023
export function authInit() {

packages-exp/auth-exp/test/integration/webdriver/static/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
import * as redirect from './redirect';
1919
import * as anonymous from './anonymous';
2020
import * as core from './core';
21+
import * as persistence from './persistence';
2122
import { initializeApp } from '@firebase/app-exp';
2223
import { getAuth, useAuthEmulator } from '@firebase/auth-exp';
2324

2425
window.core = core;
2526
window.anonymous = anonymous;
2627
window.redirect = redirect;
28+
window.persistence = persistence;
2729

2830
// The config and emulator URL are injected by the test. The test framework
2931
// calls this function after that injection.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
const INDEXED_DB_NAME = 'firebaseLocalStorageDb';
19+
20+
// Save these variables for test utils use below, since some tests may delete them.
21+
const indexedDB = window.indexedDB;
22+
const localStorage = window.localStorage;
23+
const sessionStorage = window.sessionStorage;
24+
25+
export async function clearPersistence() {
26+
sessionStorage.clear();
27+
localStorage.clear();
28+
return dbPromise(indexedDB.deleteDatabase(INDEXED_DB_NAME)).catch(
29+
() => undefined
30+
);
31+
}
32+
33+
export async function localStorageSnap() {
34+
return dumpStorage(localStorage);
35+
}
36+
export async function sessionStorageSnap() {
37+
return dumpStorage(sessionStorage);
38+
}
39+
40+
const DB_OBJECTSTORE_NAME = 'firebaseLocalStorage';
41+
42+
export async function indexedDBSnap() {
43+
const db = await dbPromise(indexedDB.open(INDEXED_DB_NAME));
44+
let entries;
45+
try {
46+
const store = db
47+
.transaction([DB_OBJECTSTORE_NAME], 'readonly')
48+
.objectStore(DB_OBJECTSTORE_NAME);
49+
entries = await dbPromise(store.getAll());
50+
} catch {
51+
// May throw if DB_OBJECTSTORE_NAME is never created -- this is normal.
52+
return {};
53+
}
54+
const result = {};
55+
for (const { fbase_key: key, value } of entries) {
56+
result[key] = value;
57+
}
58+
return result;
59+
}
60+
61+
function dumpStorage(storage) {
62+
const result = {};
63+
for (let i = 0; i < storage.length; i++) {
64+
const key = storage.key(i);
65+
result[key] = JSON.parse(storage.getItem(key));
66+
}
67+
return result;
68+
}
69+
70+
function dbPromise(dbRequest) {
71+
return new Promise((resolve, reject) => {
72+
dbRequest.addEventListener('success', () => {
73+
resolve(dbRequest.result);
74+
});
75+
dbRequest.addEventListener('error', () => {
76+
reject(dbRequest.error);
77+
});
78+
dbRequest.addEventListener('blocked', () => {
79+
reject(dbRequest.error || 'blocked');
80+
});
81+
});
82+
}

packages-exp/auth-exp/test/integration/webdriver/util/functions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,11 @@ export enum CoreFunction {
4141
AUTH_SNAPSHOT = 'core.authSnap',
4242
SIGN_OUT = 'core.signOut'
4343
}
44+
45+
/** Available persistence functions within the browser. See static/persistence.js */
46+
export enum PersistenceFunction {
47+
CLEAR_PERSISTENCE = 'persistence.clearPersistence',
48+
LOCAL_STORAGE_SNAP = 'persistence.localStorageSnap',
49+
SESSION_STORAGE_SNAP = 'persistence.sessionStorageSnap',
50+
INDEXED_DB_SNAP = 'persistence.indexedDBSnap'
51+
}

0 commit comments

Comments
 (0)