diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt deleted file mode 100644 index fc4465f43b..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt +++ /dev/null @@ -1,50 +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.isEqualTo -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.signal.core.util.deleteAll -import org.signal.donations.InAppPaymentType -import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData -import org.thoughtcrime.securesms.testing.SignalActivityRule - -@RunWith(AndroidJUnit4::class) -class InAppPaymentTableTest { - @get:Rule - val harness = SignalActivityRule() - - @Before - fun setUp() { - SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME) - } - - @Test - fun givenACreatedInAppPayment_whenIUpdateToPending_thenIExpectPendingPayment() { - val inAppPaymentId = SignalDatabase.inAppPayments.insert( - type = InAppPaymentType.ONE_TIME_DONATION, - state = InAppPaymentTable.State.CREATED, - subscriberId = null, - endOfPeriod = null, - inAppPaymentData = InAppPaymentData() - ) - - val paymentBeforeUpdate = SignalDatabase.inAppPayments.getById(inAppPaymentId) - assertThat(paymentBeforeUpdate?.state).isEqualTo(InAppPaymentTable.State.CREATED) - - SignalDatabase.inAppPayments.update( - inAppPayment = paymentBeforeUpdate!!.copy(state = InAppPaymentTable.State.PENDING) - ) - - val paymentAfterUpdate = SignalDatabase.inAppPayments.getById(inAppPaymentId) - assertThat(paymentAfterUpdate?.state).isEqualTo(InAppPaymentTable.State.PENDING) - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt deleted file mode 100644 index 97e490d379..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt +++ /dev/null @@ -1,174 +0,0 @@ -package org.thoughtcrime.securesms.database - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import assertk.assertThat -import assertk.assertions.containsExactlyInAnyOrder -import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import okio.ByteString.Companion.toByteString -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -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.conversation.colors.AvatarColor -import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile -import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId -import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule -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.SignalNotificationProfileRecord -import org.whispersystems.signalservice.api.storage.StorageId -import java.time.DayOfWeek -import java.util.UUID -import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile as RemoteNotificationProfile -import org.whispersystems.signalservice.internal.storage.protos.Recipient as RemoteRecipient - -@RunWith(AndroidJUnit4::class) -class NotificationProfileTablesTest { - - @get:Rule - val harness = SignalActivityRule() - - private lateinit var alice: RecipientId - private lateinit var profile1: NotificationProfile - - @Before - fun setUp() { - alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) - - profile1 = NotificationProfile( - id = 1, - name = "profile1", - emoji = "", - createdAt = 1000L, - schedule = NotificationProfileSchedule(id = 1), - allowedMembers = setOf(alice), - notificationProfileId = NotificationProfileId.generate(), - deletedTimestampMs = 0, - storageServiceId = StorageId.forNotificationProfile(byteArrayOf(1, 2, 3)) - ) - - SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileTable.TABLE_NAME) - SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileScheduleTable.TABLE_NAME) - SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileAllowedMembersTable.TABLE_NAME) - } - - @Test - fun givenARemoteProfile_whenIInsertLocally_thenIExpectAListWithThatProfile() { - val remoteRecord = - SignalNotificationProfileRecord( - profile1.storageServiceId!!, - RemoteNotificationProfile( - id = UuidUtil.toByteArray(profile1.notificationProfileId.uuid).toByteString(), - name = "profile1", - emoji = "", - color = profile1.color.colorInt(), - createdAtMs = 1000L, - allowedMembers = listOf(RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(alice).serviceId.get().toString()))), - allowAllMentions = false, - allowAllCalls = true, - scheduleEnabled = false, - scheduleStartTime = 900, - scheduleEndTime = 1700, - scheduleDaysEnabled = emptyList(), - deletedAtTimestampMs = 0 - ) - ) - - SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord) - val actualProfiles = SignalDatabase.notificationProfiles.getProfiles() - - assertEquals(listOf(profile1), actualProfiles) - } - - @Test - fun givenAProfile_whenIDeleteIt_thenIExpectAnEmptyList() { - val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile( - name = "Profile", - emoji = "avatar", - color = AvatarColor.A210, - createdAt = 1000L - ).profile - - SignalDatabase.notificationProfiles.deleteProfile(profile.id) - - assertThat(SignalDatabase.notificationProfiles.getProfiles()).isEmpty() - assertThat(SignalDatabase.notificationProfiles.getProfile(profile.id)) - } - - @Test - fun givenADeletedProfile_whenIGetIt_thenIExpectItToStillHaveASchedule() { - val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile( - name = "Profile", - emoji = "avatar", - color = AvatarColor.A210, - createdAt = 1000L - ).profile - - SignalDatabase.notificationProfiles.deleteProfile(profile.id) - - val deletedProfile = SignalDatabase.notificationProfiles.getProfile(profile.id)!! - assertThat(deletedProfile.schedule.enabled).isFalse() - assertThat(deletedProfile.schedule.start).isEqualTo(900) - assertThat(deletedProfile.schedule.end).isEqualTo(1700) - assertThat(deletedProfile.schedule.daysEnabled, "Contains correct default days") - .containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY) - } - - @Test - fun givenNotificationProfiles_whenIUpdateTheirStorageSyncIds_thenIExpectAnUpdatedList() { - SignalDatabase.notificationProfiles.createProfile( - name = "Profile1", - emoji = "avatar", - color = AvatarColor.A210, - createdAt = 1000L - ) - SignalDatabase.notificationProfiles.createProfile( - name = "Profile2", - emoji = "avatar", - color = AvatarColor.A210, - createdAt = 2000L - ) - - val existingMap = SignalDatabase.notificationProfiles.getStorageSyncIdsMap() - existingMap.forEach { (id, _) -> - SignalDatabase.notificationProfiles.applyStorageIdUpdate(id, StorageId.forNotificationProfile(StorageSyncHelper.generateKey())) - } - val updatedMap = SignalDatabase.notificationProfiles.getStorageSyncIdsMap() - - existingMap.forEach { (id, storageId) -> - assertNotEquals(storageId, updatedMap[id]) - } - } - - @Test - fun givenAProfileDeletedOver30Days_whenICleanUp_thenIExpectItToNotHaveAStorageId() { - val remoteRecord = - SignalNotificationProfileRecord( - profile1.storageServiceId!!, - RemoteNotificationProfile( - id = UuidUtil.toByteArray(profile1.notificationProfileId.uuid).toByteString(), - name = "profile1", - emoji = "", - color = profile1.color.colorInt(), - createdAtMs = 1000L, - deletedAtTimestampMs = 1000L - ) - ) - - SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord) - SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis()) - assertThat(SignalDatabase.notificationProfiles.getStorageSyncIds()).isEmpty() - } - - private val NotificationProfileTables.NotificationProfileChangeResult.profile: NotificationProfile - get() = (this as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt deleted file mode 100644 index 989478440e..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.thoughtcrime.securesms.database.helpers.migration - -import android.app.Application -import androidx.core.content.contentValuesOf -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Assert.fail -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.signal.core.util.SqlUtil -import org.thoughtcrime.securesms.database.DistributionListTables -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.DistributionListId -import org.thoughtcrime.securesms.testing.SignalDatabaseRule -import org.whispersystems.signalservice.api.push.DistributionId -import java.util.UUID -import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase - -@RunWith(AndroidJUnit4::class) -class MyStoryMigrationTest { - - @get:Rule val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false) - - @Test - fun givenAValidMyStory_whenIMigrate_thenIExpectMyStoryToBeValid() { - // GIVEN - assertValidMyStoryExists() - - // WHEN - runMigration() - - // THEN - assertValidMyStoryExists() - } - - @Test - fun givenNoMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { - // GIVEN - deleteMyStory() - - // WHEN - runMigration() - - // THEN - assertValidMyStoryExists() - } - - @Test - fun givenA00000000DistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { - // GIVEN - setMyStoryDistributionId("0000-0000") - - // WHEN - runMigration() - - // THEN - assertValidMyStoryExists() - } - - @Test - fun givenARandomDistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { - // GIVEN - setMyStoryDistributionId(UUID.randomUUID().toString()) - - // WHEN - runMigration() - - // THEN - assertValidMyStoryExists() - } - - private fun setMyStoryDistributionId(serializedId: String) { - SignalDatabase.rawDatabase.update( - DistributionListTables.LIST_TABLE_NAME, - contentValuesOf( - DistributionListTables.DISTRIBUTION_ID to serializedId - ), - "_id = ?", - SqlUtil.buildArgs(DistributionListId.MY_STORY) - ) - } - - private fun deleteMyStory() { - SignalDatabase.rawDatabase.delete( - DistributionListTables.LIST_TABLE_NAME, - "_id = ?", - SqlUtil.buildArgs(DistributionListId.MY_STORY) - ) - } - - private fun assertValidMyStoryExists() { - SignalDatabase.rawDatabase.query( - DistributionListTables.LIST_TABLE_NAME, - SqlUtil.COUNT, - "_id = ? AND ${DistributionListTables.DISTRIBUTION_ID} = ?", - SqlUtil.buildArgs(DistributionListId.MY_STORY, DistributionId.MY_STORY.toString()), - null, - null, - null - ).use { - if (it.moveToNext()) { - val count = it.getInt(0) - assertEquals("assertValidMyStoryExists: Query produced an unexpected count.", 1, count) - } else { - fail("assertValidMyStoryExists: Query did not produce a count.") - } - } - } - - private fun runMigration() { - V151_MyStoryMigration.migrate( - InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application, - SignalSQLiteDatabase(SignalDatabase.rawDatabase), - 0, - 1 - ) - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/BackupMediaSnapshotTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/BackupMediaSnapshotTableTest.kt similarity index 94% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/BackupMediaSnapshotTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/BackupMediaSnapshotTableTest.kt index c6f8412e05..8813989f2f 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/BackupMediaSnapshotTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/BackupMediaSnapshotTableTest.kt @@ -1,23 +1,29 @@ package org.thoughtcrime.securesms.database -import androidx.media3.common.util.Util -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application import assertk.assertThat import assertk.assertions.isEqualTo 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.count import org.signal.core.util.readToSingleInt import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.MediaEntry -import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule +import org.thoughtcrime.securesms.testutil.SignalDatabaseRule -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class BackupMediaSnapshotTableTest { @get:Rule - val harness = SignalActivityRule() + val appDependencies = MockAppDependenciesRule() + + @get:Rule + val signalDatabaseRule = SignalDatabaseRule() @Test fun givenAnEmptyTable_whenIWriteToTable_thenIExpectEmptyTable() { @@ -302,12 +308,21 @@ class BackupMediaSnapshotTableTest { return MediaEntry( mediaId = mediaId(seed, thumbnail), cdn = cdn, - plaintextHash = Util.toByteArray(seed), - remoteKey = Util.toByteArray(seed), + plaintextHash = intToByteArray(seed), + remoteKey = intToByteArray(seed), isThumbnail = thumbnail ) } + private fun intToByteArray(value: Int): ByteArray { + return byteArrayOf( + (value shr 24).toByte(), + (value shr 16).toByte(), + (value shr 8).toByte(), + value.toByte() + ) + } + private fun createArchiveMediaObject(seed: Int, thumbnail: Boolean = false, cdn: Int = 0): ArchivedMediaObject { return ArchivedMediaObject( mediaId = mediaId(seed, thumbnail), diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListTablesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/DistributionListTablesTest.kt similarity index 81% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListTablesTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/DistributionListTablesTest.kt index 40a5054698..69eecab4e5 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListTablesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/DistributionListTablesTest.kt @@ -1,17 +1,28 @@ package org.thoughtcrime.securesms.database +import android.app.Application import org.junit.Assert 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.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListRecord import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testutil.RecipientTestRule import java.util.UUID +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class DistributionListTablesTest { + @get:Rule + val recipients = RecipientTestRule() + private lateinit var distributionDatabase: DistributionListTables @Before @@ -27,8 +38,7 @@ class DistributionListTablesTest { @Test fun getList_returnCorrectList() { - createRecipients(3) - val members: List = recipientList(1, 2, 3) + val members: List = createRecipients(3) val id: DistributionListId? = distributionDatabase.createList("test", members) Assert.assertNotNull(id) @@ -42,8 +52,7 @@ class DistributionListTablesTest { @Test fun getMembers_returnsCorrectMembers() { - createRecipients(3) - val members: List = recipientList(1, 2, 3) + val members: List = createRecipients(3) val id: DistributionListId? = distributionDatabase.createList("test", members) Assert.assertNotNull(id) @@ -77,8 +86,8 @@ class DistributionListTablesTest { Assert.fail("Expected an assertion error.") } - private fun createRecipients(count: Int) { - for (i in 0 until count) { + private fun createRecipients(count: Int): List { + return (0 until count).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/InAppPaymentSubscriberTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentSubscriberTableTest.kt similarity index 92% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/InAppPaymentSubscriberTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentSubscriberTableTest.kt index 01c6966e44..de024c6ef6 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/InAppPaymentSubscriberTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentSubscriberTableTest.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.database +import android.app.Application import android.database.sqlite.SQLiteConstraintException import assertk.assertThat import assertk.assertions.isEqualTo @@ -8,20 +9,34 @@ import org.junit.Assert.fail 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.count import org.signal.core.util.deleteAll import org.signal.core.util.readToSingleInt import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData -import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule +import org.thoughtcrime.securesms.testutil.MockSignalStoreRule +import org.thoughtcrime.securesms.testutil.SignalDatabaseRule import org.whispersystems.signalservice.api.storage.IAPSubscriptionId import org.whispersystems.signalservice.api.subscriptions.SubscriberId import java.util.Currency +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class InAppPaymentSubscriberTableTest { + @get:Rule - val harness = SignalActivityRule() + val signalStore = MockSignalStoreRule() + + @get:Rule + val appDependencies = MockAppDependenciesRule() + + @get:Rule + val signalDatabaseRule = SignalDatabaseRule() @Before fun setUp() { diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt index 0f1cbf4a06..68137866ff 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/InAppPaymentTableTest.kt @@ -37,6 +37,27 @@ class InAppPaymentTableTest { SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME) } + @Test + fun givenACreatedInAppPayment_whenIUpdateToPending_thenIExpectPendingPayment() { + val inAppPaymentId = SignalDatabase.inAppPayments.insert( + type = InAppPaymentType.ONE_TIME_DONATION, + state = InAppPaymentTable.State.CREATED, + subscriberId = null, + endOfPeriod = null, + inAppPaymentData = InAppPaymentData() + ) + + val paymentBeforeUpdate = SignalDatabase.inAppPayments.getById(inAppPaymentId) + assertThat(paymentBeforeUpdate?.state).isEqualTo(InAppPaymentTable.State.CREATED) + + SignalDatabase.inAppPayments.update( + inAppPayment = paymentBeforeUpdate!!.copy(state = InAppPaymentTable.State.PENDING) + ) + + val paymentAfterUpdate = SignalDatabase.inAppPayments.getById(inAppPaymentId) + assertThat(paymentAfterUpdate?.state).isEqualTo(InAppPaymentTable.State.PENDING) + } + // region consumeDonationPaymentsToNotifyUser @Test diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt similarity index 67% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt index fcee3cb93f..fb52807636 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt @@ -5,20 +5,43 @@ package org.thoughtcrime.securesms.database +import android.app.Application import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +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 import org.signal.core.models.ServiceId.ACI import org.signal.core.models.ServiceId.PNI +import org.signal.core.util.readToSingleObject +import org.signal.core.util.requireLongOrNull +import org.signal.core.util.select +import org.signal.core.util.update import org.signal.libsignal.protocol.ReusedBaseKeyException -import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.generateECPublicKey -import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.getStaleTime -import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.insertTestRecord +import org.signal.libsignal.protocol.ecc.ECKeyPair +import org.signal.libsignal.protocol.ecc.ECPublicKey +import org.signal.libsignal.protocol.kem.KEMKeyPair +import org.signal.libsignal.protocol.kem.KEMKeyType +import org.signal.libsignal.protocol.state.KyberPreKeyRecord +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule +import org.thoughtcrime.securesms.testutil.SignalDatabaseRule +import java.security.SecureRandom import java.util.UUID +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class KyberPreKeyTableTest { + @get:Rule + val appDependencies = MockAppDependenciesRule() + + @get:Rule + val signalDatabaseRule = SignalDatabaseRule() + private val aci: ACI = ACI.from(UUID.randomUUID()) private val pni: PNI = PNI.from(UUID.randomUUID()) @@ -130,7 +153,7 @@ class KyberPreKeyTableTest { insertTestRecord(aci, id = 2, staleTime = 10, lastResort = true) insertTestRecord(aci, id = 3, staleTime = 10, lastResort = true) - SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0) + SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0) assertNotNull(getStaleTime(aci, 1)) assertNotNull(getStaleTime(aci, 2)) @@ -176,4 +199,50 @@ class KyberPreKeyTableTest { baseKey = publicKey ) } + + private fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0, lastResort: Boolean = false) { + val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024) + SignalDatabase.kyberPreKeys.insert( + serviceId = account, + keyId = id, + record = KyberPreKeyRecord( + id, + System.currentTimeMillis(), + kemKeyPair, + ECKeyPair.generate().privateKey.calculateSignature(kemKeyPair.publicKey.serialize()) + ), + lastResort = lastResort + ) + + val count = SignalDatabase.writableDatabase + .update(KyberPreKeyTable.TABLE_NAME) + .values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime) + .where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId()) + .run() + + assertEquals(1, count) + } + + private fun getStaleTime(account: ServiceId, id: Int): Long? { + return SignalDatabase.writableDatabase + .select(KyberPreKeyTable.STALE_TIMESTAMP) + .from(KyberPreKeyTable.TABLE_NAME) + .where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId()) + .run() + .readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) } + } + + private fun generateECPublicKey(): ECPublicKey { + val byteArray = ByteArray(ECPublicKey.KEY_SIZE - 1) + SecureRandom().nextBytes(byteArray) + + return ECPublicKey.fromPublicKeyBytes(byteArray) + } + + private fun ServiceId.toAccountId(): String { + return when (this) { + is ACI -> this.toString() + is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID + } + } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt b/app/src/test/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt similarity index 92% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt index aa17815753..c15d228986 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt @@ -1,26 +1,30 @@ 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.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.models.ServiceId.ACI -import org.signal.core.models.ServiceId.PNI import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge -import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testutil.RecipientTestRule import java.util.UUID @Suppress("ClassName") -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class MessageTableTest_gifts { - private lateinit var mms: MessageTable - private val localAci = ACI.from(UUID.randomUUID()) - private val localPni = PNI.from(UUID.randomUUID()) + @get:Rule + val recipientTestRule = RecipientTestRule() + + private lateinit var mms: MessageTable private lateinit var recipients: List @@ -28,11 +32,6 @@ class MessageTableTest_gifts { fun setUp() { mms = SignalDatabase.messages - mms.deleteAllThreads() - - SignalStore.account.setAci(localAci) - SignalStore.account.setPni(localPni) - recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/MmsHelper.kt b/app/src/test/java/org/thoughtcrime/securesms/database/MmsHelper.kt new file mode 100644 index 0000000000..3c5edb93ec --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/MmsHelper.kt @@ -0,0 +1,63 @@ +package org.thoughtcrime.securesms.database + +import org.thoughtcrime.securesms.database.model.ParentStoryId +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge +import org.thoughtcrime.securesms.mms.IncomingMessage +import org.thoughtcrime.securesms.mms.OutgoingMessage +import org.thoughtcrime.securesms.recipients.Recipient +import java.util.Optional + +/** + * Helper methods for inserting an MMS message into the MMS table. + */ +object MmsHelper { + + fun insert( + recipient: Recipient = Recipient.UNKNOWN, + body: String = "body", + sentTimeMillis: Long = System.currentTimeMillis(), + expiresIn: Long = 0, + viewOnce: Boolean = false, + distributionType: Int = ThreadTable.DistributionTypes.DEFAULT, + threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient, distributionType), + storyType: StoryType = StoryType.NONE, + parentStoryId: ParentStoryId? = null, + isStoryReaction: Boolean = false, + giftBadge: GiftBadge? = null, + secure: Boolean = true + ): Long { + val message = OutgoingMessage( + recipient = recipient, + body = body, + timestamp = sentTimeMillis, + expiresIn = expiresIn, + viewOnce = viewOnce, + distributionType = distributionType, + storyType = storyType, + parentStoryId = parentStoryId, + isStoryReaction = isStoryReaction, + giftBadge = giftBadge, + isSecure = secure + ) + + return insert( + message = message, + threadId = threadId + ) + } + + fun insert( + message: OutgoingMessage, + threadId: Long + ): Long { + return SignalDatabase.messages.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId + } + + fun insert( + message: IncomingMessage, + threadId: Long + ): Optional { + return SignalDatabase.messages.insertMessageInbox(message, threadId) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt index 3031cd1117..3ccad767e2 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt @@ -3,30 +3,73 @@ package org.thoughtcrime.securesms.database import android.app.Application import assertk.assertThat import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import assertk.assertions.single +import io.mockk.every +import okio.ByteString.Companion.toByteString +import org.junit.Assert.assertEquals +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.util.UuidUtil +import org.signal.core.util.deleteAll import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile +import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId +import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule -import org.thoughtcrime.securesms.testutil.SignalDatabaseRule +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.testutil.RecipientTestRule +import org.thoughtcrime.securesms.util.RemoteConfig +import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord +import org.whispersystems.signalservice.api.storage.StorageId import java.time.DayOfWeek +import java.util.UUID +import java.util.concurrent.TimeUnit +import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile as RemoteNotificationProfile +import org.whispersystems.signalservice.internal.storage.protos.Recipient as RemoteRecipient @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE, application = Application::class) class NotificationProfileTablesTest { - @get:Rule - val appDependencies = MockAppDependenciesRule() @get:Rule - val signalDatabaseRule = SignalDatabaseRule() + val recipients = RecipientTestRule() + + private lateinit var alice: RecipientId + private lateinit var profile1: NotificationProfile + + @Before + fun setUp() { + every { RemoteConfig.messageQueueTime } returns TimeUnit.DAYS.toMillis(45) + + alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) + + profile1 = NotificationProfile( + id = 1, + name = "profile1", + emoji = "", + createdAt = 1000L, + schedule = NotificationProfileSchedule(id = 1), + allowedMembers = setOf(alice), + notificationProfileId = NotificationProfileId.generate(), + deletedTimestampMs = 0, + storageServiceId = StorageId.forNotificationProfile(byteArrayOf(1, 2, 3)) + ) + + SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileTable.TABLE_NAME) + SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileScheduleTable.TABLE_NAME) + SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileAllowedMembersTable.TABLE_NAME) + } @Test fun `addProfile for profile with empty schedule and members`() { @@ -164,6 +207,114 @@ class NotificationProfileTablesTest { assertThat(updated.schedule.daysEnabled, "Contains correct default days") .containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY) } + + @Test + fun givenARemoteProfile_whenIInsertLocally_thenIExpectAListWithThatProfile() { + val remoteRecord = + SignalNotificationProfileRecord( + profile1.storageServiceId!!, + RemoteNotificationProfile( + id = UuidUtil.toByteArray(profile1.notificationProfileId.uuid).toByteString(), + name = "profile1", + emoji = "", + color = profile1.color.colorInt(), + createdAtMs = 1000L, + allowedMembers = listOf(RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(alice).serviceId.get().toString()))), + allowAllMentions = false, + allowAllCalls = true, + scheduleEnabled = false, + scheduleStartTime = 900, + scheduleEndTime = 1700, + scheduleDaysEnabled = emptyList(), + deletedAtTimestampMs = 0 + ) + ) + + SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord) + val actualProfiles = SignalDatabase.notificationProfiles.getProfiles() + + assertEquals(listOf(profile1), actualProfiles) + } + + @Test + fun givenAProfile_whenIDeleteIt_thenIExpectAnEmptyList() { + val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile( + name = "Profile", + emoji = "avatar", + color = AvatarColor.A210, + createdAt = 1000L + ).profile + + SignalDatabase.notificationProfiles.deleteProfile(profile.id) + + assertThat(SignalDatabase.notificationProfiles.getProfiles()).isEmpty() + assertThat(SignalDatabase.notificationProfiles.getProfile(profile.id)) + } + + @Test + fun givenADeletedProfile_whenIGetIt_thenIExpectItToStillHaveASchedule() { + val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile( + name = "Profile", + emoji = "avatar", + color = AvatarColor.A210, + createdAt = 1000L + ).profile + + SignalDatabase.notificationProfiles.deleteProfile(profile.id) + + val deletedProfile = SignalDatabase.notificationProfiles.getProfile(profile.id)!! + assertThat(deletedProfile.schedule.enabled).isFalse() + assertThat(deletedProfile.schedule.start).isEqualTo(900) + assertThat(deletedProfile.schedule.end).isEqualTo(1700) + assertThat(deletedProfile.schedule.daysEnabled, "Contains correct default days") + .containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY) + } + + @Test + fun givenNotificationProfiles_whenIUpdateTheirStorageSyncIds_thenIExpectAnUpdatedList() { + SignalDatabase.notificationProfiles.createProfile( + name = "Profile1", + emoji = "avatar", + color = AvatarColor.A210, + createdAt = 1000L + ) + SignalDatabase.notificationProfiles.createProfile( + name = "Profile2", + emoji = "avatar", + color = AvatarColor.A210, + createdAt = 2000L + ) + + val existingMap = SignalDatabase.notificationProfiles.getStorageSyncIdsMap() + existingMap.forEach { (id, _) -> + SignalDatabase.notificationProfiles.applyStorageIdUpdate(id, StorageId.forNotificationProfile(StorageSyncHelper.generateKey())) + } + val updatedMap = SignalDatabase.notificationProfiles.getStorageSyncIdsMap() + + existingMap.forEach { (id, storageId) -> + assertNotEquals(storageId, updatedMap[id]) + } + } + + @Test + fun givenAProfileDeletedOver30Days_whenICleanUp_thenIExpectItToNotHaveAStorageId() { + val remoteRecord = + SignalNotificationProfileRecord( + profile1.storageServiceId!!, + RemoteNotificationProfile( + id = UuidUtil.toByteArray(profile1.notificationProfileId.uuid).toByteString(), + name = "profile1", + emoji = "", + color = profile1.color.colorInt(), + createdAtMs = 1000L, + deletedAtTimestampMs = 1000L + ) + ) + + SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord) + SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis()) + assertThat(SignalDatabase.notificationProfiles.getStorageSyncIds()).isEmpty() + } } private val NotificationProfileTables.NotificationProfileChangeResult.profile: NotificationProfile diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/OneTimePreKeyTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/OneTimePreKeyTableTest.kt similarity index 88% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/OneTimePreKeyTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/OneTimePreKeyTableTest.kt index b493fdbf5c..980c3a9b6e 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/OneTimePreKeyTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/OneTimePreKeyTableTest.kt @@ -5,10 +5,15 @@ package org.thoughtcrime.securesms.database +import android.app.Application import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +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 import org.signal.core.models.ServiceId.ACI import org.signal.core.models.ServiceId.PNI @@ -18,10 +23,20 @@ import org.signal.core.util.select import org.signal.core.util.update import org.signal.libsignal.protocol.ecc.ECKeyPair import org.signal.libsignal.protocol.state.PreKeyRecord +import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule +import org.thoughtcrime.securesms.testutil.SignalDatabaseRule import java.util.UUID +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class OneTimePreKeyTableTest { + @get:Rule + val appDependencies = MockAppDependenciesRule() + + @get:Rule + val signalDatabaseRule = SignalDatabaseRule() + private val aci: ACI = ACI.from(UUID.randomUUID()) private val pni: PNI = PNI.from(UUID.randomUUID()) @@ -117,7 +132,7 @@ class OneTimePreKeyTableTest { record = PreKeyRecord(id, ECKeyPair.generate()) ) - val count = SignalDatabase.rawDatabase + val count = SignalDatabase.writableDatabase .update(OneTimePreKeyTable.TABLE_NAME) .values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime) .where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId()) @@ -127,7 +142,7 @@ class OneTimePreKeyTableTest { } private fun getStaleTime(account: ServiceId, id: Int): Long? { - return SignalDatabase.rawDatabase + return SignalDatabase.writableDatabase .select(OneTimePreKeyTable.STALE_TIMESTAMP) .from(OneTimePreKeyTable.TABLE_NAME) .where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId()) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/PollTablesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/PollTablesTest.kt similarity index 90% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/PollTablesTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/PollTablesTest.kt index f4a8b5271b..8c0b14cdf9 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/PollTablesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/PollTablesTest.kt @@ -1,12 +1,14 @@ 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.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.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.mms.IncomingMessage @@ -14,15 +16,18 @@ import org.thoughtcrime.securesms.polls.PollOption import org.thoughtcrime.securesms.polls.PollRecord import org.thoughtcrime.securesms.polls.Voter import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testutil.RecipientTestRule -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class PollTablesTest { @get:Rule - val harness = SignalActivityRule() + val recipients = RecipientTestRule() private lateinit var poll1: PollRecord + private lateinit var other0: RecipientId @Before fun setUp() { @@ -44,8 +49,9 @@ class PollTablesTest { SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollOptionTable.TABLE_NAME) SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollVoteTable.TABLE_NAME) - val message = IncomingMessage(type = MessageType.NORMAL, from = harness.others[0], sentTimeMillis = 100, serverTimeMillis = 100, receivedTimeMillis = 100) - SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(harness.others[0], isGroup = false)) + other0 = recipients.createRecipient("Buddy #0") + val message = IncomingMessage(type = MessageType.NORMAL, from = other0, sentTimeMillis = 100, serverTimeMillis = 100, receivedTimeMillis = 100) + SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other0, isGroup = false)) } @Test diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt b/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt similarity index 94% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt index 40ea81f97f..43734cde59 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt @@ -1,13 +1,17 @@ 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.isNull import assertk.assertions.isPresent +import io.mockk.every 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.Hex @@ -26,12 +30,18 @@ import org.thoughtcrime.securesms.isAbsent import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.IncomingMessage import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testutil.RecipientTestRule +import org.whispersystems.signalservice.api.push.ServiceIds import java.util.UUID @Suppress("ClassName", "TestFunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class SmsDatabaseTest_collapseJoinRequestEventsIfPossible { + @get:Rule + val recipientRule = RecipientTestRule() + private lateinit var recipients: RecipientTable private lateinit var sms: MessageTable @@ -48,8 +58,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible { recipients = SignalDatabase.recipients sms = SignalDatabase.messages - SignalStore.account.setAci(localAci) - SignalStore.account.setPni(localPni) + every { recipientRule.signalStore.account.getServiceIds() } returns ServiceIds(localAci, localPni) alice = recipients.getOrInsertFromServiceId(aliceServiceId) bob = recipients.getOrInsertFromServiceId(bobServiceId) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/StorySendTableTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/StorySendTableTest.kt similarity index 98% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/StorySendTableTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/StorySendTableTest.kt index dc290b2d92..0cf6fcb617 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/StorySendTableTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/StorySendTableTest.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.database -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.Application import assertk.assertThat import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.hasSize @@ -16,20 +16,23 @@ 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.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.StoryType 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 org.whispersystems.signalservice.api.push.DistributionId import java.util.UUID -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) class StorySendTableTest { @get:Rule - val harness = SignalActivityRule(othersCount = 0, createGroup = false) + val recipients = RecipientTestRule() private val distributionId1 = DistributionId.from(UUID.randomUUID()) private val distributionId2 = DistributionId.from(UUID.randomUUID()) diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt new file mode 100644 index 0000000000..d67e8f4614 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt @@ -0,0 +1,97 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +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.count +import org.signal.core.util.delete +import org.signal.core.util.readToSingleInt +import org.signal.core.util.update +import org.thoughtcrime.securesms.database.DistributionListTables +import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.testutil.SignalDatabaseRule +import org.whispersystems.signalservice.api.push.DistributionId +import java.util.UUID + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class MyStoryMigrationTest { + + @get:Rule val signalDatabaseRule = SignalDatabaseRule() + + @Test + fun givenAValidMyStory_whenIMigrate_thenIExpectMyStoryToBeValid() { + assertValidMyStoryExists() + + runMigration() + + assertValidMyStoryExists() + } + + @Test + fun givenNoMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { + deleteMyStory() + + runMigration() + + assertValidMyStoryExists() + } + + @Test + fun givenA00000000DistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { + setMyStoryDistributionId("0000-0000") + + runMigration() + + assertValidMyStoryExists() + } + + @Test + fun givenARandomDistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { + setMyStoryDistributionId(UUID.randomUUID().toString()) + + runMigration() + + assertValidMyStoryExists() + } + + private fun setMyStoryDistributionId(serializedId: String) { + signalDatabaseRule.writeableDatabase + .update(DistributionListTables.LIST_TABLE_NAME) + .values(DistributionListTables.DISTRIBUTION_ID to serializedId) + .where("_id = ?", DistributionListId.MY_STORY_ID) + .run() + } + + private fun deleteMyStory() { + signalDatabaseRule.writeableDatabase + .delete(DistributionListTables.LIST_TABLE_NAME) + .where("_id = ?", DistributionListId.MY_STORY_ID) + .run() + } + + private fun assertValidMyStoryExists() { + val count = signalDatabaseRule.writeableDatabase + .count() + .from(DistributionListTables.LIST_TABLE_NAME) + .where("_id = ? AND ${DistributionListTables.DISTRIBUTION_ID} = ?", DistributionListId.MY_STORY_ID, DistributionId.MY_STORY.toString()) + .run() + .readToSingleInt() + + assertEquals("assertValidMyStoryExists: Query produced an unexpected count.", 1, count) + } + + private fun runMigration() { + V151_MyStoryMigration.migrate( + ApplicationProvider.getApplicationContext(), + signalDatabaseRule.writeableDatabase, + 0, + 1 + ) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt b/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt index eebfacf03c..c19527db6d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/testing/JdbcSqliteDatabase.kt @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.testing import android.content.ContentValues import android.database.Cursor import android.database.MatrixCursor +import android.database.sqlite.SQLiteConstraintException import android.database.sqlite.SQLiteTransactionListener import android.os.CancellationSignal import android.util.Pair @@ -15,9 +16,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteProgram import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteStatement +import org.sqlite.SQLiteException import java.sql.Connection import java.sql.DriverManager import java.sql.PreparedStatement +import java.sql.SQLException import java.sql.Types import java.util.Locale @@ -130,7 +133,7 @@ class JdbcSqliteDatabase private constructor(private val connection: Connection) // sqlite-jdbc throws if you call executeQuery() on a non-SELECT statement. // Some callers (e.g. migrations) pass UPDATE/INSERT through rawQuery, so we // use execute() and check whether there's a result set. - val hasResultSet = stmt.execute() + val hasResultSet = translatingConstraintExceptions { stmt.execute() } if (!hasResultSet) { stmt.close() return MatrixCursor(emptyArray()) @@ -158,14 +161,14 @@ class JdbcSqliteDatabase private constructor(private val connection: Connection) val keys = values.keySet().toList() if (keys.isEmpty()) { val sql = "INSERT${CONFLICT_VALUES[conflictAlgorithm]} INTO $table DEFAULT VALUES" - connection.createStatement().use { it.executeUpdate(sql) } + translatingConstraintExceptions { connection.createStatement().use { it.executeUpdate(sql) } } } else { val columns = keys.joinToString(", ") val placeholders = keys.joinToString(", ") { "?" } val sql = "INSERT${CONFLICT_VALUES[conflictAlgorithm]} INTO $table ($columns) VALUES ($placeholders)" val stmt = connection.prepareStatement(sql) keys.forEachIndexed { index, key -> bindArg(stmt, index + 1, values.get(key)) } - stmt.executeUpdate() + translatingConstraintExceptions { stmt.executeUpdate() } stmt.close() } return connection.createStatement().use { s -> @@ -188,7 +191,7 @@ class JdbcSqliteDatabase private constructor(private val connection: Connection) var paramIndex = 1 keys.forEach { key -> bindArg(stmt, paramIndex++, values.get(key)) } whereArgs?.forEach { arg -> bindArg(stmt, paramIndex++, arg) } - val count = stmt.executeUpdate() + val count = translatingConstraintExceptions { stmt.executeUpdate() } stmt.close() return count } @@ -202,7 +205,7 @@ class JdbcSqliteDatabase private constructor(private val connection: Connection) } val stmt = connection.prepareStatement(sql) whereArgs?.forEachIndexed { index, arg -> bindArg(stmt, index + 1, arg) } - val count = stmt.executeUpdate() + val count = translatingConstraintExceptions { stmt.executeUpdate() } stmt.close() return count } @@ -212,13 +215,13 @@ class JdbcSqliteDatabase private constructor(private val connection: Connection) // region ExecSQL override fun execSQL(sql: String) { - connection.createStatement().use { it.execute(sql) } + translatingConstraintExceptions { connection.createStatement().use { it.execute(sql) } } } override fun execSQL(sql: String, bindArgs: Array) { val stmt = connection.prepareStatement(sql) bindArgs(stmt, bindArgs) - stmt.execute() + translatingConstraintExceptions { stmt.execute() } stmt.close() } @@ -371,15 +374,15 @@ class JdbcSqliteStatement( ) : SupportSQLiteStatement { override fun execute() { - statement.execute() + translatingConstraintExceptions { statement.execute() } } override fun executeUpdateDelete(): Int { - return statement.executeUpdate() + return translatingConstraintExceptions { statement.executeUpdate() } } override fun executeInsert(): Long { - statement.executeUpdate() + translatingConstraintExceptions { statement.executeUpdate() } return connection.createStatement().use { s -> s.executeQuery("SELECT last_insert_rowid()").use { rs -> if (rs.next()) rs.getLong(1) else -1L @@ -425,3 +428,21 @@ class JdbcSqliteStatement( statement.close() } } + +/** Primary SQLite result code for a constraint violation (extended codes share the low byte). */ +private const val SQLITE_CONSTRAINT_PRIMARY_CODE = 19 + +/** + * Runs [block], translating sqlite-jdbc constraint violations into the + * [SQLiteConstraintException] that production code catches when running on a real device. + */ +private inline fun translatingConstraintExceptions(block: () -> T): T { + try { + return block() + } catch (e: SQLException) { + if (e is SQLiteException && (e.resultCode.code and 0xFF) == SQLITE_CONSTRAINT_PRIMARY_CODE) { + throw SQLiteConstraintException(e.message) + } + throw e + } +}