From c52910e74aca0baecfe50d884d3807e191e32165 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 21 Jan 2026 10:50:30 -0600 Subject: [PATCH] PM-31043: Add unarchive button to overflow menus (#6387) --- .../platform/feature/search/SearchContent.kt | 1 + .../feature/search/SearchViewModel.kt | 56 ++++++++++++++++ .../itemlisting/VaultItemListingContent.kt | 1 + .../itemlisting/VaultItemListingViewModel.kt | 65 +++++++++++++++++++ .../model/ListingItemOverflowAction.kt | 9 +++ .../feature/util/CipherListViewExtensions.kt | 6 +- .../ui/vault/feature/vault/VaultViewModel.kt | 56 ++++++++++++++++ .../feature/search/SearchViewModelTest.kt | 59 +++++++++++++++++ .../VaultItemListingViewModelTest.kt | 59 +++++++++++++++++ .../util/CipherListViewExtensionsTest.kt | 27 ++++++++ .../vault/feature/vault/VaultViewModelTest.kt | 59 +++++++++++++++++ 11 files changed, 397 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt index 3d24618d84..6b39a94c03 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt @@ -75,6 +75,7 @@ fun SearchContent( is ListingItemOverflowAction.VaultAction.LaunchClick, is ListingItemOverflowAction.VaultAction.ViewClick, is ListingItemOverflowAction.VaultAction.ArchiveClick, + is ListingItemOverflowAction.VaultAction.UnarchiveClick, null, -> Unit } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt index c830bcc04c..a814488333 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt @@ -47,6 +47,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult +import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.feature.search.model.AutofillSelectionOption @@ -379,6 +380,10 @@ class SearchViewModel @Inject constructor( is ListingItemOverflowAction.VaultAction.ArchiveClick -> { handleArchiveClick(overflowAction) } + + is ListingItemOverflowAction.VaultAction.UnarchiveClick -> { + handleUnarchiveClick(overflowAction) + } } } @@ -456,6 +461,28 @@ class SearchViewModel @Inject constructor( } } + private fun handleUnarchiveClick(action: ListingItemOverflowAction.VaultAction.UnarchiveClick) { + mutableStateFlow.update { + it.copy( + dialogState = SearchState.DialogState.Loading( + message = BitwardenString.unarchiving.asText(), + ), + ) + } + viewModelScope.launch { + decryptCipherViewOrNull(cipherId = action.cipherId)?.let { + sendAction( + SearchAction.Internal.UnarchiveCipherReceive( + result = vaultRepo.unarchiveCipher( + cipherId = action.cipherId, + cipherView = it, + ), + ), + ) + } + } + } + private fun handleRemovePasswordClick( action: ListingItemOverflowAction.SendAction.RemovePasswordClick, ) { @@ -616,6 +643,7 @@ class SearchViewModel @Inject constructor( } is SearchAction.Internal.ArchiveCipherReceive -> handleArchiveCipherReceive(action) + is SearchAction.Internal.UnarchiveCipherReceive -> handleUnarchiveCipherReceive(action) } } @@ -660,6 +688,27 @@ class SearchViewModel @Inject constructor( } } + private fun handleUnarchiveCipherReceive(action: SearchAction.Internal.UnarchiveCipherReceive) { + when (val result = action.result) { + is UnarchiveCipherResult.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = SearchState.DialogState.Error( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.unable_to_unarchive_selected_item.asText(), + throwable = result.error, + ), + ) + } + } + + UnarchiveCipherResult.Success -> { + mutableStateFlow.update { it.copy(dialogState = null) } + sendEvent(SearchEvent.ShowSnackbar(BitwardenString.item_unarchived.asText())) + } + } + } + private fun handleIconLoadingSettingReceive( action: SearchAction.Internal.IconLoadingSettingReceive, ) { @@ -1426,6 +1475,13 @@ sealed class SearchAction { val result: ArchiveCipherResult, ) : Internal() + /** + * Indicates that the unarchive cipher result has been received. + */ + data class UnarchiveCipherReceive( + val result: UnarchiveCipherResult, + ) : Internal() + /** * Indicates a result for removing the password protection from a send has been received. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index 8280f705ab..1fb903c7b3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -78,6 +78,7 @@ fun VaultItemListingContent( is ListingItemOverflowAction.VaultAction.ViewClick, is ListingItemOverflowAction.VaultAction.CopyTotpClick, is ListingItemOverflowAction.VaultAction.ArchiveClick, + is ListingItemOverflowAction.VaultAction.UnarchiveClick, null, -> Unit } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 3ec9b78a41..edc55d8dd9 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -81,6 +81,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult +import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult @@ -1391,6 +1392,30 @@ class VaultItemListingViewModel @Inject constructor( } } + private fun handleUnarchiveClick( + action: ListingItemOverflowAction.VaultAction.UnarchiveClick, + ) { + mutableStateFlow.update { + it.copy( + dialogState = VaultItemListingState.DialogState.Loading( + message = BitwardenString.unarchiving.asText(), + ), + ) + } + viewModelScope.launch { + getCipherViewOrNull(cipherId = action.cipherId)?.let { + sendAction( + VaultItemListingsAction.Internal.UnarchiveCipherReceive( + result = vaultRepository.unarchiveCipher( + cipherId = action.cipherId, + cipherView = it, + ), + ), + ) + } + } + } + private fun handleDismissDialogClick() { clearDialogState() } @@ -1566,6 +1591,10 @@ class VaultItemListingViewModel @Inject constructor( is ListingItemOverflowAction.VaultAction.ArchiveClick -> { handleArchiveClick(overflowAction) } + + is ListingItemOverflowAction.VaultAction.UnarchiveClick -> { + handleUnarchiveClick(overflowAction) + } } } @@ -1660,6 +1689,10 @@ class VaultItemListingViewModel @Inject constructor( is VaultItemListingsAction.Internal.ArchiveCipherReceive -> { handleArchiveCipherReceive(action) } + + is VaultItemListingsAction.Internal.UnarchiveCipherReceive -> { + handleUnarchiveCipherReceive(action) + } } } @@ -1704,6 +1737,31 @@ class VaultItemListingViewModel @Inject constructor( } } + private fun handleUnarchiveCipherReceive( + action: VaultItemListingsAction.Internal.UnarchiveCipherReceive, + ) { + when (val result = action.result) { + is UnarchiveCipherResult.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = VaultItemListingState.DialogState.Error( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.unable_to_unarchive_selected_item.asText(), + throwable = result.error, + ), + ) + } + } + + UnarchiveCipherResult.Success -> { + mutableStateFlow.update { it.copy(dialogState = null) } + sendEvent( + VaultItemListingEvent.ShowSnackbar(BitwardenString.item_unarchived.asText()), + ) + } + } + } + private fun handleDecryptCipherErrorReceive( action: VaultItemListingsAction.Internal.DecryptCipherErrorReceive, ) { @@ -3698,6 +3756,13 @@ sealed class VaultItemListingsAction { val result: ArchiveCipherResult, ) : Internal() + /** + * Indicates that the unarchive cipher result has been received. + */ + data class UnarchiveCipherReceive( + val result: UnarchiveCipherResult, + ) : Internal() + /** * Indicates a result for generating a verification code has been received. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt index d86586c613..b3fbecac0f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt @@ -192,5 +192,14 @@ sealed class ListingItemOverflowAction : Parcelable { override val title: Text get() = BitwardenString.archive_verb.asText() override val requiresPasswordReprompt: Boolean get() = false } + + /** + * Click on the unarchive overflow option. + */ + @Parcelize + data class UnarchiveClick(val cipherId: String) : VaultAction() { + override val title: Text get() = BitwardenString.unarchive.asText() + override val requiresPasswordReprompt: Boolean get() = false + } } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensions.kt index ea7e3cdc81..f87ad90164 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensions.kt @@ -15,7 +15,7 @@ import kotlinx.collections.immutable.toImmutableList /** * Creates the list of overflow actions to be displayed for a [CipherView]. */ -@Suppress("LongMethod") +@Suppress("CyclomaticComplexMethod", "LongMethod") fun CipherListView.toOverflowActions( hasMasterPassword: Boolean, isPremiumUser: Boolean, @@ -91,6 +91,10 @@ fun CipherListView.toOverflowActions( .takeIf { this.archivedDate == null && deletedDate == null && isArchiveEnabled }, + ListingItemOverflowAction.VaultAction.UnarchiveClick(cipherId = cipherId) + .takeIf { + this.archivedDate != null && deletedDate == null && isArchiveEnabled + }, ) } .orEmpty() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index fc17d7fd41..c2a745eb81 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -50,6 +50,7 @@ import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult +import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType @@ -645,6 +646,10 @@ class VaultViewModel @Inject constructor( is ListingItemOverflowAction.VaultAction.ArchiveClick -> { handleArchiveClick(overflowAction) } + + is ListingItemOverflowAction.VaultAction.UnarchiveClick -> { + handleUnarchiveClick(overflowAction) + } } } @@ -804,6 +809,28 @@ class VaultViewModel @Inject constructor( } } + private fun handleUnarchiveClick(action: ListingItemOverflowAction.VaultAction.UnarchiveClick) { + mutableStateFlow.update { + it.copy( + dialog = VaultState.DialogState.Loading( + message = BitwardenString.unarchiving.asText(), + ), + ) + } + viewModelScope.launch { + getCipherForCopyOrNull(cipherId = action.cipherId)?.let { + sendAction( + VaultAction.Internal.UnarchiveCipherReceive( + result = vaultRepository.unarchiveCipher( + cipherId = action.cipherId, + cipherView = it, + ), + ), + ) + } + } + } + private fun showCipherDecryptionErrorItemClick(itemId: String) { mutableStateFlow.update { it.copy( @@ -872,6 +899,7 @@ class VaultViewModel @Inject constructor( } is VaultAction.Internal.ArchiveCipherReceive -> handleArchiveCipherReceive(action) + is VaultAction.Internal.UnarchiveCipherReceive -> handleUnarchiveCipherReceive(action) } } @@ -951,6 +979,27 @@ class VaultViewModel @Inject constructor( } } + private fun handleUnarchiveCipherReceive(action: VaultAction.Internal.UnarchiveCipherReceive) { + when (val result = action.result) { + is UnarchiveCipherResult.Error -> { + mutableStateFlow.update { + it.copy( + dialog = VaultState.DialogState.Error( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.unable_to_unarchive_selected_item.asText(), + error = result.error, + ), + ) + } + } + + UnarchiveCipherResult.Success -> { + mutableStateFlow.update { it.copy(dialog = null) } + sendEvent(VaultEvent.ShowSnackbar(BitwardenString.item_unarchived.asText())) + } + } + } + private fun handleDecryptionErrorReceive(action: VaultAction.Internal.DecryptionErrorReceive) { mutableStateFlow.update { it.copy( @@ -2198,6 +2247,13 @@ sealed class VaultAction { data class ArchiveCipherReceive( val result: ArchiveCipherResult, ) : Internal() + + /** + * Indicates that the unarchive cipher result has been received. + */ + data class UnarchiveCipherReceive( + val result: UnarchiveCipherResult, + ) : Internal() } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index dad27d927b..cf04794468 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -60,6 +60,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult +import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType @@ -397,6 +398,64 @@ class SearchViewModelTest : BaseViewModelTest() { ) } + @Test + fun `UnarchiveClick with UnarchiveCipherResult Success should emit a ShowSnackbar event`() = + runTest { + val cipherView = createMockCipherView(number = 1, clock = clock) + + val viewModel = createViewModel(initialState = null) + + coEvery { + vaultRepository.unarchiveCipher(cipherId = "mockId-1", cipherView = cipherView) + } returns UnarchiveCipherResult.Success + + viewModel.trySendAction( + SearchAction.OverflowOptionClick( + overflowAction = ListingItemOverflowAction.VaultAction.UnarchiveClick( + cipherId = "mockId-1", + ), + ), + ) + + viewModel.eventFlow.test { + assertEquals( + SearchEvent.ShowSnackbar(BitwardenString.item_unarchived.asText()), + awaitItem(), + ) + } + } + + @Test + fun `UnarchiveClick with UnarchiveCipherResult Failure should show generic error`() = runTest { + val cipherView = createMockCipherView(number = 1, clock = clock) + + val viewModel = createViewModel(initialState = null) + + val error = Throwable("Oh dang.") + coEvery { + vaultRepository.unarchiveCipher(cipherId = "mockId-1", cipherView = cipherView) + } returns UnarchiveCipherResult.Error(error = error) + + viewModel.trySendAction( + SearchAction.OverflowOptionClick( + overflowAction = ListingItemOverflowAction.VaultAction.UnarchiveClick( + cipherId = "mockId-1", + ), + ), + ) + + assertEquals( + DEFAULT_STATE.copy( + dialogState = SearchState.DialogState.Error( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.unable_to_unarchive_selected_item.asText(), + throwable = error, + ), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `AutofillItemClick should call emitAccessibilitySelection`() = runTest { val cipherView = setupForAutofill( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 9ce34ac9f3..1efebe5239 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -101,6 +101,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult +import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult @@ -1152,6 +1153,64 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } + @Test + fun `UnarchiveClick with UnarchiveCipherResult Success should emit a ShowSnackbar event`() = + runTest { + val cipherView = createMockCipherView(number = 1, clock = clock) + + val viewModel = createVaultItemListingViewModel() + + coEvery { + vaultRepository.unarchiveCipher(cipherId = "mockId-1", cipherView = cipherView) + } returns UnarchiveCipherResult.Success + + viewModel.trySendAction( + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.VaultAction.UnarchiveClick( + cipherId = "mockId-1", + ), + ), + ) + + viewModel.eventFlow.test { + assertEquals( + VaultItemListingEvent.ShowSnackbar(BitwardenString.item_unarchived.asText()), + awaitItem(), + ) + } + } + + @Test + fun `UnarchiveClick with UnarchiveCipherResult Failure should show generic error`() = runTest { + val cipherView = createMockCipherView(number = 1, clock = clock) + + val viewModel = createVaultItemListingViewModel() + + val error = Throwable("Oh dang.") + coEvery { + vaultRepository.unarchiveCipher(cipherId = "mockId-1", cipherView = cipherView) + } returns UnarchiveCipherResult.Error(error = error) + + viewModel.trySendAction( + VaultItemListingsAction.OverflowOptionClick( + action = ListingItemOverflowAction.VaultAction.UnarchiveClick( + cipherId = "mockId-1", + ), + ), + ) + + assertEquals( + createVaultItemListingState( + dialogState = VaultItemListingState.DialogState.Error( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.unable_to_unarchive_selected_item.asText(), + throwable = error, + ), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `MasterPasswordRepromptSubmit for a request Error should show a generic error dialog`() = runTest { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensionsTest.kt index 6b98d878db..25a0570e8e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/util/CipherListViewExtensionsTest.kt @@ -425,6 +425,33 @@ class CipherListViewExtensionsTest { ) } + @Suppress("MaxLineLength") + @Test + fun `toOverflowActions should return Unarchive action when cipher is archived and not deleted`() { + val loginListView = createMockLoginListView( + number = 1, + username = "", + uris = emptyList(), + totp = null, + ) + val cipher = createMockCipherListView( + number = 1, + id = id, + isArchived = true, + isDeleted = false, + edit = true, + type = CipherListViewType.Login(loginListView), + ) + + val result = cipher.toOverflowActions( + hasMasterPassword = true, + isPremiumUser = false, + isArchiveEnabled = true, + ) + + assertTrue(result.contains(ListingItemOverflowAction.VaultAction.UnarchiveClick(id))) + } + @Test fun `toOverflowActions should not return Archive action when Archive is disabled`() { val loginListView = createMockLoginListView( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 475b86523f..d69c6e2159 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -60,6 +60,7 @@ import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ArchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult +import com.x8bit.bitwarden.data.vault.repository.model.UnarchiveCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType @@ -681,6 +682,64 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @Test + fun `UnarchiveClick with UnarchiveCipherResult Success should emit a ShowSnackbar event`() = + runTest { + val cipherView = createMockCipherView(number = 1, clock = clock) + + val viewModel = createViewModel() + + coEvery { + vaultRepository.unarchiveCipher(cipherId = "mockId-1", cipherView = cipherView) + } returns UnarchiveCipherResult.Success + + viewModel.trySendAction( + VaultAction.OverflowOptionClick( + overflowAction = ListingItemOverflowAction.VaultAction.UnarchiveClick( + cipherId = "mockId-1", + ), + ), + ) + + viewModel.eventFlow.test { + assertEquals( + VaultEvent.ShowSnackbar(BitwardenString.item_unarchived.asText()), + awaitItem(), + ) + } + } + + @Test + fun `UnarchiveClick with UnarchiveCipherResult Failure should show generic error`() = runTest { + val cipherView = createMockCipherView(number = 1, clock = clock) + + val viewModel = createViewModel() + + val error = Throwable("Oh dang.") + coEvery { + vaultRepository.unarchiveCipher(cipherId = "mockId-1", cipherView = cipherView) + } returns UnarchiveCipherResult.Error(error = error) + + viewModel.trySendAction( + VaultAction.OverflowOptionClick( + overflowAction = ListingItemOverflowAction.VaultAction.UnarchiveClick( + cipherId = "mockId-1", + ), + ), + ) + + assertEquals( + DEFAULT_STATE.copy( + dialog = VaultState.DialogState.Error( + title = BitwardenString.an_error_has_occurred.asText(), + message = BitwardenString.unable_to_unarchive_selected_item.asText(), + error = error, + ), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `on LockAccountClick should call lockVault for the given account`() { val accountUserId = "userId"