Skip to content

Commit 6d6aa3e

Browse files
authored
Add persistence migration tests. (#4631)
* Add persistence tests and fix some issues. * Address review feedback. * Enable strict mode and remove module prop hacks. * Add persistence migration tests. * Use driver.getUserSnapshot(). * Remove unused imports.
1 parent a6d29aa commit 6d6aa3e

File tree

5 files changed

+168
-12
lines changed

5 files changed

+168
-12
lines changed

packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ export async function resetEmulator(): Promise<void> {
7777
await FetchProvider.fetch()(url, { method: 'DELETE' });
7878
}
7979

80+
export async function createAnonAccount(): Promise<{
81+
localId: string;
82+
idToken: string;
83+
refreshToken: string;
84+
}> {
85+
const url = `${getEmulatorUrl()}/identitytoolkit.googleapis.com/v1/accounts:signUp?key=fake-key`;
86+
const response = await (
87+
await FetchProvider.fetch()(url, {
88+
method: 'POST',
89+
body: '{}',
90+
headers: { 'Content-Type': 'application/json' }
91+
})
92+
).json();
93+
return response;
94+
}
95+
8096
function buildEmulatorUrlForPath(endpoint: string): string {
8197
const emulatorBaseUrl = getEmulatorUrl();
8298
const projectId = getAppConfig().projectId;

packages-exp/auth-exp/test/integration/webdriver/persistence.test.ts

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,31 @@
1818
// eslint-disable-next-line import/no-extraneous-dependencies
1919
import { UserCredential } from '@firebase/auth-exp';
2020
import { expect } from 'chai';
21+
import { createAnonAccount } from '../../helpers/integration/emulator_rest_helpers';
2122
import { API_KEY } from '../../helpers/integration/settings';
2223
import { AnonFunction, PersistenceFunction } from './util/functions';
2324
import { browserDescribe } from './util/test_runner';
2425

26+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
27+
async function testPersistedUser() {
28+
const account = await createAnonAccount();
29+
return {
30+
uid: account.localId,
31+
emailVerified: false,
32+
isAnonymous: true,
33+
providerData: [],
34+
stsTokenManager: {
35+
refreshToken: account.refreshToken,
36+
accessToken: account.idToken,
37+
expirationTime: Date.now() + 3600 * 1000
38+
},
39+
createdAt: Date.now().toString(),
40+
lastLoginAt: Date.now().toString()
41+
};
42+
}
43+
2544
browserDescribe('WebDriver persistence test', driver => {
45+
const fullPersistenceKey = `firebase:authUser:${API_KEY}:[DEFAULT]`;
2646
context('default persistence hierarchy (indexedDB > localStorage)', () => {
2747
it('stores user in indexedDB by default', async () => {
2848
const cred: UserCredential = await driver.call(
@@ -39,9 +59,7 @@ browserDescribe('WebDriver persistence test', driver => {
3959
).to.eql({});
4060

4161
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 });
62+
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });
4563

4664
// Persistence should survive a refresh:
4765
await driver.webDriver.navigate().refresh();
@@ -71,9 +89,7 @@ browserDescribe('WebDriver persistence test', driver => {
7189
).to.eql({});
7290

7391
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 });
92+
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });
7793

7894
// Persistence should survive a refresh:
7995
await driver.webDriver.navigate().refresh();
@@ -100,9 +116,7 @@ browserDescribe('WebDriver persistence test', driver => {
100116
).to.eql({});
101117

102118
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 });
119+
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });
106120

107121
// Persistence should survive a refresh:
108122
await driver.webDriver.navigate().refresh();
@@ -139,9 +153,84 @@ browserDescribe('WebDriver persistence test', driver => {
139153
await driver.waitForAuthInit();
140154
expect(await driver.getUserSnapshot()).to.equal(null);
141155
});
142-
});
143156

144-
// TODO: Upgrade tests (e.g. migrate user from localStorage to indexedDB).
157+
it('migrate stored user from localStorage if indexedDB is available', async () => {
158+
const persistedUser = await testPersistedUser();
159+
await driver.webDriver.navigate().refresh();
160+
await driver.call(PersistenceFunction.LOCAL_STORAGE_SET, {
161+
[fullPersistenceKey]: persistedUser
162+
});
163+
await driver.injectConfigAndInitAuth();
164+
await driver.waitForAuthInit();
165+
166+
// User from localStorage should be picked up.
167+
const user = await driver.getUserSnapshot();
168+
expect(user.uid).eql(persistedUser.uid);
169+
170+
// User should be migrated to indexedDB, and the key in localStorage should be deleted.
171+
const snap = await driver.call(PersistenceFunction.INDEXED_DB_SNAP);
172+
expect(snap)
173+
.to.have.property(fullPersistenceKey)
174+
.that.contains({ uid: persistedUser.uid });
175+
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
176+
{}
177+
);
178+
});
179+
180+
it('migrate stored user to localStorage if indexedDB is readonly', async () => {
181+
// Sign in first, which gets persisted in indexedDB.
182+
const cred: UserCredential = await driver.call(
183+
AnonFunction.SIGN_IN_ANONYMOUSLY
184+
);
185+
const uid = cred.user.uid;
186+
187+
await driver.webDriver.navigate().refresh();
188+
await driver.call(PersistenceFunction.MAKE_INDEXED_DB_READONLY);
189+
await driver.injectConfigAndInitAuth();
190+
await driver.waitForAuthInit();
191+
192+
// User from indexedDB should be picked up.
193+
const user = await driver.getUserSnapshot();
194+
expect(user.uid).eql(uid);
195+
196+
// User should be migrated to localStorage, and the key in indexedDB should be deleted.
197+
const snap = await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP);
198+
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });
199+
expect(await driver.call(PersistenceFunction.INDEXED_DB_SNAP)).to.eql({});
200+
});
201+
202+
it('use in-memory and clear all persistences if indexedDB and localStorage are both broken', async () => {
203+
const persistedUser = await testPersistedUser();
204+
await driver.webDriver.navigate().refresh();
205+
await driver.call(PersistenceFunction.LOCAL_STORAGE_SET, {
206+
[fullPersistenceKey]: persistedUser
207+
});
208+
// Simulate browsers that do not support indexedDB.
209+
await driver.webDriver.executeScript('delete window.indexedDB;');
210+
// Simulate browsers denying writes to localStorage (e.g. Safari private browsing).
211+
await driver.webDriver.executeScript(
212+
'Storage.prototype.setItem = () => { throw new Error("setItem disabled for testing"); };'
213+
);
214+
await driver.injectConfigAndInitAuth();
215+
await driver.waitForAuthInit();
216+
217+
// User from localStorage should be picked up.
218+
const user = await driver.getUserSnapshot();
219+
expect(user.uid).eql(persistedUser.uid);
220+
221+
// Both storage should be cleared.
222+
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
223+
{}
224+
);
225+
expect(await driver.call(PersistenceFunction.INDEXED_DB_SNAP)).to.eql({});
226+
227+
// User will be gone (a.k.a. logged out) after refresh.
228+
await driver.webDriver.navigate().refresh();
229+
await driver.injectConfigAndInitAuth();
230+
await driver.waitForAuthInit();
231+
expect(await driver.getUserSnapshot()).to.equal(null);
232+
});
233+
});
145234

146235
// TODO: Compatibility tests (e.g. sign in with JS SDK and should stay logged in with TS SDK).
147236
});

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@ export async function clearPersistence() {
3333
export async function localStorageSnap() {
3434
return dumpStorage(localStorage);
3535
}
36+
export async function localStorageSet(dict) {
37+
setInStorage(localStorage, dict);
38+
}
3639
export async function sessionStorageSnap() {
3740
return dumpStorage(sessionStorage);
3841
}
42+
export async function sessionStorageSet(dict) {
43+
setInStorage(sessionStorage, dict);
44+
}
3945

4046
const DB_OBJECTSTORE_NAME = 'firebaseLocalStorage';
4147

@@ -58,6 +64,21 @@ export async function indexedDBSnap() {
5864
return result;
5965
}
6066

67+
// Mock functions for testing edge cases
68+
export async function makeIndexedDBReadonly() {
69+
IDBObjectStore.prototype.add = IDBObjectStore.prototype.put = () => {
70+
return {
71+
error: 'add/put is disabled for test purposes',
72+
readyState: 'done',
73+
addEventListener(event, listener) {
74+
if (event === 'error') {
75+
void Promise.resolve({}).then(listener);
76+
}
77+
}
78+
};
79+
};
80+
}
81+
6182
function dumpStorage(storage) {
6283
const result = {};
6384
for (let i = 0; i < storage.length; i++) {
@@ -67,6 +88,16 @@ function dumpStorage(storage) {
6788
return result;
6889
}
6990

91+
function setInStorage(storage, dict) {
92+
for (const [key, value] of Object.entries(dict)) {
93+
if (value === undefined) {
94+
storage.removeItem(key);
95+
} else {
96+
storage.setItem(key, JSON.stringify(value));
97+
}
98+
}
99+
}
100+
70101
function dbPromise(dbRequest) {
71102
return new Promise((resolve, reject) => {
72103
dbRequest.addEventListener('success', () => {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ export class AuthDriver {
5252

5353
async stop(): Promise<void> {
5454
authTestServer.stop();
55+
if (process.env.WEBDRIVER_BROWSER_LOGS) {
56+
await this.webDriver
57+
.manage()
58+
.logs()
59+
.get('browser')
60+
.then(
61+
logs => {
62+
for (const { level, message } of logs) {
63+
console.log(level.name, message);
64+
}
65+
},
66+
() =>
67+
console.log(
68+
'Failed to dump browser logs (this is normal for Firefox).'
69+
)
70+
);
71+
}
5572
await this.webDriver.quit();
5673
}
5774

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export enum CoreFunction {
6464
export enum PersistenceFunction {
6565
CLEAR_PERSISTENCE = 'persistence.clearPersistence',
6666
LOCAL_STORAGE_SNAP = 'persistence.localStorageSnap',
67+
LOCAL_STORAGE_SET = 'persistence.localStorageSet',
6768
SESSION_STORAGE_SNAP = 'persistence.sessionStorageSnap',
68-
INDEXED_DB_SNAP = 'persistence.indexedDBSnap'
69+
SESSION_STORAGE_SET = 'persistence.sessionStorageSet',
70+
INDEXED_DB_SNAP = 'persistence.indexedDBSnap',
71+
MAKE_INDEXED_DB_READONLY = 'persistence.makeIndexedDBReadonly'
6972
}

0 commit comments

Comments
 (0)