mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-06-29 02:26:05 +01:00
Convert handful of recipient/db heavy androidTests to regular unit tests.
This commit is contained in:
committed by
jeffrey-signal
parent
5b7f668251
commit
90207b7dd7
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
-323
@@ -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)
|
||||
}
|
||||
}
|
||||
-230
@@ -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)!!
|
||||
}
|
||||
}
|
||||
-252
@@ -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"
|
||||
}
|
||||
}
|
||||
-153
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
-67
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user