From f0bfabf3ec76a5c26f16b646ef184de2d6a31c23 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 2 Mar 2021 17:05:49 -0800 Subject: [PATCH 1/5] Add tests for oob flows --- .../integration/emulator_rest_helpers.ts | 38 ++- .../test/helpers/integration/helpers.ts | 5 +- .../integration/flows/custom.local.test.ts | 6 +- .../test/integration/flows/oob.local.test.ts | 223 ++++++++++++++++++ 4 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 packages-exp/auth-exp/test/integration/flows/oob.local.test.ts diff --git a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts index 7cbc4393ae3..8f755591556 100644 --- a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts +++ b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts @@ -18,8 +18,20 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { Auth } from '@firebase/auth-exp'; import { getApps } from '@firebase/app-exp'; +import { FetchProvider } from '../../../src/core/util/fetch_provider'; +import * as fetchImpl from 'node-fetch'; -interface VerificationSession { +if (typeof document !== 'undefined') { + FetchProvider.initialize(fetch); +} else { + FetchProvider.initialize( + (fetchImpl.default as unknown) as typeof fetch, + (fetchImpl.Headers as unknown) as typeof Headers, + (fetchImpl.Response as unknown) as typeof Response + ); +} + +export interface VerificationSession { code: string; phoneNumber: string; sessionInfo: string; @@ -29,12 +41,23 @@ interface VerificationCodesResponse { verificationCodes: VerificationSession[]; } +export interface OobCodeSession { + email: string; + requestType: string; + oobCode: string; + oobLink: string; +} + +interface OobCodesResponse { + oobCodes: OobCodeSession[]; +} + export async function getPhoneVerificationCodes( auth: Auth ): Promise> { assertEmulator(auth); const url = getEmulatorUrl(auth, 'verificationCodes'); - const response: VerificationCodesResponse = await (await fetch(url)).json(); + const response: VerificationCodesResponse = await (await FetchProvider.fetch()(url)).json(); return response.verificationCodes.reduce((accum, session) => { accum[session.sessionInfo] = session; @@ -42,6 +65,17 @@ export async function getPhoneVerificationCodes( }, {} as Record); } +export async function getOobCodes(auth: Auth): Promise> { + assertEmulator(auth); + const url = getEmulatorUrl(auth, 'oobCodes'); + const response: OobCodesResponse = await (await FetchProvider.fetch()(url)).json(); + + return response.oobCodes.reduce((accum, session) => { + accum[session.email] = session; + return accum; + }, {} as Record); +} + function getEmulatorUrl(auth: Auth, endpoint: string): string { const { host, port, protocol } = auth.emulatorConfig!; const projectId = getProjectId(auth); diff --git a/packages-exp/auth-exp/test/helpers/integration/helpers.ts b/packages-exp/auth-exp/test/helpers/integration/helpers.ts index 4b0b091483d..5378365f358 100644 --- a/packages-exp/auth-exp/test/helpers/integration/helpers.ts +++ b/packages-exp/auth-exp/test/helpers/integration/helpers.ts @@ -31,7 +31,7 @@ export function randomEmail(): string { return `${_generateEventId('test.email.')}@test.com`; } -export function getTestInstance(): Auth { +export function getTestInstance(requireEmulator = false): Auth { const app = initializeApp(getAppConfig()); const createdUsers: User[] = []; @@ -43,6 +43,9 @@ export function getTestInstance(): Auth { const stub = stubConsoleToSilenceEmulatorWarnings(); useAuthEmulator(auth, emulatorUrl, { disableWarnings: true }); stub.restore(); + } else if (requireEmulator) { + /* Emulator wasn't configured but test must use emulator */ + throw new Error('Test may only be run using the Auth Emulator!'); } auth.onAuthStateChanged(user => { 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 index 08c7439fb6c..d482c8526bf 100644 --- a/packages-exp/auth-exp/test/integration/flows/custom.local.test.ts +++ b/packages-exp/auth-exp/test/integration/flows/custom.local.test.ts @@ -47,7 +47,7 @@ describe('Integration test: custom auth', () => { let uid: string; beforeEach(() => { - auth = getTestInstance(); + auth = getTestInstance(/* requireEmulator */ true); uid = randomEmail(); customToken = JSON.stringify({ uid, @@ -55,10 +55,6 @@ describe('Integration test: custom auth', () => { customClaim: 'some-claim' } }); - - if (!auth.emulatorConfig) { - throw new Error('Test can only be run against the emulator!'); - } }); afterEach(async () => { diff --git a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts new file mode 100644 index 00000000000..d3b059ce0d4 --- /dev/null +++ b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts @@ -0,0 +1,223 @@ +import {ActionCodeSettings, applyActionCode, Auth, confirmPasswordReset, createUserWithEmailAndPassword, deleteUser, EmailAuthProvider, fetchSignInMethodsForEmail, linkWithCredential, OperationType, reauthenticateWithCredential, sendEmailVerification, sendPasswordResetEmail, sendSignInLinkToEmail, signInAnonymously, SignInMethod, signInWithCredential, signInWithCustomToken, signInWithEmailAndPassword, signInWithEmailLink, updatePassword, verifyBeforeUpdateEmail, verifyPasswordResetCode} from '@firebase/auth-exp'; +import { FirebaseError } from '@firebase/util'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { getOobCodes, OobCodeSession } from '../../helpers/integration/emulator_rest_helpers'; +import { cleanUpTestInstance, getTestInstance, randomEmail } from '../../helpers/integration/helpers'; + +use(chaiAsPromised); + +const BASE_SETTINGS: ActionCodeSettings = { + url: 'http://localhost/action_code_return', + handleCodeInApp: true, +}; + +describe.only('Integration test: oob codes', () => { + let auth: Auth; + let email: string; + + beforeEach(() => { + auth = getTestInstance(/* requireEmulator */ true); + email = randomEmail(); + }); + + afterEach(async () => { + await cleanUpTestInstance(auth); + }); + + async function code(toEmail: string): Promise { + return (await getOobCodes(auth))[toEmail]; + } + + context('flows beginning with sendSignInLinkToEmail', () => { + let oobSession: OobCodeSession; + + beforeEach(async () => { + oobSession = await sendEmailLink(); + }); + + async function sendEmailLink(toEmail = email): Promise { + await sendSignInLinkToEmail(auth, toEmail, BASE_SETTINGS); + + // An email has been sent to the user. Normally you'd detect this state + // when the app redirects back. We will ask the emulator for the results + // and force the state instead. + return code(toEmail); + } + + it('allows user to sign in', async () => { + const {user, operationType} = await signInWithEmailLink(auth, email, oobSession.oobLink); + + expect(operationType).to.eq(OperationType.SIGN_IN); + expect(user).to.eq(auth.currentUser); + expect(user.uid).to.be.a('string'); + expect(user.email).to.eq(email); + expect(user.emailVerified).to.be.true; + expect(user.isAnonymous).to.be.false; + }); + + it('sign in works with an email credential', async () => { + const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user, operationType} = await signInWithCredential(auth, cred); + + expect(operationType).to.eq(OperationType.SIGN_IN); + expect(user).to.eq(auth.currentUser); + expect(user.uid).to.be.a('string'); + expect(user.email).to.eq(email); + expect(user.emailVerified).to.be.true; + expect(user.isAnonymous).to.be.false; + }); + + it('reauthenticate works with email credential', async () => { + let cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user: oldUser} = await signInWithCredential(auth, cred); + + const reauthSession = await sendEmailLink(); + cred = EmailAuthProvider.credentialWithLink(email, reauthSession.oobLink); + const {user: newUser, operationType} = await reauthenticateWithCredential(oldUser, cred); + + expect(newUser.uid).to.eq(oldUser.uid); + expect(operationType).to.eq(OperationType.REAUTHENTICATE); + expect(auth.currentUser).to.eq(newUser); + }); + + it('reauthenticate throws with different email', async () => { + let cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user: oldUser} = await signInWithCredential(auth, cred); + + const newEmail = randomEmail(); + const reauthSession = await sendEmailLink(newEmail); + cred = EmailAuthProvider.credentialWithLink(newEmail, reauthSession.oobLink); + await expect(reauthenticateWithCredential(oldUser, cred)).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); + expect(auth.currentUser).to.eq(oldUser); + }); + + it('reauthenticate throws if user is deleted', async () => { + let cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user: oldUser} = await signInWithCredential(auth, cred); + + await deleteUser(oldUser); + const reauthSession = await sendEmailLink(email); + cred = EmailAuthProvider.credentialWithLink(email, reauthSession.oobLink); + await expect(reauthenticateWithCredential(oldUser, cred)).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); + expect(auth.currentUser).to.be.null; + }); + + it('other accounts can be linked', async () => { + const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user: original} = await signInAnonymously(auth); + + expect(original.isAnonymous).to.be.true; + const {user: linked, operationType} = await linkWithCredential(original, cred); + + expect(operationType).to.eq(OperationType.LINK); + expect(linked.uid).to.eq(original.uid); + expect(linked.isAnonymous).to.be.false; + expect(auth.currentUser).to.eq(linked); + expect(linked.email).to.eq(email); + expect(linked.emailVerified).to.be.true; + }); + + it('can be linked to a custom token', async () => { + const {user: original} = await signInWithCustomToken(auth, JSON.stringify({ + uid: 'custom-uid', + })); + + const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user: linked} = await linkWithCredential(original, cred); + + expect(linked.uid).to.eq(original.uid); + expect(auth.currentUser).to.eq(linked); + expect(linked.email).to.eq(email); + expect(linked.emailVerified).to.be.true; + }); + + it('cannot link if original account is deleted', async () => { + const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); + const {user} = await signInAnonymously(auth); + + expect(user.isAnonymous).to.be.true; + await deleteUser(user); + await expect(linkWithCredential(user, cred)).to.be.rejectedWith(FirebaseError, 'auth/user-token-expired'); + }); + + it('code can only be used once', async () => { + const link = oobSession.oobLink; + await signInWithEmailLink(auth, email, link); + await expect(signInWithEmailLink(auth, email, link)).to.be.rejectedWith(FirebaseError, 'auth/invalid-action-code'); + }); + + it('fetchSignInMethodsForEmail returns the correct values', async () => { + const {user} = await signInWithEmailLink(auth, email, oobSession.oobLink); + expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([SignInMethod.EMAIL_LINK]); + + await updatePassword(user, 'password'); + const updatedMethods = await fetchSignInMethodsForEmail(auth, email); + expect(updatedMethods).to.have.length(2); + expect(updatedMethods).to.include(SignInMethod.EMAIL_LINK); + expect(updatedMethods).to.include(SignInMethod.EMAIL_PASSWORD); + }); + + it('throws an error if the wrong code is provided', async () => { + const otherSession = await sendEmailLink(randomEmail()); + await expect(signInWithEmailLink(auth, email, otherSession.oobLink)).to.be.rejectedWith(FirebaseError, 'auth/invalid-email'); + }); + }); + + it('can be used to verify email', async () => { + // Create an unverified user + const {user} = await createUserWithEmailAndPassword(auth, email, 'password'); + expect(user.emailVerified).to.be.false; + expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([SignInMethod.EMAIL_PASSWORD]); + await sendEmailVerification(user); + + // Apply the email verification code + await applyActionCode(auth, (await code(email)).oobCode); + await user.reload(); + expect(user.emailVerified).to.be.true; + }); + + it('can be used to initiate password reset', async () => { + const {user: original} = await createUserWithEmailAndPassword(auth, email, 'password'); + await sendEmailVerification(original); // Can only reset verified user emails + await applyActionCode(auth, (await code(email)).oobCode); + + // Send and confirm the password reset + await sendPasswordResetEmail(auth, email); + const oobCode = (await code(email)).oobCode; + expect(await verifyPasswordResetCode(auth, oobCode)).to.eq(email); + await confirmPasswordReset(auth, oobCode, 'new-password'); + + // Make sure the new password works and the old one doesn't + const {user} = await signInWithEmailAndPassword(auth, email, 'new-password'); + expect(user.uid).to.eq(original.uid); + expect(user.emailVerified).to.be.true; + expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([SignInMethod.EMAIL_PASSWORD]); + + await expect(signInWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith(FirebaseError, 'auth/wrong-password'); + }); + + // Test is ignored for now as the emulator does not currently support the + // verify-and-change-email operation. + xit('verifyBeforeUpdateEmail waits until flow completes', async () => { + const updatedEmail = randomEmail(); + + // Create an initial user with the basic email + await sendSignInLinkToEmail(auth, email, BASE_SETTINGS); + const {user} = await signInWithEmailLink(auth, email, (await code(email)).oobLink); + await verifyBeforeUpdateEmail(user, updatedEmail, BASE_SETTINGS); + expect(user.email).to.eq(email); + + // Finish the update email flow + await applyActionCode(auth, (await code(updatedEmail)).oobCode); + await user.reload(); + expect(user.emailVerified).to.be.true; + expect(user.email).to.eq(updatedEmail); + expect(auth.currentUser).to.eq(user); + + // Old email doesn't work but new one does + await expect(signInWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith(FirebaseError, 'auth/alskdjf'); + const {user: newSignIn} = await signInWithEmailAndPassword(auth, updatedEmail, 'password'); + expect(newSignIn.uid).to.eq(user.uid); + }); +}); \ No newline at end of file From df6a3e081e1517cb91bba430cbebbcc71191587f Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 2 Mar 2021 17:08:24 -0800 Subject: [PATCH 2/5] Formatting, license --- .../integration/emulator_rest_helpers.ts | 12 +- .../test/integration/flows/oob.local.test.ts | 221 ++++++++++++++---- 2 files changed, 185 insertions(+), 48 deletions(-) diff --git a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts index 8f755591556..753a81986c6 100644 --- a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts +++ b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts @@ -57,7 +57,9 @@ export async function getPhoneVerificationCodes( ): Promise> { assertEmulator(auth); const url = getEmulatorUrl(auth, 'verificationCodes'); - const response: VerificationCodesResponse = await (await FetchProvider.fetch()(url)).json(); + const response: VerificationCodesResponse = await ( + await FetchProvider.fetch()(url) + ).json(); return response.verificationCodes.reduce((accum, session) => { accum[session.sessionInfo] = session; @@ -65,10 +67,14 @@ export async function getPhoneVerificationCodes( }, {} as Record); } -export async function getOobCodes(auth: Auth): Promise> { +export async function getOobCodes( + auth: Auth +): Promise> { assertEmulator(auth); const url = getEmulatorUrl(auth, 'oobCodes'); - const response: OobCodesResponse = await (await FetchProvider.fetch()(url)).json(); + const response: OobCodesResponse = await ( + await FetchProvider.fetch()(url) + ).json(); return response.oobCodes.reduce((accum, session) => { accum[session.email] = session; diff --git a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts index d3b059ce0d4..aaffaf41c70 100644 --- a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts +++ b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts @@ -1,15 +1,63 @@ -import {ActionCodeSettings, applyActionCode, Auth, confirmPasswordReset, createUserWithEmailAndPassword, deleteUser, EmailAuthProvider, fetchSignInMethodsForEmail, linkWithCredential, OperationType, reauthenticateWithCredential, sendEmailVerification, sendPasswordResetEmail, sendSignInLinkToEmail, signInAnonymously, SignInMethod, signInWithCredential, signInWithCustomToken, signInWithEmailAndPassword, signInWithEmailLink, updatePassword, verifyBeforeUpdateEmail, verifyPasswordResetCode} from '@firebase/auth-exp'; +/** + * @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 { + ActionCodeSettings, + applyActionCode, + Auth, + confirmPasswordReset, + createUserWithEmailAndPassword, + deleteUser, + EmailAuthProvider, + fetchSignInMethodsForEmail, + linkWithCredential, + OperationType, + reauthenticateWithCredential, + sendEmailVerification, + sendPasswordResetEmail, + sendSignInLinkToEmail, + signInAnonymously, + SignInMethod, + signInWithCredential, + signInWithCustomToken, + signInWithEmailAndPassword, + signInWithEmailLink, + updatePassword, + verifyBeforeUpdateEmail, + verifyPasswordResetCode +} from '@firebase/auth-exp'; import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import { getOobCodes, OobCodeSession } from '../../helpers/integration/emulator_rest_helpers'; -import { cleanUpTestInstance, getTestInstance, randomEmail } from '../../helpers/integration/helpers'; +import { + getOobCodes, + OobCodeSession +} from '../../helpers/integration/emulator_rest_helpers'; +import { + cleanUpTestInstance, + getTestInstance, + randomEmail +} from '../../helpers/integration/helpers'; use(chaiAsPromised); const BASE_SETTINGS: ActionCodeSettings = { url: 'http://localhost/action_code_return', - handleCodeInApp: true, + handleCodeInApp: true }; describe.only('Integration test: oob codes', () => { @@ -31,7 +79,7 @@ describe.only('Integration test: oob codes', () => { context('flows beginning with sendSignInLinkToEmail', () => { let oobSession: OobCodeSession; - + beforeEach(async () => { oobSession = await sendEmailLink(); }); @@ -46,7 +94,11 @@ describe.only('Integration test: oob codes', () => { } it('allows user to sign in', async () => { - const {user, operationType} = await signInWithEmailLink(auth, email, oobSession.oobLink); + const { user, operationType } = await signInWithEmailLink( + auth, + email, + oobSession.oobLink + ); expect(operationType).to.eq(OperationType.SIGN_IN); expect(user).to.eq(auth.currentUser); @@ -57,8 +109,11 @@ describe.only('Integration test: oob codes', () => { }); it('sign in works with an email credential', async () => { - const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user, operationType} = await signInWithCredential(auth, cred); + const cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user, operationType } = await signInWithCredential(auth, cred); expect(operationType).to.eq(OperationType.SIGN_IN); expect(user).to.eq(auth.currentUser); @@ -69,12 +124,18 @@ describe.only('Integration test: oob codes', () => { }); it('reauthenticate works with email credential', async () => { - let cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user: oldUser} = await signInWithCredential(auth, cred); + let cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: oldUser } = await signInWithCredential(auth, cred); const reauthSession = await sendEmailLink(); cred = EmailAuthProvider.credentialWithLink(email, reauthSession.oobLink); - const {user: newUser, operationType} = await reauthenticateWithCredential(oldUser, cred); + const { + user: newUser, + operationType + } = await reauthenticateWithCredential(oldUser, cred); expect(newUser.uid).to.eq(oldUser.uid); expect(operationType).to.eq(OperationType.REAUTHENTICATE); @@ -82,33 +143,52 @@ describe.only('Integration test: oob codes', () => { }); it('reauthenticate throws with different email', async () => { - let cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user: oldUser} = await signInWithCredential(auth, cred); + let cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: oldUser } = await signInWithCredential(auth, cred); const newEmail = randomEmail(); const reauthSession = await sendEmailLink(newEmail); - cred = EmailAuthProvider.credentialWithLink(newEmail, reauthSession.oobLink); - await expect(reauthenticateWithCredential(oldUser, cred)).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); + cred = EmailAuthProvider.credentialWithLink( + newEmail, + reauthSession.oobLink + ); + await expect( + reauthenticateWithCredential(oldUser, cred) + ).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); expect(auth.currentUser).to.eq(oldUser); }); it('reauthenticate throws if user is deleted', async () => { - let cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user: oldUser} = await signInWithCredential(auth, cred); + let cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: oldUser } = await signInWithCredential(auth, cred); await deleteUser(oldUser); const reauthSession = await sendEmailLink(email); cred = EmailAuthProvider.credentialWithLink(email, reauthSession.oobLink); - await expect(reauthenticateWithCredential(oldUser, cred)).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); + await expect( + reauthenticateWithCredential(oldUser, cred) + ).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); expect(auth.currentUser).to.be.null; }); it('other accounts can be linked', async () => { - const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user: original} = await signInAnonymously(auth); + const cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: original } = await signInAnonymously(auth); expect(original.isAnonymous).to.be.true; - const {user: linked, operationType} = await linkWithCredential(original, cred); + const { user: linked, operationType } = await linkWithCredential( + original, + cred + ); expect(operationType).to.eq(OperationType.LINK); expect(linked.uid).to.eq(original.uid); @@ -119,13 +199,19 @@ describe.only('Integration test: oob codes', () => { }); it('can be linked to a custom token', async () => { - const {user: original} = await signInWithCustomToken(auth, JSON.stringify({ - uid: 'custom-uid', - })); + const { user: original } = await signInWithCustomToken( + auth, + JSON.stringify({ + uid: 'custom-uid' + }) + ); + + const cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: linked } = await linkWithCredential(original, cred); - const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user: linked} = await linkWithCredential(original, cred); - expect(linked.uid).to.eq(original.uid); expect(auth.currentUser).to.eq(linked); expect(linked.email).to.eq(email); @@ -133,23 +219,38 @@ describe.only('Integration test: oob codes', () => { }); it('cannot link if original account is deleted', async () => { - const cred = EmailAuthProvider.credentialWithLink(email, oobSession.oobLink); - const {user} = await signInAnonymously(auth); + const cred = EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user } = await signInAnonymously(auth); expect(user.isAnonymous).to.be.true; await deleteUser(user); - await expect(linkWithCredential(user, cred)).to.be.rejectedWith(FirebaseError, 'auth/user-token-expired'); + await expect(linkWithCredential(user, cred)).to.be.rejectedWith( + FirebaseError, + 'auth/user-token-expired' + ); }); it('code can only be used once', async () => { const link = oobSession.oobLink; await signInWithEmailLink(auth, email, link); - await expect(signInWithEmailLink(auth, email, link)).to.be.rejectedWith(FirebaseError, 'auth/invalid-action-code'); + await expect(signInWithEmailLink(auth, email, link)).to.be.rejectedWith( + FirebaseError, + 'auth/invalid-action-code' + ); }); it('fetchSignInMethodsForEmail returns the correct values', async () => { - const {user} = await signInWithEmailLink(auth, email, oobSession.oobLink); - expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([SignInMethod.EMAIL_LINK]); + const { user } = await signInWithEmailLink( + auth, + email, + oobSession.oobLink + ); + expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([ + SignInMethod.EMAIL_LINK + ]); await updatePassword(user, 'password'); const updatedMethods = await fetchSignInMethodsForEmail(auth, email); @@ -160,15 +261,23 @@ describe.only('Integration test: oob codes', () => { it('throws an error if the wrong code is provided', async () => { const otherSession = await sendEmailLink(randomEmail()); - await expect(signInWithEmailLink(auth, email, otherSession.oobLink)).to.be.rejectedWith(FirebaseError, 'auth/invalid-email'); + await expect( + signInWithEmailLink(auth, email, otherSession.oobLink) + ).to.be.rejectedWith(FirebaseError, 'auth/invalid-email'); }); }); it('can be used to verify email', async () => { // Create an unverified user - const {user} = await createUserWithEmailAndPassword(auth, email, 'password'); + const { user } = await createUserWithEmailAndPassword( + auth, + email, + 'password' + ); expect(user.emailVerified).to.be.false; - expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([SignInMethod.EMAIL_PASSWORD]); + expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([ + SignInMethod.EMAIL_PASSWORD + ]); await sendEmailVerification(user); // Apply the email verification code @@ -178,8 +287,12 @@ describe.only('Integration test: oob codes', () => { }); it('can be used to initiate password reset', async () => { - const {user: original} = await createUserWithEmailAndPassword(auth, email, 'password'); - await sendEmailVerification(original); // Can only reset verified user emails + const { user: original } = await createUserWithEmailAndPassword( + auth, + email, + 'password' + ); + await sendEmailVerification(original); // Can only reset verified user emails await applyActionCode(auth, (await code(email)).oobCode); // Send and confirm the password reset @@ -189,12 +302,20 @@ describe.only('Integration test: oob codes', () => { await confirmPasswordReset(auth, oobCode, 'new-password'); // Make sure the new password works and the old one doesn't - const {user} = await signInWithEmailAndPassword(auth, email, 'new-password'); + const { user } = await signInWithEmailAndPassword( + auth, + email, + 'new-password' + ); expect(user.uid).to.eq(original.uid); expect(user.emailVerified).to.be.true; - expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([SignInMethod.EMAIL_PASSWORD]); + expect(await fetchSignInMethodsForEmail(auth, email)).to.eql([ + SignInMethod.EMAIL_PASSWORD + ]); - await expect(signInWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith(FirebaseError, 'auth/wrong-password'); + await expect( + signInWithEmailAndPassword(auth, email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/wrong-password'); }); // Test is ignored for now as the emulator does not currently support the @@ -204,7 +325,11 @@ describe.only('Integration test: oob codes', () => { // Create an initial user with the basic email await sendSignInLinkToEmail(auth, email, BASE_SETTINGS); - const {user} = await signInWithEmailLink(auth, email, (await code(email)).oobLink); + const { user } = await signInWithEmailLink( + auth, + email, + (await code(email)).oobLink + ); await verifyBeforeUpdateEmail(user, updatedEmail, BASE_SETTINGS); expect(user.email).to.eq(email); @@ -216,8 +341,14 @@ describe.only('Integration test: oob codes', () => { expect(auth.currentUser).to.eq(user); // Old email doesn't work but new one does - await expect(signInWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith(FirebaseError, 'auth/alskdjf'); - const {user: newSignIn} = await signInWithEmailAndPassword(auth, updatedEmail, 'password'); + await expect( + signInWithEmailAndPassword(auth, email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/alskdjf'); + const { user: newSignIn } = await signInWithEmailAndPassword( + auth, + updatedEmail, + 'password' + ); expect(newSignIn.uid).to.eq(user.uid); }); -}); \ No newline at end of file +}); From 0c74b34c4970c95e659ff95ab07520624f200606 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 2 Mar 2021 17:16:25 -0800 Subject: [PATCH 3/5] Fix linter problems --- .../auth-exp/test/integration/flows/oob.local.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts index aaffaf41c70..df4d746def8 100644 --- a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts +++ b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts @@ -39,6 +39,7 @@ import { updatePassword, verifyBeforeUpdateEmail, verifyPasswordResetCode + // eslint-disable-next-line import/no-extraneous-dependencies } from '@firebase/auth-exp'; import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; @@ -55,12 +56,14 @@ import { use(chaiAsPromised); +declare const xit: typeof it; + const BASE_SETTINGS: ActionCodeSettings = { url: 'http://localhost/action_code_return', handleCodeInApp: true }; -describe.only('Integration test: oob codes', () => { +describe('Integration test: oob codes', () => { let auth: Auth; let email: string; From 9fd8b28551c2cc5c0ed4201ef49bb52cbb7558b1 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Wed, 3 Mar 2021 08:45:56 -0800 Subject: [PATCH 4/5] PR feedback --- .../test/helpers/integration/emulator_rest_helpers.ts | 8 ++------ .../auth-exp/test/integration/flows/oob.local.test.ts | 3 ++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts index 753a81986c6..433ee2b70c6 100644 --- a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts +++ b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts @@ -69,17 +69,13 @@ export async function getPhoneVerificationCodes( export async function getOobCodes( auth: Auth -): Promise> { +): Promise { assertEmulator(auth); const url = getEmulatorUrl(auth, 'oobCodes'); const response: OobCodesResponse = await ( await FetchProvider.fetch()(url) ).json(); - - return response.oobCodes.reduce((accum, session) => { - accum[session.email] = session; - return accum; - }, {} as Record); + return response.oobCodes; } function getEmulatorUrl(auth: Auth, endpoint: string): string { diff --git a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts index df4d746def8..80647a0f62b 100644 --- a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts +++ b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts @@ -77,7 +77,8 @@ describe('Integration test: oob codes', () => { }); async function code(toEmail: string): Promise { - return (await getOobCodes(auth))[toEmail]; + const codes = await getOobCodes(auth); + return codes.reverse().find(({email}) => email === toEmail)!; } context('flows beginning with sendSignInLinkToEmail', () => { From 7a2274ae7224d4140823f51b1d2b61dcd9737d10 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Wed, 3 Mar 2021 08:46:24 -0800 Subject: [PATCH 5/5] Formatting --- .../test/helpers/integration/emulator_rest_helpers.ts | 4 +--- .../auth-exp/test/integration/flows/oob.local.test.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts index 433ee2b70c6..9ec13f09b15 100644 --- a/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts +++ b/packages-exp/auth-exp/test/helpers/integration/emulator_rest_helpers.ts @@ -67,9 +67,7 @@ export async function getPhoneVerificationCodes( }, {} as Record); } -export async function getOobCodes( - auth: Auth -): Promise { +export async function getOobCodes(auth: Auth): Promise { assertEmulator(auth); const url = getEmulatorUrl(auth, 'oobCodes'); const response: OobCodesResponse = await ( diff --git a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts index 80647a0f62b..6c699290fce 100644 --- a/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts +++ b/packages-exp/auth-exp/test/integration/flows/oob.local.test.ts @@ -78,7 +78,7 @@ describe('Integration test: oob codes', () => { async function code(toEmail: string): Promise { const codes = await getOobCodes(auth); - return codes.reverse().find(({email}) => email === toEmail)!; + return codes.reverse().find(({ email }) => email === toEmail)!; } context('flows beginning with sendSignInLinkToEmail', () => {