Skip to content

Commit 9cc3657

Browse files
sam-gcavolkovi
authored andcommitted
Initial auth object implementation + initializeAuth() (#2932)
1 parent f8a2671 commit 9cc3657

File tree

6 files changed

+339
-9
lines changed

6 files changed

+339
-9
lines changed

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ const es5Builds = [
5959
*/
6060
{
6161
input: 'index.rn.ts',
62-
output: [{ file: pkg['react-native'], format: 'cjs', sourcemap: true }],
62+
output: [{ file: pkg['react-native'], format: 'cjs', sourcemap: true }],
6363
plugins: es5BuildPlugins,
64-
external: id => [...deps, 'react-native'].some(dep => id === dep || id.startsWith(`${dep}/`))
65-
},
64+
external: id =>
65+
[...deps, 'react-native'].some(
66+
dep => id === dep || id.startsWith(`${dep}/`)
67+
)
68+
}
6669
];
6770

6871
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 sinon from 'sinon';
20+
import * as sinonChai from 'sinon-chai';
21+
22+
import { FirebaseApp } from '@firebase/app-types-exp';
23+
import { FirebaseError } from '@firebase/util';
24+
25+
import { testUser } from '../../../test/mock_auth';
26+
import { Auth } from '../../model/auth';
27+
import { Persistence } from '../persistence';
28+
import { browserLocalPersistence } from '../persistence/browser';
29+
import { inMemoryPersistence } from '../persistence/in_memory';
30+
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
31+
import { ClientPlatform, getClientVersion } from '../util/version';
32+
import {
33+
DEFAULT_API_HOST,
34+
DEFAULT_API_SCHEME,
35+
initializeAuth
36+
} from './auth_impl';
37+
38+
use(sinonChai);
39+
40+
const FAKE_APP: FirebaseApp = {
41+
name: 'test-app',
42+
options: {
43+
apiKey: 'api-key',
44+
authDomain: 'auth-domain'
45+
},
46+
automaticDataCollectionEnabled: false
47+
};
48+
49+
describe('AuthImpl', () => {
50+
let auth: Auth;
51+
let persistenceStub: sinon.SinonStubbedInstance<Persistence>;
52+
53+
beforeEach(() => {
54+
persistenceStub = sinon.stub(inMemoryPersistence);
55+
auth = initializeAuth(FAKE_APP, { persistence: inMemoryPersistence });
56+
});
57+
58+
afterEach(sinon.restore);
59+
60+
describe('#updateCurrentUser', () => {
61+
it('sets the field on the auth object', async () => {
62+
const user = testUser('uid');
63+
await auth.updateCurrentUser(user);
64+
expect(auth.currentUser).to.eql(user);
65+
});
66+
67+
it('orders async operations correctly', async () => {
68+
const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => {
69+
return testUser(`${n}`);
70+
});
71+
72+
persistenceStub.set.callsFake(() => {
73+
return new Promise(resolve => {
74+
// Force into the async flow to make this test actually meaningful
75+
setTimeout(() => resolve(), 1);
76+
});
77+
});
78+
79+
await Promise.all(users.map(u => auth.updateCurrentUser(u)));
80+
for (let i = 0; i < 10; i++) {
81+
expect(persistenceStub.set.getCall(i)).to.have.been.calledWith(
82+
sinon.match.any,
83+
users[i].toPlainObject()
84+
);
85+
}
86+
});
87+
88+
it('setting to null triggers a remove call', async () => {
89+
await auth.updateCurrentUser(null);
90+
expect(persistenceStub.remove).to.have.been.called;
91+
});
92+
});
93+
94+
describe('#signOut', () => {
95+
it('sets currentUser to null, calls remove', async () => {
96+
await auth.updateCurrentUser(testUser('test'));
97+
await auth.signOut();
98+
expect(persistenceStub.remove).to.have.been.called;
99+
expect(auth.currentUser).to.be.null;
100+
});
101+
});
102+
103+
describe('#setPersistence', () => {
104+
it('swaps underlying persistence', async () => {
105+
const newPersistence = browserLocalPersistence;
106+
const newStub = sinon.stub(newPersistence);
107+
persistenceStub.get.returns(
108+
Promise.resolve(testUser('test').toPlainObject())
109+
);
110+
111+
await auth.setPersistence(newPersistence);
112+
expect(persistenceStub.get).to.have.been.called;
113+
expect(persistenceStub.remove).to.have.been.called;
114+
expect(newStub.set).to.have.been.calledWith(
115+
sinon.match.any,
116+
testUser('test').toPlainObject()
117+
);
118+
});
119+
});
120+
});
121+
122+
describe('initializeAuth', () => {
123+
afterEach(sinon.restore);
124+
125+
it('throws an API error if key not provided', () => {
126+
expect(() =>
127+
initializeAuth({
128+
...FAKE_APP,
129+
options: {} // apiKey is missing
130+
})
131+
).to.throw(
132+
FirebaseError,
133+
'Firebase: Your API key is invalid, please check you have copied it correctly. (auth/invalid-api-key).'
134+
);
135+
});
136+
137+
describe('persistence manager creation', () => {
138+
let createManagerStub: sinon.SinonSpy;
139+
beforeEach(() => {
140+
createManagerStub = sinon.spy(PersistenceUserManager, 'create');
141+
});
142+
143+
async function initAndWait(
144+
persistence: Persistence | Persistence[]
145+
): Promise<Auth> {
146+
const auth = initializeAuth(FAKE_APP, { persistence });
147+
// Auth initializes async. We can make sure the initialization is
148+
// flushed by awaiting a method on the queue.
149+
await auth.setPersistence(inMemoryPersistence);
150+
return auth;
151+
}
152+
153+
it('converts single persistence to array', async () => {
154+
const auth = await initAndWait(inMemoryPersistence);
155+
expect(createManagerStub).to.have.been.calledWith(auth, [
156+
inMemoryPersistence
157+
]);
158+
});
159+
160+
it('pulls the user from storage', async () => {
161+
sinon
162+
.stub(inMemoryPersistence, 'get')
163+
.returns(Promise.resolve(testUser('uid').toPlainObject()));
164+
const auth = await initAndWait(inMemoryPersistence);
165+
expect(auth.currentUser!.uid).to.eq('uid');
166+
});
167+
168+
it('calls create with the persistence in order', async () => {
169+
const auth = await initAndWait([
170+
inMemoryPersistence,
171+
browserLocalPersistence
172+
]);
173+
expect(createManagerStub).to.have.been.calledWith(auth, [
174+
inMemoryPersistence,
175+
browserLocalPersistence
176+
]);
177+
});
178+
179+
it('sets auth name and config', async () => {
180+
const auth = await initAndWait(inMemoryPersistence);
181+
expect(auth.name).to.eq(FAKE_APP.name);
182+
expect(auth.config).to.eql({
183+
apiKey: FAKE_APP.options.apiKey,
184+
authDomain: FAKE_APP.options.authDomain,
185+
apiHost: DEFAULT_API_HOST,
186+
apiScheme: DEFAULT_API_SCHEME,
187+
sdkClientVersion: getClientVersion(ClientPlatform.BROWSER)
188+
});
189+
});
190+
});
191+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 { getApp } from '@firebase/app-exp';
19+
import { FirebaseApp } from '@firebase/app-types-exp';
20+
21+
import { Auth, Config, Dependencies } from '../../model/auth';
22+
import { User } from '../../model/user';
23+
import { AuthErrorCode } from '../errors';
24+
import { Persistence } from '../persistence';
25+
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
26+
import { assert } from '../util/assert';
27+
import { ClientPlatform, getClientVersion } from '../util/version';
28+
29+
interface AsyncAction {
30+
(): Promise<void>;
31+
}
32+
33+
export const DEFAULT_API_HOST = 'identitytoolkit.googleapis.com';
34+
export const DEFAULT_API_SCHEME = 'https';
35+
36+
class AuthImpl implements Auth {
37+
currentUser: User | null = null;
38+
private operations = Promise.resolve();
39+
private persistenceManager?: PersistenceUserManager;
40+
41+
constructor(
42+
public readonly name: string,
43+
public readonly config: Config,
44+
persistenceHierarchy: Persistence[]
45+
) {
46+
// This promise is intended to float; auth initialization happens in the
47+
// background, meanwhile the auth object may be used by the app.
48+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
49+
this.queue(async () => {
50+
this.persistenceManager = await PersistenceUserManager.create(
51+
this,
52+
persistenceHierarchy
53+
);
54+
55+
const storedUser = await this.persistenceManager.getCurrentUser();
56+
// TODO: Check redirect user, if not redirect user, call refresh on stored user
57+
if (storedUser) {
58+
await this.directlySetCurrentUser(storedUser);
59+
}
60+
});
61+
}
62+
63+
updateCurrentUser(user: User | null): Promise<void> {
64+
return this.queue(() => this.directlySetCurrentUser(user));
65+
}
66+
67+
signOut(): Promise<void> {
68+
return this.queue(() => this.directlySetCurrentUser(null));
69+
}
70+
71+
setPersistence(persistence: Persistence): Promise<void> {
72+
return this.queue(async () => {
73+
await this.assertedPersistence.setPersistence(persistence);
74+
});
75+
}
76+
77+
/**
78+
* Unprotected (from race conditions) method to set the current user. This
79+
* should only be called from within a queued callback. This is necessary
80+
* because the queue shouldn't rely on another queued callback.
81+
*/
82+
private async directlySetCurrentUser(user: User | null): Promise<void> {
83+
this.currentUser = user;
84+
85+
if (user) {
86+
await this.assertedPersistence.setCurrentUser(user);
87+
} else {
88+
await this.assertedPersistence.removeCurrentUser();
89+
}
90+
}
91+
92+
private queue(action: AsyncAction): Promise<void> {
93+
// In case something errors, the callback still should be called in order
94+
// to keep the promise chain alive
95+
this.operations = this.operations.then(action, action);
96+
return this.operations;
97+
}
98+
99+
private get assertedPersistence(): PersistenceUserManager {
100+
return assert(this.persistenceManager, this.name);
101+
}
102+
}
103+
104+
export function initializeAuth(
105+
app: FirebaseApp = getApp(),
106+
deps?: Dependencies
107+
): Auth {
108+
const persistence = deps?.persistence || [];
109+
const hierarchy = Array.isArray(persistence) ? persistence : [persistence];
110+
const { apiKey, authDomain } = app.options;
111+
112+
// TODO: platform needs to be determined using heuristics
113+
const config: Config = {
114+
apiKey: assert(apiKey, app.name, AuthErrorCode.INVALID_API_KEY),
115+
authDomain,
116+
apiHost: DEFAULT_API_HOST,
117+
apiScheme: DEFAULT_API_SCHEME,
118+
sdkClientVersion: getClientVersion(ClientPlatform.BROWSER)
119+
};
120+
121+
return new AuthImpl(app.name, config, hierarchy);
122+
}

packages-exp/auth-exp/src/core/util/assert.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
1919

2020
export function assert<T>(
2121
expression: T | null | undefined,
22-
appName: string
22+
appName: string,
23+
code = AuthErrorCode.INTERNAL_ERROR
2324
): T {
2425
if (!expression) {
25-
throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, { appName });
26+
throw AUTH_ERROR_FACTORY.create(code, { appName });
2627
}
2728

2829
return expression;

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { Persistence } from '../core/persistence';
19+
import { User } from './user';
20+
1821
export type AppName = string;
1922
export type ApiKey = string;
2023
export type AuthDomain = string;
2124

22-
export const DEFAULT_API_HOST = 'identitytoolkit.googleapis.com';
23-
export const DEFAULT_API_SCHEME = 'https';
24-
2525
export interface Config {
2626
apiKey: ApiKey;
2727
apiHost: string;
@@ -31,6 +31,15 @@ export interface Config {
3131
}
3232

3333
export interface Auth {
34+
currentUser: User | null;
3435
readonly name: AppName;
3536
readonly config: Config;
37+
38+
setPersistence(persistence: Persistence): Promise<void>;
39+
updateCurrentUser(user: User | null): Promise<void>;
40+
signOut(): Promise<void>;
41+
}
42+
43+
export interface Dependencies {
44+
persistence?: Persistence | Persistence[];
3645
}

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ export const mockAuth: Auth = {
3131
apiHost: TEST_HOST,
3232
apiScheme: TEST_SCHEME,
3333
sdkClientVersion: 'testSDK/0.0.0'
34-
}
34+
},
35+
currentUser: null,
36+
async setPersistence() {},
37+
async updateCurrentUser() {},
38+
async signOut() {}
3539
};
3640

3741
export function testUser(uid: string, email?: string): User {

0 commit comments

Comments
 (0)