Skip to content

Commit d637e2e

Browse files
committed
Add updateProfile, updateEmail, updatePassword
1 parent 6d57cd2 commit d637e2e

File tree

8 files changed

+236
-83
lines changed

8 files changed

+236
-83
lines changed

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

Lines changed: 18 additions & 10 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,13 +132,20 @@ export class AuthImpl implements Auth {
129132
);
130133
}
131134

132-
async _persistAndNotifyIfCurrent(user: User): Promise<void> {
135+
async _persistUserIfCurrent(user: User): Promise<void> {
133136
if (user === this.currentUser) {
134-
return this.updateCurrentUser(user);
137+
return this.queue(async () => this.directlySetCurrentUser(user));
135138
}
136139
}
137140

138-
_notifyStateListeners(): void {
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 {
139149
if (!this._isInitialized) {
140150
return;
141151
}
@@ -184,8 +194,6 @@ export class AuthImpl implements Auth {
184194
} else {
185195
await this.assertedPersistence.removeCurrentUser();
186196
}
187-
188-
this._notifyStateListeners();
189197
}
190198

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

packages-exp/auth-exp/src/core/user/account_info.test.ts

Lines changed: 164 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ import { UserInfo } from '@firebase/auth-types-exp';
2424

2525
// import { UserInfo } from '@firebase/auth-types-exp';
2626
import { mockEndpoint } from '../../../test/api/helper';
27-
import { testUser } from '../../../test/mock_auth';
27+
import { testPersistence, testUser } from '../../../test/mock_auth';
2828
import * as fetch from '../../../test/mock_fetch';
2929
import { Endpoint } from '../../api';
30+
import { Auth } from '../../model/auth';
3031
import { User } from '../../model/user';
3132
import { ProviderId } from '../providers';
32-
// import { ProviderId } from '../providers';
33-
import { updateProfile } from './account_info';
33+
import { updateEmail, updatePassword, updateProfile } from './account_info';
3434

3535
use(chaiAsPromised);
3636
use(sinonChai);
@@ -49,83 +49,194 @@ describe('core/user/profile', () => {
4949

5050
beforeEach(() => {
5151
user = testUser('uid', '', true);
52+
fetch.setUp();
5253
});
5354

5455
afterEach(() => {
5556
sinon.restore();
57+
fetch.tearDown();
5658
});
5759

58-
beforeEach(fetch.setUp);
59-
afterEach(fetch.tearDown);
60-
61-
it('returns immediately if profile object is empty', async () => {
62-
const ep = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
63-
await updateProfile(user, {});
64-
expect(ep.calls).to.be.empty;
65-
});
60+
describe('#updateProfile', () => {
61+
it('returns immediately if profile object is empty', async () => {
62+
const ep = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
63+
await updateProfile(user, {});
64+
expect(ep.calls).to.be.empty;
65+
});
6666

67-
it('calls the setAccountInfo endpoint', async () => {
68-
const ep =mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
67+
it('calls the setAccountInfo endpoint', async () => {
68+
const ep =mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
6969

70-
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
71-
expect(ep.calls[0].request).to.eql({
72-
idToken: 'access-token',
73-
displayName: 'displayname',
74-
photoUrl: 'photo',
70+
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
71+
expect(ep.calls[0].request).to.eql({
72+
idToken: 'access-token',
73+
displayName: 'displayname',
74+
photoUrl: 'photo',
75+
});
7576
});
76-
});
7777

78-
it('sets the fields on the user based on the response', async () => {
79-
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
80-
displayName: 'response-name',
81-
photoUrl: 'response-photo',
78+
it('sets the fields on the user based on the response', async () => {
79+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
80+
displayName: 'response-name',
81+
photoUrl: 'response-photo',
82+
});
83+
84+
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
85+
expect(user.displayName).to.eq('response-name');
86+
expect(user.photoURL).to.eq('response-photo');
8287
});
8388

84-
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
85-
expect(user.displayName).to.eq('response-name');
86-
expect(user.photoURL).to.eq('response-photo');
89+
it('sets the fields on the password provider', async () => {
90+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
91+
displayName: 'response-name',
92+
photoUrl: 'response-photo',
93+
});
94+
user.providerData = [{...PASSWORD_PROVIDER}];
95+
96+
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
97+
const provider = user.providerData[0];
98+
expect(provider.displayName).to.eq('response-name');
99+
expect(provider.photoURL).to.eq('response-photo');
100+
});
87101
});
88102

89-
it('sets the fields on the passwd provider', async () => {
90-
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
91-
displayName: 'response-name',
92-
photoUrl: 'response-photo',
103+
describe('#updateEmail', () => {
104+
it('calls the setAccountInfo endpoint and reloads the user', async () => {
105+
const set = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
106+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {users: [
107+
{localId: 'new-uid-to-prove-refresh-got-called'},
108+
]});
109+
110+
await updateEmail(user, '[email protected]');
111+
expect(set.calls[0].request).to.eql({
112+
idToken: 'access-token',
113+
114+
});
115+
116+
expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called');
93117
});
94-
user.providerData = [{...PASSWORD_PROVIDER}];
118+
});
119+
120+
describe('#updatePassword', () => {
121+
it('calls the setAccountInfo endpoint and reloads the user', async () => {
122+
const set = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
123+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {users: [
124+
{localId: 'new-uid-to-prove-refresh-got-called'},
125+
]});
126+
127+
await updatePassword(user, 'pass');
128+
expect(set.calls[0].request).to.eql({
129+
idToken: 'access-token',
130+
password: 'pass',
131+
});
95132

96-
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
97-
const provider = user.providerData[0];
98-
expect(provider.displayName).to.eq('response-name');
99-
expect(provider.photoURL).to.eq('response-photo');
133+
expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called');
134+
});
100135
});
101136

102137
describe('notifications', () => {
103-
beforeEach(() => {
104-
user.auth.currentUser = user;
138+
let auth: Auth;
139+
let idTokenChange: sinon.SinonStub;
140+
141+
beforeEach(async () => {
142+
auth = user.auth;
143+
idTokenChange = sinon.stub();
144+
auth.onIdTokenChanged(idTokenChange);
145+
146+
// Flush token change promises which are floating
147+
await auth.updateCurrentUser(user);
148+
auth._isInitialized = true;
149+
idTokenChange.resetHistory();
105150
});
106151

107-
it('triggers a token update if necessary', async () => {
108-
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
109-
idToken: 'new-id-token',
110-
refreshToken: 'new-refresh-token',
111-
expiresIn: 300,
152+
describe('#updateProfile', () => {
153+
it('triggers a token update if necessary', async () => {
154+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
155+
idToken: 'new-id-token',
156+
refreshToken: 'new-refresh-token',
157+
expiresIn: 300,
158+
});
159+
160+
await updateProfile(user, {displayName: 'd'});
161+
expect(idTokenChange).to.have.been.called;
162+
expect(testPersistence.lastPersistedBlob).to.eql(user.toPlainObject());
112163
});
113164

114-
const notifySpy = sinon.stub(user.auth, '_notifyStateListeners');
115-
await updateProfile(user, {displayName: 'd'});
116-
expect(notifySpy).to.have.been.called;
165+
it('does NOT trigger a token update if unnecessary', async () => {
166+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
167+
idToken: 'access-token',
168+
refreshToken: 'refresh-token',
169+
expiresIn: 300,
170+
});
171+
172+
await updateProfile(user, {displayName: 'd'});
173+
expect(idTokenChange).not.to.have.been.called;
174+
expect(testPersistence.lastPersistedBlob).to.eql(user.toPlainObject());
175+
});
117176
});
118177

119-
it('does NOT trigger a token update if unnecessary', async () => {
120-
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
121-
idToken: 'access-token',
122-
refreshToken: 'refresh-token',
123-
expiresIn: 300,
178+
describe('#updateEmail', () => {
179+
beforeEach(() => {
180+
// This is necessary because this method calls reload; we don't care about that though,
181+
// for these tests we're looking at the change listeners
182+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {users: [{}]});
124183
});
125184

126-
const notifySpy = sinon.stub(user.auth, '_notifyStateListeners');
127-
await updateProfile(user, {displayName: 'd'});
128-
expect(notifySpy).not.to.have.been.called;
185+
it('triggers a token update if necessary', async () => {
186+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
187+
idToken: 'new-id-token',
188+
refreshToken: 'new-refresh-token',
189+
expiresIn: 300,
190+
});
191+
192+
await updatePassword(user, '[email protected]');
193+
expect(idTokenChange).to.have.been.called;
194+
expect(testPersistence.lastPersistedBlob).to.eql(user.toPlainObject());
195+
});
196+
197+
it('does NOT trigger a token update if unnecessary', async () => {
198+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
199+
idToken: 'access-token',
200+
refreshToken: 'refresh-token',
201+
expiresIn: 300,
202+
});
203+
204+
await updateEmail(user, '[email protected]');
205+
expect(idTokenChange).not.to.have.been.called;
206+
expect(testPersistence.lastPersistedBlob).to.eql(user.toPlainObject());
207+
});
208+
});
209+
210+
describe('#updatePassword', () => {
211+
beforeEach(() => {
212+
// This is necessary because this method calls reload; we don't care about that though,
213+
// for these tests we're looking at the change listeners
214+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {users: [{}]});
215+
});
216+
217+
it('triggers a token update if necessary', async () => {
218+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
219+
idToken: 'new-id-token',
220+
refreshToken: 'new-refresh-token',
221+
expiresIn: 300,
222+
});
223+
224+
await updatePassword(user, 'pass');
225+
expect(idTokenChange).to.have.been.called;
226+
expect(testPersistence.lastPersistedBlob).to.eql(user.toPlainObject());
227+
});
228+
229+
it('does NOT trigger a token update if unnecessary', async () => {
230+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
231+
idToken: 'access-token',
232+
refreshToken: 'refresh-token',
233+
expiresIn: 300,
234+
});
235+
236+
await updatePassword(user, 'pass');
237+
expect(idTokenChange).not.to.have.been.called;
238+
expect(testPersistence.lastPersistedBlob).to.eql(user.toPlainObject());
239+
});
129240
});
130241
});
131242
});

packages-exp/auth-exp/src/core/user/account_info.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import * as externs from '@firebase/auth-types-exp';
1919

2020
import {
21-
updateEmailPassword as apiUpdateEmailPassword
21+
updateEmailPassword as apiUpdateEmailPassword, UpdateEmailPasswordRequest
2222
} from '../../api/account_management/email_and_password';
2323
import { updateProfile as apiUpdateProfile } from '../../api/account_management/profile';
2424
import { User } from '../../model/user';
@@ -36,6 +36,7 @@ export async function updateProfile(externUser: externs.User, {displayName, phot
3636
}
3737

3838
const user = externUser as User;
39+
const {auth} = user;
3940
const idToken = await user.getIdToken();
4041
const profileRequest = {idToken, displayName, photoUrl};
4142
const response = await apiUpdateProfile(user.auth, profileRequest);
@@ -50,18 +51,42 @@ export async function updateProfile(externUser: externs.User, {displayName, phot
5051
passwordProvider.photoURL = user.photoURL;
5152
}
5253

53-
user._updateTokensIfNecessary(response);
54-
return user.auth._persistAndNotifyIfCurrent(user);
54+
const tokensRefreshed = user._updateTokensIfNecessary(response);
55+
await auth._persistUserIfCurrent(user);
56+
if (tokensRefreshed) {
57+
auth._notifyListenersIfCurrent(user);
58+
}
59+
}
60+
61+
export function updateEmail(externUser: externs.User, newEmail: string): Promise<void> {
62+
const user = externUser as User;
63+
return updateEmailOrPassword(user, newEmail, null);
5564
}
5665

57-
export async function updateEmail(externUser: externs.User, newEmail: string): Promise<void> {
66+
export function updatePassword(externUser: externs.User, newPassword: string): Promise<void> {
5867
const user = externUser as User;
68+
return updateEmailOrPassword(user, null, newPassword);
69+
}
70+
71+
async function updateEmailOrPassword(user: User, email: string|null, password: string|null): Promise<void> {
5972
const {auth} = user;
6073
const idToken = await user.getIdToken();
61-
const response = await apiUpdateEmailPassword(auth, {idToken, email: newEmail});
62-
user._updateTokensIfNecessary(response);
74+
const request: UpdateEmailPasswordRequest = {idToken};
75+
76+
if (email) {
77+
request.email = email;
78+
}
79+
80+
if (password) {
81+
request.password = password;
82+
}
6383

64-
// To update
84+
const response = await apiUpdateEmailPassword(auth, request);
85+
86+
const tokensRefreshed = user._updateTokensIfNecessary(response);
6587
await _reloadWithoutSaving(user);
66-
return auth._persistAndNotifyIfCurrent(user);
67-
}
88+
await auth._persistUserIfCurrent(user);
89+
if (tokensRefreshed) {
90+
auth._notifyListenersIfCurrent(user);
91+
}
92+
}

0 commit comments

Comments
 (0)