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 000000000..d5e6d0dec --- /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. + * + *

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 getTemplateAsync() { + return getTemplateOp().callAsync(app); + } + + private CallableOperation getTemplateOp() { + final FirebaseRemoteConfigClient remoteConfigClient = getRemoteConfigClient(); + return new CallableOperation() { + @Override + protected RemoteConfigTemplate execute() throws FirebaseRemoteConfigException { + return remoteConfigClient.getTemplate(); + } + }; + } + + @VisibleForTesting + FirebaseRemoteConfigClient getRemoteConfigClient() { + return remoteConfigClient; + } + + private static class FirebaseRemoteConfigService extends FirebaseService { + + 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 9a7ef31da..20b5e3a61 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 @@ public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException { .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 66b066f38..061593105 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 e8028b000..632fb7777 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 String getETag() { return this.etag; } - void setETag(String etag) { + RemoteConfigTemplate setETag(String etag) { this.etag = etag; + return this; } } 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 000000000..9e5bd97f3 --- /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 000000000..47b298250 --- /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; + } +}