Repository: firebase/firebase-admin-java
Pull request #269 (2019-04-29 23:12:40)

Deferring the initialization of FirebaseUserManager
`FirebaseUserManager` requires a project ID. Currently we init the user manager when creating `FirebaseAuth`, which fails if a project ID is not specified. To be consistent with how other child-components are initialized in `FirebaseAuth`, I'm using the `Supplier` interface to defer the initialization of `FirebaseUserManager`.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 0e02f83..4c488cf 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -67,16 +67,21 @@ public class FirebaseAuth {
   private final Supplier<FirebaseTokenFactory> tokenFactory;
   private final Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
   private final Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
+  private final Supplier<? extends FirebaseUserManager> userManager;
   private final JsonFactory jsonFactory;
-  private final FirebaseUserManager userManager;
 
   private FirebaseAuth(Builder builder) {
     this.firebaseApp = checkNotNull(builder.firebaseApp);
     this.tokenFactory = threadSafeMemoize(builder.tokenFactory);
     this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier);
     this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier);
+    this.userManager = threadSafeMemoize(new Supplier<FirebaseUserManager>() {
+      @Override
+      public FirebaseUserManager get() {
+        return new FirebaseUserManager(firebaseApp);
+      }
+    });
     this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
-    this.userManager = new FirebaseUserManager(firebaseApp);
   }
 
   /**
@@ -139,6 +144,7 @@ public class FirebaseAuth {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty");
     checkNotNull(options, "options must not be null");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<String, FirebaseAuthException>() {
       @Override
       protected String execute() throws FirebaseAuthException {
@@ -225,6 +231,7 @@ public class FirebaseAuth {
   FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
     FirebaseTokenVerifier verifier = cookieVerifier.get();
     if (checkRevoked) {
+      FirebaseUserManager userManager = getUserManager();
       verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
     }
     return verifier;
@@ -432,6 +439,7 @@ public class FirebaseAuth {
   FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) {
     FirebaseTokenVerifier verifier = idTokenVerifier.get();
     if (checkRevoked) {
+      FirebaseUserManager userManager = getUserManager();
       verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager);
     }
     return verifier;
@@ -472,6 +480,7 @@ public class FirebaseAuth {
   private CallableOperation<Void, FirebaseAuthException> revokeRefreshTokensOp(final String uid) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
       @Override
       protected Void execute() throws FirebaseAuthException {
@@ -511,6 +520,7 @@ public class FirebaseAuth {
   private CallableOperation<UserRecord, FirebaseAuthException> getUserOp(final String uid) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
       @Override
       protected UserRecord execute() throws FirebaseAuthException {
@@ -548,6 +558,7 @@ public class FirebaseAuth {
       final String email) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
       @Override
       protected UserRecord execute() throws FirebaseAuthException {
@@ -585,6 +596,7 @@ public class FirebaseAuth {
       final String phoneNumber) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
       @Override
       protected UserRecord execute() throws FirebaseAuthException {
@@ -652,6 +664,7 @@ public class FirebaseAuth {
   private CallableOperation<ListUsersPage, FirebaseAuthException> listUsersOp(
       @Nullable final String pageToken, final int maxResults) {
     checkNotDestroyed();
+    final FirebaseUserManager userManager = getUserManager();
     final PageFactory factory = new PageFactory(
         new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken);
     return new CallableOperation<ListUsersPage, FirebaseAuthException>() {
@@ -692,6 +705,7 @@ public class FirebaseAuth {
       final CreateRequest request) {
     checkNotDestroyed();
     checkNotNull(request, "create request must not be null");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
       @Override
       protected UserRecord execute() throws FirebaseAuthException {
@@ -731,6 +745,7 @@ public class FirebaseAuth {
       final UpdateRequest request) {
     checkNotDestroyed();
     checkNotNull(request, "update request must not be null");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
       @Override
       protected UserRecord execute() throws FirebaseAuthException {
@@ -783,6 +798,7 @@ public class FirebaseAuth {
       final String uid, final Map<String, Object> claims) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
       @Override
       protected Void execute() throws FirebaseAuthException {
@@ -820,6 +836,7 @@ public class FirebaseAuth {
   private CallableOperation<Void, FirebaseAuthException> deleteUserOp(final String uid) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
       @Override
       protected Void execute() throws FirebaseAuthException {
@@ -901,6 +918,7 @@ public class FirebaseAuth {
       final List<ImportUserRecord> users, final UserImportOptions options) {
     checkNotDestroyed();
     final UserImportRequest request = new UserImportRequest(users, options, jsonFactory);
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserImportResult, FirebaseAuthException>() {
       @Override
       protected UserImportResult execute() throws FirebaseAuthException {
@@ -1071,7 +1089,7 @@ public class FirebaseAuth {
 
   @VisibleForTesting
   FirebaseUserManager getUserManager() {
-    return this.userManager;
+    return this.userManager.get();
   }
 
   private CallableOperation<String, FirebaseAuthException> generateEmailActionLinkOp(
@@ -1081,6 +1099,7 @@ public class FirebaseAuth {
     if (type == EmailLinkType.EMAIL_SIGNIN) {
       checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links");
     }
+    final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<String, FirebaseAuthException>() {
       @Override
       protected String execute() throws FirebaseAuthException {


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index c495c41..cf3b683 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -46,7 +46,6 @@ import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Test;
 
 public class FirebaseAuthTest {
@@ -139,21 +138,12 @@ public class FirebaseAuthTest {
   }
 
   @Test
-  public void testProjectIdRequired() {
+  public void testProjectIdNotRequiredAtInitialization() {
     FirebaseOptions options = FirebaseOptions.builder()
         .setCredentials(new MockGoogleCredentials())
         .build();
     FirebaseApp app = FirebaseApp.initializeApp(options, "testProjectIdRequired");
-    try {
-      FirebaseAuth.getInstance(app);
-      fail("Expected exception.");
-    } catch (IllegalArgumentException expected) {
-      Assert.assertEquals(
-          "Project ID is required to access the auth service. Use a service account credential "
-              + "or set the project ID explicitly via FirebaseOptions. Alternatively you can "
-              + "also set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.",
-          expected.getMessage());
-    }
+    assertNotNull(FirebaseAuth.getInstance(app));
   }
 
   @Test(expected = IllegalArgumentException.class)
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 72e81fd..97ff744 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -85,10 +85,16 @@ public class FirebaseUserManagerTest {
     FirebaseApp.initializeApp(new FirebaseOptions.Builder()
             .setCredentials(credentials)
             .build());
+    FirebaseAuth auth = FirebaseAuth.getInstance();
     try {
-      FirebaseAuth.getInstance();
+      auth.getUserManager();
       fail("No error thrown for missing project ID");
     } catch (IllegalArgumentException expected) {
+      assertEquals(
+          "Project ID is required to access the auth service. Use a service account credential "
+              + "or set the project ID explicitly via FirebaseOptions. Alternatively you can "
+              + "also set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.",
+          expected.getMessage());
     }
   }