Repository: firebase/firebase-admin-java
Pull request #224 (2018-11-28 00:39:13)

Add android channel id support
Hey there! So you want to contribute to a Firebase SDK? 
Before you file this pull request, please read these guidelines:

### Discussion

  * Read the contribution guidelines (CONTRIBUTING.md).
  * If this has been discussed in an issue, make sure to link to the issue here. 
    If not, go file an issue about this **before creating a pull request** to discuss.

### Testing

  * Make sure all existing tests in the repository pass after your change.
  * If you fixed a bug or added a feature, add a new test to cover your code.

### API Changes

  * At this time we cannot accept changes that affect the public API.  If you'd like to help 
    us make Firebase APIs better, please propose your change in an issue so that we 
    can discuss it together.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96fad10..3f94cc4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Unreleased
 
+-
+
+# v6.6.0
+
+- [added] Added a new `FirebaseProjectManagement` API for managing
+  apps in a Firebase project.
 - [fixed] Fixing error handling in FCM. The SDK now checks the key
   `type.googleapis.com/google.firebase.fcm.v1.FcmError` to set error
   code.
@@ -7,6 +13,9 @@
   that are helpful when debugging problems.
 - [changed] Migrated the `FirebaseAuth` user management API to the
   new Identity Toolkit endpoint.
+- [added] Added new `setChannelId()` to the 
+  AndroidNotification.Builder API for setting the Android 
+  notification channel ID (new in Android O).
 
 # v6.5.0
 
diff --git a/pom.xml b/pom.xml
index 9269f3b..fbd144d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
     <groupId>com.google.firebase</groupId>
     <artifactId>firebase-admin</artifactId>
-    <version>6.5.1-SNAPSHOT</version>
+    <version>6.6.1-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>firebase-admin</name>
diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java
index 27f3155..0a6cd06 100644
--- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java
+++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java
@@ -63,6 +63,9 @@ public class AndroidNotification {
 
   @Key("title_loc_args")
   private final List<String> titleLocArgs;
+  
+  @Key("channel_id")
+  private final String channelId;
 
   private AndroidNotification(Builder builder) {
     this.title = builder.title;
@@ -93,6 +96,7 @@ public class AndroidNotification {
     } else {
       this.titleLocArgs = null;
     }
+    this.channelId = builder.channelId;
   }
 
   /**
@@ -117,6 +121,7 @@ public class AndroidNotification {
     private List<String> bodyLocArgs = new ArrayList<>();
     private String titleLocKey;
     private List<String> titleLocArgs = new ArrayList<>();
+    private String channelId;
 
     private Builder() {}
 
@@ -273,6 +278,20 @@ public class AndroidNotification {
       return this;
     }
 
+    /**
+     * Sets the Android notification channel ID (new in Android O). The app must create a channel
+     * with this channel ID before any notification with this channel ID is received. If you 
+     * don't send this channel ID in the request, or if the channel ID provided has not yet been
+     * created by the app, FCM uses the channel ID specified in the app manifest.
+     *
+     * @param channelId The notification's channel ID.
+     * @return This builder.
+     */
+    public Builder setChannelId(String channelId) {
+      this.channelId = channelId;
+      return this;
+    }
+
     /**
      * Creates a new {@link AndroidNotification} instance from the parameters set on this builder.
      *
diff --git a/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java b/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java
index d1af6db..dd7a00e 100644
--- a/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java
+++ b/src/main/java/com/google/firebase/projectmanagement/AndroidApp.java
@@ -120,7 +120,7 @@ public class AndroidApp {
   }
 
   /**
-   * Adds a SHA certificate to this Android app.
+   * Adds the given SHA certificate to this Android app.
    *
    * @param certificateToAdd the SHA certificate to be added to this Android app
    * @return a {@link ShaCertificate} that was created for this Android app, containing resource
@@ -133,7 +133,7 @@ public class AndroidApp {
   }
 
   /**
-   * Asynchronously adds a SHA certificate to this Android app.
+   * Asynchronously adds the given SHA certificate to this Android app.
    *
    * @param certificateToAdd the SHA certificate to be added to this Android app
    * @return a {@code ApiFuture} of a {@link ShaCertificate} that was created for this Android app,
@@ -144,7 +144,7 @@ public class AndroidApp {
   }
 
   /**
-   * Removes a SHA certificate from this Android app.
+   * Removes the given SHA certificate from this Android app.
    *
    * @param certificateToRemove the SHA certificate to be removed from this Android app
    * @throws FirebaseProjectManagementException if there was an error during the RPC
@@ -155,7 +155,7 @@ public class AndroidApp {
   }
 
   /**
-   * Asynchronously removes a SHA certificate from this Android app.
+   * Asynchronously removes the given SHA certificate from this Android app.
    *
    * @param certificateToRemove the SHA certificate to be removed from this Android app
    */
diff --git a/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java b/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java
index 805ac3d..d587f81 100644
--- a/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java
+++ b/src/main/java/com/google/firebase/projectmanagement/ShaCertificate.java
@@ -23,7 +23,7 @@ import com.google.common.base.Preconditions;
 import java.util.regex.Pattern;
 
 /**
- * Contains detailed information of a SHA certificate, which can be associated to an Android app.
+ * Information about an SHA certificate associated with an Android app.
  */
 public class ShaCertificate {
 
@@ -41,11 +41,13 @@ public class ShaCertificate {
   }
 
   /**
-   * Creates a {@link ShaCertificate} from certificate hash. Name will be left as empty string since
-   * the certificate doesn't have a generated name yet.
+   * Creates an {@link ShaCertificate} from the given certificate hash.
+   *
+   * <p>The fully qualified resource name of this certificate will be set to the empty string since
+   * it has not been generated yet.
    *
    * @param shaHash SHA hash of the certificate
-   * @return a SHA certificate
+   * @return a new {@link ShaCertificate} instance
    */
   public static ShaCertificate create(String shaHash) {
     return new ShaCertificate("", shaHash, getTypeFromHash(shaHash));
@@ -69,7 +71,7 @@ public class ShaCertificate {
     } else if (SHA256_PATTERN.matcher(shaHash).matches()) {
       return ShaCertificateType.SHA_256;
     }
-    throw new IllegalArgumentException("Invalid SHA hash, it is neither SHA-1 nor SHA-256.");
+    throw new IllegalArgumentException("Invalid SHA hash; it is neither SHA-1 nor SHA-256.");
   }
 
   /**
@@ -87,7 +89,7 @@ public class ShaCertificate {
   }
 
   /**
-   * Returns the type {@link ShaCertificateType} of this SHA certificate.
+   * Returns the type of this SHA certificate.
    */
   public ShaCertificateType getCertType() {
     return certType;


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java
index 6cc0b71..c7036b4 100644
--- a/src/test/java/com/google/firebase/FirebaseAppTest.java
+++ b/src/test/java/com/google/firebase/FirebaseAppTest.java
@@ -19,6 +19,7 @@ package com.google.firebase;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -448,6 +449,19 @@ public class FirebaseAppTest {
     assertEquals(0, refresher.cancelCalls);
   }
 
+  @Test
+  public void testAppWithAuthVariableOverrides() {
+    Map<String, Object> authVariableOverrides = ImmutableMap.<String, Object>of("uid", "uid1");
+    FirebaseOptions options =
+        new FirebaseOptions.Builder(getMockCredentialOptions())
+            .setDatabaseAuthVariableOverride(authVariableOverrides)
+            .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options, "testGetAppWithUid");
+    assertEquals("uid1", app.getOptions().getDatabaseAuthVariableOverride().get("uid"));
+    String token = TestOnlyImplFirebaseTrampolines.getToken(app, false);
+    Assert.assertTrue(!token.isEmpty());
+  }
+
   @Test(expected = IllegalArgumentException.class)
   public void testEmptyFirebaseConfigFile() {
     setFirebaseConfigEnvironmentVariable("firebase_config_empty.json");
@@ -458,9 +472,9 @@ public class FirebaseAppTest {
   public void testEmptyFirebaseConfigString() {
     setFirebaseConfigEnvironmentVariable("");
     FirebaseApp firebaseApp = FirebaseApp.initializeApp();
-    assertEquals(null, firebaseApp.getOptions().getProjectId());
-    assertEquals(null, firebaseApp.getOptions().getStorageBucket());
-    assertEquals(null, firebaseApp.getOptions().getDatabaseUrl());
+    assertNull(firebaseApp.getOptions().getProjectId());
+    assertNull(firebaseApp.getOptions().getStorageBucket());
+    assertNull(firebaseApp.getOptions().getDatabaseUrl());
     assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); 
   }
 
@@ -468,9 +482,9 @@ public class FirebaseAppTest {
   public void testEmptyFirebaseConfigJSONObject() {
     setFirebaseConfigEnvironmentVariable("{}");
     FirebaseApp firebaseApp = FirebaseApp.initializeApp();
-    assertEquals(null, firebaseApp.getOptions().getProjectId());
-    assertEquals(null, firebaseApp.getOptions().getStorageBucket());
-    assertEquals(null, firebaseApp.getOptions().getDatabaseUrl());
+    assertNull(firebaseApp.getOptions().getProjectId());
+    assertNull(firebaseApp.getOptions().getStorageBucket());
+    assertNull(firebaseApp.getOptions().getDatabaseUrl());
     assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty());
   }
 
@@ -514,9 +528,9 @@ public class FirebaseAppTest {
   public void testEnvironmentVariableIgnored() {
     setFirebaseConfigEnvironmentVariable("firebase_config.json");
     FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS);
-    assertEquals(null, firebaseApp.getOptions().getProjectId());
-    assertEquals(null, firebaseApp.getOptions().getStorageBucket());
-    assertEquals(null, firebaseApp.getOptions().getDatabaseUrl());
+    assertNull(firebaseApp.getOptions().getProjectId());
+    assertNull(firebaseApp.getOptions().getStorageBucket());
+    assertNull(firebaseApp.getOptions().getDatabaseUrl());
     assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty());
   }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index a4c2dc5..8f19e5c 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -19,137 +19,39 @@ package com.google.firebase.auth;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
-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 com.google.api.client.googleapis.testing.auth.oauth2.MockTokenServerTransport;
-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.api.client.json.gson.GsonFactory;
 import com.google.api.core.ApiFuture;
-import com.google.auth.http.HttpTransportFactory;
-import com.google.auth.oauth2.GoogleCredentials;
-import com.google.auth.oauth2.ServiceAccountCredentials;
-import com.google.auth.oauth2.UserCredentials;
 import com.google.common.base.Defaults;
-import com.google.common.base.Strings;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
-import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
-import com.google.firebase.auth.internal.FirebaseCustomAuthToken;
-import com.google.firebase.database.MapBuilder;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestUtils;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 import org.junit.After;
 import org.junit.Assert;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
-@RunWith(Parameterized.class)
 public class FirebaseAuthTest {
 
-  private static final String ACCESS_TOKEN = "mockaccesstoken";
-  private static final String CLIENT_SECRET = "mockclientsecret";
-  private static final String CLIENT_ID = "mockclientid";
-  private static final String REFRESH_TOKEN = "mockrefreshtoken";
-  private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory();
-
-  private final FirebaseOptions firebaseOptions;
-  private final boolean isCertCredential;
-
-  public FirebaseAuthTest(FirebaseOptions baseOptions, boolean isCertCredential) {
-    this.firebaseOptions = baseOptions;
-    this.isCertCredential = isCertCredential;
-  }
-
-  @Parameters
-  public static Collection<Object[]> data() throws Exception {
-    // Initialize this test suite with all available credential implementations.
-    return Arrays.asList(
-        new Object[][] {
-          {
-            new FirebaseOptions.Builder().setCredentials(createCertificateCredential()).build(),
-            /* isCertCredential */ true
-          },
-          {
-            new FirebaseOptions.Builder()
-                .setCredentials(createRefreshTokenCredential())
-                .setProjectId("test-project-id")
-                .build(),
-            /* isCertCredential */ false
-          },
-          {
-            new FirebaseOptions.Builder()
-                .setCredentials(TestUtils.getApplicationDefaultCredentials())
-                .build(),
-            /* isCertCredential */ false
-          },
-        });
-  }
-
-  private static GoogleCredentials createRefreshTokenCredential() throws IOException {
-
-    final MockTokenServerTransport transport = new MockTokenServerTransport();
-    transport.addClient(CLIENT_ID, CLIENT_SECRET);
-    transport.addRefreshToken(REFRESH_TOKEN, ACCESS_TOKEN);
-
-    Map<String, Object> secretJson = new HashMap<>();
-    secretJson.put("client_id", CLIENT_ID);
-    secretJson.put("client_secret", CLIENT_SECRET);
-    secretJson.put("refresh_token", REFRESH_TOKEN);
-    secretJson.put("type", "authorized_user");
-    InputStream refreshTokenStream =
-        new ByteArrayInputStream(JSON_FACTORY.toByteArray(secretJson));
-
-    return UserCredentials.fromStream(refreshTokenStream, new HttpTransportFactory() {
-      @Override
-      public HttpTransport create() {
-        return transport;
-      }
-    });
-  }
-
-  private static GoogleCredentials createCertificateCredential() throws IOException {
-    final MockTokenServerTransport transport = new MockTokenServerTransport(
-        "https://accounts.google.com/o/oauth2/token");
-    transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN);
-    return ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream(),
-        new HttpTransportFactory() {
-          @Override
-          public HttpTransport create() {
-            return transport;
-          }
-        });
-  }
+  private static final FirebaseOptions firebaseOptions = FirebaseOptions.builder()
+      .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream()))
+      .build();
 
   @Before
   public void setup() {
-    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
     FirebaseApp.initializeApp(firebaseOptions);
   }
 
@@ -163,8 +65,6 @@ public class FirebaseAuthTest {
     FirebaseAuth defaultAuth = FirebaseAuth.getInstance();
     assertNotNull(defaultAuth);
     assertSame(defaultAuth, FirebaseAuth.getInstance());
-    String token = TestOnlyImplFirebaseTrampolines.getToken(FirebaseApp.getInstance(), false);
-    Assert.assertTrue(!token.isEmpty());
   }
 
   @Test
@@ -173,8 +73,6 @@ public class FirebaseAuthTest {
     FirebaseAuth auth = FirebaseAuth.getInstance(app);
     assertNotNull(auth);
     assertSame(auth, FirebaseAuth.getInstance(app));
-    String token = TestOnlyImplFirebaseTrampolines.getToken(app, false);
-    Assert.assertTrue(!token.isEmpty());
   }
 
   @Test
@@ -234,126 +132,26 @@ public class FirebaseAuthTest {
     assertNotNull(auth2);
     assertNotSame(auth1, auth2);
 
-    if (isCertCredential) {
-      ApiFuture<String> future = auth2.createCustomTokenAsync("foo");
-      assertNotNull(future);
-      assertNotNull(future.get(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
-    }
-  }
-
-  @Test
-  public void testAppWithAuthVariableOverrides() {
-    Map<String, Object> authVariableOverrides = Collections.singletonMap("uid", (Object) "uid1");
-    FirebaseOptions options =
-        new FirebaseOptions.Builder(firebaseOptions)
-            .setDatabaseAuthVariableOverride(authVariableOverrides)
-            .build();
-    FirebaseApp app = FirebaseApp.initializeApp(options, "testGetAppWithUid");
-    assertEquals("uid1", app.getOptions().getDatabaseAuthVariableOverride().get("uid"));
-    String token = TestOnlyImplFirebaseTrampolines.getToken(app, false);
-    Assert.assertTrue(!token.isEmpty());
-  }
-
-  @Test
-  public void testCreateCustomToken() throws Exception {
-    GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions);
-    Assume.assumeTrue("Skipping testCredentialCertificateRequired for cert credential",
-        credentials instanceof ServiceAccountCredentials);
-
-    FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testCreateCustomToken");
-    FirebaseAuth auth = FirebaseAuth.getInstance(app);
-
-    String token = auth.createCustomTokenAsync("user1").get();
-
-    FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token);
-    assertEquals(parsedToken.getPayload().getUid(), "user1");
-    assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail());
-    assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail());
-    assertNull(parsedToken.getPayload().getDeveloperClaims());
-    assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken));
-  }
-
-  @Test
-  public void testCreateCustomTokenWithDeveloperClaims() throws Exception {
-    GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions);
-    Assume.assumeTrue("Skipping testCredentialCertificateRequired for cert credential",
-        credentials instanceof ServiceAccountCredentials);
-
-    FirebaseApp app =
-        FirebaseApp.initializeApp(firebaseOptions, "testCreateCustomTokenWithDeveloperClaims");
-    FirebaseAuth auth = FirebaseAuth.getInstance(app);
-
-    String token =
-        auth.createCustomTokenAsync("user1", MapBuilder.of("claim", "value")).get();
-
-    FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token);
-    assertEquals(parsedToken.getPayload().getUid(), "user1");
-    assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail());
-    assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail());
-    assertEquals(parsedToken.getPayload().getDeveloperClaims().keySet().size(), 1);
-    assertEquals(parsedToken.getPayload().getDeveloperClaims().get("claim"), "value");
-    assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken));
-  }
-
-  @Test
-  public void testServiceAccountRequired() throws Exception {
-    GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions);
-    Assume.assumeFalse("Skipping testServiceAccountRequired for service account credentials",
-        credentials instanceof ServiceAccountCredentials);
-
-    FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testServiceAccountRequired");
-    try {
-      FirebaseAuth.getInstance(app).createCustomTokenAsync("foo").get();
-      fail("Expected exception.");
-    } catch (IllegalStateException expected) {
-      Assert.assertEquals(
-          "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK with "
-              + "service account credentials or specify a service account ID with "
-              + "iam.serviceAccounts.signBlob permission. Please refer to "
-              + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details "
-              + "on creating custom tokens.",
-          expected.getMessage());
-    }
+    ApiFuture<String> future = auth2.createCustomTokenAsync("foo");
+    assertNotNull(future);
+    assertNotNull(future.get(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
   }
 
   @Test
-  public void testProjectIdRequired() throws Exception {
-    FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testProjectIdRequired");
-    String projectId = ImplFirebaseTrampolines.getProjectId(app);
-    Assume.assumeTrue("Skipping testProjectIdRequired for settings with project ID",
-        Strings.isNullOrEmpty(projectId));
-
+  public void testProjectIdRequired() {
+    FirebaseOptions options = FirebaseOptions.builder()
+        .setCredentials(new MockGoogleCredentials())
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options, "testProjectIdRequired");
     try {
-      FirebaseAuth.getInstance(app).verifyIdTokenAsync("foo").get();
+      FirebaseAuth.getInstance(app);
       fail("Expected exception.");
     } catch (IllegalArgumentException expected) {
       Assert.assertEquals(
-          "Must initialize FirebaseApp with a project ID to call verifyIdToken()",
-          expected.getMessage());
-    }
-  }
-
-  @Test
-  public void testVerifyIdTokenWithExplicitProjectId() throws Exception {
-    GoogleCredentials credentials = TestOnlyImplFirebaseTrampolines.getCredentials(firebaseOptions);
-    Assume.assumeFalse(
-        "Skipping testVerifyIdTokenWithExplicitProjectId for service account credentials",
-        credentials instanceof ServiceAccountCredentials);
-
-    FirebaseOptions options =
-        new FirebaseOptions.Builder(firebaseOptions)
-            .setProjectId("mock-project-id")
-            .build();
-    FirebaseApp app = FirebaseApp.initializeApp(options, "testVerifyIdTokenWithExplicitProjectId");
-    try {
-      FirebaseAuth.getInstance(app).verifyIdTokenAsync("foo").get();
-      fail("Expected exception.");
-    } catch (ExecutionException expected) {
-      Assert.assertNotEquals(
-          "com.google.firebase.FirebaseException: Must initialize FirebaseApp with a project ID "
-              + "to call verifyIdToken()",
+          "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());
-      assertTrue(expected.getCause() instanceof IllegalArgumentException);
     }
   }
 
diff --git a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java
new file mode 100644
index 0000000..17a99c3
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2018 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.auth;
+
+import static org.junit.Assert.assertEquals;
+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.json.gson.GsonFactory;
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.auth.oauth2.ServiceAccountCredentials;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.internal.FirebaseCustomAuthToken;
+import com.google.firebase.database.MapBuilder;
+import com.google.firebase.testing.MultiRequestMockHttpTransport;
+import com.google.firebase.testing.ServiceAccount;
+import java.util.List;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FirebaseCustomTokenTest {
+
+  @After
+  public void cleanup() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testCreateCustomToken() throws Exception {
+    FirebaseOptions options = FirebaseOptions.builder()
+        .setCredentials(ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    FirebaseAuth auth = FirebaseAuth.getInstance(app);
+
+    String token = auth.createCustomTokenAsync("user1").get();
+    FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token);
+    assertEquals(parsedToken.getPayload().getUid(), "user1");
+    assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail());
+    assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail());
+    assertNull(parsedToken.getPayload().getDeveloperClaims());
+    assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken));
+  }
+
+  @Test
+  public void testCreateCustomTokenWithDeveloperClaims() throws Exception {
+    FirebaseOptions options = FirebaseOptions.builder()
+        .setCredentials(ServiceAccountCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    FirebaseAuth auth = FirebaseAuth.getInstance(app);
+
+    String token = auth.createCustomTokenAsync(
+        "user1", MapBuilder.of("claim", "value")).get();
+    FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token);
+    assertEquals(parsedToken.getPayload().getUid(), "user1");
+    assertEquals(parsedToken.getPayload().getSubject(), ServiceAccount.EDITOR.getEmail());
+    assertEquals(parsedToken.getPayload().getIssuer(), ServiceAccount.EDITOR.getEmail());
+    assertEquals(parsedToken.getPayload().getDeveloperClaims().keySet().size(), 1);
+    assertEquals(parsedToken.getPayload().getDeveloperClaims().get("claim"), "value");
+    assertTrue(ServiceAccount.EDITOR.verifySignature(parsedToken));
+  }
+
+  @Test
+  public void testCreateCustomTokenWithoutServiceAccountCredentials() throws Exception {
+    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
+    String content = Utils.getDefaultJsonFactory().toString(
+        ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes())));
+    response.setContent(content);
+    MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of(response));
+
+    FirebaseOptions options = FirebaseOptions.builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId("test-project-id")
+        .setServiceAccountId("test@service.account")
+        .setHttpTransport(transport)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    FirebaseAuth auth = FirebaseAuth.getInstance(app);
+
+    String token = auth.createCustomTokenAsync("user1").get();
+    FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token);
+    assertEquals(parsedToken.getPayload().getUid(), "user1");
+    assertEquals(parsedToken.getPayload().getSubject(), "test@service.account");
+    assertEquals(parsedToken.getPayload().getIssuer(), "test@service.account");
+    assertNull(parsedToken.getPayload().getDeveloperClaims());
+    assertEquals("test-signature", new String(parsedToken.getSignatureBytes()));
+  }
+
+  @Test
+  public void testCreateCustomTokenWithDiscoveredServiceAccount() throws Exception {
+    String content = Utils.getDefaultJsonFactory().toString(
+        ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes())));
+    List<MockLowLevelHttpResponse> responses = ImmutableList.of(
+        // Service account discovery response
+        new MockLowLevelHttpResponse().setContent("test@service.account"),
+
+        // Sign blob response
+        new MockLowLevelHttpResponse().setContent(content)
+    );
+    MockHttpTransport transport = new MultiRequestMockHttpTransport(responses);
+
+    FirebaseOptions options = FirebaseOptions.builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId("test-project-id")
+        .setHttpTransport(transport)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    FirebaseAuth auth = FirebaseAuth.getInstance(app);
+
+    String token = auth.createCustomTokenAsync("user1").get();
+    FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token);
+    assertEquals(parsedToken.getPayload().getUid(), "user1");
+    assertEquals(parsedToken.getPayload().getSubject(), "test@service.account");
+    assertEquals(parsedToken.getPayload().getIssuer(), "test@service.account");
+    assertNull(parsedToken.getPayload().getDeveloperClaims());
+    assertEquals("test-signature", new String(parsedToken.getSignatureBytes()));
+  }
+
+  @Test
+  public void testNoServiceAccount() throws Exception {
+    FirebaseOptions options = FirebaseOptions.builder()
+        .setCredentials(new MockGoogleCredentials("test-token"))
+        .setProjectId("test-project-id")
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options);
+    try {
+      FirebaseAuth.getInstance(app).createCustomTokenAsync("foo").get();
+      fail("Expected exception.");
+    } catch (IllegalStateException expected) {
+      Assert.assertEquals(
+          "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK with "
+              + "service account credentials or specify a service account ID with "
+              + "iam.serviceAccounts.signBlob permission. Please refer to "
+              + "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details "
+              + "on creating custom tokens.",
+          expected.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
index 1ad74ac..d18e2be 100644
--- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
+++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java
@@ -606,6 +606,7 @@ public class FirebaseMessagingTest {
                     .addAllTitleLocalizationArgs(ImmutableList.of("t-arg2", "t-arg3"))
                     .addBodyLocalizationArg("b-arg1")
                     .addAllBodyLocalizationArgs(ImmutableList.of("b-arg2", "b-arg3"))
+                    .setChannelId("channel-id")
                     .build())
                 .build())
             .setTopic("test-topic")
@@ -629,6 +630,7 @@ public class FirebaseMessagingTest {
                     .put("title_loc_args", ImmutableList.of("t-arg1", "t-arg2", "t-arg3"))
                     .put("body_loc_key", "test-body-key")
                     .put("body_loc_args", ImmutableList.of("b-arg1", "b-arg2", "b-arg3"))
+                    .put("channel_id", "channel-id")
                     .build()
             )
         ));
diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java
index b6025c0..a44cd2a 100644
--- a/src/test/java/com/google/firebase/messaging/MessageTest.java
+++ b/src/test/java/com/google/firebase/messaging/MessageTest.java
@@ -147,6 +147,7 @@ public class MessageTest {
                 .setBodyLocalizationKey("body-loc")
                 .addBodyLocalizationArg("body-arg1")
                 .addAllBodyLocalizationArgs(ImmutableList.of("body-arg2", "body-arg3"))
+                .setChannelId("channel-id")
                 .build())
             .build())
         .setTopic("test-topic")
@@ -163,6 +164,7 @@ public class MessageTest {
         .put("title_loc_args", ImmutableList.of("title-arg1", "title-arg2", "title-arg3"))
         .put("body_loc_key", "body-loc")
         .put("body_loc_args", ImmutableList.of("body-arg1", "body-arg2", "body-arg3"))
+        .put("channel_id", "channel-id")
         .build();
     Map<String, Object> data = ImmutableMap.of(
         "collapse_key", "test-key",