Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Back up restore progress #3659

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e2cf910
fix: Ensure proper resource management by using .use for file operations
kl3jvi Oct 17, 2024
98d64a5
Merge branch 'develop' into develop
kl3jvi Oct 21, 2024
f3e9113
Merge branch 'wireapp:develop' into develop
kl3jvi Oct 22, 2024
d04ae83
fix: Ensure proper resource management by using .use for file operations
kl3jvi Oct 23, 2024
b5f27c9
Merge branch 'develop' into develop
MohamadJaara Oct 23, 2024
f69f640
Merge branch 'develop' into develop
kl3jvi Oct 24, 2024
82b4799
Merge branch 'develop' into develop
kl3jvi Oct 25, 2024
cb2218f
fix: Ensure proper resource management by using .use for file operations
kl3jvi Oct 25, 2024
2f0bec1
Merge remote-tracking branch 'origin/develop' into develop
kl3jvi Oct 25, 2024
1e10eb8
refactor: AudioMediaRecorder to improve readability and resource mana…
kl3jvi Nov 7, 2024
36a60c4
refactor: AudioMediaRecorder to improve readability and resource mana…
kl3jvi Nov 7, 2024
293fb28
Merge branch 'wireapp:develop' into develop
kl3jvi Nov 11, 2024
9b586cd
Merge branch 'wireapp:develop' into develop
kl3jvi Nov 13, 2024
9b1e2e0
Merge remote-tracking branch 'origin/develop' into develop
kl3jvi Nov 13, 2024
329e536
Merge branch 'develop' into develop
kl3jvi Nov 14, 2024
9ff4216
Merge branch 'develop' into develop
MohamadJaara Nov 14, 2024
c5d3efd
Merge branch 'wireapp:develop' into develop
kl3jvi Nov 15, 2024
bdb4a98
Merge branch 'develop' into develop
MohamadJaara Nov 15, 2024
e6bf6c9
Merge branch 'develop' into develop
kl3jvi Nov 18, 2024
0bbf8d2
Merge remote-tracking branch 'origin/develop' into develop
kl3jvi Nov 18, 2024
0411eae
Merge remote-tracking branch 'origin/develop' into develop
kl3jvi Nov 18, 2024
55bc4f3
Merge remote-tracking branch 'origin/develop' into develop
kl3jvi Nov 21, 2024
eb153c5
fix: backup and restore progress updates in a more meaningful way.
kl3jvi Nov 23, 2024
841b472
fix: backup and restore progress updates in a more meaningful way.
kl3jvi Nov 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,11 @@ class BackupAndRestoreViewModel
}

fun createBackup() = viewModelScope.launch {
// TODO: Find a way to update the creation progress more faithfully. For now we will just show this small delays to mimic the
// progress also for small backups
updateCreationProgress(PROGRESS_25)
delay(SMALL_DELAY)
updateCreationProgress(PROGRESS_50)
delay(SMALL_DELAY)

when (val result = createBackupFile(createBackupPasswordState.text.toString())) {
when (val result = createBackupFile(createBackupPasswordState.text.toString(), ::updateCreationProgress)) {
is CreateBackupResult.Success -> {
state = state.copy(backupCreationProgress = BackupCreationProgress.Finished(result.backupFileName))
latestCreatedBackup = BackupAndRestoreState.CreatedBackup(
result.backupFilePath,
result.backupFileName,
result.backupFileSize,
createBackupPasswordState.text.isNotEmpty()
result.backupFilePath, result.backupFileName, result.backupFileSize, createBackupPasswordState.text.isNotEmpty()
)
createBackupPasswordState.clearText()
}
Expand Down Expand Up @@ -223,8 +213,7 @@ class BackupAndRestoreViewModel
is RestoreBackupResult.Failure -> {
appLogger.e("Error when restoring the backup db file caused by: ${result.failure.cause}")
state = state.copy(
restoreFileValidation = RestoreFileValidation.IncompatibleBackup,
backupRestoreProgress = BackupRestoreProgress.Failed
restoreFileValidation = RestoreFileValidation.IncompatibleBackup, backupRestoreProgress = BackupRestoreProgress.Failed
)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
}
Expand All @@ -243,8 +232,7 @@ class BackupAndRestoreViewModel
when (val result = importBackup(latestImportedBackupTempPath, restoreBackupPasswordState.text.toString())) {
RestoreBackupResult.Success -> {
state = state.copy(
backupRestoreProgress = BackupRestoreProgress.Finished,
restorePasswordValidation = PasswordValidation.Valid
backupRestoreProgress = BackupRestoreProgress.Finished, restorePasswordValidation = PasswordValidation.Valid
)
restoreBackupPasswordState.clearText()
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
Expand Down Expand Up @@ -317,8 +305,10 @@ class BackupAndRestoreViewModel
}
}

private suspend fun updateCreationProgress(progress: Float) = withContext(dispatcher.main()) {
state = state.copy(backupCreationProgress = BackupCreationProgress.InProgress(progress))
private fun updateCreationProgress(progress: Float) {
viewModelScope.launch(dispatcher.main()) {
state = state.copy(backupCreationProgress = BackupCreationProgress.InProgress(progress))
}
}

internal companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,6 @@ class FakeKaliumFileSystem(
override fun selfUserAvatarPath(): Path = providePersistentAssetPath("self_user_avatar.jpg")

override suspend fun listDirectories(dir: Path): List<Path> = fakeFileSystem.list(dir)
override fun fileSize(path: Path): Long = fakeFileSystem.metadata(path).size ?: 0

}
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ class BackupAndRestoreViewModelTest {
fun givenAnEmptyPassword_whenCreatingABackup_thenItCreatesItSuccessfully() = runTest {
// Given
val emptyPassword = ""
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withValidPassword()
.withSuccessfulCreation(emptyPassword)
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withValidPassword().withSuccessfulCreation(emptyPassword).arrange()
backupAndRestoreViewModel.createBackupPasswordState.setTextAndPlaceCursorAtEnd(emptyPassword)

// When
Expand All @@ -107,17 +104,14 @@ class BackupAndRestoreViewModelTest {
// Then
assert(backupAndRestoreViewModel.state.backupCreationProgress is BackupCreationProgress.Finished)
assertFalse(backupAndRestoreViewModel.latestCreatedBackup?.isEncrypted!!)
coVerify(exactly = 1) { arrangement.createBackupFile(password = emptyPassword) }
coVerify(exactly = 1) { arrangement.createBackupFile(password = emptyPassword, any()) }
}

@Test
fun givenANonEmptyPassword_whenCreatingABackup_thenItCreatesItSuccessfully() = runTest(dispatcher.default()) {
// Given
val password = "mayTh3ForceBeWIthYou"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withValidPassword()
.withSuccessfulCreation(password)
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withValidPassword().withSuccessfulCreation(password).arrange()
backupAndRestoreViewModel.createBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -127,16 +121,14 @@ class BackupAndRestoreViewModelTest {
// Then
assertInstanceOf(BackupCreationProgress.Finished::class.java, backupAndRestoreViewModel.state.backupCreationProgress)
assertTrue(backupAndRestoreViewModel.latestCreatedBackup?.isEncrypted!!)
coVerify(exactly = 1) { arrangement.createBackupFile(password = password) }
coVerify(exactly = 1) { arrangement.createBackupFile(password = password, any()) }
}

@Test
fun givenAnEmptyPassword_whenValidating_thenItUpdatePasswordStateToValid() = runTest(dispatcher.default()) {
// Given
val password = ""
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withInvalidPassword()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withInvalidPassword().arrange()

// When
backupAndRestoreViewModel.validateBackupCreationPassword(password)
Expand All @@ -151,9 +143,7 @@ class BackupAndRestoreViewModelTest {
fun givenANonEmptyPassword_whenItIsInvalid_thenItUpdatePasswordValidationState() = runTest(dispatcher.default()) {
// Given
val password = "mayTh3ForceBeWIthYou"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withInvalidPassword()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withInvalidPassword().arrange()

// When
backupAndRestoreViewModel.validateBackupCreationPassword(password)
Expand All @@ -167,9 +157,7 @@ class BackupAndRestoreViewModelTest {
fun givenANonEmptyPassword_whenItIsValid_thenItUpdatePasswordValidationState() = runTest(dispatcher.default()) {
// Given
val password = "mayTh3ForceBeWIthYou_"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withValidPassword()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withValidPassword().arrange()

// When
backupAndRestoreViewModel.validateBackupCreationPassword(password)
Expand All @@ -183,10 +171,7 @@ class BackupAndRestoreViewModelTest {
fun givenANonEmptyPassword_whenCreatingABackupWithAGivenError_thenItReturnsAFailure() = runTest {
// Given
val password = "mayTh3ForceBeWIthYou"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withValidPassword()
.withFailedCreation(password)
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withValidPassword().withFailedCreation(password).arrange()
backupAndRestoreViewModel.createBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -196,16 +181,14 @@ class BackupAndRestoreViewModelTest {
// Then
assertEquals(backupAndRestoreViewModel.state.backupCreationProgress, BackupCreationProgress.Failed)
assert(backupAndRestoreViewModel.latestCreatedBackup == null)
coVerify(exactly = 1) { arrangement.createBackupFile(password = password) }
coVerify(exactly = 1) { arrangement.createBackupFile(password = password, any()) }
}

@Test
fun givenACreatedBackup_whenSharingIt_thenTheStateIsResetButKeepsTheLastBackupDate() = runTest {
// Given
val storedBackup = BackupAndRestoreState.CreatedBackup("backupFilePath".toPath(), "backupName.zip", 100L, true)
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withPreviouslyCreatedBackup(storedBackup)
.withUpdateLastBackupData()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withPreviouslyCreatedBackup(storedBackup).withUpdateLastBackupData()
.arrange()

// When
Expand Down Expand Up @@ -290,9 +273,7 @@ class BackupAndRestoreViewModelTest {
fun givenAStoredEncryptedBackup_whenChoosingIt_thenTheRequirePasswordDialogIsShown() = runTest(dispatcher.default()) {
// Given
val isBackupEncrypted = true
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withSuccessfulDBImport(isBackupEncrypted)
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withSuccessfulDBImport(isBackupEncrypted).arrange()
val backupUri = "some-backup".toUri()

// When
Expand All @@ -310,9 +291,7 @@ class BackupAndRestoreViewModelTest {
@Test
fun givenAStoredBackup_whenThereIsAnErrorVerifyingItsEncryption_thenTheRightErrorDialogIsShown() = runTest(dispatcher.default()) {
// Given
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withFailedBackupVerification()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withFailedBackupVerification().arrange()
val backupUri = "some-backup".toUri()

// When
Expand Down Expand Up @@ -352,9 +331,7 @@ class BackupAndRestoreViewModelTest {
fun givenARestoreDialogShown_whenDismissingIt_thenTheTempImportedBackupPathIsDeleted() = runTest(dispatcher.default()) {
// Given
val mockUri = "some-backup"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withSuccessfulDBImport(false)
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withSuccessfulDBImport(false).arrange()
val backupUri = mockUri.toUri()

// When
Expand All @@ -377,10 +354,7 @@ class BackupAndRestoreViewModelTest {
fun givenAPasswordEncryptedBackup_whenRestoringIt_thenTheCorrectSuccessDialogIsShown() = runTest(dispatcher.default()) {
// Given
val password = "some-password"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withSuccessfulBackupRestore()
.withRequestedPasswordDialog()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withSuccessfulBackupRestore().withRequestedPasswordDialog().arrange()
backupAndRestoreViewModel.restoreBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -400,10 +374,8 @@ class BackupAndRestoreViewModelTest {
fun givenAPasswordEncryptedBackup_whenRestoringWithWrongPassword_thenTheCorrectErrorDialogIsShown() = runTest(dispatcher.default()) {
// Given
val password = "some-password"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withFailedDBImport(Failure(InvalidPassword))
.withRequestedPasswordDialog()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withFailedDBImport(Failure(InvalidPassword))
.withRequestedPasswordDialog().arrange()
backupAndRestoreViewModel.restoreBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -424,10 +396,8 @@ class BackupAndRestoreViewModelTest {
runTest(dispatcher.default()) {
// Given
val password = "some-password"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withFailedDBImport(Failure(InvalidUserId))
.withRequestedPasswordDialog()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withFailedDBImport(Failure(InvalidUserId))
.withRequestedPasswordDialog().arrange()
backupAndRestoreViewModel.restoreBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -447,10 +417,8 @@ class BackupAndRestoreViewModelTest {
fun givenAPasswordEncryptedBackup_whenRestoringAnIncompatibleBackup_thenTheCorrectErrorDialogIsShown() = runTest(dispatcher.default()) {
// Given
val password = "some-password"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withFailedDBImport(Failure(IncompatibleBackup("old format backup")))
.withRequestedPasswordDialog()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withFailedDBImport(Failure(IncompatibleBackup("old format backup")))
.withRequestedPasswordDialog().arrange()
backupAndRestoreViewModel.restoreBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -470,11 +438,8 @@ class BackupAndRestoreViewModelTest {
fun givenAPasswordEncryptedBackup_whenRestoringABackupWithAnIOError_thenTheCorrectErrorDialogIsShown() = runTest(dispatcher.default()) {
// Given
val password = "some-password"
val (arrangement, backupAndRestoreViewModel) = Arrangement()
.withFailedDBImport(Failure(BackupIOFailure("IO error")))
.withRequestedPasswordDialog()
.withValidPassword()
.arrange()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withFailedDBImport(Failure(BackupIOFailure("IO error")))
.withRequestedPasswordDialog().withValidPassword().arrange()
backupAndRestoreViewModel.restoreBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
Expand All @@ -490,6 +455,42 @@ class BackupAndRestoreViewModelTest {
}
}

@Test
fun givenBackupCreation_whenProgressUpdates_thenStateIsUpdatedCorrectly() = runTest {
// Given
val password = "blackAndRedFl4g"
val (arrangement, backupAndRestoreViewModel) = Arrangement().withValidPassword().withSuccessfulCreation(password).arrange()
backupAndRestoreViewModel.createBackupPasswordState.setTextAndPlaceCursorAtEnd(password)

// When
backupAndRestoreViewModel.createBackup()
advanceUntilIdle()

// Then
assert(backupAndRestoreViewModel.state.backupCreationProgress is BackupCreationProgress.Finished)
assertTrue(backupAndRestoreViewModel.latestCreatedBackup?.isEncrypted!!)
coVerify(exactly = 1) { arrangement.createBackupFile(password = password, any()) }
}

@Test
fun givenBackupRestore_whenProgressUpdates_thenStateIsUpdatedCorrectly() = runTest {
// Given
val backupUri = "some-backup".toUri()
val (arrangement, backupAndRestoreViewModel) = Arrangement().withSuccessfulDBImport(false).arrange()

// When
backupAndRestoreViewModel.chooseBackupFileToRestore(backupUri)
advanceUntilIdle()

// Then
assert(backupAndRestoreViewModel.state.backupRestoreProgress == BackupRestoreProgress.Finished)
assert(backupAndRestoreViewModel.state.restoreFileValidation == RestoreFileValidation.ValidNonEncryptedBackup)
assert(arrangement.fakeKaliumFileSystem.exists(backupAndRestoreViewModel.latestImportedBackupTempPath))
coVerify(exactly = 1) {
arrangement.fileManager.copyToPath(backupUri, backupAndRestoreViewModel.latestImportedBackupTempPath, any())
}
}

private inner class Arrangement {

init {
Expand All @@ -500,7 +501,7 @@ class BackupAndRestoreViewModelTest {
withGetLastBackupDateSeconds()
every { Uri.parse("some-backup") } returns mockUri
coEvery { importBackup(any(), any()) } returns RestoreBackupResult.Success
coEvery { createBackupFile(any()) } returns CreateBackupResult.Success("".toPath(), 0L, "")
coEvery { createBackupFile(any(), any()) } returns CreateBackupResult.Success("".toPath(), 0L, "")
coEvery { verifyBackup(any()) } returns VerifyBackupResult.Success.Encrypted
}

Expand Down Expand Up @@ -539,11 +540,17 @@ class BackupAndRestoreViewModelTest {
val backupFilePath = "some-file-path".toPath()
val backupSize = 1000L
val backupName = "some-backup.zip"
coEvery { createBackupFile(eq(password)) } returns CreateBackupResult.Success(backupFilePath, backupSize, backupName)
coEvery {
createBackupFile(eq(password), any())
} returns CreateBackupResult.Success(backupFilePath, backupSize, backupName)
}

fun withFailedCreation(password: String) = apply {
coEvery { createBackupFile(eq(password)) } returns CreateBackupResult.Failure(CoreFailure.Unknown(IOException("Some db error")))
coEvery {
createBackupFile(
eq(password), any()
)
} returns CreateBackupResult.Failure(CoreFailure.Unknown(IOException("Some db error")))
}

fun withPreviouslyCreatedBackup(backup: BackupAndRestoreState.CreatedBackup) = apply {
Expand Down
Loading