From 90207b7dd78e1c1274086a328244bdfb500c65f5 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 17 Apr 2026 09:59:53 -0400 Subject: [PATCH] Convert handful of recipient/db heavy androidTests to regular unit tests. --- .../database/ChatFolderTablesTest.kt | 32 +-- .../database/CollapsingMessagesTests.kt | 45 ++-- .../database/EditMessageRevisionTest.kt | 27 ++- .../securesms/database/GroupTableTest.kt | 75 +++---- .../database/NameCollisionTablesTest.kt | 157 +++++--------- .../securesms/database/RecipientTableTest.kt | 174 ++++++++------- .../database/RemappedRecordsTestHelper.kt | 15 ++ .../database/ThreadTableTest_active.kt | 60 +++--- .../database/ThreadTableTest_pinned.kt | 41 ++-- .../database/ThreadTableTest_recents.kt | 43 ++-- .../testing/TestSignalSQLiteDatabase.kt | 8 + .../securesms/testutil/MockSignalStoreRule.kt | 6 + .../securesms/testutil/RecipientTestRule.kt | 202 ++++++++++++++++++ .../securesms/testutil/SignalDatabaseRule.kt | 8 + 14 files changed, 543 insertions(+), 350 deletions(-) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt (92%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt (91%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt (93%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/GroupTableTest.kt (83%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt (50%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt (70%) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/RemappedRecordsTestHelper.kt rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt (73%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt (69%) rename app/src/{androidTest => test}/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt (55%) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt similarity index 92% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt index 9037dd0067..3ce22240f9 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/ChatFolderTablesTest.kt @@ -1,11 +1,11 @@ /* - * Copyright 2024 Signal Messenger, LLC + * Copyright 2026 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ package org.thoughtcrime.securesms.database -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application import okio.ByteString.Companion.toByteString import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -14,7 +14,8 @@ 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.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import org.signal.core.util.UuidUtil import org.signal.core.util.deleteAll import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId @@ -22,18 +23,18 @@ import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFold 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.thoughtcrime.securesms.testutil.RecipientTestRule 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) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class ChatFolderTablesTest { @get:Rule - val harness = SignalActivityRule() + val recipients = RecipientTestRule() private lateinit var alice: RecipientId private lateinit var bob: RecipientId @@ -44,19 +45,15 @@ class ChatFolderTablesTest { private lateinit var folder3: ChatFolderRecord private lateinit var folder4: ChatFolderRecord - private lateinit var recipientIds: List - 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] + alice = recipients.createRecipient("Alice One") + bob = recipients.createRecipient("Bob Two") + charlie = recipients.createRecipient("Charlie Three") aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice)) bobThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(bob)) @@ -189,7 +186,6 @@ class ChatFolderTablesTest { excludedRecipients = listOf( RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(charlie).serviceId.get().toString())) ) - ) ) @@ -223,10 +219,4 @@ class ChatFolderTablesTest { assertTrue(actualFolderIsEmpty) } - - private fun createRecipients(count: Int): List { - return (1..count).map { - SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) - } - } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt b/app/src/test/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt similarity index 91% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt index 74a4ce8ccb..20912240a6 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/CollapsingMessagesTests.kt @@ -1,35 +1,38 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.database +import android.app.Application 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.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 org.thoughtcrime.securesms.testutil.RecipientTestRule import kotlin.time.Duration.Companion.days -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class CollapsingMessagesTests { + @get:Rule + val recipients = RecipientTestRule() + 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 @@ -37,18 +40,12 @@ class CollapsingMessagesTests { @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())) + alice = recipients.createRecipient("Alice Android") aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice)) - bob = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) + bob = recipients.createRecipient("Bob Android") } @Test @@ -83,7 +80,7 @@ class CollapsingMessagesTests { @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 messageId2 = recipients.insertOutgoingMessage(OutgoingMessage.identityVerifiedMessage(Recipient.resolved(alice), 2000L), aliceThread) val msg1 = message.getMessageRecord(messageId1) val msg2 = message.getMessageRecord(messageId2) @@ -97,7 +94,7 @@ class CollapsingMessagesTests { @Test fun givenNonCollapsibleTypes_whenIInsert_thenNoCollapsing() { - val messageId = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 1000L) + val messageId = recipients.insertOutgoingMessage(alice, sentTimeMillis = 1000L) val msg = message.getMessageRecord(messageId) assertEquals(CollapsedState.NONE, msg.collapsedState) @@ -125,7 +122,7 @@ class CollapsingMessagesTests { @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 messageId2 = recipients.insertOutgoingMessage(alice, sentTimeMillis = 2000L) val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId val msg1 = message.getMessageRecord(messageId1) @@ -199,8 +196,8 @@ class CollapsingMessagesTests { 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) + val identity1Id = recipients.insertOutgoingMessage(OutgoingMessage.identityVerifiedMessage(recipient, 3000L), call1.threadId) + val identity2Id = recipients.insertOutgoingMessage(OutgoingMessage.identityVerifiedMessage(recipient, 4000L), call1.threadId) message.deleteMessage(call1.messageId, call1.threadId) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt similarity index 93% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt index cda34787a2..19068834a5 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/EditMessageRevisionTest.kt @@ -1,6 +1,11 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.database -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull @@ -10,27 +15,27 @@ 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.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 +import org.thoughtcrime.securesms.testutil.RecipientTestRule -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class EditMessageRevisionTest { @get:Rule - val databaseRule = SignalDatabaseRule() + val recipients = RecipientTestRule() private lateinit var senderId: RecipientId private var threadId: Long = 0 @Before fun setUp() { - val senderAci = ACI.from(UUID.randomUUID()) - senderId = SignalDatabase.recipients.getOrInsertFromServiceId(senderAci) + senderId = recipients.createRecipient("Sender Name") threadId = SignalDatabase.threads.getOrCreateThreadIdFor(senderId, false, ThreadTable.DistributionTypes.DEFAULT) } @@ -192,7 +197,7 @@ class EditMessageRevisionTest { } private fun getLatestRevisionId(messageId: Long): Long? { - return SignalDatabase.rawDatabase + return SignalDatabase.writableDatabase .query(MessageTable.TABLE_NAME, arrayOf(MessageTable.LATEST_REVISION_ID), "${MessageTable.ID} = ?", arrayOf(messageId.toString()), null, null, null) .use { cursor -> if (cursor.moveToFirst()) { @@ -215,14 +220,14 @@ class EditMessageRevisionTest { } private fun markAsRead(messageId: Long) { - SignalDatabase.rawDatabase.execSQL( + SignalDatabase.writableDatabase.execSQL( "UPDATE ${MessageTable.TABLE_NAME} SET ${MessageTable.READ} = 1 WHERE ${MessageTable.ID} = ?", arrayOf(messageId) ) } private fun markAsReadAndNotified(messageId: Long) { - SignalDatabase.rawDatabase.execSQL( + SignalDatabase.writableDatabase.execSQL( "UPDATE ${MessageTable.TABLE_NAME} SET ${MessageTable.READ} = 1, ${MessageTable.NOTIFIED} = 1 WHERE ${MessageTable.ID} = ?", arrayOf(messageId) ) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTableTest.kt similarity index 83% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/GroupTableTest.kt index 3db8f17d31..e46a4751a2 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTableTest.kt @@ -1,11 +1,20 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.database +import android.app.Application 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.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import org.signal.core.util.deleteAll import org.signal.core.util.readToList import org.signal.core.util.requireLong @@ -17,16 +26,20 @@ 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 org.thoughtcrime.securesms.testutil.RecipientTestRule import java.security.SecureRandom import kotlin.random.Random +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class GroupTableTest { @get:Rule - val harness = SignalActivityRule() + val recipients = RecipientTestRule() private lateinit var groupTable: GroupTable + private lateinit var alice: RecipientId + private lateinit var bob: RecipientId @Before fun setUp() { @@ -34,6 +47,9 @@ class GroupTableTest { groupTable.writableDatabase.deleteAll(GroupTable.TABLE_NAME) groupTable.writableDatabase.deleteAll(GroupTable.MembershipTable.TABLE_NAME) + + alice = recipients.createRecipient("Buddy #0") + bob = recipients.createRecipient("Buddy #1") } @Test @@ -43,7 +59,7 @@ class GroupTableTest { //language=sql val members: List = groupTable.writableDatabase.query( """ - SELECT ${GroupTable.MembershipTable.RECIPIENT_ID} + SELECT ${GroupTable.MembershipTable.RECIPIENT_ID} FROM ${GroupTable.MembershipTable.TABLE_NAME} WHERE ${GroupTable.MembershipTable.GROUP_ID} = "${groupId.serialize()}" """.trimIndent() @@ -59,7 +75,7 @@ class GroupTableTest { val groupId = insertPushGroup() insertThread(groupId) - val groups = groupTable.getGroupsContainingMember(harness.others[0], false) + val groups = groupTable.getGroupsContainingMember(alice, false) assertEquals(1, groups.size) assertEquals(groupId, groups[0].id) @@ -77,7 +93,7 @@ class GroupTableTest { @Test fun givenGroups_whenIGetGroups_thenIExpectBothGroups() { insertPushGroup() - insertMmsGroup(members = listOf(harness.others[1])) + insertMmsGroup(members = listOf(bob)) val groups = groupTable.getGroups() @@ -90,7 +106,7 @@ class GroupTableTest { insertThread(v2Group) val groupRecord = groupTable.getGroup(v2Group).get() - assertEquals(setOf(harness.self.id, harness.others[0]), groupRecord.members.toSet()) + assertEquals(setOf(recipients.self, alice), groupRecord.members.toSet()) } @Test @@ -99,29 +115,24 @@ class GroupTableTest { insertThread(v2Group) groupTable.writableDatabase.withinTransaction { - RemappedRecords.getInstance().addRecipient(harness.others[0], harness.others[1]) + RemappedRecords.getInstance().addRecipient(alice, bob) } val groupRecord = groupTable.getGroup(v2Group).get() - assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet()) + assertEquals(setOf(recipients.self, bob), groupRecord.members.toSet()) } @Test fun givenAGroup_whenIRemapRecipientsThatHaveAConflict_thenIExpectDeletion() { - val v2Group = insertPushGroupWithSelfAndOthers( - listOf( - harness.others[0], - harness.others[1] - ) - ) + val v2Group = insertPushGroupWithSelfAndOthers(listOf(alice, bob)) insertThread(v2Group) - groupTable.remapRecipient(harness.others[0], harness.others[1]) + groupTable.remapRecipient(alice, bob) val groupRecord = groupTable.getGroup(v2Group).get() - assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet()) + assertEquals(setOf(recipients.self, bob), groupRecord.members.toSet()) } @Test @@ -129,19 +140,18 @@ class GroupTableTest { val v2Group = insertPushGroup() insertThread(v2Group) - val newId = harness.others[1] - groupTable.remapRecipient(harness.others[0], newId) + groupTable.remapRecipient(alice, bob) val groupRecord = groupTable.getGroup(v2Group).get() - assertEquals(setOf(harness.self.id, newId), groupRecord.members.toSet()) + assertEquals(setOf(recipients.self, bob), groupRecord.members.toSet()) } @Test fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() { val v2Group = insertPushGroup() - val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0]) + val actual = groupTable.isCurrentMember(v2Group.requirePush(), alice) assertTrue(actual) } @@ -150,8 +160,8 @@ class GroupTableTest { fun givenAGroupAndMember_whenIRemove_thenIExpectNotAMember() { val v2Group = insertPushGroup() - groupTable.remove(v2Group, harness.others[0]) - val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0]) + groupTable.remove(v2Group, alice) + val actual = groupTable.isCurrentMember(v2Group.requirePush(), alice) assertFalse(actual) } @@ -160,7 +170,7 @@ class GroupTableTest { fun givenAGroupAndNonMember_whenIIsCurrentMember_thenIExpectFalse() { val v2Group = insertPushGroup() - val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[1]) + val actual = groupTable.isCurrentMember(v2Group.requirePush(), bob) assertFalse(actual) } @@ -180,7 +190,7 @@ class GroupTableTest { @Test fun givenASharedActiveGroupWithoutAThread_whenISearchForRecipientsWithGroupsInCommon_thenIExpectThatGroup() { val groupInCommon = insertPushGroup() - val expected = Recipient.resolved(harness.others[0]) + val expected = Recipient.resolved(alice) SignalDatabase.recipients.setProfileSharing(expected.id, false) @@ -279,16 +289,9 @@ class GroupTableTest { return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient)) } - private fun insertMmsGroup(members: List = listOf(harness.self.id, harness.others[0])): GroupId { + private fun insertMmsGroup(members: List = listOf(recipients.self, alice)): GroupId { val id = GroupId.createMms(SecureRandom()) - groupTable.create( - id, - null, - members.apply { - println("Creating a group with ${members.size} members") - } - ) - + groupTable.create(id, null, members) return id } @@ -296,12 +299,12 @@ class GroupTableTest { title: String = "Test Group", members: List = listOf( DecryptedMember.Builder() - .aciBytes(harness.self.requireAci().toByteString()) + .aciBytes(recipients.selfAci.toByteString()) .joinedAtRevision(0) .role(Member.Role.DEFAULT) .build(), DecryptedMember.Builder() - .aciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString()) + .aciBytes(Recipient.resolved(alice).requireAci().toByteString()) .joinedAtRevision(0) .role(Member.Role.DEFAULT) .build() @@ -321,7 +324,7 @@ class GroupTableTest { val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE)) val selfMember: DecryptedMember = DecryptedMember.Builder() - .aciBytes(harness.self.requireAci().toByteString()) + .aciBytes(recipients.selfAci.toByteString()) .joinedAtRevision(0) .role(Member.Role.DEFAULT) .build() diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt similarity index 50% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt index 5ce5aea97d..842290dbcc 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/NameCollisionTablesTest.kt @@ -1,32 +1,30 @@ /* - * Copyright 2024 Signal Messenger, LLC + * Copyright 2026 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ package org.thoughtcrime.securesms.database -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application 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.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 +import org.thoughtcrime.securesms.testutil.RecipientTestRule -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class NameCollisionTablesTest { @get:Rule - val harness = SignalActivityRule(createGroup = true) + val recipients = RecipientTestRule() private lateinit var alice: RecipientId private lateinit var bob: RecipientId @@ -34,27 +32,24 @@ class NameCollisionTablesTest { @Before fun setUp() { - alice = setUpRecipient(harness.others[0]) - bob = setUpRecipient(harness.others[1]) - charlie = setUpRecipient(harness.others[2]) + alice = recipients.createRecipient("Buddy #0", profileSharing = false).also { recipients.insertIncomingMessage(it) } + bob = recipients.createRecipient("Buddy #1", profileSharing = false).also { recipients.insertIncomingMessage(it) } + charlie = recipients.createRecipient("Buddy #2", profileSharing = false).also { recipients.insertIncomingMessage(it) } } @Test fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() { - val threadRecipientId = alice - SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId)) - val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId) + SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice)) + val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) 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() + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Alice", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob) @@ -65,9 +60,9 @@ class NameCollisionTablesTest { @Test fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() { - setProfileName(alice, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob, ProfileName.fromParts("Bob", "Android")) - setProfileName(alice, ProfileName.fromParts("Alice", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Alice", "Android")) val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob) @@ -78,12 +73,10 @@ class NameCollisionTablesTest { @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() + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(charlie, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Alice", "Android")) val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob) @@ -96,14 +89,12 @@ class NameCollisionTablesTest { @Test fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() { - setProfileName(alice, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice) - setProfileName(alice, ProfileName.fromParts("Alice", "Android")) - setProfileName(alice, ProfileName.fromParts("Bob", "Android")) - - AppDependencies.recipientCache.clear() + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Alice", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) @@ -112,8 +103,8 @@ class NameCollisionTablesTest { @Test fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() { - setProfileName(alice, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice) val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) @@ -125,10 +116,10 @@ class NameCollisionTablesTest { fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() { SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice)) - setProfileName(alice, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice) - setProfileName(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice) @@ -139,12 +130,10 @@ class NameCollisionTablesTest { fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() { SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice)) - setProfileName(alice, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice) - AppDependencies.recipientCache.clear() - val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob) assertThat(actualCollisions).hasSize(2) @@ -152,17 +141,13 @@ class NameCollisionTablesTest { @Test fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() { - val alice = Recipient.resolved(alice) - val bob = Recipient.resolved(bob) - val info = createGroup() + val info = recipients.createGroup(alice, bob) - setProfileName(alice.id, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob.id, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId)) - SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android") - - AppDependencies.recipientCache.clear() + SignalDatabase.messages.insertProfileNameChangeMessages(Recipient.resolved(alice), "Bob Android", "Alice Android") val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId) @@ -171,17 +156,15 @@ class NameCollisionTablesTest { @Test fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() { - val alice = Recipient.resolved(alice) - val bob = Recipient.resolved(bob) - val info = createGroup() + val info = recipients.createGroup(alice, bob) - setProfileName(alice.id, ProfileName.fromParts("Bob", "Android")) - setProfileName(bob.id, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId)) - SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android") + SignalDatabase.messages.insertProfileNameChangeMessages(Recipient.resolved(alice), "Bob Android", "Alice Android") SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId) - SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android") + SignalDatabase.messages.insertProfileNameChangeMessages(Recipient.resolved(alice), "Bob Android", "Alice Android") val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId) @@ -190,63 +173,21 @@ class NameCollisionTablesTest { @Test fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() { - val alice = Recipient.resolved(alice) - val bob = Recipient.resolved(bob) - val info = createGroup() + val info = recipients.createGroup(alice, bob) - setProfileName(alice.id, ProfileName.fromParts("Alice", "Android")) - setProfileName(bob.id, ProfileName.fromParts("Bob", "Android")) + setProfileNameAndCheckCollision(alice, ProfileName.fromParts("Alice", "Android")) + setProfileNameAndCheckCollision(bob, ProfileName.fromParts("Bob", "Android")) SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId)) - SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android") + SignalDatabase.messages.insertProfileNameChangeMessages(Recipient.resolved(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() + private fun setProfileNameAndCheckCollision(recipientId: RecipientId, name: ProfileName) { + recipients.setProfileName(recipientId, name) 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 - ) - ) - } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt similarity index 70% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt index 62326bd70a..8f41960eea 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientTableTest.kt @@ -1,31 +1,48 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.database -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 org.thoughtcrime.securesms.testutil.RecipientTestRule import java.util.UUID -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class RecipientTableTest { @get:Rule - val harness = SignalActivityRule() + val recipients = RecipientTestRule() + + private lateinit var target: RecipientId + private lateinit var other: RecipientId + + @Before + fun setUp() { + target = recipients.createRecipient("Target Person") + other = recipients.createRecipient("Other Person") + } @Test fun givenAHiddenRecipient_whenIQueryAllContacts_thenIExpectHiddenToBeReturned() { - val hiddenRecipient = harness.others[0] - SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) - SignalDatabase.recipients.markHidden(hiddenRecipient) + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(target) val results = SignalDatabase.recipients.queryAllContacts("Hidden", RecipientTable.IncludeSelfMode.Exclude)!! @@ -34,69 +51,8 @@ class RecipientTableTest { @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 = SignalDatabase.recipients.getSignalContacts(RecipientTable.IncludeSelfMode.Exclude).use { - val ids = mutableListOf() - 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 = SignalDatabase.recipients.getNonGroupContacts(RecipientTable.IncludeSelfMode.Exclude)?.use { - val ids = mutableListOf() - 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) + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(target) val results: MutableList = SignalDatabase.recipients.getSignalContacts(RecipientTable.IncludeSelfMode.Exclude).use { val ids = mutableListOf() @@ -108,25 +64,23 @@ class RecipientTableTest { } assertNotEquals(0, results.size) - assertFalse(blockedRecipient in results) + assertFalse(target 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) + fun givenAHiddenRecipient_whenIQuerySignalContacts_thenIDoNotExpectHiddenToBeReturned() { + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(target) - val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", RecipientTable.IncludeSelfMode.Exclude))!! + val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", 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) + fun givenAHiddenRecipient_whenIGetNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() { + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(target) val results: MutableList = SignalDatabase.recipients.getNonGroupContacts(RecipientTable.IncludeSelfMode.Exclude)?.use { val ids = mutableListOf() @@ -138,7 +92,63 @@ class RecipientTableTest { }!! assertNotEquals(0, results.size) - assertFalse(blockedRecipient in results) + assertFalse(target in results) + } + + @Test + fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() { + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Blocked", "Person")) + SignalDatabase.recipients.setBlocked(target, true) + + val results = SignalDatabase.recipients.queryAllContacts("Blocked", RecipientTable.IncludeSelfMode.Exclude)!! + + assertEquals(0, results.count) + } + + @Test + fun givenABlockedRecipient_whenIGetSignalContacts_thenIDoNotExpectBlockedToBeReturned() { + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Blocked", "Person")) + SignalDatabase.recipients.setBlocked(target, true) + + val results: MutableList = SignalDatabase.recipients.getSignalContacts(RecipientTable.IncludeSelfMode.Exclude).use { + val ids = mutableListOf() + while (it.moveToNext()) { + ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID))) + } + + ids + } + + assertNotEquals(0, results.size) + assertFalse(target in results) + } + + @Test + fun givenABlockedRecipient_whenIQuerySignalContacts_thenIDoNotExpectBlockedToBeReturned() { + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Blocked", "Person")) + SignalDatabase.recipients.setBlocked(target, true) + + val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", RecipientTable.IncludeSelfMode.Exclude))!! + + assertEquals(0, results.count) + } + + @Test + fun givenABlockedRecipient_whenIGetNonGroupContacts_thenIDoNotExpectBlockedToBeReturned() { + SignalDatabase.recipients.setProfileName(target, ProfileName.fromParts("Blocked", "Person")) + SignalDatabase.recipients.setBlocked(target, true) + + val results: MutableList = SignalDatabase.recipients.getNonGroupContacts(RecipientTable.IncludeSelfMode.Exclude)?.use { + val ids = mutableListOf() + while (it.moveToNext()) { + ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID))) + } + + ids + }!! + + assertNotEquals(0, results.size) + assertFalse(target in results) } @Test @@ -148,7 +158,6 @@ class RecipientTableTest { 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() @@ -165,7 +174,6 @@ class RecipientTableTest { 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() diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RemappedRecordsTestHelper.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RemappedRecordsTestHelper.kt new file mode 100644 index 0000000000..328e28c116 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RemappedRecordsTestHelper.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database + +/** + * Bridge to package-private [RemappedRecords] internals for use from test rules. + */ +object RemappedRecordsTestHelper { + fun resetInstance() { + RemappedRecords.getInstance().resetCache() + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt similarity index 73% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt index 70de2526bc..ce56d3dd65 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt @@ -1,46 +1,46 @@ /* - * Copyright 2023 Signal Messenger, LLC + * Copyright 2026 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 android.app.Application +import androidx.test.core.app.ApplicationProvider 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.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testutil.RecipientTestRule @Suppress("ClassName") +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class ThreadTableTest_active { - @Rule - @JvmField - val databaseRule = SignalDatabaseRule() + @get:Rule + val recipients = RecipientTestRule() - private lateinit var recipient: Recipient + private lateinit var recipientId: RecipientId 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()))) + recipientId = recipients.createRecipient("Alice Android") } @Test fun givenActiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectThread() { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + recipients.insertOutgoingMessage(recipientId) SignalDatabase.threads.update(threadId, false) SignalDatabase.threads.getUnarchivedConversationList( @@ -52,17 +52,17 @@ class ThreadTableTest_active { ).use { threads -> assertEquals(1, threads.count) - val record = ThreadTable.StaticReader(threads, InstrumentationRegistry.getInstrumentation().context).getNext() + val record = ThreadTable.StaticReader(threads, ApplicationProvider.getApplicationContext()).getNext() assertNotNull(record) - assertEquals(record!!.recipient.id, recipient.id) + assertEquals(record!!.recipient.id, recipientId) } } @Test fun givenInactiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + recipients.insertOutgoingMessage(recipientId) SignalDatabase.threads.update(threadId, false) SignalDatabase.threads.deleteConversation(threadId) @@ -76,14 +76,14 @@ class ThreadTableTest_active { assertEquals(0, threads.count) } - val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) assertEquals(threadId2, threadId) } @Test fun givenActiveArchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + recipients.insertOutgoingMessage(recipientId) SignalDatabase.threads.update(threadId, false) SignalDatabase.threads.setArchived(setOf(threadId), true) @@ -100,8 +100,8 @@ class ThreadTableTest_active { @Test fun givenActiveArchivedThread_whenIGetArchivedConversationList_thenIExpectThread() { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + recipients.insertOutgoingMessage(recipientId) SignalDatabase.threads.update(threadId, false) SignalDatabase.threads.setArchived(setOf(threadId), true) @@ -116,8 +116,8 @@ class ThreadTableTest_active { @Test fun givenInactiveArchivedThread_whenIGetArchivedConversationList_thenIExpectNoThread() { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + recipients.insertOutgoingMessage(recipientId) SignalDatabase.threads.update(threadId, false) SignalDatabase.threads.deleteConversation(threadId) SignalDatabase.threads.setArchived(setOf(threadId), true) @@ -130,14 +130,14 @@ class ThreadTableTest_active { assertEquals(0, threads.count) } - val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) assertEquals(threadId2, threadId) } @Test fun givenActiveArchivedThread_whenIDeactivateThread_thenIExpectNoMessages() { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + recipients.insertOutgoingMessage(recipientId) SignalDatabase.threads.update(threadId, false) SignalDatabase.messages.getConversation(threadId).use { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt similarity index 69% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt index d46ee39a03..28cc80c982 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_pinned.kt @@ -1,42 +1,47 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.database -import io.mockk.mockkStatic +import android.app.Application 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.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testutil.RecipientTestRule @Suppress("ClassName") +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class ThreadTableTest_pinned { - @Rule - @JvmField - val databaseRule = SignalDatabaseRule() + @get:Rule + val recipients = RecipientTestRule() - private lateinit var recipient: Recipient + private lateinit var recipient: RecipientId 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()))) + recipient = recipients.createRecipient("Alice Android") } @Test fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() { // GIVEN - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipient)) + val messageId = recipients.insertOutgoingMessage(recipient) SignalDatabase.threads.pinConversations(listOf(threadId)) // WHEN @@ -50,8 +55,8 @@ class ThreadTableTest_pinned { @Test fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() { // GIVEN - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipient)) + val messageId = recipients.insertOutgoingMessage(recipient) SignalDatabase.threads.pinConversations(listOf(threadId)) // WHEN @@ -65,8 +70,8 @@ class ThreadTableTest_pinned { @Test fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() { // GIVEN - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipient)) + val messageId = recipients.insertOutgoingMessage(recipient) SignalDatabase.threads.pinConversations(listOf(threadId)) // WHEN diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt similarity index 55% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt index c3276512f9..1484250f0c 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_recents.kt @@ -1,54 +1,59 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.database -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application 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.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config 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 +import org.thoughtcrime.securesms.testutil.RecipientTestRule @Suppress("ClassName") -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class ThreadTableTest_recents { - @Rule - @JvmField - val databaseRule = SignalDatabaseRule() + @get:Rule + val recipients = RecipientTestRule() - private lateinit var recipient: Recipient + private lateinit var recipientId: RecipientId @Before fun setUp() { - recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))) + recipientId = recipients.createRecipient("Alice Android") } @Test fun getRecentConversationList_excludes_blocked_recipients() { - createActiveThreadFor(recipient) + createActiveThreadFor(recipientId) - SignalDatabase.recipients.setBlocked(recipient.id, true) + SignalDatabase.recipients.setBlocked(recipientId, true) - assertFalse(recipient.id in getRecentConversationRecipients(limit = 10)) + assertFalse(recipientId in getRecentConversationRecipients(limit = 10)) } @Test fun getRecentConversationList_excludes_hidden_recipients() { - createActiveThreadFor(recipient) + createActiveThreadFor(recipientId) - SignalDatabase.recipients.markHidden(recipient.id) + SignalDatabase.recipients.markHidden(recipientId) - assertFalse(recipient.id in getRecentConversationRecipients(limit = 10)) + assertFalse(recipientId in getRecentConversationRecipients(limit = 10)) } - private fun createActiveThreadFor(recipient: Recipient) { - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) - MmsHelper.insert(recipient = recipient, threadId = threadId) + private fun createActiveThreadFor(id: RecipientId) { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id)) + recipients.insertOutgoingMessage(id) SignalDatabase.threads.update(threadId, true) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/testing/TestSignalSQLiteDatabase.kt b/app/src/test/java/org/thoughtcrime/securesms/testing/TestSignalSQLiteDatabase.kt index 57cd6db751..e156687c9d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testing/TestSignalSQLiteDatabase.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testing/TestSignalSQLiteDatabase.kt @@ -191,6 +191,14 @@ class TestSignalSQLiteDatabase(private val database: SupportSQLiteDatabase) : Si return database.inTransaction() } + override fun runPostSuccessfulTransaction(task: Runnable) { + task.run() + } + + override fun runPostSuccessfulTransaction(dedupeKey: String, task: Runnable) { + task.run() + } + override val isDbLockedByCurrentThread: Boolean get() = database.isDbLockedByCurrentThread diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/MockSignalStoreRule.kt b/app/src/test/java/org/thoughtcrime/securesms/testutil/MockSignalStoreRule.kt index 91f9af0554..b0f9fe6cf6 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/MockSignalStoreRule.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/MockSignalStoreRule.kt @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.keyvalue.EmojiValues import org.thoughtcrime.securesms.keyvalue.InAppPaymentValues import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues import org.thoughtcrime.securesms.keyvalue.RegistrationValues +import org.thoughtcrime.securesms.keyvalue.ReleaseChannelValues import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SvrValues @@ -57,6 +58,9 @@ class MockSignalStoreRule(private val relaxed: Set> = emptySet()) : Ex lateinit var settings: SettingsValues private set + lateinit var releaseChannel: ReleaseChannelValues + private set + override fun before() { account = mockk(relaxed = relaxed.contains(AccountValues::class), relaxUnitFun = true) phoneNumberPrivacy = mockk(relaxed = relaxed.contains(PhoneNumberPrivacyValues::class), relaxUnitFun = true) @@ -66,6 +70,7 @@ class MockSignalStoreRule(private val relaxed: Set> = emptySet()) : Ex inAppPayments = mockk(relaxed = relaxed.contains(InAppPaymentValues::class), relaxUnitFun = true) backup = mockk(relaxed = relaxed.contains(BackupValues::class), relaxUnitFun = true) settings = mockk(relaxed = relaxed.contains(SettingsValues::class), relaxUnitFun = true) + releaseChannel = mockk(relaxed = relaxed.contains(ReleaseChannelValues::class), relaxUnitFun = true) mockkObject(SignalStore) every { SignalStore.account } returns account @@ -76,6 +81,7 @@ class MockSignalStoreRule(private val relaxed: Set> = emptySet()) : Ex every { SignalStore.inAppPayments } returns inAppPayments every { SignalStore.backup } returns backup every { SignalStore.settings } returns settings + every { SignalStore.releaseChannel } returns releaseChannel } override fun after() { diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt b/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt new file mode 100644 index 0000000000..ec3a3431f7 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/RecipientTestRule.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.testutil + +import androidx.test.core.app.ApplicationProvider +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkObject +import io.mockk.unmockkStatic +import org.junit.rules.ExternalResource +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.signal.core.models.ServiceId.ACI +import org.signal.libsignal.zkgroup.groups.GroupMasterKey +import org.signal.libsignal.zkgroup.profiles.ProfileKey +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.database.GroupReceiptTable +import org.thoughtcrime.securesms.database.MessageType +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.keyvalue.ReleaseChannelValues +import org.thoughtcrime.securesms.keyvalue.SettingsValues +import org.thoughtcrime.securesms.mms.IncomingMessage +import org.thoughtcrime.securesms.mms.OutgoingMessage +import org.thoughtcrime.securesms.profiles.ProfileName +import org.thoughtcrime.securesms.recipients.LiveRecipientCache +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.RemoteConfig +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile +import java.util.UUID +import kotlin.random.Random + +/** + * Test rule for unit tests that need to insert recipients and groups into the database. Handles + * the various database, dependencies, and store setup/teardown. Self is created by default. + */ +class RecipientTestRule : TestRule { + + val signalStore = MockSignalStoreRule(relaxed = setOf(SettingsValues::class, ReleaseChannelValues::class)) + val signalDatabase = SignalDatabaseRule() + val appDependencies = MockAppDependenciesRule() + + val selfAci: ACI = ACI.from(UUID.randomUUID()) + val selfE164: String = "+15555555555" + + lateinit var self: RecipientId + private set + + private val extras = object : ExternalResource() { + override fun before() { + mockkStatic(AppDependencies::class) + every { AppDependencies.recipientCache } returns LiveRecipientCache( + ApplicationProvider.getApplicationContext(), + Runnable::run + ) + + mockkObject(RemoteConfig) + every { RemoteConfig.collapseEvents } returns true + + every { signalStore.account.aci } returns selfAci + every { signalStore.account.requireAci() } returns selfAci + every { signalStore.account.e164 } returns selfE164 + every { signalStore.account.requireE164() } returns selfE164 + every { signalStore.account.isRegistered } returns true + every { signalStore.account.deviceId } returns 1 + every { signalStore.account.isMultiDevice } returns false + every { signalStore.account.isLinkedDevice } returns false + every { signalStore.account.isPrimaryDevice } returns true + + every { signalStore.registration.isRegistrationComplete } returns true + + self = insertRecipient(selfAci, ProfileName.fromParts("Tester", "McTesterson")) + } + + override fun after() { + unmockkObject(RemoteConfig) + unmockkStatic(AppDependencies::class) + } + } + + override fun apply(base: Statement, description: Description): Statement { + return RuleChain + .outerRule(signalStore) + .around(signalDatabase) + .around(appDependencies) + .around(extras) + .apply(base, description) + } + + fun createRecipient(profileName: ProfileName, profileSharing: Boolean = true): RecipientId { + return insertRecipient(ACI.from(UUID.randomUUID()), profileName, profileSharing) + } + + /** + * Convenience overload: splits [profileName] on the first space into given/family name parts. + */ + fun createRecipient(profileName: String, profileSharing: Boolean = true): RecipientId { + val name = profileName.split(" ", limit = 2).let { ProfileName.fromParts(it[0], it.getOrNull(1)) } + return createRecipient(name, profileSharing) + } + + /** + * Create a thread for [id] (1:1) if necessary and insert a simple outgoing MMS-style message. + * Mirrors the androidTest `MmsHelper.insert(recipient, threadId)` shape. + */ + fun insertOutgoingMessage(id: RecipientId, body: String = "body", sentTimeMillis: Long = System.currentTimeMillis()): Long { + val recipient = Recipient.resolved(id) + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + val message = OutgoingMessage( + recipient = recipient, + body = body, + timestamp = sentTimeMillis, + isSecure = true + ) + return insertOutgoingMessage(message, threadId) + } + + fun insertOutgoingMessage(message: OutgoingMessage, threadId: Long): Long { + return SignalDatabase.messages.insertMessageOutbox( + message = message, + threadId = threadId, + forceSms = false, + defaultReceiptStatus = GroupReceiptTable.STATUS_UNKNOWN, + insertListener = null + ).messageId + } + + /** + * Create a thread for [id] if necessary and insert a single normal incoming message. + */ + fun insertIncomingMessage(id: RecipientId) { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(id, false) + SignalDatabase.messages.insertMessageInbox( + IncomingMessage( + type = MessageType.NORMAL, + from = id, + groupId = null, + body = "hi", + sentTimeMillis = 100L, + receivedTimeMillis = 200L, + serverTimeMillis = 100L, + isUnidentified = true + ), + threadId + ) + } + + /** + * Insert a v2 group containing [self] plus the given members, all as administrators. + */ + fun createGroup(vararg members: RecipientId): TestGroupInfo { + val masterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE)) + val decryptedGroup = DecryptedGroup.Builder() + .members( + listOf(asAdminMember(selfAci)) + + members.map { asAdminMember(Recipient.resolved(it).requireAci()) } + ) + .revision(0) + .title("Test group") + .build() + + val groupId: GroupId.V2 = SignalDatabase.groups.create(masterKey, decryptedGroup, null)!! + val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId) + SignalDatabase.recipients.setProfileSharing(groupRecipientId, true) + return TestGroupInfo(groupId, masterKey, groupRecipientId) + } + + fun setProfileName(id: RecipientId, name: ProfileName) { + SignalDatabase.recipients.setProfileName(id, name) + Recipient.live(id).refresh() + } + + private fun insertRecipient(aci: ACI, profileName: ProfileName, profileSharing: Boolean = true): RecipientId { + val id = SignalDatabase.recipients.getOrInsertFromServiceId(aci) + SignalDatabase.recipients.setProfileName(id, profileName) + SignalDatabase.recipients.setProfileKeyIfAbsent(id, ProfileKey(Random.nextBytes(32))) + SignalDatabase.recipients.setCapabilities(id, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setProfileSharing(id, profileSharing) + SignalDatabase.recipients.markRegistered(id, aci) + return id + } + + private fun asAdminMember(aci: ACI): DecryptedMember { + return DecryptedMember(aciBytes = aci.toByteString(), role = Member.Role.ADMINISTRATOR) + } + + data class TestGroupInfo( + val groupId: GroupId.V2, + val masterKey: GroupMasterKey, + val recipientId: RecipientId + ) +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt b/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt index 6a0b3da7d2..d9e2f8f5dd 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/SignalDatabaseRule.kt @@ -10,9 +10,11 @@ import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject import org.junit.rules.ExternalResource +import org.thoughtcrime.securesms.database.RemappedRecordsTestHelper import org.thoughtcrime.securesms.database.SQLiteDatabase import org.thoughtcrime.securesms.database.SearchTable import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.testing.JdbcSqliteDatabase import org.thoughtcrime.securesms.testing.TestSignalDatabase @@ -27,15 +29,21 @@ class SignalDatabaseRule : ExternalResource() { get() = signalDatabase.signalWritableDatabase override fun before() { + RecipientId.clearCache() + RemappedRecordsTestHelper.resetInstance() + signalDatabase = inMemorySignalDatabase() mockkObject(SignalDatabase) every { SignalDatabase.instance } returns signalDatabase + every { SignalDatabase.inTransaction } answers { signalDatabase.signalWritableDatabase.inTransaction() } } override fun after() { unmockkObject(SignalDatabase) signalDatabase.close() + RecipientId.clearCache() + RemappedRecordsTestHelper.resetInstance() } companion object {