diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 0d681c7487..51fa0f0b7b 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -44,6 +44,7 @@ import to.bitkit.di.IoDispatcher import to.bitkit.di.json import to.bitkit.ext.formatPlural import to.bitkit.ext.nowMillis +import to.bitkit.ext.runSuspendCatching import to.bitkit.models.ActivityBackupV1 import to.bitkit.models.BackupCategory import to.bitkit.models.BackupItemStatus @@ -435,7 +436,11 @@ class BackupRepo @Inject constructor( it.copy(running = true, required = backupRequired) } - vssBackupClient.putObject(key = category.name, data = getBackupDataBytes(category)) + val data = runSuspendCatching { getBackupDataBytes(category) } + .onFailure { markBackupFailed(category, backupRequired, it) } + .getOrNull() ?: return@withContext + + vssBackupClient.putObject(key = category.name, data = data) .onSuccess { runningBackups -= category failedBackupRequired -= category @@ -447,18 +452,20 @@ class BackupRepo @Inject constructor( } Logger.info("Backup succeeded for: '$category'", context = TAG) } - .onFailure { e -> - runningBackups -= category - cacheStore.updateBackupStatus(category) { - if (it.required == backupRequired) { - failedBackupRequired[category] = backupRequired - } else { - failedBackupRequired -= category - } - it.copy(running = false) - } - Logger.error("Backup failed for: '$category'", e, context = TAG) + .onFailure { markBackupFailed(category, backupRequired, it) } + } + + private suspend fun markBackupFailed(category: BackupCategory, backupRequired: Long, e: Throwable) { + runningBackups -= category + cacheStore.updateBackupStatus(category) { + if (it.required == backupRequired) { + failedBackupRequired[category] = backupRequired + } else { + failedBackupRequired -= category } + it.copy(running = false) + } + Logger.error("Backup failed for: '$category'", e, context = TAG) } private suspend fun getBackupDataBytes(category: BackupCategory): ByteArray = when (category) { diff --git a/app/src/test/java/to/bitkit/repositories/BackupRepoTest.kt b/app/src/test/java/to/bitkit/repositories/BackupRepoTest.kt index 84bafb8348..6a9078c298 100644 --- a/app/src/test/java/to/bitkit/repositories/BackupRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/BackupRepoTest.kt @@ -40,6 +40,7 @@ import to.bitkit.services.PaykitSdkService import to.bitkit.test.BaseUnitTest import to.bitkit.utils.AppError import javax.inject.Provider +import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlin.time.Clock import kotlin.time.ExperimentalTime @@ -111,6 +112,35 @@ class BackupRepoTest : BaseUnitTest() { verify(settingsStore, never()).update(any()) } + @Test + fun `automatic wallet backup marks failure when Paykit snapshot fails`() = test { + whenever { privatePaykitRepo.backupSnapshot() } + .thenReturn(Result.failure(BackupRepoTestError("paykit session missing capabilities"))) + val backupStatuses = MutableStateFlow( + mapOf( + BackupCategory.WALLET to BackupItemStatus( + synced = 1_000, + required = 2_000, + ), + ) + ) + val allowWalletClear = CompletableDeferred().apply { complete(Unit) } + stubBackupStatuses(backupStatuses, allowWalletClear) {} + stubBackupObservers() + + try { + sut.startObservingBackups() + runCurrent() + advanceTimeBy(5_000) + runCurrent() + + verify(vssBackupClient, never()).putObject(eq(BackupCategory.WALLET.name), any()) + assertFalse(backupStatuses.value.getValue(BackupCategory.WALLET).running) + } finally { + sut.stopObservingBackups() + } + } + @Test fun `start observing backs up stale required status after clearing running flag`() = test { val backupStatuses = MutableStateFlow( diff --git a/changelog.d/next/1092.fixed.md b/changelog.d/next/1092.fixed.md new file mode 100644 index 0000000000..b4fc5af7a6 --- /dev/null +++ b/changelog.d/next/1092.fixed.md @@ -0,0 +1 @@ +Fixed a crash during automatic wallet backup when Paykit state could not be read.