Repository: firebase/firebase-admin-java
Pull request #43 (2017-06-08 23:46:00)

Setting X-Client-Version Header in HTTP Requests Made by User Management API
We should set the `X-Client-Version` header when making HTTP calls to identity toolkit (see #42). This PR does the following:

* Moved the SDK version calculation logic into a new internal class named `SdkUtils`.
* Changed `FirebaseDatabase` to pick the client version from `SdkUtils`.
* Changed `FirebaseUserManager` to pick the client version from `SdkUtils`, and set it as a HTTP header in outgoing requests.
* Updated test cases to check for this functionality.


Patch:
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/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/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"
+}