Skip to content

Commit 2901f05

Browse files
committed
Merge branch 'master' into fei-writetree-ref
2 parents 67b78e7 + a6d29aa commit 2901f05

File tree

72 files changed

+1274
-236
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1274
-236
lines changed

packages-exp/auth-compat-exp/rollup.config.shared.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export function getEs5Builds(additionalTypescriptPlugins = {}) {
7575
extend: true,
7676
name: 'firebase',
7777
globals: {
78-
'@firebase/app-compat': 'firebase'
78+
'@firebase/app-compat': 'firebase',
79+
'@firebase/app': 'firebase.INTERNAL.modularAPIs'
7980
},
8081
/**
8182
* use iife to avoid below error in the old Safari browser
@@ -97,7 +98,7 @@ export function getEs5Builds(additionalTypescriptPlugins = {}) {
9798
}`
9899
},
99100
plugins: [...es5BuildPlugins, uglify()],
100-
external: ['@firebase/app-compat']
101+
external: ['@firebase/app-compat', '@firebase/app']
101102
}
102103
];
103104
}

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import * as sinonChai from 'sinon-chai';
2323
import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth';
2424
import { UserImpl } from '../user/user_impl';
2525
import { _getInstance } from '../util/instantiator';
26-
import { PersistenceInternal, PersistenceType, StorageEventListener } from './';
26+
import {
27+
PersistenceInternal,
28+
PersistenceType,
29+
PersistenceValue,
30+
StorageEventListener
31+
} from './';
2732
import { inMemoryPersistence } from './in_memory';
2833
import { KeyName, PersistenceUserManager } from './persistence_user_manager';
2934

@@ -64,19 +69,78 @@ describe('core/persistence/persistence_user_manager', () => {
6469
expect(manager.persistence).to.eq(_getInstance(inMemoryPersistence));
6570
});
6671

67-
it('searches in order for a user', async () => {
72+
it('chooses the first one available', async () => {
6873
const a = makePersistence();
6974
const b = makePersistence();
7075
const c = makePersistence();
7176
const search = [a.persistence, b.persistence, c.persistence];
7277
const auth = await testAuth();
73-
b.stub._get.returns(Promise.resolve(testUser(auth, 'uid').toJSON()));
78+
a.stub._isAvailable.resolves(false);
79+
a.stub._get.onFirstCall().resolves(testUser(auth, 'uid').toJSON());
80+
b.stub._isAvailable.resolves(true);
7481

7582
const out = await PersistenceUserManager.create(auth, search);
83+
expect(a.stub._isAvailable).to.have.been.calledOnce;
84+
expect(b.stub._isAvailable).to.have.been.calledOnce;
85+
expect(c.stub._isAvailable).to.not.have.been.called;
86+
87+
// a should not be chosen since it is not available (despite having a user).
7688
expect(out.persistence).to.eq(b.persistence);
89+
});
90+
91+
it('searches in order for a user', async () => {
92+
const a = makePersistence();
93+
const b = makePersistence();
94+
const c = makePersistence();
95+
const search = [a.persistence, b.persistence, c.persistence];
96+
const auth = await testAuth();
97+
const user = testUser(auth, 'uid');
98+
a.stub._isAvailable.resolves(true);
99+
a.stub._get.resolves(user.toJSON());
100+
b.stub._get.resolves(testUser(auth, 'wrong-uid').toJSON());
101+
102+
const out = await PersistenceUserManager.create(auth, search);
77103
expect(a.stub._get).to.have.been.calledOnce;
78-
expect(b.stub._get).to.have.been.calledOnce;
104+
expect(b.stub._get).not.to.have.been.called;
79105
expect(c.stub._get).not.to.have.been.called;
106+
107+
expect(out.persistence).to.eq(a.persistence);
108+
expect((await out.getCurrentUser())!.uid).to.eq(user.uid);
109+
});
110+
111+
it('migrate found user to the selected persistence and clear others', async () => {
112+
const a = makePersistence();
113+
const b = makePersistence();
114+
const c = makePersistence();
115+
const search = [a.persistence, b.persistence, c.persistence];
116+
const auth = await testAuth();
117+
const user = testUser(auth, 'uid');
118+
a.stub._isAvailable.resolves(true);
119+
b.stub._get.resolves(user.toJSON());
120+
c.stub._get.resolves(testUser(auth, 'wrong-uid').toJSON());
121+
122+
let persistedUserInA: PersistenceValue | null = null;
123+
a.stub._set.callsFake(async (_, value) => {
124+
persistedUserInA = value;
125+
});
126+
a.stub._get.callsFake(async () => persistedUserInA);
127+
128+
const out = await PersistenceUserManager.create(auth, search);
129+
expect(a.stub._set).to.have.been.calledOnceWith(
130+
'firebase:authUser:test-api-key:test-app',
131+
user.toJSON()
132+
);
133+
expect(b.stub._set).to.not.have.been.called;
134+
expect(c.stub._set).to.not.have.been.called;
135+
expect(b.stub._remove).to.have.been.calledOnceWith(
136+
'firebase:authUser:test-api-key:test-app'
137+
);
138+
expect(c.stub._remove).to.have.been.calledOnceWith(
139+
'firebase:authUser:test-api-key:test-app'
140+
);
141+
142+
expect(out.persistence).to.eq(a.persistence);
143+
expect((await out.getCurrentUser())!.uid).to.eq(user.uid);
80144
});
81145

82146
it('uses default user key if none provided', async () => {
@@ -99,13 +163,17 @@ describe('core/persistence/persistence_user_manager', () => {
99163
);
100164
});
101165

102-
it('returns zeroth persistence if all else fails', async () => {
166+
it('returns in-memory persistence if all else fails', async () => {
103167
const a = makePersistence();
104168
const b = makePersistence();
105169
const c = makePersistence();
106170
const search = [a.persistence, b.persistence, c.persistence];
171+
a.stub._isAvailable.resolves(false);
172+
b.stub._isAvailable.resolves(false);
173+
c.stub._isAvailable.resolves(false);
174+
107175
const out = await PersistenceUserManager.create(auth, search);
108-
expect(out.persistence).to.eq(a.persistence);
176+
expect(out.persistence).to.eq(_getInstance(inMemoryPersistence));
109177
expect(a.stub._get).to.have.been.calledOnce;
110178
expect(b.stub._get).to.have.been.calledOnce;
111179
expect(c.stub._get).to.have.been.called;
@@ -118,6 +186,7 @@ describe('core/persistence/persistence_user_manager', () => {
118186

119187
beforeEach(async () => {
120188
const { persistence, stub } = makePersistence(PersistenceType.SESSION);
189+
stub._isAvailable.resolves(true);
121190
persistenceStub = stub;
122191
manager = await PersistenceUserManager.create(auth, [persistence]);
123192
});

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

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,52 @@ 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 (or fallback to memory).
117+
let chosenPersistence = _getInstance<PersistenceInternal>(
118+
inMemoryPersistence
119+
);
117120
for (const persistence of persistenceHierarchy) {
118-
if (await persistence._get(key)) {
119-
return new PersistenceUserManager(persistence, auth, userKey);
121+
if (await persistence._isAvailable()) {
122+
chosenPersistence = persistence;
123+
break;
120124
}
121125
}
122126

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

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);
151+
// Attempt to clear the key in other persistences but ignore errors. This helps prevent issues
152+
// such as users getting stuck with a previous account after signing out and refreshing the tab.
153+
await Promise.all(
154+
persistenceHierarchy.map(async persistence => {
155+
if (persistence !== chosenPersistence) {
156+
try {
157+
await persistence._remove(key);
158+
} catch {}
159+
}
160+
})
161+
);
162+
return new PersistenceUserManager(chosenPersistence, auth, userKey);
130163
}
131164
}

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 {

packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_loader.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ describe('platform_browser/recaptcha/recaptcha_loader', () => {
6767
triggerNetworkTimeout = stubSingleTimeout(networkTimeoutId);
6868

6969
sinon.stub(jsHelpers, '_loadJS').callsFake(() => {
70-
return new Promise((resolve, reject) => {
70+
return (new Promise<void>((resolve, reject) => {
7171
jsLoader = { resolve, reject };
72-
});
72+
}) as unknown) as Promise<Event>;
7373
});
7474

7575
loader = new ReCaptchaLoaderImpl();
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+
});

0 commit comments

Comments
 (0)