Generalize push notification scheduler and add support for delayed "new messages" notifications

This commit is contained in:
Jon Chambers
2024-08-16 16:16:55 -04:00
committed by GitHub
parent 5892dc71fa
commit 659ac2c107
15 changed files with 979 additions and 757 deletions

View File

@@ -76,6 +76,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junitpioneer.jupiter.cartesian.ArgumentSets;
@@ -109,6 +110,7 @@ import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
@@ -179,6 +181,7 @@ class MessageControllerTest {
private static final CardinalityEstimator cardinalityEstimator = mock(CardinalityEstimator.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
private static final PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class);
private static final PushNotificationScheduler pushNotificationScheduler = mock(PushNotificationScheduler.class);
private static final ReportMessageManager reportMessageManager = mock(ReportMessageManager.class);
private static final ExecutorService multiRecipientMessageExecutor = MoreExecutors.newDirectExecutorService();
private static final Scheduler messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery");
@@ -200,13 +203,15 @@ class MessageControllerTest {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(
new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager,
messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor,
messagesManager, pushNotificationManager, pushNotificationScheduler, reportMessageManager, multiRecipientMessageExecutor,
messageDeliveryScheduler, ReportSpamTokenProvider.noop(), mock(ClientReleaseManager.class), dynamicConfigurationManager,
serverSecretParams, SpamChecker.noop(), new MessageMetrics(), clock))
.build();
@BeforeEach
void setup() {
reset(pushNotificationScheduler);
final List<Device> singleDeviceList = List.of(
generateTestDevice(SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, SINGLE_DEVICE_PNI_REG_ID1, true)
);
@@ -630,8 +635,13 @@ class MessageControllerTest {
}
@ParameterizedTest
@MethodSource
void testGetMessages(boolean receiveStories) {
@CsvSource({
"false, false",
"false, true",
"true, false",
"true, true"
})
void testGetMessages(final boolean receiveStories, final boolean hasMore) {
final long timestampOne = 313377;
final long timestampTwo = 313388;
@@ -651,7 +661,7 @@ class MessageControllerTest {
);
when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_UUID), eq(AuthHelper.VALID_DEVICE), anyBoolean()))
.thenReturn(Mono.just(new Pair<>(envelopes, false)));
.thenReturn(Mono.just(new Pair<>(envelopes, hasMore)));
final String userAgent = "Test-UA";
@@ -685,13 +695,12 @@ class MessageControllerTest {
}
verify(pushNotificationManager).handleMessagesRetrieved(AuthHelper.VALID_ACCOUNT, AuthHelper.VALID_DEVICE, userAgent);
}
private static Stream<Arguments> testGetMessages() {
return Stream.of(
Arguments.of(true),
Arguments.of(false)
);
if (hasMore) {
verify(pushNotificationScheduler).scheduleDelayedNotification(eq(AuthHelper.VALID_ACCOUNT), eq(AuthHelper.VALID_DEVICE), any());
} else {
verify(pushNotificationScheduler, never()).scheduleDelayedNotification(any(), any(), any());
}
}
@Test

View File

@@ -1,252 +0,0 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.push;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import io.lettuce.core.cluster.SlotHash;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.TestClock;
class ApnPushNotificationSchedulerTest {
@RegisterExtension
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
private Account account;
private Device device;
private APNSender apnSender;
private TestClock clock;
private ApnPushNotificationScheduler apnPushNotificationScheduler;
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
private static final String ACCOUNT_NUMBER = "+18005551234";
private static final byte DEVICE_ID = 1;
private static final String APN_ID = RandomStringUtils.randomAlphanumeric(32);
private static final String VOIP_APN_ID = RandomStringUtils.randomAlphanumeric(32);
@BeforeEach
void setUp() throws Exception {
device = mock(Device.class);
when(device.getId()).thenReturn(DEVICE_ID);
when(device.getApnId()).thenReturn(APN_ID);
when(device.getVoipApnId()).thenReturn(VOIP_APN_ID);
when(device.getLastSeen()).thenReturn(System.currentTimeMillis());
account = mock(Account.class);
when(account.getUuid()).thenReturn(ACCOUNT_UUID);
when(account.getNumber()).thenReturn(ACCOUNT_NUMBER);
when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device));
final AccountsManager accountsManager = mock(AccountsManager.class);
when(accountsManager.getByE164(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(ACCOUNT_UUID)).thenReturn(Optional.of(account));
apnSender = mock(APNSender.class);
clock = TestClock.now();
apnPushNotificationScheduler = new ApnPushNotificationScheduler(REDIS_CLUSTER_EXTENSION.getRedisCluster(),
apnSender, accountsManager, clock, 1);
}
@Test
void testClusterInsert() throws ExecutionException, InterruptedException {
final String endpoint = ApnPushNotificationScheduler.getEndpointKey(account, device);
final long currentTimeMillis = System.currentTimeMillis();
assertTrue(
apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty());
clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000));
apnPushNotificationScheduler.scheduleRecurringVoipNotification(account, device).toCompletableFuture().get();
clock.pin(Instant.ofEpochMilli(currentTimeMillis));
final List<String> pendingDestinations = apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 2);
assertEquals(1, pendingDestinations.size());
final Optional<Pair<String, Byte>> maybeUuidAndDeviceId = ApnPushNotificationScheduler.getSeparated(
pendingDestinations.get(0));
assertTrue(maybeUuidAndDeviceId.isPresent());
assertEquals(ACCOUNT_UUID.toString(), maybeUuidAndDeviceId.get().first());
assertEquals(DEVICE_ID, maybeUuidAndDeviceId.get().second());
assertTrue(
apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty());
}
@Test
void testProcessRecurringVoipNotifications() throws ExecutionException, InterruptedException {
final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker();
final long currentTimeMillis = System.currentTimeMillis();
clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000));
apnPushNotificationScheduler.scheduleRecurringVoipNotification(account, device).toCompletableFuture().get();
clock.pin(Instant.ofEpochMilli(currentTimeMillis));
final int slot = SlotHash.getSlot(ApnPushNotificationScheduler.getEndpointKey(account, device));
assertEquals(1, worker.processRecurringVoipNotifications(slot));
final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
verify(apnSender).sendNotification(notificationCaptor.capture());
final PushNotification pushNotification = notificationCaptor.getValue();
assertEquals(VOIP_APN_ID, pushNotification.deviceToken());
assertEquals(account, pushNotification.destination());
assertEquals(device, pushNotification.destinationDevice());
assertEquals(0, worker.processRecurringVoipNotifications(slot));
}
@Test
void testScheduleBackgroundNotificationWithNoRecentNotification() throws ExecutionException, InterruptedException {
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(now);
assertEquals(Optional.empty(),
apnPushNotificationScheduler.getLastBackgroundNotificationTimestamp(account, device));
assertEquals(Optional.empty(),
apnPushNotificationScheduler.getNextScheduledBackgroundNotificationTimestamp(account, device));
apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get();
assertEquals(Optional.of(now),
apnPushNotificationScheduler.getNextScheduledBackgroundNotificationTimestamp(account, device));
}
@Test
void testScheduleBackgroundNotificationWithRecentNotification() throws ExecutionException, InterruptedException {
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
final Instant recentNotificationTimestamp =
now.minus(ApnPushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD.dividedBy(2));
// Insert a timestamp for a recently-sent background push notification
clock.pin(Instant.ofEpochMilli(recentNotificationTimestamp.toEpochMilli()));
apnPushNotificationScheduler.sendBackgroundNotification(account, device);
clock.pin(now);
apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get();
final Instant expectedScheduledTimestamp =
recentNotificationTimestamp.plus(ApnPushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD);
assertEquals(Optional.of(expectedScheduledTimestamp),
apnPushNotificationScheduler.getNextScheduledBackgroundNotificationTimestamp(account, device));
}
@Test
void testProcessScheduledBackgroundNotifications() throws ExecutionException, InterruptedException {
final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker();
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(Instant.ofEpochMilli(now.toEpochMilli()));
apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get();
final int slot =
SlotHash.getSlot(ApnPushNotificationScheduler.getPendingBackgroundNotificationQueueKey(account, device));
clock.pin(Instant.ofEpochMilli(now.minusMillis(1).toEpochMilli()));
assertEquals(0, worker.processScheduledBackgroundNotifications(slot));
clock.pin(now);
assertEquals(1, worker.processScheduledBackgroundNotifications(slot));
final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
verify(apnSender).sendNotification(notificationCaptor.capture());
final PushNotification pushNotification = notificationCaptor.getValue();
assertEquals(PushNotification.TokenType.APN, pushNotification.tokenType());
assertEquals(APN_ID, pushNotification.deviceToken());
assertEquals(account, pushNotification.destination());
assertEquals(device, pushNotification.destinationDevice());
assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType());
assertFalse(pushNotification.urgent());
assertEquals(0, worker.processRecurringVoipNotifications(slot));
}
@Test
void testProcessScheduledBackgroundNotificationsCancelled() throws ExecutionException, InterruptedException {
final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker();
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(now);
apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get();
apnPushNotificationScheduler.cancelScheduledNotifications(account, device).toCompletableFuture().get();
final int slot =
SlotHash.getSlot(ApnPushNotificationScheduler.getPendingBackgroundNotificationQueueKey(account, device));
assertEquals(0, worker.processScheduledBackgroundNotifications(slot));
verify(apnSender, never()).sendNotification(any());
}
@ParameterizedTest
@CsvSource({
"1, true",
"0, false",
})
void testDedicatedProcessDynamicConfiguration(final int dedicatedThreadCount, final boolean expectActivity)
throws Exception {
final FaultTolerantRedisCluster redisCluster = mock(FaultTolerantRedisCluster.class);
when(redisCluster.withCluster(any())).thenReturn(0L);
final AccountsManager accountsManager = mock(AccountsManager.class);
apnPushNotificationScheduler = new ApnPushNotificationScheduler(redisCluster, apnSender,
accountsManager, dedicatedThreadCount);
apnPushNotificationScheduler.start();
apnPushNotificationScheduler.stop();
if (expectActivity) {
verify(redisCluster, atLeastOnce()).withCluster(any());
} else {
verifyNoInteractions(redisCluster);
verifyNoInteractions(accountsManager);
verifyNoInteractions(apnSender);
}
}
}

View File

@@ -33,7 +33,7 @@ class PushNotificationManagerTest {
private AccountsManager accountsManager;
private APNSender apnSender;
private FcmSender fcmSender;
private ApnPushNotificationScheduler apnPushNotificationScheduler;
private PushNotificationScheduler pushNotificationScheduler;
private PushNotificationManager pushNotificationManager;
@@ -42,12 +42,12 @@ class PushNotificationManagerTest {
accountsManager = mock(AccountsManager.class);
apnSender = mock(APNSender.class);
fcmSender = mock(FcmSender.class);
apnPushNotificationScheduler = mock(ApnPushNotificationScheduler.class);
pushNotificationScheduler = mock(PushNotificationScheduler.class);
AccountsHelper.setupMockUpdate(accountsManager);
pushNotificationManager =
new PushNotificationManager(accountsManager, apnSender, fcmSender, apnPushNotificationScheduler);
new PushNotificationManager(accountsManager, apnSender, fcmSender, pushNotificationScheduler);
}
@ParameterizedTest
@@ -152,7 +152,7 @@ class PushNotificationManagerTest {
verifyNoInteractions(apnSender);
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
verify(device, never()).setGcmId(any());
verifyNoInteractions(apnPushNotificationScheduler);
verifyNoInteractions(pushNotificationScheduler);
}
@ParameterizedTest
@@ -171,7 +171,7 @@ class PushNotificationManagerTest {
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty())));
if (!urgent) {
when(apnPushNotificationScheduler.scheduleBackgroundNotification(account, device))
when(pushNotificationScheduler.scheduleBackgroundApnsNotification(account, device))
.thenReturn(CompletableFuture.completedFuture(null));
}
@@ -181,10 +181,10 @@ class PushNotificationManagerTest {
if (urgent) {
verify(apnSender).sendNotification(pushNotification);
verifyNoInteractions(apnPushNotificationScheduler);
verifyNoInteractions(pushNotificationScheduler);
} else {
verifyNoInteractions(apnSender);
verify(apnPushNotificationScheduler).scheduleBackgroundNotification(account, device);
verify(pushNotificationScheduler).scheduleBackgroundApnsNotification(account, device);
}
}
@@ -210,8 +210,8 @@ class PushNotificationManagerTest {
verifyNoInteractions(fcmSender);
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
verify(device, never()).setGcmId(any());
verify(apnPushNotificationScheduler).scheduleRecurringVoipNotification(account, device);
verify(apnPushNotificationScheduler, never()).scheduleBackgroundNotification(any(), any());
verify(pushNotificationScheduler).scheduleRecurringApnsVoipNotification(account, device);
verify(pushNotificationScheduler, never()).scheduleBackgroundApnsNotification(any(), any());
}
@Test
@@ -236,7 +236,7 @@ class PushNotificationManagerTest {
verify(accountsManager).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
verify(device).setGcmId(null);
verifyNoInteractions(apnSender);
verifyNoInteractions(apnPushNotificationScheduler);
verifyNoInteractions(pushNotificationScheduler);
}
@Test
@@ -257,7 +257,7 @@ class PushNotificationManagerTest {
when(apnSender.sendNotification(pushNotification))
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, Optional.empty(), true, Optional.empty())));
when(apnPushNotificationScheduler.cancelScheduledNotifications(account, device))
when(pushNotificationScheduler.cancelScheduledNotifications(account, device))
.thenReturn(CompletableFuture.completedFuture(null));
pushNotificationManager.sendNotification(pushNotification);
@@ -266,7 +266,7 @@ class PushNotificationManagerTest {
verify(accountsManager).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
verify(device).setVoipApnId(null);
verify(device, never()).setApnId(any());
verify(apnPushNotificationScheduler).cancelScheduledNotifications(account, device);
verify(pushNotificationScheduler).cancelScheduledNotifications(account, device);
}
@Test
@@ -290,7 +290,7 @@ class PushNotificationManagerTest {
when(apnSender.sendNotification(pushNotification))
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, Optional.empty(), true, Optional.of(tokenTimestamp.minusSeconds(60)))));
when(apnPushNotificationScheduler.cancelScheduledNotifications(account, device))
when(pushNotificationScheduler.cancelScheduledNotifications(account, device))
.thenReturn(CompletableFuture.completedFuture(null));
pushNotificationManager.sendNotification(pushNotification);
@@ -299,7 +299,7 @@ class PushNotificationManagerTest {
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
verify(device, never()).setVoipApnId(any());
verify(device, never()).setApnId(any());
verify(apnPushNotificationScheduler, never()).cancelScheduledNotifications(account, device);
verify(pushNotificationScheduler, never()).cancelScheduledNotifications(account, device);
}
@Test
@@ -312,11 +312,11 @@ class PushNotificationManagerTest {
when(account.getUuid()).thenReturn(accountIdentifier);
when(device.getId()).thenReturn(Device.PRIMARY_ID);
when(apnPushNotificationScheduler.cancelScheduledNotifications(account, device))
when(pushNotificationScheduler.cancelScheduledNotifications(account, device))
.thenReturn(CompletableFuture.completedFuture(null));
pushNotificationManager.handleMessagesRetrieved(account, device, userAgent);
verify(apnPushNotificationScheduler).cancelScheduledNotifications(account, device);
verify(pushNotificationScheduler).cancelScheduledNotifications(account, device);
}
}

View File

@@ -0,0 +1,327 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.push;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import io.lettuce.core.cluster.SlotHash;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.TestClock;
class PushNotificationSchedulerTest {
@RegisterExtension
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
private Account account;
private Device device;
private APNSender apnSender;
private FcmSender fcmSender;
private TestClock clock;
private PushNotificationScheduler pushNotificationScheduler;
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
private static final String ACCOUNT_NUMBER = "+18005551234";
private static final byte DEVICE_ID = 1;
private static final String APN_ID = RandomStringUtils.randomAlphanumeric(32);
private static final String VOIP_APN_ID = RandomStringUtils.randomAlphanumeric(32);
@BeforeEach
void setUp() throws Exception {
device = mock(Device.class);
when(device.getId()).thenReturn(DEVICE_ID);
when(device.getApnId()).thenReturn(APN_ID);
when(device.getVoipApnId()).thenReturn(VOIP_APN_ID);
when(device.getLastSeen()).thenReturn(System.currentTimeMillis());
account = mock(Account.class);
when(account.getUuid()).thenReturn(ACCOUNT_UUID);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(ACCOUNT_UUID);
when(account.getNumber()).thenReturn(ACCOUNT_NUMBER);
when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device));
final AccountsManager accountsManager = mock(AccountsManager.class);
when(accountsManager.getByE164(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifierAsync(ACCOUNT_UUID))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
apnSender = mock(APNSender.class);
fcmSender = mock(FcmSender.class);
clock = TestClock.now();
when(apnSender.sendNotification(any()))
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty())));
when(fcmSender.sendNotification(any()))
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty())));
pushNotificationScheduler = new PushNotificationScheduler(REDIS_CLUSTER_EXTENSION.getRedisCluster(),
apnSender, fcmSender, accountsManager, clock, 1, 1);
}
@Test
void testClusterInsert() throws ExecutionException, InterruptedException {
final String endpoint = PushNotificationScheduler.getVoipEndpointKey(ACCOUNT_UUID, DEVICE_ID);
final long currentTimeMillis = System.currentTimeMillis();
assertTrue(
pushNotificationScheduler.getPendingDestinationsForRecurringApnsVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty());
clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000));
pushNotificationScheduler.scheduleRecurringApnsVoipNotification(account, device).toCompletableFuture().get();
clock.pin(Instant.ofEpochMilli(currentTimeMillis));
final List<String> pendingDestinations = pushNotificationScheduler.getPendingDestinationsForRecurringApnsVoipNotifications(SlotHash.getSlot(endpoint), 2);
assertEquals(1, pendingDestinations.size());
final Pair<UUID, Byte> aciAndDeviceId =
PushNotificationScheduler.decodeAciAndDeviceId(pendingDestinations.getFirst());
assertEquals(ACCOUNT_UUID, aciAndDeviceId.first());
assertEquals(DEVICE_ID, aciAndDeviceId.second());
assertTrue(
pushNotificationScheduler.getPendingDestinationsForRecurringApnsVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty());
}
@Test
void testProcessRecurringVoipNotifications() throws ExecutionException, InterruptedException {
final PushNotificationScheduler.NotificationWorker worker = pushNotificationScheduler.new NotificationWorker(1);
final long currentTimeMillis = System.currentTimeMillis();
clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000));
pushNotificationScheduler.scheduleRecurringApnsVoipNotification(account, device).toCompletableFuture().get();
clock.pin(Instant.ofEpochMilli(currentTimeMillis));
final int slot = SlotHash.getSlot(PushNotificationScheduler.getVoipEndpointKey(ACCOUNT_UUID, DEVICE_ID));
assertEquals(1, worker.processRecurringApnsVoipNotifications(slot));
final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
verify(apnSender).sendNotification(notificationCaptor.capture());
final PushNotification pushNotification = notificationCaptor.getValue();
assertEquals(VOIP_APN_ID, pushNotification.deviceToken());
assertEquals(account, pushNotification.destination());
assertEquals(device, pushNotification.destinationDevice());
assertEquals(0, worker.processRecurringApnsVoipNotifications(slot));
}
@Test
void testScheduleBackgroundNotificationWithNoRecentApnsNotification() throws ExecutionException, InterruptedException {
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(now);
assertEquals(Optional.empty(),
pushNotificationScheduler.getLastBackgroundApnsNotificationTimestamp(account, device));
assertEquals(Optional.empty(),
pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
pushNotificationScheduler.scheduleBackgroundApnsNotification(account, device).toCompletableFuture().get();
assertEquals(Optional.of(now),
pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
}
@Test
void testScheduleBackgroundNotificationWithRecentApnsNotification() throws ExecutionException, InterruptedException {
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
final Instant recentNotificationTimestamp =
now.minus(PushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD.dividedBy(2));
// Insert a timestamp for a recently-sent background push notification
clock.pin(Instant.ofEpochMilli(recentNotificationTimestamp.toEpochMilli()));
pushNotificationScheduler.sendBackgroundApnsNotification(account, device);
clock.pin(now);
pushNotificationScheduler.scheduleBackgroundApnsNotification(account, device).toCompletableFuture().get();
final Instant expectedScheduledTimestamp =
recentNotificationTimestamp.plus(PushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD);
assertEquals(Optional.of(expectedScheduledTimestamp),
pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
}
@Test
void testCancelBackgroundApnsNotifications() {
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(now);
pushNotificationScheduler.scheduleBackgroundApnsNotification(account, device).toCompletableFuture().join();
pushNotificationScheduler.cancelBackgroundApnsNotifications(account, device).join();
assertEquals(Optional.empty(),
pushNotificationScheduler.getLastBackgroundApnsNotificationTimestamp(account, device));
assertEquals(Optional.empty(),
pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
}
@Test
void testProcessScheduledBackgroundNotifications() {
final PushNotificationScheduler.NotificationWorker worker = pushNotificationScheduler.new NotificationWorker(1);
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(Instant.ofEpochMilli(now.toEpochMilli()));
pushNotificationScheduler.scheduleBackgroundApnsNotification(account, device).toCompletableFuture().join();
final int slot =
SlotHash.getSlot(PushNotificationScheduler.getPendingBackgroundApnsNotificationQueueKey(account, device));
clock.pin(Instant.ofEpochMilli(now.minusMillis(1).toEpochMilli()));
assertEquals(0, worker.processScheduledBackgroundApnsNotifications(slot));
clock.pin(now);
assertEquals(1, worker.processScheduledBackgroundApnsNotifications(slot));
final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
verify(apnSender).sendNotification(notificationCaptor.capture());
final PushNotification pushNotification = notificationCaptor.getValue();
assertEquals(PushNotification.TokenType.APN, pushNotification.tokenType());
assertEquals(APN_ID, pushNotification.deviceToken());
assertEquals(account, pushNotification.destination());
assertEquals(device, pushNotification.destinationDevice());
assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType());
assertFalse(pushNotification.urgent());
assertEquals(0, worker.processRecurringApnsVoipNotifications(slot));
assertEquals(Optional.empty(),
pushNotificationScheduler.getNextScheduledBackgroundApnsNotificationTimestamp(account, device));
}
@Test
void testProcessScheduledBackgroundNotificationsCancelled() throws ExecutionException, InterruptedException {
final PushNotificationScheduler.NotificationWorker worker = pushNotificationScheduler.new NotificationWorker(1);
final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
clock.pin(now);
pushNotificationScheduler.scheduleBackgroundApnsNotification(account, device).toCompletableFuture().get();
pushNotificationScheduler.cancelScheduledNotifications(account, device).toCompletableFuture().get();
final int slot =
SlotHash.getSlot(PushNotificationScheduler.getPendingBackgroundApnsNotificationQueueKey(account, device));
assertEquals(0, worker.processScheduledBackgroundApnsNotifications(slot));
verify(apnSender, never()).sendNotification(any());
}
@Test
void testScheduleDelayedNotification() {
clock.pin(Instant.now());
assertEquals(Optional.empty(),
pushNotificationScheduler.getNextScheduledDelayedNotificationTimestamp(account, device));
pushNotificationScheduler.scheduleDelayedNotification(account, device, Duration.ofMinutes(1)).join();
assertEquals(Optional.of(clock.instant().truncatedTo(ChronoUnit.MILLIS).plus(Duration.ofMinutes(1))),
pushNotificationScheduler.getNextScheduledDelayedNotificationTimestamp(account, device));
pushNotificationScheduler.scheduleDelayedNotification(account, device, Duration.ofMinutes(2)).join();
assertEquals(Optional.of(clock.instant().truncatedTo(ChronoUnit.MILLIS).plus(Duration.ofMinutes(2))),
pushNotificationScheduler.getNextScheduledDelayedNotificationTimestamp(account, device));
}
@Test
void testCancelDelayedNotification() {
pushNotificationScheduler.scheduleDelayedNotification(account, device, Duration.ofMinutes(1)).join();
pushNotificationScheduler.cancelDelayedNotifications(account, device).join();
assertEquals(Optional.empty(),
pushNotificationScheduler.getNextScheduledDelayedNotificationTimestamp(account, device));
}
@Test
void testProcessScheduledDelayedNotifications() {
final PushNotificationScheduler.NotificationWorker worker = pushNotificationScheduler.new NotificationWorker(1);
final int slot = SlotHash.getSlot(PushNotificationScheduler.getDelayedNotificationQueueKey(account, device));
clock.pin(Instant.now());
pushNotificationScheduler.scheduleDelayedNotification(account, device, Duration.ofMinutes(1)).join();
assertEquals(0, worker.processScheduledDelayedNotifications(slot));
clock.pin(clock.instant().plus(Duration.ofMinutes(1)));
assertEquals(1, worker.processScheduledDelayedNotifications(slot));
assertEquals(Optional.empty(),
pushNotificationScheduler.getNextScheduledDelayedNotificationTimestamp(account, device));
}
@ParameterizedTest
@CsvSource({
"1, true",
"0, false",
})
void testDedicatedProcessDynamicConfiguration(final int dedicatedThreadCount, final boolean expectActivity)
throws Exception {
final FaultTolerantRedisCluster redisCluster = mock(FaultTolerantRedisCluster.class);
when(redisCluster.withCluster(any())).thenReturn(0L);
final AccountsManager accountsManager = mock(AccountsManager.class);
pushNotificationScheduler = new PushNotificationScheduler(redisCluster, apnSender, fcmSender,
accountsManager, dedicatedThreadCount, 1);
pushNotificationScheduler.start();
pushNotificationScheduler.stop();
if (expectActivity) {
verify(redisCluster, atLeastOnce()).withCluster(any());
} else {
verifyNoInteractions(redisCluster);
verifyNoInteractions(accountsManager);
verifyNoInteractions(apnSender);
}
}
}

View File

@@ -48,6 +48,7 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -127,6 +128,7 @@ class WebSocketConnectionIntegrationTest {
new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, sharedExecutorService),
new MessageMetrics(),
mock(PushNotificationManager.class),
mock(PushNotificationScheduler.class),
new AuthenticatedDevice(account, device),
webSocketClient,
scheduledExecutorService,
@@ -213,6 +215,7 @@ class WebSocketConnectionIntegrationTest {
new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, sharedExecutorService),
new MessageMetrics(),
mock(PushNotificationManager.class),
mock(PushNotificationScheduler.class),
new AuthenticatedDevice(account, device),
webSocketClient,
scheduledExecutorService,
@@ -280,6 +283,7 @@ class WebSocketConnectionIntegrationTest {
new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, sharedExecutorService),
new MessageMetrics(),
mock(PushNotificationManager.class),
mock(PushNotificationScheduler.class),
new AuthenticatedDevice(account, device),
webSocketClient,
100, // use a very short timeout, so that this test completes quickly

View File

@@ -59,6 +59,7 @@ import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
@@ -122,8 +123,8 @@ class WebSocketConnectionTest {
WebSocketAccountAuthenticator webSocketAuthenticator =
new WebSocketAccountAuthenticator(accountAuthenticator, mock(PrincipalSupplier.class));
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(receiptSender, messagesManager,
new MessageMetrics(), mock(PushNotificationManager.class), mock(ClientPresenceManager.class),
retrySchedulingExecutor, messageDeliveryScheduler, clientReleaseManager);
new MessageMetrics(), mock(PushNotificationManager.class), mock(PushNotificationScheduler.class),
mock(ClientPresenceManager.class), retrySchedulingExecutor, messageDeliveryScheduler, clientReleaseManager);
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
@@ -626,7 +627,7 @@ class WebSocketConnectionTest {
private @NotNull WebSocketConnection webSocketConnection(final WebSocketClient client) {
return new WebSocketConnection(receiptSender, messagesManager, new MessageMetrics(),
mock(PushNotificationManager.class), auth, client,
mock(PushNotificationManager.class), mock(PushNotificationScheduler.class), auth, client,
retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager);
}