From dfcdc724999d81b369a880e491eaba0ff49be267 Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:20:44 -0400 Subject: [PATCH] PM-19241 folder result errors propagated to UI (#4870) --- .../vault/repository/VaultRepositoryImpl.kt | 29 +++++++++++---- .../repository/model/CreateFolderResult.kt | 2 +- .../repository/model/DeleteFolderResult.kt | 2 +- .../repository/model/UpdateFolderResult.kt | 2 +- .../folders/addedit/FolderAddEditScreen.kt | 1 + .../folders/addedit/FolderAddEditViewModel.kt | 12 ++++-- .../vault/repository/VaultRepositoryTest.kt | 37 +++++++++++-------- .../addedit/FolderAddEditViewModelTest.kt | 14 +++++-- 8 files changed, 65 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 4177625e6d..c9e25b8b15 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -788,7 +788,8 @@ class VaultRepositoryImpl( } override suspend fun createFolder(folderView: FolderView): CreateFolderResult { - val userId = activeUserId ?: return CreateFolderResult.Error + val userId = activeUserId + ?: return CreateFolderResult.Error(error = NoActiveUserException()) return vaultSdkSource .encryptFolder( userId = userId, @@ -804,7 +805,7 @@ class VaultRepositoryImpl( .flatMap { vaultSdkSource.decryptFolder(userId, it.toEncryptedSdkFolder()) } .fold( onSuccess = { CreateFolderResult.Success(folderView = it) }, - onFailure = { CreateFolderResult.Error }, + onFailure = { CreateFolderResult.Error(error = it) }, ) } @@ -812,7 +813,10 @@ class VaultRepositoryImpl( folderId: String, folderView: FolderView, ): UpdateFolderResult { - val userId = activeUserId ?: return UpdateFolderResult.Error(null) + val userId = activeUserId ?: return UpdateFolderResult.Error( + errorMessage = null, + error = NoActiveUserException(), + ) return vaultSdkSource .encryptFolder( userId = userId, @@ -837,21 +841,30 @@ class VaultRepositoryImpl( ) .fold( onSuccess = { UpdateFolderResult.Success(it) }, - onFailure = { UpdateFolderResult.Error(errorMessage = null) }, + onFailure = { + UpdateFolderResult.Error( + errorMessage = null, + error = it, + ) + }, ) } is UpdateFolderResponseJson.Invalid -> { - UpdateFolderResult.Error(response.message) + UpdateFolderResult.Error( + errorMessage = response.message, + error = null, + ) } } }, - onFailure = { UpdateFolderResult.Error(it.message) }, + onFailure = { UpdateFolderResult.Error(it.message, error = it) }, ) } override suspend fun deleteFolder(folderId: String): DeleteFolderResult { - val userId = activeUserId ?: return DeleteFolderResult.Error + val userId = activeUserId + ?: return DeleteFolderResult.Error(error = NoActiveUserException()) return folderService .deleteFolder( folderId = folderId, @@ -862,7 +875,7 @@ class VaultRepositoryImpl( } .fold( onSuccess = { DeleteFolderResult.Success }, - onFailure = { DeleteFolderResult.Error }, + onFailure = { DeleteFolderResult.Error(error = it) }, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt index df69d8ad9b..2fd3f3dabd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/CreateFolderResult.kt @@ -15,5 +15,5 @@ sealed class CreateFolderResult { /** * Generic error while creating a folder. */ - data object Error : CreateFolderResult() + data class Error(val error: Throwable) : CreateFolderResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt index 27f90c632b..d559375ad7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DeleteFolderResult.kt @@ -13,5 +13,5 @@ sealed class DeleteFolderResult { /** * Generic error while deleting a folder. */ - data object Error : DeleteFolderResult() + data class Error(val error: Throwable) : DeleteFolderResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt index b2b3867245..c631c6a80f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/UpdateFolderResult.kt @@ -16,5 +16,5 @@ sealed class UpdateFolderResult { * Generic error while updating a folder. The optional [errorMessage] * may be displayed directly in the UI when present. */ - data class Error(val errorMessage: String?) : UpdateFolderResult() + data class Error(val errorMessage: String?, val error: Throwable?) : UpdateFolderResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt index c04691fa17..e874688f42 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt @@ -178,6 +178,7 @@ private fun FolderAddEditItemDialogs( title = stringResource(id = R.string.an_error_has_occurred), message = dialogState.message(), onDismissRequest = onDismissRequest, + throwable = dialogState.throwable, ) null -> Unit diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt index ed925a1f3c..e4fee85b42 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt @@ -250,12 +250,13 @@ class FolderAddEditViewModel @Inject constructor( it.copy(dialog = null) } - when (action.result) { + when (val result = action.result) { is UpdateFolderResult.Error -> { mutableStateFlow.update { it.copy( dialog = FolderAddEditState.DialogState.Error( message = R.string.generic_error_message.asText(), + throwable = result.error, ), ) } @@ -275,12 +276,13 @@ class FolderAddEditViewModel @Inject constructor( it.copy(dialog = null) } - when (action.result) { + when (val result = action.result) { is CreateFolderResult.Error -> { mutableStateFlow.update { it.copy( dialog = FolderAddEditState.DialogState.Error( message = R.string.generic_error_message.asText(), + throwable = result.error, ), ) } @@ -296,12 +298,13 @@ class FolderAddEditViewModel @Inject constructor( private fun handleDeleteResultReceive( action: FolderAddEditAction.Internal.DeleteFolderResultReceive, ) { - when (action.result) { - DeleteFolderResult.Error -> { + when (val result = action.result) { + is DeleteFolderResult.Error -> { mutableStateFlow.update { it.copy( dialog = FolderAddEditState.DialogState.Error( message = R.string.generic_error_message.asText(), + throwable = result.error, ), ) } @@ -398,6 +401,7 @@ data class FolderAddEditState( @Parcelize data class Error( val message: Text, + val throwable: Throwable? = null, ) : DialogState() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 60ddb0b372..ca7987837a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -2724,7 +2724,7 @@ class VaultRepositoryTest { val result = vaultRepository.deleteFolder("Test") assertEquals( - DeleteFolderResult.Error, + DeleteFolderResult.Error(error = NoActiveUserException()), result, ) } @@ -2734,11 +2734,12 @@ class VaultRepositoryTest { fun `DeleteFolder with folderService Delete failure should return DeleteFolderResult Failure`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE + val error = Throwable("fail") val folderId = "mockId-1" - coEvery { folderService.deleteFolder(folderId) } returns Throwable("fail").asFailure() + coEvery { folderService.deleteFolder(folderId) } returns error.asFailure() val result = vaultRepository.deleteFolder(folderId) - assertEquals(DeleteFolderResult.Error, result) + assertEquals(DeleteFolderResult.Error(error = error), result) } @Suppress("MaxLineLength") @@ -2800,7 +2801,7 @@ class VaultRepositoryTest { val result = vaultRepository.createFolder(mockk()) assertEquals( - CreateFolderResult.Error, + CreateFolderResult.Error(NoActiveUserException()), result, ) } @@ -2811,10 +2812,11 @@ class VaultRepositoryTest { runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val folderId = "mockId-1" - coEvery { folderService.deleteFolder(folderId) } returns Throwable("fail").asFailure() + val error = Throwable("fail") + coEvery { folderService.deleteFolder(folderId) } returns error.asFailure() val result = vaultRepository.deleteFolder(folderId) - assertEquals(DeleteFolderResult.Error, result) + assertEquals(DeleteFolderResult.Error(error = error), result) } @Test @@ -2826,16 +2828,17 @@ class VaultRepositoryTest { name = "TestName", revisionDate = DateTime.now(), ) + val error = IllegalStateException() coEvery { vaultSdkSource.encryptFolder( userId = MOCK_USER_STATE.activeUserId, folder = folderView, ) - } returns IllegalStateException().asFailure() + } returns error.asFailure() val result = vaultRepository.createFolder(folderView) - assertEquals(CreateFolderResult.Error, result) + assertEquals(CreateFolderResult.Error(error = error), result) } @Test @@ -2850,6 +2853,7 @@ class VaultRepositoryTest { name = testFolderName, revisionDate = date, ) + val error = IllegalStateException() coEvery { vaultSdkSource.encryptFolder( @@ -2862,10 +2866,10 @@ class VaultRepositoryTest { folderService.createFolder( body = FolderJsonRequest(testFolderName), ) - } returns IllegalStateException().asFailure() + } returns error.asFailure() val result = vaultRepository.createFolder(folderView) - assertEquals(CreateFolderResult.Error, result) + assertEquals(CreateFolderResult.Error(error = error), result) } @Test @@ -2926,7 +2930,7 @@ class VaultRepositoryTest { val result = vaultRepository.updateFolder("Test", mockk()) assertEquals( - UpdateFolderResult.Error(null), + UpdateFolderResult.Error(errorMessage = null, error = NoActiveUserException()), result, ) } @@ -2941,17 +2945,18 @@ class VaultRepositoryTest { name = "TestName", revisionDate = DateTime.now(), ) + val error = IllegalStateException() coEvery { vaultSdkSource.encryptFolder( userId = MOCK_USER_STATE.activeUserId, folder = folderView, ) - } returns IllegalStateException().asFailure() + } returns error.asFailure() val result = vaultRepository.updateFolder(folderId, folderView) - assertEquals(UpdateFolderResult.Error(errorMessage = null), result) + assertEquals(UpdateFolderResult.Error(errorMessage = null, error = error), result) } @Test @@ -2967,6 +2972,7 @@ class VaultRepositoryTest { name = testFolderName, revisionDate = date, ) + val error = IllegalStateException() coEvery { vaultSdkSource.encryptFolder( @@ -2980,10 +2986,10 @@ class VaultRepositoryTest { folderId = folderId, body = FolderJsonRequest(testFolderName), ) - } returns IllegalStateException().asFailure() + } returns error.asFailure() val result = vaultRepository.updateFolder(folderId, folderView) - assertEquals(UpdateFolderResult.Error(errorMessage = null), result) + assertEquals(UpdateFolderResult.Error(errorMessage = null, error = error), result) } @Suppress("MaxLineLength") @@ -3024,6 +3030,7 @@ class VaultRepositoryTest { assertEquals( UpdateFolderResult.Error( errorMessage = "You do not have permission to edit this.", + error = null, ), result, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt index fbf6353055..9a4fb203f3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt @@ -199,10 +199,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { @Test fun `DeleteClick with DeleteFolderResult Failure should show an error dialog`() = runTest { + val error = Throwable("Oops") val stateWithDialog = FolderAddEditState( folderAddEditType = FolderAddEditType.EditItem((DEFAULT_EDIT_ITEM_ID)), dialog = FolderAddEditState.DialogState.Error( R.string.generic_error_message.asText(), + throwable = error, ), viewState = FolderAddEditState.ViewState.Content( folderName = DEFAULT_FOLDER_NAME, @@ -231,7 +233,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { coEvery { vaultRepository.deleteFolder(folderId = DEFAULT_EDIT_ITEM_ID) - } returns DeleteFolderResult.Error + } returns DeleteFolderResult.Error(error = error) viewModel.trySendAction(FolderAddEditAction.DeleteClick) @@ -404,9 +406,10 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { ), ) + val error = Throwable("Oops") coEvery { vaultRepository.createFolder(any()) - } returns CreateFolderResult.Error + } returns CreateFolderResult.Error(error = error) viewModel.trySendAction(FolderAddEditAction.SaveClick) @@ -414,6 +417,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { state.copy( dialog = FolderAddEditState.DialogState.Error( R.string.generic_error_message.asText(), + throwable = error, ), ), viewModel.stateFlow.value, @@ -485,6 +489,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { state = state, ), ) + val error = Throwable("Oops") mutableFoldersStateFlow.value = DataState.Loaded( @@ -497,14 +502,15 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { coEvery { vaultRepository.updateFolder(any(), any()) - } returns UpdateFolderResult.Error(errorMessage = null) + } returns UpdateFolderResult.Error(errorMessage = null, error = error) viewModel.trySendAction(FolderAddEditAction.SaveClick) assertEquals( state.copy( dialog = FolderAddEditState.DialogState.Error( - R.string.generic_error_message.asText(), + message = R.string.generic_error_message.asText(), + throwable = error, ), ), viewModel.stateFlow.value,