mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
BackupDeleteJob integration tests.
This commit is contained in:
committed by
Greyson Parrelli
parent
995215be2a
commit
e93f889115
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.contains
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isNull
|
||||||
|
import assertk.assertions.isTrue
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import io.mockk.verify
|
||||||
|
import okio.IOException
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.thoughtcrime.securesms.backup.DeletionState
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||||
|
|
||||||
|
class BackupDeleteJobTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkObject(RemoteConfig)
|
||||||
|
every { RemoteConfig.messageBackups } returns true
|
||||||
|
every { RemoteConfig.internalUser } returns true
|
||||||
|
every { RemoteConfig.defaultMaxBackoff } returns 1000L
|
||||||
|
|
||||||
|
mockkObject(BackupRepository)
|
||||||
|
every { BackupRepository.getBackupTier() } returns NetworkResult.Success(MessageBackupTier.PAID)
|
||||||
|
every { BackupRepository.deleteBackup() } returns NetworkResult.Success(Unit)
|
||||||
|
every { BackupRepository.deleteMediaBackup() } returns NetworkResult.Success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenBackupsNotEnabled_whenIRun_thenIExpectFailure() {
|
||||||
|
every { RemoteConfig.messageBackups } returns false
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenUserNotRegistered_whenIRun_thenIExpectFailure() {
|
||||||
|
mockkObject(SignalStore) {
|
||||||
|
every { SignalStore.account.isRegistered } returns false
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenLinkedDevice_whenIRun_thenIExpectFailure() {
|
||||||
|
mockkObject(SignalStore) {
|
||||||
|
every { SignalStore.account.isRegistered } returns true
|
||||||
|
every { SignalStore.account.isLinkedDevice } returns true
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenDeletionStateNone_whenIRun_thenIExpectFailure() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.NONE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenDeletionStateFailed_whenIRun_thenIExpectFailure() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.FAILED
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenDeletionStateComplete_whenIRun_thenIExpectFailure() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.NONE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenDeletionStateAwaitingMediaDownload_whenIRun_thenIExpectRetry() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.AWAITING_MEDIA_DOWNLOAD
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isRetry).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenDeletionStateClearLocalState_whenIRun_thenIDeleteLocalState() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
job.run()
|
||||||
|
|
||||||
|
val jobData = BackupDeleteJobData.ADAPTER.decode(job.serialize())
|
||||||
|
|
||||||
|
assertThat(SignalStore.backup.backupTier).isNull()
|
||||||
|
assertThat(jobData.tier).isEqualTo(BackupDeleteJobData.Tier.PAID)
|
||||||
|
assertThat(jobData.completed).contains(BackupDeleteJobData.Stage.CLEAR_LOCAL_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenDeletionStateClearLocalState_whenIRun_thenIUnsubscribe() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
job.run()
|
||||||
|
|
||||||
|
val jobData = BackupDeleteJobData.ADAPTER.decode(job.serialize())
|
||||||
|
|
||||||
|
assertThat(SignalStore.backup.backupTier).isNull()
|
||||||
|
assertThat(jobData.tier).isEqualTo(BackupDeleteJobData.Tier.PAID)
|
||||||
|
assertThat(jobData.completed).contains(BackupDeleteJobData.Stage.CANCEL_SUBSCRIBER)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenMediaOffloaded_whenIRun_thenIExpectAwaitingMediaDownload() {
|
||||||
|
mockkObject(SignalDatabase)
|
||||||
|
every { SignalDatabase.attachments.getRemainingRestorableAttachmentSize() } returns 1
|
||||||
|
every { SignalDatabase.attachments.getOptimizedMediaAttachmentSize() } returns 1
|
||||||
|
every { SignalDatabase.attachments.clearAllArchiveData() } returns Unit
|
||||||
|
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
val result = job.run()
|
||||||
|
val jobData = BackupDeleteJobData.ADAPTER.decode(job.serialize())
|
||||||
|
|
||||||
|
assertThat(SignalStore.backup.backupTier).isNull()
|
||||||
|
assertThat(jobData.tier).isEqualTo(BackupDeleteJobData.Tier.PAID)
|
||||||
|
assertThat(jobData.completed).contains(BackupDeleteJobData.Stage.CLEAR_LOCAL_STATE)
|
||||||
|
assertThat(jobData.completed).contains(BackupDeleteJobData.Stage.CANCEL_SUBSCRIBER)
|
||||||
|
|
||||||
|
assertThat(SignalStore.backup.deletionState).isEqualTo(DeletionState.AWAITING_MEDIA_DOWNLOAD)
|
||||||
|
assertThat(result.isRetry).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenMediaDownloadFinished_whenIRun_thenIExpectDeletion() {
|
||||||
|
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
|
||||||
|
|
||||||
|
val job = BackupDeleteJob(
|
||||||
|
backupDeleteJobData = BackupDeleteJobData(
|
||||||
|
tier = BackupDeleteJobData.Tier.PAID,
|
||||||
|
completed = listOf(
|
||||||
|
BackupDeleteJobData.Stage.CLEAR_LOCAL_STATE,
|
||||||
|
BackupDeleteJobData.Stage.CANCEL_SUBSCRIBER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
BackupRepository.deleteBackup()
|
||||||
|
BackupRepository.deleteMediaBackup()
|
||||||
|
BackupRepository.resetInitializedStateAndAuthCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
assertThat(SignalStore.backup.deletionState).isEqualTo(DeletionState.COMPLETE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNetworkErrorDuringMessageBackupDeletion_whenIRun_thenIExpectRetry() {
|
||||||
|
every { BackupRepository.deleteBackup() } returns NetworkResult.NetworkError(IOException())
|
||||||
|
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isRetry).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNetworkErrorDuringMediaBackupDeletion_whenIRun_thenIExpectRetry() {
|
||||||
|
every { BackupRepository.deleteMediaBackup() } returns NetworkResult.NetworkError(IOException())
|
||||||
|
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isRetry).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenRateLimitedDuringMessageBackupDeletion_whenIRun_thenIExpectRetry() {
|
||||||
|
every { BackupRepository.deleteBackup() } returns NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(429))
|
||||||
|
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isRetry).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenRateLimitedDuringMediaBackupDeletion_whenIRun_thenIExpectRetry() {
|
||||||
|
every { BackupRepository.deleteMediaBackup() } returns NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(429))
|
||||||
|
|
||||||
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
|
val result = job.run()
|
||||||
|
|
||||||
|
assertThat(result.isRetry).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
|
|||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
import org.whispersystems.signalservice.api.NetworkResult
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -52,6 +53,26 @@ class BackupDeleteJob private constructor(
|
|||||||
override fun getFactoryKey(): String = KEY
|
override fun getFactoryKey(): String = KEY
|
||||||
|
|
||||||
override fun run(): Result {
|
override fun run(): Result {
|
||||||
|
if (!RemoteConfig.messageBackups) {
|
||||||
|
Log.w(TAG, "Message backups are not available on this device. Exiting without local cleanup.")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SignalStore.account.isRegistered) {
|
||||||
|
Log.w(TAG, "User not registered. Exiting without local cleanup.")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SignalStore.account.isLinkedDevice) {
|
||||||
|
Log.w(TAG, "User is on a linked device. Exiting without local cleanup.")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SignalStore.backup.deletionState.isIdle()) {
|
||||||
|
Log.w(TAG, "Invalid state ${SignalStore.backup.deletionState}. Exiting without local cleanup.")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
val result = doRun()
|
val result = doRun()
|
||||||
|
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
@@ -63,11 +84,6 @@ class BackupDeleteJob private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun doRun(): Result {
|
private fun doRun(): Result {
|
||||||
if (SignalStore.backup.deletionState.isIdle()) {
|
|
||||||
Log.w(TAG, "Invalid state ${SignalStore.backup.deletionState}. Exiting.")
|
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
|
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
|
||||||
Log.i(TAG, "Awaiting media download. Scheduling retry.")
|
Log.i(TAG, "Awaiting media download. Scheduling retry.")
|
||||||
return Result.retry(5.seconds.inWholeMilliseconds)
|
return Result.retry(5.seconds.inWholeMilliseconds)
|
||||||
|
|||||||
Reference in New Issue
Block a user