diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java new file mode 100644 index 000000000..9c844503d --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * An interface for managing Firebase Remote Config templates. + */ +interface FirebaseRemoteConfigClient { + + /** + * Gets the current active version of the Remote Config template. + * + * @return A {@link RemoteConfigTemplate}. + * @throws FirebaseRemoteConfigException If an error occurs while getting the template. + */ + RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException; +} diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java new file mode 100644 index 000000000..9a7ef31da --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java @@ -0,0 +1,204 @@ +/* + * 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.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.JsonFactory; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.internal.AbstractPlatformErrorHandler; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.remoteconfig.internal.RemoteConfigServiceErrorResponse; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with Firebase Remote Config service. + */ +final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient { + + private static final String REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/remoteConfig"; + + private static final Map COMMON_HEADERS = + ImmutableMap.of( + "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(), + // There is a known issue in which the ETag is not properly returned in cases + // where the request does not specify a compression type. Currently, it is + // required to include the header `Accept-Encoding: gzip` or equivalent in all + // requests. https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + "Accept-Encoding", "gzip" + ); + + private final String remoteConfigUrl; + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final ErrorHandlingHttpClient httpClient; + + private FirebaseRemoteConfigClientImpl(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.projectId)); + this.remoteConfigUrl = String.format(REMOTE_CONFIG_URL, builder.projectId); + this.requestFactory = checkNotNull(builder.requestFactory); + this.jsonFactory = checkNotNull(builder.jsonFactory); + HttpResponseInterceptor responseInterceptor = builder.responseInterceptor; + RemoteConfigErrorHandler errorHandler = new RemoteConfigErrorHandler(this.jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler) + .setInterceptor(responseInterceptor); + } + + @VisibleForTesting + String getRemoteConfigUrl() { + return remoteConfigUrl; + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; + } + + @VisibleForTesting + JsonFactory getJsonFactory() { + return jsonFactory; + } + + @Override + public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException { + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(remoteConfigUrl) + .addAllHeaders(COMMON_HEADERS); + IncomingHttpResponse response = httpClient.send(request); + RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class); + parsed.setETag(getETag(response)); + return parsed; + } + + private String getETag(IncomingHttpResponse response) { + List etagList = (List) response.getHeaders().get("etag"); + checkState(etagList != null && !etagList.isEmpty(), + "ETag header is not available in the server response."); + + String etag = etagList.get(0); + checkState(!Strings.isNullOrEmpty(etag), + "ETag header is not available in the server response."); + + return etag; + } + + static FirebaseRemoteConfigClientImpl fromApp(FirebaseApp app) { + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "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."); + return FirebaseRemoteConfigClientImpl.builder() + .setProjectId(projectId) + .setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) + .setJsonFactory(app.getOptions().getJsonFactory()) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static final class Builder { + + private String projectId; + private HttpRequestFactory requestFactory; + private JsonFactory jsonFactory; + private HttpResponseInterceptor responseInterceptor; + + private Builder() { } + + Builder setProjectId(String projectId) { + this.projectId = projectId; + return this; + } + + Builder setRequestFactory(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + return this; + } + + Builder setJsonFactory(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + return this; + } + + Builder setResponseInterceptor( + HttpResponseInterceptor responseInterceptor) { + this.responseInterceptor = responseInterceptor; + return this; + } + + FirebaseRemoteConfigClientImpl build() { + return new FirebaseRemoteConfigClientImpl(this); + } + } + + private static class RemoteConfigErrorHandler + extends AbstractPlatformErrorHandler { + + private RemoteConfigErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); + } + + @Override + protected FirebaseRemoteConfigException createException(FirebaseException base) { + String response = getResponse(base); + RemoteConfigServiceErrorResponse parsed = safeParse(response); + return FirebaseRemoteConfigException.withRemoteConfigErrorCode( + base, parsed.getRemoteConfigErrorCode()); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private RemoteConfigServiceErrorResponse safeParse(String response) { + if (!Strings.isNullOrEmpty(response)) { + try { + return jsonFactory.createJsonParser(response) + .parseAndClose(RemoteConfigServiceErrorResponse.class); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return new RemoteConfigServiceErrorResponse(); + } + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java new file mode 100644 index 000000000..66b066f38 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java @@ -0,0 +1,57 @@ +/* + * 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 com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; + +/** + * Generic exception related to Firebase Remote Config. Check the error code and message for more + * details. + */ +public final class FirebaseRemoteConfigException extends FirebaseException { + + private final RemoteConfigErrorCode errorCode; + + public FirebaseRemoteConfigException( + @NonNull ErrorCode errorCode, + @NonNull String message, + @Nullable Throwable cause, + @Nullable IncomingHttpResponse response, + @Nullable RemoteConfigErrorCode remoteConfigErrorCode) { + super(errorCode, message, cause, response); + this.errorCode = remoteConfigErrorCode; + } + + static FirebaseRemoteConfigException withRemoteConfigErrorCode( + FirebaseException base, @Nullable RemoteConfigErrorCode errorCode) { + return new FirebaseRemoteConfigException( + base.getErrorCode(), + base.getMessage(), + base.getCause(), + base.getHttpResponse(), + errorCode); + } + + @Nullable + public RemoteConfigErrorCode getRemoteConfigErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java new file mode 100644 index 000000000..6aa94027c --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Error codes that can be raised by the Remote Config APIs. + */ +public enum RemoteConfigErrorCode { + + /** + * One or more arguments specified in the request were invalid. + */ + INVALID_ARGUMENT, + + /** + * Internal server error. + */ + INTERNAL, +} diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java new file mode 100644 index 000000000..e8028b000 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java @@ -0,0 +1,33 @@ +/* + * 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 com.google.api.client.util.Key; + +public final class RemoteConfigTemplate { + + @Key("etag") + private String etag; + + public String getETag() { + return this.etag; + } + + void setETag(String etag) { + this.etag = etag; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java new file mode 100644 index 000000000..0c517e14e --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java @@ -0,0 +1,67 @@ +/* + * 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.internal; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.RemoteConfigErrorCode; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The DTO for parsing error responses from the Remote Config service. + * These error messages take the form, + * `"error": {"code": 123, "message": "[CODE]: Message Details", "status": "ERROR_STATUS"}` + */ +public final class RemoteConfigServiceErrorResponse extends GenericJson { + + private static final Map RC_ERROR_CODES = + ImmutableMap.builder() + .put("INTERNAL", RemoteConfigErrorCode.INTERNAL) + .put("INVALID_ARGUMENT", RemoteConfigErrorCode.INVALID_ARGUMENT) + .build(); + + private static final Pattern RC_ERROR_CODE_PATTERN = Pattern.compile("^\\[(\\w+)\\]:.*$"); + + @Key("error") + private Map error; + + @Nullable + public RemoteConfigErrorCode getRemoteConfigErrorCode() { + if (error == null) { + return null; + } + + String message = (String) error.get("message"); + if (Strings.isNullOrEmpty(message)) { + return null; + } + + Matcher errorMatcher = RC_ERROR_CODE_PATTERN.matcher(message); + if (errorMatcher.find()) { + String errorCode = errorMatcher.group(1); + return RC_ERROR_CODES.get(errorCode); + } + + return null; + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java new file mode 100644 index 000000000..8b6e94ce1 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -0,0 +1,312 @@ +/* + * 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.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.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.HttpTransport; +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.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.OutgoingHttpRequest; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +public class FirebaseRemoteConfigClientImplTest { + + private static final String TEST_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + + private static final List HTTP_STATUS_CODES = ImmutableList.of(401, 404, 500); + + private static final Map HTTP_STATUS_TO_ERROR_CODE = ImmutableMap.of( + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); + + private static final String MOCK_TEMPLATE_RESPONSE = "{\"conditions\": [], \"parameters\": {}}"; + + private static final String TEST_ETAG = "etag-123456789012-1"; + + private MockLowLevelHttpResponse response; + private TestResponseInterceptor interceptor; + private FirebaseRemoteConfigClient client; + + @Before + public void setUp() { + response = new MockLowLevelHttpResponse(); + interceptor = new TestResponseInterceptor(); + client = initRemoteConfigClient(response, interceptor); + } + + @Test + public void testGetTemplate() throws Exception { + response.addHeader("etag", TEST_ETAG); + response.setContent(MOCK_TEMPLATE_RESPONSE); + + RemoteConfigTemplate template = client.getTemplate(); + + assertEquals(TEST_ETAG, template.getETag()); + checkGetRequestHeader(interceptor.getLastRequest()); + } + + @Test(expected = IllegalStateException.class) + public void testGetTemplateWithInvalidEtags() throws FirebaseRemoteConfigException { + // ETag does not exist + response.setContent(MOCK_TEMPLATE_RESPONSE); + + client.getTemplate(); + + // Empty ETag + response.addHeader("etag", ""); + response.setContent(MOCK_TEMPLATE_RESPONSE); + + client.getTemplate(); + } + + @Test + public void testGetTemplateHttpError() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("{}"); + + try { + client.getTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, + "Unexpected HTTP response with status: " + code + "\n{}"); + } + checkGetRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testGetTemplateTransportError() { + client = initClientWithFaultyTransport(); + + try { + client.getTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals("Unknown error while making a remote service call: transport error", + error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getRemoteConfigErrorCode()); + } + } + + @Test + public void testGetTemplateSuccessResponseWithUnexpectedPayload() { + response.setContent("not valid json"); + + try { + client.getTemplate(); + fail("No error thrown for malformed response"); + } catch (FirebaseRemoteConfigException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); + assertNotNull(error.getCause()); + assertNotNull(error.getHttpResponse()); + assertNull(error.getRemoteConfigErrorCode()); + } + checkGetRequestHeader(interceptor.getLastRequest()); + } + + @Test + public void testGetTemplateErrorWithZeroContentResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setZeroContent(); + + try { + client.getTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnull"); + } + checkGetRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testGetTemplateErrorWithMalformedResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("not json"); + + try { + client.getTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnot json"); + } + checkGetRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testGetTemplateErrorWithDetails() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + client.getTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error"); + } + checkGetRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testGetTemplateErrorWithRcError() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + try { + client.getTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error"); + } + checkGetRequestHeader(interceptor.getLastRequest()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderNullProjectId() { + fullyPopulatedBuilder().setProjectId(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderEmptyProjectId() { + fullyPopulatedBuilder().setProjectId("").build(); + } + + @Test(expected = NullPointerException.class) + public void testBuilderNullRequestFactory() { + fullyPopulatedBuilder().setRequestFactory(null).build(); + } + + @Test + public void testFromApp() throws IOException { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + try { + FirebaseRemoteConfigClientImpl client = FirebaseRemoteConfigClientImpl.fromApp(app); + + assertEquals(TEST_REMOTE_CONFIG_URL, client.getRemoteConfigUrl()); + assertSame(options.getJsonFactory(), client.getJsonFactory()); + + HttpRequest request = client.getRequestFactory().buildGetRequest( + new GenericUrl("https://example.com")); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } finally { + app.delete(); + } + } + + private FirebaseRemoteConfigClientImpl initRemoteConfigClient( + MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + + return FirebaseRemoteConfigClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); + } + + private FirebaseRemoteConfigClientImpl initClientWithFaultyTransport() { + HttpTransport transport = TestUtils.createFaultyHttpTransport(); + return FirebaseRemoteConfigClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .build(); + } + + private FirebaseRemoteConfigClientImpl.Builder fullyPopulatedBuilder() { + return FirebaseRemoteConfigClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + } + + private void checkGetRequestHeader(HttpRequest request) { + assertEquals("GET", request.getRequestMethod()); + assertEquals(TEST_REMOTE_CONFIG_URL, request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals("gzip", headers.getAcceptEncoding()); + } + + private void checkExceptionFromHttpResponse( + FirebaseRemoteConfigException error, + ErrorCode expectedCode, + RemoteConfigErrorCode expectedRemoteConfigCode, + String expectedMessage) { + assertEquals(expectedCode, error.getErrorCode()); + assertEquals(expectedMessage, error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + assertEquals(expectedRemoteConfigCode, error.getRemoteConfigErrorCode()); + + assertNotNull(error.getHttpResponse()); + OutgoingHttpRequest request = error.getHttpResponse().getRequest(); + assertEquals(HttpMethods.GET, request.getMethod()); + assertTrue(request.getUrl().startsWith("https://firebaseremoteconfig.googleapis.com")); + } +}