From 4c9b5926b912c4deec674d8fdd50201cbbaf55b5 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 16 Aug 2024 15:39:33 -0400 Subject: [PATCH] Do not enqueue no-op read receipt jobs. --- .../securesms/jobs/SendReadReceiptJob.java | 4 + .../MockApplicationDependencyProvider.java | 247 ------------------ .../MockApplicationDependencyProvider.kt | 205 +++++++++++++++ .../notifications/MarkReadReceiverTest.java | 115 -------- .../notifications/MarkReadReceiverTest.kt | 104 ++++++++ 5 files changed, 313 insertions(+), 362 deletions(-) delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java index 35f226810a..b884340258 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java @@ -98,6 +98,10 @@ public class SendReadReceiptJob extends BaseJob { * maximum size. */ public static void enqueue(long threadId, @NonNull RecipientId recipientId, List markedMessageInfos) { + if (!TextSecurePreferences.isReadReceiptsEnabled(AppDependencies.getApplication())) { + return; + } + if (recipientId.equals(Recipient.self().getId())) { return; } diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java deleted file mode 100644 index 2a92c7c198..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java +++ /dev/null @@ -1,247 +0,0 @@ -package org.thoughtcrime.securesms.dependencies; - -import androidx.annotation.NonNull; - -import org.signal.core.util.concurrent.DeadlockDetector; -import org.signal.libsignal.net.Network; -import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; -import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; -import org.thoughtcrime.securesms.billing.GooglePlayBillingApi; -import org.thoughtcrime.securesms.components.TypingStatusRepository; -import org.thoughtcrime.securesms.components.TypingStatusSender; -import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl; -import org.thoughtcrime.securesms.database.DatabaseObserver; -import org.thoughtcrime.securesms.database.PendingRetryReceiptCache; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; -import org.thoughtcrime.securesms.messages.IncomingMessageObserver; -import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.payments.Payments; -import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; -import org.thoughtcrime.securesms.recipients.LiveRecipientCache; -import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager; -import org.thoughtcrime.securesms.service.DeletedCallEventManager; -import org.thoughtcrime.securesms.service.ExpiringMessageManager; -import org.thoughtcrime.securesms.service.ExpiringStoriesManager; -import org.thoughtcrime.securesms.service.PendingRetryReceiptManager; -import org.thoughtcrime.securesms.service.ScheduledMessageManager; -import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; -import org.thoughtcrime.securesms.service.webrtc.SignalCallManager; -import org.thoughtcrime.securesms.shakereport.ShakeToReport; -import org.thoughtcrime.securesms.util.AppForegroundObserver; -import org.thoughtcrime.securesms.util.EarlyMessageCache; -import org.thoughtcrime.securesms.util.FrameRateTracker; -import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache; -import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool; -import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.SignalServiceDataStore; -import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; -import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.SignalWebSocket; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; -import org.whispersystems.signalservice.api.services.CallLinksService; -import org.whispersystems.signalservice.api.services.DonationsService; -import org.whispersystems.signalservice.api.services.ProfileService; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; - -import java.util.function.Supplier; - -import static org.mockito.Mockito.mock; - -@SuppressWarnings("ConstantConditions") -public class MockApplicationDependencyProvider implements AppDependencies.Provider { - @Override - public @NonNull GroupsV2Operations provideGroupsV2Operations(@NonNull SignalServiceConfiguration signalServiceConfiguration) { - return null; - } - - @Override - public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) { - return null; - } - - @Override - public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore, @NonNull SignalServiceConfiguration signalServiceConfiguration) { - return null; - } - - @Override - public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(@NonNull SignalServiceConfiguration signalServiceConfiguration) { - return null; - } - - @Override - public @NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess() { - return null; - } - - @Override - public @NonNull LiveRecipientCache provideRecipientCache() { - return null; - } - - @Override - public @NonNull JobManager provideJobManager() { - return null; - } - - @Override - public @NonNull FrameRateTracker provideFrameRateTracker() { - return null; - } - - @Override - public @NonNull MegaphoneRepository provideMegaphoneRepository() { - return null; - } - - @Override - public @NonNull EarlyMessageCache provideEarlyMessageCache() { - return null; - } - - @Override - public @NonNull MessageNotifier provideMessageNotifier() { - return null; - } - - @Override - public @NonNull IncomingMessageObserver provideIncomingMessageObserver() { - return null; - } - - @Override - public @NonNull TrimThreadsByDateManager provideTrimThreadsByDateManager() { - return null; - } - - @Override - public @NonNull ViewOnceMessageManager provideViewOnceMessageManager() { - return null; - } - - @Override - public @NonNull ExpiringStoriesManager provideExpiringStoriesManager() { - return null; - } - - @Override - public @NonNull ExpiringMessageManager provideExpiringMessageManager() { - return null; - } - - @Override - public @NonNull DeletedCallEventManager provideDeletedCallEventManager() { - return null; - } - - @Override - public @NonNull TypingStatusRepository provideTypingStatusRepository() { - return null; - } - - @Override - public @NonNull TypingStatusSender provideTypingStatusSender() { - return null; - } - - @Override - public @NonNull DatabaseObserver provideDatabaseObserver() { - return mock(DatabaseObserver.class); - } - - @Override - public @NonNull Payments providePayments(@NonNull SignalServiceAccountManager signalServiceAccountManager) { - return null; - } - - @Override - public @NonNull ShakeToReport provideShakeToReport() { - return null; - } - - @Override - public @NonNull AppForegroundObserver provideAppForegroundObserver() { - return mock(AppForegroundObserver.class); - } - - @Override - public @NonNull SignalCallManager provideSignalCallManager() { - return null; - } - - @Override - public @NonNull PendingRetryReceiptManager providePendingRetryReceiptManager() { - return null; - } - - @Override - public @NonNull PendingRetryReceiptCache providePendingRetryReceiptCache() { - return null; - } - - @Override - public @NonNull SignalWebSocket provideSignalWebSocket(@NonNull Supplier signalServiceConfigurationSupplier, @NonNull Supplier libSignalNetworkSupplier) { - return null; - } - - @Override - public @NonNull SignalServiceDataStoreImpl provideProtocolStore() { - return null; - } - - @Override - public @NonNull GiphyMp4Cache provideGiphyMp4Cache() { - return null; - } - - @Override - public @NonNull SimpleExoPlayerPool provideExoPlayerPool() { - return null; - } - - @Override - public @NonNull AudioManagerCompat provideAndroidCallAudioManager() { - return null; - } - - @Override - public @NonNull DonationsService provideDonationsService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) { - return null; - } - - @NonNull @Override public CallLinksService provideCallLinksService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) { - return null; - } - - @Override - public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket) { - return null; - } - - @Override - public @NonNull DeadlockDetector provideDeadlockDetector() { - return null; - } - - @Override - public @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration) { - return null; - } - - @Override - public @NonNull ScheduledMessageManager provideScheduledMessageManager() { - return null; - } - - @Override - public @NonNull Network provideLibsignalNetwork(@NonNull SignalServiceConfiguration config) { - return null; - } - - @Override - public @NonNull GooglePlayBillingApi provideBillingApi() { - return null; - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt new file mode 100644 index 0000000000..31092b10ed --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt @@ -0,0 +1,205 @@ +package org.thoughtcrime.securesms.dependencies + +import io.mockk.mockk +import org.mockito.Mockito +import org.signal.core.util.concurrent.DeadlockDetector +import org.signal.libsignal.net.Network +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations +import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations +import org.thoughtcrime.securesms.billing.GooglePlayBillingApi +import org.thoughtcrime.securesms.components.TypingStatusRepository +import org.thoughtcrime.securesms.components.TypingStatusSender +import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl +import org.thoughtcrime.securesms.database.DatabaseObserver +import org.thoughtcrime.securesms.database.PendingRetryReceiptCache +import org.thoughtcrime.securesms.jobmanager.JobManager +import org.thoughtcrime.securesms.megaphone.MegaphoneRepository +import org.thoughtcrime.securesms.messages.IncomingMessageObserver +import org.thoughtcrime.securesms.notifications.MessageNotifier +import org.thoughtcrime.securesms.payments.Payments +import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess +import org.thoughtcrime.securesms.recipients.LiveRecipientCache +import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager +import org.thoughtcrime.securesms.service.DeletedCallEventManager +import org.thoughtcrime.securesms.service.ExpiringMessageManager +import org.thoughtcrime.securesms.service.ExpiringStoriesManager +import org.thoughtcrime.securesms.service.PendingRetryReceiptManager +import org.thoughtcrime.securesms.service.ScheduledMessageManager +import org.thoughtcrime.securesms.service.TrimThreadsByDateManager +import org.thoughtcrime.securesms.service.webrtc.SignalCallManager +import org.thoughtcrime.securesms.shakereport.ShakeToReport +import org.thoughtcrime.securesms.util.AppForegroundObserver +import org.thoughtcrime.securesms.util.EarlyMessageCache +import org.thoughtcrime.securesms.util.FrameRateTracker +import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache +import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool +import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat +import org.whispersystems.signalservice.api.SignalServiceAccountManager +import org.whispersystems.signalservice.api.SignalServiceDataStore +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver +import org.whispersystems.signalservice.api.SignalServiceMessageSender +import org.whispersystems.signalservice.api.SignalWebSocket +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations +import org.whispersystems.signalservice.api.services.CallLinksService +import org.whispersystems.signalservice.api.services.DonationsService +import org.whispersystems.signalservice.api.services.ProfileService +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration +import java.util.function.Supplier + +class MockApplicationDependencyProvider : AppDependencies.Provider { + override fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations { + return mockk() + } + + override fun provideSignalServiceAccountManager(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager { + return mockk() + } + + override fun provideSignalServiceMessageSender(signalWebSocket: SignalWebSocket, protocolStore: SignalServiceDataStore, signalServiceConfiguration: SignalServiceConfiguration): SignalServiceMessageSender { + return mockk() + } + + override fun provideSignalServiceMessageReceiver(signalServiceConfiguration: SignalServiceConfiguration): SignalServiceMessageReceiver { + return mockk() + } + + override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess { + return mockk() + } + + override fun provideRecipientCache(): LiveRecipientCache { + return mockk() + } + + override fun provideJobManager(): JobManager { + return mockk() + } + + override fun provideFrameRateTracker(): FrameRateTracker { + return mockk() + } + + override fun provideMegaphoneRepository(): MegaphoneRepository { + return mockk() + } + + override fun provideEarlyMessageCache(): EarlyMessageCache { + return mockk() + } + + override fun provideMessageNotifier(): MessageNotifier { + return mockk() + } + + override fun provideIncomingMessageObserver(): IncomingMessageObserver { + return mockk() + } + + override fun provideTrimThreadsByDateManager(): TrimThreadsByDateManager { + return mockk() + } + + override fun provideViewOnceMessageManager(): ViewOnceMessageManager { + return mockk() + } + + override fun provideExpiringStoriesManager(): ExpiringStoriesManager { + return mockk() + } + + override fun provideExpiringMessageManager(): ExpiringMessageManager { + return mockk() + } + + override fun provideDeletedCallEventManager(): DeletedCallEventManager { + return mockk() + } + + override fun provideTypingStatusRepository(): TypingStatusRepository { + return mockk() + } + + override fun provideTypingStatusSender(): TypingStatusSender { + return mockk() + } + + override fun provideDatabaseObserver(): DatabaseObserver { + return Mockito.mock(DatabaseObserver::class.java) + } + + override fun providePayments(signalServiceAccountManager: SignalServiceAccountManager): Payments { + return mockk() + } + + override fun provideShakeToReport(): ShakeToReport { + return mockk() + } + + override fun provideAppForegroundObserver(): AppForegroundObserver { + return Mockito.mock(AppForegroundObserver::class.java) + } + + override fun provideSignalCallManager(): SignalCallManager { + return mockk() + } + + override fun providePendingRetryReceiptManager(): PendingRetryReceiptManager { + return mockk() + } + + override fun providePendingRetryReceiptCache(): PendingRetryReceiptCache { + return mockk() + } + + override fun provideSignalWebSocket(signalServiceConfigurationSupplier: Supplier, libSignalNetworkSupplier: Supplier): SignalWebSocket { + return mockk() + } + + override fun provideProtocolStore(): SignalServiceDataStoreImpl { + return mockk() + } + + override fun provideGiphyMp4Cache(): GiphyMp4Cache { + return mockk() + } + + override fun provideExoPlayerPool(): SimpleExoPlayerPool { + return mockk() + } + + override fun provideAndroidCallAudioManager(): AudioManagerCompat { + return mockk() + } + + override fun provideDonationsService(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): DonationsService { + return mockk() + } + + override fun provideCallLinksService(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): CallLinksService { + return mockk() + } + + override fun provideProfileService(profileOperations: ClientZkProfileOperations, signalServiceMessageReceiver: SignalServiceMessageReceiver, signalWebSocket: SignalWebSocket): ProfileService { + return mockk() + } + + override fun provideDeadlockDetector(): DeadlockDetector { + return mockk() + } + + override fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations { + return mockk() + } + + override fun provideScheduledMessageManager(): ScheduledMessageManager { + return mockk() + } + + override fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network { + return mockk() + } + + override fun provideBillingApi(): GooglePlayBillingApi { + return mockk() + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java b/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java deleted file mode 100644 index 74bd2bbd4b..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.mockito.stubbing.Answer; -import org.signal.libsignal.protocol.util.Pair; -import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.model.MessageId; -import org.thoughtcrime.securesms.database.model.StoryType; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.Util; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class MarkReadReceiverTest { - - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - @Mock - private MockedStatic applicationDependenciesMockedStatic; - - @Mock - private MockedStatic recipientMockedStatic; - - private final Context mockContext = mock(Context.class); - private final JobManager mockJobManager = mock(JobManager.class); - private final Recipient mockSelf = mock(Recipient.class); - private final List jobs = new LinkedList<>(); - - @Before - public void setUp() { - applicationDependenciesMockedStatic.when(AppDependencies::getJobManager).thenReturn(mockJobManager); - doAnswer((Answer) invocation -> { - jobs.add((Job) invocation.getArguments()[0]); - return null; - }).when(mockJobManager).add(any()); - recipientMockedStatic.when(Recipient::self).thenReturn(mockSelf); - when(mockSelf.getId()).thenReturn(RecipientId.from(-1)); - } - - @Test - public void givenMultipleThreadsWithMultipleMessagesEach_whenIProcess_thenIProperlyGroupByThreadAndRecipient() { - // GIVEN - List recipients = Stream.range(1L, 4L).map(RecipientId::from).toList(); - List threads = Stream.range(4L, 7L).toList(); - int expected = recipients.size() * threads.size() + 1; - - List infoList = Stream.of(threads) - .flatMap(threadId -> Stream.of(recipients) - .map(recipientId -> createMarkedMessageInfo(threadId, recipientId))) - .toList(); - - List duplicatedList = Util.concatenatedList(infoList, infoList); - - // WHEN - MarkReadReceiver.process(duplicatedList); - - // THEN - assertEquals("Should have 10 total jobs, including MultiDeviceReadUpdateJob", expected, jobs.size()); - - Set> threadRecipientPairs = new HashSet<>(); - Stream.of(jobs).forEach(job -> { - if (job instanceof MultiDeviceReadUpdateJob) { - return; - } - - JsonJobData data = JsonJobData.deserialize(job.serialize()); - - long threadId = data.getLong("thread"); - String recipientId = data.getString("recipient"); - long[] messageIds = data.getLongArray("message_ids"); - - assertEquals("Each job should contain two messages.", 2, messageIds.length); - assertTrue("Each thread recipient pair should only exist once.", threadRecipientPairs.add(new Pair<>(threadId, recipientId))); - }); - - assertEquals("Should have 9 total combinations.", 9, threadRecipientPairs.size()); - } - - private MessageTable.MarkedMessageInfo createMarkedMessageInfo(long threadId, @NonNull RecipientId recipientId) { - return new MessageTable.MarkedMessageInfo(threadId, - new MessageTable.SyncMessageId(recipientId, 0), - new MessageId(1), - new MessageTable.ExpirationInfo(0, 0, 0, false), - StoryType.NONE); - } -} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.kt b/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.kt new file mode 100644 index 0000000000..f0dad25090 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.kt @@ -0,0 +1,104 @@ +package org.thoughtcrime.securesms.notifications + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.libsignal.protocol.util.Pair +import org.thoughtcrime.securesms.database.MessageTable.ExpirationInfo +import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo +import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.dependencies.MockApplicationDependencyProvider +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.JobManager +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.TextSecurePreferences +import java.util.LinkedList + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class MarkReadReceiverTest { + + private val jobs: MutableList = LinkedList() + + @Before + fun setUp() { + if (!AppDependencies.isInitialized) { + AppDependencies.init( + ApplicationProvider.getApplicationContext(), + MockApplicationDependencyProvider() + ) + } + + val jobManager: JobManager = AppDependencies.jobManager + every { jobManager.add(capture(jobs)) } returns Unit + + mockkObject(Recipient) + every { Recipient.self() } returns Recipient() + + mockkStatic(TextSecurePreferences::class) + every { TextSecurePreferences.isReadReceiptsEnabled(any()) } returns true + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun givenMultipleThreadsWithMultipleMessagesEach_whenIProcess_thenIProperlyGroupByThreadAndRecipient() { + // GIVEN + val recipients = (1L until 4L).map { id -> RecipientId.from(id) } + val threads = (4L until 7L).toList() + val expected = recipients.size * threads.size + 1 + val infoList = threads.map { threadId -> recipients.map { recipientId -> createMarkedMessageInfo(threadId, recipientId) } }.flatten() + + // WHEN + MarkReadReceiver.process(infoList + infoList) + + // THEN + Assert.assertEquals("Should have 10 total jobs, including MultiDeviceReadUpdateJob", expected.toLong(), jobs.size.toLong()) + + val threadRecipientPairs: MutableSet> = HashSet() + jobs.forEach { job -> + if (job is MultiDeviceReadUpdateJob) { + return@forEach + } + val data = JsonJobData.deserialize(job.serialize()) + + val threadId = data.getLong("thread") + val recipientId = data.getString("recipient") + val messageIds = data.getLongArray("message_ids") + + Assert.assertEquals("Each job should contain two messages.", 2, messageIds.size.toLong()) + Assert.assertTrue("Each thread recipient pair should only exist once.", threadRecipientPairs.add(Pair(threadId, recipientId))) + } + + Assert.assertEquals("Should have 9 total combinations.", 9, threadRecipientPairs.size.toLong()) + } + + private fun createMarkedMessageInfo(threadId: Long, recipientId: RecipientId): MarkedMessageInfo { + return MarkedMessageInfo( + threadId, + SyncMessageId(recipientId, 0), + MessageId(1), + ExpirationInfo(0, 0, 0, false), + StoryType.NONE + ) + } +}