Introduce an `FcmSender`
This commit is contained in:
		
							parent
							
								
									9c03f2e468
								
							
						
					
					
						commit
						421d594507
					
				| 
						 | 
					@ -206,6 +206,10 @@ gcm: # GCM Configuration
 | 
				
			||||||
  senderId: 123456789
 | 
					  senderId: 123456789
 | 
				
			||||||
  apiKey: unset
 | 
					  apiKey: unset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fcm: # FCM configuration
 | 
				
			||||||
 | 
					  credentials: |
 | 
				
			||||||
 | 
					    { "json": true }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cdn:
 | 
					cdn:
 | 
				
			||||||
  accessKey: test    # AWS Access Key ID
 | 
					  accessKey: test    # AWS Access Key ID
 | 
				
			||||||
  accessSecret: test # AWS Access Secret
 | 
					  accessSecret: test # AWS Access Secret
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -195,6 +195,39 @@
 | 
				
			||||||
      <artifactId>commons-csv</artifactId>
 | 
					      <artifactId>commons-csv</artifactId>
 | 
				
			||||||
    </dependency>
 | 
					    </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependency>
 | 
				
			||||||
 | 
					      <groupId>com.google.firebase</groupId>
 | 
				
			||||||
 | 
					      <artifactId>firebase-admin</artifactId>
 | 
				
			||||||
 | 
					      <version>9.0.0</version>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- firebase-admin has conflicting versions of these artifacts in its dependency tree; for firebase-admin
 | 
				
			||||||
 | 
					      9.0.0, we'll need to depend directly on com.google.api-client:google-api-client:1.35.1 and
 | 
				
			||||||
 | 
					      com.google.oauth-client:google-oauth-client:1.34.1 -->
 | 
				
			||||||
 | 
					      <exclusions>
 | 
				
			||||||
 | 
					        <exclusion>
 | 
				
			||||||
 | 
					          <groupId>com.google.api-client</groupId>
 | 
				
			||||||
 | 
					          <artifactId>google-api-client</artifactId>
 | 
				
			||||||
 | 
					        </exclusion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <exclusion>
 | 
				
			||||||
 | 
					          <groupId>com.google.oauth-client</groupId>
 | 
				
			||||||
 | 
					          <artifactId>google-oauth-client</artifactId>
 | 
				
			||||||
 | 
					        </exclusion>
 | 
				
			||||||
 | 
					      </exclusions>
 | 
				
			||||||
 | 
					    </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependency>
 | 
				
			||||||
 | 
					      <groupId>com.google.api-client</groupId>
 | 
				
			||||||
 | 
					      <artifactId>google-api-client</artifactId>
 | 
				
			||||||
 | 
					      <version>1.35.1</version>
 | 
				
			||||||
 | 
					    </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependency>
 | 
				
			||||||
 | 
					      <groupId>com.google.oauth-client</groupId>
 | 
				
			||||||
 | 
					      <artifactId>google-oauth-client</artifactId>
 | 
				
			||||||
 | 
					      <version>1.34.1</version>
 | 
				
			||||||
 | 
					    </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <dependency>
 | 
					    <dependency>
 | 
				
			||||||
      <groupId>com.google.code.findbugs</groupId>
 | 
					      <groupId>com.google.code.findbugs</groupId>
 | 
				
			||||||
      <artifactId>jsr305</artifactId>
 | 
					      <artifactId>jsr305</artifactId>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
 | 
					import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GiftConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.GiftConfiguration;
 | 
				
			||||||
| 
						 | 
					@ -168,6 +169,11 @@ public class WhisperServerConfiguration extends Configuration {
 | 
				
			||||||
  @JsonProperty
 | 
					  @JsonProperty
 | 
				
			||||||
  private GcmConfiguration gcm;
 | 
					  private GcmConfiguration gcm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Valid
 | 
				
			||||||
 | 
					  @NotNull
 | 
				
			||||||
 | 
					  @JsonProperty
 | 
				
			||||||
 | 
					  private FcmConfiguration fcm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Valid
 | 
					  @Valid
 | 
				
			||||||
  @NotNull
 | 
					  @NotNull
 | 
				
			||||||
  @JsonProperty
 | 
					  @JsonProperty
 | 
				
			||||||
| 
						 | 
					@ -340,6 +346,10 @@ public class WhisperServerConfiguration extends Configuration {
 | 
				
			||||||
    return gcm;
 | 
					    return gcm;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public FcmConfiguration getFcmConfiguration() {
 | 
				
			||||||
 | 
					    return fcm;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public ApnConfiguration getApnConfiguration() {
 | 
					  public ApnConfiguration getApnConfiguration() {
 | 
				
			||||||
    return apn;
 | 
					    return apn;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2013-2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotBlank;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record FcmConfiguration(@NotBlank String credentials) {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,144 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2013-2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.push;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.api.core.ApiFuture;
 | 
				
			||||||
 | 
					import com.google.auth.oauth2.GoogleCredentials;
 | 
				
			||||||
 | 
					import com.google.common.annotations.VisibleForTesting;
 | 
				
			||||||
 | 
					import com.google.firebase.FirebaseApp;
 | 
				
			||||||
 | 
					import com.google.firebase.FirebaseOptions;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.AndroidConfig;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.FirebaseMessaging;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.FirebaseMessagingException;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.Message;
 | 
				
			||||||
 | 
					import io.micrometer.core.instrument.Metrics;
 | 
				
			||||||
 | 
					import io.micrometer.core.instrument.Tags;
 | 
				
			||||||
 | 
					import java.io.ByteArrayInputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutionException;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutorService;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.Account;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.Util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class FcmSender {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private final Logger logger = LoggerFactory.getLogger(FcmSender.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private final AccountsManager accountsManager;
 | 
				
			||||||
 | 
					  private final ExecutorService executor;
 | 
				
			||||||
 | 
					  private final FirebaseMessaging firebaseMessagingClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final String SENT_MESSAGE_COUNTER_NAME = name(FcmSender.class, "sentMessage");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public FcmSender(ExecutorService executor, AccountsManager accountsManager, String credentials) throws IOException {
 | 
				
			||||||
 | 
					    try (final ByteArrayInputStream credentialInputStream = new ByteArrayInputStream(credentials.getBytes(StandardCharsets.UTF_8))) {
 | 
				
			||||||
 | 
					      FirebaseOptions options = FirebaseOptions.builder()
 | 
				
			||||||
 | 
					          .setCredentials(GoogleCredentials.fromStream(credentialInputStream))
 | 
				
			||||||
 | 
					          .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      FirebaseApp.initializeApp(options);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.executor = executor;
 | 
				
			||||||
 | 
					    this.accountsManager = accountsManager;
 | 
				
			||||||
 | 
					    this.firebaseMessagingClient = FirebaseMessaging.getInstance();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @VisibleForTesting
 | 
				
			||||||
 | 
					  public FcmSender(ExecutorService executor, AccountsManager accountsManager, FirebaseMessaging firebaseMessagingClient) {
 | 
				
			||||||
 | 
					    this.accountsManager = accountsManager;
 | 
				
			||||||
 | 
					    this.executor = executor;
 | 
				
			||||||
 | 
					    this.firebaseMessagingClient = firebaseMessagingClient;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public void sendMessage(GcmMessage message) {
 | 
				
			||||||
 | 
					    Message.Builder builder = Message.builder()
 | 
				
			||||||
 | 
					        .setToken(message.getGcmId())
 | 
				
			||||||
 | 
					        .setAndroidConfig(AndroidConfig.builder()
 | 
				
			||||||
 | 
					            .setPriority(AndroidConfig.Priority.HIGH)
 | 
				
			||||||
 | 
					            .build());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final String key = switch (message.getType()) {
 | 
				
			||||||
 | 
					      case NOTIFICATION -> "notification";
 | 
				
			||||||
 | 
					      case CHALLENGE -> "challenge";
 | 
				
			||||||
 | 
					      case RATE_LIMIT_CHALLENGE -> "rateLimitChallenge";
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder.putData(key, message.getData().orElse(""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final ApiFuture<String> sendFuture = firebaseMessagingClient.sendAsync(builder.build());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sendFuture.addListener(() -> {
 | 
				
			||||||
 | 
					      Tags tags = Tags.of("type", key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        sendFuture.get();
 | 
				
			||||||
 | 
					      } catch (ExecutionException e) {
 | 
				
			||||||
 | 
					        if (e.getCause() instanceof final FirebaseMessagingException firebaseMessagingException) {
 | 
				
			||||||
 | 
					          tags = tags.and("errorCode", firebaseMessagingException.getMessagingErrorCode().name().toLowerCase());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          switch (firebaseMessagingException.getMessagingErrorCode()) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case UNREGISTERED -> handleBadRegistration(message);
 | 
				
			||||||
 | 
					            case THIRD_PARTY_AUTH_ERROR, INVALID_ARGUMENT, INTERNAL, QUOTA_EXCEEDED, SENDER_ID_MISMATCH, UNAVAILABLE ->
 | 
				
			||||||
 | 
					                logger.debug("Unrecoverable Error ::: (error={}}), (gcm_id={}}), (destination={}}), (device_id={}})",
 | 
				
			||||||
 | 
					                    firebaseMessagingException.getMessagingErrorCode(), message.getGcmId(), message.getUuid(), message.getDeviceId());
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw new RuntimeException("Failed to send message", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (InterruptedException e) {
 | 
				
			||||||
 | 
					        // This should never happen; by definition, if we're in the future's listener, the future is done, and so
 | 
				
			||||||
 | 
					        // `get()` should return immediately.
 | 
				
			||||||
 | 
					        throw new IllegalStateException("Interrupted while getting send future result", e);
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, executor);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private void handleBadRegistration(GcmMessage message) {
 | 
				
			||||||
 | 
					    Optional<Account> account = getAccountForEvent(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (account.isPresent()) {
 | 
				
			||||||
 | 
					      //noinspection OptionalGetWithoutIsPresent
 | 
				
			||||||
 | 
					      Device device = account.get().getDevice(message.getDeviceId()).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (device.getUninstalledFeedbackTimestamp() == 0) {
 | 
				
			||||||
 | 
					        accountsManager.updateDevice(account.get(), message.getDeviceId(), d ->
 | 
				
			||||||
 | 
					            d.setUninstalledFeedbackTimestamp(Util.todayInMillis()));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private Optional<Account> getAccountForEvent(GcmMessage message) {
 | 
				
			||||||
 | 
					    Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (account.isPresent()) {
 | 
				
			||||||
 | 
					      Optional<Device> device = account.get().getDevice(message.getDeviceId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (device.isPresent()) {
 | 
				
			||||||
 | 
					        if (message.getGcmId().equals(device.get().getGcmId())) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (device.get().getPushTimestamp() == 0 || System.currentTimeMillis() > (device.get().getPushTimestamp() + TimeUnit.SECONDS.toMillis(10))) {
 | 
				
			||||||
 | 
					            return account;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Optional.empty();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,105 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2013-2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.push;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static org.mockito.ArgumentMatchers.any;
 | 
				
			||||||
 | 
					import static org.mockito.Mockito.eq;
 | 
				
			||||||
 | 
					import static org.mockito.Mockito.mock;
 | 
				
			||||||
 | 
					import static org.mockito.Mockito.verify;
 | 
				
			||||||
 | 
					import static org.mockito.Mockito.when;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.api.core.SettableApiFuture;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.FirebaseMessaging;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.FirebaseMessagingException;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.Message;
 | 
				
			||||||
 | 
					import com.google.firebase.messaging.MessagingErrorCode;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutorService;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.AfterEach;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.BeforeEach;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.Test;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.Account;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.Util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FcmSenderTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private ExecutorService executorService;
 | 
				
			||||||
 | 
					  private AccountsManager accountsManager;
 | 
				
			||||||
 | 
					  private FirebaseMessaging firebaseMessaging;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private FcmSender fcmSender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @BeforeEach
 | 
				
			||||||
 | 
					  void setUp() {
 | 
				
			||||||
 | 
					    executorService = new SynchronousExecutorService();
 | 
				
			||||||
 | 
					    accountsManager = mock(AccountsManager.class);
 | 
				
			||||||
 | 
					    firebaseMessaging = mock(FirebaseMessaging.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fcmSender = new FcmSender(executorService, accountsManager, firebaseMessaging);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @AfterEach
 | 
				
			||||||
 | 
					  void tearDown() throws InterruptedException {
 | 
				
			||||||
 | 
					    executorService.shutdown();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //noinspection ResultOfMethodCallIgnored
 | 
				
			||||||
 | 
					    executorService.awaitTermination(1, TimeUnit.SECONDS);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void testSendMessage() {
 | 
				
			||||||
 | 
					    AccountsHelper.setupMockUpdate(accountsManager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final GcmMessage message = new GcmMessage("foo", UUID.randomUUID(), 1, GcmMessage.Type.NOTIFICATION, Optional.empty());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
 | 
				
			||||||
 | 
					    sendFuture.set("message-id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fcmSender.sendMessage(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    verify(firebaseMessaging).sendAsync(any(Message.class));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void testSendUninstalled() {
 | 
				
			||||||
 | 
					    final UUID destinationUuid = UUID.randomUUID();
 | 
				
			||||||
 | 
					    final String gcmId = "foo";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Account destinationAccount = mock(Account.class);
 | 
				
			||||||
 | 
					    final Device  destinationDevice  = mock(Device.class );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    AccountsHelper.setupMockUpdate(accountsManager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
 | 
				
			||||||
 | 
					    when(accountsManager.getByAccountIdentifier(destinationUuid)).thenReturn(Optional.of(destinationAccount));
 | 
				
			||||||
 | 
					    when(destinationDevice.getGcmId()).thenReturn(gcmId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final GcmMessage message = new GcmMessage(gcmId, destinationUuid, 1, GcmMessage.Type.NOTIFICATION, Optional.empty());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final FirebaseMessagingException unregisteredException = mock(FirebaseMessagingException.class);
 | 
				
			||||||
 | 
					    when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
 | 
				
			||||||
 | 
					    sendFuture.setException(unregisteredException);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fcmSender.sendMessage(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    verify(firebaseMessaging).sendAsync(any(Message.class));
 | 
				
			||||||
 | 
					    verify(accountsManager).getByAccountIdentifier(destinationUuid);
 | 
				
			||||||
 | 
					    verify(accountsManager).updateDevice(eq(destinationAccount), eq(1L), any());
 | 
				
			||||||
 | 
					    verify(destinationDevice).setUninstalledFeedbackTimestamp(Util.todayInMillis());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue