mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 10:51:27 +01:00
Add key reuse to create keys operation in backup job.
This commit is contained in:
committed by
Michelle Tang
parent
2872020c1f
commit
0d390769d4
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentValues
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import org.junit.BeforeClass
|
||||
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.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
|
||||
import org.thoughtcrime.securesms.testutil.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class AttachmentTableTest_createRemoteKeyForAttachmentsThatNeedArchiveUpload {
|
||||
|
||||
@get:Rule val signalDatabaseRule = SignalDatabaseRule()
|
||||
|
||||
@get:Rule val applicationDependencies = MockAppDependenciesRule()
|
||||
|
||||
companion object {
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUpClass() {
|
||||
Log.initialize(SystemOutLogger())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenNoEligibleAttachments_returnsZero() {
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenAttachmentHasArchiveTransferStateInProgress_returnsZero() {
|
||||
val attachmentId = insertWithData()
|
||||
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS)
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenAttachmentMissingDataFile_returnsZero() {
|
||||
val attachmentId = insertWithoutData() // No data file set
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTransferStateNotDone_returnsZero() {
|
||||
val attachmentId = insertWithData()
|
||||
SignalDatabase.attachments.setTransferState(1L, attachmentId, 1) // Not TRANSFER_PROGRESS_DONE (0)
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenAttachmentAlreadyHasRemoteKey_returnsZero() {
|
||||
val attachmentId = insertWithData()
|
||||
// Set a remote key
|
||||
val remoteKey = Base64.encodeWithPadding(byteArrayOf(1, 2, 3, 4))
|
||||
setRemoteKey(attachmentId, remoteKey)
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenOneEligibleAttachment_returnsOneAndCreatesRemoteKey() {
|
||||
val attachmentId = insertWithData()
|
||||
|
||||
// Verify attachment has no remote key initially
|
||||
val attachmentBefore = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertThat(attachmentBefore?.remoteKey).isNull()
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(1)
|
||||
|
||||
// Verify remote key was created
|
||||
val attachmentAfter = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertThat(attachmentAfter?.remoteKey).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenMultipleEligibleAttachments_returnsCorrectCountAndCreatesKeys() {
|
||||
val attachmentId1 = insertWithData()
|
||||
val attachmentId2 = insertWithData()
|
||||
val attachmentId3 = insertWithData()
|
||||
|
||||
// Verify all attachments have no remote keys initially
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId1)?.remoteKey).isNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId2)?.remoteKey).isNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId3)?.remoteKey).isNull()
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(3)
|
||||
|
||||
// Verify all remote keys were created
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId1)?.remoteKey).isNotNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId2)?.remoteKey).isNotNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId3)?.remoteKey).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenMixedScenarios_returnsCorrectCount() {
|
||||
// Eligible attachment - has data file, transfer done, archive state NONE, no remote key
|
||||
val eligibleAttachmentId = insertWithData()
|
||||
|
||||
// Ineligible - has remote key already
|
||||
val attachmentWithKeyId = insertWithData()
|
||||
setRemoteKey(attachmentWithKeyId, Base64.encodeWithPadding(byteArrayOf(1, 2, 3, 4)))
|
||||
|
||||
// Ineligible - archive transfer state is not NONE
|
||||
val inProgressAttachmentId = insertWithData()
|
||||
SignalDatabase.attachments.setArchiveTransferState(inProgressAttachmentId, AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS)
|
||||
|
||||
// Ineligible - no data file
|
||||
val noDataFileAttachmentId = insertWithoutData()
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(1)
|
||||
|
||||
// Verify only the eligible attachment got a remote key
|
||||
assertThat(SignalDatabase.attachments.getAttachment(eligibleAttachmentId)?.remoteKey).isNotNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentWithKeyId)?.remoteKey).isNotNull() // Already had one
|
||||
assertThat(SignalDatabase.attachments.getAttachment(inProgressAttachmentId)?.remoteKey).isNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(noDataFileAttachmentId)?.remoteKey).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenCalledTwice_secondCallReturnsZero() {
|
||||
val attachmentId = insertWithData()
|
||||
|
||||
// First call should create remote key
|
||||
val firstResult = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(firstResult.totalCount).isEqualTo(1)
|
||||
|
||||
// Second call should find no eligible attachments since remote key now exists
|
||||
val secondResult = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(secondResult.totalCount).isEqualTo(0)
|
||||
|
||||
// Verify attachment still has remote key
|
||||
assertThat(SignalDatabase.attachments.getAttachment(attachmentId)?.remoteKey).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenMatchingDataFileAndHashExists_reusesRemoteKey() {
|
||||
val dataFile = "/shared/path/attachment.jpg"
|
||||
val dataHashEnd = "shared_hash_end"
|
||||
val existingRemoteKey = Base64.encodeWithPadding(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8))
|
||||
|
||||
// Create source attachment with remote key, location, digest
|
||||
val sourceAttachmentId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
remoteKey = existingRemoteKey,
|
||||
dataHashEnd = dataHashEnd,
|
||||
remoteLocation = "cdn-location-123",
|
||||
remoteDigest = byteArrayOf(9, 10, 11, 12)
|
||||
)
|
||||
|
||||
// Create target attachment with same data file and hash but no remote key
|
||||
val targetAttachmentId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
remoteKey = null,
|
||||
dataHashEnd = dataHashEnd
|
||||
)
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(1)
|
||||
|
||||
// Verify target attachment reused the remote key, location, and digest
|
||||
val targetAttachment = SignalDatabase.attachments.getAttachment(targetAttachmentId)!!
|
||||
assertThat(targetAttachment.remoteKey).isEqualTo(existingRemoteKey)
|
||||
assertThat(targetAttachment.remoteLocation).isEqualTo("cdn-location-123")
|
||||
assertThat(targetAttachment.remoteDigest.contentEquals(byteArrayOf(9, 10, 11, 12))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenMultipleMatchesExist_reusesFromLatestMatch() {
|
||||
val dataFile = "/shared/path/attachment.jpg"
|
||||
val firstRemoteKey = Base64.encodeWithPadding(byteArrayOf(1, 2, 3, 4))
|
||||
val secondRemoteKey = Base64.encodeWithPadding(byteArrayOf(5, 6, 7, 8))
|
||||
|
||||
// Create first source attachment
|
||||
val firstSourceId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
remoteKey = firstRemoteKey,
|
||||
remoteLocation = "first-location",
|
||||
remoteDigest = byteArrayOf(9, 10, 11, 12)
|
||||
)
|
||||
|
||||
// Create second source attachment (inserted later)
|
||||
val secondSourceId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
remoteKey = secondRemoteKey,
|
||||
remoteLocation = "second-location",
|
||||
remoteDigest = byteArrayOf(13, 14, 15, 16)
|
||||
)
|
||||
|
||||
// Create target attachment
|
||||
val targetAttachmentId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(1)
|
||||
|
||||
// Verify target attachment reused from the first match (by ID order desc)
|
||||
val targetAttachment = SignalDatabase.attachments.getAttachment(targetAttachmentId)!!
|
||||
assertThat(targetAttachment.remoteKey).isEqualTo(secondRemoteKey)
|
||||
assertThat(targetAttachment.remoteLocation).isEqualTo("second-location")
|
||||
assertThat(targetAttachment.remoteDigest.contentEquals(byteArrayOf(13, 14, 15, 16))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSourceHasNoRemoteData_generatesNewKey() {
|
||||
val dataFile = "/shared/path/attachment.jpg"
|
||||
|
||||
// Create source attachment without remote key (should not be used for reuse)
|
||||
val sourceAttachmentId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
// Create target attachment
|
||||
val targetAttachmentId = insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
val result = SignalDatabase.attachments.createRemoteKeyForAttachmentsThatNeedArchiveUpload()
|
||||
assertThat(result.totalCount).isEqualTo(2) // Both should get new keys
|
||||
|
||||
// Verify both attachments got new keys
|
||||
assertThat(SignalDatabase.attachments.getAttachment(sourceAttachmentId)?.remoteKey).isNotNull()
|
||||
assertThat(SignalDatabase.attachments.getAttachment(targetAttachmentId)?.remoteKey).isNotNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an attachment that meets all criteria for archive upload:
|
||||
* - ARCHIVE_TRANSFER_STATE = NONE (0)
|
||||
* - DATA_FILE is not null
|
||||
* - TRANSFER_STATE = TRANSFER_PROGRESS_DONE (0)
|
||||
* - REMOTE_KEY is null
|
||||
*/
|
||||
fun insertWithData(dataFile: String = "/fake/path/attachment-${UUID.randomUUID()}.jpg"): AttachmentId {
|
||||
return insertAttachmentDirectly(
|
||||
dataFile = dataFile,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
remoteKey = null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an attachment without a data file (ineligible for archive upload)
|
||||
*/
|
||||
fun insertWithoutData(): AttachmentId {
|
||||
return insertAttachmentDirectly(
|
||||
dataFile = null,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
remoteKey = null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly inserts an attachment with minimal required columns for testing
|
||||
*/
|
||||
private fun insertAttachmentDirectly(
|
||||
dataFile: String?,
|
||||
transferState: Int,
|
||||
archiveTransferState: Int,
|
||||
remoteKey: String?,
|
||||
dataHashEnd: String? = null,
|
||||
remoteLocation: String? = null,
|
||||
remoteDigest: ByteArray? = null
|
||||
): AttachmentId {
|
||||
val db = SignalDatabase.attachments.writableDatabase
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(AttachmentTable.DATA_FILE, dataFile)
|
||||
put(AttachmentTable.DATA_RANDOM, dataFile?.toByteArray())
|
||||
put(AttachmentTable.TRANSFER_STATE, transferState)
|
||||
put(AttachmentTable.ARCHIVE_TRANSFER_STATE, archiveTransferState)
|
||||
put(AttachmentTable.REMOTE_KEY, remoteKey)
|
||||
put(AttachmentTable.DATA_HASH_END, dataHashEnd)
|
||||
put(AttachmentTable.REMOTE_LOCATION, remoteLocation)
|
||||
put(AttachmentTable.REMOTE_DIGEST, remoteDigest)
|
||||
}
|
||||
|
||||
val id = db.insert(AttachmentTable.TABLE_NAME, null, values)
|
||||
return AttachmentId(id)
|
||||
}
|
||||
|
||||
private fun setRemoteKey(attachmentId: AttachmentId, remoteKey: String) {
|
||||
SignalDatabase.attachments.writableDatabase
|
||||
.update(AttachmentTable.TABLE_NAME)
|
||||
.values(AttachmentTable.REMOTE_KEY to remoteKey)
|
||||
.where("${AttachmentTable.ID} = ?", attachmentId.id)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentValues
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
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.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.testutil.SignalDatabaseMigrationRule
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class V287_FixInvalidArchiveStateTest {
|
||||
|
||||
@get:Rule val signalDatabaseRule = SignalDatabaseMigrationRule(286)
|
||||
|
||||
@Test
|
||||
fun migrate_whenArchiveTransferStateIsFinishedAndRemoteKeyIsNull_clearsArchiveCdnAndSetsStateToNone() {
|
||||
val attachmentId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
val db = signalDatabaseRule.database
|
||||
V287_FixInvalidArchiveState.migrate(ApplicationProvider.getApplicationContext(), db, 286, 287)
|
||||
|
||||
val cursor = db.query(
|
||||
AttachmentTable.TABLE_NAME,
|
||||
arrayOf(AttachmentTable.ARCHIVE_CDN, AttachmentTable.ARCHIVE_TRANSFER_STATE),
|
||||
"${AttachmentTable.ID} = ?",
|
||||
arrayOf(attachmentId.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.isNull(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_CDN))).isEqualTo(true)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_TRANSFER_STATE)))
|
||||
.isEqualTo(AttachmentTable.ArchiveTransferState.NONE.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenArchiveTransferStateIsFinishedButHasRemoteKey_noChanges() {
|
||||
val attachmentId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = "some-remote-key"
|
||||
)
|
||||
|
||||
val db = signalDatabaseRule.database
|
||||
V287_FixInvalidArchiveState.migrate(ApplicationProvider.getApplicationContext(), db, 286, 287)
|
||||
|
||||
val cursor = db.query(
|
||||
AttachmentTable.TABLE_NAME,
|
||||
arrayOf(AttachmentTable.ARCHIVE_CDN, AttachmentTable.ARCHIVE_TRANSFER_STATE),
|
||||
"${AttachmentTable.ID} = ?",
|
||||
arrayOf(attachmentId.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_CDN))).isEqualTo(2)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_TRANSFER_STATE)))
|
||||
.isEqualTo(AttachmentTable.ArchiveTransferState.FINISHED.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenArchiveTransferStateIsNoneAndRemoteKeyIsNull_noChanges() {
|
||||
val attachmentId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
val db = signalDatabaseRule.database
|
||||
V287_FixInvalidArchiveState.migrate(ApplicationProvider.getApplicationContext(), db, 286, 287)
|
||||
|
||||
val cursor = db.query(
|
||||
AttachmentTable.TABLE_NAME,
|
||||
arrayOf(AttachmentTable.ARCHIVE_CDN, AttachmentTable.ARCHIVE_TRANSFER_STATE),
|
||||
"${AttachmentTable.ID} = ?",
|
||||
arrayOf(attachmentId.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_CDN))).isEqualTo(2)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_TRANSFER_STATE)))
|
||||
.isEqualTo(AttachmentTable.ArchiveTransferState.NONE.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenArchiveTransferStateIsUploadInProgressAndRemoteKeyIsNull_noChanges() {
|
||||
val attachmentId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
val db = signalDatabaseRule.database
|
||||
V287_FixInvalidArchiveState.migrate(ApplicationProvider.getApplicationContext(), db, 286, 287)
|
||||
|
||||
val cursor = db.query(
|
||||
AttachmentTable.TABLE_NAME,
|
||||
arrayOf(AttachmentTable.ARCHIVE_CDN, AttachmentTable.ARCHIVE_TRANSFER_STATE),
|
||||
"${AttachmentTable.ID} = ?",
|
||||
arrayOf(attachmentId.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_CDN))).isEqualTo(2)
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_TRANSFER_STATE)))
|
||||
.isEqualTo(AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenMultipleAttachmentsWithMixedStates_onlyUpdatesFinishedStateWithNullRemoteKey() {
|
||||
// These should be updated (FINISHED state + null remote_key)
|
||||
val finishedNoKeyId1 = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
archiveCdn = 1,
|
||||
remoteKey = null
|
||||
)
|
||||
val finishedNoKeyId2 = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
archiveCdn = 3,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
// These should NOT be updated
|
||||
val finishedWithKeyId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = "some-key"
|
||||
)
|
||||
val noneId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = null
|
||||
)
|
||||
val inProgressId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value,
|
||||
archiveCdn = 1,
|
||||
remoteKey = null
|
||||
)
|
||||
|
||||
val db = signalDatabaseRule.database
|
||||
V287_FixInvalidArchiveState.migrate(ApplicationProvider.getApplicationContext(), db, 286, 287)
|
||||
|
||||
// Check finished attachments with null remote_key were updated
|
||||
assertArchiveState(finishedNoKeyId1, expectedCdn = null, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
|
||||
assertArchiveState(finishedNoKeyId2, expectedCdn = null, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
|
||||
|
||||
// Check other states were not changed
|
||||
assertArchiveState(finishedWithKeyId, expectedCdn = 2, expectedState = AttachmentTable.ArchiveTransferState.FINISHED.value)
|
||||
assertArchiveState(noneId, expectedCdn = 2, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
|
||||
assertArchiveState(inProgressId, expectedCdn = 1, expectedState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenNoAttachmentsMatchCriteria_noChanges() {
|
||||
val noneId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE.value,
|
||||
archiveCdn = 2,
|
||||
remoteKey = null
|
||||
)
|
||||
val inProgressId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value,
|
||||
archiveCdn = 1,
|
||||
remoteKey = null
|
||||
)
|
||||
val finishedWithKeyId = insertAttachmentWithArchiveState(
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
|
||||
archiveCdn = 3,
|
||||
remoteKey = "has-key"
|
||||
)
|
||||
|
||||
val db = signalDatabaseRule.database
|
||||
V287_FixInvalidArchiveState.migrate(ApplicationProvider.getApplicationContext(), db, 286, 287)
|
||||
|
||||
// Check no changes were made
|
||||
assertArchiveState(noneId, expectedCdn = 2, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
|
||||
assertArchiveState(inProgressId, expectedCdn = 1, expectedState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value)
|
||||
assertArchiveState(finishedWithKeyId, expectedCdn = 3, expectedState = AttachmentTable.ArchiveTransferState.FINISHED.value)
|
||||
}
|
||||
|
||||
private fun insertAttachmentWithArchiveState(archiveTransferState: Int, archiveCdn: Int?, remoteKey: String?): Long {
|
||||
val db = signalDatabaseRule.database
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(AttachmentTable.DATA_FILE, "/fake/path/attachment.jpg")
|
||||
put(AttachmentTable.DATA_RANDOM, "/fake/path/attachment.jpg".toByteArray())
|
||||
put(AttachmentTable.TRANSFER_STATE, AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
put(AttachmentTable.ARCHIVE_TRANSFER_STATE, archiveTransferState)
|
||||
if (archiveCdn != null) {
|
||||
put(AttachmentTable.ARCHIVE_CDN, archiveCdn)
|
||||
}
|
||||
put(AttachmentTable.REMOTE_KEY, remoteKey)
|
||||
}
|
||||
|
||||
return db.insert(AttachmentTable.TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
private fun assertArchiveState(attachmentId: Long, expectedCdn: Int?, expectedState: Int) {
|
||||
val db = signalDatabaseRule.database
|
||||
val cursor = db.query(
|
||||
AttachmentTable.TABLE_NAME,
|
||||
arrayOf(AttachmentTable.ARCHIVE_CDN, AttachmentTable.ARCHIVE_TRANSFER_STATE),
|
||||
"${AttachmentTable.ID} = ?",
|
||||
arrayOf(attachmentId.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
|
||||
if (expectedCdn == null) {
|
||||
assertThat(it.isNull(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_CDN))).isEqualTo(true)
|
||||
} else {
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_CDN))).isEqualTo(expectedCdn)
|
||||
}
|
||||
|
||||
assertThat(it.getInt(it.getColumnIndexOrThrow(AttachmentTable.ARCHIVE_TRANSFER_STATE))).isEqualTo(expectedState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import android.database.Cursor
|
||||
import android.database.SQLException
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder
|
||||
import java.util.Locale
|
||||
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
|
||||
@@ -228,7 +230,17 @@ class TestSignalSQLiteDatabase(private val database: SupportSQLiteDatabase) : Si
|
||||
}
|
||||
|
||||
override fun compileStatement(sql: String): SQLCipherSQLiteStatement {
|
||||
throw UnsupportedOperationException()
|
||||
val statement = database.compileStatement(sql)
|
||||
return mockk<SQLCipherSQLiteStatement> {
|
||||
every { bindNull(any()) } answers { statement.bindNull(firstArg()) }
|
||||
every { bindLong(any(), any()) } answers { statement.bindLong(firstArg(), secondArg()) }
|
||||
every { bindDouble(any(), any()) } answers { statement.bindDouble(firstArg(), secondArg()) }
|
||||
every { bindString(any(), any()) } answers { statement.bindString(firstArg(), secondArg()) }
|
||||
every { bindBlob(any(), any()) } answers { statement.bindBlob(firstArg(), secondArg()) }
|
||||
every { clearBindings() } answers { statement.clearBindings() }
|
||||
every { executeUpdateDelete() } answers { statement.executeUpdateDelete() }
|
||||
every { close() } answers { statement.close() }
|
||||
}
|
||||
}
|
||||
|
||||
override val isReadOnly: Boolean
|
||||
|
||||
Reference in New Issue
Block a user