Skip to content

Commit 1654a71

Browse files
committed
Add initial auth implementation, initializeAuth()
1 parent ea70a8a commit 1654a71

File tree

5 files changed

+314
-6
lines changed

5 files changed

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

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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 12 additions & 3 deletions
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>;
3641
}
42+
43+
export interface Dependencies {
44+
persistence?: Persistence | Persistence[];
45+
}

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

Lines changed: 5 additions & 1 deletion
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): User {

0 commit comments

Comments
 (0)