Skip to content

Commit

Permalink
Add persistence migration tests. (#4631)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
yuchenshi authored Mar 16, 2021
1 parent a6d29aa commit 6d6aa3e
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ export async function resetEmulator(): Promise<void> {
await FetchProvider.fetch()(url, { method: 'DELETE' });
}

export async function createAnonAccount(): Promise<{
localId: string;
idToken: string;
refreshToken: string;
}> {
const url = `${getEmulatorUrl()}/identitytoolkit.googleapis.com/v1/accounts:signUp?key=fake-key`;
const response = await (
await FetchProvider.fetch()(url, {
method: 'POST',
body: '{}',
headers: { 'Content-Type': 'application/json' }
})
).json();
return response;
}

function buildEmulatorUrlForPath(endpoint: string): string {
const emulatorBaseUrl = getEmulatorUrl();
const projectId = getAppConfig().projectId;
Expand Down
111 changes: 100 additions & 11 deletions packages-exp/auth-exp/test/integration/webdriver/persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,31 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { UserCredential } from '@firebase/auth-exp';
import { expect } from 'chai';
import { createAnonAccount } from '../../helpers/integration/emulator_rest_helpers';
import { API_KEY } from '../../helpers/integration/settings';
import { AnonFunction, PersistenceFunction } from './util/functions';
import { browserDescribe } from './util/test_runner';

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async function testPersistedUser() {
const account = await createAnonAccount();
return {
uid: account.localId,
emailVerified: false,
isAnonymous: true,
providerData: [],
stsTokenManager: {
refreshToken: account.refreshToken,
accessToken: account.idToken,
expirationTime: Date.now() + 3600 * 1000
},
createdAt: Date.now().toString(),
lastLoginAt: Date.now().toString()
};
}

browserDescribe('WebDriver persistence test', driver => {
const fullPersistenceKey = `firebase:authUser:${API_KEY}:[DEFAULT]`;
context('default persistence hierarchy (indexedDB > localStorage)', () => {
it('stores user in indexedDB by default', async () => {
const cred: UserCredential = await driver.call(
Expand All @@ -39,9 +59,7 @@ browserDescribe('WebDriver persistence test', driver => {
).to.eql({});

const snap = await driver.call(PersistenceFunction.INDEXED_DB_SNAP);
expect(snap)
.to.have.property(`firebase:authUser:${API_KEY}:[DEFAULT]`)
.that.contains({ uid });
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });

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

const snap = await driver.call(PersistenceFunction.INDEXED_DB_SNAP);
expect(snap)
.to.have.property(`firebase:authUser:${API_KEY}:[DEFAULT]`)
.that.contains({ uid });
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });

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

const snap = await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP);
expect(snap)
.to.have.property(`firebase:authUser:${API_KEY}:[DEFAULT]`)
.that.contains({ uid });
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });

// Persistence should survive a refresh:
await driver.webDriver.navigate().refresh();
Expand Down Expand Up @@ -139,9 +153,84 @@ browserDescribe('WebDriver persistence test', driver => {
await driver.waitForAuthInit();
expect(await driver.getUserSnapshot()).to.equal(null);
});
});

// TODO: Upgrade tests (e.g. migrate user from localStorage to indexedDB).
it('migrate stored user from localStorage if indexedDB is available', async () => {
const persistedUser = await testPersistedUser();
await driver.webDriver.navigate().refresh();
await driver.call(PersistenceFunction.LOCAL_STORAGE_SET, {
[fullPersistenceKey]: persistedUser
});
await driver.injectConfigAndInitAuth();
await driver.waitForAuthInit();

// User from localStorage should be picked up.
const user = await driver.getUserSnapshot();
expect(user.uid).eql(persistedUser.uid);

// User should be migrated to indexedDB, and the key in localStorage should be deleted.
const snap = await driver.call(PersistenceFunction.INDEXED_DB_SNAP);
expect(snap)
.to.have.property(fullPersistenceKey)
.that.contains({ uid: persistedUser.uid });
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
{}
);
});

it('migrate stored user to localStorage if indexedDB is readonly', async () => {
// Sign in first, which gets persisted in indexedDB.
const cred: UserCredential = await driver.call(
AnonFunction.SIGN_IN_ANONYMOUSLY
);
const uid = cred.user.uid;

await driver.webDriver.navigate().refresh();
await driver.call(PersistenceFunction.MAKE_INDEXED_DB_READONLY);
await driver.injectConfigAndInitAuth();
await driver.waitForAuthInit();

// User from indexedDB should be picked up.
const user = await driver.getUserSnapshot();
expect(user.uid).eql(uid);

// User should be migrated to localStorage, and the key in indexedDB should be deleted.
const snap = await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP);
expect(snap).to.have.property(fullPersistenceKey).that.contains({ uid });
expect(await driver.call(PersistenceFunction.INDEXED_DB_SNAP)).to.eql({});
});

it('use in-memory and clear all persistences if indexedDB and localStorage are both broken', async () => {
const persistedUser = await testPersistedUser();
await driver.webDriver.navigate().refresh();
await driver.call(PersistenceFunction.LOCAL_STORAGE_SET, {
[fullPersistenceKey]: persistedUser
});
// Simulate browsers that do not support indexedDB.
await driver.webDriver.executeScript('delete window.indexedDB;');
// Simulate browsers denying writes to localStorage (e.g. Safari private browsing).
await driver.webDriver.executeScript(
'Storage.prototype.setItem = () => { throw new Error("setItem disabled for testing"); };'
);
await driver.injectConfigAndInitAuth();
await driver.waitForAuthInit();

// User from localStorage should be picked up.
const user = await driver.getUserSnapshot();
expect(user.uid).eql(persistedUser.uid);

// Both storage should be cleared.
expect(await driver.call(PersistenceFunction.LOCAL_STORAGE_SNAP)).to.eql(
{}
);
expect(await driver.call(PersistenceFunction.INDEXED_DB_SNAP)).to.eql({});

// User will be gone (a.k.a. logged out) after refresh.
await driver.webDriver.navigate().refresh();
await driver.injectConfigAndInitAuth();
await driver.waitForAuthInit();
expect(await driver.getUserSnapshot()).to.equal(null);
});
});

// TODO: Compatibility tests (e.g. sign in with JS SDK and should stay logged in with TS SDK).
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ export async function clearPersistence() {
export async function localStorageSnap() {
return dumpStorage(localStorage);
}
export async function localStorageSet(dict) {
setInStorage(localStorage, dict);
}
export async function sessionStorageSnap() {
return dumpStorage(sessionStorage);
}
export async function sessionStorageSet(dict) {
setInStorage(sessionStorage, dict);
}

const DB_OBJECTSTORE_NAME = 'firebaseLocalStorage';

Expand All @@ -58,6 +64,21 @@ export async function indexedDBSnap() {
return result;
}

// Mock functions for testing edge cases
export async function makeIndexedDBReadonly() {
IDBObjectStore.prototype.add = IDBObjectStore.prototype.put = () => {
return {
error: 'add/put is disabled for test purposes',
readyState: 'done',
addEventListener(event, listener) {
if (event === 'error') {
void Promise.resolve({}).then(listener);
}
}
};
};
}

function dumpStorage(storage) {
const result = {};
for (let i = 0; i < storage.length; i++) {
Expand All @@ -67,6 +88,16 @@ function dumpStorage(storage) {
return result;
}

function setInStorage(storage, dict) {
for (const [key, value] of Object.entries(dict)) {
if (value === undefined) {
storage.removeItem(key);
} else {
storage.setItem(key, JSON.stringify(value));
}
}
}

function dbPromise(dbRequest) {
return new Promise((resolve, reject) => {
dbRequest.addEventListener('success', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ export class AuthDriver {

async stop(): Promise<void> {
authTestServer.stop();
if (process.env.WEBDRIVER_BROWSER_LOGS) {
await this.webDriver
.manage()
.logs()
.get('browser')
.then(
logs => {
for (const { level, message } of logs) {
console.log(level.name, message);
}
},
() =>
console.log(
'Failed to dump browser logs (this is normal for Firefox).'
)
);
}
await this.webDriver.quit();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export enum CoreFunction {
export enum PersistenceFunction {
CLEAR_PERSISTENCE = 'persistence.clearPersistence',
LOCAL_STORAGE_SNAP = 'persistence.localStorageSnap',
LOCAL_STORAGE_SET = 'persistence.localStorageSet',
SESSION_STORAGE_SNAP = 'persistence.sessionStorageSnap',
INDEXED_DB_SNAP = 'persistence.indexedDBSnap'
SESSION_STORAGE_SET = 'persistence.sessionStorageSet',
INDEXED_DB_SNAP = 'persistence.indexedDBSnap',
MAKE_INDEXED_DB_READONLY = 'persistence.makeIndexedDBReadonly'
}

0 comments on commit 6d6aa3e

Please sign in to comment.