diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 7ac527778..63c46f2b1 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -17,29 +17,27 @@ package com.google.firebase.iid; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; -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.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; -import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.database.annotations.Nullable; +import com.google.firebase.internal.AbstractHttpErrorHandler; +import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.internal.ErrorHandlingHttpClient; import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; -import java.io.IOException; import java.util.Map; /** @@ -64,22 +62,32 @@ public class FirebaseInstanceId { .build(); private final FirebaseApp app; - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; private final String projectId; + private final ErrorHandlingHttpClient httpClient; private HttpResponseInterceptor interceptor; private FirebaseInstanceId(FirebaseApp app) { - HttpTransport httpTransport = app.getOptions().getHttpTransport(); - this.app = app; - this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app)); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.projectId = ImplFirebaseTrampolines.getProjectId(app); + this(app, null); + } + + @VisibleForTesting + FirebaseInstanceId(FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { + this.app = checkNotNull(app, "app must not be null"); + String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access instance ID 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."); + this.projectId = projectId; + if (requestFactory == null) { + requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + } + + this.httpClient = new ErrorHandlingHttpClient<>( + requestFactory, + app.getOptions().getJsonFactory(), + new InstanceIdErrorHandler()); } /** @@ -146,42 +154,46 @@ private CallableOperation deleteInstanceIdOp( protected Void execute() throws FirebaseInstanceIdException { String url = String.format( "%s/project/%s/instanceId/%s", IID_SERVICE_URL, projectId, instanceId); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildDeleteRequest(new GenericUrl(url)); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - ByteStreams.exhaust(response.getContent()); - } catch (Exception e) { - handleError(instanceId, e); - } finally { - disconnectQuietly(response); - } + HttpRequestInfo request = HttpRequestInfo.buildDeleteRequest(url) + .setResponseInterceptor(interceptor); + httpClient.send(request); return null; } }; } - private static void disconnectQuietly(HttpResponse response) { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // ignored + private static class InstanceIdErrorHandler + extends AbstractHttpErrorHandler { + + @Override + protected FirebaseInstanceIdException createException(FirebaseException base) { + String message = base.getMessage(); + String customMessage = getCustomMessage(base); + if (!Strings.isNullOrEmpty(customMessage)) { + message = customMessage; } + + return new FirebaseInstanceIdException(base, message); } - } - private void handleError(String instanceId, Exception e) throws FirebaseInstanceIdException { - String msg = "Error while invoking instance ID service."; - if (e instanceof HttpResponseException) { - int statusCode = ((HttpResponseException) e).getStatusCode(); - if (ERROR_CODES.containsKey(statusCode)) { - msg = String.format("Instance ID \"%s\": %s", instanceId, ERROR_CODES.get(statusCode)); + private String getCustomMessage(FirebaseException base) { + IncomingHttpResponse response = base.getHttpResponse(); + if (response != null) { + String instanceId = extractInstanceId(response); + String description = ERROR_CODES.get(response.getStatusCode()); + if (description != null) { + return String.format("Instance ID \"%s\": %s", instanceId, description); + } } + + return null; + } + + private String extractInstanceId(IncomingHttpResponse response) { + String url = response.getRequest().getUrl(); + int index = url.lastIndexOf('/'); + return url.substring(index + 1); } - throw new FirebaseInstanceIdException(msg, e); } private static final String SERVICE_ID = FirebaseInstanceId.class.getName(); diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java index dfe0087fc..30553c1c8 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java @@ -21,9 +21,9 @@ /** * Represents an exception encountered while interacting with the Firebase instance ID service. */ -public class FirebaseInstanceIdException extends FirebaseException { +public final class FirebaseInstanceIdException extends FirebaseException { - FirebaseInstanceIdException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + FirebaseInstanceIdException(FirebaseException base, String message) { + super(base.getErrorCodeNew(), message, base.getCause(), base.getHttpResponse()); } } diff --git a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java index d19bab58a..8a29a8c7b 100644 --- a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java +++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java @@ -63,6 +63,10 @@ public static HttpRequestInfo buildGetRequest(String url) { return new HttpRequestInfo(HttpMethods.GET, url, null); } + public static HttpRequestInfo buildDeleteRequest(String url) { + return new HttpRequestInfo(HttpMethods.DELETE, url, null); + } + public static HttpRequestInfo buildPostRequest(String url, HttpContent content) { return new HttpRequestInfo(HttpMethods.POST, url, content); } diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index 366bb7e29..1038ea43f 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -23,18 +23,25 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +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.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.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.GenericFunction; 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 java.util.concurrent.ExecutionException; @@ -43,6 +50,25 @@ public class FirebaseInstanceIdTest { + private static final Map ERROR_MESSAGES = ImmutableMap.of( + 404, "Instance ID \"test-iid\": Failed to find the instance ID.", + 409, "Instance ID \"test-iid\": Already deleted.", + 429, "Instance ID \"test-iid\": Request throttled out by the backend server.", + 500, "Instance ID \"test-iid\": Internal server error.", + 501, "Unexpected HTTP response with status: 501\ntest error" + ); + + private static final Map ERROR_CODES = ImmutableMap.of( + 404, ErrorCode.NOT_FOUND, + 409, ErrorCode.CONFLICT, + 429, ErrorCode.RESOURCE_EXHAUSTED, + 500, ErrorCode.INTERNAL, + 501, ErrorCode.UNKNOWN + ); + + private static final String TEST_URL = + "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -123,7 +149,6 @@ public Void call(Object... args) throws Exception { } ); - String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; for (GenericFunction fn : functions) { TestResponseInterceptor interceptor = new TestResponseInterceptor(); instanceId.setInterceptor(interceptor); @@ -131,54 +156,113 @@ public Void call(Object... args) throws Exception { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); + assertEquals(HttpMethods.DELETE, request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } } @Test public void testDeleteInstanceIdError() throws Exception { - Map errors = ImmutableMap.of( - 404, "Instance ID \"test-iid\": Failed to find the instance ID.", - 429, "Instance ID \"test-iid\": Request throttled out by the backend server.", - 500, "Instance ID \"test-iid\": Internal server error.", - 501, "Error while invoking instance ID service." - ); + final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); - String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; - for (Map.Entry entry : errors.entrySet()) { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setStatusCode(entry.getKey()) - .setContent("test error"); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(options); - - FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - instanceId.setInterceptor(interceptor); - try { - instanceId.deleteInstanceIdAsync("test-iid").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseInstanceIdException); - assertEquals(entry.getValue(), e.getCause().getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); - } + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + try { + for (int statusCode : ERROR_CODES.keySet()) { + response.setStatusCode(statusCode).setContent("test error"); + + try { + instanceId.deleteInstanceIdAsync("test-iid").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseInstanceIdException); + checkFirebaseInstanceIdException((FirebaseInstanceIdException) e.getCause(), statusCode); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(HttpMethods.DELETE, request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); + } + } finally { app.delete(); } } + + @Test + public void testDeleteInstanceIdTransportError() throws Exception { + HttpTransport transport = TestUtils.createFaultyHttpTransport(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + + try { + instanceId.deleteInstanceIdAsync("test-iid").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseInstanceIdException); + FirebaseInstanceIdException error = (FirebaseInstanceIdException) e.getCause(); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCodeNew()); + assertEquals( + "Unknown error while making a remote service call: transport error", + error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + } + } + + @Test + public void testDeleteInstanceIdInvalidJsonIgnored() throws Exception { + final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); + response.setContent("not json"); + + instanceId.deleteInstanceIdAsync("test-iid").get(); + + assertNotNull(interceptor.getResponse()); + } + + private void checkFirebaseInstanceIdException(FirebaseInstanceIdException error, int statusCode) { + assertEquals(ERROR_CODES.get(statusCode), error.getErrorCodeNew()); + assertEquals(ERROR_MESSAGES.get(statusCode), error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + + IncomingHttpResponse httpResponse = error.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + OutgoingHttpRequest request = httpResponse.getRequest(); + assertEquals(HttpMethods.DELETE, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } }