UNPKG

voluptasmollitia

Version:
464 lines (392 loc) 15.5 kB
/** * @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 { FirebaseApp } from '@firebase/app-exp'; import { FirebaseError } from '@firebase/util'; import { testAuth, testUser } from '../../../test/helpers/mock_auth'; import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { PersistenceInternal } from '../persistence'; import { inMemoryPersistence } from '../persistence/in_memory'; import { _getInstance } from '../util/instantiator'; import * as navigator from '../util/navigator'; import * as reload from '../user/reload'; import { AuthImpl, DefaultConfig } from './auth_impl'; import { _initializeAuthInstance } from './initialize'; import { ClientPlatform } from '../util/version'; use(sinonChai); use(chaiAsPromised); const FAKE_APP: FirebaseApp = { name: 'test-app', options: { apiKey: 'api-key', authDomain: 'auth-domain' }, automaticDataCollectionEnabled: false }; describe('core/auth/auth_impl', () => { let auth: AuthInternal; let persistenceStub: sinon.SinonStubbedInstance<PersistenceInternal>; beforeEach(async () => { persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); const authImpl = new AuthImpl(FAKE_APP, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, tokenApiHost: DefaultConfig.TOKEN_API_HOST, clientPlatform: ClientPlatform.BROWSER, sdkClientVersion: 'v' }); _initializeAuthInstance(authImpl, { persistence: inMemoryPersistence }); auth = authImpl; }); afterEach(sinon.restore); describe('#updateCurrentUser', () => { it('sets the field on the auth object', async () => { const user = testUser(auth, 'uid'); await auth._updateCurrentUser(user); expect(auth.currentUser).to.eq(user); }); it('public version makes a copy', async () => { const user = testUser(auth, 'uid'); await auth.updateCurrentUser(user); // currentUser should deeply equal the user passed in, but should be a // different block in memory. expect(auth.currentUser).not.to.eq(user); expect(auth.currentUser).to.eql(user); }); it('public version throws if the auth is mismatched', async () => { const auth2 = await testAuth(); Object.assign(auth2.config, { apiKey: 'not-the-right-auth' }); const user = testUser(auth2, 'uid'); await expect(auth.updateCurrentUser(user)).to.be.rejectedWith( FirebaseError, 'auth/invalid-user-token' ); }); it('orders async operations correctly', async () => { const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => { return testUser(auth, `${n}`); }); persistenceStub._set.callsFake(() => { return new Promise(resolve => { // Force into the async flow to make this test actually meaningful setTimeout(() => resolve(), 1); }); }); await Promise.all(users.map(u => auth._updateCurrentUser(u))); for (let i = 0; i < 10; i++) { expect(persistenceStub._set.getCall(i)).to.have.been.calledWith( sinon.match.any, users[i].toJSON() ); } }); it('setting to null triggers a remove call', async () => { await auth._updateCurrentUser(null); expect(persistenceStub._remove).to.have.been.called; }); it('should throw an error if the user is from a different tenant', async () => { const user = testUser(auth, 'uid'); user.tenantId = 'other-tenant-id'; await expect(auth._updateCurrentUser(user)).to.be.rejectedWith( FirebaseError, '(auth/tenant-id-mismatch)' ); }); }); describe('#signOut', () => { it('sets currentUser to null, calls remove', async () => { await auth._updateCurrentUser(testUser(auth, 'test')); await auth.signOut(); expect(persistenceStub._remove).to.have.been.called; expect(auth.currentUser).to.be.null; }); }); describe('#useDeviceLanguage', () => { it('should update the language code', () => { const mock = sinon.stub(navigator, '_getUserLanguage'); mock.callsFake(() => 'jp'); expect(auth.languageCode).to.be.null; auth.useDeviceLanguage(); expect(auth.languageCode).to.eq('jp'); }); }); describe('change listeners', () => { // // Helpers to convert auth state change results to promise // function onAuthStateChange(callback: NextFn<User|null>) it('immediately calls authStateChange if initialization finished', done => { const user = testUser(auth, 'uid'); auth.currentUser = user; auth._isInitialized = true; auth.onAuthStateChanged(user => { expect(user).to.eq(user); done(); }); }); it('waits for initialization for authStateChange', done => { const user = testUser(auth, 'uid'); auth.currentUser = user; auth._isInitialized = false; auth.onAuthStateChanged(user => { expect(user).to.eq(user); done(); }); }); it('immediately calls idTokenChange if initialization finished', done => { const user = testUser(auth, 'uid'); auth.currentUser = user; auth._isInitialized = true; auth.onIdTokenChanged(user => { expect(user).to.eq(user); done(); }); }); it('waits for initialization for idTokenChanged', done => { const user = testUser(auth, 'uid'); auth.currentUser = user; auth._isInitialized = false; auth.onIdTokenChanged(user => { expect(user).to.eq(user); done(); }); }); it('immediate callback is done async', () => { auth._isInitialized = true; let callbackCalled = false; auth.onIdTokenChanged(() => { callbackCalled = true; }); expect(callbackCalled).to.be.false; }); describe('user logs in/out, tokens refresh', () => { let user: UserInternal; let authStateCallback: sinon.SinonSpy; let idTokenCallback: sinon.SinonSpy; beforeEach(() => { user = testUser(auth, 'uid'); authStateCallback = sinon.spy(); idTokenCallback = sinon.spy(); }); context('initially currentUser is null', () => { beforeEach(async () => { auth.onAuthStateChanged(authStateCallback); auth.onIdTokenChanged(idTokenCallback); await auth._updateCurrentUser(null); authStateCallback.resetHistory(); idTokenCallback.resetHistory(); }); it('onAuthStateChange triggers on log in', async () => { await auth._updateCurrentUser(user); expect(authStateCallback).to.have.been.calledWith(user); }); it('onIdTokenChange triggers on log in', async () => { await auth._updateCurrentUser(user); expect(idTokenCallback).to.have.been.calledWith(user); }); }); context('initially currentUser is user', () => { beforeEach(async () => { auth.onAuthStateChanged(authStateCallback); auth.onIdTokenChanged(idTokenCallback); await auth._updateCurrentUser(user); authStateCallback.resetHistory(); idTokenCallback.resetHistory(); }); it('onAuthStateChange triggers on log out', async () => { await auth._updateCurrentUser(null); expect(authStateCallback).to.have.been.calledWith(null); }); it('onIdTokenChange triggers on log out', async () => { await auth._updateCurrentUser(null); expect(idTokenCallback).to.have.been.calledWith(null); }); it('onAuthStateChange does not trigger for user props change', async () => { user.photoURL = 'blah'; await auth._updateCurrentUser(user); expect(authStateCallback).not.to.have.been.called; }); it('onIdTokenChange triggers for user props change', async () => { user.photoURL = 'hey look I changed'; await auth._updateCurrentUser(user); expect(idTokenCallback).to.have.been.calledWith(user); }); it('onAuthStateChange triggers if uid changes', async () => { const newUser = testUser(auth, 'different-uid'); await auth._updateCurrentUser(newUser); expect(authStateCallback).to.have.been.calledWith(newUser); }); }); it('onAuthStateChange works for multiple listeners', async () => { const cb1 = sinon.spy(); const cb2 = sinon.spy(); auth.onAuthStateChanged(cb1); auth.onAuthStateChanged(cb2); await auth._updateCurrentUser(null); cb1.resetHistory(); cb2.resetHistory(); await auth._updateCurrentUser(user); expect(cb1).to.have.been.calledWith(user); expect(cb2).to.have.been.calledWith(user); }); it('onIdTokenChange works for multiple listeners', async () => { const cb1 = sinon.spy(); const cb2 = sinon.spy(); auth.onIdTokenChanged(cb1); auth.onIdTokenChanged(cb2); await auth._updateCurrentUser(null); cb1.resetHistory(); cb2.resetHistory(); await auth._updateCurrentUser(user); expect(cb1).to.have.been.calledWith(user); expect(cb2).to.have.been.calledWith(user); }); }); }); describe('#_onStorageEvent', () => { let authStateCallback: sinon.SinonSpy; let idTokenCallback: sinon.SinonSpy; beforeEach(async () => { authStateCallback = sinon.spy(); idTokenCallback = sinon.spy(); auth.onAuthStateChanged(authStateCallback); auth.onIdTokenChanged(idTokenCallback); await auth._updateCurrentUser(null); // force event handlers to clear out authStateCallback.resetHistory(); idTokenCallback.resetHistory(); }); context('previously logged out', () => { context('still logged out', () => { it('should do nothing', async () => { await auth._onStorageEvent(); expect(authStateCallback).not.to.have.been.called; expect(idTokenCallback).not.to.have.been.called; }); }); context('now logged in', () => { let user: UserInternal; beforeEach(() => { user = testUser(auth, 'uid'); persistenceStub._get.returns(Promise.resolve(user.toJSON())); }); it('should update the current user', async () => { await auth._onStorageEvent(); expect(auth.currentUser?.toJSON()).to.eql(user.toJSON()); expect(authStateCallback).to.have.been.called; expect(idTokenCallback).to.have.been.called; }); }); }); context('previously logged in', () => { let user: UserInternal; beforeEach(async () => { user = testUser(auth, 'uid', undefined, true); await auth._updateCurrentUser(user); authStateCallback.resetHistory(); idTokenCallback.resetHistory(); }); context('now logged out', () => { beforeEach(() => { persistenceStub._get.returns(Promise.resolve(null)); }); it('should log out', async () => { await auth._onStorageEvent(); expect(auth.currentUser).to.be.null; expect(authStateCallback).to.have.been.called; expect(idTokenCallback).to.have.been.called; }); }); context('still logged in as same user', () => { it('should do nothing if nothing changed', async () => { persistenceStub._get.returns(Promise.resolve(user.toJSON())); await auth._onStorageEvent(); expect(auth.currentUser?.toJSON()).to.eql(user.toJSON()); expect(authStateCallback).not.to.have.been.called; expect(idTokenCallback).not.to.have.been.called; }); it('should update fields if they have changed', async () => { const userObj = user.toJSON(); userObj['displayName'] = 'other-name'; persistenceStub._get.returns(Promise.resolve(userObj)); await auth._onStorageEvent(); expect(auth.currentUser?.uid).to.eq(user.uid); expect(auth.currentUser?.displayName).to.eq('other-name'); expect(authStateCallback).not.to.have.been.called; expect(idTokenCallback).not.to.have.been.called; }); it('should update tokens if they have changed', async () => { const userObj = user.toJSON(); (userObj['stsTokenManager'] as any)['accessToken'] = 'new-access-token'; persistenceStub._get.returns(Promise.resolve(userObj)); await auth._onStorageEvent(); expect(auth.currentUser?.uid).to.eq(user.uid); expect( (auth.currentUser as UserInternal)?.stsTokenManager.accessToken ).to.eq('new-access-token'); expect(authStateCallback).not.to.have.been.called; expect(idTokenCallback).to.have.been.called; }); }); context('now logged in as different user', () => { it('should re-login as the new user', async () => { const newUser = testUser(auth, 'other-uid', undefined, true); persistenceStub._get.returns(Promise.resolve(newUser.toJSON())); await auth._onStorageEvent(); expect(auth.currentUser?.toJSON()).to.eql(newUser.toJSON()); expect(authStateCallback).to.have.been.called; expect(idTokenCallback).to.have.been.called; }); }); }); }); context('#_delete', () => { beforeEach(async () => { sinon.stub(reload, '_reloadWithoutSaving').returns(Promise.resolve()); }); it('prevents initialization from completing', async () => { const authImpl = new AuthImpl(FAKE_APP, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, tokenApiHost: DefaultConfig.TOKEN_API_HOST, clientPlatform: ClientPlatform.BROWSER, sdkClientVersion: 'v' }); persistenceStub._get.returns( Promise.resolve(testUser(auth, 'uid').toJSON()) ); await authImpl._delete(); await authImpl._initializeWithPersistence([ persistenceStub as PersistenceInternal ]); expect(authImpl.currentUser).to.be.null; }); it('no longer calls listeners', async () => { const spy = sinon.spy(); auth.onAuthStateChanged(spy); await Promise.resolve(); spy.resetHistory(); await (auth as AuthImpl)._delete(); await auth._updateCurrentUser(testUser(auth, 'blah')); expect(spy).not.to.have.been.called; }); }); });