Repository: firebase/firebase-admin-java
Pull request #478 (2020-09-21 20:38:23)

Add the entry point for Remote Config
- Add the entry point for Remote Config services `FirebaseRemoteConfig.java`
- Add unit tests to cover `getTemplate()` and `getTemplateAsync()`
- Integration tests will be added later once all the operations are implemented

Issue: #446 

Note: Merging to `remote-config` branch.


Patch:
diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
new file mode 100644
index 0000000..d5e6d0d
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.core.ApiFuture;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.internal.CallableOperation;
+import com.google.firebase.internal.FirebaseService;
+
+/**
+ * This class is the entry point for all server-side Firebase Remote Config actions.
+ *
+ * <p>You can get an instance of {@link FirebaseRemoteConfig} via {@link #getInstance(FirebaseApp)},
+ * and then use it to manage Remote Config templates.
+ */
+public final class FirebaseRemoteConfig {
+
+  private static final String SERVICE_ID = FirebaseRemoteConfig.class.getName();
+  private final FirebaseApp app;
+  private final FirebaseRemoteConfigClient remoteConfigClient;
+
+  @VisibleForTesting
+  FirebaseRemoteConfig(FirebaseApp app, FirebaseRemoteConfigClient client) {
+    this.app = checkNotNull(app);
+    this.remoteConfigClient = checkNotNull(client);
+  }
+
+  private FirebaseRemoteConfig(FirebaseApp app) {
+    this(app, FirebaseRemoteConfigClientImpl.fromApp(app));
+  }
+
+  /**
+   * Gets the {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}.
+   *
+   * @return The {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}.
+   */
+  public static FirebaseRemoteConfig getInstance() {
+    return getInstance(FirebaseApp.getInstance());
+  }
+
+  /**
+   * Gets the {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}.
+   *
+   * @return The {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}.
+   */
+  public static synchronized FirebaseRemoteConfig getInstance(FirebaseApp app) {
+    FirebaseRemoteConfigService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID,
+            FirebaseRemoteConfigService.class);
+    if (service == null) {
+      service = ImplFirebaseTrampolines.addService(app, new FirebaseRemoteConfigService(app));
+    }
+    return service.getInstance();
+  }
+
+  /**
+   * Gets the current active version of the Remote Config template.
+   *
+   * @return A {@link RemoteConfigTemplate}.
+   * @throws FirebaseRemoteConfigException If an error occurs while getting the template.
+   */
+  public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
+    return getTemplateOp().call();
+  }
+
+  /**
+   * Similar to {@link #getTemplate()} but performs the operation asynchronously.
+   *
+   * @return An {@code ApiFuture} that completes with a {@link RemoteConfigTemplate} when
+   *      the template is available.
+   */
+  public ApiFuture<RemoteConfigTemplate> getTemplateAsync() {
+    return getTemplateOp().callAsync(app);
+  }
+
+  private CallableOperation<RemoteConfigTemplate, FirebaseRemoteConfigException> getTemplateOp() {
+    final FirebaseRemoteConfigClient remoteConfigClient = getRemoteConfigClient();
+    return new CallableOperation<RemoteConfigTemplate, FirebaseRemoteConfigException>() {
+      @Override
+      protected RemoteConfigTemplate execute() throws FirebaseRemoteConfigException {
+        return remoteConfigClient.getTemplate();
+      }
+    };
+  }
+
+  @VisibleForTesting
+  FirebaseRemoteConfigClient getRemoteConfigClient() {
+    return remoteConfigClient;
+  }
+
+  private static class FirebaseRemoteConfigService extends FirebaseService<FirebaseRemoteConfig> {
+
+    FirebaseRemoteConfigService(FirebaseApp app) {
+      super(SERVICE_ID, new FirebaseRemoteConfig(app));
+    }
+
+    @Override
+    public void destroy() {
+      // NOTE: We don't explicitly tear down anything here, but public methods of
+      // FirebaseRemoteConfig will now fail because calls to getOptions() and getToken()
+      // will hit FirebaseApp, which will throw once the app is deleted.
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
index 9a7ef31..20b5e3a 100644
--- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
@@ -95,8 +95,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient
             .addAllHeaders(COMMON_HEADERS);
     IncomingHttpResponse response = httpClient.send(request);
     RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class);
-    parsed.setETag(getETag(response));
-    return parsed;
+    return parsed.setETag(getETag(response));
   }
 
   private String getETag(IncomingHttpResponse response) {
diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java
index 66b066f..0615931 100644
--- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java
@@ -16,6 +16,7 @@
 
 package com.google.firebase.remoteconfig;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.firebase.ErrorCode;
 import com.google.firebase.FirebaseException;
 import com.google.firebase.IncomingHttpResponse;
@@ -30,6 +31,11 @@ public final class FirebaseRemoteConfigException extends FirebaseException {
 
   private final RemoteConfigErrorCode errorCode;
 
+  @VisibleForTesting
+  FirebaseRemoteConfigException(@NonNull ErrorCode code, @NonNull String message) {
+    this(code, message, null, null, null);
+  }
+
   public FirebaseRemoteConfigException(
           @NonNull ErrorCode errorCode,
           @NonNull String message,
diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
index e8028b0..632fb77 100644
--- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
+++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
@@ -27,7 +27,8 @@ public final class RemoteConfigTemplate {
     return this.etag;
   }
 
-  void setETag(String etag) {
+  RemoteConfigTemplate setETag(String etag) {
     this.etag = etag;
+    return this;
   }
 }


Test patch:
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
new file mode 100644
index 0000000..9e5bd97
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.firebase.ErrorCode;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.MockGoogleCredentials;
+import java.util.concurrent.ExecutionException;
+import org.junit.After;
+import org.junit.Test;
+
+public class FirebaseRemoteConfigTest {
+
+  private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder()
+          .setCredentials(new MockGoogleCredentials("test-token"))
+          .setProjectId("test-project")
+          .build();
+  private static final FirebaseRemoteConfigException TEST_EXCEPTION =
+          new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message");
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testGetInstance() {
+    FirebaseApp.initializeApp(TEST_OPTIONS);
+
+    FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance();
+
+    assertSame(remoteConfig, FirebaseRemoteConfig.getInstance());
+  }
+
+  @Test
+  public void testGetInstanceByApp() {
+    FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
+
+    FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
+
+    assertSame(remoteConfig, FirebaseRemoteConfig.getInstance(app));
+  }
+
+  @Test
+  public void testDefaultRemoteConfigClient() {
+    FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
+    FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
+
+    FirebaseRemoteConfigClient client = remoteConfig.getRemoteConfigClient();
+
+    assertTrue(client instanceof FirebaseRemoteConfigClientImpl);
+    assertSame(client, remoteConfig.getRemoteConfigClient());
+    String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
+    assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl());
+  }
+
+  @Test
+  public void testAppDelete() {
+    FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
+    FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
+    assertNotNull(remoteConfig);
+
+    app.delete();
+
+    try {
+      FirebaseRemoteConfig.getInstance(app);
+      fail("No error thrown when getting remote config instance after deleting app");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testRemoteConfigClientWithoutProjectId() {
+    FirebaseOptions options = FirebaseOptions.builder()
+            .setCredentials(new MockGoogleCredentials("test-token"))
+            .build();
+    FirebaseApp.initializeApp(options);
+
+    try {
+      FirebaseRemoteConfig.getInstance();
+      fail("No error thrown for missing project ID");
+    } catch (IllegalArgumentException expected) {
+      String message = "Project ID is required to access Remote Config 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.";
+      assertEquals(message, expected.getMessage());
+    }
+  }
+
+  private static final String TEST_ETAG = "etag-123456789012-1";
+
+  @Test
+  public void testGetTemplate() throws FirebaseRemoteConfigException {
+    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
+            new RemoteConfigTemplate().setETag(TEST_ETAG));
+    FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+    RemoteConfigTemplate template = remoteConfig.getTemplate();
+
+    assertEquals(TEST_ETAG, template.getETag());
+  }
+
+  @Test
+  public void testGetTemplateFailure() {
+    MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION);
+    FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+    try {
+      remoteConfig.getTemplate();
+    } catch (FirebaseRemoteConfigException e) {
+      assertSame(TEST_EXCEPTION, e);
+    }
+  }
+
+  @Test
+  public void testGetTemplateAsync() throws Exception {
+    MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
+            new RemoteConfigTemplate().setETag(TEST_ETAG));
+    FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+    RemoteConfigTemplate template = remoteConfig.getTemplateAsync().get();
+
+    assertEquals(TEST_ETAG, template.getETag());
+  }
+
+  @Test
+  public void testGetTemplateAsyncFailure() throws InterruptedException {
+    MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION);
+    FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+    try {
+      remoteConfig.getTemplateAsync().get();
+    } catch (ExecutionException e) {
+      assertSame(TEST_EXCEPTION, e.getCause());
+    }
+  }
+
+  private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) {
+    FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
+    return new FirebaseRemoteConfig(app, client);
+  }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
new file mode 100644
index 0000000..47b2982
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.remoteconfig;
+
+public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{
+
+  private RemoteConfigTemplate resultTemplate;
+  private FirebaseRemoteConfigException exception;
+
+  private MockRemoteConfigClient(RemoteConfigTemplate resultTemplate,
+                                 FirebaseRemoteConfigException exception) {
+    this.resultTemplate = resultTemplate;
+    this.exception = exception;
+  }
+
+  static MockRemoteConfigClient fromTemplate(RemoteConfigTemplate resultTemplate) {
+    return new MockRemoteConfigClient(resultTemplate, null);
+  }
+
+  static MockRemoteConfigClient fromException(FirebaseRemoteConfigException exception) {
+    return new MockRemoteConfigClient(null, exception);
+  }
+
+  @Override
+  public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
+    if (exception != null) {
+      throw exception;
+    }
+    return resultTemplate;
+  }
+}