Skip to content

Add persistence migration tests. #4631

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -1,2 +1,6 @@
<!DOCTYPE html>
<script src="dist/bundle.js"></script>
<!--
type=module makes browers treat it as an ES6 module and enforces strict mode. This helps catch
some errors in the test (e.g. extending frozen objects or accidentally creating global variables).
-->
<script type="module" src="dist/bundle.js"></script>
16 changes: 9 additions & 7 deletions packages-exp/auth-exp/test/integration/webdriver/static/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ import * as persistence from './persistence';
import { initializeApp } from '@firebase/app-exp';
import { getAuth, useAuthEmulator } from '@firebase/auth-exp';

window.core = { ...core };
window.anonymous = { ...anonymous };
window.redirect = { ...redirect };
window.popup = { ...popup };
window.email = { ...email };
window.persistence = { ...persistence };
window.core = core;
window.anonymous = anonymous;
window.redirect = redirect;
window.popup = popup;
window.email = email;
window.persistence = persistence;

window.auth = null;

// The config and emulator URL are injected by the test. The test framework
// calls this function after that injection.
window.startAuth = async () => {
const app = initializeApp(firebaseConfig);
auth = getAuth(app);
const auth = getAuth(app);
useAuthEmulator(auth, emulatorUrl);
window.auth = auth;
};
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
28 changes: 14 additions & 14 deletions packages-exp/auth-exp/test/integration/webdriver/static/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,46 +30,46 @@ import {
// pass data back to the main Node process. Because of that setup, we can't
// return the popup tasks as pending promises as they won't resolve until
// the WebDriver is allowed to do other stuff. Instead, we'll store the
// promises on the window and provide a way to retrieve them later, unblocking
// promises in variables and provide a way to retrieve them later, unblocking
// the WebDriver process.
let popupPromise = null;
let popupCred = null;
let errorCred = null;

export function idpPopup(optProvider) {
const provider = optProvider
? new OAuthProvider(optProvider)
: new GoogleAuthProvider();
window.popup.popupPromise = signInWithPopup(auth, provider);
popupPromise = signInWithPopup(auth, provider);
}

export function idpReauthPopup() {
window.popup.popupPromise = reauthenticateWithPopup(
popupPromise = reauthenticateWithPopup(
auth.currentUser,
new GoogleAuthProvider()
);
}

export function idpLinkPopup() {
window.popup.popupPromise = linkWithPopup(
auth.currentUser,
new GoogleAuthProvider()
);
popupPromise = linkWithPopup(auth.currentUser, new GoogleAuthProvider());
}

export function popupResult() {
return window.popup.popupPromise;
return popupPromise;
}

export async function generateCredentialFromResult() {
const result = await window.popup.popupPromise;
window.popup.popupCred = GoogleAuthProvider.credentialFromResult(result);
return window.popup.popupCred;
const result = await popupPromise;
popupCred = GoogleAuthProvider.credentialFromResult(result);
return popupCred;
}

export async function signInWithPopupCredential() {
return signInWithCredential(auth, window.popup.popupCred);
return signInWithCredential(auth, popupCred);
}

export async function linkWithErrorCredential() {
await linkWithCredential(auth.currentUser, window.popup.errorCred);
await linkWithCredential(auth.currentUser, errorCred);
}

// These below are not technically popup functions but they're helpers for
Expand All @@ -93,7 +93,7 @@ export async function tryToSignInUnverified(email) {
)
);
} catch (e) {
window.popup.errorCred = FacebookAuthProvider.credentialFromError(e);
errorCred = FacebookAuthProvider.credentialFromError(e);
throw e;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import {
signInWithRedirect
} from '@firebase/auth-exp';

let redirectCred = null;
let errorCred = null;

export function idpRedirect(optProvider) {
const provider = optProvider
? new OAuthProvider(optProvider)
Expand All @@ -48,18 +51,16 @@ export function redirectResult() {

export async function generateCredentialFromRedirectResultAndStore() {
const result = await getRedirectResult(auth);
window.redirect.redirectCred = GoogleAuthProvider.credentialFromResult(
result
);
return window.redirect.redirectCred;
redirectCred = GoogleAuthProvider.credentialFromResult(result);
return redirectCred;
}

export async function signInWithRedirectCredential() {
return signInWithCredential(auth, window.redirect.redirectCred);
return signInWithCredential(auth, redirectCred);
}

export async function linkWithErrorCredential() {
await linkWithCredential(auth.currentUser, window.redirect.errorCred);
await linkWithCredential(auth.currentUser, errorCred);
}

// These below are not technically redirect functions but they're helpers for
Expand All @@ -83,7 +84,7 @@ export async function tryToSignInUnverified(email) {
)
);
} catch (e) {
window.redirect.errorCred = FacebookAuthProvider.credentialFromError(e);
errorCred = FacebookAuthProvider.credentialFromError(e);
throw e;
}
}
Loading