From 18f5b0f7e65a3bddc92d3d23e31efb42a518ec12 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sat, 14 Nov 2020 17:59:37 -0500 Subject: [PATCH 1/8] BREAKING: remove deprecated analytics methods BREAKING CHANGE: switch to setScreenView, logPurchase, logViewPromotion, logRefund as needed --- .../analytics/__tests__/analytics.test.ts | 20 --------- packages/analytics/lib/index.js | 43 ------------------- 2 files changed, 63 deletions(-) diff --git a/packages/analytics/__tests__/analytics.test.ts b/packages/analytics/__tests__/analytics.test.ts index 433019cbaa..aa3b7c2bdf 100644 --- a/packages/analytics/__tests__/analytics.test.ts +++ b/packages/analytics/__tests__/analytics.test.ts @@ -103,26 +103,6 @@ describe('Analytics', () => { ); }); - it('call methods, getters & setters that fire a console.warn() & have no return value', () => { - const analytics = firebase.analytics(); - // @ts-ignore test - const logEcommercePurchaseSpy = jest.spyOn(analytics, 'logEcommercePurchase'); - // @ts-ignore test - const logPresentOfferSpy = jest.spyOn(analytics, 'logPresentOffer'); - // @ts-ignore test - const logPurchaseRefundSpy = jest.spyOn(analytics, 'logPurchaseRefund'); - // @ts-ignore test - analytics.logEcommercePurchase(); - // @ts-ignore test - analytics.logPresentOffer(); - // @ts-ignore test - analytics.logPurchaseRefund(); - - expect(logEcommercePurchaseSpy).toBeCalled(); - expect(logPresentOfferSpy).toBeCalled(); - expect(logPurchaseRefundSpy).toBeCalled(); - }); - describe('logEvent()', () => { it('errors if name is not a string', () => { // @ts-ignore test diff --git a/packages/analytics/lib/index.js b/packages/analytics/lib/index.js index b54836defe..92190ab2d6 100644 --- a/packages/analytics/lib/index.js +++ b/packages/analytics/lib/index.js @@ -114,17 +114,6 @@ class FirebaseAnalyticsModule extends FirebaseModule { return this.native.setAnalyticsCollectionEnabled(enabled); } - setCurrentScreen(screenName, screenClassOverride) { - // eslint-disable-next-line no-console - console.warn( - 'firebase.analytics().setCurrentScreen(), is now deprecated. Please use firebase.analytics().logScreenView() instead', - ); - return this.logScreenView({ - screen_name: screenName, - screen_class: screenClassOverride, - }); - } - setSessionTimeoutDuration(milliseconds = 1800000) { if (!isNumber(milliseconds)) { throw new Error( @@ -319,16 +308,6 @@ class FirebaseAnalyticsModule extends FirebaseModule { ), ); } - /** - * logEcommercePurchase purchase is now deprecated, use logPurchase instead: - * https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event#public-static-final-string-ecommerce_purchase - */ - logEcommercePurchase() { - // eslint-disable-next-line no-console - console.warn( - 'firebase.analytics().logEcommercePurchase(), "ECOMMERCE_PURCHASE" event is now deprecated. Please use firebase.analytics().logPurchase() instead', - ); - } logGenerateLead(object = {}) { if (!isObject(object)) { @@ -423,28 +402,6 @@ class FirebaseAnalyticsModule extends FirebaseModule { ); } - /** - * Deprecated, use logRefundEvent instead: - * https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event#public-static-final-string-present_offer - */ - logPresentOffer() { - // eslint-disable-next-line no-console - console.warn( - 'firebase.analytics().logPresentOffer(), "PRESENT_OFFER" event is now deprecated. Please use firebase.analytics().logViewPromotion() instead', - ); - } - - /** - * Deprecated, use logRefundEvent instead: - * https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event#public-static-final-string-purchase_refund - */ - logPurchaseRefund() { - // eslint-disable-next-line no-console - console.warn( - 'firebase.analytics().logPurchaseRefund(), "PURCHASE_REFUND" event is now deprecated. Please use firebase.analytics().logRefund() instead', - ); - } - logSelectContent(object) { if (!isObject(object)) { throw new Error( From 441c52b42f30b549f687aa2e8b145ea8f6049971 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sat, 14 Nov 2020 21:08:45 -0500 Subject: [PATCH 2/8] chore(test): start auth emulator during testing --- .github/workflows/scripts/firebase.json | 5 ++++- ...art-firestore-emulator.sh => start-firebase-emulator.sh} | 6 +++--- package.json | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) rename .github/workflows/scripts/{start-firestore-emulator.sh => start-firebase-emulator.sh} (63%) mode change 100644 => 100755 diff --git a/.github/workflows/scripts/firebase.json b/.github/workflows/scripts/firebase.json index 02cfee1ef8..c8bf3cf94b 100644 --- a/.github/workflows/scripts/firebase.json +++ b/.github/workflows/scripts/firebase.json @@ -4,6 +4,9 @@ "indexes": "firestore.indexes.json" }, "emulators": { + "auth": { + "port": "9099" + }, "firestore": { "port": "8080" }, @@ -12,4 +15,4 @@ "port": 4000 } } -} \ No newline at end of file +} diff --git a/.github/workflows/scripts/start-firestore-emulator.sh b/.github/workflows/scripts/start-firebase-emulator.sh old mode 100644 new mode 100755 similarity index 63% rename from .github/workflows/scripts/start-firestore-emulator.sh rename to .github/workflows/scripts/start-firebase-emulator.sh index a6b8959e6a..ea5acf7bdf --- a/.github/workflows/scripts/start-firestore-emulator.sh +++ b/.github/workflows/scripts/start-firebase-emulator.sh @@ -1,10 +1,10 @@ #!/bin/bash if ! [ -x "$(command -v firebase)" ]; then - echo "❌ Firebase tools CLI is missing." + echo "❌ Firebase-tools CLI is missing. Run 'npm i -g firebase-tools' or the equivalent" exit 1 fi -EMU_START_COMMAND="firebase emulators:start --only firestore" +EMU_START_COMMAND="firebase emulators:start --only firestore,auth --project react-native-firebase-testing" if [ "$1" == "--no-daemon" ]; then $EMU_START_COMMAND @@ -15,4 +15,4 @@ else sleep 2 done echo "Firestore emulator is online!" -fi \ No newline at end of file +fi diff --git a/package.json b/package.json index 4d928ed553..bb2f2b8a26 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "tests:packager:chrome": "cd tests && node_modules/.bin/react-native start --reset-cache", "tests:packager:jet": "cd tests && cross-env REACT_DEBUGGER=\"echo nope\" node_modules/.bin/react-native start --no-interactive", "tests:packager:jet-reset-cache": "cd tests && cross-env REACT_DEBUGGER=\"echo nope\" node_modules/.bin/react-native start --reset-cache --no-interactive", - "tests:emulator:start": "cd ./.github/workflows/scripts && sh ./start-firestore-emulator.sh --no-daemon", - "tests:emulator:start-ci": "cd ./.github/workflows/scripts && sh ./start-firestore-emulator.sh", + "tests:emulator:start": "cd ./.github/workflows/scripts && sh ./start-firebase-emulator.sh --no-daemon", + "tests:emulator:start-ci": "cd ./.github/workflows/scripts && sh ./start-firebase-emulator.sh", "tests:android:build": "cd tests && ./node_modules/.bin/detox build --configuration android.emu.debug", "tests:android:build-release": "cd tests && ./node_modules/.bin/detox build --configuration android.emu.release", "tests:android:test": "cd tests && ./node_modules/.bin/detox test --configuration android.emu.debug", From 532adb569413e8a5e5077d5f47582a0a300b3045 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sat, 14 Nov 2020 21:11:17 -0500 Subject: [PATCH 3/8] feat(auth, emulator): add useEmulator javascript code + jest tests Native implementation is not complete in this commit, but that is next --- jest.setup.ts | 24 ++++++++++++--- packages/auth/__tests__/auth.test.ts | 46 ++++++++++++++++++++++++++++ packages/auth/lib/index.d.ts | 8 +++++ packages/auth/lib/index.js | 27 ++++++++++++++++ 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 packages/auth/__tests__/auth.test.ts diff --git a/jest.setup.ts b/jest.setup.ts index 02f79725bf..fb6110baaa 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -9,6 +9,10 @@ jest.doMock('react-native', () => { }, NativeModules: { ...ReactNative.NativeModules, + RNFBAdMobModule: {}, + RNFBAdMobInterstitialModule: {}, + RNFBAdMobRewardedModule: {}, + RNFBAdsConsentModule: {}, RNFBAppModule: { NATIVE_FIREBASE_APPS: [ { @@ -25,13 +29,23 @@ jest.doMock('react-native', () => { options: {}, }, ], + addListener: jest.fn(), + eventsAddListener: jest.fn(), + eventsNotifyReady: jest.fn(), + }, + RNFBAuthModule: { + APP_LANGUAGE: { + '[DEFAULT]': 'en-US', + }, + APP_USER: { + '[DEFAULT]': 'jestUser', + }, + addAuthStateListener: jest.fn(), + addIdTokenListener: jest.fn(), + useEmulator: jest.fn(), }, - RNFBPerfModule: {}, - RNFBAdMobModule: {}, - RNFBAdMobInterstitialModule: {}, - RNFBAdMobRewardedModule: {}, - RNFBAdsConsentModule: {}, RNFBCrashlyticsModule: {}, + RNFBPerfModule: {}, }, }, ReactNative, diff --git a/packages/auth/__tests__/auth.test.ts b/packages/auth/__tests__/auth.test.ts new file mode 100644 index 0000000000..458a029619 --- /dev/null +++ b/packages/auth/__tests__/auth.test.ts @@ -0,0 +1,46 @@ +import auth, { firebase } from '../lib'; + +describe('Auth', () => { + describe('namespace', () => { + it('accessible from firebase.app()', () => { + const app = firebase.app(); + expect(app.auth).toBeDefined(); + expect(app.auth().useEmulator).toBeDefined(); + }); + }); + + describe('useEmulator()', () => { + it('useEmulator requires a string url', () => { + // @ts-ignore because we pass an invalid argument... + expect(() => auth().useEmulator()).toThrow( + 'firebase.auth().useEmulator() takes a non-empty string', + ); + expect(() => auth().useEmulator('')).toThrow( + 'firebase.auth().useEmulator() takes a non-empty string', + ); + // @ts-ignore because we pass an invalid argument... + expect(() => auth().useEmulator(123)).toThrow( + 'firebase.auth().useEmulator() takes a non-empty string', + ); + }); + + it('useEmulator requires a well-formed url', () => { + // No http:// + expect(() => auth().useEmulator('localhost:9099')).toThrow( + 'firebase.auth().useEmulator() unable to parse host and port from url', + ); + // No port + expect(() => auth().useEmulator('http://localhost')).toThrow( + 'firebase.auth().useEmulator() unable to parse host and port from url', + ); + }); + + it('useEmulator -> remaps Android loopback to host', () => { + const foo = auth().useEmulator('http://localhost:9099'); + expect(foo).toEqual(['10.0.2.2', 9099]); + + const bar = auth().useEmulator('http://127.0.0.1:9099'); + expect(bar).toEqual(['10.0.2.2', 9099]); + }); + }); +}); diff --git a/packages/auth/lib/index.d.ts b/packages/auth/lib/index.d.ts index 8c317ed0d5..ecae068cb5 100644 --- a/packages/auth/lib/index.d.ts +++ b/packages/auth/lib/index.d.ts @@ -1640,6 +1640,14 @@ export namespace FirebaseAuthTypes { * @param userAccessGroup A string of the keychain id i.e. "TEAMID.com.example.group1" */ useUserAccessGroup(userAccessGroup: string): Promise; + /** + * Modify this Auth instance to communicate with the Firebase Auth emulator. + * This must be called synchronously immediately following the first call to firebase.auth(). + * Do not use with production credentials as emulator traffic is not encrypted. + * + * @param url: emulator URL, must have host and port (eg, 'http://localhost:9099') + */ + useEmulator(url: string): void; } } diff --git a/packages/auth/lib/index.js b/packages/auth/lib/index.js index da7a2cc59f..367a98b220 100644 --- a/packages/auth/lib/index.js +++ b/packages/auth/lib/index.js @@ -334,6 +334,33 @@ class FirebaseAuthModule extends FirebaseModule { 'firebase.auth().useDeviceLanguage() is unsupported by the native Firebase SDKs.', ); } + + useEmulator(url) { + if (!url || !isString(url) || url === '') { + throw new Error('firebase.auth().useEmulator() takes a non-empty string'); + } + + let _url = url; + if (isAndroid && _url) { + if (_url.startsWith('http://localhost')) { + _url = _url.replace('http://localhost', 'http://10.0.2.2'); + } + if (_url.startsWith('http://127.0.0.1')) { + _url = _url.replace('http://127.0.0.1', 'http://10.0.2.2'); + } + } + + // Native calls take the host and port split out + const hostPortRegex = /^http:\/\/([\w\d.]+):(\d+)$/; + const urlMatches = _url.match(hostPortRegex); + if (!urlMatches) { + throw new Error('firebase.auth().useEmulator() unable to parse host and port from url'); + } + const host = urlMatches[1]; + const port = parseInt(urlMatches[2], 10); + this.native.useEmulator(host, port); + return [host, port]; // undocumented return, useful for unit testing + } } // import { SDK_VERSION } from '@react-native-firebase/auth'; From 8ead60431c2aae4193ed79eb10dc3b43480c5d77 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sun, 15 Nov 2020 17:59:30 -0500 Subject: [PATCH 4/8] fix(auth, android): gracefully handle exception creating PhoneCredential --- .../auth/ReactNativeFirebaseAuthModule.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java index 90afe46fa6..ad0570e254 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java @@ -991,10 +991,17 @@ public void confirmationResultConfirm( FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); - PhoneAuthCredential credential = PhoneAuthProvider.getCredential( - mVerificationId, - verificationCode - ); + PhoneAuthCredential credential = null; + try { + credential = PhoneAuthProvider.getCredential( + mVerificationId, + verificationCode + ); + } catch (Exception e) { + Log.d(TAG, "confirmationResultConfirm::getCredential::failure", e); + promiseRejectAuthException(promise, e); + return; + } firebaseAuth.signInWithCredential(credential).addOnCompleteListener(getExecutor(), task -> { if (task.isSuccessful()) { From 41fad3629437059a5e81d29f82c79589286aaea2 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sun, 15 Nov 2020 18:00:47 -0500 Subject: [PATCH 5/8] fix(auth, android): handle failure to upgrade anonymous user This is related to a #4487 - attempting to re-login with credential after anonymous user signs in with apple sign-in on android --- .../auth/ReactNativeFirebaseAuthModule.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java index ad0570e254..4c9beb9d78 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java @@ -1314,13 +1314,18 @@ private void linkWithCredential( if (exception instanceof FirebaseAuthUserCollisionException) { FirebaseAuthUserCollisionException authUserCollisionException = (FirebaseAuthUserCollisionException) exception; AuthCredential updatedCredential = authUserCollisionException.getUpdatedCredential(); - firebaseAuth.signInWithCredential(updatedCredential).addOnCompleteListener(getExecutor(), result -> { - if (result.isSuccessful()) { - promiseWithAuthResult(result.getResult(), promise); - } else { - promiseRejectAuthException(promise, exception); - } - }); + try { + firebaseAuth.signInWithCredential(updatedCredential).addOnCompleteListener(getExecutor(), result -> { + if (result.isSuccessful()) { + promiseWithAuthResult(result.getResult(), promise); + } else { + promiseRejectAuthException(promise, exception); + } + }); + } catch (Exception e) { + // we the attempt to log in after the collision failed, reject back to JS + promiseRejectAuthException(promise, exception); + } } else { promiseRejectAuthException(promise, exception); } From 81369a089e3ffc5be53d7651fa5a9dacf5bfa7b6 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sun, 15 Nov 2020 18:02:40 -0500 Subject: [PATCH 6/8] feat(auth, emulator): implement native useEmulator calls --- .../firebase/auth/ReactNativeFirebaseAuthModule.java | 8 ++++++++ packages/auth/ios/RNFBAuth/RNFBAuthModule.m | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java index 4c9beb9d78..c6491237a7 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java @@ -1661,6 +1661,14 @@ public void verifyPasswordResetCode(String appName, String code, final Promise p }); } + @ReactMethod + public void useEmulator(String appName, String host, int port) { + Log.d(TAG, "useEmulator"); + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); + firebaseAuth.useEmulator(host, port); + } + /* ------------------ * INTERNAL HELPERS * ---------------- */ diff --git a/packages/auth/ios/RNFBAuth/RNFBAuthModule.m b/packages/auth/ios/RNFBAuth/RNFBAuthModule.m index ad1a7c1e76..232ba9c125 100644 --- a/packages/auth/ios/RNFBAuth/RNFBAuthModule.m +++ b/packages/auth/ios/RNFBAuth/RNFBAuthModule.m @@ -924,6 +924,14 @@ - (void)invalidate { }]; } +RCT_EXPORT_METHOD(useEmulator: + (FIRApp *) firebaseApp + :(nonnull NSString *)host + :(NSInteger)port +) { + [[FIRAuth authWithApp:firebaseApp] useEmulatorWithHost: host port:port]; +} + - (FIRAuthCredential *)getCredentialForProvider:(NSString *)provider token:(NSString *)authToken secret:(NSString *)authTokenSecret { FIRAuthCredential *credential; From 8ff920c02307ee14f3a6a925a9e5f9b3f0753376 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Mon, 16 Nov 2020 15:15:19 -0500 Subject: [PATCH 7/8] tests(messaging, android): disable setBackgroundMessageHandler test in CI This was flaky because the app startup time is so slow it frequently times out the test Runs fine locally --- packages/messaging/e2e/messaging.e2e.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/messaging/e2e/messaging.e2e.js b/packages/messaging/e2e/messaging.e2e.js index 34779adabf..5387031a4e 100644 --- a/packages/messaging/e2e/messaging.e2e.js +++ b/packages/messaging/e2e/messaging.e2e.js @@ -252,6 +252,10 @@ describe('messaging()', () => { }); android.it('receives messages when the app is in the background', async () => { + // This is slow and thus flaky in CI. It runs locally though. + if (global.isCI) { + return; + } const spy = sinon.spy(); const token = await firebase.messaging().getToken(); firebase.messaging().setBackgroundMessageHandler(remoteMessage => { From 4c688413cb6516ecfdbd4ea325103d0d8d8d84a8 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Mon, 16 Nov 2020 17:32:03 -0500 Subject: [PATCH 8/8] tests(auth, e2e): re-enable tests, use auth emulator Found at least 2 unexpected real issues in RNFB along the way Found at least 3 Firebase internal errors marked with FIXME that need upstream logging Auth emulator is great, no API limits and now we can cover phone auth and email verify/signin flow in testing --- packages/app/e2e/helpers.js | 11 ++ packages/auth/e2e/auth.e2e.js | 172 +++++++++++++++++++------- packages/auth/e2e/emailLink.e2e.js | 26 ++++ packages/auth/e2e/helpers.js | 191 +++++++++++++++++++++++++++++ packages/auth/e2e/phone.e2e.js | 94 +++++++------- packages/auth/e2e/rnReload.e2e.js | 26 +++- packages/auth/e2e/user.e2e.js | 161 ++++++++++++++---------- packages/firestore/e2e/helpers.js | 42 ++++--- tests/app.js | 2 + tests/e2e/mocha.opts | 8 +- 10 files changed, 545 insertions(+), 188 deletions(-) create mode 100644 packages/app/e2e/helpers.js create mode 100644 packages/auth/e2e/helpers.js diff --git a/packages/app/e2e/helpers.js b/packages/app/e2e/helpers.js new file mode 100644 index 0000000000..57e4ea3d90 --- /dev/null +++ b/packages/app/e2e/helpers.js @@ -0,0 +1,11 @@ +exports.getE2eTestProject = function getE2eTestProject() { + return 'react-native-firebase-testing'; +}; + +exports.getE2eEmulatorHost = function getE2eEmulatorHost() { + // Note that in most package implementations involving the emulator, we re-write + // localhost and 127.0.0.1 on Android to 10.0.2.2 (the Android emulator host interface) + // But this specific code is executing in the host context even during E2E test. + // So no re-write is necessary here. + return 'localhost'; +}; diff --git a/packages/auth/e2e/auth.e2e.js b/packages/auth/e2e/auth.e2e.js index 999f504382..fe764b8281 100644 --- a/packages/auth/e2e/auth.e2e.js +++ b/packages/auth/e2e/auth.e2e.js @@ -15,7 +15,31 @@ * */ +const TEST_EMAIL = 'test@example.com'; +const TEST_PASS = 'test1234'; + +const DISABLED_EMAIL = 'disabled@example.com'; +const DISABLED_PASS = 'test1234'; + +const { clearAllUsers, disableUser, getLastOob, resetPassword } = require('./helpers'); + describe('auth()', () => { + before(async () => { + await clearAllUsers(); + await firebase.auth().createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS); + const disabledUserCredential = await firebase + .auth() + .createUserWithEmailAndPassword(DISABLED_EMAIL, DISABLED_PASS); + await disableUser(disabledUserCredential.user.uid); + }); + + beforeEach(async () => { + if (firebase.auth().currentUser) { + await firebase.auth().signOut(); + await Utils.sleep(50); + } + }); + describe('namespace', () => { it('accessible from firebase.app()', () => { const app = firebase.app(); @@ -39,6 +63,7 @@ describe('auth()', () => { }); describe('applyActionCode()', () => { + // Needs a different setup to work against the auth emulator xit('works as expected', async () => { await firebase .auth() @@ -106,18 +131,8 @@ describe('auth()', () => { }); }); - describe('verifyPasswordResetCode()', () => { - it('errors on invalid code', async () => { - try { - await firebase.auth().verifyPasswordResetCode('fooby shooby dooby'); - } catch (e) { - e.message.should.containEql('code is invalid'); - } - }); - }); - describe('confirmPasswordReset()', () => { - it('errors on invalid code', async () => { + it('errors on invalid code via API', async () => { try { await firebase.auth().confirmPasswordReset('fooby shooby dooby', 'passwordthing'); } catch (e) { @@ -127,16 +142,14 @@ describe('auth()', () => { }); describe('signInWithCustomToken()', () => { - it('signs in with a admin sdk created custom auth token', async () => { - const email = 'test@test.com'; - const pass = 'test1234'; - + // Needs a different setup when running against the emulator + xit('signs in with a admin sdk created custom auth token', async () => { const successCb = currentUserCredential => { const currentUser = currentUserCredential.user; currentUser.should.be.an.Object(); currentUser.uid.should.be.a.String(); currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(email); + currentUser.toJSON().email.should.eql(TEST_EMAIL); currentUser.isAnonymous.should.equal(false); currentUser.providerId.should.equal('firebase'); currentUser.should.equal(firebase.auth().currentUser); @@ -150,7 +163,7 @@ describe('auth()', () => { const user = await firebase .auth() - .signInWithEmailAndPassword(email, pass) + .signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS) .then(successCb); const IdToken = await firebase.auth().currentUser.getIdToken(); @@ -162,7 +175,7 @@ describe('auth()', () => { await firebase.auth().signInWithCustomToken(token); - firebase.auth().currentUser.email.should.equal('test@test.com'); + firebase.auth().currentUser.email.should.equal(TEST_EMAIL); }); }); @@ -540,15 +553,12 @@ describe('auth()', () => { describe('signInWithEmailAndPassword()', () => { it('it should login with email and password', () => { - const email = 'test@test.com'; - const pass = 'test1234'; - const successCb = currentUserCredential => { const currentUser = currentUserCredential.user; currentUser.should.be.an.Object(); currentUser.uid.should.be.a.String(); currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(email); + currentUser.toJSON().email.should.eql(TEST_EMAIL); currentUser.isAnonymous.should.equal(false); currentUser.providerId.should.equal('firebase'); currentUser.should.equal(firebase.auth().currentUser); @@ -562,14 +572,11 @@ describe('auth()', () => { return firebase .auth() - .signInWithEmailAndPassword(email, pass) + .signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS) .then(successCb); }); it('it should error on login if user is disabled', () => { - const email = 'disabled@account.com'; - const pass = 'test1234'; - const successCb = () => Promise.reject(new Error('Did not error.')); const failureCb = error => { @@ -580,15 +587,12 @@ describe('auth()', () => { return firebase .auth() - .signInWithEmailAndPassword(email, pass) + .signInWithEmailAndPassword(DISABLED_EMAIL, DISABLED_PASS) .then(successCb) .catch(failureCb); }); it('it should error on login if password incorrect', () => { - const email = 'test@test.com'; - const pass = 'test1234666'; - const successCb = () => Promise.reject(new Error('Did not error.')); const failureCb = error => { @@ -601,7 +605,7 @@ describe('auth()', () => { return firebase .auth() - .signInWithEmailAndPassword(email, pass) + .signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS + '666') .then(successCb) .catch(failureCb); }); @@ -631,14 +635,14 @@ describe('auth()', () => { describe('signInWithCredential()', () => { it('it should login with email and password', () => { - const credential = firebase.auth.EmailAuthProvider.credential('test@test.com', 'test1234'); + const credential = firebase.auth.EmailAuthProvider.credential(TEST_EMAIL, TEST_PASS); const successCb = currentUserCredential => { const currentUser = currentUserCredential.user; currentUser.should.be.an.Object(); currentUser.uid.should.be.a.String(); currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql('test@test.com'); + currentUser.toJSON().email.should.eql(TEST_EMAIL); currentUser.isAnonymous.should.equal(false); currentUser.providerId.should.equal('firebase'); currentUser.should.equal(firebase.auth().currentUser); @@ -657,10 +661,7 @@ describe('auth()', () => { }); it('it should error on login if user is disabled', () => { - const credential = firebase.auth.EmailAuthProvider.credential( - 'disabled@account.com', - 'test1234', - ); + const credential = firebase.auth.EmailAuthProvider.credential(DISABLED_EMAIL, DISABLED_PASS); const successCb = () => Promise.reject(new Error('Did not error.')); @@ -678,7 +679,7 @@ describe('auth()', () => { }); it('it should error on login if password incorrect', () => { - const credential = firebase.auth.EmailAuthProvider.credential('test@test.com', 'test1234666'); + const credential = firebase.auth.EmailAuthProvider.credential(TEST_EMAIL, TEST_PASS + '666'); const successCb = () => Promise.reject(new Error('Did not error.')); @@ -770,9 +771,6 @@ describe('auth()', () => { }); it('it should error on create if email in use', () => { - const email = 'test@test.com'; - const pass = 'test123456789'; - const successCb = () => Promise.reject(new Error('Did not error.')); const failureCb = error => { @@ -783,7 +781,7 @@ describe('auth()', () => { return firebase .auth() - .createUserWithEmailAndPassword(email, pass) + .createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS) .then(successCb) .catch(failureCb); }); @@ -824,7 +822,7 @@ describe('auth()', () => { return firebase .auth() - .fetchSignInMethodsForEmail('test@test.com') + .fetchSignInMethodsForEmail(TEST_EMAIL) .then(successCb) .catch(failureCb); })); @@ -982,11 +980,97 @@ describe('auth()', () => { try { await firebase.auth().sendPasswordResetEmail(email); + } catch (error) { + throw new Error('sendPasswordResetEmail() caused an error', error); + } finally { await firebase.auth().currentUser.delete(); + } + }); + + it('should verify with valid code', async () => { + // FIXME Fails on android against auth emulator with: + // com.google.firebase.FirebaseException: An internal error has occurred. + if (device.getPlatform() === 'ios') { + const random = Utils.randString(12, '#a'); + const email = `${random}@${random}.com`; + const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); + userCredential.user.emailVerified.should.equal(false); + firebase.auth().currentUser.email.should.equal(email); + firebase.auth().currentUser.emailVerified.should.equal(false); + + try { + await firebase.auth().sendPasswordResetEmail(email); + const { oobCode } = await getLastOob(email); + await firebase.auth().verifyPasswordResetCode(oobCode); + } catch (error) { + throw new Error('sendPasswordResetEmail() caused an error', error); + } finally { + await firebase.auth().currentUser.delete(); + } + } + }); + + it('should fail to verify with invalid code', async () => { + const random = Utils.randString(12, '#a'); + const email = `${random}@${random}.com`; + const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); + userCredential.user.emailVerified.should.equal(false); + firebase.auth().currentUser.email.should.equal(email); + firebase.auth().currentUser.emailVerified.should.equal(false); + + try { + await firebase.auth().sendPasswordResetEmail(email); + const { oobCode } = await getLastOob(email); + await firebase.auth().verifyPasswordResetCode(oobCode + 'badcode'); + throw new Error('Invalid code should throw an error'); + } catch (error) { + error.message.should.containEql('[auth/invalid-action-code]'); + } finally { + await firebase.auth().currentUser.delete(); + } + }); + + it('should change password correctly OOB', async () => { + const random = Utils.randString(12, '#a'); + const email = `${random}@${random}.com`; + const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); + userCredential.user.emailVerified.should.equal(false); + firebase.auth().currentUser.email.should.equal(email); + firebase.auth().currentUser.emailVerified.should.equal(false); + + try { + await firebase.auth().sendPasswordResetEmail(email); + const { oobCode } = await getLastOob(email); + await resetPassword(oobCode, 'testNewPassword'); + await firebase.auth().signOut(); + await Utils.sleep(50); + await firebase.auth().signInWithEmailAndPassword(email, 'testNewPassword'); } catch (error) { - // Reject + throw new Error('sendPasswordResetEmail() caused an error', error); + } finally { await firebase.auth().currentUser.delete(); + } + }); + + it('should change password correctly via API', async () => { + const random = Utils.randString(12, '#a'); + const email = `${random}@${random}.com`; + const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); + userCredential.user.emailVerified.should.equal(false); + firebase.auth().currentUser.email.should.equal(email); + firebase.auth().currentUser.emailVerified.should.equal(false); + + try { + await firebase.auth().sendPasswordResetEmail(email); + const { oobCode } = await getLastOob(email); + await firebase.auth().confirmPasswordReset(oobCode, 'testNewPassword'); + await firebase.auth().signOut(); + await Utils.sleep(50); + await firebase.auth().signInWithEmailAndPassword(email, 'testNewPassword'); + } catch (error) { throw new Error('sendPasswordResetEmail() caused an error', error); + } finally { + await firebase.auth().currentUser.delete(); } }); }); diff --git a/packages/auth/e2e/emailLink.e2e.js b/packages/auth/e2e/emailLink.e2e.js index 3332d601dc..bc2ced2b7d 100644 --- a/packages/auth/e2e/emailLink.e2e.js +++ b/packages/auth/e2e/emailLink.e2e.js @@ -1,3 +1,5 @@ +const { getLastOob, signInUser } = require('./helpers'); + describe('auth() -> emailLink Provider', () => { beforeEach(async () => { if (firebase.auth().currentUser) { @@ -26,6 +28,30 @@ describe('auth() -> emailLink Provider', () => { await firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings); }); + it('sign in via email works', async () => { + const random = Utils.randString(12, '#aa'); + const email = `${random}@${random}.com`; + const continueUrl = 'http://localhost:1337/authLinkFoo?bar=' + random; + const actionCodeSettings = { + url: continueUrl, + handleCodeInApp: true, + iOS: { + bundleId: 'com.testing', + }, + android: { + packageName: 'com.testing', + installApp: true, + minimumVersion: '12', + }, + }; + await firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings); + const oobInfo = await getLastOob(email); + oobInfo.oobLink.should.containEql(encodeURIComponent(continueUrl)); + const signInResponse = await signInUser(oobInfo.oobLink); + signInResponse.should.containEql(continueUrl); + signInResponse.should.containEql(oobInfo.oobCode); + }); + xit('should send email with defaults', async () => { const random = Utils.randString(12, '#aA'); const email = `${random}@${random}.com`; diff --git a/packages/auth/e2e/helpers.js b/packages/auth/e2e/helpers.js new file mode 100644 index 0000000000..536cd17c86 --- /dev/null +++ b/packages/auth/e2e/helpers.js @@ -0,0 +1,191 @@ +/* eslint-disable no-console */ +const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers'); +const http = require('http'); + +// Call HTTP REST API URL and return JSON response parsed into object +const callRestApi = async function callRestAPI(url, rawResult = false) { + // const TAG = 'auth::e2e:helpers:callRestApi - '; + return await new Promise((resolve, reject) => { + // console.log(TAG + 'making request'); + const req = http.get(url, response => { + // console.log(TAG + 'callback'); + let data = ''; + response.on('data', chunk => { + // console.log(TAG + 'request callback response data callback'); + // console.log(TAG + 'data event, got chunk: ' + chunk); + data += chunk; + }); + response.on('end', () => { + // console.log(TAG + 'request callback response end callback'); + if (rawResult) { + resolve(data); + } else { + resolve(JSON.parse(data)); + } + }); + }); + req.on('error', error => reject(error)); + }); +}; + +exports.getRandomPhoneNumber = function getRandomPhoneNumber() { + return '+593' + Utils.randString(9, '#19'); +}; + +exports.clearAllUsers = async function clearAllUsers() { + // console.log('auth::helpers::clearAllUsers'); + try { + const deleteOptions = { + method: 'DELETE', + headers: { + // Undocumented, but necessary - from Emulator UI network requests + Authorization: 'Bearer owner', + }, + port: 9099, + host: getE2eEmulatorHost(), + path: '/emulator/v1/projects/' + getE2eTestProject() + '/accounts', + }; + // console.log('request: ' + JSON.stringify(deleteOptions)); + await new Promise((resolve, reject) => { + const req = http.request(deleteOptions); + req.on('error', error => reject(error)); + req.end(resolve()); + }); + } catch (e) { + console.error('Unable to wipe auth:', e); + throw e; + } +}; + +exports.disableUser = async function disableUser(userId) { + // console.log('auth::helpers::disableUser on userId: ' + userId); + const reqBody = JSON.stringify({ disableUser: true, localId: userId }); + try { + const postOptions = { + method: 'POST', + headers: { + // Undocumented, but necessary - from Emulator UI network requests + Authorization: 'Bearer owner', + 'Content-Type': 'application/json', + 'Content-Length': reqBody.length, + }, + port: 9099, + host: getE2eEmulatorHost(), + path: '/identitytoolkit.googleapis.com/v1/accounts:update', + }; + // console.log('request: ' + JSON.stringify(postOptions)); + await new Promise((resolve, reject) => { + const req = http.request(postOptions); + req.on('error', error => reject(error)); + req.write(reqBody); + req.end(resolve()); + }); + } catch (e) { + console.error('Unable to update user:', e); + throw e; + } +}; + +exports.getLastSmsCode = async function getLastSmsCode(specificPhone) { + let lastSmsCode = null; + try { + // console.log('auth::e2e:helpers:getLastSmsCode - start'); + const getSmsCodesUrl = + 'http://' + + getE2eEmulatorHost() + + ':9099/emulator/v1/projects/' + + getE2eTestProject() + + '/verificationCodes'; + + const responseData = await callRestApi(getSmsCodesUrl); + + // Process the codes, the last one in the array is the one... + // console.log('getLastSmsCode got ', JSON.stringify(responseData, null, 2)); + const codes = responseData ? responseData.verificationCodes : undefined; + if (codes && codes.length > 0) { + if (specificPhone) { + // roll through backwards (to get last valid code) searching for the specific phone + for (let i = codes.length - 1; i >= 0 && !lastSmsCode; i--) { + const codeBlock = codes[i]; + if (codeBlock.phoneNumber === specificPhone) { + lastSmsCode = codeBlock.code; + } + } + } else { + lastSmsCode = codes[codes.length - 1].code; + } + } else { + throw new Error('There were no unused verification codes'); + } + } catch (e) { + console.error('Unable to get SMS Verification codes', e); + throw e; + } + // console.log('getLastSmsCode returning code: ' + lastSmsCode); + return lastSmsCode; +}; + +exports.getLastOob = async function getLastOob(specificEmail) { + let lastOob = null; + try { + // console.log('auth::e2e:helpers:getLastOob - start'); + const getOobCodesUrl = + 'http://' + + getE2eEmulatorHost() + + ':9099/emulator/v1/projects/' + + getE2eTestProject() + + '/oobCodes'; + + const responseData = await callRestApi(getOobCodesUrl); + + // Process the codes, the last one in the array is the one... + // console.log('getLastOob got ', JSON.stringify(responseData, null, 2)); + const codes = responseData ? responseData.oobCodes : undefined; + if (codes && codes.length > 0) { + if (specificEmail) { + // roll through backwards (to get last valid code) searching for the specific email + for (let i = codes.length - 1; i >= 0 && !lastOob; i--) { + const codeBlock = codes[i]; + if (codeBlock.email === specificEmail) { + lastOob = codeBlock; + } + } + } else { + lastOob = codes[codes.length - 1]; + } + } else { + throw new Error('There were no unused OOB codes'); + } + } catch (e) { + console.error('Unable to get Email OOB codes', e); + throw e; + } + // console.log('getLastOob returning code: ' + JSON.stringify(lastOob, null, 2); + return lastOob; +}; + +exports.resetPassword = async function resetPassword(oobCode, newPassword) { + const resetPasswordUrl = + 'http://' + + getE2eEmulatorHost() + + ':9099/emulator/action?mode=resetPassword&lang=en&oobCode=' + + oobCode + + '&apiKey=fake-api-key&newPassword=' + + newPassword; + return await callRestApi(resetPasswordUrl); +}; + +exports.verifyEmail = async function verifyEmail(oobCode) { + const verifyEmailUrl = + 'http://' + + getE2eEmulatorHost() + + ':9099/emulator/action?mode=verifyEmail&lang=en&oobCode=' + + oobCode + + '&apiKey=fake-api-key'; + return await callRestApi(verifyEmailUrl); +}; + +// This URL comes from the Auth Emulator's oobCode blocks +exports.signInUser = async function signInUser(oobUrl) { + return await callRestApi(oobUrl, true); +}; diff --git a/packages/auth/e2e/phone.e2e.js b/packages/auth/e2e/phone.e2e.js index b45deca381..22e5695ed2 100644 --- a/packages/auth/e2e/phone.e2e.js +++ b/packages/auth/e2e/phone.e2e.js @@ -1,13 +1,16 @@ -const TEST_PHONE_A = '+447445255123'; -const TEST_CODE_A = '123456'; +// const TEST_EMAIL = 'test@test.com'; +// const TEST_PASS = 'test1234'; -// const TEST_PHONE_B = '+447445123457'; -// const TEST_CODE_B = '654321'; +const { clearAllUsers, getLastSmsCode, getRandomPhoneNumber } = require('./helpers'); describe('auth() => Phone', () => { before(async () => { + try { + await clearAllUsers(); + } catch (e) { + throw e; + } firebase.auth().settings.appVerificationDisabledForTesting = true; - await firebase.auth().settings.setAutoRetrievedSmsCodeForPhoneNumber(TEST_PHONE_A, TEST_CODE_A); await Utils.sleep(50); }); @@ -19,12 +22,14 @@ describe('auth() => Phone', () => { }); describe('signInWithPhoneNumber', () => { - xit('signs in with a valid code', async () => { - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); + it('signs in with a valid code', async () => { + const testPhone = await getRandomPhoneNumber(); + const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); confirmResult.verificationId.should.be.a.String(); should.ok(confirmResult.verificationId.length, 'verificationId string should not be empty'); confirmResult.confirm.should.be.a.Function(); - const userCredential = await confirmResult.confirm(TEST_CODE_A); + const lastSmsCode = await getLastSmsCode(testPhone); + const userCredential = await confirmResult.confirm(lastSmsCode); userCredential.user.should.be.instanceOf(jet.require('packages/auth/lib/User')); // Broken check, phone number is undefined @@ -32,42 +37,46 @@ describe('auth() => Phone', () => { }); it('errors on invalid code', async () => { - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); + const testPhone = await getRandomPhoneNumber(); + const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); confirmResult.verificationId.should.be.a.String(); should.ok(confirmResult.verificationId.length, 'verificationId string should not be empty'); confirmResult.confirm.should.be.a.Function(); - await confirmResult.confirm('666999').should.be.rejected(); + // Get the last SMS code just to make absolutely sure we don't accidentally use it + const lastSmsCode = await getLastSmsCode(testPhone); + await confirmResult + .confirm(lastSmsCode === '000000' ? '111111' : '000000') + .should.be.rejected(); // TODO test error code and message + + // If you don't consume the valid code, then it sticks around + await confirmResult.confirm(lastSmsCode); }); }); describe('verifyPhoneNumber', async () => { it('successfully verifies', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - await firebase.auth().verifyPhoneNumber(TEST_PHONE_A, false, false); + const testPhone = await getRandomPhoneNumber(); + const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); + const lastSmsCode = await getLastSmsCode(testPhone); + await confirmResult.confirm(lastSmsCode); + await firebase.auth().verifyPhoneNumber(testPhone, false, false); }); it('uses the autoVerifyTimeout when a non boolean autoVerifyTimeoutOrForceResend is provided', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - await firebase.auth().verifyPhoneNumber(TEST_PHONE_A, 0, false); + const testPhone = await getRandomPhoneNumber(); + const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); + const lastSmsCode = await getLastSmsCode(testPhone); + await confirmResult.confirm(lastSmsCode); + await firebase.auth().verifyPhoneNumber(testPhone, 0, false); }); it('throws an error with an invalid on event', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - + const testPhone = await getRandomPhoneNumber(); try { await firebase .auth() - .verifyPhoneNumber(TEST_PHONE_A) + .verifyPhoneNumber(testPhone) .on('example', () => {}); return Promise.reject(new Error('Did not throw Error.')); @@ -80,15 +89,11 @@ describe('auth() => Phone', () => { }); it('throws an error with an invalid observer event', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - + const testPhone = await getRandomPhoneNumber(); try { await firebase .auth() - .verifyPhoneNumber(TEST_PHONE_A) + .verifyPhoneNumber(testPhone) .on('state_changed', null, null, () => {}); return Promise.reject(new Error('Did not throw Error.')); @@ -101,45 +106,30 @@ describe('auth() => Phone', () => { }); it('successfully runs verification complete handler', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - + const testPhone = await getRandomPhoneNumber(); await firebase .auth() - .verifyPhoneNumber(TEST_PHONE_A) + .verifyPhoneNumber(testPhone) .then($ => $); return Promise.resolve(); }); - it('successfully runs and adds emiters', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - + it('successfully runs and adds emitters', async () => { + const testPhone = await getRandomPhoneNumber(); const obervserCb = () => {}; - const errorCb = () => {}; - const successCb = () => { return Promise.resolve(); }; await firebase .auth() - .verifyPhoneNumber(TEST_PHONE_A) + .verifyPhoneNumber(testPhone) .on('state_changed', obervserCb, errorCb, successCb, () => {}); }); it('catches an error and emits an error event', async () => { - const TEST_PHONE_A = '+447445255123'; - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); - return firebase .auth() .verifyPhoneNumber('test') diff --git a/packages/auth/e2e/rnReload.e2e.js b/packages/auth/e2e/rnReload.e2e.js index 5ebfdb1623..bbb89ac014 100644 --- a/packages/auth/e2e/rnReload.e2e.js +++ b/packages/auth/e2e/rnReload.e2e.js @@ -1,4 +1,22 @@ +const TEST_EMAIL = 'test@test.com'; +const TEST_PASS = 'test1234'; + +const { clearAllUsers } = require('./helpers'); + describe('auth()', () => { + before(async () => { + try { + await clearAllUsers(); + } catch (e) { + throw e; + } + try { + await firebase.auth().createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS); + } catch (e) { + // they may already exist, that's fine + } + }); + beforeEach(async () => { if (firebase.auth().currentUser) { await firebase.auth().signOut(); @@ -39,15 +57,13 @@ describe('auth()', () => { // in with a different user then reloading await firebase.auth().signOut(); - const email = 'test@test.com'; - const pass = 'test1234'; - await firebase.auth().signInWithEmailAndPassword(email, pass); + await firebase.auth().signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS); ({ currentUser } = firebase.auth()); currentUser.should.be.an.Object(); currentUser.uid.should.be.a.String(); currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(email); + currentUser.toJSON().email.should.eql(TEST_EMAIL); currentUser.isAnonymous.should.equal(false); currentUser.providerId.should.equal('firebase'); currentUser.should.equal(firebase.auth().currentUser); @@ -60,7 +76,7 @@ describe('auth()', () => { currentUser.should.be.an.Object(); currentUser.uid.should.be.a.String(); currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(email); + currentUser.toJSON().email.should.eql(TEST_EMAIL); currentUser.isAnonymous.should.equal(false); currentUser.providerId.should.equal('firebase'); currentUser.should.equal(firebase.auth().currentUser); diff --git a/packages/auth/e2e/user.e2e.js b/packages/auth/e2e/user.e2e.js index 9bb487583c..c59cff961e 100644 --- a/packages/auth/e2e/user.e2e.js +++ b/packages/auth/e2e/user.e2e.js @@ -1,4 +1,29 @@ +const TEST_EMAIL = 'test@test.com'; +const TEST_PASS = 'test1234'; + +const { + clearAllUsers, + getLastSmsCode, + getRandomPhoneNumber, + getLastOob, + verifyEmail, +} = require('./helpers'); + describe('auth().currentUser', () => { + before(async () => { + try { + await clearAllUsers(); + } catch (e) { + throw e; + } + firebase.auth().settings.appVerificationDisabledForTesting = true; + try { + await firebase.auth().createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS); + } catch (e) { + // they may already exist, that's fine + } + }); + beforeEach(async () => { if (firebase.auth().currentUser) { await firebase.auth().signOut(); @@ -57,6 +82,7 @@ describe('auth().currentUser', () => { }); describe('linkWithCredential()', () => { + // hanging against auth emulator? it('should link anonymous account <-> email account', async () => { const random = Utils.randString(12, '#aA'); const email = `${random}@${random}.com`; @@ -85,15 +111,12 @@ describe('auth().currentUser', () => { }); it('should error on link anon <-> email if email already exists', async () => { - const email = 'test@test.com'; - const pass = 'test1234'; - await firebase.auth().signInAnonymously(); const { currentUser } = firebase.auth(); // Test try { - const credential = firebase.auth.EmailAuthProvider.credential(email, pass); + const credential = firebase.auth.EmailAuthProvider.credential(TEST_EMAIL, TEST_PASS); await currentUser.linkWithCredential(credential); // Clean up @@ -315,15 +338,33 @@ describe('auth().currentUser', () => { try { await firebase.auth().currentUser.sendEmailVerification(); + } catch (error) { + return Promise.reject(new Error('sendEmailVerification() caused an error', error)); + } finally { await firebase.auth().currentUser.delete(); + } + + return Promise.resolve(); + }); + + it('should correctly report emailVerified status', async () => { + const random = Utils.randString(12, '#a'); + const email = `${random}@${random}.com`; + await firebase.auth().createUserWithEmailAndPassword(email, random); + firebase.auth().currentUser.email.should.equal(email); + firebase.auth().currentUser.emailVerified.should.equal(false); + + try { + await firebase.auth().currentUser.sendEmailVerification(); + const { oobCode } = await getLastOob(email); + await verifyEmail(oobCode); + firebase.auth().currentUser.emailVerified.should.equal(false); + await firebase.auth().currentUser.reload(); + firebase.auth().currentUser.emailVerified.should.equal(true); } catch (error) { - // Reject - try { - await firebase.auth().currentUser.delete(); - } catch (_) { - /* do nothing */ - } return Promise.reject(new Error('sendEmailVerification() caused an error', error)); + } finally { + await firebase.auth().currentUser.delete(); } return Promise.resolve(); @@ -340,18 +381,12 @@ describe('auth().currentUser', () => { try { await firebase.auth().currentUser.sendEmailVerification(actionCodeSettings); - await firebase.auth().currentUser.delete(); } catch (error) { - // Reject - try { - await firebase.auth().currentUser.delete(); - } catch (_) { - /* do nothing */ - } - return Promise.reject( - new Error('sendEmailVerification(actionCodeSettings) caused an error' + error.message), + new Error('sendEmailVerification(actionCodeSettings) error' + error.message), ); + } finally { + await firebase.auth().currentUser.delete(); } return Promise.resolve(); @@ -580,6 +615,8 @@ describe('auth().currentUser', () => { await firebase.auth().currentUser.delete(); }); + // FIXME ios+android failing with an internal error against auth emulator + // com.google.firebase.FirebaseException: An internal error has occurred. [ VERIFY_AND_CHANGE_EMAIL ] xit('should not error', async () => { const random = Utils.randString(12, '#aA'); const random2 = Utils.randString(12, '#aA'); @@ -589,12 +626,14 @@ describe('auth().currentUser', () => { try { await firebase.auth().createUserWithEmailAndPassword(email, random); await firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail); - } catch (_) { + } catch (e) { return Promise.reject("'verifyBeforeUpdateEmail()' did not work"); } await firebase.auth().currentUser.delete(); }); + // FIXME ios+android failing with an internal error against auth emulator + // com.google.firebase.FirebaseException: An internal error has occurred. [ VERIFY_AND_CHANGE_EMAIL ] xit('should work with actionCodeSettings', async () => { const random = Utils.randString(12, '#aA'); const random2 = Utils.randString(12, '#aA'); @@ -610,7 +649,8 @@ describe('auth().currentUser', () => { } catch (error) { try { await firebase.auth().currentUser.delete(); - } catch (_) { + } catch (e) { + consle.log(e); /* do nothing */ } @@ -646,68 +686,65 @@ describe('auth().currentUser', () => { describe('updateEmail()', () => { it('should update the email address', async () => { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); + const random = Utils.randString(12, '#a'); + const random2 = Utils.randString(12, '#a'); const email = `${random}@${random}.com`; const email2 = `${random2}@${random2}.com`; // Setup await firebase.auth().createUserWithEmailAndPassword(email, random); - firebase - .auth() - .currentUser.email.toLowerCase() - .should.equal(email.toLowerCase()); + firebase.auth().currentUser.email.should.equal(email); // Update user email await firebase.auth().currentUser.updateEmail(email2); // Assertions - firebase - .auth() - .currentUser.email.toLowerCase() - .should.equal(email2.toLowerCase()); + firebase.auth().currentUser.email.should.equal(email2); // Clean up await firebase.auth().currentUser.delete(); }); }); - // TODO: Figure how to mock phone credentials on updating a phone number describe('updatePhoneNumber()', () => { - it('should update the profile', async () => { - // Create with initial number - const TEST_PHONE_A = '+447445255123'; - const TEST_CODE_A = '123456'; - - firebase.auth().settings.appVerificationDisabledForTesting = true; + it('should update the phone number', async () => { + const testPhone = await getRandomPhoneNumber(); + const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); + const smsCode = await getLastSmsCode(testPhone); + await confirmResult.confirm(smsCode); - await firebase - .auth() - .settings.setAutoRetrievedSmsCodeForPhoneNumber(TEST_PHONE_A, TEST_CODE_A); - await Utils.sleep(50); + firebase.auth().currentUser.phoneNumber.should.equal(testPhone); - //Sign Out - if (firebase.auth().currentUser) { - await firebase.auth().signOut(); - await Utils.sleep(50); - } - - //Sign in number - const confirmResult = await firebase.auth().signInWithPhoneNumber(TEST_PHONE_A); - - await confirmResult.confirm(TEST_CODE_A); + const newPhone = await getRandomPhoneNumber(); + const newPhoneVerificationId = await new Promise((resolve, reject) => { + firebase + .auth() + .verifyPhoneNumber(newPhone) + .on('state_changed', phoneAuthSnapshot => { + if (phoneAuthSnapshot.error) { + reject(phoneAuthSnapshot.error); + } else { + resolve(phoneAuthSnapshot.verificationId); + } + }); + }); - const credential = await firebase.auth.PhoneAuthProvider.credential( - confirmResult.verificationId, - TEST_CODE_A, - ); + try { + const newSmsCode = await getLastSmsCode(newPhone); + const credential = await firebase.auth.PhoneAuthProvider.credential( + newPhoneVerificationId, + newSmsCode, + ); - //Update with number? - await firebase - .auth() - .currentUser.updatePhoneNumber(credential) - .then($ => $); + //Update with number? + await firebase + .auth() + .currentUser.updatePhoneNumber(credential) + .then($ => $); + } catch (e) { + throw e; + } - // TODO Add assertions, what exactly does this update. No phone number included to update? + firebase.auth().currentUser.phoneNumber.should.equal(newPhone); }); }); diff --git a/packages/firestore/e2e/helpers.js b/packages/firestore/e2e/helpers.js index a3c60a60ec..9a7d0a5289 100644 --- a/packages/firestore/e2e/helpers.js +++ b/packages/firestore/e2e/helpers.js @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +const { getE2eTestProject, getE2eEmulatorHost } = require('../../app/e2e/helpers'); /* * Copyright (c) 2016-present Invertase Limited & Contributors @@ -18,28 +19,31 @@ */ const http = require('http'); -const deleteOptions = { - method: 'DELETE', - port: 8080, - host: '127.0.0.1', - path: '/emulator/v1/projects/react-native-firebase-testing/databases/(default)/documents', -}; - exports.wipe = async function wipe(debug = false) { - if (debug) { - console.time('wipe'); - } + const deleteOptions = { + method: 'DELETE', + port: 8080, + host: getE2eEmulatorHost(), + path: '/emulator/v1/projects/' + getE2eTestProject() + '/databases/(default)/documents', + }; - await new Promise((resolve, reject) => { - const req = http.request(deleteOptions); + try { + if (debug) { + console.time('wipe'); + } + await new Promise((resolve, reject) => { + const req = http.request(deleteOptions); - req.on('error', error => reject(error)); + req.on('error', error => reject(error)); - req.end(() => { - if (debug) { - console.timeEnd('wipe'); - } - resolve(); + req.end(() => { + if (debug) { + console.timeEnd('wipe'); + } + resolve(); + }); }); - }); + } catch (e) { + console.error('Unable to wipe firestore:', e); + } }; diff --git a/tests/app.js b/tests/app.js index 0ddfed57b9..6e10f5de52 100644 --- a/tests/app.js +++ b/tests/app.js @@ -44,6 +44,8 @@ jet.exposeContextProperty('module', firebase); const firestore = firebase.firestore(); firestore.settings({ host: 'localhost:8080', ssl: false, persistence: true }); +firebase.auth().useEmulator('http://localhost:9099'); + function Root() { return (