diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt index c41e9393c..03d2855c6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt @@ -59,6 +59,7 @@ fun SearchContent( is ListingItemOverflowAction.VaultAction.CopyNoteClick, is ListingItemOverflowAction.VaultAction.CopyNumberClick, is ListingItemOverflowAction.VaultAction.CopyPasswordClick, + is ListingItemOverflowAction.VaultAction.CopyTotpClick, is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick, is ListingItemOverflowAction.VaultAction.CopyUsernameClick, is ListingItemOverflowAction.VaultAction.EditClick, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt index 6f47c460b..b8ec96418 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl import com.x8bit.bitwarden.data.vault.repository.VaultRepository 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.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModel @@ -190,6 +191,10 @@ class SearchViewModel @Inject constructor( is ListingItemOverflowAction.VaultAction.ViewClick -> { handleViewCipherClick(overflowAction) } + + is ListingItemOverflowAction.VaultAction.CopyTotpClick -> { + handleCopyTotpClick(overflowAction) + } } } @@ -211,6 +216,15 @@ class SearchViewModel @Inject constructor( sendEvent(SearchEvent.NavigateToEditSend(action.sendId)) } + private fun handleCopyTotpClick( + action: ListingItemOverflowAction.VaultAction.CopyTotpClick, + ) { + viewModelScope.launch { + val result = vaultRepo.generateTotp(action.totpCode, clock.instant()) + sendAction(SearchAction.Internal.GenerateTotpResultReceive(result)) + } + } + private fun handleRemovePasswordClick( action: ListingItemOverflowAction.SendAction.RemovePasswordClick, ) { @@ -283,6 +297,10 @@ class SearchViewModel @Inject constructor( handleDeleteSendResultReceive(action) } + is SearchAction.Internal.GenerateTotpResultReceive -> { + handleGenerateTotpResultReceive(action) + } + is SearchAction.Internal.RemovePasswordSendResultReceive -> { handleRemovePasswordSendResultReceive(action) } @@ -320,6 +338,17 @@ class SearchViewModel @Inject constructor( } } + private fun handleGenerateTotpResultReceive( + action: SearchAction.Internal.GenerateTotpResultReceive, + ) { + when (val result = action.result) { + is GenerateTotpResult.Error -> Unit + is GenerateTotpResult.Success -> { + clipboardManager.setText(result.code) + } + } + } + private fun handleRemovePasswordSendResultReceive( action: SearchAction.Internal.RemovePasswordSendResultReceive, ) { @@ -758,6 +787,13 @@ sealed class SearchAction { val result: DeleteSendResult, ) : Internal() + /** + * Indicates a result for generating a verification code has been received. + */ + data class GenerateTotpResultReceive( + val result: GenerateTotpResult, + ) : Internal() + /** * Indicates a result for removing the password protection from a send has been received. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index 539d46e47..590ee2f35 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -66,6 +66,7 @@ fun VaultItemListingContent( is ListingItemOverflowAction.VaultAction.EditClick, is ListingItemOverflowAction.VaultAction.LaunchClick, is ListingItemOverflowAction.VaultAction.ViewClick, + is ListingItemOverflowAction.VaultAction.CopyTotpClick, null, -> Unit } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 2683a47ba..531b29ddb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl import com.x8bit.bitwarden.data.platform.repository.util.map import com.x8bit.bitwarden.data.vault.repository.VaultRepository 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.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModel @@ -256,6 +257,15 @@ class VaultItemListingViewModel @Inject constructor( clipboardManager.setText(action.securityCode) } + private fun handleCopyTotpClick( + action: ListingItemOverflowAction.VaultAction.CopyTotpClick, + ) { + viewModelScope.launch { + val result = vaultRepository.generateTotp(action.totpCode, clock.instant()) + sendAction(VaultItemListingsAction.Internal.GenerateTotpResultReceive(result)) + } + } + private fun handleCopyUsernameClick( action: ListingItemOverflowAction.VaultAction.CopyUsernameClick, ) { @@ -347,6 +357,10 @@ class VaultItemListingViewModel @Inject constructor( handleCopySecurityCodeClick(overflowAction) } + is ListingItemOverflowAction.VaultAction.CopyTotpClick -> { + handleCopyTotpClick(overflowAction) + } + is ListingItemOverflowAction.VaultAction.CopyUsernameClick -> { handleCopyUsernameClick(overflowAction) } @@ -383,6 +397,10 @@ class VaultItemListingViewModel @Inject constructor( is VaultItemListingsAction.Internal.IconLoadingSettingReceive -> { handleIconsSettingReceived(action) } + + is VaultItemListingsAction.Internal.GenerateTotpResultReceive -> { + handleGenerateTotpResultReceive(action) + } } } @@ -445,6 +463,17 @@ class VaultItemListingViewModel @Inject constructor( } } + private fun handleGenerateTotpResultReceive( + action: VaultItemListingsAction.Internal.GenerateTotpResultReceive, + ) { + when (val result = action.result) { + is GenerateTotpResult.Error -> Unit + is GenerateTotpResult.Success -> { + clipboardManager.setText(result.code) + } + } + } + private fun handleVaultDataReceive( action: VaultItemListingsAction.Internal.VaultDataReceive, ) { @@ -1033,6 +1062,13 @@ sealed class VaultItemListingsAction { */ data class DeleteSendResultReceive(val result: DeleteSendResult) : Internal() + /** + * Indicates a result for generating a verification code has been received. + */ + data class GenerateTotpResultReceive( + val result: GenerateTotpResult, + ) : Internal() + /** * Indicates a result for removing the password protection from a send has been received. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt index 385ae0537..856421268 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt @@ -97,6 +97,14 @@ sealed class ListingItemOverflowAction : Parcelable { override val title: Text get() = R.string.copy_password.asText() } + /** + * Click on the copy TOTP code overflow option. + */ + @Parcelize + data class CopyTotpClick(val totpCode: String) : VaultAction() { + override val title: Text get() = R.string.copy_totp.asText() + } + /** * Click on the copy number overflow option. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensions.kt index 4a847036e..8d36bb8ee 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensions.kt @@ -23,6 +23,9 @@ fun CipherView.toOverflowActions(): List this.login?.password?.let { ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = it) }, + this.login?.totp + ?.let { ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode = it) } + .takeIf { this.type == CipherType.LOGIN }, this.card?.number?.let { ListingItemOverflowAction.VaultAction.CopyNumberClick(number = it) }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 2e9ce97ab..35b40b58d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text @@ -37,7 +38,9 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +import java.time.Clock import javax.inject.Inject /** @@ -46,10 +49,11 @@ import javax.inject.Inject @Suppress("TooManyFunctions") @HiltViewModel class VaultViewModel @Inject constructor( - private val clipboardManager: BitwardenClipboardManager, private val authRepository: AuthRepository, - private val vaultRepository: VaultRepository, + private val clipboardManager: BitwardenClipboardManager, + private val clock: Clock, private val settingsRepository: SettingsRepository, + private val vaultRepository: VaultRepository, ) : BaseViewModel( initialState = run { val userState = requireNotNull(authRepository.userStateFlow.value) @@ -293,6 +297,10 @@ class VaultViewModel @Inject constructor( handleCopySecurityCodeClick(overflowAction) } + is ListingItemOverflowAction.VaultAction.CopyTotpClick -> { + handleCopyTotpClick(overflowAction) + } + is ListingItemOverflowAction.VaultAction.CopyUsernameClick -> { handleCopyUsernameClick(overflowAction) } @@ -333,6 +341,15 @@ class VaultViewModel @Inject constructor( clipboardManager.setText(action.securityCode) } + private fun handleCopyTotpClick( + action: ListingItemOverflowAction.VaultAction.CopyTotpClick, + ) { + viewModelScope.launch { + val result = vaultRepository.generateTotp(action.totpCode, clock.instant()) + sendAction(VaultAction.Internal.GenerateTotpResultReceive(result)) + } + } + private fun handleCopyUsernameClick( action: ListingItemOverflowAction.VaultAction.CopyUsernameClick, ) { @@ -353,6 +370,10 @@ class VaultViewModel @Inject constructor( private fun handleInternalAction(action: VaultAction.Internal) { when (action) { + is VaultAction.Internal.GenerateTotpResultReceive -> { + handleGenerateTotpResultReceive(action) + } + is VaultAction.Internal.PullToRefreshEnableReceive -> { handlePullToRefreshEnableReceive(action) } @@ -365,6 +386,17 @@ class VaultViewModel @Inject constructor( } } + private fun handleGenerateTotpResultReceive( + action: VaultAction.Internal.GenerateTotpResultReceive, + ) { + when (val result = action.result) { + is GenerateTotpResult.Error -> Unit + is GenerateTotpResult.Success -> { + clipboardManager.setText(result.code) + } + } + } + private fun handlePullToRefreshEnableReceive( action: VaultAction.Internal.PullToRefreshEnableReceive, ) { @@ -1002,6 +1034,13 @@ sealed class VaultAction { val isIconLoadingDisabled: Boolean, ) : Internal() + /** + * Indicates a result for generating a verification code has been received. + */ + data class GenerateTotpResultReceive( + val result: GenerateTotpResult, + ) : Internal() + /** * Indicates that the pull to refresh feature toggle has changed. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index 579522de3..06c4f2672 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.repository.VaultRepository 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.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest @@ -331,6 +332,51 @@ class SearchViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `OverflowOptionClick Vault CopyTotpClick with GenerateTotpCode success should call setText on the ClipboardManager`() = + runTest { + val totpCode = "totpCode" + val code = "Code" + + coEvery { + vaultRepository.generateTotp(totpCode, clock.instant()) + } returns GenerateTotpResult.Success(code, 30) + + val viewModel = createViewModel() + viewModel.trySendAction( + SearchAction.OverflowOptionClick( + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode), + ), + ) + + verify(exactly = 1) { + clipboardManager.setText(code) + } + } + + @Suppress("MaxLineLength") + @Test + fun `OverflowOptionClick Vault CopyTotpClick with GenerateTotpCode failure should not call setText on the ClipboardManager`() = + runTest { + val totpCode = "totpCode" + + coEvery { + vaultRepository.generateTotp(totpCode, clock.instant()) + } returns GenerateTotpResult.Error + + val viewModel = createViewModel() + viewModel.trySendAction( + SearchAction.OverflowOptionClick( + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode), + ), + ) + + verify(exactly = 0) { + clipboardManager.setText(text = any()) + } + } + @Suppress("MaxLineLength") @Test fun `OverflowOptionClick Vault CopyPasswordClick should call setText on the ClipboardManager`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt index ebcf70111..c176dcd94 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchUtil.kt @@ -45,6 +45,9 @@ fun createMockDisplayItemForCipher( ListingItemOverflowAction.VaultAction.CopyPasswordClick( password = "mockPassword-$number", ), + ListingItemOverflowAction.VaultAction.CopyTotpClick( + totpCode = "mockTotp-$number", + ), ListingItemOverflowAction.VaultAction.LaunchClick( url = "www.mockuri$number.com", ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 1a48dc133..7b8c3378f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.repository.VaultRepository 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.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest @@ -511,6 +512,51 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `OverflowOptionClick Vault CopyTotpClick with GenerateTotpCode success should call setText on the ClipboardManager`() = + runTest { + val totpCode = "totpCode" + val code = "Code" + + coEvery { + vaultRepository.generateTotp(totpCode, clock.instant()) + } returns GenerateTotpResult.Success(code, 30) + + val viewModel = createVaultItemListingViewModel() + viewModel.trySendAction( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode), + ), + ) + + verify(exactly = 1) { + clipboardManager.setText(code) + } + } + + @Suppress("MaxLineLength") + @Test + fun `OverflowOptionClick Vault CopyTotpClick with GenerateTotpCode failure should not call setText on the ClipboardManager`() = + runTest { + val totpCode = "totpCode" + + coEvery { + vaultRepository.generateTotp(totpCode, clock.instant()) + } returns GenerateTotpResult.Error + + val viewModel = createVaultItemListingViewModel() + viewModel.trySendAction( + VaultItemListingsAction.OverflowOptionClick( + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode), + ), + ) + + verify(exactly = 0) { + clipboardManager.setText(text = any()) + } + } + @Suppress("MaxLineLength") @Test fun `OverflowOptionClick Vault CopyUsernameClick should call setText on the ClipboardManager`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt index 99dfec002..6c706d158 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt @@ -46,6 +46,9 @@ fun createMockDisplayItemForCipher( ListingItemOverflowAction.VaultAction.CopyPasswordClick( password = "mockPassword-$number", ), + ListingItemOverflowAction.VaultAction.CopyTotpClick( + totpCode = "mockTotp-$number", + ), ListingItemOverflowAction.VaultAction.LaunchClick( url = "www.mockuri$number.com", ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensionsTest.kt index 7b5949a3a..89e46737f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CipherViewExtensionsTest.kt @@ -20,6 +20,7 @@ class CipherViewExtensionsTest { val id = "mockId-1" val username = "Bitwarden" val password = "password" + val totpCode = "mockTotp-1" val uri = "www.test.com" val cipher = createMockCipherView(number = 1, cipherType = CipherType.LOGIN).copy( id = id, @@ -38,6 +39,7 @@ class CipherViewExtensionsTest { ListingItemOverflowAction.VaultAction.EditClick(cipherId = id), ListingItemOverflowAction.VaultAction.CopyUsernameClick(username = username), ListingItemOverflowAction.VaultAction.CopyPasswordClick(password = password), + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode = totpCode), ListingItemOverflowAction.VaultAction.LaunchClick(url = uri), ), result, @@ -58,6 +60,7 @@ class CipherViewExtensionsTest { username = null, password = null, uris = null, + totp = null, ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index d68dab9fc..dafff09ac 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionV import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -25,6 +26,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType +import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk @@ -36,9 +38,16 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset @Suppress("LargeClass") class VaultViewModelTest : BaseViewModelTest() { + private val clock: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, + ) private val clipboardManager: BitwardenClipboardManager = mockk { every { setText(any()) } just runs @@ -1108,6 +1117,51 @@ class VaultViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `OverflowOptionClick Vault CopyTotpClick with GenerateTotpCode success should call setText on the ClipboardManager`() = + runTest { + val totpCode = "totpCode" + val code = "Code" + + coEvery { + vaultRepository.generateTotp(totpCode, clock.instant()) + } returns GenerateTotpResult.Success(code, 30) + + val viewModel = createViewModel() + viewModel.trySendAction( + VaultAction.OverflowOptionClick( + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode), + ), + ) + + verify(exactly = 1) { + clipboardManager.setText(code) + } + } + + @Suppress("MaxLineLength") + @Test + fun `OverflowOptionClick Vault CopyTotpClick with GenerateTotpCode failure should not call setText on the ClipboardManager`() = + runTest { + val totpCode = "totpCode" + + coEvery { + vaultRepository.generateTotp(totpCode, clock.instant()) + } returns GenerateTotpResult.Error + + val viewModel = createViewModel() + viewModel.trySendAction( + VaultAction.OverflowOptionClick( + ListingItemOverflowAction.VaultAction.CopyTotpClick(totpCode), + ), + ) + + verify(exactly = 0) { + clipboardManager.setText(text = any()) + } + } + @Suppress("MaxLineLength") @Test fun `OverflowOptionClick Vault CopySecurityCodeClick should call setText on the ClipboardManager`() = @@ -1188,8 +1242,9 @@ class VaultViewModelTest : BaseViewModelTest() { private fun createViewModel(): VaultViewModel = VaultViewModel( - clipboardManager = clipboardManager, authRepository = authRepository, + clipboardManager = clipboardManager, + clock = clock, settingsRepository = settingsRepository, vaultRepository = vaultRepository, )