Skip to content

Add updateProfile, updateEmail, updatePassword #3122

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 11 commits into from
Jun 1, 2020
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 2 additions & 2 deletions packages-exp/auth-exp/src/core/auth/auth_impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ describe('core/auth/auth_impl', () => {
});

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

it('onIdTokenChange triggers for user props change', async () => {
user.refreshToken = 'hey look I changed';
user.photoURL = 'hey look I changed';
await auth.updateCurrentUser(user);
expect(idTokenCallback).to.have.been.calledWith(user);
});
Expand Down
30 changes: 22 additions & 8 deletions packages-exp/auth-exp/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,23 @@ export class AuthImpl implements Auth {
}

this._isInitialized = true;
this._notifyStateListeners();
this.notifyAuthListeners();
});
}

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

updateCurrentUser(user: User | null): Promise<void> {
return this.queue(() => this.directlySetCurrentUser(user));
async updateCurrentUser(user: User | null): Promise<void> {
return this.queue(async () => {
await this.directlySetCurrentUser(user);
this.notifyAuthListeners();
});
}

signOut(): Promise<void> {
return this.queue(() => this.directlySetCurrentUser(null));
async signOut(): Promise<void> {
return this.updateCurrentUser(null);
}

setPersistence(persistence: Persistence): Promise<void> {
Expand Down Expand Up @@ -129,7 +132,20 @@ export class AuthImpl implements Auth {
);
}

_notifyStateListeners(): void {
async _persistUserIfCurrent(user: User): Promise<void> {
if (user === this.currentUser) {
return this.queue(async () => this.directlySetCurrentUser(user));
}
}

/** Notifies listeners only if the user is current */
_notifyListenersIfCurrent(user: User): void {
if (user === this.currentUser) {
this.notifyAuthListeners();
}
}

private notifyAuthListeners(): void {
if (!this._isInitialized) {
return;
}
Expand Down Expand Up @@ -178,8 +194,6 @@ export class AuthImpl implements Auth {
} else {
await this.assertedPersistence.removeCurrentUser();
}

this._notifyStateListeners();
}

private queue(action: AsyncAction): Promise<void> {
Expand Down
8 changes: 6 additions & 2 deletions packages-exp/auth-exp/src/core/strategies/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@
* limitations under the License.
*/

import * as externs from '@firebase/auth-types-exp';
import { OperationType, UserCredential } from '@firebase/auth-types-exp';

import { Auth } from '../../model/auth';
import { AuthCredential } from '../../model/auth_credential';
import { User } from '../../model/user';
import { UserCredentialImpl } from '../user/user_credential_impl';

export async function signInWithCredential(
auth: Auth,
credential: AuthCredential
authExtern: externs.Auth,
credentialExtern: externs.AuthCredential
): Promise<UserCredential> {
const auth = authExtern as Auth;
const credential = credentialExtern as AuthCredential;
// TODO: handle mfa by wrapping with callApiWithMfaContext
const response = await credential._getIdTokenResponse(auth);
const userCredential = await UserCredentialImpl._fromIdTokenResponse(
Expand Down
261 changes: 261 additions & 0 deletions packages-exp/auth-exp/src/core/user/account_info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';

import { ProviderId, UserInfo } from '@firebase/auth-types-exp';

// import { UserInfo } from '@firebase/auth-types-exp';
import { mockEndpoint } from '../../../test/api/helper';
import { TestAuth, testAuth, testUser } from '../../../test/mock_auth';
import * as fetch from '../../../test/mock_fetch';
import { Endpoint } from '../../api';
import { User } from '../../model/user';
import { updateEmail, updatePassword, updateProfile } from './account_info';

use(chaiAsPromised);
use(sinonChai);

const PASSWORD_PROVIDER: UserInfo = {
providerId: ProviderId.PASSWORD,
uid: 'uid',
email: 'email',
displayName: 'old-name',
phoneNumber: 'phone-number',
photoURL: 'old-url'
};

describe('core/user/profile', () => {
let user: User;
let auth: TestAuth;

beforeEach(async () => {
auth = await testAuth();
user = testUser(auth, 'uid', '', true);
fetch.setUp();
});

afterEach(() => {
sinon.restore();
fetch.tearDown();
});

describe('#updateProfile', () => {
it('returns immediately if profile object is empty', async () => {
const ep = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
await updateProfile(user, {});
expect(ep.calls).to.be.empty;
});

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

await updateProfile(user, {
displayName: 'displayname',
photoURL: 'photo'
});
expect(ep.calls[0].request).to.eql({
idToken: 'access-token',
displayName: 'displayname',
photoUrl: 'photo'
});
});

it('sets the fields on the user based on the response', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
displayName: 'response-name',
photoUrl: 'response-photo'
});

await updateProfile(user, {
displayName: 'displayname',
photoURL: 'photo'
});
expect(user.displayName).to.eq('response-name');
expect(user.photoURL).to.eq('response-photo');
});

it('sets the fields on the password provider', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
displayName: 'response-name',
photoUrl: 'response-photo'
});
user.providerData = [{ ...PASSWORD_PROVIDER }];

await updateProfile(user, {
displayName: 'displayname',
photoURL: 'photo'
});
const provider = user.providerData[0];
expect(provider.displayName).to.eq('response-name');
expect(provider.photoURL).to.eq('response-photo');
});
});

describe('#updateEmail', () => {
it('calls the setAccountInfo endpoint and reloads the user', async () => {
const set = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
users: [{ localId: 'new-uid-to-prove-refresh-got-called' }]
});

await updateEmail(user, '[email protected]');
expect(set.calls[0].request).to.eql({
idToken: 'access-token',
email: '[email protected]'
});

expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called');
});
});

describe('#updatePassword', () => {
it('calls the setAccountInfo endpoint and reloads the user', async () => {
const set = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
users: [{ localId: 'new-uid-to-prove-refresh-got-called' }]
});

await updatePassword(user, 'pass');
expect(set.calls[0].request).to.eql({
idToken: 'access-token',
password: 'pass'
});

expect(user.uid).to.eq('new-uid-to-prove-refresh-got-called');
});
});

describe('notifications', () => {
let idTokenChange: sinon.SinonStub;

beforeEach(async () => {
idTokenChange = sinon.stub();
auth.onIdTokenChanged(idTokenChange);

// Flush token change promises which are floating
await auth.updateCurrentUser(user);
auth._isInitialized = true;
idTokenChange.resetHistory();
});

describe('#updateProfile', () => {
it('triggers a token update if necessary', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
idToken: 'new-id-token',
refreshToken: 'new-refresh-token',
expiresIn: 300
});

await updateProfile(user, { displayName: 'd' });
expect(idTokenChange).to.have.been.called;
expect(auth.persistenceLayer.lastObjectSet).to.eql(
user.toPlainObject()
);
});

it('does NOT trigger a token update if unnecessary', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
idToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 300
});

await updateProfile(user, { displayName: 'd' });
expect(idTokenChange).not.to.have.been.called;
expect(auth.persistenceLayer.lastObjectSet).to.eql(
user.toPlainObject()
);
});
});

describe('#updateEmail', () => {
beforeEach(() => {
// This is necessary because this method calls reload; we don't care about that though,
// for these tests we're looking at the change listeners
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { users: [{}] });
});

it('triggers a token update if necessary', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
idToken: 'new-id-token',
refreshToken: 'new-refresh-token',
expiresIn: 300
});

await updatePassword(user, '[email protected]');
expect(idTokenChange).to.have.been.called;
expect(auth.persistenceLayer.lastObjectSet).to.eql(
user.toPlainObject()
);
});

it('does NOT trigger a token update if unnecessary', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
idToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 300
});

await updateEmail(user, '[email protected]');
expect(idTokenChange).not.to.have.been.called;
expect(auth.persistenceLayer.lastObjectSet).to.eql(
user.toPlainObject()
);
});
});

describe('#updatePassword', () => {
beforeEach(() => {
// This is necessary because this method calls reload; we don't care about that though,
// for these tests we're looking at the change listeners
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { users: [{}] });
});

it('triggers a token update if necessary', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
idToken: 'new-id-token',
refreshToken: 'new-refresh-token',
expiresIn: 300
});

await updatePassword(user, 'pass');
expect(idTokenChange).to.have.been.called;
expect(auth.persistenceLayer.lastObjectSet).to.eql(
user.toPlainObject()
);
});

it('does NOT trigger a token update if unnecessary', async () => {
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
idToken: 'access-token',
refreshToken: 'refresh-token',
expiresIn: 300
});

await updatePassword(user, 'pass');
expect(idTokenChange).not.to.have.been.called;
expect(auth.persistenceLayer.lastObjectSet).to.eql(
user.toPlainObject()
);
});
});
});
});
Loading