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

@@ -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);
}
}
}