From 5cec94499145b76ff49383d6c2ad919b268ce6bf Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 14 Mar 2019 17:44:09 -0700 Subject: [PATCH 1/9] Introduced FirebaseCloudMessaging interface --- .../firebase/messaging/FirebaseMessaging.java | 75 +- .../messaging/FirebaseMessagingClient.java | 238 +-- .../FirebaseMessagingClientImpl.java | 327 ++++ .../FirebaseMessagingClientImplTest.java | 828 +++++++++++ .../messaging/FirebaseMessagingTest.java | 1314 +---------------- .../messaging/InstanceIdClientTest.java | 390 +++++ .../google/firebase/testing/TestUtils.java | 10 + 7 files changed, 1677 insertions(+), 1505 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java create mode 100644 src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java create mode 100644 src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 49b85b27b..1a056958c 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -23,9 +23,12 @@ import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; @@ -46,18 +49,14 @@ public class FirebaseMessaging { static final String UNKNOWN_ERROR = "unknown-error"; private final FirebaseApp app; - private final FirebaseMessagingClient messagingClient; - private final InstanceIdClient instanceIdClient; - - private FirebaseMessaging(FirebaseApp app) { - this(app, null); - } + private final Supplier messagingClient; + private final Supplier instanceIdClient; @VisibleForTesting - FirebaseMessaging(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { - this.app = checkNotNull(app, "app must not be null"); - this.messagingClient = new FirebaseMessagingClient(app, responseInterceptor); - this.instanceIdClient = new InstanceIdClient(app, responseInterceptor); + FirebaseMessaging(Builder builder) { + this.app = checkNotNull(builder.firebaseApp); + this.messagingClient = Suppliers.memoize(builder.messagingClient); + this.instanceIdClient = Suppliers.memoize(builder.instanceIdClient); } /** @@ -137,6 +136,7 @@ public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { private CallableOperation sendOp( final Message message, final boolean dryRun) { checkNotNull(message, "message must not be null"); + final FirebaseMessagingClient messagingClient = this.messagingClient.get(); return new CallableOperation() { @Override protected String execute() throws FirebaseMessagingException { @@ -290,6 +290,7 @@ private CallableOperation sendAllOp( checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); checkArgument(immutableMessages.size() <= 100, "messages list must not contain more than 100 elements"); + final FirebaseMessagingClient messagingClient = this.messagingClient.get(); return new CallableOperation() { @Override protected BatchResponse execute() throws FirebaseMessagingException { @@ -328,6 +329,7 @@ private CallableOperation s final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); + final InstanceIdClient instanceIdClient = this.instanceIdClient.get(); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { @@ -367,6 +369,7 @@ private CallableOperation u final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); + final InstanceIdClient instanceIdClient = this.instanceIdClient.get(); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { @@ -396,7 +399,7 @@ private static void checkTopic(String topic) { private static class FirebaseMessagingService extends FirebaseService { FirebaseMessagingService(FirebaseApp app) { - super(SERVICE_ID, new FirebaseMessaging(app)); + super(SERVICE_ID, FirebaseMessaging.fromApp(app)); } @Override @@ -406,4 +409,54 @@ public void destroy() { // which will throw once the app is deleted. } } + + private static FirebaseMessaging fromApp(final FirebaseApp app) { + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(new Supplier() { + @Override + public FirebaseMessagingClient get() { + return FirebaseMessagingClientImpl.fromApp(app); + } + }) + .setInstanceIdClient(new Supplier() { + @Override + public InstanceIdClient get() { + return new InstanceIdClient(app, null); + } + }) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + + private FirebaseApp firebaseApp; + private Supplier messagingClient; + private Supplier instanceIdClient; + + private Builder() { } + + Builder setFirebaseApp(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + return this; + } + + Builder setMessagingClient(Supplier messagingClient) { + this.messagingClient = messagingClient; + return this; + } + + Builder setInstanceIdClient(Supplier instanceIdClient) { + this.instanceIdClient = instanceIdClient; + return this; + } + + FirebaseMessaging build() { + return new FirebaseMessaging(this); + } + } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java index 5e694ca4d..63d06b315 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java @@ -1,243 +1,11 @@ -/* - * Copyright 2019 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.messaging; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.api.client.googleapis.batch.BatchCallback; -import com.google.api.client.googleapis.batch.BatchRequest; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpHeaders; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpRequestInitializer; -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.json.JsonHttpContent; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.json.JsonParser; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.firebase.FirebaseApp; -import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.internal.ApiClientUtils; -import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.SdkUtils; -import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; -import com.google.firebase.messaging.internal.MessagingServiceResponse; -import java.io.IOException; import java.util.List; -import java.util.Map; - -/** - * A helper class for interacting with Firebase Cloud Messaging service. - */ -final class FirebaseMessagingClient { - - private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; - - private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; - - private static final Map FCM_ERROR_CODES = - ImmutableMap.builder() - // FCM v1 canonical error codes - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "mismatched-credential") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "invalid-apns-credentials") - - // FCM v1 new error codes - .put("APNS_AUTH_ERROR", "invalid-apns-credentials") - .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) - .put("INVALID_ARGUMENT", "invalid-argument") - .put("QUOTA_EXCEEDED", "message-rate-exceeded") - .put("SENDER_ID_MISMATCH", "mismatched-credential") - .put("UNAVAILABLE", "server-unavailable") - .put("UNREGISTERED", "registration-token-not-registered") - .build(); - - private final String fcmSendUrl; - private final HttpRequestFactory requestFactory; - private final HttpRequestFactory childRequestFactory; - private final JsonFactory jsonFactory; - private final HttpResponseInterceptor responseInterceptor; - private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); - - FirebaseMessagingClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { - String projectId = ImplFirebaseTrampolines.getProjectId(app); - checkArgument(!Strings.isNullOrEmpty(projectId), - "Project ID is required to access messaging 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.fcmSendUrl = String.format(FCM_URL, projectId); - this.requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); - this.childRequestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.responseInterceptor = responseInterceptor; - } - - String send(Message message, boolean dryRun) throws FirebaseMessagingException { - try { - return sendSingleRequest(message, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } - } - - BatchResponse sendAll( - List messages, boolean dryRun) throws FirebaseMessagingException { - try { - return sendBatchRequest(messages, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } - } - - private String sendSingleRequest(Message message, boolean dryRun) throws IOException { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(fcmSendUrl), - new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); - setCommonFcmHeaders(request.getHeaders()); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - HttpResponse response = request.execute(); - try { - MessagingServiceResponse parsed = new MessagingServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - return parsed.getMessageId(); - } finally { - ApiClientUtils.disconnectQuietly(response); - } - } - - private BatchResponse sendBatchRequest( - List messages, boolean dryRun) throws IOException { - - MessagingBatchCallback callback = new MessagingBatchCallback(); - BatchRequest batch = newBatchRequest(messages, dryRun, callback); - batch.execute(); - return new BatchResponse(callback.getResponses()); - } - - private BatchRequest newBatchRequest( - List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { - - BatchRequest batch = new BatchRequest( - requestFactory.getTransport(), getBatchRequestInitializer()); - batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); - - final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); - final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); - for (Message message : messages) { - // Using a separate request factory without authorization is faster for large batches. - // A simple performance test showed a 400-500ms speed up for batches of 1000 messages. - HttpRequest request = childRequestFactory.buildPostRequest( - sendUrl, - new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); - request.setParser(jsonParser); - setCommonFcmHeaders(request.getHeaders()); - batch.queue( - request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); - } - - return batch; - } - - private void setCommonFcmHeaders(HttpHeaders headers) { - headers.set("X-GOOG-API-FORMAT-VERSION", "2"); - headers.set("X-Client-Version", clientVersion); - } - - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - - return newException(response, e); - } - - private HttpRequestInitializer getBatchRequestInitializer() { - return new HttpRequestInitializer(){ - @Override - public void initialize(HttpRequest request) throws IOException { - requestFactory.getInitializer().initialize(request); - request.setResponseInterceptor(responseInterceptor); - } - }; - } - - private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { - return newException(response, null); - } - - private static FirebaseMessagingException newException( - MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { - String code = FCM_ERROR_CODES.get(response.getErrorCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - - String msg = response.getErrorMessage(); - if (Strings.isNullOrEmpty(msg)) { - if (e != null) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } else { - msg = String.format("Unexpected HTTP response: %s", response.toString()); - } - } - - return new FirebaseMessagingException(code, msg, e); - } - - private static class MessagingBatchCallback - implements BatchCallback { - private final ImmutableList.Builder responses = ImmutableList.builder(); +interface FirebaseMessagingClient { - @Override - public void onSuccess( - MessagingServiceResponse response, HttpHeaders responseHeaders) { - responses.add(SendResponse.fromMessageId(response.getMessageId())); - } + String send(Message message, boolean dryRun) throws FirebaseMessagingException; - @Override - public void onFailure( - MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { - responses.add(SendResponse.fromException(newException(error))); - } + BatchResponse sendAll(List messages, boolean dryRun) throws FirebaseMessagingException; - List getResponses() { - return this.responses.build(); - } - } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java new file mode 100644 index 000000000..0df0e0a65 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -0,0 +1,327 @@ +/* + * Copyright 2019 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.messaging; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.googleapis.batch.BatchCallback; +import com.google.api.client.googleapis.batch.BatchRequest; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpRequestInitializer; +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.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.JsonParser; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; +import com.google.firebase.messaging.internal.MessagingServiceResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with Firebase Cloud Messaging service. + */ +final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { + + private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + + private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; + + private static final Map FCM_ERROR_CODES = + ImmutableMap.builder() + // FCM v1 canonical error codes + .put("NOT_FOUND", "registration-token-not-registered") + .put("PERMISSION_DENIED", "mismatched-credential") + .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") + .put("UNAUTHENTICATED", "invalid-apns-credentials") + + // FCM v1 new error codes + .put("APNS_AUTH_ERROR", "invalid-apns-credentials") + .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) + .put("INVALID_ARGUMENT", "invalid-argument") + .put("QUOTA_EXCEEDED", "message-rate-exceeded") + .put("SENDER_ID_MISMATCH", "mismatched-credential") + .put("UNAVAILABLE", "server-unavailable") + .put("UNREGISTERED", "registration-token-not-registered") + .build(); + + private final String fcmSendUrl; + private final HttpRequestFactory requestFactory; + private final HttpRequestFactory childRequestFactory; + private final JsonFactory jsonFactory; + private final HttpResponseInterceptor responseInterceptor; + private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + + private FirebaseMessagingClientImpl(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.projectId)); + this.fcmSendUrl = String.format(FCM_URL, builder.projectId); + this.requestFactory = checkNotNull(builder.requestFactory); + this.childRequestFactory = checkNotNull(builder.childRequestFactory); + this.jsonFactory = checkNotNull(builder.jsonFactory); + this.responseInterceptor = builder.responseInterceptor; + } + + @VisibleForTesting + String getFcmSendUrl() { + return fcmSendUrl; + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; + } + + @VisibleForTesting + HttpRequestFactory getChildRequestFactory() { + return childRequestFactory; + } + + @VisibleForTesting + JsonFactory getJsonFactory() { + return jsonFactory; + } + + @VisibleForTesting + String getClientVersion() { + return clientVersion; + } + + static FirebaseMessagingClientImpl fromApp(FirebaseApp app) { + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access messaging 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 FirebaseMessagingClientImpl.builder() + .setProjectId(projectId) + .setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) + .setChildRequestFactory(ApiClientUtils.newUnauthorizedRequestFactory(app)) + .setJsonFactory(app.getOptions().getJsonFactory()) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + public String send(Message message, boolean dryRun) throws FirebaseMessagingException { + try { + return sendSingleRequest(message, dryRun); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); + } + } + + public BatchResponse sendAll( + List messages, boolean dryRun) throws FirebaseMessagingException { + try { + return sendBatchRequest(messages, dryRun); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); + } + } + + private String sendSingleRequest(Message message, boolean dryRun) throws IOException { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(fcmSendUrl), + new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); + setCommonFcmHeaders(request.getHeaders()); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(responseInterceptor); + HttpResponse response = request.execute(); + try { + MessagingServiceResponse parsed = new MessagingServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + return parsed.getMessageId(); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + private BatchResponse sendBatchRequest( + List messages, boolean dryRun) throws IOException { + + MessagingBatchCallback callback = new MessagingBatchCallback(); + BatchRequest batch = newBatchRequest(messages, dryRun, callback); + batch.execute(); + return new BatchResponse(callback.getResponses()); + } + + private BatchRequest newBatchRequest( + List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { + + BatchRequest batch = new BatchRequest( + requestFactory.getTransport(), getBatchRequestInitializer()); + batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); + + final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); + final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); + for (Message message : messages) { + // Using a separate request factory without authorization is faster for large batches. + // A simple performance test showed a 400-500ms speed up for batches of 1000 messages. + HttpRequest request = childRequestFactory.buildPostRequest( + sendUrl, + new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); + request.setParser(jsonParser); + setCommonFcmHeaders(request.getHeaders()); + batch.queue( + request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); + } + + return batch; + } + + private void setCommonFcmHeaders(HttpHeaders headers) { + headers.set("X-GOOG-API-FORMAT-VERSION", "2"); + headers.set("X-Client-Version", clientVersion); + } + + private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { + MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + } + + return newException(response, e); + } + + private HttpRequestInitializer getBatchRequestInitializer() { + return new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest request) throws IOException { + HttpRequestInitializer initializer = requestFactory.getInitializer(); + if (initializer != null) { + initializer.initialize(request); + } + request.setResponseInterceptor(responseInterceptor); + } + }; + } + + static final class Builder { + + private String projectId; + private HttpRequestFactory requestFactory; + private HttpRequestFactory childRequestFactory; + 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 setChildRequestFactory(HttpRequestFactory childRequestFactory) { + this.childRequestFactory = childRequestFactory; + return this; + } + + Builder setJsonFactory(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + return this; + } + + Builder setResponseInterceptor(HttpResponseInterceptor responseInterceptor) { + this.responseInterceptor = responseInterceptor; + return this; + } + + FirebaseMessagingClientImpl build() { + return new FirebaseMessagingClientImpl(this); + } + } + + private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { + return newException(response, null); + } + + private static FirebaseMessagingException newException( + MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { + String code = FCM_ERROR_CODES.get(response.getErrorCode()); + if (code == null) { + code = FirebaseMessaging.UNKNOWN_ERROR; + } + + String msg = response.getErrorMessage(); + if (Strings.isNullOrEmpty(msg)) { + if (e != null) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } else { + msg = String.format("Unexpected HTTP response: %s", response.toString()); + } + } + + return new FirebaseMessagingException(code, msg, e); + } + + private static class MessagingBatchCallback + implements BatchCallback { + + private final ImmutableList.Builder responses = ImmutableList.builder(); + + @Override + public void onSuccess( + MessagingServiceResponse response, HttpHeaders responseHeaders) { + responses.add(SendResponse.fromMessageId(response.getMessageId())); + } + + @Override + public void onFailure( + MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { + responses.add(SendResponse.fromException(newException(error))); + } + + List getResponses() { + return this.responses.build(); + } + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java new file mode 100644 index 000000000..c2fc60aa1 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -0,0 +1,828 @@ +/* + * Copyright 2018 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.messaging; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +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.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.json.JsonParser; +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.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.messaging.WebpushNotification.Action; +import com.google.firebase.messaging.WebpushNotification.Direction; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class FirebaseMessagingClientImplTest { + + private static final String TEST_FCM_URL = + "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + + private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; + + private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( + "fcm_batch_success.txt"); + + private static final String MOCK_BATCH_FAILURE_RESPONSE = TestUtils.loadResource( + "fcm_batch_failure.txt"); + + private static final Message EMPTY_MESSAGE = Message.builder() + .setTopic("test-topic") + .build(); + + @Test + public void testSend() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent(MOCK_RESPONSE); + String resp = messaging.send(entry.getKey(), false); + + assertEquals("mock-name", resp); + checkRequestHeader(interceptor.getLastRequest()); + checkRequest(interceptor.getLastRequest(), + ImmutableMap.of("message", entry.getValue())); + } + } + + @Test + public void testSendDryRun() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent(MOCK_RESPONSE); + String resp = messaging.send(entry.getKey(), true); + + assertEquals("mock-name", resp); + checkRequestHeader(interceptor.getLastRequest()); + checkRequest(interceptor.getLastRequest(), + ImmutableMap.of("message", entry.getValue(), "validate_only", true)); + } + } + + @Test + public void testSendHttpError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("{}"); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendTransportError() { + FirebaseMessagingClient messaging = initMessagingClientWithFaultyTransport(); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSendErrorWithZeroContentResponse() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setZeroContent(); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: null", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithDetails() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("invalid-argument", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithCanonicalCode() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithFcmError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAll() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClientForBatchRequests( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + List messages = ImmutableList.of( + EMPTY_MESSAGE, EMPTY_MESSAGE + ); + + BatchResponse responses = messaging.sendAll(messages, false); + + assertSendBatchSuccess(responses, interceptor); + } + + @Test + public void testSendAllDryRun() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClientForBatchRequests( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + List messages = ImmutableList.of( + EMPTY_MESSAGE, EMPTY_MESSAGE + ); + + BatchResponse responses = messaging.sendAll(messages, true); + + assertSendBatchSuccess(responses, interceptor); + } + + @Test + public void testSendAllFailure() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClientForBatchRequests( + MOCK_BATCH_FAILURE_RESPONSE, interceptor); + List messages = ImmutableList.of( + EMPTY_MESSAGE, EMPTY_MESSAGE, EMPTY_MESSAGE + ); + + BatchResponse responses = messaging.sendAll(messages, false); + + assertSendBatchFailure(responses, interceptor); + } + + @Test + public void testSendAllHttpError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("{}"); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllTransportError() { + FirebaseMessagingClient messaging = initMessagingClientWithFaultyTransport(); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSendAllErrorWithEmptyResponse() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setZeroContent(); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: null", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithDetails() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("invalid-argument", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithCanonicalCode() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithFcmError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithoutMessage() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + messaging.sendAll(messages, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Unexpected HTTP response")); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(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(expected = NullPointerException.class) + public void testBuilderNullChildRequestFactory() { + fullyPopulatedBuilder().setChildRequestFactory(null).build(); + } + + @Test + public void testFromApp() throws IOException { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + try { + FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.fromApp(app); + + assertEquals(TEST_FCM_URL, client.getFcmSendUrl()); + assertEquals("Java/Admin/" + SdkUtils.getVersion(), client.getClientVersion()); + assertSame(options.getJsonFactory(), client.getJsonFactory()); + + HttpRequest request = client.getRequestFactory().buildGetRequest( + new GenericUrl("https://example.com")); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + + request = client.getChildRequestFactory().buildGetRequest( + new GenericUrl("https://example.com")); + assertNull(request.getHeaders().getAuthorization()); + } finally { + app.delete(); + } + } + + private FirebaseMessagingClientImpl initMessagingClient( + MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + + return FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); + } + + private FirebaseMessagingClientImpl initMessagingClientForBatchRequests( + String responsePayload, TestResponseInterceptor interceptor) { + MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse() + .setContentType("multipart/mixed; boundary=test_boundary") + .setContent(responsePayload); + return initMessagingClient(httpResponse, interceptor); + } + + private FirebaseMessagingClientImpl initMessagingClientWithFaultyTransport() { + HttpTransport transport = TestUtils.faultyHttpTransport(); + return FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .build(); + } + + private void checkRequestHeader(HttpRequest request) { + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); + assertEquals("Java/Admin/" + SdkUtils.getVersion(), headers.get("X-Client-Version")); + } + + private void checkRequest( + HttpRequest request, Map expected) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + Map parsed = new HashMap<>(); + parser.parseAndClose(parsed); + assertEquals(expected, parsed); + } + + private void assertSendBatchSuccess( + BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { + + assertEquals(2, batchResponse.getSuccessCount()); + assertEquals(0, batchResponse.getFailureCount()); + + List responses = batchResponse.getResponses(); + assertEquals(2, responses.size()); + for (int i = 0; i < 2; i++) { + SendResponse sendResponse = responses.get(i); + assertTrue(sendResponse.isSuccessful()); + assertEquals("projects/test-project/messages/" + (i + 1), sendResponse.getMessageId()); + assertNull(sendResponse.getException()); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + checkBatchRequest(interceptor.getLastRequest(), 2); + } + + private void assertSendBatchFailure( + BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { + + assertEquals(1, batchResponse.getSuccessCount()); + assertEquals(2, batchResponse.getFailureCount()); + + List responses = batchResponse.getResponses(); + assertEquals(3, responses.size()); + SendResponse firstResponse = responses.get(0); + assertTrue(firstResponse.isSuccessful()); + assertEquals("projects/test-project/messages/1", firstResponse.getMessageId()); + assertNull(firstResponse.getException()); + + SendResponse secondResponse = responses.get(1); + assertFalse(secondResponse.isSuccessful()); + assertNull(secondResponse.getMessageId()); + FirebaseMessagingException exception = secondResponse.getException(); + assertNotNull(exception); + assertEquals("invalid-argument", exception.getErrorCode()); + + SendResponse thirdResponse = responses.get(2); + assertFalse(thirdResponse.isSuccessful()); + assertNull(thirdResponse.getMessageId()); + exception = thirdResponse.getException(); + assertNotNull(exception); + assertEquals("invalid-argument", exception.getErrorCode()); + + checkBatchRequestHeader(interceptor.getLastRequest()); + checkBatchRequest(interceptor.getLastRequest(), 3); + } + + private void checkBatchRequestHeader(HttpRequest request) { + assertEquals("POST", request.getRequestMethod()); + assertEquals("https://fcm.googleapis.com/batch", request.getUrl().toString()); + } + + private void checkBatchRequest(HttpRequest request, int expectedParts) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + String[] lines = out.toString().split("\n"); + assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); + assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); + assertEquals(expectedParts, countLinesWithPrefix( + lines, "x-client-version: Java/Admin/" + SdkUtils.getVersion())); + } + + private int countLinesWithPrefix(String[] lines, String prefix) { + int matchCount = 0; + for (String line : lines) { + if (line.trim().startsWith(prefix)) { + matchCount++; + } + } + return matchCount; + } + + private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { + return FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + } + + private static Map> buildTestMessages() { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + // Empty message + builder.put( + EMPTY_MESSAGE, + ImmutableMap.of("topic", "test-topic")); + + // Notification message + builder.put( + Message.builder() + .setNotification(new Notification("test title", "test body")) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "notification", ImmutableMap.of("title", "test title", "body", "test body"))); + + // Data message + builder.put( + Message.builder() + .putData("k1", "v1") + .putData("k2", "v2") + .putAllData(ImmutableMap.of("k3", "v3", "k4", "v4")) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4"))); + + // Android message + builder.put( + Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setTtl(TimeUnit.SECONDS.toMillis(123)) + .setRestrictedPackageName("test-package") + .setCollapseKey("test-key") + .setNotification(AndroidNotification.builder() + .setClickAction("test-action") + .setTitle("test-title") + .setBody("test-body") + .setIcon("test-icon") + .setColor("#112233") + .setTag("test-tag") + .setSound("test-sound") + .setTitleLocalizationKey("test-title-key") + .setBodyLocalizationKey("test-body-key") + .addTitleLocalizationArg("t-arg1") + .addAllTitleLocalizationArgs(ImmutableList.of("t-arg2", "t-arg3")) + .addBodyLocalizationArg("b-arg1") + .addAllBodyLocalizationArgs(ImmutableList.of("b-arg2", "b-arg3")) + .setChannelId("channel-id") + .build()) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "android", ImmutableMap.of( + "priority", "high", + "collapse_key", "test-key", + "ttl", "123s", + "restricted_package_name", "test-package", + "notification", ImmutableMap.builder() + .put("click_action", "test-action") + .put("title", "test-title") + .put("body", "test-body") + .put("icon", "test-icon") + .put("color", "#112233") + .put("tag", "test-tag") + .put("sound", "test-sound") + .put("title_loc_key", "test-title-key") + .put("title_loc_args", ImmutableList.of("t-arg1", "t-arg2", "t-arg3")) + .put("body_loc_key", "test-body-key") + .put("body_loc_args", ImmutableList.of("b-arg1", "b-arg2", "b-arg3")) + .put("channel_id", "channel-id") + .build() + ) + )); + + // APNS message + builder.put( + Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putAllCustomData(ImmutableMap.of("k1", "v1", "k2", true)) + .setAps(Aps.builder() + .setBadge(42) + .setAlert(ApsAlert.builder() + .setTitle("test-title") + .setSubtitle("test-subtitle") + .setBody("test-body") + .build()) + .build()) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "payload", ImmutableMap.of("k1", "v1", "k2", true, + "aps", ImmutableMap.of("badge", new BigDecimal(42), + "alert", ImmutableMap.of( + "title", "test-title", "subtitle", "test-subtitle", + "body", "test-body")))) + )); + + // Webpush message (no notification) + builder.put( + Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "webpush", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3")) + )); + + // Webpush message (simple notification) + builder.put( + Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(new WebpushNotification("test-title", "test-body", "test-icon")) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "webpush", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "notification", ImmutableMap.of( + "title", "test-title", "body", "test-body", "icon", "test-icon")) + )); + + // Webpush message (all fields) + builder.put( + Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(WebpushNotification.builder() + .setTitle("test-title") + .setBody("test-body") + .setIcon("test-icon") + .setBadge("test-badge") + .setImage("test-image") + .setLanguage("test-lang") + .setTag("test-tag") + .setData(ImmutableList.of("arbitrary", "data")) + .setDirection(Direction.AUTO) + .setRenotify(true) + .setRequireInteraction(false) + .setSilent(true) + .setTimestampMillis(100L) + .setVibrate(new int[]{200, 100, 200}) + .addAction(new Action("action1", "title1")) + .addAllActions(ImmutableList.of(new Action("action2", "title2", "icon2"))) + .putCustomData("k4", "v4") + .putAllCustomData(ImmutableMap.of("k5", "v5", "k6", "v6")) + .build()) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "webpush", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "notification", ImmutableMap.builder() + .put("title", "test-title") + .put("body", "test-body") + .put("icon", "test-icon") + .put("badge", "test-badge") + .put("image", "test-image") + .put("lang", "test-lang") + .put("tag", "test-tag") + .put("data", ImmutableList.of("arbitrary", "data")) + .put("renotify", true) + .put("requireInteraction", false) + .put("silent", true) + .put("dir", "auto") + .put("timestamp", new BigDecimal(100)) + .put("vibrate", ImmutableList.of( + new BigDecimal(200), new BigDecimal(100), new BigDecimal(200))) + .put("actions", ImmutableList.of( + ImmutableMap.of("action", "action1", "title", "title1"), + ImmutableMap.of("action", "action2", "title", "title2", "icon", "icon2"))) + .put("k4", "v4") + .put("k5", "v5") + .put("k6", "v6") + .build()) + )); + + return builder.build(); + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 3c9b7c4fb..438b41189 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -1,102 +1,20 @@ -/* - * Copyright 2018 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.messaging; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -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.HttpHeaders; -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.http.LowLevelHttpRequest; -import com.google.api.client.json.JsonParser; -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.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.internal.SdkUtils; -import com.google.firebase.messaging.WebpushNotification.Action; -import com.google.firebase.messaging.WebpushNotification.Direction; -import com.google.firebase.testing.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; -import com.google.firebase.testing.TestUtils; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; public class FirebaseMessagingTest { - private static final String TEST_FCM_URL = - "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; - - private static final String TEST_IID_SUBSCRIBE_URL = - "https://iid.googleapis.com/iid/v1:batchAdd"; - - private static final String TEST_IID_UNSUBSCRIBE_URL = - "https://iid.googleapis.com/iid/v1:batchRemove"; - - private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); - - private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; - - private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( - "fcm_batch_success.txt"); - - private static final String MOCK_BATCH_FAILURE_RESPONSE = TestUtils.loadResource( - "fcm_batch_failure.txt"); - - private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); - - static { - for (int i = 0; i < 1001; i++) { - TOO_MANY_IDS.add("id" + i); - } - } - - private static final List INVALID_TOPIC_MGT_ARGS = ImmutableList.of( - new TopicMgtArgs(null, null), - new TopicMgtArgs(null, "test-topic"), - new TopicMgtArgs(ImmutableList.of(), "test-topic"), - new TopicMgtArgs(ImmutableList.of(""), "test-topic"), - new TopicMgtArgs(TOO_MANY_IDS.build(), "test-topic"), - new TopicMgtArgs(ImmutableList.of(""), null), - new TopicMgtArgs(ImmutableList.of("id"), ""), - new TopicMgtArgs(ImmutableList.of("id"), "foo*") - ); - @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -143,1193 +61,71 @@ public void testPostDeleteApp() { } @Test - public void testNoProjectId() { + public void testNoProjectId() throws FirebaseMessagingException { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("test-token")) .build(); FirebaseApp.initializeApp(options); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); try { - FirebaseMessaging.getInstance(); + messaging.send(Message.builder() + .setTopic("test-topic") + .build()); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { // expected } } - @Test - public void testSendNullMessage() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initDefaultMessaging(interceptor); - try { - messaging.sendAsync(null); - fail("No error thrown for null message"); - } catch (NullPointerException expected) { - // expected - } - - assertNull(interceptor.getResponse()); - } - - @Test - public void testSend() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - Map> testMessages = buildTestMessages(); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.sendAsync((Message) args[0]).get(); - } - }, - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.send((Message) args[0]); - } - } - ); - for (GenericFunction fn : functions) { - for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent(MOCK_RESPONSE); - String resp = fn.call(entry.getKey()); - assertEquals("mock-name", resp); - - checkRequestHeader(interceptor.getLastRequest()); - checkRequest(interceptor.getLastRequest(), - ImmutableMap.of("message", entry.getValue())); - } - } - } - - @Test - public void testSendDryRun() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - Map> testMessages = buildTestMessages(); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.sendAsync((Message) args[0], true).get(); - } - }, - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.send((Message) args[0], true); - } - } - ); - - for (GenericFunction fn : functions) { - for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent(MOCK_RESPONSE); - String resp = fn.call(entry.getKey()); - assertEquals("mock-name", resp); - - checkRequestHeader(interceptor.getLastRequest()); - checkRequest(interceptor.getLastRequest(), - ImmutableMap.of("message", entry.getValue(), "validate_only", true)); - } - } - } - - @Test - public void testSendError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent("{}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendErrorWithZeroContentResponse() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setZeroContent(); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: null", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendErrorWithDetails() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("invalid-argument", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendErrorWithCanonicalCode() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendErrorWithFcmError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendMulticastWithNull() { - FirebaseMessaging messaging = initDefaultMessaging(); - try { - messaging.sendMulticastAsync(null); - fail("No error thrown for null multicast message"); - } catch (NullPointerException expected) { - // expected - } - } - - @Test - public void testSendMulticast() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .build(); - - BatchResponse responses = messaging.sendMulticast(multicast); - - assertSendBatchSuccess(responses, interceptor); - } - - @Test - public void testSendMulticastAsync() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .build(); - - BatchResponse responses = messaging.sendMulticastAsync(multicast).get(); - - assertSendBatchSuccess(responses, interceptor); - } - - @Test - public void testSendMulticastFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .addToken("token3") - .build(); - - BatchResponse responses = messaging.sendMulticast(multicast); - - assertSendBatchFailure(responses, interceptor); - } - - @Test - public void testSendMulticastAsyncFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .addToken("token3") - .build(); - - BatchResponse responses = messaging.sendMulticastAsync(multicast).get(); - - assertSendBatchFailure(responses, interceptor); - } - - @Test - public void testSendAllWithNull() { - FirebaseMessaging messaging = initDefaultMessaging(); - try { - messaging.sendAllAsync(null); - fail("No error thrown for null message list"); - } catch (NullPointerException expected) { - // expected - } - } - - @Test - public void testSendAllWithEmptyList() { - FirebaseMessaging messaging = initDefaultMessaging(); - try { - messaging.sendAllAsync(ImmutableList.of()); - fail("No error thrown for empty message list"); - } catch (IllegalArgumentException expected) { - // expected - } - } - - @Test - public void testSendAllWithTooManyMessages() { - FirebaseMessaging messaging = initDefaultMessaging(); - ImmutableList.Builder listBuilder = ImmutableList.builder(); - for (int i = 0; i < 101; i++) { - listBuilder.add(Message.builder().setTopic("topic").build()); - } - try { - messaging.sendAllAsync(listBuilder.build()); - fail("No error thrown for too many messages in the list"); - } catch (IllegalArgumentException expected) { - // expected - } - } - - @Test - public void testSendAll() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build() - ); - - BatchResponse responses = messaging.sendAll(messages); - - assertSendBatchSuccess(responses, interceptor); - } - - @Test - public void testSendAllAsync() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build() - ); - - BatchResponse responses = messaging.sendAllAsync(messages).get(); - - assertSendBatchSuccess(responses, interceptor); - } - - @Test - public void testSendAllFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build(), - Message.builder().setTopic("topic3").build() - ); - - BatchResponse responses = messaging.sendAll(messages); - - assertSendBatchFailure(responses, interceptor); - } - - @Test - public void testSendAllAsyncFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build(), - Message.builder().setTopic("topic3").build() - ); - - BatchResponse responses = messaging.sendAllAsync(messages).get(); - - assertSendBatchFailure(responses, interceptor); - } - - @Test - public void testSendAllHttpError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent("{}"); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); - } - } - - @Test - public void testSendAllErrorWithEmptyResponse() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setZeroContent(); - - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: null", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithDetails() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); - - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("invalid-argument", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithCanonicalCode() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); - - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithFcmError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); - - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testSendAllErrorWithoutMessage() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); - - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertTrue(error.getMessage().startsWith("Unexpected HTTP response")); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } - } - - @Test - public void testInvalidSubscribe() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initDefaultMessaging(interceptor); - - for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { - try { - messaging.subscribeToTopicAsync(args.registrationTokens, args.topic); - fail("No error thrown for invalid args"); - } catch (IllegalArgumentException expected) { - // expected - } - } - - assertNull(interceptor.getResponse()); - } - - @Test - public void testSubscribe() throws Exception { - final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), - "test-topic").get(); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), - "/topics/test-topic"); - } - } - ); - - for (GenericFunction fn : functions) { - response.setContent(responseString); - TopicManagementResponse result = fn.call(); - checkTopicManagementRequestHeader( - interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - checkTopicManagementRequest(interceptor.getLastRequest(), result); - } - } - - @Test - public void testSubscribeError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - } - } - - @Test - public void testSubscribeUnknownError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - } - - @Test - public void testSubscribeTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); - } - } - - @Test - public void testInvalidUnsubscribe() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initDefaultMessaging(interceptor); - - for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { - try { - messaging.unsubscribeFromTopicAsync(args.registrationTokens, args.topic); - fail("No error thrown for invalid args"); - } catch (IllegalArgumentException expected) { - // expected - } - } - - assertNull(interceptor.getResponse()); - } - - @Test - public void testUnsubscribe() throws Exception { - final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), - "test-topic").get(); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), - "/topics/test-topic"); - } - } - ); - - for (GenericFunction fn : functions) { - response.setContent(responseString); - TopicManagementResponse result = fn.call(); - checkTopicManagementRequestHeader( - interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - checkTopicManagementRequest(interceptor.getLastRequest(), result); - } - } - - @Test - public void testUnsubscribeError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - } - } - - @Test - public void testUnsubscribeUnknownError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - } - - @Test - public void testUnsubscribeTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); - } - } - - private static FirebaseMessaging initMessaging( - MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(mockResponse) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - - return new FirebaseMessaging(app, interceptor); - } - - private static FirebaseMessaging initDefaultMessaging() { - return initDefaultMessaging(null); - } - - private static FirebaseMessaging initDefaultMessaging(HttpResponseInterceptor interceptor) { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - return new FirebaseMessaging(app, interceptor); - } - - private static FirebaseMessaging initFaultyTransportMessaging() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(new FailingHttpTransport()) - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - return new FirebaseMessaging(app, null); - } - - private void checkRequest( - HttpRequest request, Map expected) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); - Map parsed = new HashMap<>(); - parser.parseAndClose(parsed); - assertEquals(expected, parsed); - } - - private void checkRequestHeader(HttpRequest request) { - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - HttpHeaders headers = request.getHeaders(); - assertEquals("Bearer test-token", headers.getAuthorization()); - assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); - assertEquals("Java/Admin/" + SdkUtils.getVersion(), headers.get("X-Client-Version")); - } - - private FirebaseMessaging getMessagingForBatchRequest( - String responsePayload, TestResponseInterceptor interceptor) { - MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse() - .setContentType("multipart/mixed; boundary=test_boundary") - .setContent(responsePayload); - return initMessaging(httpResponse, interceptor); - } - - private void assertSendBatchSuccess( - BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { - - assertEquals(2, batchResponse.getSuccessCount()); - assertEquals(0, batchResponse.getFailureCount()); - - List responses = batchResponse.getResponses(); - assertEquals(2, responses.size()); - for (int i = 0; i < 2; i++) { - SendResponse sendResponse = responses.get(i); - assertTrue(sendResponse.isSuccessful()); - assertEquals("projects/test-project/messages/" + (i + 1), sendResponse.getMessageId()); - assertNull(sendResponse.getException()); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - checkBatchRequest(interceptor.getLastRequest(), 2); - } - - private void assertSendBatchFailure( - BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { - - assertEquals(1, batchResponse.getSuccessCount()); - assertEquals(2, batchResponse.getFailureCount()); - - List responses = batchResponse.getResponses(); - assertEquals(3, responses.size()); - SendResponse firstResponse = responses.get(0); - assertTrue(firstResponse.isSuccessful()); - assertEquals("projects/test-project/messages/1", firstResponse.getMessageId()); - assertNull(firstResponse.getException()); - - SendResponse secondResponse = responses.get(1); - assertFalse(secondResponse.isSuccessful()); - assertNull(secondResponse.getMessageId()); - FirebaseMessagingException exception = secondResponse.getException(); - assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); +// @Test +// public void testSendNullMessage() throws FirebaseMessagingException { +// TestResponseInterceptor interceptor = new TestResponseInterceptor(); +// FirebaseMessagingClient messaging = initDefaultMessaging(interceptor); +// try { +// messaging.send(null, false); +// fail("No error thrown for null message"); +// } catch (NullPointerException expected) { +// // expected +// } +// +// assertNull(interceptor.getResponse()); +// } +// +// @Test +// public void testSendAllWithNull() throws FirebaseMessagingException { +// FirebaseMessagingClient messaging = initDefaultMessaging(); +// try { +// messaging.sendAll(null, false); +// fail("No error thrown for null message list"); +// } catch (NullPointerException expected) { +// // expected +// } +// } +// +// @Test +// public void testSendAllWithEmptyList() throws FirebaseMessagingException { +// FirebaseMessagingClient messaging = initDefaultMessaging(); +// try { +// messaging.sendAll(ImmutableList.of(), false); +// fail("No error thrown for empty message list"); +// } catch (IllegalArgumentException expected) { +// // expected +// } +// } +// +// @Test +// public void testSendAllWithTooManyMessages() throws FirebaseMessagingException { +// FirebaseMessagingClient messaging = initDefaultMessaging(); +// ImmutableList.Builder listBuilder = ImmutableList.builder(); +// for (int i = 0; i < 101; i++) { +// listBuilder.add(Message.builder().setTopic("topic").build()); +// } +// try { +// messaging.sendAll(listBuilder.build(), false); +// fail("No error thrown for too many messages in the list"); +// } catch (IllegalArgumentException expected) { +// // expected +// } +// } - SendResponse thirdResponse = responses.get(2); - assertFalse(thirdResponse.isSuccessful()); - assertNull(thirdResponse.getMessageId()); - exception = thirdResponse.getException(); - assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); - - checkBatchRequestHeader(interceptor.getLastRequest()); - checkBatchRequest(interceptor.getLastRequest(), 3); - } - - private void checkBatchRequestHeader(HttpRequest request) { - assertEquals("POST", request.getRequestMethod()); - assertEquals("https://fcm.googleapis.com/batch", request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - } - - private void checkBatchRequest(HttpRequest request, int expectedParts) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - String[] lines = out.toString().split("\n"); - assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); - assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); - assertEquals(expectedParts, countLinesWithPrefix( - lines, "x-client-version: Java/Admin/" + SdkUtils.getVersion())); - } - - private int countLinesWithPrefix(String[] lines, String prefix) { - int matchCount = 0; - for (String line : lines) { - if (line.trim().startsWith(prefix)) { - matchCount++; - } - } - return matchCount; - } - - private static String getTopicManagementErrorCode(int statusCode) { - String code = InstanceIdClient.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; - } - return code; - } - - - private void checkTopicManagementRequest( - HttpRequest request, TopicManagementResponse result) throws IOException { - assertEquals(1, result.getSuccessCount()); - assertEquals(1, result.getFailureCount()); - assertEquals(1, result.getErrors().size()); - assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("unknown-error", result.getErrors().get(0).getReason()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - Map parsed = new HashMap<>(); - JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); - parser.parseAndClose(parsed); - assertEquals(2, parsed.size()); - assertEquals("/topics/test-topic", parsed.get("to")); - assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); - } - - private void checkTopicManagementRequestHeader( - HttpRequest request, String expectedUrl) { - assertEquals("POST", request.getRequestMethod()); - assertEquals(expectedUrl, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - } - - private static class TopicMgtArgs { - private final List registrationTokens; - private final String topic; - - TopicMgtArgs(List registrationTokens, String topic) { - this.registrationTokens = registrationTokens; - this.topic = topic; - } - } - - private static class FailingHttpTransport extends HttpTransport { - @Override - protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - throw new IOException("transport error"); - } - } - - private static Map> buildTestMessages() { - ImmutableMap.Builder> builder = ImmutableMap.builder(); - - // Empty message - builder.put( - Message.builder().setTopic("test-topic").build(), - ImmutableMap.of("topic", "test-topic")); - - // Notification message - builder.put( - Message.builder() - .setNotification(new Notification("test title", "test body")) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "notification", ImmutableMap.of("title", "test title", "body", "test body"))); - - // Data message - builder.put( - Message.builder() - .putData("k1", "v1") - .putData("k2", "v2") - .putAllData(ImmutableMap.of("k3", "v3", "k4", "v4")) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4"))); - - // Android message - builder.put( - Message.builder() - .setAndroidConfig(AndroidConfig.builder() - .setPriority(AndroidConfig.Priority.HIGH) - .setTtl(TimeUnit.SECONDS.toMillis(123)) - .setRestrictedPackageName("test-package") - .setCollapseKey("test-key") - .setNotification(AndroidNotification.builder() - .setClickAction("test-action") - .setTitle("test-title") - .setBody("test-body") - .setIcon("test-icon") - .setColor("#112233") - .setTag("test-tag") - .setSound("test-sound") - .setTitleLocalizationKey("test-title-key") - .setBodyLocalizationKey("test-body-key") - .addTitleLocalizationArg("t-arg1") - .addAllTitleLocalizationArgs(ImmutableList.of("t-arg2", "t-arg3")) - .addBodyLocalizationArg("b-arg1") - .addAllBodyLocalizationArgs(ImmutableList.of("b-arg2", "b-arg3")) - .setChannelId("channel-id") - .build()) - .build()) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "android", ImmutableMap.of( - "priority", "high", - "collapse_key", "test-key", - "ttl", "123s", - "restricted_package_name", "test-package", - "notification", ImmutableMap.builder() - .put("click_action", "test-action") - .put("title", "test-title") - .put("body", "test-body") - .put("icon", "test-icon") - .put("color", "#112233") - .put("tag", "test-tag") - .put("sound", "test-sound") - .put("title_loc_key", "test-title-key") - .put("title_loc_args", ImmutableList.of("t-arg1", "t-arg2", "t-arg3")) - .put("body_loc_key", "test-body-key") - .put("body_loc_args", ImmutableList.of("b-arg1", "b-arg2", "b-arg3")) - .put("channel_id", "channel-id") - .build() - ) - )); - - // APNS message - builder.put( - Message.builder() - .setApnsConfig(ApnsConfig.builder() - .putHeader("h1", "v1") - .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .putAllCustomData(ImmutableMap.of("k1", "v1", "k2", true)) - .setAps(Aps.builder() - .setBadge(42) - .setAlert(ApsAlert.builder() - .setTitle("test-title") - .setSubtitle("test-subtitle") - .setBody("test-body") - .build()) - .build()) - .build()) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "apns", ImmutableMap.of( - "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), - "payload", ImmutableMap.of("k1", "v1", "k2", true, - "aps", ImmutableMap.of("badge", new BigDecimal(42), - "alert", ImmutableMap.of( - "title", "test-title", "subtitle", "test-subtitle", - "body", "test-body")))) - )); - - // Webpush message (no notification) - builder.put( - Message.builder() - .setWebpushConfig(WebpushConfig.builder() - .putHeader("h1", "v1") - .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .putData("k1", "v1") - .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) - .build()) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "webpush", ImmutableMap.of( - "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), - "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3")) - )); - - // Webpush message (simple notification) - builder.put( - Message.builder() - .setWebpushConfig(WebpushConfig.builder() - .putHeader("h1", "v1") - .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .putData("k1", "v1") - .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) - .setNotification(new WebpushNotification("test-title", "test-body", "test-icon")) - .build()) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "webpush", ImmutableMap.of( - "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), - "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), - "notification", ImmutableMap.of( - "title", "test-title", "body", "test-body", "icon", "test-icon")) - )); - - // Webpush message (all fields) - builder.put( - Message.builder() - .setWebpushConfig(WebpushConfig.builder() - .putHeader("h1", "v1") - .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .putData("k1", "v1") - .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) - .setNotification(WebpushNotification.builder() - .setTitle("test-title") - .setBody("test-body") - .setIcon("test-icon") - .setBadge("test-badge") - .setImage("test-image") - .setLanguage("test-lang") - .setTag("test-tag") - .setData(ImmutableList.of("arbitrary", "data")) - .setDirection(Direction.AUTO) - .setRenotify(true) - .setRequireInteraction(false) - .setSilent(true) - .setTimestampMillis(100L) - .setVibrate(new int[]{200, 100, 200}) - .addAction(new Action("action1", "title1")) - .addAllActions(ImmutableList.of(new Action("action2", "title2", "icon2"))) - .putCustomData("k4", "v4") - .putAllCustomData(ImmutableMap.of("k5", "v5", "k6", "v6")) - .build()) - .build()) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "webpush", ImmutableMap.of( - "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), - "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), - "notification", ImmutableMap.builder() - .put("title", "test-title") - .put("body", "test-body") - .put("icon", "test-icon") - .put("badge", "test-badge") - .put("image", "test-image") - .put("lang", "test-lang") - .put("tag", "test-tag") - .put("data", ImmutableList.of("arbitrary", "data")) - .put("renotify", true) - .put("requireInteraction", false) - .put("silent", true) - .put("dir", "auto") - .put("timestamp", new BigDecimal(100)) - .put("vibrate", ImmutableList.of( - new BigDecimal(200), new BigDecimal(100), new BigDecimal(200))) - .put("actions", ImmutableList.of( - ImmutableMap.of("action", "action1", "title", "title1"), - ImmutableMap.of("action", "action2", "title", "title2", "icon", "icon2"))) - .put("k4", "v4") - .put("k5", "v5") - .put("k6", "v6") - .build()) - )); - - return builder.build(); - } } diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java new file mode 100644 index 000000000..7283f63d1 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java @@ -0,0 +1,390 @@ +package com.google.firebase.messaging; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +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.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.http.LowLevelHttpRequest; +import com.google.api.client.json.JsonParser; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.GenericFunction; +import com.google.firebase.testing.TestResponseInterceptor; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Test; + +public class InstanceIdClientTest { + + private static final String TEST_IID_SUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchAdd"; + + private static final String TEST_IID_UNSUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchRemove"; + + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + + private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); + + static { + for (int i = 0; i < 1001; i++) { + TOO_MANY_IDS.add("id" + i); + } + } + + private static final List INVALID_TOPIC_MGT_ARGS = ImmutableList.of( + new TopicMgtArgs(null, null), + new TopicMgtArgs(null, "test-topic"), + new TopicMgtArgs(ImmutableList.of(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), "test-topic"), + new TopicMgtArgs(TOO_MANY_IDS.build(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), null), + new TopicMgtArgs(ImmutableList.of("id"), ""), + new TopicMgtArgs(ImmutableList.of("id"), "foo*") + ); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testInvalidSubscribe() { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(new MockLowLevelHttpResponse(), interceptor); + + for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { + try { + messaging.subscribeToTopicAsync(args.registrationTokens, args.topic); + fail("No error thrown for invalid args"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testSubscribe() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessaging messaging = initMessaging(response, interceptor); + + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), + "test-topic").get(); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), + "/topics/test-topic"); + } + } + ); + + for (GenericFunction fn : functions) { + response.setContent(responseString); + TopicManagementResponse result = fn.call(); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + } + + @Test + public void testSubscribeError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + for (int statusCode : HTTP_ERRORS) { + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + try { + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + } + + @Test + public void testSubscribeUnknownError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("{}"); + try { + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeTransportError() throws Exception { + FirebaseMessaging messaging = initFaultyTransportMessaging(); + try { + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testInvalidUnsubscribe() { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(new MockLowLevelHttpResponse(), interceptor); + + for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { + try { + messaging.unsubscribeFromTopicAsync(args.registrationTokens, args.topic); + fail("No error thrown for invalid args"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testUnsubscribe() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessaging messaging = initMessaging(response, interceptor); + + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), + "test-topic").get(); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), + "/topics/test-topic"); + } + } + ); + + for (GenericFunction fn : functions) { + response.setContent(responseString); + TopicManagementResponse result = fn.call(); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + } + + @Test + public void testUnsubscribeError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + for (int statusCode : HTTP_ERRORS) { + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + } + + @Test + public void testUnsubscribeUnknownError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("{}"); + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeTransportError() throws Exception { + FirebaseMessaging messaging = initFaultyTransportMessaging(); + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + private static String getTopicManagementErrorCode(int statusCode) { + String code = InstanceIdClient.IID_ERROR_CODES.get(statusCode); + if (code == null) { + code = "unknown-error"; + } + return code; + } + + private static FirebaseMessaging initMessaging( + final MockLowLevelHttpResponse mockResponse, + final HttpResponseInterceptor interceptor) { + + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + final FirebaseApp app = FirebaseApp.initializeApp(options); + + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(Suppliers.ofInstance(null)) + .setInstanceIdClient(new Supplier() { + @Override + public InstanceIdClient get() { + return new InstanceIdClient(app, interceptor); + } + }) + .build(); + } + + private void checkTopicManagementRequest( + HttpRequest request, TopicManagementResponse result) throws IOException { + assertEquals(1, result.getSuccessCount()); + assertEquals(1, result.getFailureCount()); + assertEquals(1, result.getErrors().size()); + assertEquals(1, result.getErrors().get(0).getIndex()); + assertEquals("unknown-error", result.getErrors().get(0).getReason()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + Map parsed = new HashMap<>(); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + parser.parseAndClose(parsed); + assertEquals(2, parsed.size()); + assertEquals("/topics/test-topic", parsed.get("to")); + assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); + } + + private void checkTopicManagementRequestHeader( + HttpRequest request, String expectedUrl) { + assertEquals("POST", request.getRequestMethod()); + assertEquals(expectedUrl, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } + + private static class TopicMgtArgs { + private final List registrationTokens; + private final String topic; + + TopicMgtArgs(List registrationTokens, String topic) { + this.registrationTokens = registrationTokens; + this.topic = topic; + } + } + + private static FirebaseMessaging initFaultyTransportMessaging() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(new FailingHttpTransport()) + .build(); + final FirebaseApp app = FirebaseApp.initializeApp(options); + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(Suppliers.ofInstance(null)) + .setInstanceIdClient(new Supplier() { + @Override + public InstanceIdClient get() { + return new InstanceIdClient(app, null); + } + }) + .build(); + } + + private static class FailingHttpTransport extends HttpTransport { + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw new IOException("transport error"); + } + } + +} diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index 3e33a52ea..b324df612 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -24,6 +24,7 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpRequest; @@ -142,4 +143,13 @@ public static HttpRequest createRequest(MockLowLevelHttpRequest request) throws HttpRequestFactory requestFactory = transport.createRequestFactory(); return requestFactory.buildPostRequest(TEST_URL, new EmptyContent()); } + + public static HttpTransport faultyHttpTransport() { + return new HttpTransport() { + @Override + protected LowLevelHttpRequest buildRequest(String s, String s1) throws IOException { + throw new IOException("transport error"); + } + }; + } } From 83fb7770f9fae3ecc3c8c7057632b7957a467937 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 14 Mar 2019 18:36:02 -0700 Subject: [PATCH 2/9] Improving FCM test coverage --- .../firebase/messaging/FirebaseMessaging.java | 45 +- .../FirebaseMessagingClientImplTest.java | 21 + .../messaging/FirebaseMessagingTest.java | 397 +++++++++++++++--- 3 files changed, 369 insertions(+), 94 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 1a056958c..d1e4acea4 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -19,20 +19,16 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.core.ApiFuture; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; import java.util.List; @@ -49,11 +45,10 @@ public class FirebaseMessaging { static final String UNKNOWN_ERROR = "unknown-error"; private final FirebaseApp app; - private final Supplier messagingClient; + private final Supplier messagingClient; private final Supplier instanceIdClient; - @VisibleForTesting - FirebaseMessaging(Builder builder) { + private FirebaseMessaging(Builder builder) { this.app = checkNotNull(builder.firebaseApp); this.messagingClient = Suppliers.memoize(builder.messagingClient); this.instanceIdClient = Suppliers.memoize(builder.instanceIdClient); @@ -378,7 +373,7 @@ protected TopicManagementResponse execute() throws FirebaseMessagingException { }; } - private static void checkRegistrationTokens(List registrationTokens) { + private void checkRegistrationTokens(List registrationTokens) { checkArgument(registrationTokens != null && !registrationTokens.isEmpty(), "registrationTokens list must not be null or empty"); checkArgument(registrationTokens.size() <= 1000, @@ -389,27 +384,13 @@ private static void checkRegistrationTokens(List registrationTokens) { } } - private static void checkTopic(String topic) { + private void checkTopic(String topic) { checkArgument(!Strings.isNullOrEmpty(topic), "topic must not be null or empty"); checkArgument(topic.matches("^(/topics/)?(private/)?[a-zA-Z0-9-_.~%]+$"), "invalid topic name"); } private static final String SERVICE_ID = FirebaseMessaging.class.getName(); - private static class FirebaseMessagingService extends FirebaseService { - - FirebaseMessagingService(FirebaseApp app) { - super(SERVICE_ID, FirebaseMessaging.fromApp(app)); - } - - @Override - public void destroy() { - // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseMessaging - // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, - // which will throw once the app is deleted. - } - } - private static FirebaseMessaging fromApp(final FirebaseApp app) { return FirebaseMessaging.builder() .setFirebaseApp(app) @@ -428,6 +409,20 @@ public InstanceIdClient get() { .build(); } + private static class FirebaseMessagingService extends FirebaseService { + + FirebaseMessagingService(FirebaseApp app) { + super(SERVICE_ID, FirebaseMessaging.fromApp(app)); + } + + @Override + public void destroy() { + // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseMessaging + // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, + // which will throw once the app is deleted. + } + } + static Builder builder() { return new Builder(); } @@ -435,7 +430,7 @@ static Builder builder() { static class Builder { private FirebaseApp firebaseApp; - private Supplier messagingClient; + private Supplier messagingClient; private Supplier instanceIdClient; private Builder() { } @@ -445,7 +440,7 @@ Builder setFirebaseApp(FirebaseApp firebaseApp) { return this; } - Builder setMessagingClient(Supplier messagingClient) { + Builder setMessagingClient(Supplier messagingClient) { this.messagingClient = messagingClient; return this; } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index c2fc60aa1..881a2347c 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -144,6 +144,27 @@ public void testSendTransportError() { } } + @Test + public void testSendSuccessResponseWithUnexpectedPayload() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent("not valid json"); + + try { + messaging.send(entry.getKey(), false); + fail("No error thrown for malformed response"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + @Test public void testSendErrorWithZeroContentResponse() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 438b41189..1a1b87ac6 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -1,20 +1,36 @@ package com.google.firebase.messaging; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; 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.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.testing.TestResponseInterceptor; +import java.util.List; +import java.util.concurrent.ExecutionException; import org.junit.After; import org.junit.Test; public class FirebaseMessagingTest { + private static final FirebaseOptions TEST_OPTIONS = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final Message EMPTY_MESSAGE = Message.builder() + .setTopic("test-topic") + .build(); + private static final FirebaseMessagingException TEST_EXCEPTION = + new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception()); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -22,36 +38,28 @@ public void tearDown() { @Test public void testGetInstance() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); + FirebaseApp.initializeApp(TEST_OPTIONS); FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + assertSame(messaging, FirebaseMessaging.getInstance()); } @Test public void testGetInstanceByApp() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "custom-app"); + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + assertSame(messaging, FirebaseMessaging.getInstance(app)); } @Test public void testPostDeleteApp() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "custom-app"); + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + app.delete(); + try { FirebaseMessaging.getInstance(app); fail("No error thrown for deleted app"); @@ -67,65 +75,316 @@ public void testNoProjectId() throws FirebaseMessagingException { .build(); FirebaseApp.initializeApp(options); FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + try { - messaging.send(Message.builder() - .setTopic("test-topic") - .build()); + messaging.send(EMPTY_MESSAGE); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { + String message = "Project ID is required to access messaging 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()); + } + } + + @Test + public void testSendNullMessage() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.send(null); + fail("No error thrown for null message"); + } catch (NullPointerException expected) { // expected } + + assertNull(client.lastMessage); } -// @Test -// public void testSendNullMessage() throws FirebaseMessagingException { -// TestResponseInterceptor interceptor = new TestResponseInterceptor(); -// FirebaseMessagingClient messaging = initDefaultMessaging(interceptor); -// try { -// messaging.send(null, false); -// fail("No error thrown for null message"); -// } catch (NullPointerException expected) { -// // expected -// } -// -// assertNull(interceptor.getResponse()); -// } -// -// @Test -// public void testSendAllWithNull() throws FirebaseMessagingException { -// FirebaseMessagingClient messaging = initDefaultMessaging(); -// try { -// messaging.sendAll(null, false); -// fail("No error thrown for null message list"); -// } catch (NullPointerException expected) { -// // expected -// } -// } -// -// @Test -// public void testSendAllWithEmptyList() throws FirebaseMessagingException { -// FirebaseMessagingClient messaging = initDefaultMessaging(); -// try { -// messaging.sendAll(ImmutableList.of(), false); -// fail("No error thrown for empty message list"); -// } catch (IllegalArgumentException expected) { -// // expected -// } -// } -// -// @Test -// public void testSendAllWithTooManyMessages() throws FirebaseMessagingException { -// FirebaseMessagingClient messaging = initDefaultMessaging(); -// ImmutableList.Builder listBuilder = ImmutableList.builder(); -// for (int i = 0; i < 101; i++) { -// listBuilder.add(Message.builder().setTopic("topic").build()); -// } -// try { -// messaging.sendAll(listBuilder.build(), false); -// fail("No error thrown for too many messages in the list"); -// } catch (IllegalArgumentException expected) { -// // expected -// } -// } + @Test + public void testSend() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + String messageId = messaging.send(EMPTY_MESSAGE); + + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); + } + @Test + public void testSendDryRun() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + String messageId = messaging.send(EMPTY_MESSAGE, true); + + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendFailure() { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.send(EMPTY_MESSAGE); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); + } + + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendAsync() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + String messageId = messaging.sendAsync(EMPTY_MESSAGE).get(); + + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendAsyncDryRun() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + String messageId = messaging.sendAsync(EMPTY_MESSAGE, true).get(); + + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendAsyncFailure() throws InterruptedException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendAsync(EMPTY_MESSAGE).get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendAllWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendAll(null); + fail("No error thrown for null message list"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(client.lastBatch); + } + + @Test + public void testSendAllWithEmptyList() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendAll(ImmutableList.of()); + fail("No error thrown for empty message list"); + } catch (IllegalArgumentException expected) { + // expected + } + + assertNull(client.lastBatch); + } + + @Test + public void testSendAllWithTooManyMessages() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (int i = 0; i < 101; i++) { + listBuilder.add(Message.builder().setTopic("topic").build()); + } + + try { + messaging.sendAll(listBuilder.build(), false); + fail("No error thrown for too many messages in the list"); + } catch (IllegalArgumentException expected) { + // expected + } + + assertNull(client.lastBatch); + } + + @Test + public void testSendAll() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAll(messages); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendAllDryRun() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAll(messages, true); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendAllFailure() { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + try { + messaging.sendAll(messages); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); + } + + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendAllAsync() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAllAsync(messages).get(); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendAllAsyncDryRun() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAllAsync(messages, true).get(); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendAllAsyncFailure() throws InterruptedException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + try { + messaging.sendAllAsync(messages).get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); + } + + private FirebaseMessaging getMessagingForSend( + Supplier supplier) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(supplier) + .setInstanceIdClient(Suppliers.ofInstance(null)) + .build(); + } + + private BatchResponse getBatchResponse(String messageId) { + SendResponse response = SendResponse.fromMessageId(messageId); + return new BatchResponse(ImmutableList.of(response)); + } + + private static class MockFirebaseMessagingClient implements FirebaseMessagingClient { + + private String messageId; + private BatchResponse batchResponse; + private FirebaseMessagingException exception; + + private Message lastMessage; + private List lastBatch; + private boolean isLastDryRun; + + private MockFirebaseMessagingClient( + String messageId, BatchResponse batchResponse, FirebaseMessagingException exception) { + this.messageId = messageId; + this.batchResponse = batchResponse; + this.exception = exception; + } + + static MockFirebaseMessagingClient fromMessageId(String messageId) { + return new MockFirebaseMessagingClient(messageId, null, null); + } + + static MockFirebaseMessagingClient fromBatchResponse(BatchResponse batchResponse) { + return new MockFirebaseMessagingClient(null, batchResponse, null); + } + + static MockFirebaseMessagingClient fromException(FirebaseMessagingException exception) { + return new MockFirebaseMessagingClient(null, null, exception); + } + + @Override + public String send(Message message, boolean dryRun) throws FirebaseMessagingException { + lastMessage = message; + isLastDryRun = dryRun; + if (exception != null) { + throw exception; + } + return messageId; + } + + @Override + public BatchResponse sendAll( + List messages, boolean dryRun) throws FirebaseMessagingException { + lastBatch = messages; + isLastDryRun = dryRun; + if (exception != null) { + throw exception; + } + return batchResponse; + } + } } From c3d9176e0ac069f7bf45564acd6fe517582ecb14 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 14 Mar 2019 18:52:36 -0700 Subject: [PATCH 3/9] More unit tests --- .../firebase/messaging/FirebaseMessaging.java | 8 +- .../messaging/FirebaseMessagingTest.java | 130 ++++++++++++++++++ .../messaging/InstanceIdClientTest.java | 12 +- 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index d1e4acea4..8a1092947 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -131,7 +131,7 @@ public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { private CallableOperation sendOp( final Message message, final boolean dryRun) { checkNotNull(message, "message must not be null"); - final FirebaseMessagingClient messagingClient = this.messagingClient.get(); + final FirebaseMessagingClient messagingClient = getMessagingClient(); return new CallableOperation() { @Override protected String execute() throws FirebaseMessagingException { @@ -285,7 +285,7 @@ private CallableOperation sendAllOp( checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); checkArgument(immutableMessages.size() <= 100, "messages list must not contain more than 100 elements"); - final FirebaseMessagingClient messagingClient = this.messagingClient.get(); + final FirebaseMessagingClient messagingClient = getMessagingClient(); return new CallableOperation() { @Override protected BatchResponse execute() throws FirebaseMessagingException { @@ -294,6 +294,10 @@ protected BatchResponse execute() throws FirebaseMessagingException { }; } + FirebaseMessagingClient getMessagingClient() { + return messagingClient.get(); + } + /** * Subscribes a list of registration tokens to a topic. * diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 1a1b87ac6..2f89432a7 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -28,6 +28,10 @@ public class FirebaseMessagingTest { private static final Message EMPTY_MESSAGE = Message.builder() .setTopic("test-topic") .build(); + private static final MulticastMessage TEST_MULTICAST_MESSAGE = MulticastMessage.builder() + .addToken("test-fcm-token1") + .addToken("test-fcm-token2") + .build(); private static final FirebaseMessagingException TEST_EXCEPTION = new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception()); @@ -54,6 +58,19 @@ public void testGetInstanceByApp() { assertSame(messaging, FirebaseMessaging.getInstance(app)); } + @Test + public void testDefaultMessagingClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + + FirebaseMessagingClient client = messaging.getMessagingClient(); + + assertTrue(client instanceof FirebaseMessagingClientImpl); + assertSame(client, messaging.getMessagingClient()); + String expectedUrl = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + assertEquals(expectedUrl, ((FirebaseMessagingClientImpl) client).getFcmSendUrl()); + } + @Test public void testPostDeleteApp() { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); @@ -322,6 +339,119 @@ public void testSendAllAsyncFailure() throws InterruptedException { assertFalse(client.isLastDryRun); } + @Test + public void testSendMulticastWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendMulticast(null); + fail("No error thrown for null multicast message"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(client.lastBatch); + } + + @Test + public void testSendMulticast() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendMulticast(TEST_MULTICAST_MESSAGE); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendMulticastDryRun() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendMulticast(TEST_MULTICAST_MESSAGE, true); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendMulticastFailure() { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendMulticast(TEST_MULTICAST_MESSAGE); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); + } + + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendMulticastAsync() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendMulticastAsyncDryRun() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE, true).get(); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendMulticastAsyncFailure() throws InterruptedException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); + } + private FirebaseMessaging getMessagingForSend( Supplier supplier) { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java index 7283f63d1..e71fd9199 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java @@ -11,6 +11,7 @@ import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonParser; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; @@ -292,6 +293,16 @@ public void testUnsubscribeTransportError() throws Exception { } } + @Test(expected = IllegalArgumentException.class) + public void testTopicManagementResponseWithNullList() { + new TopicManagementResponse(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testTopicManagementResponseWithEmptyList() { + new TopicManagementResponse(ImmutableList.of()); + } + private static String getTopicManagementErrorCode(int statusCode) { String code = InstanceIdClient.IID_ERROR_CODES.get(statusCode); if (code == null) { @@ -386,5 +397,4 @@ protected LowLevelHttpRequest buildRequest(String method, String url) throws IOE throw new IOException("transport error"); } } - } From 086b0c374bbbd09caadcedabbb5aa9dcc4bb6620 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 14 Mar 2019 21:41:40 -0700 Subject: [PATCH 4/9] Made InstanceIdClient into an interface --- .../firebase/messaging/FirebaseMessaging.java | 18 +- .../firebase/messaging/InstanceIdClient.java | 159 +------ .../messaging/InstanceIdClientImpl.java | 192 +++++++++ .../messaging/FirebaseMessagingTest.java | 76 ++++ .../messaging/InstanceIdClientImplTest.java | 288 +++++++++++++ .../messaging/InstanceIdClientTest.java | 400 ------------------ 6 files changed, 570 insertions(+), 563 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java create mode 100644 src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java delete mode 100644 src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 8a1092947..d6b15cfd4 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -46,7 +46,7 @@ public class FirebaseMessaging { private final FirebaseApp app; private final Supplier messagingClient; - private final Supplier instanceIdClient; + private final Supplier instanceIdClient; private FirebaseMessaging(Builder builder) { this.app = checkNotNull(builder.firebaseApp); @@ -328,7 +328,7 @@ private CallableOperation s final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); - final InstanceIdClient instanceIdClient = this.instanceIdClient.get(); + final InstanceIdClient instanceIdClient = getInstanceIdClient(); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { @@ -368,7 +368,7 @@ private CallableOperation u final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); - final InstanceIdClient instanceIdClient = this.instanceIdClient.get(); + final InstanceIdClient instanceIdClient = getInstanceIdClient(); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { @@ -377,6 +377,10 @@ protected TopicManagementResponse execute() throws FirebaseMessagingException { }; } + InstanceIdClient getInstanceIdClient() { + return this.instanceIdClient.get(); + } + private void checkRegistrationTokens(List registrationTokens) { checkArgument(registrationTokens != null && !registrationTokens.isEmpty(), "registrationTokens list must not be null or empty"); @@ -406,8 +410,8 @@ public FirebaseMessagingClient get() { }) .setInstanceIdClient(new Supplier() { @Override - public InstanceIdClient get() { - return new InstanceIdClient(app, null); + public InstanceIdClientImpl get() { + return InstanceIdClientImpl.fromApp(app); } }) .build(); @@ -435,7 +439,7 @@ static class Builder { private FirebaseApp firebaseApp; private Supplier messagingClient; - private Supplier instanceIdClient; + private Supplier instanceIdClient; private Builder() { } @@ -449,7 +453,7 @@ Builder setMessagingClient(Supplier messaging return this; } - Builder setInstanceIdClient(Supplier instanceIdClient) { + Builder setInstanceIdClient(Supplier instanceIdClient) { this.instanceIdClient = instanceIdClient; return this; } diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java index 017029094..9cdfe292a 100644 --- a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java @@ -1,166 +1,13 @@ -/* - * Copyright 2019 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.messaging; -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.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.api.client.json.JsonParser; -import com.google.api.client.util.Key; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -import com.google.firebase.FirebaseApp; -import com.google.firebase.internal.ApiClientUtils; -import com.google.firebase.internal.Nullable; - -import java.io.IOException; import java.util.List; -import java.util.Map; - -/** - * A helper class for interacting with the Firebase Instance ID service. Implements the FCM - * topic management functionality. - */ -final class InstanceIdClient { - - private static final String IID_HOST = "https://iid.googleapis.com"; - - private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; - - private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; - - static final Map IID_ERROR_CODES = - ImmutableMap.builder() - .put(400, "invalid-argument") - .put(401, "authentication-error") - .put(403, "authentication-error") - .put(500, FirebaseMessaging.INTERNAL_ERROR) - .put(503, "server-unavailable") - .build(); - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; - private final HttpResponseInterceptor responseInterceptor; - - InstanceIdClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { - this.requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.responseInterceptor = responseInterceptor; - } +interface InstanceIdClient { TopicManagementResponse subscribeToTopic( - String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } - } + String topic, List registrationTokens) throws FirebaseMessagingException; TopicManagementResponse unsubscribeFromTopic( - String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } - } - - private TopicManagementResponse sendInstanceIdRequest( - String topic, List registrationTokens, String path) throws IOException { - String url = String.format("%s/%s", IID_HOST, path); - Map payload = ImmutableMap.of( - "to", getPrefixedTopic(topic), - "registration_tokens", registrationTokens - ); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - response = request.execute(); - - JsonParser parser = jsonFactory.createJsonParser(response.getContent()); - InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); - parser.parse(parsedResponse); - return new TopicManagementResponse(parsedResponse.results); - } finally { - ApiClientUtils.disconnectQuietly(response); - } - } - - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - return newException(response, e); - } - - private String getPrefixedTopic(String topic) { - if (topic.startsWith("/topics/")) { - return topic; - } else { - return "/topics/" + topic; - } - } - - private static FirebaseMessagingException newException( - InstanceIdServiceErrorResponse response, HttpResponseException e) { - // Infer error code from HTTP status - String code = IID_ERROR_CODES.get(e.getStatusCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - String msg = response.error; - if (Strings.isNullOrEmpty(msg)) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } - return new FirebaseMessagingException(code, msg, e); - } - - private static class InstanceIdServiceResponse { - @Key("results") - private List results; - } + String topic, List registrationTokens) throws FirebaseMessagingException; - private static class InstanceIdServiceErrorResponse { - @Key("error") - private String error; - } } diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java new file mode 100644 index 000000000..15f8158a5 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java @@ -0,0 +1,192 @@ +/* + * Copyright 2019 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.messaging; + +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.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.api.client.json.JsonParser; +import com.google.api.client.util.Key; +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.internal.ApiClientUtils; +import com.google.firebase.internal.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with the Firebase Instance ID service. Implements the FCM + * topic management functionality. + */ +final class InstanceIdClientImpl implements InstanceIdClient { + + private static final String IID_HOST = "https://iid.googleapis.com"; + + private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; + + private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; + + static final Map IID_ERROR_CODES = + ImmutableMap.builder() + .put(400, "invalid-argument") + .put(401, "authentication-error") + .put(403, "authentication-error") + .put(500, FirebaseMessaging.INTERNAL_ERROR) + .put(503, "server-unavailable") + .build(); + + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final HttpResponseInterceptor responseInterceptor; + + InstanceIdClientImpl(HttpRequestFactory requestFactory, JsonFactory jsonFactory) { + this(requestFactory, jsonFactory, null); + } + + InstanceIdClientImpl( + HttpRequestFactory requestFactory, + JsonFactory jsonFactory, + @Nullable HttpResponseInterceptor responseInterceptor) { + this.requestFactory = checkNotNull(requestFactory); + this.jsonFactory = checkNotNull(jsonFactory); + this.responseInterceptor = responseInterceptor; + } + + static InstanceIdClientImpl fromApp(FirebaseApp app) { + return new InstanceIdClientImpl( + ApiClientUtils.newAuthorizedRequestFactory(app), + app.getOptions().getJsonFactory()); + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; + } + + @VisibleForTesting + JsonFactory getJsonFactory() { + return jsonFactory; + } + + public TopicManagementResponse subscribeToTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + try { + return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); + } + } + + public TopicManagementResponse unsubscribeFromTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + try { + return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); + } + } + + private TopicManagementResponse sendInstanceIdRequest( + String topic, List registrationTokens, String path) throws IOException { + String url = String.format("%s/%s", IID_HOST, path); + Map payload = ImmutableMap.of( + "to", getPrefixedTopic(topic), + "registration_tokens", registrationTokens + ); + HttpResponse response = null; + try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + request.getHeaders().set("access_token_auth", "true"); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(responseInterceptor); + response = request.execute(); + + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); + parser.parse(parsedResponse); + return new TopicManagementResponse(parsedResponse.results); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { + InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + } + return newException(response, e); + } + + private String getPrefixedTopic(String topic) { + if (topic.startsWith("/topics/")) { + return topic; + } else { + return "/topics/" + topic; + } + } + + private static FirebaseMessagingException newException( + InstanceIdServiceErrorResponse response, HttpResponseException e) { + // Infer error code from HTTP status + String code = IID_ERROR_CODES.get(e.getStatusCode()); + if (code == null) { + code = FirebaseMessaging.UNKNOWN_ERROR; + } + String msg = response.error; + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } + return new FirebaseMessagingException(code, msg, e); + } + + private static class InstanceIdServiceResponse { + @Key("results") + private List results; + } + + private static class InstanceIdServiceErrorResponse { + @Key("error") + private String error; + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 2f89432a7..6b13e7f88 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -14,6 +14,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestResponseInterceptor; import java.util.List; import java.util.concurrent.ExecutionException; import org.junit.After; @@ -35,6 +36,25 @@ public class FirebaseMessagingTest { private static final FirebaseMessagingException TEST_EXCEPTION = new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception()); + private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); + + static { + for (int i = 0; i < 1001; i++) { + TOO_MANY_IDS.add("id" + i); + } + } + + private static final List INVALID_TOPIC_MGT_ARGS = ImmutableList.of( + new TopicMgtArgs(null, null), + new TopicMgtArgs(null, "test-topic"), + new TopicMgtArgs(ImmutableList.of(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), "test-topic"), + new TopicMgtArgs(TOO_MANY_IDS.build(), "test-topic"), + new TopicMgtArgs(ImmutableList.of(""), null), + new TopicMgtArgs(ImmutableList.of("id"), ""), + new TopicMgtArgs(ImmutableList.of("id"), "foo*") + ); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -452,6 +472,42 @@ public void testSendMulticastAsyncFailure() throws InterruptedException { assertFalse(client.isLastDryRun); } + @Test + public void testInvalidSubscribe() throws FirebaseMessagingException { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForTopicManagement( + Suppliers.ofInstance(null)); + + for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { + try { + messaging.subscribeToTopic(args.registrationTokens, args.topic); + fail("No error thrown for invalid args"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testInvalidUnsubscribe() throws FirebaseMessagingException { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForTopicManagement( + Suppliers.ofInstance(null)); + + for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { + try { + messaging.unsubscribeFromTopic(args.registrationTokens, args.topic); + fail("No error thrown for invalid args"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + assertNull(interceptor.getResponse()); + } + private FirebaseMessaging getMessagingForSend( Supplier supplier) { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); @@ -462,6 +518,16 @@ private FirebaseMessaging getMessagingForSend( .build(); } + private FirebaseMessaging getMessagingForTopicManagement( + Supplier supplier) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(Suppliers.ofInstance(null)) + .setInstanceIdClient(supplier) + .build(); + } + private BatchResponse getBatchResponse(String messageId) { SendResponse response = SendResponse.fromMessageId(messageId); return new BatchResponse(ImmutableList.of(response)); @@ -517,4 +583,14 @@ public BatchResponse sendAll( return batchResponse; } } + + private static class TopicMgtArgs { + private final List registrationTokens; + private final String topic; + + TopicMgtArgs(List registrationTokens, String topic) { + this.registrationTokens = registrationTokens; + this.topic = topic; + } + } } diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java new file mode 100644 index 000000000..f6325ad7e --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -0,0 +1,288 @@ +package com.google.firebase.messaging; + +import static org.junit.Assert.assertEquals; +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.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonParser; +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.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Test; + +public class InstanceIdClientImplTest { + + private static final String TEST_IID_SUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchAdd"; + + private static final String TEST_IID_UNSUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchRemove"; + + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testSubscribe() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient messaging = initMessaging(response, interceptor); + TopicManagementResponse result = messaging.subscribeToTopic( + "test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testSubscribeWithPrefixedTopic() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient messaging = initMessaging(response, interceptor); + TopicManagementResponse result = messaging.subscribeToTopic( + "/topics/test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testSubscribeError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + for (int statusCode : HTTP_ERRORS) { + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + try { + messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + } + + @Test + public void testSubscribeUnknownError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("{}"); + try { + messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeTransportError() { + InstanceIdClient messaging = initFaultyTransportMessaging(); + try { + messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testUnsubscribe() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient messaging = initMessaging(response, interceptor); + + TopicManagementResponse result = messaging.unsubscribeFromTopic( + "test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testUnsubscribeWithPrefixedTopic() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient messaging = initMessaging(response, interceptor); + + TopicManagementResponse result = messaging.unsubscribeFromTopic( + "/topics/test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testUnsubscribeError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + for (int statusCode : HTTP_ERRORS) { + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + try { + messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + } + + @Test + public void testUnsubscribeUnknownError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("{}"); + try { + messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeTransportError() { + InstanceIdClient messaging = initFaultyTransportMessaging(); + try { + messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testFromApp() throws IOException { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + try { + InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app); + + 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(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTopicManagementResponseWithNullList() { + new TopicManagementResponse(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testTopicManagementResponseWithEmptyList() { + new TopicManagementResponse(ImmutableList.of()); + } + + private static String getTopicManagementErrorCode(int statusCode) { + String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode); + if (code == null) { + code = "unknown-error"; + } + return code; + } + + private static InstanceIdClientImpl initMessaging( + final MockLowLevelHttpResponse mockResponse, + final HttpResponseInterceptor interceptor) { + + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + return new InstanceIdClientImpl( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + interceptor); + } + + private void checkTopicManagementRequest( + HttpRequest request, TopicManagementResponse result) throws IOException { + assertEquals(1, result.getSuccessCount()); + assertEquals(1, result.getFailureCount()); + assertEquals(1, result.getErrors().size()); + assertEquals(1, result.getErrors().get(0).getIndex()); + assertEquals("unknown-error", result.getErrors().get(0).getReason()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + Map parsed = new HashMap<>(); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + parser.parseAndClose(parsed); + assertEquals(2, parsed.size()); + assertEquals("/topics/test-topic", parsed.get("to")); + assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); + } + + private void checkTopicManagementRequestHeader( + HttpRequest request, String expectedUrl) { + assertEquals("POST", request.getRequestMethod()); + assertEquals(expectedUrl, request.getUrl().toString()); + } + + private static InstanceIdClient initFaultyTransportMessaging() { + return new InstanceIdClientImpl( + TestUtils.faultyHttpTransport().createRequestFactory(), + Utils.getDefaultJsonFactory()); + } +} diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java deleted file mode 100644 index e71fd9199..000000000 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientTest.java +++ /dev/null @@ -1,400 +0,0 @@ -package com.google.firebase.messaging; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -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.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.http.LowLevelHttpRequest; -import com.google.api.client.json.GenericJson; -import com.google.api.client.json.JsonParser; -import com.google.api.client.testing.http.MockHttpTransport; -import com.google.api.client.testing.http.MockLowLevelHttpResponse; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableList; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.testing.GenericFunction; -import com.google.firebase.testing.TestResponseInterceptor; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import org.junit.After; -import org.junit.Test; - -public class InstanceIdClientTest { - - private static final String TEST_IID_SUBSCRIBE_URL = - "https://iid.googleapis.com/iid/v1:batchAdd"; - - private static final String TEST_IID_UNSUBSCRIBE_URL = - "https://iid.googleapis.com/iid/v1:batchRemove"; - - private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); - - private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); - - static { - for (int i = 0; i < 1001; i++) { - TOO_MANY_IDS.add("id" + i); - } - } - - private static final List INVALID_TOPIC_MGT_ARGS = ImmutableList.of( - new TopicMgtArgs(null, null), - new TopicMgtArgs(null, "test-topic"), - new TopicMgtArgs(ImmutableList.of(), "test-topic"), - new TopicMgtArgs(ImmutableList.of(""), "test-topic"), - new TopicMgtArgs(TOO_MANY_IDS.build(), "test-topic"), - new TopicMgtArgs(ImmutableList.of(""), null), - new TopicMgtArgs(ImmutableList.of("id"), ""), - new TopicMgtArgs(ImmutableList.of("id"), "foo*") - ); - - @After - public void tearDown() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - } - - @Test - public void testInvalidSubscribe() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(new MockLowLevelHttpResponse(), interceptor); - - for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { - try { - messaging.subscribeToTopicAsync(args.registrationTokens, args.topic); - fail("No error thrown for invalid args"); - } catch (IllegalArgumentException expected) { - // expected - } - } - - assertNull(interceptor.getResponse()); - } - - @Test - public void testSubscribe() throws Exception { - final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), - "test-topic").get(); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), - "/topics/test-topic"); - } - } - ); - - for (GenericFunction fn : functions) { - response.setContent(responseString); - TopicManagementResponse result = fn.call(); - checkTopicManagementRequestHeader( - interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - checkTopicManagementRequest(interceptor.getLastRequest(), result); - } - } - - @Test - public void testSubscribeError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - } - } - - @Test - public void testSubscribeUnknownError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - } - - @Test - public void testSubscribeTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); - } - } - - @Test - public void testInvalidUnsubscribe() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(new MockLowLevelHttpResponse(), interceptor); - - for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { - try { - messaging.unsubscribeFromTopicAsync(args.registrationTokens, args.topic); - fail("No error thrown for invalid args"); - } catch (IllegalArgumentException expected) { - // expected - } - } - - assertNull(interceptor.getResponse()); - } - - @Test - public void testUnsubscribe() throws Exception { - final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), - "test-topic").get(); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), - "/topics/test-topic"); - } - } - ); - - for (GenericFunction fn : functions) { - response.setContent(responseString); - TopicManagementResponse result = fn.call(); - checkTopicManagementRequestHeader( - interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - checkTopicManagementRequest(interceptor.getLastRequest(), result); - } - } - - @Test - public void testUnsubscribeError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - } - } - - @Test - public void testUnsubscribeUnknownError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - } - - @Test - public void testUnsubscribeTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); - } - } - - @Test(expected = IllegalArgumentException.class) - public void testTopicManagementResponseWithNullList() { - new TopicManagementResponse(null); - } - - @Test(expected = IllegalArgumentException.class) - public void testTopicManagementResponseWithEmptyList() { - new TopicManagementResponse(ImmutableList.of()); - } - - private static String getTopicManagementErrorCode(int statusCode) { - String code = InstanceIdClient.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; - } - return code; - } - - private static FirebaseMessaging initMessaging( - final MockLowLevelHttpResponse mockResponse, - final HttpResponseInterceptor interceptor) { - - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(mockResponse) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(options); - - return FirebaseMessaging.builder() - .setFirebaseApp(app) - .setMessagingClient(Suppliers.ofInstance(null)) - .setInstanceIdClient(new Supplier() { - @Override - public InstanceIdClient get() { - return new InstanceIdClient(app, interceptor); - } - }) - .build(); - } - - private void checkTopicManagementRequest( - HttpRequest request, TopicManagementResponse result) throws IOException { - assertEquals(1, result.getSuccessCount()); - assertEquals(1, result.getFailureCount()); - assertEquals(1, result.getErrors().size()); - assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("unknown-error", result.getErrors().get(0).getReason()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - Map parsed = new HashMap<>(); - JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); - parser.parseAndClose(parsed); - assertEquals(2, parsed.size()); - assertEquals("/topics/test-topic", parsed.get("to")); - assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); - } - - private void checkTopicManagementRequestHeader( - HttpRequest request, String expectedUrl) { - assertEquals("POST", request.getRequestMethod()); - assertEquals(expectedUrl, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - } - - private static class TopicMgtArgs { - private final List registrationTokens; - private final String topic; - - TopicMgtArgs(List registrationTokens, String topic) { - this.registrationTokens = registrationTokens; - this.topic = topic; - } - } - - private static FirebaseMessaging initFaultyTransportMessaging() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(new FailingHttpTransport()) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(options); - return FirebaseMessaging.builder() - .setFirebaseApp(app) - .setMessagingClient(Suppliers.ofInstance(null)) - .setInstanceIdClient(new Supplier() { - @Override - public InstanceIdClient get() { - return new InstanceIdClient(app, null); - } - }) - .build(); - } - - private static class FailingHttpTransport extends HttpTransport { - @Override - protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - throw new IOException("transport error"); - } - } -} From ac51fccd3e5a9c4a592ace72b55dc1fc18dad051 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 14 Mar 2019 22:16:00 -0700 Subject: [PATCH 5/9] Complete test coverage for FCM --- .../FirebaseMessagingClientImplTest.java | 62 +++++++- .../messaging/FirebaseMessagingTest.java | 138 ++++++++++++++++-- .../messaging/InstanceIdClientImplTest.java | 82 +++++++++++ 3 files changed, 269 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 881a2347c..a77d664ae 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -28,6 +28,7 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.http.HttpTransport; @@ -187,6 +188,28 @@ public void testSendErrorWithZeroContentResponse() { } } + @Test + public void testSendErrorWithMalformedResponse() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("not json"); + + try { + messaging.send(EMPTY_MESSAGE, false); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: not json", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + @Test public void testSendErrorWithDetails() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); @@ -283,6 +306,37 @@ public void testSendAllDryRun() throws Exception { assertSendBatchSuccess(responses, interceptor); } + @Test + public void testRequestInitializerAppliedToBatchRequests() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(getBatchResponse(MOCK_BATCH_SUCCESS_RESPONSE)) + .build(); + HttpRequestInitializer initializer = new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest httpRequest) { + httpRequest.getHeaders().set("x-custom-header", "test-value"); + } + }; + FirebaseMessagingClientImpl messaging = FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory(initializer)) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); + List messages = ImmutableList.of( + EMPTY_MESSAGE, EMPTY_MESSAGE + ); + + try { + messaging.sendAll(messages, false); + } finally { + HttpRequest request = interceptor.getLastRequest(); + assertEquals("test-value", request.getHeaders().get("x-custom-header")); + } + } + @Test public void testSendAllFailure() throws Exception { final TestResponseInterceptor interceptor = new TestResponseInterceptor(); @@ -518,10 +572,14 @@ private FirebaseMessagingClientImpl initMessagingClient( private FirebaseMessagingClientImpl initMessagingClientForBatchRequests( String responsePayload, TestResponseInterceptor interceptor) { - MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse() + MockLowLevelHttpResponse httpResponse = getBatchResponse(responsePayload); + return initMessagingClient(httpResponse, interceptor); + } + + private MockLowLevelHttpResponse getBatchResponse(String responsePayload) { + return new MockLowLevelHttpResponse() .setContentType("multipart/mixed; boundary=test_boundary") .setContent(responsePayload); - return initMessagingClient(httpResponse, interceptor); } private FirebaseMessagingClientImpl initMessagingClientWithFaultyTransport() { diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 6b13e7f88..ddcb64f68 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.json.GenericJson; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; @@ -14,7 +15,6 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.testing.TestResponseInterceptor; import java.util.List; import java.util.concurrent.ExecutionException; import org.junit.After; @@ -22,7 +22,7 @@ public class FirebaseMessagingTest { - private static final FirebaseOptions TEST_OPTIONS = new FirebaseOptions.Builder() + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -54,6 +54,8 @@ public class FirebaseMessagingTest { new TopicMgtArgs(ImmutableList.of("id"), ""), new TopicMgtArgs(ImmutableList.of("id"), "foo*") ); + private static final TopicManagementResponse TOPIC_MGT_RESPONSE = new TopicManagementResponse( + ImmutableList.of(new GenericJson())); @After public void tearDown() { @@ -91,6 +93,17 @@ public void testDefaultMessagingClient() { assertEquals(expectedUrl, ((FirebaseMessagingClientImpl) client).getFcmSendUrl()); } + @Test + public void testDefaultInstanceIdClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + + InstanceIdClient client = messaging.getInstanceIdClient(); + + assertTrue(client instanceof InstanceIdClientImpl); + assertSame(client, messaging.getInstanceIdClient()); + } + @Test public void testPostDeleteApp() { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); @@ -106,15 +119,15 @@ public void testPostDeleteApp() { } @Test - public void testNoProjectId() throws FirebaseMessagingException { - FirebaseOptions options = new FirebaseOptions.Builder() + public void testMessagingClientWithoutProjectId() { + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .build(); FirebaseApp.initializeApp(options); FirebaseMessaging messaging = FirebaseMessaging.getInstance(); try { - messaging.send(EMPTY_MESSAGE); + messaging.getMessagingClient(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { String message = "Project ID is required to access messaging service. Use a service " @@ -125,6 +138,20 @@ public void testNoProjectId() throws FirebaseMessagingException { } } + @Test + public void testInstanceIdClientWithoutProjectId() { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .build(); + FirebaseApp.initializeApp(options); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + + InstanceIdClient client = messaging.getInstanceIdClient(); + + assertTrue(client instanceof InstanceIdClientImpl); + assertSame(client, messaging.getInstanceIdClient()); + } + @Test public void testSendNullMessage() throws FirebaseMessagingException { MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); @@ -474,9 +501,9 @@ public void testSendMulticastAsyncFailure() throws InterruptedException { @Test public void testInvalidSubscribe() throws FirebaseMessagingException { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); FirebaseMessaging messaging = getMessagingForTopicManagement( - Suppliers.ofInstance(null)); + Suppliers.ofInstance(client)); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { @@ -485,16 +512,38 @@ public void testInvalidSubscribe() throws FirebaseMessagingException { } catch (IllegalArgumentException expected) { // expected } + assertNull(client.lastTopic); + assertNull(client.lastBatch); } + } + + @Test + public void testSubscribeToTopic() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.subscribeToTopic( + ImmutableList.of("id1", "id2"), "test-topic"); - assertNull(interceptor.getResponse()); + assertSame(TOPIC_MGT_RESPONSE, got); + } + + @Test + public void testSubscribeToTopicAsync() throws Exception { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.subscribeToTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertSame(TOPIC_MGT_RESPONSE, got); } @Test public void testInvalidUnsubscribe() throws FirebaseMessagingException { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); FirebaseMessaging messaging = getMessagingForTopicManagement( - Suppliers.ofInstance(null)); + Suppliers.ofInstance(client)); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { @@ -503,9 +552,31 @@ public void testInvalidUnsubscribe() throws FirebaseMessagingException { } catch (IllegalArgumentException expected) { // expected } + assertNull(client.lastTopic); + assertNull(client.lastBatch); } + } + + @Test + public void testUnsubscribeFromTopic() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.unsubscribeFromTopic( + ImmutableList.of("id1", "id2"), "test-topic"); - assertNull(interceptor.getResponse()); + assertSame(TOPIC_MGT_RESPONSE, got); + } + + @Test + public void testUnsubscribeFromTopicAsync() throws Exception { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.unsubscribeFromTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertSame(TOPIC_MGT_RESPONSE, got); } private FirebaseMessaging getMessagingForSend( @@ -584,6 +655,51 @@ public BatchResponse sendAll( } } + private static class MockInstanceIdClient implements InstanceIdClient { + + private TopicManagementResponse response; + private FirebaseMessagingException exception; + + private String lastTopic; + private List lastBatch; + + private MockInstanceIdClient( + TopicManagementResponse response, FirebaseMessagingException exception) { + this.response = response; + this.exception = exception; + } + + static MockInstanceIdClient fromResponse(TopicManagementResponse response) { + return new MockInstanceIdClient(response, null); + } + + static MockInstanceIdClient fromException(FirebaseMessagingException exception) { + return new MockInstanceIdClient(null, exception); + } + + @Override + public TopicManagementResponse subscribeToTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + this.lastTopic = topic; + this.lastBatch = registrationTokens; + if (exception != null) { + throw exception; + } + return response; + } + + @Override + public TopicManagementResponse unsubscribeFromTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + this.lastTopic = topic; + this.lastBatch = registrationTokens; + if (exception != null) { + throw exception; + } + return response; + } + } + private static class TopicMgtArgs { private final List registrationTokens; private final String topic; diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index f6325ad7e..e4554380d 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -110,6 +110,42 @@ public void testSubscribeUnknownError() { checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); } + @Test + public void testSubscribeMalformedError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("not json"); + try { + messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: not json", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeZeroContentError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setZeroContent(); + try { + messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: null", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + @Test public void testSubscribeTransportError() { InstanceIdClient messaging = initFaultyTransportMessaging(); @@ -191,6 +227,42 @@ public void testUnsubscribeUnknownError() { checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); } + @Test + public void testUnsubscribeMalformedError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("not json"); + try { + messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: not json", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeZeroContentError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setZeroContent(); + try { + messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: null", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + @Test public void testUnsubscribeTransportError() { InstanceIdClient messaging = initFaultyTransportMessaging(); @@ -204,6 +276,16 @@ public void testUnsubscribeTransportError() { } } + @Test(expected = NullPointerException.class) + public void testRequestFactoryIsNull() { + new InstanceIdClientImpl(null, Utils.getDefaultJsonFactory()); + } + + @Test(expected = NullPointerException.class) + public void testJsonFactoryIsNull() { + new InstanceIdClientImpl(Utils.getDefaultTransport().createRequestFactory(), null); + } + @Test public void testFromApp() throws IOException { FirebaseOptions options = new FirebaseOptions.Builder() From 70493b8b8783a3f764d50609bde7052b2c16be7c Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 15 Mar 2019 14:32:34 -0700 Subject: [PATCH 6/9] Cleaned up the tests --- .../firebase/messaging/FirebaseMessaging.java | 31 ++--- .../messaging/FirebaseMessagingClient.java | 21 ++++ .../FirebaseMessagingClientImpl.java | 36 +++--- .../firebase/messaging/InstanceIdClient.java | 17 +++ .../FirebaseMessagingClientImplTest.java | 108 +++++++--------- .../messaging/FirebaseMessagingTest.java | 57 ++++++++- .../messaging/InstanceIdClientImplTest.java | 119 ++++++++++-------- 7 files changed, 240 insertions(+), 149 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index d6b15cfd4..7330c27d3 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; @@ -294,6 +295,7 @@ protected BatchResponse execute() throws FirebaseMessagingException { }; } + @VisibleForTesting FirebaseMessagingClient getMessagingClient() { return messagingClient.get(); } @@ -377,6 +379,7 @@ protected TopicManagementResponse execute() throws FirebaseMessagingException { }; } + @VisibleForTesting InstanceIdClient getInstanceIdClient() { return this.instanceIdClient.get(); } @@ -399,6 +402,20 @@ private void checkTopic(String topic) { private static final String SERVICE_ID = FirebaseMessaging.class.getName(); + private static class FirebaseMessagingService extends FirebaseService { + + FirebaseMessagingService(FirebaseApp app) { + super(SERVICE_ID, FirebaseMessaging.fromApp(app)); + } + + @Override + public void destroy() { + // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseMessaging + // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, + // which will throw once the app is deleted. + } + } + private static FirebaseMessaging fromApp(final FirebaseApp app) { return FirebaseMessaging.builder() .setFirebaseApp(app) @@ -417,20 +434,6 @@ public InstanceIdClientImpl get() { .build(); } - private static class FirebaseMessagingService extends FirebaseService { - - FirebaseMessagingService(FirebaseApp app) { - super(SERVICE_ID, FirebaseMessaging.fromApp(app)); - } - - @Override - public void destroy() { - // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseMessaging - // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, - // which will throw once the app is deleted. - } - } - static Builder builder() { return new Builder(); } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java index 63d06b315..44d714473 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java @@ -2,10 +2,31 @@ import java.util.List; +/** + * An interface for sending Firebase Cloud Messaging (FCM) messages. + */ interface FirebaseMessagingClient { + /** + * Sends the given message with FCM. + * + * @param message A non-null {@link Message} to be sent. + * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. + * @return A message ID string. + * @throws FirebaseMessagingException If an error occurs while handing the message off to FCM for + * delivery. + */ String send(Message message, boolean dryRun) throws FirebaseMessagingException; + /** + * Sends all the messages in the given list with FCM. + * + * @param messages A non-null, non-empty list of messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. + */ BatchResponse sendAll(List messages, boolean dryRun) throws FirebaseMessagingException; } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index 0df0e0a65..743fddbbe 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -116,24 +116,6 @@ String getClientVersion() { return clientVersion; } - static FirebaseMessagingClientImpl fromApp(FirebaseApp app) { - String projectId = ImplFirebaseTrampolines.getProjectId(app); - checkArgument(!Strings.isNullOrEmpty(projectId), - "Project ID is required to access messaging 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 FirebaseMessagingClientImpl.builder() - .setProjectId(projectId) - .setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) - .setChildRequestFactory(ApiClientUtils.newUnauthorizedRequestFactory(app)) - .setJsonFactory(app.getOptions().getJsonFactory()) - .build(); - } - - static Builder builder() { - return new Builder(); - } - public String send(Message message, boolean dryRun) throws FirebaseMessagingException { try { return sendSingleRequest(message, dryRun); @@ -239,6 +221,24 @@ public void initialize(HttpRequest request) throws IOException { }; } + static FirebaseMessagingClientImpl fromApp(FirebaseApp app) { + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access messaging 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 FirebaseMessagingClientImpl.builder() + .setProjectId(projectId) + .setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) + .setChildRequestFactory(ApiClientUtils.newUnauthorizedRequestFactory(app)) + .setJsonFactory(app.getOptions().getJsonFactory()) + .build(); + } + + static Builder builder() { + return new Builder(); + } + static final class Builder { private String projectId; diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java index 9cdfe292a..8c1b70a3b 100644 --- a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java @@ -2,11 +2,28 @@ import java.util.List; +/** + * An interface for managing FCM topic subscriptions. + */ interface InstanceIdClient { + /** + * Subscribes a list of registration tokens to a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens. + * @param topic Name of the topic to subscribe to. May contain the {@code /topics/} prefix. + * @return A {@link TopicManagementResponse}. + */ TopicManagementResponse subscribeToTopic( String topic, List registrationTokens) throws FirebaseMessagingException; + /** + * Unsubscribes a list of registration tokens from a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens. + * @param topic Name of the topic to unsubscribe from. May contain the {@code /topics/} prefix. + * @return A {@link TopicManagementResponse}. + */ TopicManagementResponse unsubscribeFromTopic( String topic, List registrationTokens) throws FirebaseMessagingException; diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index a77d664ae..b5ab65ea4 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -72,17 +72,18 @@ public class FirebaseMessagingClientImplTest { private static final Message EMPTY_MESSAGE = Message.builder() .setTopic("test-topic") .build(); + private static final List MESSAGE_LIST = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE); @Test public void testSend() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); Map> testMessages = buildTestMessages(); for (Map.Entry> entry : testMessages.entrySet()) { response.setContent(MOCK_RESPONSE); - String resp = messaging.send(entry.getKey(), false); + String resp = client.send(entry.getKey(), false); assertEquals("mock-name", resp); checkRequestHeader(interceptor.getLastRequest()); @@ -95,12 +96,12 @@ public void testSend() throws Exception { public void testSendDryRun() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + final FirebaseMessagingClient client = initMessagingClient(response, interceptor); Map> testMessages = buildTestMessages(); for (Map.Entry> entry : testMessages.entrySet()) { response.setContent(MOCK_RESPONSE); - String resp = messaging.send(entry.getKey(), true); + String resp = client.send(entry.getKey(), true); assertEquals("mock-name", resp); checkRequestHeader(interceptor.getLastRequest()); @@ -113,13 +114,13 @@ public void testSendDryRun() throws Exception { public void testSendHttpError() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent("{}"); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("unknown-error", error.getErrorCode()); @@ -133,10 +134,10 @@ public void testSendHttpError() { @Test public void testSendTransportError() { - FirebaseMessagingClient messaging = initMessagingClientWithFaultyTransport(); + FirebaseMessagingClient client = initClientWithFaultyTransport(); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("internal-error", error.getErrorCode()); @@ -149,14 +150,14 @@ public void testSendTransportError() { public void testSendSuccessResponseWithUnexpectedPayload() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); Map> testMessages = buildTestMessages(); for (Map.Entry> entry : testMessages.entrySet()) { response.setContent("not valid json"); try { - messaging.send(entry.getKey(), false); + client.send(entry.getKey(), false); fail("No error thrown for malformed response"); } catch (FirebaseMessagingException error) { assertEquals("internal-error", error.getErrorCode()); @@ -170,13 +171,13 @@ public void testSendSuccessResponseWithUnexpectedPayload() { public void testSendErrorWithZeroContentResponse() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setZeroContent(); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("unknown-error", error.getErrorCode()); @@ -192,13 +193,13 @@ public void testSendErrorWithZeroContentResponse() { public void testSendErrorWithMalformedResponse() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent("not json"); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("unknown-error", error.getErrorCode()); @@ -214,14 +215,14 @@ public void testSendErrorWithMalformedResponse() { public void testSendErrorWithDetails() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("invalid-argument", error.getErrorCode()); @@ -236,14 +237,14 @@ public void testSendErrorWithDetails() { public void testSendErrorWithCanonicalCode() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("registration-token-not-registered", error.getErrorCode()); @@ -258,7 +259,7 @@ public void testSendErrorWithCanonicalCode() { public void testSendErrorWithFcmError() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( @@ -267,7 +268,7 @@ public void testSendErrorWithFcmError() { + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); try { - messaging.send(EMPTY_MESSAGE, false); + client.send(EMPTY_MESSAGE, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("registration-token-not-registered", error.getErrorCode()); @@ -281,13 +282,10 @@ public void testSendErrorWithFcmError() { @Test public void testSendAll() throws Exception { final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClientForBatchRequests( + FirebaseMessagingClient client = initMessagingClientForBatchRequests( MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - List messages = ImmutableList.of( - EMPTY_MESSAGE, EMPTY_MESSAGE - ); - BatchResponse responses = messaging.sendAll(messages, false); + BatchResponse responses = client.sendAll(MESSAGE_LIST, false); assertSendBatchSuccess(responses, interceptor); } @@ -295,20 +293,17 @@ public void testSendAll() throws Exception { @Test public void testSendAllDryRun() throws Exception { final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClientForBatchRequests( + FirebaseMessagingClient client = initMessagingClientForBatchRequests( MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - List messages = ImmutableList.of( - EMPTY_MESSAGE, EMPTY_MESSAGE - ); - BatchResponse responses = messaging.sendAll(messages, true); + BatchResponse responses = client.sendAll(MESSAGE_LIST, true); assertSendBatchSuccess(responses, interceptor); } @Test public void testRequestInitializerAppliedToBatchRequests() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(getBatchResponse(MOCK_BATCH_SUCCESS_RESPONSE)) .build(); @@ -318,19 +313,16 @@ public void initialize(HttpRequest httpRequest) { httpRequest.getHeaders().set("x-custom-header", "test-value"); } }; - FirebaseMessagingClientImpl messaging = FirebaseMessagingClientImpl.builder() + FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.builder() .setProjectId("test-project") .setJsonFactory(Utils.getDefaultJsonFactory()) .setRequestFactory(transport.createRequestFactory(initializer)) .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) .setResponseInterceptor(interceptor) .build(); - List messages = ImmutableList.of( - EMPTY_MESSAGE, EMPTY_MESSAGE - ); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); } finally { HttpRequest request = interceptor.getLastRequest(); assertEquals("test-value", request.getHeaders().get("x-custom-header")); @@ -340,13 +332,10 @@ public void initialize(HttpRequest httpRequest) { @Test public void testSendAllFailure() throws Exception { final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClientForBatchRequests( + FirebaseMessagingClient client = initMessagingClientForBatchRequests( MOCK_BATCH_FAILURE_RESPONSE, interceptor); - List messages = ImmutableList.of( - EMPTY_MESSAGE, EMPTY_MESSAGE, EMPTY_MESSAGE - ); - BatchResponse responses = messaging.sendAll(messages, false); + BatchResponse responses = client.sendAll(MESSAGE_LIST, false); assertSendBatchFailure(responses, interceptor); } @@ -355,14 +344,13 @@ public void testSendAllFailure() throws Exception { public void testSendAllHttpError() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent("{}"); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("unknown-error", error.getErrorCode()); @@ -376,11 +364,10 @@ public void testSendAllHttpError() { @Test public void testSendAllTransportError() { - FirebaseMessagingClient messaging = initMessagingClientWithFaultyTransport(); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initClientWithFaultyTransport(); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("internal-error", error.getErrorCode()); @@ -393,14 +380,13 @@ public void testSendAllTransportError() { public void testSendAllErrorWithEmptyResponse() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setZeroContent(); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("unknown-error", error.getErrorCode()); @@ -416,15 +402,14 @@ public void testSendAllErrorWithEmptyResponse() { public void testSendAllErrorWithDetails() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("invalid-argument", error.getErrorCode()); @@ -439,15 +424,14 @@ public void testSendAllErrorWithDetails() { public void testSendAllErrorWithCanonicalCode() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("registration-token-not-registered", error.getErrorCode()); @@ -462,8 +446,7 @@ public void testSendAllErrorWithCanonicalCode() { public void testSendAllErrorWithFcmError() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( @@ -472,7 +455,7 @@ public void testSendAllErrorWithFcmError() { + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("registration-token-not-registered", error.getErrorCode()); @@ -487,8 +470,7 @@ public void testSendAllErrorWithFcmError() { public void testSendAllErrorWithoutMessage() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessagingClient messaging = initMessagingClient(response, interceptor); - List messages = ImmutableList.of(EMPTY_MESSAGE); + FirebaseMessagingClient client = initMessagingClient(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( @@ -497,7 +479,7 @@ public void testSendAllErrorWithoutMessage() { + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); try { - messaging.sendAll(messages, false); + client.sendAll(MESSAGE_LIST, false); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("registration-token-not-registered", error.getErrorCode()); @@ -582,7 +564,7 @@ private MockLowLevelHttpResponse getBatchResponse(String responsePayload) { .setContent(responsePayload); } - private FirebaseMessagingClientImpl initMessagingClientWithFaultyTransport() { + private FirebaseMessagingClientImpl initClientWithFaultyTransport() { HttpTransport transport = TestUtils.faultyHttpTransport(); return FirebaseMessagingClientImpl.builder() .setProjectId("test-project") diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index ddcb64f68..508eb5f51 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -528,6 +528,18 @@ public void testSubscribeToTopic() throws FirebaseMessagingException { assertSame(TOPIC_MGT_RESPONSE, got); } + @Test + public void testSubscribeToTopicFailure() { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + try { + messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); + } + } + @Test public void testSubscribeToTopicAsync() throws Exception { MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); @@ -539,6 +551,18 @@ public void testSubscribeToTopicAsync() throws Exception { assertSame(TOPIC_MGT_RESPONSE, got); } + @Test + public void testSubscribeToTopicAsyncFailure() throws InterruptedException { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + try { + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } + @Test public void testInvalidUnsubscribe() throws FirebaseMessagingException { MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); @@ -568,6 +592,18 @@ public void testUnsubscribeFromTopic() throws FirebaseMessagingException { assertSame(TOPIC_MGT_RESPONSE, got); } + @Test + public void testUnsubscribeFromTopicFailure() { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + try { + messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); + } + } + @Test public void testUnsubscribeFromTopicAsync() throws Exception { MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); @@ -579,6 +615,18 @@ public void testUnsubscribeFromTopicAsync() throws Exception { assertSame(TOPIC_MGT_RESPONSE, got); } + @Test + public void testUnsubscribeFromTopicAsyncFailure() throws InterruptedException { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } + private FirebaseMessaging getMessagingForSend( Supplier supplier) { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); @@ -599,9 +647,12 @@ private FirebaseMessaging getMessagingForTopicManagement( .build(); } - private BatchResponse getBatchResponse(String messageId) { - SendResponse response = SendResponse.fromMessageId(messageId); - return new BatchResponse(ImmutableList.of(response)); + private BatchResponse getBatchResponse(String ...messageIds) { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (String messageId : messageIds) { + listBuilder.add(SendResponse.fromMessageId(messageId)); + } + return new BatchResponse(listBuilder.build()); } private static class MockFirebaseMessagingClient implements FirebaseMessagingClient { diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index e4554380d..e45750c74 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -50,9 +50,11 @@ public void testSubscribe() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(responseString); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final InstanceIdClient messaging = initMessaging(response, interceptor); - TopicManagementResponse result = messaging.subscribeToTopic( + final InstanceIdClient client = initInstanceIdClient(response, interceptor); + + TopicManagementResponse result = client.subscribeToTopic( "test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); checkTopicManagementRequest(interceptor.getLastRequest(), result); @@ -64,9 +66,11 @@ public void testSubscribeWithPrefixedTopic() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(responseString); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final InstanceIdClient messaging = initMessaging(response, interceptor); - TopicManagementResponse result = messaging.subscribeToTopic( + final InstanceIdClient client = initInstanceIdClient(response, interceptor); + + TopicManagementResponse result = client.subscribeToTopic( "/topics/test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); checkTopicManagementRequest(interceptor.getLastRequest(), result); @@ -76,11 +80,13 @@ public void testSubscribeWithPrefixedTopic() throws Exception { public void testSubscribeError() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + for (int statusCode : HTTP_ERRORS) { response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + try { - messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); @@ -94,12 +100,13 @@ public void testSubscribeError() { @Test public void testSubscribeUnknownError() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("{}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + try { - messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); @@ -112,12 +119,13 @@ public void testSubscribeUnknownError() { @Test public void testSubscribeMalformedError() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("not json"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("not json"); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + try { - messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); @@ -130,12 +138,13 @@ public void testSubscribeMalformedError() { @Test public void testSubscribeZeroContentError() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setZeroContent(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setZeroContent(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + try { - messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); @@ -148,9 +157,10 @@ public void testSubscribeZeroContentError() { @Test public void testSubscribeTransportError() { - InstanceIdClient messaging = initFaultyTransportMessaging(); + InstanceIdClient client = initClientWithFaultyTransport(); + try { - messaging.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("internal-error", error.getErrorCode()); @@ -165,10 +175,11 @@ public void testUnsubscribe() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(responseString); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final InstanceIdClient messaging = initMessaging(response, interceptor); + final InstanceIdClient client = initInstanceIdClient(response, interceptor); - TopicManagementResponse result = messaging.unsubscribeFromTopic( + TopicManagementResponse result = client.unsubscribeFromTopic( "test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); checkTopicManagementRequest(interceptor.getLastRequest(), result); @@ -180,10 +191,11 @@ public void testUnsubscribeWithPrefixedTopic() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(responseString); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final InstanceIdClient messaging = initMessaging(response, interceptor); + final InstanceIdClient client = initInstanceIdClient(response, interceptor); - TopicManagementResponse result = messaging.unsubscribeFromTopic( + TopicManagementResponse result = client.unsubscribeFromTopic( "/topics/test-topic", ImmutableList.of("id1", "id2")); + checkTopicManagementRequestHeader( interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); checkTopicManagementRequest(interceptor.getLastRequest(), result); @@ -193,11 +205,13 @@ public void testUnsubscribeWithPrefixedTopic() throws Exception { public void testUnsubscribeError() { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + for (int statusCode : HTTP_ERRORS) { response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + try { - messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); @@ -211,12 +225,13 @@ public void testUnsubscribeError() { @Test public void testUnsubscribeUnknownError() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("{}"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + try { - messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); @@ -229,12 +244,13 @@ public void testUnsubscribeUnknownError() { @Test public void testUnsubscribeMalformedError() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("not json"); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("not json"); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + try { - messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); @@ -247,12 +263,13 @@ public void testUnsubscribeMalformedError() { @Test public void testUnsubscribeZeroContentError() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setZeroContent(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - InstanceIdClient messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setZeroContent(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + try { - messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); @@ -265,9 +282,10 @@ public void testUnsubscribeZeroContentError() { @Test public void testUnsubscribeTransportError() { - InstanceIdClient messaging = initFaultyTransportMessaging(); + InstanceIdClient client = initClientWithFaultyTransport(); + try { - messaging.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { assertEquals("internal-error", error.getErrorCode()); @@ -298,7 +316,6 @@ public void testFromApp() throws IOException { InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app); assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = client.getRequestFactory().buildGetRequest( new GenericUrl("https://example.com")); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); @@ -317,15 +334,7 @@ public void testTopicManagementResponseWithEmptyList() { new TopicManagementResponse(ImmutableList.of()); } - private static String getTopicManagementErrorCode(int statusCode) { - String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; - } - return code; - } - - private static InstanceIdClientImpl initMessaging( + private static InstanceIdClientImpl initInstanceIdClient( final MockLowLevelHttpResponse mockResponse, final HttpResponseInterceptor interceptor) { @@ -362,9 +371,17 @@ private void checkTopicManagementRequestHeader( assertEquals(expectedUrl, request.getUrl().toString()); } - private static InstanceIdClient initFaultyTransportMessaging() { + private static InstanceIdClient initClientWithFaultyTransport() { return new InstanceIdClientImpl( TestUtils.faultyHttpTransport().createRequestFactory(), Utils.getDefaultJsonFactory()); } + + private String getTopicManagementErrorCode(int statusCode) { + String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode); + if (code == null) { + code = "unknown-error"; + } + return code; + } } From e079ef78b75384b49454df228977669ab5dab300 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 15 Mar 2019 14:40:43 -0700 Subject: [PATCH 7/9] Fixing a failing test --- .../firebase/messaging/FirebaseMessagingClientImplTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index b5ab65ea4..f55ae70f8 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -334,8 +334,9 @@ public void testSendAllFailure() throws Exception { final TestResponseInterceptor interceptor = new TestResponseInterceptor(); FirebaseMessagingClient client = initMessagingClientForBatchRequests( MOCK_BATCH_FAILURE_RESPONSE, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE, EMPTY_MESSAGE); - BatchResponse responses = client.sendAll(MESSAGE_LIST, false); + BatchResponse responses = client.sendAll(messages, false); assertSendBatchFailure(responses, interceptor); } From d0144cd895b6f647999c64ec23399b0379e06d32 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 18 Mar 2019 15:11:45 -0700 Subject: [PATCH 8/9] Enabled HTTP retries for FCM --- .../firebase/internal/ApiClientUtils.java | 17 ++- .../firebase/internal/ApiClientUtilsTest.java | 118 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 62e4320e4..7506ff8ee 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -19,6 +19,7 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import java.io.IOException; @@ -28,9 +29,23 @@ */ public class ApiClientUtils { + private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() + .setMaxRetries(4) + .setRetryStatusCodes(ImmutableList.of(500, 503)) + .setMaxIntervalMillis(60 * 1000) + .build(); + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) { HttpTransport transport = app.getOptions().getHttpTransport(); - return transport.createRequestFactory(new FirebaseRequestInitializer(app)); + return transport.createRequestFactory( + new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG)); } public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) { diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java new file mode 100644 index 000000000..78eabaf32 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +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.HttpUnsuccessfulResponseHandler; +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.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; +import java.io.IOException; +import org.junit.After; +import org.junit.Test; + +public class ApiClientUtilsTest { + + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .build(); + private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testAuthorizedHttpClient() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertTrue(retryHandler instanceof RetryHandlerDecorator); + RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler() + .getRetryConfig(); + assertEquals(4, retryConfig.getMaxRetries()); + assertEquals(60 * 1000, retryConfig.getMaxIntervalMillis()); + assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503)); + } + + @Test + public void testUnauthorizedHttpClient() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); + + assertNull(requestFactory.getInitializer()); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertNull(request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertNull(retryHandler); + } + + @Test + public void testDisconnect() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(lowLevelResponse) + .build(); + HttpResponse response = transport.createRequestFactory().buildGetRequest(TEST_URL).execute(); + assertFalse(lowLevelResponse.isDisconnected()); + + ApiClientUtils.disconnectQuietly(response); + + assertTrue(lowLevelResponse.isDisconnected()); + } + + @Test + public void testDisconnectWithErrorSuppression() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse(){ + @Override + public void disconnect() throws IOException { + super.disconnect(); + throw new IOException("test error"); + } + }; + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(lowLevelResponse) + .build(); + HttpResponse response = transport.createRequestFactory().buildGetRequest(TEST_URL).execute(); + assertFalse(lowLevelResponse.isDisconnected()); + + ApiClientUtils.disconnectQuietly(response); + + assertTrue(lowLevelResponse.isDisconnected()); + } +} From 4708ec4d49f2d35e0aaa0f99e827a6a0b038a46a Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 19 Apr 2019 10:46:08 -0700 Subject: [PATCH 9/9] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cea3b25ec..7417de125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [fixed] Enabled automatic retries for FCM API calls failing with + HTTP 500 and 503 errors. # v6.8.0