Skip to content

Commit a08aacb

Browse files
authored
Merge 6e22793 into cb58a66
2 parents cb58a66 + 6e22793 commit a08aacb

File tree

11 files changed

+442
-21
lines changed

11 files changed

+442
-21
lines changed

packages-exp/auth-exp/src/core/auth/auth_impl.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,13 @@ describe('core/auth/auth_impl', () => {
208208
});
209209

210210
it('onAuthStateChange does not trigger for user props change', async () => {
211-
user.refreshToken = 'hey look I changed';
211+
user.photoURL = 'blah';
212212
await auth.updateCurrentUser(user);
213213
expect(authStateCallback).not.to.have.been.called;
214214
});
215215

216216
it('onIdTokenChange triggers for user props change', async () => {
217-
user.refreshToken = 'hey look I changed';
217+
user.photoURL = 'hey look I changed';
218218
await auth.updateCurrentUser(user);
219219
expect(idTokenCallback).to.have.been.calledWith(user);
220220
});

packages-exp/auth-exp/src/core/auth/auth_impl.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,23 @@ export class AuthImpl implements Auth {
8181
}
8282

8383
this._isInitialized = true;
84-
this._notifyStateListeners();
84+
this.notifyAuthListeners();
8585
});
8686
}
8787

8888
useDeviceLanguage(): void {
8989
throw new Error('Method not implemented.');
9090
}
9191

92-
updateCurrentUser(user: User | null): Promise<void> {
93-
return this.queue(() => this.directlySetCurrentUser(user));
92+
async updateCurrentUser(user: User | null): Promise<void> {
93+
return this.queue(async () => {
94+
await this.directlySetCurrentUser(user);
95+
this.notifyAuthListeners();
96+
});
9497
}
9598

96-
signOut(): Promise<void> {
97-
return this.queue(() => this.directlySetCurrentUser(null));
99+
async signOut(): Promise<void> {
100+
return this.updateCurrentUser(null);
98101
}
99102

100103
setPersistence(persistence: Persistence): Promise<void> {
@@ -129,7 +132,20 @@ export class AuthImpl implements Auth {
129132
);
130133
}
131134

132-
_notifyStateListeners(): void {
135+
async _persistUserIfCurrent(user: User): Promise<void> {
136+
if (user === this.currentUser) {
137+
return this.queue(async () => this.directlySetCurrentUser(user));
138+
}
139+
}
140+
141+
/** Notifies listeners only if the user is current */
142+
_notifyListenersIfCurrent(user: User): void {
143+
if (user === this.currentUser) {
144+
this.notifyAuthListeners();
145+
}
146+
}
147+
148+
private notifyAuthListeners(): void {
133149
if (!this._isInitialized) {
134150
return;
135151
}
@@ -178,8 +194,6 @@ export class AuthImpl implements Auth {
178194
} else {
179195
await this.assertedPersistence.removeCurrentUser();
180196
}
181-
182-
this._notifyStateListeners();
183197
}
184198

185199
private queue(action: AsyncAction): Promise<void> {

packages-exp/auth-exp/src/core/strategies/credential.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515
* limitations under the License.
1616
*/
1717

18+
import * as externs from '@firebase/auth-types-exp';
1819
import { OperationType, UserCredential } from '@firebase/auth-types-exp';
20+
1921
import { Auth } from '../../model/auth';
2022
import { AuthCredential } from '../../model/auth_credential';
2123
import { User } from '../../model/user';
2224
import { UserCredentialImpl } from '../user/user_credential_impl';
2325

2426
export async function signInWithCredential(
25-
auth: Auth,
26-
credential: AuthCredential
27+
authExtern: externs.Auth,
28+
credentialExtern: externs.AuthCredential
2729
): Promise<UserCredential> {
30+
const auth = authExtern as Auth;
31+
const credential = credentialExtern as AuthCredential;
2832
// TODO: handle mfa by wrapping with callApiWithMfaContext
2933
const response = await credential._getIdTokenResponse(auth);
3034
const userCredential = await UserCredentialImpl._fromIdTokenResponse(
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
import * as sinonChai from 'sinon-chai';
22+
23+
import { ProviderId, UserInfo } from '@firebase/auth-types-exp';
24+
25+
import { mockEndpoint } from '../../../test/api/helper';
26+
import { TestAuth, testAuth, testUser } from '../../../test/mock_auth';
27+
import * as fetch from '../../../test/mock_fetch';
28+
import { Endpoint } from '../../api';
29+
import { User } from '../../model/user';
30+
import { updateEmail, updatePassword, updateProfile } from './account_info';
31+
32+
use(chaiAsPromised);
33+
use(sinonChai);
34+
35+
const PASSWORD_PROVIDER: UserInfo = {
36+
providerId: ProviderId.PASSWORD,
37+
uid: 'uid',
38+
email: 'email',
39+
displayName: 'old-name',
40+
phoneNumber: 'phone-number',
41+
photoURL: 'old-url'
42+
};
43+
44+
describe('core/user/profile', () => {
45+
let user: User;
46+
let auth: TestAuth;
47+
48+
beforeEach(async () => {
49+
auth = await testAuth();
50+
user = testUser(auth, 'uid', '', true);
51+
fetch.setUp();
52+
});
53+
54+
afterEach(() => {
55+
sinon.restore();
56+
fetch.tearDown();
57+
});
58+
59+
describe('#updateProfile', () => {
60+
it('returns immediately if profile object is empty', async () => {
61+
const ep = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
62+
await updateProfile(user, {});
63+
expect(ep.calls).to.be.empty;
64+
});
65+
66+
it('calls the setAccountInfo endpoint', async () => {
67+
const ep = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
68+
69+
await updateProfile(user, {
70+
displayName: 'displayname',
71+
photoURL: 'photo'
72+
});
73+
expect(ep.calls[0].request).to.eql({
74+
idToken: 'access-token',
75+
displayName: 'displayname',
76+
photoUrl: 'photo'
77+
});
78+
});
79+
80+
it('sets the fields on the user based on the response', async () => {
81+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
82+
displayName: 'response-name',
83+
photoUrl: 'response-photo'
84+
});
85+
86+
await updateProfile(user, {
87+
displayName: 'displayname',
88+
photoURL: 'photo'
89+
});
90+
expect(user.displayName).to.eq('response-name');
91+
expect(user.photoURL).to.eq('response-photo');
92+
});
93+
94+
it('sets the fields on the password provider', async () => {
95+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
96+
displayName: 'response-name',
97+
photoUrl: 'response-photo'
98+
});
99+
user.providerData = [{ ...PASSWORD_PROVIDER }];
100+
101+
await updateProfile(user, {
102+
displayName: 'displayname',
103+
photoURL: 'photo'
104+
});
105+
const provider = user.providerData[0];
106+
expect(provider.displayName).to.eq('response-name');
107+
expect(provider.photoURL).to.eq('response-photo');
108+
});
109+
});
110+
111+
describe('#updateEmail', () => {
112+
it('calls the setAccountInfo endpoint and reloads the user', async () => {
113+
const set = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
114+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
115+
users: [{ localId: 'new-uid-to-prove-refresh-got-called' }]
116+
});
117+
118+
await updateEmail(user, '[email protected]');
119+
expect(set.calls[0].request).to.eql({
120+
idToken: 'access-token',
121+
122+
});
123+
124+
expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called');
125+
});
126+
});
127+
128+
describe('#updatePassword', () => {
129+
it('calls the setAccountInfo endpoint and reloads the user', async () => {
130+
const set = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
131+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
132+
users: [{ localId: 'new-uid-to-prove-refresh-got-called' }]
133+
});
134+
135+
await updatePassword(user, 'pass');
136+
expect(set.calls[0].request).to.eql({
137+
idToken: 'access-token',
138+
password: 'pass'
139+
});
140+
141+
expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called');
142+
});
143+
});
144+
145+
describe('notifications', () => {
146+
let idTokenChange: sinon.SinonStub;
147+
148+
beforeEach(async () => {
149+
idTokenChange = sinon.stub();
150+
auth.onIdTokenChanged(idTokenChange);
151+
152+
// Flush token change promises which are floating
153+
await auth.updateCurrentUser(user);
154+
auth._isInitialized = true;
155+
idTokenChange.resetHistory();
156+
});
157+
158+
describe('#updateProfile', () => {
159+
it('triggers a token update if necessary', async () => {
160+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
161+
idToken: 'new-id-token',
162+
refreshToken: 'new-refresh-token',
163+
expiresIn: 300
164+
});
165+
166+
await updateProfile(user, { displayName: 'd' });
167+
expect(idTokenChange).to.have.been.called;
168+
expect(auth.persistenceLayer.lastObjectSet).to.eql(
169+
user.toPlainObject()
170+
);
171+
});
172+
173+
it('does NOT trigger a token update if unnecessary', async () => {
174+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
175+
idToken: 'access-token',
176+
refreshToken: 'refresh-token',
177+
expiresIn: 300
178+
});
179+
180+
await updateProfile(user, { displayName: 'd' });
181+
expect(idTokenChange).not.to.have.been.called;
182+
expect(auth.persistenceLayer.lastObjectSet).to.eql(
183+
user.toPlainObject()
184+
);
185+
});
186+
});
187+
188+
describe('#updateEmail', () => {
189+
beforeEach(() => {
190+
// This is necessary because this method calls reload; we don't care about that though,
191+
// for these tests we're looking at the change listeners
192+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { users: [{}] });
193+
});
194+
195+
it('triggers a token update if necessary', async () => {
196+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
197+
idToken: 'new-id-token',
198+
refreshToken: 'new-refresh-token',
199+
expiresIn: 300
200+
});
201+
202+
await updatePassword(user, '[email protected]');
203+
expect(idTokenChange).to.have.been.called;
204+
expect(auth.persistenceLayer.lastObjectSet).to.eql(
205+
user.toPlainObject()
206+
);
207+
});
208+
209+
it('does NOT trigger a token update if unnecessary', async () => {
210+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
211+
idToken: 'access-token',
212+
refreshToken: 'refresh-token',
213+
expiresIn: 300
214+
});
215+
216+
await updateEmail(user, '[email protected]');
217+
expect(idTokenChange).not.to.have.been.called;
218+
expect(auth.persistenceLayer.lastObjectSet).to.eql(
219+
user.toPlainObject()
220+
);
221+
});
222+
});
223+
224+
describe('#updatePassword', () => {
225+
beforeEach(() => {
226+
// This is necessary because this method calls reload; we don't care about that though,
227+
// for these tests we're looking at the change listeners
228+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { users: [{}] });
229+
});
230+
231+
it('triggers a token update if necessary', async () => {
232+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
233+
idToken: 'new-id-token',
234+
refreshToken: 'new-refresh-token',
235+
expiresIn: 300
236+
});
237+
238+
await updatePassword(user, 'pass');
239+
expect(idTokenChange).to.have.been.called;
240+
expect(auth.persistenceLayer.lastObjectSet).to.eql(
241+
user.toPlainObject()
242+
);
243+
});
244+
245+
it('does NOT trigger a token update if unnecessary', async () => {
246+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
247+
idToken: 'access-token',
248+
refreshToken: 'refresh-token',
249+
expiresIn: 300
250+
});
251+
252+
await updatePassword(user, 'pass');
253+
expect(idTokenChange).not.to.have.been.called;
254+
expect(auth.persistenceLayer.lastObjectSet).to.eql(
255+
user.toPlainObject()
256+
);
257+
});
258+
});
259+
});
260+
});

0 commit comments

Comments
 (0)