From 65f4435d33aecb208a0e5a8c533788180df5c86d Mon Sep 17 00:00:00 2001 From: Doris Ge Date: Fri, 17 Mar 2023 15:27:20 -0700 Subject: [PATCH] Implement sendEach, sendEachAsync, sendEachForMulticast and sendEachForMulticastAsync `sendEach` vs `sendAll` 1. `sendEach` sends one HTTP request to V1 Send endpoint for each message in the array. `sendAll` sends only one HTTP request to V1 Batch Send endpoint to send all messages in the array. 2. `sendEach` calls `messagingClient.send` to send each message and constructs a `SendResponse` with the returned `messageId`. If `messagingClient.send` throws out an exception, `sendEach` will catch the exception and also turn it into a `SendResponse` with the exception in it. `sendEach` calls `ApiFutures.allAsList().get()` to execute all `messagingClient.send` calls asynchronously and wait for all of them to complete and construct a `BatchResponse` with all `SendResponse`s. Therefore, unlike `sendAll`, `sendEach` does not always throw an error for a total failure. It can also return a `BatchResponse` with only errors in it. `sendEachForMulticast` calls `sendEach` under the hood. `sendEachAsync` is the async version of `sendEach`. `sendEachForMulticastAsync` is the async version of `sendEachForMulticast`. --- .../firebase/messaging/FirebaseMessaging.java | 195 ++++++++++- .../messaging/FirebaseMessagingTest.java | 307 +++++++++++++++++- 2 files changed, 498 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 95366eaa7..8878a3785 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -20,18 +20,22 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; 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.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; /** * This class is the entry point for all server-side Firebase Cloud Messaging actions. @@ -91,7 +95,7 @@ public String send(@NonNull Message message) throws FirebaseMessagingException { * *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead * FCM performs all the necessary validations and emulates the send operation. The {@code dryRun} - * option is useful for determining whether an FCM registration has been deleted. However, it + * option is useful for determining whether an FCM registration has been deleted. However, it * cannot be used to validate APNs tokens. * * @param message A non-null {@link Message} to be sent. @@ -139,6 +143,191 @@ protected String execute() throws FirebaseMessagingException { }; } + /** + * Sends each message in the given list via Firebase Cloud Messaging. + * Unlike {@link #sendAll(List)}, this method makes a single HTTP call for each message in the + * given array. + * + *

The responses list obtained by calling {@link BatchResponse#getResponses()} on the return + * value corresponds to the order of input messages. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @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. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure -- i.e. none of the messages in the list could be sent. Partial failures or no + * failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEach(@NonNull List messages) throws FirebaseMessagingException { + return sendEachOp(messages, false).call(); + } + + + /** + * Sends each message in the given list via Firebase Cloud Messaging. + * Unlike {@link #sendAll(List)}, this method makes a single HTTP call for each message in the + * given array. + * + *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. The {@code dryRun} + * option is useful for determining whether an FCM registration has been deleted. But it cannot be + * used to validate APNs tokens. + * + *

The responses list obtained by calling {@link BatchResponse#getResponses()} on the return + * value corresponds to the order of input messages. + * + * @param messages A non-null, non-empty list containing up to 500 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. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure -- i.e. none of the messages in the list could be sent. Partial failures or no + * failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEach( + @NonNull List messages, boolean dryRun) throws FirebaseMessagingException { + return sendEachOp(messages, dryRun).call(); + } + + /** + * Similar to {@link #sendEach(List)} but performs the operation asynchronously. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachAsync(@NonNull List messages) { + return sendEachOp(messages, false).callAsync(app); + } + + /** + * Similar to {@link #sendEach(List, boolean)} but performs the operation asynchronously. + * + * @param messages A non-null, non-empty list containing up to 500 messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachAsync(@NonNull List messages, boolean dryRun) { + return sendEachOp(messages, dryRun).callAsync(app); + } + + private CallableOperation sendEachOp( + final List messages, final boolean dryRun) { + final List immutableMessages = ImmutableList.copyOf(messages); + checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); + checkArgument(immutableMessages.size() <= 500, + "messages list must not contain more than 500 elements"); + + return new CallableOperation() { + @Override + protected BatchResponse execute() throws FirebaseMessagingException { + List> list = new ArrayList<>(); + for (Message message : immutableMessages) { + ApiFuture messageId = sendOpForSendResponse(message, dryRun).callAsync(app); + list.add(messageId); + } + try { + List responses = ApiFutures.allAsList(list).get(); + return new BatchResponseImpl(responses); + } catch (InterruptedException | ExecutionException e) { + throw new FirebaseMessagingException(ErrorCode.CANCELLED, SERVICE_ID); + } + } + }; + } + + private CallableOperation sendOpForSendResponse( + final Message message, final boolean dryRun) { + checkNotNull(message, "message must not be null"); + final FirebaseMessagingClient messagingClient = getMessagingClient(); + return new CallableOperation() { + @Override + protected SendResponse execute() { + try { + String messageId = messagingClient.send(message, dryRun); + return SendResponse.fromMessageId(messageId); + } catch (FirebaseMessagingException e) { + return SendResponse.fromException(e); + } + } + }; + } + + /** + * Sends the given multicast message to all the FCM registration tokens specified in it. + * + *

This method uses the {@link #sendEach(List)} API under the hood to send the given + * message to all the target recipients. The responses list obtained by calling + * {@link BatchResponse#getResponses()} on the return value corresponds to the order of tokens + * in the {@link MulticastMessage}. + * + * @param message A non-null {@link MulticastMessage} + * @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. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure -- i.e. none of the messages in the list could be sent. Partial failures or no + * failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEachForMulticast( + @NonNull MulticastMessage message) throws FirebaseMessagingException { + return sendEachForMulticast(message, false); + } + + /** + * Sends the given multicast message to all the FCM registration tokens specified in it. + * + *

If the {@code dryRun} option is set to true, the message will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. The {@code dryRun} + * option is useful for determining whether an FCM registration has been deleted. But it cannot be + * used to validate APNs tokens. + * + *

This method uses the {@link #sendEach(List)} API under the hood to send the given + * message to all the target recipients. The responses list obtained by calling + * {@link BatchResponse#getResponses()} on the return value corresponds to the order of tokens + * in the {@link MulticastMessage}. + * + * @param message A non-null {@link MulticastMessage}. + * @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. An exception here or a {@link BatchResponse} with all failures indicates a total + * failure -- i.e. none of the messages in the list could be sent. Partial failures or no + * failures are only indicated by a {@link BatchResponse}. + */ + public BatchResponse sendEachForMulticast(@NonNull MulticastMessage message, boolean dryRun) + throws FirebaseMessagingException { + checkNotNull(message, "multicast message must not be null"); + return sendEach(message.getMessageList(), dryRun); + } + + /** + * Similar to {@link #sendEachForMulticast(MulticastMessage)} but performs the operation + * asynchronously. + * + * @param message A non-null {@link MulticastMessage}. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachForMulticastAsync(@NonNull MulticastMessage message) { + return sendEachForMulticastAsync(message, false); + } + + /** + * Similar to {@link #sendEachForMulticast(MulticastMessage, boolean)} but performs the operation + * asynchronously. + * + * @param message A non-null {@link MulticastMessage}. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendEachForMulticastAsync( + @NonNull MulticastMessage message, boolean dryRun) { + checkNotNull(message, "multicast message must not be null"); + return sendEachAsync(message.getMessageList(), dryRun); + } + /** * Sends all the messages in the given list via Firebase Cloud Messaging. Employs batching to * send the entire list as a single RPC call. Compared to the {@link #send(Message)} method, this @@ -187,7 +376,7 @@ public BatchResponse sendAll( * Similar to {@link #sendAll(List)} but performs the operation asynchronously. * * @param messages A non-null, non-empty list containing up to 500 messages. - * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. */ public ApiFuture sendAllAsync(@NonNull List messages) { @@ -199,7 +388,7 @@ public ApiFuture sendAllAsync(@NonNull List messages) { * * @param messages A non-null, non-empty list containing up to 500 messages. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. - * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent, or when the emulation has finished. */ public ApiFuture sendAllAsync( diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 8e8e79e07..082efa5c9 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -16,6 +16,9 @@ package com.google.firebase.messaging; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -23,17 +26,24 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import autovalue.shaded.org.jetbrains.annotations.Nullable; 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; +import com.google.common.collect.ImmutableMap; import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; + +import java.util.ArrayList; +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; @@ -46,6 +56,9 @@ public class FirebaseMessagingTest { private static final Message EMPTY_MESSAGE = Message.builder() .setTopic("test-topic") .build(); + private static final Message EMPTY_MESSAGE_2 = Message.builder() + .setTopic("test-topic2") + .build(); private static final MulticastMessage TEST_MULTICAST_MESSAGE = MulticastMessage.builder() .addToken("test-fcm-token1") .addToken("test-fcm-token2") @@ -262,6 +275,277 @@ public void testSendAsyncFailure() throws InterruptedException { assertFalse(client.isLastDryRun); } + @Test + public void testSendEachWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendEach(null); + fail("No error thrown for null message list"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEachWithEmptyList() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendEach(ImmutableList.of()); + fail("No error thrown for empty message list"); + } catch (IllegalArgumentException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEachWithTooManyMessages() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (int i = 0; i < 501; i++) { + listBuilder.add(Message.builder().setTopic("topic").build()); + } + + try { + messaging.sendEach(listBuilder.build(), false); + fail("No error thrown for too many messages in the list"); + } catch (IllegalArgumentException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEach() throws FirebaseMessagingException { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEach(messages); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachDryRun() throws FirebaseMessagingException { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEach(messages, true); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachFailure() throws FirebaseMessagingException { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + Map messageMap = new HashMap<>(); + messageMap.put(messages.get(0), SendResponse.fromMessageId("test")); + messageMap.put(messages.get(1), SendResponse.fromException(TEST_EXCEPTION)); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEach(messages); + + assertEquals(1, response.getFailureCount()); + assertEquals(1, response.getSuccessCount()); + assertEquals("test", response.getResponses().get(0).getMessageId()); + assertEquals(TEST_EXCEPTION, response.getResponses().get(1).getException()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachAsync() throws Exception { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachAsync(messages).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachAsyncDryRun() throws Exception { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + ImmutableList messageIds = ImmutableList.of("test1", "test2"); + Map messageMap = new HashMap<>(); + for (int i = 0; i < 2; i++) { + messageMap.put(messages.get(i), SendResponse.fromMessageId(messageIds.get(i))); + } + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachAsync(messages, true).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(messageIds.get(i), response.getResponses().get(i).getMessageId()); + } + assertThat(client.lastMessage, anyOf(is(EMPTY_MESSAGE), is(EMPTY_MESSAGE_2))); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachAsyncFailure() throws Exception { + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE_2); + Map messageMap = new HashMap<>(); + messageMap.put(messages.get(0), SendResponse.fromMessageId("test")); + messageMap.put(messages.get(1), SendResponse.fromException(TEST_EXCEPTION)); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageMap(messageMap); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachAsync(messages).get(); + + assertEquals(1, response.getFailureCount()); + assertEquals(1, response.getSuccessCount()); + assertEquals("test", response.getResponses().get(0).getMessageId()); + assertEquals(TEST_EXCEPTION, response.getResponses().get(1).getException()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendEachForMulticast(null); + fail("No error thrown for null multicast message"); + } catch (NullPointerException expected) { + // expected + } + + assertNull(client.lastMessage); + } + + @Test + public void testSendEachForMulticast() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticast(TEST_MULTICAST_MESSAGE); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastDryRun() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticast(TEST_MULTICAST_MESSAGE, true); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastFailure() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticast(TEST_MULTICAST_MESSAGE); + + assertEquals(2, response.getFailureCount()); + assertEquals(0, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(TEST_EXCEPTION, response.getResponses().get(i).getException()); + } + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastAsync() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastAsyncDryRun() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticastAsync( + TEST_MULTICAST_MESSAGE, true).get(); + + assertEquals(2, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals("test", response.getResponses().get(i).getMessageId()); + } + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendEachForMulticastAsyncFailure() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendEachForMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + + assertEquals(2, response.getFailureCount()); + assertEquals(0, response.getSuccessCount()); + for (int i = 0; i < 2; i++) { + assertEquals(TEST_EXCEPTION, response.getResponses().get(i).getException()); + } + assertFalse(client.isLastDryRun); + } + @Test public void testSendAllWithNull() throws FirebaseMessagingException { MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); @@ -681,6 +965,7 @@ private static class MockFirebaseMessagingClient implements FirebaseMessagingCli private Message lastMessage; private List lastBatch; private boolean isLastDryRun; + private ImmutableMap messageMap; private MockFirebaseMessagingClient( String messageId, BatchResponse batchResponse, FirebaseMessagingException exception) { @@ -689,10 +974,20 @@ private MockFirebaseMessagingClient( this.exception = exception; } + private MockFirebaseMessagingClient( + Map messageMap, FirebaseMessagingException exception) { + this.messageMap = ImmutableMap.copyOf(messageMap); + this.exception = exception; + } + static MockFirebaseMessagingClient fromMessageId(String messageId) { return new MockFirebaseMessagingClient(messageId, null, null); } + static MockFirebaseMessagingClient fromMessageMap(Map messageMap) { + return new MockFirebaseMessagingClient(messageMap, null); + } + static MockFirebaseMessagingClient fromBatchResponse(BatchResponse batchResponse) { return new MockFirebaseMessagingClient(null, batchResponse, null); } @@ -702,13 +997,23 @@ static MockFirebaseMessagingClient fromException(FirebaseMessagingException exce } @Override + @Nullable public String send(Message message, boolean dryRun) throws FirebaseMessagingException { lastMessage = message; isLastDryRun = dryRun; if (exception != null) { throw exception; } - return messageId; + if (messageMap == null) { + return messageId; + } + if (!messageMap.containsKey(message)) { + return null; + } + if (messageMap.get(message).getException() != null) { + throw messageMap.get(message).getException(); + } + return messageMap.get(message).getMessageId(); } @Override