Repository: firebase/firebase-admin-java
Pull request #38 (2017-06-06 00:09:07)

Making JsonFactory and HttpFactory Configurable
* Exposed protected factory methods in `FirebaseCredentials`, which accept `JsonFactory` and `HttpTransport` arguments.
* Added `setJsonFactory()` and `setHttpTransport()` methods to `FirebaseOptions`.

Related to #35 


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index eed0277..29c926d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -134,21 +134,19 @@ requires an Internet connection. Create a new project in the
 [Firebase console](https://console.firebase.google.com/) if you do not already have one. Use a 
 separate, dedicated project for integration tests since the test suite makes a large number of
 writes to the Firebase realtime database. Download the service account private key from the 
-"Settings" page of the project. Now run the following command to invoke the integration test suite:
+"Settings > Service Accounts" page of the project. Also obtain the web API key of the project
+from the "Settings > General" page. Now run the following command to invoke the integration
+test suite:
 
 ```
-mvn verify -Dfirebase.it.certificate=path/to/your/serviceAccount.json
+mvn verify -Dfirebase.it.certificate=path/to/serviceAccount.json -Dfirebase.it.apikey=API-Key
 ```
 
 Make sure to specify the correct path to your downloaded service account key file as the
 `firebase.it.certificate` system property. 
 
 The above command invokes both unit and integration test suites. To execute only the integration
-tests, run the command as follows:
-
-```
-mvn verify -Dfirebase.it.certificate=path/to/your/serviceAccount.json -DskipUTs
-```
+tests, specify the `-DskipUTs` flag.
 
 ### Generating API Docs
 
diff --git a/pom.xml b/pom.xml
index 9aecd08..2695e17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>com.google.firebase</groupId>
     <artifactId>firebase-admin</artifactId>
-    <version>5.0.2-SNAPSHOT</version>
+    <version>5.1.1-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>firebase-admin</name>
@@ -187,12 +187,6 @@
                     <plugin>
                         <artifactId>maven-source-plugin</artifactId>
                         <version>2.2.1</version>
-                        <configuration>
-                            <excludes>
-                                <!-- Exclude all files until we go open source -->
-                                <exclude>com/**</exclude>
-                            </excludes>
-                        </configuration>
                         <executions>
                             <execution>
                                 <id>attach-sources</id>
@@ -347,12 +341,6 @@
             <version>0.6</version>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.hamcrest</groupId>
-            <artifactId>hamcrest-library</artifactId>
-            <version>1.3</version>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>com.cedarsoftware</groupId>
             <artifactId>java-util</artifactId>
diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java
index ef8490b..f707d97 100644
--- a/src/main/java/com/google/firebase/FirebaseOptions.java
+++ b/src/main/java/com/google/firebase/FirebaseOptions.java
@@ -16,9 +16,11 @@
 
 package com.google.firebase;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.json.JsonFactory;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.firebase.auth.FirebaseCredential;
@@ -37,15 +39,18 @@ public final class FirebaseOptions {
   private final String databaseUrl;
   private final FirebaseCredential firebaseCredential;
   private final Map<String, Object> databaseAuthVariableOverride;
-
-  private FirebaseOptions(
-      @Nullable String databaseUrl,
-      @NonNull FirebaseCredential firebaseCredential,
-      @Nullable Map<String, Object> databaseAuthVariableOverride) {
-
-    this.databaseUrl = databaseUrl;
-    this.firebaseCredential = checkNotNull(firebaseCredential, "Service Account must be provided.");
-    this.databaseAuthVariableOverride = databaseAuthVariableOverride;
+  private final HttpTransport httpTransport;
+  private final JsonFactory jsonFactory;
+
+  private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
+    this.firebaseCredential = checkNotNull(builder.firebaseCredential,
+        "FirebaseOptions must be initialized with setCredential().");
+    this.databaseUrl = builder.databaseUrl;
+    this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride;
+    this.httpTransport = checkNotNull(builder.httpTransport,
+        "FirebaseOptions must be initialized with a non-null HttpTransport.");
+    this.jsonFactory = checkNotNull(builder.jsonFactory,
+        "FirebaseOptions must be initialized with a non-null JsonFactory.");
   }
 
   /**
@@ -71,6 +76,26 @@ public final class FirebaseOptions {
     return databaseAuthVariableOverride;
   }
 
+  /**
+   * Returns the HttpTransport used to call remote HTTP endpoints.
+   *
+   * @return A Google API client HttpTransport instance.
+   */
+  @NonNull
+  public HttpTransport getHttpTransport() {
+    return httpTransport;
+  }
+
+  /**
+   * Returns the JsonFactory used to parse JSON when calling remote HTTP endpoints.
+   *
+   * @return A Google API client JsonFactory instance.
+   */
+  @NonNull
+  public JsonFactory getJsonFactory() {
+    return jsonFactory;
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (!(obj instanceof FirebaseOptions)) {
@@ -104,6 +129,8 @@ public final class FirebaseOptions {
     private String databaseUrl;
     private FirebaseCredential firebaseCredential;
     private Map<String, Object> databaseAuthVariableOverride = new HashMap<>();
+    private HttpTransport httpTransport = Utils.getDefaultTransport();
+    private JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
 
     /** Constructs an empty builder. */
     public Builder() {}
@@ -118,6 +145,8 @@ public final class FirebaseOptions {
       databaseUrl = options.databaseUrl;
       firebaseCredential = options.firebaseCredential;
       databaseAuthVariableOverride = options.databaseAuthVariableOverride;
+      httpTransport = options.httpTransport;
+      jsonFactory = options.jsonFactory;
     }
 
     /**
@@ -173,15 +202,37 @@ public final class FirebaseOptions {
       return this;
     }
 
+    /**
+     * Sets the HttpTransport used to make remote HTTP calls. A reasonable default
+     * will be used if not explicitly set.
+     *
+     * @param httpTransport An HttpTransport instance
+     * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
+     */
+    public Builder setHttpTransport(HttpTransport httpTransport) {
+      this.httpTransport = httpTransport;
+      return this;
+    }
+
+    /**
+     * Sets the JsonFactory used to parse JSON when making remote HTTP calls. A reasonable default
+     * will be used if not explicitly set.
+     *
+     * @param jsonFactory A JsonFactory instance.
+     * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
+     */
+    public Builder setJsonFactory(JsonFactory jsonFactory) {
+      this.jsonFactory = jsonFactory;
+      return this;
+    }
+
     /**
      * Builds the {@link FirebaseOptions} instance from the previously set options.
      *
      * @return A {@link FirebaseOptions} instance created from the previously set options.
      */
     public FirebaseOptions build() {
-      checkArgument(firebaseCredential != null,
-          "FirebaseOptions must be initialized with setCredential().");
-      return new FirebaseOptions(databaseUrl, firebaseCredential, databaseAuthVariableOverride);
+      return new FirebaseOptions(this);
     }
   }
 }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index d505645..804bb36 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -21,9 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
 import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
-import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
-import com.google.api.client.json.gson.GsonFactory;
 import com.google.api.client.util.Clock;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
@@ -53,12 +51,10 @@ import java.util.Map;
  */
 public class FirebaseAuth {
 
-  /** A global, thread-safe Json Factory built using Gson. */
-  private static final JsonFactory jsonFactory = new GsonFactory();
-
   private final FirebaseApp firebaseApp;
   private final GooglePublicKeysManager googlePublicKeysManager;
   private final Clock clock;
+  private final JsonFactory jsonFactory;
   private final FirebaseUserManager userManager;
 
   private FirebaseAuth(FirebaseApp firebaseApp) {
@@ -75,7 +71,9 @@ public class FirebaseAuth {
     this.firebaseApp = firebaseApp;
     this.googlePublicKeysManager = googlePublicKeysManager;
     this.clock = clock;
-    this.userManager = new FirebaseUserManager(jsonFactory, Utils.getDefaultTransport());
+    this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
+    this.userManager = new FirebaseUserManager(jsonFactory,
+        firebaseApp.getOptions().getHttpTransport());
   }
 
   /**
diff --git a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
index aa59b11..6635eee 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java
@@ -22,7 +22,6 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.http.HttpTransport;
 import com.google.api.client.json.JsonFactory;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.CharStreams;
 import com.google.firebase.internal.NonNull;
@@ -80,8 +79,27 @@ public class FirebaseCredentials {
     return DefaultCredentialsHolder.INSTANCE;
   }
 
-  @VisibleForTesting
-  static FirebaseCredential applicationDefault(HttpTransport transport, JsonFactory jsonFactory) {
+  /**
+   * Returns a {@link FirebaseCredential} based on Google Application Default Credentials which can
+   * be used to authenticate the SDK. Allows specifying the HttpTransport and the
+   * JsonFactory to be used when communicating with the remote authentication server.
+   *
+   * <p>See <a
+   * href="https://developers.google.com/identity/protocols/application-default-credentials">Google
+   * Application Default Credentials</a> for details on Google Application Deafult Credentials.
+   *
+   * <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
+   * and detailed documentation.
+   *
+   * @param transport HttpTransport used to communicate with the remote authentication server.
+   * @param jsonFactory JsonFactory used to parse JSON responses from the remote authentication
+   *     server.
+   * @return A {@link FirebaseCredential} based on Google Application Default Credentials which can
+   *     be used to authenticate the SDK.
+   */
+  @NonNull
+  public static FirebaseCredential applicationDefault(
+      HttpTransport transport, JsonFactory jsonFactory) {
     return new ApplicationDefaultCredential(transport, jsonFactory);
   }
 
@@ -104,9 +122,26 @@ public class FirebaseCredentials {
         Utils.getDefaultJsonFactory());
   }
 
-  @VisibleForTesting
-  static FirebaseCredential fromCertificate(InputStream serviceAccount, HttpTransport transport,
-      JsonFactory jsonFactory) throws IOException {
+  /**
+   * Returns a {@link FirebaseCredential} generated from the provided service account certificate
+   * which can be used to authenticate the SDK. Allows specifying the HttpTransport and the
+   * JsonFactory to be used when communicating with the remote authentication server.
+   *
+   * <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
+   * and detailed documentation.
+   *
+   * @param serviceAccount An <code>InputStream</code> containing the JSON representation of a
+   *     service account certificate.
+   * @param transport HttpTransport used to communicate with the remote authentication server.
+   * @param jsonFactory JsonFactory used to parse JSON responses from the remote authentication
+   *     server.
+   * @return A {@link FirebaseCredential} generated from the provided service account certificate
+   *     which can be used to authenticate the SDK.
+   * @throws IOException If an error occurs while parsing the service account certificate.
+   */
+  @NonNull
+  public static FirebaseCredential fromCertificate(InputStream serviceAccount,
+      HttpTransport transport, JsonFactory jsonFactory) throws IOException {
     return new CertCredential(serviceAccount, transport, jsonFactory);
   }
 
@@ -129,8 +164,25 @@ public class FirebaseCredentials {
         refreshToken, Utils.getDefaultTransport(), Utils.getDefaultJsonFactory());
   }
 
-  @VisibleForTesting
-  static FirebaseCredential fromRefreshToken(final InputStream refreshToken,
+  /**
+   * Returns a {@link FirebaseCredential} generated from the provided refresh token which can be
+   * used to authenticate the SDK. Allows specifying the HttpTransport and the
+   * JsonFactory to be used when communicating with the remote authentication server.
+   *
+   * <p>See <a href="/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
+   * and detailed documentation.
+   *
+   * @param refreshToken An <code>InputStream</code> containing the JSON representation of a refresh
+   *     token.
+   * @param transport HttpTransport used to communicate with the remote authentication server.
+   * @param jsonFactory JsonFactory used to parse JSON responses from the remote authentication
+   *     server.
+   * @return A {@link FirebaseCredential} generated from the provided service account credential
+   *     which can be used to authenticate the SDK.
+   * @throws IOException If an error occurs while parsing the refresh token.
+   */
+  @NonNull
+  public static FirebaseCredential fromRefreshToken(final InputStream refreshToken,
       HttpTransport transport, JsonFactory jsonFactory) throws IOException {
     return new RefreshTokenCredential(refreshToken, transport, jsonFactory);
   }
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index ff57a63..658124e 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -23,11 +23,13 @@ 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.HttpResponse;
+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;
 import com.google.api.client.json.JsonObjectParser;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -35,6 +37,7 @@ import com.google.firebase.auth.UserRecord.CreateRequest;
 import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.auth.internal.GetAccountInfoResponse;
 
+import com.google.firebase.internal.SdkUtils;
 import java.io.IOException;
 import java.util.Map;
 
@@ -55,9 +58,13 @@ class FirebaseUserManager {
 
   private static final String ID_TOOLKIT_URL =
       "https://www.googleapis.com/identitytoolkit/v3/relyingparty/";
+  private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
 
   private final JsonFactory jsonFactory;
   private final HttpRequestFactory requestFactory;
+  private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
+
+  private HttpResponseInterceptor interceptor;
 
   /**
    * Creates a new FirebaseUserManager instance.
@@ -70,6 +77,11 @@ class FirebaseUserManager {
     this.requestFactory = transport.createRequestFactory();
   }
 
+  @VisibleForTesting
+  void setInterceptor(HttpResponseInterceptor interceptor) {
+    this.interceptor = interceptor;
+  }
+
   UserRecord getUserById(String uid, String token) throws FirebaseAuthException {
     final Map<String, Object> payload = ImmutableMap.<String, Object>of(
         "localId", ImmutableList.of(uid));
@@ -165,7 +177,9 @@ class FirebaseUserManager {
     HttpRequest request = requestFactory.buildPostRequest(url,
         new JsonHttpContent(jsonFactory, content));
     request.setParser(new JsonObjectParser(jsonFactory));
-    request.getHeaders().setAuthorization("Bearer " + token);
+    request.getHeaders().setAuthorization("Bearer " + token)
+        .set(CLIENT_VERSION_HEADER, clientVersion);
+    request.setResponseInterceptor(interceptor);
     HttpResponse response = request.execute();
     try {
       return response.parseAs(clazz);
diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
index 62d8c47..388175a 100644
--- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java
+++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
@@ -32,12 +32,10 @@ import com.google.firebase.database.utilities.Utilities;
 import com.google.firebase.database.utilities.Validation;
 import com.google.firebase.internal.FirebaseService;
 
-import java.io.IOException;
-import java.io.InputStream;
+import com.google.firebase.internal.SdkUtils;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Properties;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -47,9 +45,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
  */
 public class FirebaseDatabase {
 
-  private static final String ADMIN_SDK_PROPERTIES = "admin_sdk.properties";
-  private static final String SDK_VERSION = loadSdkVersion();
-
   private final FirebaseApp app;
   private final RepoInfo repoInfo;
   private final DatabaseConfig config;
@@ -165,7 +160,7 @@ public class FirebaseDatabase {
    * @return The version for this build of the Firebase Database client
    */
   public static String getSdkVersion() {
-    return SDK_VERSION;
+    return SdkUtils.getVersion();
   }
 
   /**
@@ -370,17 +365,6 @@ public class FirebaseDatabase {
     }
   }
 
-  private static String loadSdkVersion() {
-    try (InputStream in = FirebaseDatabase.class.getClassLoader()
-        .getResourceAsStream(ADMIN_SDK_PROPERTIES)) {
-      Properties properties = new Properties();
-      properties.load(checkNotNull(in, "Failed to load: " + ADMIN_SDK_PROPERTIES));
-      return properties.getProperty("sdk.version");
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
   private static final String SERVICE_ID = FirebaseDatabase.class.getName();
 
   private static class DatabaseInstances {
diff --git a/src/main/java/com/google/firebase/internal/SdkUtils.java b/src/main/java/com/google/firebase/internal/SdkUtils.java
new file mode 100644
index 0000000..2fa5e57
--- /dev/null
+++ b/src/main/java/com/google/firebase/internal/SdkUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 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.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * A collection of internal utilities that can be used anywhere in the Admin SDK.
+ */
+public class SdkUtils {
+
+  private static final String ADMIN_SDK_PROPERTIES = "admin_sdk.properties";
+  private static final String SDK_VERSION = loadSdkVersion();
+
+  /**
+   * Returns the version of this Admin SDK distribution.
+   *
+   * @return A semver version string.
+   */
+  public static String getVersion() {
+    return SDK_VERSION;
+  }
+
+  private static String loadSdkVersion() {
+    try (InputStream in = SdkUtils.class.getClassLoader()
+        .getResourceAsStream(ADMIN_SDK_PROPERTIES)) {
+      Properties properties = new Properties();
+      properties.load(checkNotNull(in, "Failed to load: " + ADMIN_SDK_PROPERTIES));
+      return properties.getProperty("sdk.version");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+}


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
index ab1a196..386df0c 100644
--- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java
+++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
@@ -19,11 +19,15 @@ package com.google.firebase;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
 import com.google.firebase.auth.FirebaseCredential;
 import com.google.firebase.auth.FirebaseCredentials;
 import com.google.firebase.auth.TestOnlyImplFirebaseAuthTrampolines;
@@ -53,12 +57,18 @@ public class FirebaseOptionsTest {
   @Test
   public void createOptionsWithAllValuesSet() throws IOException, InterruptedException {
     final Semaphore semaphore = new Semaphore(0);
+    GsonFactory jsonFactory = new GsonFactory();
+    NetHttpTransport httpTransport = new NetHttpTransport();
     FirebaseOptions firebaseOptions =
         new FirebaseOptions.Builder()
             .setDatabaseUrl(FIREBASE_DB_URL)
             .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
+            .setJsonFactory(jsonFactory)
+            .setHttpTransport(httpTransport)
             .build();
     assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl());
+    assertSame(jsonFactory, firebaseOptions.getJsonFactory());
+    assertSame(httpTransport, firebaseOptions.getHttpTransport());
     TestOnlyImplFirebaseAuthTrampolines.getCertificate(firebaseOptions.getCredential())
         .addOnSuccessListener(
             new OnSuccessListener<GoogleCredential>() {
@@ -79,6 +89,8 @@ public class FirebaseOptionsTest {
         new FirebaseOptions.Builder()
             .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
             .build();
+    assertNotNull(firebaseOptions.getJsonFactory());
+    assertNotNull(firebaseOptions.getHttpTransport());
     TestOnlyImplFirebaseAuthTrampolines.getCertificate(firebaseOptions.getCredential())
         .addOnSuccessListener(
             new OnSuccessListener<GoogleCredential>() {
@@ -109,7 +121,7 @@ public class FirebaseOptionsTest {
     assertEquals("mock-project-id", Tasks.await(projectId));
   }
 
-  @Test(expected = IllegalArgumentException.class)
+  @Test(expected = NullPointerException.class)
   public void createOptionsWithCredentialMissing() {
     new FirebaseOptions.Builder().build();
   }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index ed15672..5cb3408 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -22,11 +22,23 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+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;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.auth.UserRecord.CreateRequest;
 import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.tasks.Tasks;
 import com.google.firebase.testing.IntegrationTestUtils;
+import java.io.IOException;
+import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import org.junit.BeforeClass;
@@ -34,6 +46,11 @@ import org.junit.Test;
 
 public class FirebaseAuthIT {
 
+  private static final String ID_TOOLKIT_URL =
+      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken";
+  private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+  private static final HttpTransport transport = Utils.getDefaultTransport();
+
   private static FirebaseAuth auth;
 
   @BeforeClass
@@ -193,4 +210,41 @@ public class FirebaseAuthIT {
     }
   }
 
+  @Test
+  public void testCustomToken() throws Exception {
+    String customToken = Tasks.await(auth.createCustomToken("user1"));
+    String idToken = signInWithCustomToken(customToken);
+    FirebaseToken decoded = Tasks.await(auth.verifyIdToken(idToken));
+    assertEquals("user1", decoded.getUid());
+  }
+
+  @Test
+  public void testCustomTokenWithClaims() throws Exception {
+    Map<String, Object> devClaims = ImmutableMap.<String, Object>of(
+        "premium", true, "subscription", "silver");
+    String customToken = Tasks.await(auth.createCustomToken("user2", devClaims));
+    String idToken = signInWithCustomToken(customToken);
+    FirebaseToken decoded = Tasks.await(auth.verifyIdToken(idToken));
+    assertEquals("user2", decoded.getUid());
+    assertTrue((Boolean) decoded.getClaims().get("premium"));
+    assertEquals("silver", decoded.getClaims().get("subscription"));
+  }
+
+  private String signInWithCustomToken(String customToken) throws IOException {
+    GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + "?key="
+        + IntegrationTestUtils.getApiKey());
+    Map<String, Object> content = ImmutableMap.<String, Object>of(
+        "token", customToken, "returnSecureToken", true);
+    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
+        new JsonHttpContent(jsonFactory, content));
+    request.setParser(new JsonObjectParser(jsonFactory));
+    HttpResponse response = request.execute();
+    try {
+      GenericJson json = response.parseAs(GenericJson.class);
+      return json.get("idToken").toString();
+    } finally {
+      response.disconnect();
+    }
+  }
+
 }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 8858f51..0a9bcee 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -21,12 +21,16 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseInterceptor;
 import com.google.api.client.json.gson.GsonFactory;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.common.collect.ImmutableList;
 import com.google.firebase.auth.UserRecord.CreateRequest;
 import com.google.firebase.auth.UserRecord.UpdateRequest;
+import com.google.firebase.internal.SdkUtils;
 import com.google.firebase.testing.TestUtils;
 
 import java.io.IOException;
@@ -35,6 +39,7 @@ import org.junit.Test;
 
 public class FirebaseUserManagerTest {
 
+  private static final String TEST_TOKEN = "token";
   private static final GsonFactory gson = new GsonFactory();
 
   @Test
@@ -45,8 +50,11 @@ public class FirebaseUserManagerTest {
         .setLowLevelHttpResponse(response)
         .build();
     FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
-    UserRecord userRecord = userManager.getUserById("testuser", "token");
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    userManager.setInterceptor(interceptor);
+    UserRecord userRecord = userManager.getUserById("testuser", TEST_TOKEN);
     checkUserRecord(userRecord);
+    checkRequestHeaders(interceptor);
   }
 
   @Test
@@ -58,7 +66,7 @@ public class FirebaseUserManagerTest {
         .build();
     FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
     try {
-      userManager.getUserById("testuser", "token");
+      userManager.getUserById("testuser", TEST_TOKEN);
       fail("No error thrown for invalid response");
     } catch (FirebaseAuthException e) {
       assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, e.getErrorCode());
@@ -73,8 +81,11 @@ public class FirebaseUserManagerTest {
         .setLowLevelHttpResponse(response)
         .build();
     FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
-    UserRecord userRecord = userManager.getUserByEmail("testuser@example.com", "token");
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    userManager.setInterceptor(interceptor);
+    UserRecord userRecord = userManager.getUserByEmail("testuser@example.com", TEST_TOKEN);
     checkUserRecord(userRecord);
+    checkRequestHeaders(interceptor);
   }
 
   @Test
@@ -85,8 +96,11 @@ public class FirebaseUserManagerTest {
         .setLowLevelHttpResponse(response)
         .build();
     FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
-    String uid = userManager.createUser(new CreateRequest(), "token");
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    userManager.setInterceptor(interceptor);
+    String uid = userManager.createUser(new CreateRequest(), TEST_TOKEN);
     assertEquals("testuser", uid);
+    checkRequestHeaders(interceptor);
   }
 
   @Test
@@ -97,8 +111,26 @@ public class FirebaseUserManagerTest {
         .setLowLevelHttpResponse(response)
         .build();
     FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    userManager.setInterceptor(interceptor);
+    // should not throw
+    userManager.updateUser(new UpdateRequest("testuser"), TEST_TOKEN);
+    checkRequestHeaders(interceptor);
+  }
+
+  @Test
+  public void testDeleteUser() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    response.setContent(TestUtils.loadResource("deleteUser.json"));
+    MockHttpTransport transport = new MockHttpTransport.Builder()
+        .setLowLevelHttpResponse(response)
+        .build();
+    FirebaseUserManager userManager = new FirebaseUserManager(gson, transport);
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    userManager.setInterceptor(interceptor);
     // should not throw
-    userManager.updateUser(new UpdateRequest("testuser"), "token");
+    userManager.deleteUser("testuser", TEST_TOKEN);
+    checkRequestHeaders(interceptor);
   }
 
   @Test
@@ -381,4 +413,23 @@ public class FirebaseUserManagerTest {
     assertEquals("password", provider.getProviderId());
   }
 
+  private void checkRequestHeaders(TestResponseInterceptor interceptor) {
+    HttpHeaders headers = interceptor.response.getRequest().getHeaders();
+    String auth = "Bearer " + TEST_TOKEN;
+    assertEquals(auth, headers.getFirstHeaderStringValue("Authorization"));
+
+    String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
+    assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version"));
+  }
+
+  private static class TestResponseInterceptor implements HttpResponseInterceptor {
+
+    private HttpResponse response;
+
+    @Override
+    public void interceptResponse(HttpResponse response) throws IOException {
+      this.response = response;
+    }
+  }
+
 }
diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java
index 8751cd2..808340a 100644
--- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java
+++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java
@@ -17,7 +17,7 @@
 package com.google.firebase.auth.internal;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 import com.google.api.client.auth.openidconnect.IdToken;
 import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
@@ -42,7 +42,6 @@ import java.security.PrivateKey;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.KeySpec;
 import java.security.spec.PKCS8EncodedKeySpec;
-import org.hamcrest.collection.IsIterableContainingInOrder;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -130,7 +129,7 @@ public class FirebaseTokenVerifierTest {
             FACTORY, createToken(createHeader(), createPayload()));
 
     IdToken.Payload payload = (IdToken.Payload) token.getClaims();
-    assertThat(payload.getAudienceAsList(), IsIterableContainingInOrder.contains(PROJECT_ID));
+    assertTrue(payload.getAudienceAsList().contains(PROJECT_ID));
     assertEquals(ISSUER, payload.getIssuer());
 
     verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token));
diff --git a/src/test/java/com/google/firebase/tasks/TaskImplTest.java b/src/test/java/com/google/firebase/tasks/TaskImplTest.java
index fd16a01..8a3a65c 100644
--- a/src/test/java/com/google/firebase/tasks/TaskImplTest.java
+++ b/src/test/java/com/google/firebase/tasks/TaskImplTest.java
@@ -16,29 +16,25 @@
 
 package com.google.firebase.tasks;
 
-import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.tasks.testing.TestOnCompleteListener;
 import com.google.firebase.tasks.testing.TestOnFailureListener;
 import com.google.firebase.tasks.testing.TestOnSuccessListener;
 import java.rmi.RemoteException;
-import org.hamcrest.Matchers;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class TaskImplTest {
 
   private static final Exception EXCEPTION = new RemoteException();
   private static final Void NULL_RESULT = null;
   private static final String NON_NULL_RESULT = "Success";
-  @Rule public ExpectedException expectedException = ExpectedException.none();
 
   @Test
   public void testIsComplete_notComplete() {
@@ -133,10 +129,12 @@ public class TaskImplTest {
     TaskImpl<Void> task = new TaskImpl<>();
     task.setException(EXCEPTION);
 
-    expectedException.expect(RuntimeExecutionException.class);
-    expectedException.expectCause(Matchers.is(EXCEPTION));
-
-    task.getResult();
+    try {
+      task.getResult();
+      fail("No exception thrown");
+    } catch (RuntimeExecutionException e) {
+      assertSame(EXCEPTION, e.getCause());
+    }
   }
 
   @Test
@@ -144,9 +142,12 @@ public class TaskImplTest {
     TaskImpl<Void> task = new TaskImpl<>();
     task.setException(EXCEPTION);
 
-    expectedException.expect(Matchers.is(EXCEPTION));
-
-    task.getResult(RemoteException.class);
+    try {
+      task.getResult(RemoteException.class);
+      fail("No exception thrown");
+    } catch (RemoteException e) {
+      assertSame(EXCEPTION, e);
+    }
   }
 
   @Test
@@ -155,10 +156,12 @@ public class TaskImplTest {
     Exception exception = new RuntimeException();
     task.setException(exception);
 
-    expectedException.expect(RuntimeExecutionException.class);
-    expectedException.expectCause(Matchers.is(exception));
-
-    task.getResult(RemoteException.class);
+    try {
+      task.getResult(RemoteException.class);
+      fail("No exception thrown");
+    } catch (RuntimeExecutionException e) {
+      assertSame(exception, e.getCause());
+    }
   }
 
   @Test
@@ -407,7 +410,7 @@ public class TaskImplTest {
               }
             });
     task.setResult(null);
-    assertThat(task2.getException(), instanceOf(RuntimeExecutionException.class));
+    assertTrue(task2.getException() instanceof RuntimeExecutionException);
   }
 
   @Test
@@ -507,7 +510,7 @@ public class TaskImplTest {
               }
             });
     task.setResult(null);
-    assertThat(task2.getException(), instanceOf(RuntimeExecutionException.class));
+    assertTrue(task2.getException() instanceof RuntimeExecutionException);
   }
 
   @Test
@@ -560,7 +563,7 @@ public class TaskImplTest {
               }
             });
     task.setResult(NON_NULL_RESULT);
-    assertThat(task2.getException(), instanceOf(NullPointerException.class));
+    assertTrue(task2.getException() instanceof NullPointerException);
   }
 
   @Test(expected = IllegalStateException.class)
diff --git a/src/test/java/com/google/firebase/tasks/TasksTest.java b/src/test/java/com/google/firebase/tasks/TasksTest.java
index aed2ff2..067ed93 100644
--- a/src/test/java/com/google/firebase/tasks/TasksTest.java
+++ b/src/test/java/com/google/firebase/tasks/TasksTest.java
@@ -18,6 +18,9 @@ package com.google.firebase.tasks;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.rmi.RemoteException;
 import java.util.concurrent.Callable;
@@ -27,10 +30,7 @@ import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import org.hamcrest.Matchers;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class TasksTest {
 
@@ -39,7 +39,6 @@ public class TasksTest {
   private static final int SCHEDULE_DELAY_MS = 50;
   private static final int TIMEOUT_MS = 200;
   private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
-  @Rule public ExpectedException expectedException = ExpectedException.none();
 
   @Test
   public void testForResult() throws Exception {
@@ -141,42 +140,52 @@ public class TasksTest {
 
   @Test
   public void testAwait_exception() throws Exception {
-    expectedException.expect(ExecutionException.class);
-    expectedException.expectCause(Matchers.is(EXCEPTION));
-
     TaskCompletionSource<Void> completionSource = new TaskCompletionSource<>();
     scheduleException(completionSource);
 
-    Tasks.await(completionSource.getTask(), TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    try {
+      Tasks.await(completionSource.getTask(), TIMEOUT_MS, TimeUnit.MILLISECONDS);
+      fail("No exception thrown");
+    } catch (ExecutionException e) {
+      assertSame(EXCEPTION, e.getCause());
+    }
   }
 
   @Test
   public void testAwait_noTimeoutException() throws Exception {
-    expectedException.expect(ExecutionException.class);
-    expectedException.expectCause(Matchers.is(EXCEPTION));
-
     TaskCompletionSource<Void> completionSource = new TaskCompletionSource<>();
     scheduleException(completionSource);
 
-    Tasks.await(completionSource.getTask());
+    try {
+      Tasks.await(completionSource.getTask());
+      fail("No exception thrown");
+    } catch (ExecutionException e) {
+      assertSame(EXCEPTION, e.getCause());
+    }
   }
 
   @Test
   public void testAwait_alreadyFailed() throws Exception {
-    expectedException.expect(ExecutionException.class);
-    expectedException.expectCause(Matchers.is(EXCEPTION));
-
     Task<Object> task = Tasks.forException(EXCEPTION);
-    Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+    try {
+      Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS);
+      fail("No exception thrown");
+    } catch (ExecutionException e) {
+      assertSame(EXCEPTION, e.getCause());
+    }
   }
 
   @Test
   public void testAwait_noTimeoutAlreadyFailed() throws Exception {
-    expectedException.expect(ExecutionException.class);
-    expectedException.expectCause(Matchers.is(EXCEPTION));
-
     Task<Object> task = Tasks.forException(EXCEPTION);
-    Tasks.await(task);
+
+    try {
+      Tasks.await(task);
+      fail("No exception thrown");
+    } catch (ExecutionException e) {
+      assertSame(EXCEPTION, e.getCause());
+    }
   }
 
   @Test
@@ -229,13 +238,16 @@ public class TasksTest {
 
   @Test
   public void testWhenAll_completedFailure() throws Exception {
-    expectedException.expect(ExecutionException.class);
-    expectedException.expectCause(Matchers.any(ExecutionException.class));
-
     Task<Object> task1 = Tasks.forResult(RESULT);
     Task<Object> task2 = Tasks.forException(EXCEPTION);
     Task<Void> task = Tasks.whenAll(task1, task2);
-    Tasks.await(task);
+
+    try {
+      Tasks.await(task);
+      fail("No exception thrown");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof ExecutionException);
+    }
   }
 
   @Test
diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
index b710038..296b718 100644
--- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
+++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java
@@ -78,6 +78,13 @@ public class IntegrationTestUtils {
     return "https://" + getProjectId() + ".firebaseio.com";
   }
 
+  public static String getApiKey() {
+    String apiKey = System.getProperty("firebase.it.apikey");
+    checkArgument(!Strings.isNullOrEmpty(apiKey), "API key not specified. Set the "
+        + "firebase.it.apikey system property and try again.");
+    return apiKey;
+  }
+
   /**
    * Initializes the default FirebaseApp for integration testing (if not already initialized), and
    * returns it. Integration tests that interact with the default FirebaseApp should call this
diff --git a/src/test/resources/deleteUser.json b/src/test/resources/deleteUser.json
new file mode 100644
index 0000000..53f346e
--- /dev/null
+++ b/src/test/resources/deleteUser.json
@@ -0,0 +1,3 @@
+{
+  "kind" : "identitytoolkit#DeleteUserResponse"
+}