veritatisdoloremque
Version:
Monorepo for the Firebase JavaScript SDK
275 lines (231 loc) • 8.38 kB
text/typescript
/**
* @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 {
linkWithPhoneNumber,
PhoneAuthProvider,
reauthenticateWithPhoneNumber,
RecaptchaVerifier,
signInAnonymously,
signInWithPhoneNumber,
unlink,
updatePhoneNumber,
Auth,
OperationType,
ProviderId,
UserCredential,
ConfirmationResult
// eslint-disable-next-line import/no-extraneous-dependencies
} from '@firebase/auth-exp';
import { FirebaseError } from '@firebase/util';
import {
cleanUpTestInstance,
getTestInstance
} from '../../helpers/integration/helpers';
import { getPhoneVerificationCodes } from '../../helpers/integration/emulator_rest_helpers';
use(chaiAsPromised);
// NOTE: These tests don't use a real phone number. In order to run these tests
// you must whitelist the following phone numbers as "testing" numbers in the
// auth console
// https://console.firebase.google.com/u/0/project/_/authentication/providers
// • +1 (555) 555-1000, SMS code 123456
// • +1 (555) 555-2000, SMS code 654321
const PHONE_A = {
phoneNumber: '+15555551000',
code: '123456'
};
const PHONE_B = {
phoneNumber: '+15555552000',
code: '654321'
};
describe('Integration test: phone auth', () => {
let auth: Auth;
let verifier: RecaptchaVerifier;
let fakeRecaptchaContainer: HTMLElement;
beforeEach(() => {
auth = getTestInstance();
fakeRecaptchaContainer = document.createElement('div');
document.body.appendChild(fakeRecaptchaContainer);
verifier = new RecaptchaVerifier(
fakeRecaptchaContainer,
undefined as any,
auth
);
});
afterEach(async () => {
await cleanUpTestInstance(auth);
document.body.removeChild(fakeRecaptchaContainer);
});
function resetVerifier(): void {
verifier.clear();
verifier = new RecaptchaVerifier(
fakeRecaptchaContainer,
undefined as any,
auth
);
}
/** If in the emulator, search for the code in the API */
async function code(
crOrId: ConfirmationResult | string,
fallback: string
): Promise<string> {
if (auth.emulatorConfig) {
const codes = await getPhoneVerificationCodes();
const vid = typeof crOrId === 'string' ? crOrId : crOrId.verificationId;
return codes[vid].code;
}
return fallback;
}
it('allows user to sign up', async () => {
const cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier);
const userCred = await cr.confirm(await code(cr, PHONE_A.code));
expect(auth.currentUser).to.eq(userCred.user);
expect(userCred.operationType).to.eq(OperationType.SIGN_IN);
const user = userCred.user;
expect(user.isAnonymous).to.be.false;
expect(user.uid).to.be.a('string');
expect(user.phoneNumber).to.eq(PHONE_A.phoneNumber);
});
it('anonymous users can link (and unlink) phone number', async () => {
const { user } = await signInAnonymously(auth);
const { uid: anonId } = user;
const cr = await linkWithPhoneNumber(user, PHONE_A.phoneNumber, verifier);
const linkResult = await cr.confirm(await code(cr, PHONE_A.code));
expect(linkResult.operationType).to.eq(OperationType.LINK);
expect(linkResult.user.uid).to.eq(user.uid);
expect(linkResult.user.phoneNumber).to.eq(PHONE_A.phoneNumber);
await unlink(user, ProviderId.PHONE);
expect(auth.currentUser!.uid).to.eq(anonId);
// Is anonymous stays false even after unlinking
expect(auth.currentUser!.isAnonymous).to.be.false;
expect(auth.currentUser!.phoneNumber).to.be.null;
});
it('anonymous users can upgrade using phone number', async () => {
const { user } = await signInAnonymously(auth);
const { uid: anonId } = user;
const provider = new PhoneAuthProvider(auth);
const verificationId = await provider.verifyPhoneNumber(
PHONE_B.phoneNumber,
verifier
);
await updatePhoneNumber(
user,
PhoneAuthProvider.credential(
verificationId,
await code(verificationId, PHONE_B.code)
)
);
expect(user.phoneNumber).to.eq(PHONE_B.phoneNumber);
await auth.signOut();
resetVerifier();
const cr = await signInWithPhoneNumber(auth, PHONE_B.phoneNumber, verifier);
const { user: secondSignIn } = await cr.confirm(
await code(cr, PHONE_B.code)
);
expect(secondSignIn.uid).to.eq(anonId);
expect(secondSignIn.isAnonymous).to.be.false;
expect(secondSignIn.providerData[0].phoneNumber).to.eq(PHONE_B.phoneNumber);
expect(secondSignIn.providerData[0].providerId).to.eq('phone');
});
context('with already-created user', () => {
let signUpCred: UserCredential;
beforeEach(async () => {
const cr = await signInWithPhoneNumber(
auth,
PHONE_A.phoneNumber,
verifier
);
signUpCred = await cr.confirm(await code(cr, PHONE_A.code));
resetVerifier();
await auth.signOut();
});
it('allows the user to sign in again', async () => {
const cr = await signInWithPhoneNumber(
auth,
PHONE_A.phoneNumber,
verifier
);
const signInCred = await cr.confirm(await code(cr, PHONE_A.code));
expect(signInCred.user.uid).to.eq(signUpCred.user.uid);
});
it('allows the user to update their phone number', async () => {
let cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier);
const { user } = await cr.confirm(await code(cr, PHONE_A.code));
resetVerifier();
const provider = new PhoneAuthProvider(auth);
const verificationId = await provider.verifyPhoneNumber(
PHONE_B.phoneNumber,
verifier
);
await updatePhoneNumber(
user,
PhoneAuthProvider.credential(
verificationId,
await code(verificationId, PHONE_B.code)
)
);
expect(user.phoneNumber).to.eq(PHONE_B.phoneNumber);
await auth.signOut();
resetVerifier();
cr = await signInWithPhoneNumber(auth, PHONE_B.phoneNumber, verifier);
const { user: secondSignIn } = await cr.confirm(
await code(cr, PHONE_B.code)
);
expect(secondSignIn.uid).to.eq(user.uid);
});
it('allows the user to reauthenticate with phone number', async () => {
let cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier);
const { user } = await cr.confirm(await code(cr, PHONE_A.code));
const oldToken = await user.getIdToken();
resetVerifier();
// Wait a bit to ensure the sign in time is different in the token
await new Promise((resolve): void => {
setTimeout(resolve, 1500);
});
cr = await reauthenticateWithPhoneNumber(
user,
PHONE_A.phoneNumber,
verifier
);
await cr.confirm(await code(cr, PHONE_A.code));
expect(await user.getIdToken()).not.to.eq(oldToken);
});
it('prevents reauthentication with wrong phone number', async () => {
let cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier);
const { user } = await cr.confirm(await code(cr, PHONE_A.code));
resetVerifier();
cr = await reauthenticateWithPhoneNumber(
user,
PHONE_B.phoneNumber,
verifier
);
await expect(cr.confirm(await code(cr, PHONE_B.code))).to.be.rejectedWith(
FirebaseError,
'auth/user-mismatch'
);
// We need to manually delete PHONE_B number since a failed
// reauthenticateWithPhoneNumber does not trigger a state change
resetVerifier();
cr = await signInWithPhoneNumber(auth, PHONE_B.phoneNumber, verifier);
const { user: otherUser } = await cr.confirm(
await code(cr, PHONE_B.code)
);
await otherUser.delete();
});
});
});