Repository: firebase/firebase-admin-java
Pull request #356 (2020-01-28 20:01:06)

fix: Enabled automatic HTTP retries for FirebaseProjectManagement
Enables automatic HTTP retries for the `FirebaseProjectManagement` API. We disable retries for unit tests for faster execution of tests.

RELEASE NOTE: Project management APIs in the `FirebaseProjectManagement` class now automatically retries operations that fail due to retry-eligible HTTP errors.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 4c488cf..f7f6231 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -75,12 +75,7 @@ public class FirebaseAuth {
     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.userManager = threadSafeMemoize(builder.userManager);
     this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
   }
 
@@ -1109,10 +1104,10 @@ public class FirebaseAuth {
   }
 
   private <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
-    checkNotNull(supplier);
     return Suppliers.memoize(new Supplier<T>() {
       @Override
       public T get() {
+        checkNotNull(supplier);
         synchronized (lock) {
           checkNotDestroyed();
           return supplier.get();
@@ -1155,6 +1150,12 @@ public class FirebaseAuth {
             return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM);
           }
         })
+        .setUserManager(new Supplier<FirebaseUserManager>() {
+          @Override
+          public FirebaseUserManager get() {
+            return new FirebaseUserManager(app);
+          }
+        })
         .build();
   }
 
@@ -1168,6 +1169,7 @@ public class FirebaseAuth {
     private Supplier<FirebaseTokenFactory> tokenFactory;
     private Supplier<? extends FirebaseTokenVerifier> idTokenVerifier;
     private Supplier<? extends FirebaseTokenVerifier> cookieVerifier;
+    private Supplier<FirebaseUserManager> userManager;
 
     private Builder() { }
 
@@ -1191,6 +1193,11 @@ public class FirebaseAuth {
       return this;
     }
 
+    Builder setUserManager(Supplier<FirebaseUserManager> userManager) {
+      this.userManager = userManager;
+      return this;
+    }
+
     FirebaseAuth build() {
       return new FirebaseAuth(this);
     }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 8298499..03c2813 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -26,7 +26,6 @@ import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponse;
 import com.google.api.client.http.HttpResponseException;
 import com.google.api.client.http.HttpResponseInterceptor;
-import com.google.api.client.http.HttpTransport;
 import com.google.api.client.http.json.JsonHttpContent;
 import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
@@ -45,7 +44,7 @@ import com.google.firebase.auth.internal.GetAccountInfoResponse;
 
 import com.google.firebase.auth.internal.HttpErrorResponse;
 import com.google.firebase.auth.internal.UploadAccountResponse;
-import com.google.firebase.internal.FirebaseRequestInitializer;
+import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 import com.google.firebase.internal.SdkUtils;
@@ -111,6 +110,10 @@ class FirebaseUserManager {
    * @param app A non-null {@link FirebaseApp}.
    */
   FirebaseUserManager(@NonNull FirebaseApp app) {
+    this(app, null);
+  }
+
+  FirebaseUserManager(@NonNull FirebaseApp app, @Nullable HttpRequestFactory requestFactory) {
     checkNotNull(app, "FirebaseApp must not be null");
     String projectId = ImplFirebaseTrampolines.getProjectId(app);
     checkArgument(!Strings.isNullOrEmpty(projectId),
@@ -119,8 +122,12 @@ class FirebaseUserManager {
             + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
     this.baseUrl = String.format(ID_TOOLKIT_URL, projectId);
     this.jsonFactory = app.getOptions().getJsonFactory();
-    HttpTransport transport = app.getOptions().getHttpTransport();
-    this.requestFactory = transport.createRequestFactory(new FirebaseRequestInitializer(app));
+
+    if (requestFactory == null) {
+      requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app);
+    }
+
+    this.requestFactory = requestFactory;
   }
 
   @VisibleForTesting
diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java
index 7506ff8..36ccf5c 100644
--- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java
+++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java
@@ -29,7 +29,7 @@ import java.io.IOException;
  */
 public class ApiClientUtils {
 
-  private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder()
+  static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder()
       .setMaxRetries(4)
       .setRetryStatusCodes(ImmutableList.of(500, 503))
       .setMaxIntervalMillis(60 * 1000)
@@ -43,9 +43,21 @@ public class ApiClientUtils {
    * @return A new {@code HttpRequestFactory} instance.
    */
   public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) {
+    return newAuthorizedRequestFactory(app, DEFAULT_RETRY_CONFIG);
+  }
+
+  /**
+   * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
+   * automatic retries.
+   *
+   * @param app {@link FirebaseApp} from which to obtain authorization credentials.
+   * @param retryConfig {@link RetryConfig} instance or null to disable retries.
+   * @return A new {@code HttpRequestFactory} instance.
+   */
+  public static HttpRequestFactory newAuthorizedRequestFactory(
+      FirebaseApp app, @Nullable RetryConfig retryConfig) {
     HttpTransport transport = app.getOptions().getHttpTransport();
-    return transport.createRequestFactory(
-        new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG));
+    return transport.createRequestFactory(new FirebaseRequestInitializer(app, retryConfig));
   }
 
   public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) {
diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java
index 72c570c..8abced6 100644
--- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java
+++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java
@@ -18,6 +18,7 @@ package com.google.firebase.projectmanagement;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponseInterceptor;
 import com.google.api.client.util.Base64;
 import com.google.api.client.util.Key;
@@ -32,8 +33,8 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.internal.ApiClientUtils;
 import com.google.firebase.internal.CallableOperation;
-import com.google.firebase.internal.FirebaseRequestInitializer;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
@@ -55,6 +56,7 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS
   private final FirebaseApp app;
   private final Sleeper sleeper;
   private final Scheduler scheduler;
+  private final HttpRequestFactory requestFactory;
   private final HttpHelper httpHelper;
 
   private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction =
@@ -63,17 +65,26 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS
       new CreateIosAppFromAppIdFunction();
 
   FirebaseProjectManagementServiceImpl(FirebaseApp app) {
-    this(app, Sleeper.DEFAULT, new FirebaseAppScheduler(app));
+    this(
+        app,
+        Sleeper.DEFAULT,
+        new FirebaseAppScheduler(app),
+        ApiClientUtils.newAuthorizedRequestFactory(app));
   }
 
-  FirebaseProjectManagementServiceImpl(FirebaseApp app, Sleeper sleeper, Scheduler scheduler) {
+  @VisibleForTesting
+  FirebaseProjectManagementServiceImpl(
+      FirebaseApp app, Sleeper sleeper, Scheduler scheduler, HttpRequestFactory requestFactory) {
     this.app = checkNotNull(app);
     this.sleeper = checkNotNull(sleeper);
     this.scheduler = checkNotNull(scheduler);
-    this.httpHelper = new HttpHelper(
-        app.getOptions().getJsonFactory(),
-        app.getOptions().getHttpTransport().createRequestFactory(
-            new FirebaseRequestInitializer(app)));
+    this.requestFactory = checkNotNull(requestFactory);
+    this.httpHelper = new HttpHelper(app.getOptions().getJsonFactory(), requestFactory);
+  }
+
+  @VisibleForTesting
+  HttpRequestFactory getRequestFactory() {
+    return requestFactory;
   }
 
   @VisibleForTesting


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 cf3b683..1bc0517 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -429,11 +429,11 @@ public class FirebaseAuthTest {
   private FirebaseAuth getAuthForIdTokenVerification(
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    FirebaseUserManager userManager = new FirebaseUserManager(app);
     return FirebaseAuth.builder()
         .setFirebaseApp(app)
-        .setTokenFactory(Suppliers.<FirebaseTokenFactory>ofInstance(null))
         .setIdTokenVerifier(tokenVerifierSupplier)
-        .setCookieVerifier(Suppliers.<FirebaseTokenVerifier>ofInstance(null))
+        .setUserManager(Suppliers.ofInstance(userManager))
         .build();
   }
 
@@ -444,11 +444,11 @@ public class FirebaseAuthTest {
   private FirebaseAuth getAuthForSessionCookieVerification(
       Supplier<? extends FirebaseTokenVerifier> tokenVerifierSupplier) {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);
+    FirebaseUserManager userManager = new FirebaseUserManager(app);
     return FirebaseAuth.builder()
         .setFirebaseApp(app)
-        .setTokenFactory(Suppliers.<FirebaseTokenFactory>ofInstance(null))
-        .setIdTokenVerifier(Suppliers.<FirebaseTokenVerifier>ofInstance(null))
         .setCookieVerifier(tokenVerifierSupplier)
+        .setUserManager(Suppliers.ofInstance(userManager))
         .build();
   }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 97ff744..de0b7fa 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -33,6 +33,7 @@ import com.google.api.client.json.JsonFactory;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -550,14 +551,7 @@ public class FirebaseUserManagerTest {
         .build();
 
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
-    MockHttpTransport transport = new MockHttpTransport.Builder()
-        .setLowLevelHttpResponse(response)
-        .build();
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setProjectId("test-project-id")
-        .setHttpTransport(transport)
-        .build());
+    FirebaseAuth auth = getRetryDisabledAuth(response);
 
     // Test for common HTTP error codes
     for (int code : ImmutableList.of(302, 400, 401, 404, 500)) {
@@ -566,7 +560,7 @@ public class FirebaseUserManagerTest {
         response.setContent("{}");
         response.setStatusCode(code);
         try {
-          operation.call(FirebaseAuth.getInstance());
+          operation.call(auth);
           fail("No error thrown for HTTP error: " + code);
         } catch (ExecutionException e) {
           assertTrue(e.getCause() instanceof FirebaseAuthException);
@@ -584,7 +578,7 @@ public class FirebaseUserManagerTest {
       response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND\"}}");
       response.setStatusCode(500);
       try {
-        operation.call(FirebaseAuth.getInstance());
+        operation.call(auth);
         fail("No error thrown for HTTP error");
       }  catch (ExecutionException e) {
         assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException);
@@ -615,16 +609,9 @@ public class FirebaseUserManagerTest {
     MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
     response.setContent("{\"not\" json}");
     response.setStatusCode(500);
-    MockHttpTransport transport = new MockHttpTransport.Builder()
-        .setLowLevelHttpResponse(response)
-        .build();
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setProjectId("test-project-id")
-        .setHttpTransport(transport)
-        .build());
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     try {
-      FirebaseAuth.getInstance().getUserAsync("testuser").get();
+      auth.getUserAsync("testuser").get();
       fail("No error thrown for JSON error");
     }  catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
@@ -1173,15 +1160,10 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testHttpErrorWithCode() {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of(
-            new MockLowLevelHttpResponse()
-                .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}")
-                .setStatusCode(500))))
-        .setProjectId("test-project-id")
-        .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}")
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     FirebaseUserManager userManager = auth.getUserManager();
     try {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
@@ -1194,15 +1176,10 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testUnexpectedHttpError() {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of(
-            new MockLowLevelHttpResponse()
-                .setContent("{}")
-                .setStatusCode(500))))
-        .setProjectId("test-project-id")
-        .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse()
+        .setContent("{}")
+        .setStatusCode(500);
+    FirebaseAuth auth = getRetryDisabledAuth(response);
     FirebaseUserManager userManager = auth.getUserManager();
     try {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
@@ -1231,6 +1208,26 @@ public class FirebaseUserManagerTest {
     return interceptor;
   }
 
+  private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) {
+    final MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    final FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(credentials)
+        .setProjectId("test-project-id")
+        .setHttpTransport(transport)
+        .build());
+    return FirebaseAuth.builder()
+        .setFirebaseApp(app)
+        .setUserManager(new Supplier<FirebaseUserManager>() {
+          @Override
+          public FirebaseUserManager get() {
+            return new FirebaseUserManager(app, transport.createRequestFactory());
+          }
+        })
+        .build();
+  }
+
   private static void checkUserRecord(UserRecord userRecord) {
     assertEquals("testuser", userRecord.getUid());
     assertEquals("testuser@example.com", userRecord.getEmail());
diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java
index 78eabaf..c19f5f5 100644
--- a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java
+++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java
@@ -69,6 +69,19 @@ public class ApiClientUtilsTest {
     assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503));
   }
 
+  @Test
+  public void testAuthorizedHttpClientWithoutRetry() throws IOException {
+    FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
+
+    HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app, null);
+
+    assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer);
+    HttpRequest request = requestFactory.buildGetRequest(TEST_URL);
+    assertEquals("Bearer test-token", request.getHeaders().getAuthorization());
+    HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler();
+    assertFalse(retryHandler instanceof RetryHandlerDecorator);
+  }
+
   @Test
   public void testUnauthorizedHttpClient() throws IOException {
     FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
diff --git a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java
new file mode 100644
index 0000000..9f4bc2d
--- /dev/null
+++ b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * 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.
+ */
+
+package com.google.firebase.internal;
+
+import static com.google.firebase.internal.ApiClientUtils.DEFAULT_RETRY_CONFIG;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
+import com.google.api.client.testing.util.MockSleeper;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator;
+import java.io.IOException;
+
+public class TestApiClientUtils {
+
+  private static final RetryConfig TEST_RETRY_CONFIG = RetryConfig.builder()
+      .setMaxRetries(DEFAULT_RETRY_CONFIG.getMaxRetries())
+      .setRetryStatusCodes(DEFAULT_RETRY_CONFIG.getRetryStatusCodes())
+      .setMaxIntervalMillis(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis())
+      .setSleeper(new MockSleeper())
+      .build();
+
+  private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com");
+
+  /**
+   * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and
+   * automatic retries. Bypasses exponential backoff between consecutive retries for faster
+   * execution during tests.
+   *
+   * @param app {@link FirebaseApp} from which to obtain authorization credentials.
+   * @return A new {@code HttpRequestFactory} instance.
+   */
+  public static HttpRequestFactory delayBypassedRequestFactory(FirebaseApp app) {
+    return ApiClientUtils.newAuthorizedRequestFactory(app, TEST_RETRY_CONFIG);
+  }
+
+  /**
+   * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts but
+   * no retries.
+   *
+   * @param app {@link FirebaseApp} from which to obtain authorization credentials.
+   * @return A new {@code HttpRequestFactory} instance.
+   */
+  public static HttpRequestFactory retryDisabledRequestFactory(FirebaseApp app) {
+    return ApiClientUtils.newAuthorizedRequestFactory(app, null);
+  }
+
+  /**
+   * Checks whther the given HttpRequestFactory has been configured for authorization and
+   * automatic retries.
+   *
+   * @param requestFactory The HttpRequestFactory to check.
+   */
+  public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) {
+    assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer);
+    HttpRequest request;
+    try {
+      request = requestFactory.buildGetRequest(TEST_URL);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to initialize request", e);
+    }
+
+    // Verify authorization
+    assertTrue(request.getHeaders().getAuthorization().startsWith("Bearer "));
+
+    // Verify retry support
+    HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler();
+    assertTrue(retryHandler instanceof RetryHandlerDecorator);
+    RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler()
+        .getRetryConfig();
+    assertEquals(DEFAULT_RETRY_CONFIG.getMaxRetries(), retryConfig.getMaxRetries());
+    assertEquals(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis(), retryConfig.getMaxIntervalMillis());
+    assertFalse(retryConfig.isRetryOnIOExceptions());
+    assertEquals(DEFAULT_RETRY_CONFIG.getRetryStatusCodes(), retryConfig.getRetryStatusCodes());
+  }
+}
diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java
index a2fe872..87afe60 100644
--- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java
+++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java
@@ -29,6 +29,7 @@ import static org.junit.Assert.fail;
 
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
 import com.google.api.client.http.HttpResponse;
 import com.google.api.client.http.HttpResponseInterceptor;
 import com.google.api.client.json.JsonParser;
@@ -43,6 +44,7 @@ import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.auth.MockGoogleCredentials;
+import com.google.firebase.internal.TestApiClientUtils;
 import com.google.firebase.testing.MultiRequestMockHttpTransport;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -370,7 +372,7 @@ public class FirebaseProjectManagementServiceImplTest {
     MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse();
     secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE);
     serviceImpl = initServiceImpl(
-        ImmutableList.<MockLowLevelHttpResponse>of(firstRpcResponse, secondRpcResponse),
+        ImmutableList.of(firstRpcResponse, secondRpcResponse),
         interceptor);
 
     List<IosApp> iosAppList = serviceImpl.listIosAppsAsync(PROJECT_ID).get();
@@ -400,7 +402,7 @@ public class FirebaseProjectManagementServiceImplTest {
     MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse();
     thirdRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE);
     serviceImpl = initServiceImpl(
-        ImmutableList.<MockLowLevelHttpResponse>of(
+        ImmutableList.of(
             firstRpcResponse, secondRpcResponse, thirdRpcResponse),
         interceptor);
 
@@ -624,7 +626,7 @@ public class FirebaseProjectManagementServiceImplTest {
     MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse();
     secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE);
     serviceImpl = initServiceImpl(
-        ImmutableList.<MockLowLevelHttpResponse>of(firstRpcResponse, secondRpcResponse),
+        ImmutableList.of(firstRpcResponse, secondRpcResponse),
         interceptor);
 
     List<AndroidApp> androidAppList = serviceImpl.listAndroidApps(PROJECT_ID);
@@ -652,7 +654,7 @@ public class FirebaseProjectManagementServiceImplTest {
     MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse();
     secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE);
     serviceImpl = initServiceImpl(
-        ImmutableList.<MockLowLevelHttpResponse>of(firstRpcResponse, secondRpcResponse),
+        ImmutableList.of(firstRpcResponse, secondRpcResponse),
         interceptor);
 
     List<AndroidApp> androidAppList = serviceImpl.listAndroidAppsAsync(PROJECT_ID).get();
@@ -682,7 +684,7 @@ public class FirebaseProjectManagementServiceImplTest {
     MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse();
     thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE);
     serviceImpl = initServiceImpl(
-        ImmutableList.<MockLowLevelHttpResponse>of(
+        ImmutableList.of(
             firstRpcResponse, secondRpcResponse, thirdRpcResponse),
         interceptor);
 
@@ -714,7 +716,7 @@ public class FirebaseProjectManagementServiceImplTest {
     MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse();
     thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE);
     serviceImpl = initServiceImpl(
-        ImmutableList.<MockLowLevelHttpResponse>of(
+        ImmutableList.of(
             firstRpcResponse, secondRpcResponse, thirdRpcResponse),
         interceptor);
 
@@ -915,10 +917,48 @@ public class FirebaseProjectManagementServiceImplTest {
     checkRequestHeader(expectedUrl, HttpMethod.DELETE);
   }
 
+  @Test
+  public void testAuthAndRetriesSupport() {
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId(PROJECT_ID)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+
+    FirebaseProjectManagementServiceImpl serviceImpl =
+        new FirebaseProjectManagementServiceImpl(app);
+
+    TestApiClientUtils.assertAuthAndRetrySupport(serviceImpl.getRequestFactory());
+  }
+
+  @Test
+  public void testHttpRetries() throws Exception {
+    List<MockLowLevelHttpResponse> mockResponses = ImmutableList.of(
+        firstRpcResponse.setStatusCode(503).setContent("{}"),
+        new MockLowLevelHttpResponse().setContent("{}"));
+    MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses);
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId(PROJECT_ID)
+        .setHttpTransport(transport)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    HttpRequestFactory requestFactory = TestApiClientUtils.delayBypassedRequestFactory(app);
+    FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl(
+        app, new MockSleeper(), new MockScheduler(), requestFactory);
+    serviceImpl.setInterceptor(interceptor);
+
+    serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME);
+
+    String expectedUrl = String.format(
+        "%s/v1beta1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, SHA1_RESOURCE_NAME);
+    checkRequestHeader(expectedUrl, HttpMethod.DELETE);
+  }
+
   private static FirebaseProjectManagementServiceImpl initServiceImpl(
       MockLowLevelHttpResponse mockResponse,
       MultiRequestTestResponseInterceptor interceptor) {
-    return initServiceImpl(ImmutableList.<MockLowLevelHttpResponse>of(mockResponse), interceptor);
+    return initServiceImpl(ImmutableList.of(mockResponse), interceptor);
   }
 
   private static FirebaseProjectManagementServiceImpl initServiceImpl(
@@ -931,8 +971,9 @@ public class FirebaseProjectManagementServiceImplTest {
         .setHttpTransport(transport)
         .build();
     FirebaseApp app = FirebaseApp.initializeApp(options);
-    FirebaseProjectManagementServiceImpl serviceImpl =
-        new FirebaseProjectManagementServiceImpl(app, new MockSleeper(), new MockScheduler());
+    HttpRequestFactory requestFactory = TestApiClientUtils.retryDisabledRequestFactory(app);
+    FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl(
+        app, new MockSleeper(), new MockScheduler(), requestFactory);
     serviceImpl.setInterceptor(interceptor);
     return serviceImpl;
   }