diff --git a/packages-exp/auth-exp/karma.conf.js b/packages-exp/auth-exp/karma.conf.js index 3b6355af507..9d64f08da1b 100644 --- a/packages-exp/auth-exp/karma.conf.js +++ b/packages-exp/auth-exp/karma.conf.js @@ -36,7 +36,9 @@ function getTestFiles(argv) { if (argv.unit) { return ['src/**/*.test.ts', 'test/helpers/**/*.test.ts']; } else if (argv.integration) { - return ['test/integration/**/*.test.ts']; + return argv.local + ? ['test/integration/**/*.test.ts'] + : ['test/integration/**/*!(local).test.ts']; } else if (argv.cordova) { return ['src/platform_cordova/**/*.test.ts']; } else { diff --git a/packages-exp/auth-exp/scripts/run-node-tests.js b/packages-exp/auth-exp/scripts/run-node-tests.js index c83ddee1a20..ddef213a88e 100644 --- a/packages-exp/auth-exp/scripts/run-node-tests.js +++ b/packages-exp/auth-exp/scripts/run-node-tests.js @@ -1,3 +1,4 @@ +'use strict'; /** * @license * Copyright 2020 Google LLC @@ -13,10 +14,7 @@ * 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. - */ - -'use strict'; -var __spreadArrays = + */ var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) @@ -44,6 +42,9 @@ var testConfig = [ ]; if (argv.integration) { testConfig = ['test/integration/flows/{email,anonymous}.test.ts']; + if (argv.local) { + testConfig.push('test/integration/flows/*.local.test.ts'); + } } var args = __spreadArrays(['--reporter', 'lcovonly', mocha], testConfig, [ '--config', diff --git a/packages-exp/auth-exp/scripts/run-node-tests.ts b/packages-exp/auth-exp/scripts/run-node-tests.ts index f3a5eba5a3c..316f6ac7d3c 100644 --- a/packages-exp/auth-exp/scripts/run-node-tests.ts +++ b/packages-exp/auth-exp/scripts/run-node-tests.ts @@ -42,6 +42,9 @@ let testConfig = [ if (argv.integration) { testConfig = ['test/integration/flows/{email,anonymous}.test.ts']; + if (argv.local) { + testConfig.push('test/integration/flows/*.local.test.ts'); + } } let args = [ diff --git a/packages-exp/auth-exp/test/helpers/integration/helpers.ts b/packages-exp/auth-exp/test/helpers/integration/helpers.ts index 831179c58b5..4b0b091483d 100644 --- a/packages-exp/auth-exp/test/helpers/integration/helpers.ts +++ b/packages-exp/auth-exp/test/helpers/integration/helpers.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import * as sinon from 'sinon'; import { deleteApp, initializeApp } from '@firebase/app-exp'; import { Auth, User } from '../../../src/model/public_types'; @@ -39,7 +40,9 @@ export function getTestInstance(): Auth { const emulatorUrl = getEmulatorUrl(); if (emulatorUrl) { + const stub = stubConsoleToSilenceEmulatorWarnings(); useAuthEmulator(auth, emulatorUrl, { disableWarnings: true }); + stub.restore(); } auth.onAuthStateChanged(user => { @@ -68,3 +71,16 @@ export async function cleanUpTestInstance(auth: Auth): Promise { await auth.signOut(); await (auth as IntegrationTestAuth).cleanUp(); } + +function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { + const originalConsoleInfo = console.info.bind(console); + return sinon.stub(console, 'info').callsFake((...args: unknown[]) => { + if ( + !JSON.stringify(args[0]).includes( + 'WARNING: You are using the Auth Emulator' + ) + ) { + originalConsoleInfo(...args); + } + }); +} diff --git a/packages-exp/auth-exp/test/integration/flows/custom.local.test.ts b/packages-exp/auth-exp/test/integration/flows/custom.local.test.ts new file mode 100644 index 00000000000..08c7439fb6c --- /dev/null +++ b/packages-exp/auth-exp/test/integration/flows/custom.local.test.ts @@ -0,0 +1,214 @@ +/** + * @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 { + Auth, + createUserWithEmailAndPassword, + EmailAuthProvider, + linkWithCredential, + OperationType, + reload, + signInAnonymously, + signInWithCustomToken, + signInWithEmailAndPassword, + updateEmail, + updatePassword, + updateProfile + // eslint-disable-next-line import/no-extraneous-dependencies +} from '@firebase/auth-exp'; +import { FirebaseError } from '@firebase/util'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { + cleanUpTestInstance, + getTestInstance, + randomEmail +} from '../../helpers/integration/helpers'; + +use(chaiAsPromised); + +describe('Integration test: custom auth', () => { + let auth: Auth; + let customToken: string; + let uid: string; + + beforeEach(() => { + auth = getTestInstance(); + uid = randomEmail(); + customToken = JSON.stringify({ + uid, + claims: { + customClaim: 'some-claim' + } + }); + + if (!auth.emulatorConfig) { + throw new Error('Test can only be run against the emulator!'); + } + }); + + afterEach(async () => { + await cleanUpTestInstance(auth); + }); + + it('signs in with custom token', async () => { + const cred = await signInWithCustomToken(auth, customToken); + expect(auth.currentUser).to.eq(cred.user); + expect(cred.operationType).to.eq(OperationType.SIGN_IN); + + const { user } = cred; + expect(user.isAnonymous).to.be.false; + expect(user.uid).to.eq(uid); + expect((await user.getIdTokenResult(false)).claims.customClaim).to.eq( + 'some-claim' + ); + expect(user.providerId).to.eq('firebase'); + }); + + it('uid will overwrite existing user, joining accounts', async () => { + const { user: anonUser } = await signInAnonymously(auth); + const customCred = await signInWithCustomToken( + auth, + JSON.stringify({ + uid: anonUser.uid + }) + ); + + expect(auth.currentUser).to.eq(customCred.user); + expect(customCred.user.uid).to.eq(anonUser.uid); + expect(customCred.user.isAnonymous).to.be.false; + }); + + it('allows the user to delete the account', async () => { + let { user } = await signInWithCustomToken(auth, customToken); + await updateProfile(user, { displayName: 'Display Name' }); + expect(user.displayName).to.eq('Display Name'); + + await user.delete(); + await expect(reload(user)).to.be.rejectedWith( + FirebaseError, + 'auth/user-token-expired' + ); + expect(auth.currentUser).to.be.null; + + ({ user } = await signInWithCustomToken(auth, customToken)); + // New user in the system: the display name should be missing + expect(user.displayName).to.be.null; + }); + + it('sign in can be called twice successively', async () => { + const { user: userA } = await signInWithCustomToken(auth, customToken); + const { user: userB } = await signInWithCustomToken(auth, customToken); + expect(userA.uid).to.eq(userB.uid); + }); + + it('allows user to update profile', async () => { + let { user } = await signInWithCustomToken(auth, customToken); + await updateProfile(user, { + displayName: 'Display Name', + photoURL: 'photo-url' + }); + expect(user.displayName).to.eq('Display Name'); + expect(user.photoURL).to.eq('photo-url'); + + await auth.signOut(); + + user = (await signInWithCustomToken(auth, customToken)).user; + expect(user.displayName).to.eq('Display Name'); + expect(user.photoURL).to.eq('photo-url'); + }); + + context('email/password interaction', () => { + let email: string; + let customToken: string; + + beforeEach(() => { + email = randomEmail(); + customToken = JSON.stringify({ + uid: email + }); + }); + + it('custom / email-password accounts remain independent', async () => { + let customCred = await signInWithCustomToken(auth, customToken); + const emailCred = await createUserWithEmailAndPassword( + auth, + email, + 'password' + ); + expect(emailCred.user.uid).not.to.eql(customCred.user.uid); + + await auth.signOut(); + customCred = await signInWithCustomToken(auth, customToken); + const emailSignIn = await signInWithEmailAndPassword( + auth, + email, + 'password' + ); + expect(emailCred.user.uid).to.eql(emailSignIn.user.uid); + expect(emailSignIn.user.uid).not.to.eql(customCred.user.uid); + }); + + it('account can have email / password attached', async () => { + const { user: customUser } = await signInWithCustomToken( + auth, + customToken + ); + await updateEmail(customUser, email); + await updatePassword(customUser, 'password'); + + await auth.signOut(); + + const { user: emailPassUser } = await signInWithEmailAndPassword( + auth, + email, + 'password' + ); + expect(emailPassUser.uid).to.eq(customUser.uid); + }); + + it('account can be linked using email and password', async () => { + const { user: customUser } = await signInWithCustomToken( + auth, + customToken + ); + const cred = EmailAuthProvider.credential(email, 'password'); + await linkWithCredential(customUser, cred); + await auth.signOut(); + + const { user: emailPassUser } = await signInWithEmailAndPassword( + auth, + email, + 'password' + ); + expect(emailPassUser.uid).to.eq(customUser.uid); + }); + + it('account cannot be linked with existing email/password', async () => { + await createUserWithEmailAndPassword(auth, email, 'password'); + const { user: customUser } = await signInWithCustomToken( + auth, + customToken + ); + const cred = EmailAuthProvider.credential(email, 'password'); + await expect(linkWithCredential(customUser, cred)).to.be.rejectedWith( + FirebaseError, + 'auth/email-already-in-use' + ); + }); + }); +});