Convert a batch of androidTest db tests to unit tests.

This commit is contained in:
Greyson Parrelli
2026-06-08 11:53:36 -04:00
committed by Cody Henthorne
parent 0ebeb5aa92
commit 135bc6e560
17 changed files with 555 additions and 406 deletions
@@ -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)
}
}
@@ -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
}
@@ -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
)
}
}
@@ -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),
@@ -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<RecipientId> = recipientList(1, 2, 3)
val members: List<RecipientId> = 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<RecipientId> = recipientList(1, 2, 3)
val members: List<RecipientId> = 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<RecipientId> {
return (0 until count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}
@@ -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() {
@@ -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
@@ -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
}
}
}
@@ -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<RecipientId>
@@ -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())) }
}
@@ -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<MessageTable.InsertResult> {
return SignalDatabase.messages.insertMessageInbox(message, threadId)
}
}
@@ -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
@@ -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())
@@ -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
@@ -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)
@@ -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())
@@ -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
)
}
}
@@ -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<out Any?>) {
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 <T> 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
}
}