Convert handful of recipient/db heavy androidTests to regular unit tests.

This commit is contained in:
Cody Henthorne
2026-04-17 09:59:53 -04:00
committed by jeffrey-signal
parent 5b7f668251
commit 90207b7dd7
14 changed files with 543 additions and 350 deletions
@@ -1,232 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.UuidUtil
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
import org.whispersystems.signalservice.api.storage.StorageId
import java.util.UUID
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
import org.whispersystems.signalservice.internal.storage.protos.Recipient as RemoteRecipient
@RunWith(AndroidJUnit4::class)
class ChatFolderTablesTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var charlie: RecipientId
private lateinit var folder1: ChatFolderRecord
private lateinit var folder2: ChatFolderRecord
private lateinit var folder3: ChatFolderRecord
private lateinit var folder4: ChatFolderRecord
private lateinit var recipientIds: List<RecipientId>
private var aliceThread: Long = 0
private var bobThread: Long = 0
private var charlieThread: Long = 0
@Before
fun setUp() {
recipientIds = createRecipients(5)
alice = recipientIds[0]
bob = recipientIds[1]
charlie = recipientIds[2]
aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
bobThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(bob))
charlieThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(charlie))
folder1 = ChatFolderRecord(
id = 2,
name = "folder1",
position = 0,
includedChats = listOf(aliceThread, bobThread),
excludedChats = listOf(charlieThread),
showUnread = true,
showMutedChats = true,
showIndividualChats = true,
folderType = ChatFolderRecord.FolderType.CUSTOM,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(1, 2, 3))
)
folder2 = ChatFolderRecord(
name = "folder2",
position = 2,
includedChats = listOf(bobThread),
showUnread = true,
showMutedChats = true,
showIndividualChats = true,
folderType = ChatFolderRecord.FolderType.INDIVIDUAL,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(2, 3, 4))
)
folder3 = ChatFolderRecord(
name = "folder3",
position = 3,
includedChats = listOf(bobThread),
excludedChats = listOf(aliceThread, charlieThread),
showUnread = true,
showMutedChats = true,
showGroupChats = true,
folderType = ChatFolderRecord.FolderType.GROUP,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(3, 4, 5))
)
folder4 = ChatFolderRecord(
name = "folder4",
position = 4,
excludedChats = listOf(aliceThread, charlieThread),
showUnread = true,
showMutedChats = true,
showGroupChats = true,
folderType = ChatFolderRecord.FolderType.UNREAD,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(4, 5, 6))
)
SignalDatabase.chatFolders.writableDatabase.deleteAll(ChatFolderTables.ChatFolderTable.TABLE_NAME)
SignalDatabase.chatFolders.writableDatabase.deleteAll(ChatFolderTables.ChatFolderMembershipTable.TABLE_NAME)
}
@Test
fun givenChatFolder_whenIGetFolder_thenIExpectFolderWithChats() {
SignalDatabase.chatFolders.createFolder(folder1)
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
assertEquals(listOf(folder1), actualFolders)
}
@Test
fun givenChatFolder_whenIUpdateFolder_thenIExpectUpdatedFolderWithChats() {
SignalDatabase.chatFolders.createFolder(folder2)
val folder = SignalDatabase.chatFolders.getCurrentChatFolders().first()
val updatedFolder = folder.copy(
name = "updatedFolder2",
position = 1,
includedChats = listOf(aliceThread, charlieThread),
excludedChats = listOf(bobThread)
)
SignalDatabase.chatFolders.updateFolder(updatedFolder)
val actualFolder = SignalDatabase.chatFolders.getCurrentChatFolders().first()
assertEquals(updatedFolder, actualFolder)
}
@Test
fun givenADeletedChatFolder_whenIGetFolders_thenIExpectAListWithoutThatFolder() {
SignalDatabase.chatFolders.createFolder(folder1)
SignalDatabase.chatFolders.createFolder(folder2)
val folders = SignalDatabase.chatFolders.getCurrentChatFolders()
SignalDatabase.chatFolders.deleteChatFolder(folders.last())
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
assertEquals(listOf(folder1), actualFolders)
}
@Test
fun givenChatFolders_whenIUpdateTheirStorageSyncIds_thenIExpectAnUpdatedList() {
val existingMap = SignalDatabase.chatFolders.getStorageSyncIdsMap()
existingMap.forEach { (id, _) ->
SignalDatabase.chatFolders.applyStorageIdUpdate(id, StorageId.forChatFolder(StorageSyncHelper.generateKey()))
}
val updatedMap = SignalDatabase.chatFolders.getStorageSyncIdsMap()
existingMap.forEach { (id, storageId) ->
assertNotEquals(storageId, updatedMap[id])
}
}
@Test
fun givenARemoteFolder_whenIInsertLocally_thenIExpectAListWithThatFolder() {
val remoteRecord =
SignalChatFolderRecord(
folder1.storageServiceId!!,
RemoteChatFolderRecord(
identifier = UuidUtil.toByteArray(folder1.chatFolderId.uuid).toByteString(),
name = folder1.name,
position = folder1.position,
showOnlyUnread = folder1.showUnread,
showMutedChats = folder1.showMutedChats,
includeAllIndividualChats = folder1.showIndividualChats,
includeAllGroupChats = folder1.showGroupChats,
folderType = RemoteChatFolderRecord.FolderType.CUSTOM,
deletedAtTimestampMs = folder1.deletedTimestampMs,
includedRecipients = listOf(
RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(alice).serviceId.get().toString())),
RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(bob).serviceId.get().toString()))
),
excludedRecipients = listOf(
RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(charlie).serviceId.get().toString()))
)
)
)
SignalDatabase.chatFolders.insertChatFolderFromStorageSync(remoteRecord)
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
assertEquals(listOf(folder1), actualFolders)
}
@Test
fun givenADeletedChatFolder_whenIGetPositions_thenIExpectPositionsToStillBeConsecutive() {
SignalDatabase.chatFolders.createFolder(folder1)
SignalDatabase.chatFolders.createFolder(folder2)
SignalDatabase.chatFolders.createFolder(folder3)
val folders = SignalDatabase.chatFolders.getCurrentChatFolders()
SignalDatabase.chatFolders.deleteChatFolder(folders[1])
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
actualFolders.forEachIndexed { index, folder ->
assertEquals(folder.position, index)
}
}
@Test
fun givenAnEmptyFolder_whenIGetItsEmptyStatus_thenIExpectTrue() {
SignalDatabase.chatFolders.createFolder(folder4)
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
val unreadCountAndEmptyAndMutedStatus = SignalDatabase.chatFolders.getUnreadCountAndEmptyAndMutedStatusForFolders(actualFolders)
val actualFolderIsEmpty = unreadCountAndEmptyAndMutedStatus[actualFolders.first().id]!!.second
assertTrue(actualFolderIsEmpty)
}
private fun createRecipients(count: Int): List<RecipientId> {
return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}
}
@@ -1,323 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkObject
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId.ACI
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.UUID
import kotlin.time.Duration.Companion.days
@RunWith(AndroidJUnit4::class)
class CollapsingMessagesTests {
private lateinit var message: MessageTable
private lateinit var thread: ThreadTable
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var alice: RecipientId
private var aliceThread: Long = 0
private lateinit var bob: RecipientId
@Before
fun setUp() {
mockkStatic(RemoteConfig::class)
every { RemoteConfig.collapseEvents } returns true
message = SignalDatabase.messages
message.deleteAllThreads()
thread = SignalDatabase.threads
alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
bob = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
@Test
fun givenCollapsibleMessage_whenIInsert_thenItBecomesHead() {
val messageId = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val msg = message.getMessageRecord(messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg.collapsedState)
assertEquals(messageId, msg.collapsedHeadId)
}
@Test
fun givenSameCollapsibleTypes_whenIInsert_thenAllCollapseUnderHead() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg2.collapsedState)
assertEquals(messageId1, msg2.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
assertEquals(messageId1, msg3.collapsedHeadId)
}
@Test
fun givenDifferentCollapsedTypes_whenIInsert_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(Recipient.resolved(alice), 2000L), threadId = aliceThread)
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
}
@Test
fun givenNonCollapsibleTypes_whenIInsert_thenNoCollapsing() {
val messageId = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 1000L)
val msg = message.getMessageRecord(messageId)
assertEquals(CollapsedState.NONE, msg.collapsedState)
assertEquals(0, msg.collapsedHeadId)
}
@Test
fun givenMessagesOnDifferentDays_whenIInsert_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
message.writableDatabase.update(
MessageTable.TABLE_NAME,
contentValuesOf(MessageTable.DATE_RECEIVED to (System.currentTimeMillis() - 1.days.inWholeMilliseconds)),
"${MessageTable.ID} = ?",
arrayOf(messageId1.toString())
)
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val msg2 = message.getMessageRecord(messageId2)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
}
@Test
fun givenRegularMessageBetweenCollapsed_whenIInsertCollapsed_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 2000L)
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.NONE, msg2.collapsedState)
assertEquals(0, msg2.collapsedHeadId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg3.collapsedState)
assertEquals(messageId3, msg3.collapsedHeadId)
}
@Test
fun givenDifferentThreads_whenIInsertCollapsed_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(bob, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
}
@Test
fun givenCollapsedMessages_whenIDeleteFirstMessage_thenNextMessageBecomesHead() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
message.deleteMessage(messageId1, aliceThread)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
assertEquals(messageId2, msg3.collapsedHeadId)
}
@Test
fun givenCollapsedMessages_whenIDeleteNonFirstMessage_thenFirstMessageStaysHead() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
message.deleteMessage(messageId2, aliceThread)
val msg1 = message.getMessageRecord(messageId1)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
assertEquals(messageId1, msg3.collapsedHeadId)
}
@Test
fun givenTwoCollapsingTypes_whenIDeleteHeadOfFirstGroup_thenSecondGroupIsUnchanged() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
val recipient = Recipient.resolved(alice)
val identity1Id = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(recipient, 3000L), threadId = call1.threadId)
val identity2Id = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(recipient, 4000L), threadId = call1.threadId)
message.deleteMessage(call1.messageId, call1.threadId)
val msgCall2 = message.getMessageRecord(call2.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall2.collapsedState)
assertEquals(call2.messageId, msgCall2.collapsedHeadId)
val msgIdentity1 = message.getMessageRecord(identity1Id)
val msgIdentity2 = message.getMessageRecord(identity2Id)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgIdentity1.collapsedState)
assertEquals(identity1Id, msgIdentity1.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgIdentity2.collapsedState)
assertEquals(identity1Id, msgIdentity2.collapsedHeadId)
}
@Test
fun givenPendingCollapsingEvents_whenIMarkSeenAtASpecificTime_thenEverythingBeforeThatTimeIsCollapsed() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
message.collapsePendingCollapsibleEvents(aliceThread, System.currentTimeMillis())
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
val msgCall3 = message.getMessageRecord(call3.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall3.collapsedState)
}
@Test
fun givenPendingCollapsingEvents_whenIMarkAllAsSeen_thenEverythingIsCollapsed() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
message.collapseAllPendingCollapsibleEvents()
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
val msgCall3 = message.getMessageRecord(call3.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall3.collapsedState)
}
@Test
fun givenCollapsedEvents_whenITrimTheThreadByCount_thenIExpectANewHead() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val call4 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 4000L, false)
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall2.collapsedState)
thread.trimThread(threadId = aliceThread, syncThreadTrimDeletes = false, length = 2)
val msgCall3 = message.getMessageRecord(call3.messageId)
val msgCall4 = message.getMessageRecord(call4.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall3.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall4.collapsedState)
assertEquals(call3.messageId, msgCall4.collapsedHeadId)
}
@Test
fun givenCollapsedEvents_whenITrimTheThreadByDate_thenIExpectANewHead() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
val trimBeforeDate = System.currentTimeMillis()
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val call4 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 4000L, false)
message.collapsePendingCollapsibleEvents(aliceThread, System.currentTimeMillis())
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
thread.trimThread(threadId = aliceThread, syncThreadTrimDeletes = false, trimBeforeDate = trimBeforeDate)
val msgCall3 = message.getMessageRecord(call3.messageId)
val msgCall4 = message.getMessageRecord(call4.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall3.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall4.collapsedState)
assertEquals(call3.messageId, msgCall4.collapsedHeadId)
}
@Test
fun givenMaxCollapsedSet_whenIAddAnotherEvent_thenIExpectANewHead() {
mockkObject(CollapsibleEvents)
every { CollapsibleEvents.MAX_SIZE } returns 2
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg2.collapsedState)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg3.collapsedState)
assertEquals(messageId3, msg3.collapsedHeadId)
unmockkObject(CollapsibleEvents)
}
}
@@ -1,230 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class EditMessageRevisionTest {
@get:Rule
val databaseRule = SignalDatabaseRule()
private lateinit var senderId: RecipientId
private var threadId: Long = 0
@Before
fun setUp() {
val senderAci = ACI.from(UUID.randomUUID())
senderId = SignalDatabase.recipients.getOrInsertFromServiceId(senderAci)
threadId = SignalDatabase.threads.getOrCreateThreadIdFor(senderId, false, ThreadTable.DistributionTypes.DEFAULT)
}
@Test
fun singleEditSetsLatestRevisionIdOnOriginal() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
val editId = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(editId)
assertThat(getLatestRevisionId(editId)).isNull()
}
@Test
fun singleEditOnlyLatestRevisionAppearsInNotificationState() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
val editId = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
val notificationIds = getNotificationStateMessageIds()
assertEquals(listOf(editId), notificationIds)
}
@Test
fun multiEditSetsLatestRevisionIdOnAllPreviousRevisions() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
val edit1Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(edit1Id)
assertThat(getLatestRevisionId(edit1Id)).isNull()
val edit2Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(edit2Id)
assertThat(getLatestRevisionId(edit1Id)).isNotNull().isEqualTo(edit2Id)
}
@Test
fun multiEditOnlyLatestRevisionAppearsInNotificationState() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
val edit2Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
val notificationIds = getNotificationStateMessageIds()
assertEquals("Only the latest revision should appear in notification state", listOf(edit2Id), notificationIds)
}
@Test
fun readSyncThenMultipleEditsDoNotCreateOrphanedUnreadRevisions() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
markAsRead(originalId)
assertEquals("No notifications after read sync", 0, getNotificationStateMessageIds().size)
insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
val notificationIds = getNotificationStateMessageIds()
assertEquals(
"No notifications should appear after edits to a message that was already read via sync",
emptyList<Long>(),
notificationIds
)
}
@Test
fun readSyncOnLatestRevisionThenSecondEditDoesNotCreateOrphanedNotification() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
val edit1Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
// Read sync updates the latestRevisionId (edit1), not the original
markAsRead(edit1Id)
assertEquals("No notifications after read sync on edited message", 0, getNotificationStateMessageIds().size)
val edit2Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
val notificationIds = getNotificationStateMessageIds()
assertEquals(
"Only the latest revision or no revisions should appear depending on read state",
notificationIds.filter { it != edit2Id },
emptyList<Long>()
)
}
@Test
fun tripleEditCorrectlyChainsAllRevisions() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
val edit1Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
val edit2Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
val edit3Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1003)
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(edit3Id)
assertThat(getLatestRevisionId(edit1Id)).isNotNull().isEqualTo(edit3Id)
assertThat(getLatestRevisionId(edit2Id)).isNotNull().isEqualTo(edit3Id)
assertThat(getLatestRevisionId(edit3Id)).isNull()
assertEquals(listOf(edit3Id), getNotificationStateMessageIds())
}
@Test
fun multiEditWithReadSyncBetweenEditsNotificationDismissedAndStaysDismissed() {
val originalId = insertOriginalMessage(sentTimeMillis = 1000)
assertEquals("Original unread message should be in notification state", 1, getNotificationStateMessageIds().size)
markAsReadAndNotified(originalId)
assertEquals("No notifications after read sync", 0, getNotificationStateMessageIds().size)
insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1001)
assertEquals("No notifications after first edit (original was read)", 0, getNotificationStateMessageIds().size)
val edit2Id = insertEdit(originalSentTimestamp = 1000, editSentTimeMillis = 1002)
val notificationIds = getNotificationStateMessageIds()
assertEquals(
"No notifications should appear - message was read via sync before edits arrived",
emptyList<Long>(),
notificationIds
)
// Verify revision chain integrity
assertThat(getLatestRevisionId(originalId)).isNotNull().isEqualTo(edit2Id)
val edit1Id = edit2Id - 1 // edit1 was inserted right before edit2
assertThat(getLatestRevisionId(edit1Id)).isNotNull().isEqualTo(edit2Id)
assertThat(getLatestRevisionId(edit2Id)).isNull()
}
private fun insertOriginalMessage(sentTimeMillis: Long): Long {
val message = IncomingMessage(
type = MessageType.NORMAL,
from = senderId,
sentTimeMillis = sentTimeMillis,
serverTimeMillis = sentTimeMillis,
receivedTimeMillis = System.currentTimeMillis(),
body = "original message"
)
return SignalDatabase.messages.insertMessageInbox(message, threadId).get().messageId
}
/**
* The target is always retrieved via [MessageTable.getMessageFor] using the original sent
* timestamp — this matches what [EditMessageProcessor] does and means targetMessage.id
* is always the original message's row ID.
*/
private fun insertEdit(originalSentTimestamp: Long, editSentTimeMillis: Long): Long {
val targetMessage = SignalDatabase.messages.getMessageFor(originalSentTimestamp, senderId) as MmsMessageRecord
val editMessage = IncomingMessage(
type = MessageType.NORMAL,
from = senderId,
sentTimeMillis = editSentTimeMillis,
serverTimeMillis = editSentTimeMillis,
receivedTimeMillis = System.currentTimeMillis(),
body = "edited at $editSentTimeMillis"
)
return SignalDatabase.messages.insertEditMessageInbox(editMessage, targetMessage).get().messageId
}
private fun getLatestRevisionId(messageId: Long): Long? {
return SignalDatabase.rawDatabase
.query(MessageTable.TABLE_NAME, arrayOf(MessageTable.LATEST_REVISION_ID), "${MessageTable.ID} = ?", arrayOf(messageId.toString()), null, null, null)
.use { cursor ->
if (cursor.moveToFirst()) {
val idx = cursor.getColumnIndexOrThrow(MessageTable.LATEST_REVISION_ID)
if (cursor.isNull(idx)) null else cursor.getLong(idx)
} else {
null
}
}
}
private fun getNotificationStateMessageIds(): List<Long> {
return SignalDatabase.messages.getMessagesForNotificationState(emptyList()).use { cursor ->
val ids = mutableListOf<Long>()
while (cursor.moveToNext()) {
ids.add(CursorUtil.requireLong(cursor, MessageTable.ID))
}
ids
}
}
private fun markAsRead(messageId: Long) {
SignalDatabase.rawDatabase.execSQL(
"UPDATE ${MessageTable.TABLE_NAME} SET ${MessageTable.READ} = 1 WHERE ${MessageTable.ID} = ?",
arrayOf(messageId)
)
}
private fun markAsReadAndNotified(messageId: Long) {
SignalDatabase.rawDatabase.execSQL(
"UPDATE ${MessageTable.TABLE_NAME} SET ${MessageTable.READ} = 1, ${MessageTable.NOTIFIED} = 1 WHERE ${MessageTable.ID} = ?",
arrayOf(messageId)
)
}
}
@@ -1,344 +0,0 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.deleteAll
import org.signal.core.util.readToList
import org.signal.core.util.requireLong
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.storage.protos.groups.Member
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import java.security.SecureRandom
import kotlin.random.Random
class GroupTableTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var groupTable: GroupTable
@Before
fun setUp() {
groupTable = SignalDatabase.groups
groupTable.writableDatabase.deleteAll(GroupTable.TABLE_NAME)
groupTable.writableDatabase.deleteAll(GroupTable.MembershipTable.TABLE_NAME)
}
@Test
fun whenICreateGroupV2_thenIExpectMemberRowsPopulated() {
val groupId = insertPushGroup()
//language=sql
val members: List<RecipientId> = groupTable.writableDatabase.query(
"""
SELECT ${GroupTable.MembershipTable.RECIPIENT_ID}
FROM ${GroupTable.MembershipTable.TABLE_NAME}
WHERE ${GroupTable.MembershipTable.GROUP_ID} = "${groupId.serialize()}"
""".trimIndent()
).readToList {
RecipientId.from(it.requireLong(GroupTable.RECIPIENT_ID))
}
assertEquals(2, members.size)
}
@Test
fun givenAGroupV2_whenIGetGroupsContainingMember_thenIExpectGroup() {
val groupId = insertPushGroup()
insertThread(groupId)
val groups = groupTable.getGroupsContainingMember(harness.others[0], false)
assertEquals(1, groups.size)
assertEquals(groupId, groups[0].id)
}
@Test
fun givenAnMmsGroup_whenIGetMembers_thenIExpectAllMembers() {
val groupId = insertMmsGroup()
val groups = groupTable.getGroupMemberIds(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
assertEquals(2, groups.size)
}
@Test
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
insertPushGroup()
insertMmsGroup(members = listOf(harness.others[1]))
val groups = groupTable.getGroups()
assertEquals(2, groups.cursor?.count)
}
@Test
fun givenAGroup_whenIGetGroup_thenIExpectGroup() {
val v2Group = insertPushGroup()
insertThread(v2Group)
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, harness.others[0]), groupRecord.members.toSet())
}
@Test
fun givenAGroupAndARemap_whenIGetGroup_thenIExpectRemap() {
val v2Group = insertPushGroup()
insertThread(v2Group)
groupTable.writableDatabase.withinTransaction {
RemappedRecords.getInstance().addRecipient(harness.others[0], harness.others[1])
}
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
}
@Test
fun givenAGroup_whenIRemapRecipientsThatHaveAConflict_thenIExpectDeletion() {
val v2Group = insertPushGroupWithSelfAndOthers(
listOf(
harness.others[0],
harness.others[1]
)
)
insertThread(v2Group)
groupTable.remapRecipient(harness.others[0], harness.others[1])
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
}
@Test
fun givenAGroup_whenIRemapRecipients_thenIExpectRemap() {
val v2Group = insertPushGroup()
insertThread(v2Group)
val newId = harness.others[1]
groupTable.remapRecipient(harness.others[0], newId)
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, newId), groupRecord.members.toSet())
}
@Test
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
val v2Group = insertPushGroup()
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
assertTrue(actual)
}
@Test
fun givenAGroupAndMember_whenIRemove_thenIExpectNotAMember() {
val v2Group = insertPushGroup()
groupTable.remove(v2Group, harness.others[0])
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
assertFalse(actual)
}
@Test
fun givenAGroupAndNonMember_whenIIsCurrentMember_thenIExpectFalse() {
val v2Group = insertPushGroup()
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[1])
assertFalse(actual)
}
@Test
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
val g1 = insertPushGroup(members = emptyList())
val g2 = insertPushGroup(members = emptyList())
val gr1 = groupTable.getGroup(g1)
val gr2 = groupTable.getGroup(g2)
assertEquals(g1, gr1.get().id)
assertEquals(g2, gr2.get().id)
}
@Test
fun givenASharedActiveGroupWithoutAThread_whenISearchForRecipientsWithGroupsInCommon_thenIExpectThatGroup() {
val groupInCommon = insertPushGroup()
val expected = Recipient.resolved(harness.others[0])
SignalDatabase.recipients.setProfileSharing(expected.id, false)
SignalDatabase.recipients.queryGroupMemberContacts("Buddy")!!.use {
assertTrue(it.moveToFirst())
assertEquals(1, it.count)
assertEquals(expected.id.toLong(), it.requireLong(RecipientTable.ID))
}
val groups = groupTable.getPushGroupsContainingMember(expected.id)
assertEquals(1, groups.size)
assertEquals(groups[0].id, groupInCommon)
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForTheSharedToken_thenIExpectBothGroups() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val secondGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
assertEquals("Group Bob", secondGroup?.title)
}
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForAnUnsharedToken_thenIExpectOneGroup() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
}
}
@Test
fun givenAGroupWithThreeTokens_whenISearchForTheFirstAndLastToken_thenIExpectThatGroup() {
insertPushGroup("Group & Alice")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group & Alice", firstGroup?.title)
}
}
@Test
fun givenTwoGroupsWithSharedTokens_whenISearchForAnExactMatch_thenIExpectThatGroupFirst() {
insertPushGroup("Group Alice Bob")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Bob",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val second = it.getNext()
assertEquals("Group Bob", firstGroup?.title)
assertEquals("Group Alice Bob", second?.title)
}
}
private fun insertThread(groupId: GroupId): Long {
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
}
private fun insertMmsGroup(members: List<RecipientId> = listOf(harness.self.id, harness.others[0])): GroupId {
val id = GroupId.createMms(SecureRandom())
groupTable.create(
id,
null,
members.apply {
println("Creating a group with ${members.size} members")
}
)
return id
}
private fun insertPushGroup(
title: String = "Test Group",
members: List<DecryptedMember> = listOf(
DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
.build(),
DecryptedMember.Builder()
.aciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
.build()
)
): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.Builder()
.title(title)
.members(members)
.revision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
}
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val selfMember: DecryptedMember = DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
.build()
val otherMembers: List<DecryptedMember> = others.map { id ->
DecryptedMember.Builder()
.aciBytes(Recipient.resolved(id).requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
.build()
}
val decryptedGroupState = DecryptedGroup.Builder()
.members(listOf(selfMember) + otherMembers)
.revision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
}
}
@@ -1,252 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import assertk.assertThat
import assertk.assertions.hasSize
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.storage.protos.groups.Member
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
class NameCollisionTablesTest {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var charlie: RecipientId
@Before
fun setUp() {
alice = setUpRecipient(harness.others[0])
bob = setUpRecipient(harness.others[1])
charlie = setUpRecipient(harness.others[2])
}
@Test
fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() {
val threadRecipientId = alice
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId))
val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId)
assertThat(actual).hasSize(0)
}
@Test
fun givenTwoUsers_whenOneChangesTheirProfileNameToMatchTheOther_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
AppDependencies.recipientCache.clear()
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
assertThat(actualAlice).hasSize(2)
assertThat(actualBob).hasSize(2)
}
@Test
fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
assertThat(actualAlice).hasSize(0)
assertThat(actualBob).hasSize(0)
}
@Test
fun givenThreeUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectTwoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(charlie, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
AppDependencies.recipientCache.clear()
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
val actualCharlie = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(charlie)
assertThat(actualAlice).hasSize(0)
assertThat(actualBob).hasSize(2)
assertThat(actualCharlie).hasSize(2)
}
@Test
fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
AppDependencies.recipientCache.clear()
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
assertThat(actualAlice).hasSize(2)
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
assertThat(actualCollisions).hasSize(0)
}
@Test
fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
assertThat(actualCollisions).hasSize(0)
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
AppDependencies.recipientCache.clear()
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
assertThat(actualCollisions).hasSize(2)
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
AppDependencies.recipientCache.clear()
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
assertThat(collisions).hasSize(2)
}
@Test
fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
assertThat(collisions).hasSize(0)
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
assertThat(collisions).hasSize(0)
}
private fun setUpRecipient(recipientId: RecipientId): RecipientId {
SignalDatabase.recipients.setProfileSharing(recipientId, false)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
MmsHelper.insert(
threadId = threadId,
message = IncomingMessage(
type = MessageType.NORMAL,
from = recipientId,
groupId = null,
body = "hi",
sentTimeMillis = 100L,
receivedTimeMillis = 200L,
serverTimeMillis = 100L,
isUnidentified = true
)
)
return recipientId
}
private fun setProfileName(recipientId: RecipientId, name: ProfileName) {
SignalDatabase.recipients.setProfileName(recipientId, name)
Recipient.live(recipientId).refresh()
SignalDatabase.nameCollisions.handleIndividualNameCollision(recipientId)
}
private fun createGroup(): GroupTestingUtils.TestGroupInfo {
return GroupTestingUtils.insertGroup(
revision = 0,
DecryptedMember(
aciBytes = harness.self.requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(alice).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(bob).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
)
)
}
}
@@ -1,182 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.ServiceId.PNI
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientTableTest {
@get:Rule
val harness = SignalActivityRule()
@Test
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.queryAllContacts("Hidden", RecipientTable.IncludeSelfMode.Exclude)!!
assertEquals(1, results.count)
}
@Test
fun givenAHiddenRecipient_whenIGetSignalContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(RecipientTable.IncludeSelfMode.Exclude).use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(hiddenRecipient in results)
}
@Test
fun givenAHiddenRecipient_whenIQuerySignalContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", RecipientTable.IncludeSelfMode.Exclude))!!
assertEquals(0, results.count)
}
@Test
fun givenAHiddenRecipient_whenIGetNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(RecipientTable.IncludeSelfMode.Exclude)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(hiddenRecipient in results)
}
@Test
fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.queryAllContacts("Blocked", RecipientTable.IncludeSelfMode.Exclude)!!
assertEquals(0, results.count)
}
@Test
fun givenABlockedRecipient_whenIGetSignalContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(RecipientTable.IncludeSelfMode.Exclude).use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
}
assertNotEquals(0, results.size)
assertFalse(blockedRecipient in results)
}
@Test
fun givenABlockedRecipient_whenIQuerySignalContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", RecipientTable.IncludeSelfMode.Exclude))!!
assertEquals(0, results.count)
}
@Test
fun givenABlockedRecipient_whenIGetNonGroupContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(RecipientTable.IncludeSelfMode.Exclude)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(blockedRecipient in results)
}
@Test
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
SignalDatabase.recipients.markUnregistered(mainId)
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(mainId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
@Test
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
SignalDatabase.recipients.splitForStorageSyncIfNecessary(mainRecord.aci!!)
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(mainId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val PNI_A = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
const val E164_A = "+12222222222"
}
}
@@ -1,153 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import androidx.test.platform.app.InstrumentationRegistry
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.models.ServiceId.ACI
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.UUID
@Suppress("ClassName")
class ThreadTableTest_active {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
private val allChats: ChatFolderRecord = ChatFolderRecord(folderType = ChatFolderRecord.FolderType.ALL)
@Before
fun setUp() {
mockkStatic(RemoteConfig::class)
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
}
@Test
fun givenActiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectThread() {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, false)
SignalDatabase.threads.getUnarchivedConversationList(
ConversationFilter.OFF,
false,
0,
10,
allChats
).use { threads ->
assertEquals(1, threads.count)
val record = ThreadTable.StaticReader(threads, InstrumentationRegistry.getInstrumentation().context).getNext()
assertNotNull(record)
assertEquals(record!!.recipient.id, recipient.id)
}
}
@Test
fun givenInactiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, false)
SignalDatabase.threads.deleteConversation(threadId)
SignalDatabase.threads.getUnarchivedConversationList(
ConversationFilter.OFF,
false,
0,
10,
allChats
).use { threads ->
assertEquals(0, threads.count)
}
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
assertEquals(threadId2, threadId)
}
@Test
fun givenActiveArchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, false)
SignalDatabase.threads.setArchived(setOf(threadId), true)
SignalDatabase.threads.getUnarchivedConversationList(
ConversationFilter.OFF,
false,
0,
10,
allChats
).use { threads ->
assertEquals(0, threads.count)
}
}
@Test
fun givenActiveArchivedThread_whenIGetArchivedConversationList_thenIExpectThread() {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, false)
SignalDatabase.threads.setArchived(setOf(threadId), true)
SignalDatabase.threads.getArchivedConversationList(
ConversationFilter.OFF,
0,
10
).use { threads ->
assertEquals(1, threads.count)
}
}
@Test
fun givenInactiveArchivedThread_whenIGetArchivedConversationList_thenIExpectNoThread() {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, false)
SignalDatabase.threads.deleteConversation(threadId)
SignalDatabase.threads.setArchived(setOf(threadId), true)
SignalDatabase.threads.getArchivedConversationList(
ConversationFilter.OFF,
0,
10
).use { threads ->
assertEquals(0, threads.count)
}
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
assertEquals(threadId2, threadId)
}
@Test
fun givenActiveArchivedThread_whenIDeactivateThread_thenIExpectNoMessages() {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, false)
SignalDatabase.messages.getConversation(threadId).use {
assertEquals(1, it.count)
}
SignalDatabase.threads.deleteConversation(threadId)
SignalDatabase.messages.getConversation(threadId).use {
assertEquals(0, it.count)
}
}
}
@@ -1,81 +0,0 @@
package org.thoughtcrime.securesms.database
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.UUID
@Suppress("ClassName")
class ThreadTableTest_pinned {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
private val allChats: ChatFolderRecord = ChatFolderRecord(folderType = ChatFolderRecord.FolderType.ALL)
@Before
fun setUp() {
mockkStatic(RemoteConfig::class)
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.messages.deleteMessage(messageId)
// THEN
val pinned = SignalDatabase.threads.getPinnedThreadIds()
assertTrue(threadId in pinned)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.messages.deleteMessage(messageId)
// THEN
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount(ConversationFilter.OFF, allChats)
assertEquals(1, unarchivedCount)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.messages.deleteMessage(messageId)
// THEN
SignalDatabase.threads.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 1, allChats).use {
it.moveToFirst()
assertEquals(threadId, CursorUtil.requireLong(it, ThreadTable.ID))
}
}
}
@@ -1,67 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class ThreadTableTest_recents {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
}
@Test
fun getRecentConversationList_excludes_blocked_recipients() {
createActiveThreadFor(recipient)
SignalDatabase.recipients.setBlocked(recipient.id, true)
assertFalse(recipient.id in getRecentConversationRecipients(limit = 10))
}
@Test
fun getRecentConversationList_excludes_hidden_recipients() {
createActiveThreadFor(recipient)
SignalDatabase.recipients.markHidden(recipient.id)
assertFalse(recipient.id in getRecentConversationRecipients(limit = 10))
}
private fun createActiveThreadFor(recipient: Recipient) {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, true)
}
@Suppress("SameParameterValue")
private fun getRecentConversationRecipients(limit: Int = 10): Set<RecipientId> {
return SignalDatabase.threads
.getRecentConversationList(limit = limit, includeInactiveGroups = false, individualsOnly = false, groupsOnly = false, hideV1Groups = false, hideSms = false, hideSelf = false)
.use { cursor ->
buildSet {
while (cursor.moveToNext()) {
add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)))
}
}
}
}
}