mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 06:58:04 +01:00
Generalize push notification scheduler and add support for delayed "new messages" notifications
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user