Skip to content

Commit 0f8a4ac

Browse files
committed
Implement user.reload()
1 parent 4cdd12f commit 0f8a4ac

File tree

7 files changed

+263
-19
lines changed

7 files changed

+263
-19
lines changed

packages-exp/auth-exp/src/api/account_management/account.ts

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

18-
import { Endpoint, HttpMethod, performApiRequest } from '..';
18+
import { Endpoint, HttpMethod, performApiRequest } from '../';
1919
import { Auth } from '../../model/auth';
2020
import { APIMFAInfo } from '../../model/id_token';
2121

@@ -74,7 +74,7 @@ export interface APIUserInfo {
7474
createdAt?: number;
7575
tenantId?: string;
7676
passwordHash?: string;
77-
providerUserInfo: ProviderUserInfo[];
77+
providerUserInfo?: ProviderUserInfo[];
7878
mfaInfo?: APIMFAInfo[];
7979
}
8080

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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 { mockEndpoint } from '../../../test/api/helper';
24+
import { testUser } from '../../../test/mock_auth';
25+
import * as fetch from '../../../test/mock_fetch';
26+
import { Endpoint } from '../../api';
27+
import { APIUserInfo, ProviderUserInfo } from '../../api/account_management/account';
28+
import { UserInfo } from '../../model/user';
29+
import { ProviderId } from '../providers';
30+
import { _reloadWithoutSaving, reload } from './reload';
31+
32+
use(chaiAsPromised);
33+
use(sinonChai);
34+
35+
const BASIC_USER_INFO: UserInfo = {
36+
providerId: ProviderId.FIREBASE,
37+
uid: 'uid',
38+
email: 'email',
39+
displayName: 'displayName',
40+
phoneNumber: 'phoneNumber',
41+
photoURL: 'photoURL',
42+
};
43+
44+
const BASIC_PROVIDER_USER_INFO: ProviderUserInfo = {
45+
providerId: ProviderId.FIREBASE,
46+
rawId: 'uid',
47+
email: 'email',
48+
displayName: 'displayName',
49+
phoneNumber: 'phoneNumber',
50+
photoUrl: 'photoURL',
51+
};
52+
53+
describe('reload()', () => {
54+
beforeEach(fetch.setUp);
55+
afterEach(fetch.tearDown);
56+
57+
it('sets all the new properties', async () => {
58+
const serverUser: APIUserInfo = {
59+
localId: 'localId',
60+
displayName: 'displayName',
61+
photoUrl: 'photoURL',
62+
email: 'email',
63+
emailVerified: true,
64+
phoneNumber: 'phoneNumber',
65+
tenantId: 'tenantId',
66+
createdAt: 123,
67+
lastLoginAt: 456,
68+
};
69+
70+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
71+
users: [serverUser],
72+
});
73+
74+
const user = testUser('abc', '', true);
75+
await _reloadWithoutSaving(user);
76+
expect(user.uid).to.eq('localId');
77+
expect(user.displayName).to.eq('displayName');
78+
expect(user.photoURL).to.eq('photoURL');
79+
expect(user.email).to.eq('email');
80+
expect(user.emailVerified).to.be.true;
81+
expect(user.phoneNumber).to.eq('phoneNumber');
82+
expect(user.tenantId).to.eq('tenantId');
83+
expect(user.metadata).to.eql({
84+
creationTime: '123',
85+
lastSignInTime: '456',
86+
});
87+
});
88+
89+
it('adds missing provider data', async () => {
90+
const user = testUser('abc', '', true);
91+
user.providerData = [{...BASIC_USER_INFO}];
92+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
93+
users: [{
94+
providerUserInfo: [{...BASIC_PROVIDER_USER_INFO, providerId: ProviderId.FACEBOOK}],
95+
}],
96+
});
97+
await _reloadWithoutSaving(user);
98+
expect(user.providerData).to.eql([
99+
{...BASIC_USER_INFO},
100+
{...BASIC_USER_INFO, providerId: ProviderId.FACEBOOK},
101+
]);
102+
});
103+
104+
it('merges provider data, using the new data for overlaps', async () => {
105+
const user = testUser('abc', '', true);
106+
user.providerData = [
107+
{
108+
...BASIC_USER_INFO,
109+
providerId: ProviderId.GITHUB,
110+
uid: 'i-will-be-overwritten',
111+
},
112+
{
113+
...BASIC_USER_INFO
114+
}
115+
];
116+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
117+
users: [{
118+
providerUserInfo: [{...BASIC_PROVIDER_USER_INFO, providerId: ProviderId.GITHUB, rawId: 'new-uid'}],
119+
}],
120+
});
121+
await _reloadWithoutSaving(user);
122+
console.warn(user.providerData);
123+
expect(user.providerData).to.eql([
124+
{...BASIC_USER_INFO},
125+
{
126+
...BASIC_USER_INFO,
127+
providerId: ProviderId.GITHUB,
128+
uid: 'new-uid',
129+
},
130+
]);
131+
});
132+
133+
it('reload calls auth.updateCurrentUser after completion', async () => {
134+
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
135+
users: [{}],
136+
});
137+
138+
const user = testUser('user', '', true);
139+
const spy = sinon.spy(user.auth, 'updateCurrentUser');
140+
await reload(user);
141+
expect(spy).to.have.been.calledWith(user);
142+
});
143+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @license
3+
* Copyright 2019 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 { getAccountInfo, ProviderUserInfo } from '../../api/account_management/account';
19+
import { User, UserInfo } from '../../model/user';
20+
import { ProviderId } from '../providers';
21+
import { assert } from '../util/assert';
22+
23+
export async function _reloadWithoutSaving(
24+
user: User
25+
): Promise<void> {
26+
const auth = user.auth;
27+
const idToken = await user.getIdToken();
28+
const response = await getAccountInfo(auth, { idToken });
29+
30+
assert(response?.users.length, auth.name);
31+
32+
const coreAccount = response.users[0];
33+
const newProviderData = coreAccount.providerUserInfo?.length
34+
? extractProviderData(coreAccount.providerUserInfo)
35+
: [];
36+
const updates: Partial<User> = {
37+
uid: coreAccount.localId,
38+
displayName: coreAccount.displayName || null,
39+
photoURL: coreAccount.photoUrl || null,
40+
email: coreAccount.email || null,
41+
emailVerified: coreAccount.emailVerified || false,
42+
phoneNumber: coreAccount.phoneNumber || null,
43+
tenantId: coreAccount.tenantId || null,
44+
providerData: mergeProviderData(user.providerData, newProviderData),
45+
metadata: {
46+
creationTime: coreAccount.createdAt?.toString(),
47+
lastSignInTime: coreAccount.lastLoginAt?.toString()
48+
}
49+
};
50+
51+
Object.assign(user, updates);
52+
}
53+
54+
export async function reload(user: User): Promise<void> {
55+
await _reloadWithoutSaving(user);
56+
57+
// Even though the current user hasn't changed, update
58+
// current user will trigger a persistence update w/ the
59+
// new info.
60+
return user.auth.updateCurrentUser(user);
61+
}
62+
63+
function mergeProviderData(
64+
original: UserInfo[],
65+
newData: UserInfo[]
66+
): UserInfo[] {
67+
const deduped = original.filter(
68+
o => !newData.some(n => n.providerId === o.providerId)
69+
);
70+
return [...deduped, ...newData];
71+
}
72+
73+
function extractProviderData(providers: ProviderUserInfo[]): UserInfo[] {
74+
return providers.map(provider => ({
75+
uid: provider.rawId || '',
76+
displayName: provider.displayName || null,
77+
email: provider.email || null,
78+
phoneNumber: provider.phoneNumber || null,
79+
providerId: provider.providerId as ProviderId,
80+
photoURL: provider.photoUrl || null
81+
}));
82+
}

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
import { expect, use } from 'chai';
1919
import * as chaiAsPromised from 'chai-as-promised';
20+
21+
import { FirebaseError } from '@firebase/util';
22+
2023
import { mockAuth } from '../../../test/mock_auth';
2124
import { IdTokenResponse } from '../../model/id_token';
2225
import { StsTokenManager } from './token_manager';
2326
import { UserImpl } from './user_impl';
24-
import { FirebaseError } from '@firebase/util';
2527

2628
use(chaiAsPromised);
2729

@@ -93,13 +95,6 @@ describe('core/user/user_impl', () => {
9395
});
9496
});
9597

96-
describe('#reload', () => {
97-
it('throws', () => {
98-
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
99-
expect(() => user.reload()).to.throw();
100-
});
101-
});
102-
10398
describe('#delete', () => {
10499
it('throws', () => {
105100
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { User } from '../../model/user';
2121
import { PersistedBlob } from '../persistence';
2222
import { ProviderId } from '../providers';
2323
import { assert, assertType } from '../util/assert';
24+
import { reload } from './reload';
2425
import { StsTokenManager } from './token_manager';
2526

2627
export interface UserParameters {
@@ -43,6 +44,9 @@ export class UserImpl implements User {
4344
uid: string;
4445
auth: Auth;
4546
emailVerified = false;
47+
tenantId = null;
48+
metadata = {};
49+
providerData = [];
4650

4751
// Optional fields from UserInfo
4852
displayName: string | null;
@@ -77,7 +81,7 @@ export class UserImpl implements User {
7781
}
7882

7983
reload(): Promise<void> {
80-
throw new Error('Method not implemented.');
84+
return reload(this);
8185
}
8286

8387
delete(): Promise<void> {

packages-exp/auth-exp/src/model/user.d.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,31 @@
1717

1818
import { PersistedBlob } from '../core/persistence';
1919
import { ProviderId } from '../core/providers';
20+
import { Auth } from './auth';
2021
import { IdTokenResult } from './id_token';
2122

23+
export interface UserMetadata {
24+
readonly creationTime?: string;
25+
readonly lastSignInTime?: string;
26+
}
27+
2228
export interface UserInfo {
23-
readonly uid: string;
24-
readonly providerId: ProviderId;
25-
readonly displayName: string | null;
26-
readonly email: string | null;
27-
readonly phoneNumber: string | null;
28-
readonly photoURL: string | null;
29+
uid: string;
30+
providerId: ProviderId;
31+
displayName: string | null;
32+
email: string | null;
33+
phoneNumber: string | null;
34+
photoURL: string | null;
2935
}
3036

3137
export interface User extends UserInfo {
38+
auth: Auth;
3239
providerId: ProviderId.FIREBASE;
3340
refreshToken: string;
3441
emailVerified: boolean;
42+
tenantId: string | null;
43+
providerData: UserInfo[];
44+
metadata: UserMetadata;
3545

3646
getIdToken(forceRefresh?: boolean): Promise<string>;
3747
getIdTokenResult(forceRefresh?: boolean): Promise<IdTokenResult>;

packages-exp/auth-exp/test/mock_auth.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,21 @@ export const mockAuth: Auth = {
4242
_notifyStateListeners() {},
4343
};
4444

45-
export function testUser(uid: string, email?: string): User {
45+
export function testUser(uid: string, email?: string, fakeTokens = false): User {
46+
// Create a token manager that's valid off the bat to avoid refresh calls
47+
const stsTokenManager = new StsTokenManager();
48+
if (fakeTokens) {
49+
Object.assign<StsTokenManager, Partial<StsTokenManager>>(stsTokenManager, {
50+
expirationTime: Date.now() + 100_000,
51+
accessToken: 'access-token',
52+
refreshToken: 'refresh-token',
53+
});
54+
}
55+
4656
return new UserImpl({
4757
uid,
4858
auth: mockAuth,
49-
stsTokenManager: new StsTokenManager(),
59+
stsTokenManager,
5060
email
5161
});
5262
}

0 commit comments

Comments
 (0)