diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VerifyPasswordResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VerifyPasswordResult.kt deleted file mode 100644 index c14fa4fd58..0000000000 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VerifyPasswordResult.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.x8bit.bitwarden.data.vault.repository.model - -/** - * Models result of verifying the master password. - */ -sealed class VerifyPasswordResult { - - /** - * Master password is successfully verified. - */ - data class Success( - val isVerified: Boolean, - ) : VerifyPasswordResult() - - /** - * An error occurred while trying to verify the master password. - */ - data object Error : VerifyPasswordResult() -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index dc7afc1969..34df9d74b3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates @@ -15,7 +16,6 @@ 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.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult -import com.x8bit.bitwarden.data.vault.repository.model.VerifyPasswordResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -26,7 +26,6 @@ import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -160,17 +159,8 @@ class VaultItemViewModel @Inject constructor( it.copy(dialog = VaultItemState.DialogState.Loading(R.string.loading.asText())) } viewModelScope.launch { - @Suppress("MagicNumber") - delay(2_000) - // TODO: Actually verify the password (BIT-1213) - sendAction( - VaultItemAction.Internal.VerifyPasswordReceive( - VerifyPasswordResult.Success(isVerified = true), - ), - ) - sendEvent( - VaultItemEvent.ShowToast("Password verification not yet implemented.".asText()), - ) + val result = authRepository.validatePassword(action.masterPassword) + sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result)) } } @@ -479,7 +469,10 @@ class VaultItemViewModel @Inject constructor( when (action) { is VaultItemAction.Internal.PasswordBreachReceive -> handlePasswordBreachReceive(action) is VaultItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action) - is VaultItemAction.Internal.VerifyPasswordReceive -> handleVerifyPasswordReceive(action) + is VaultItemAction.Internal.ValidatePasswordReceive -> handleValidatePasswordReceive( + action, + ) + is VaultItemAction.Internal.DeleteCipherReceive -> handleDeleteCipherReceive(action) is VaultItemAction.Internal.RestoreCipherReceive -> handleRestoreCipherReceive(action) } @@ -574,29 +567,37 @@ class VaultItemViewModel @Inject constructor( } } - private fun handleVerifyPasswordReceive( - action: VaultItemAction.Internal.VerifyPasswordReceive, + private fun handleValidatePasswordReceive( + action: VaultItemAction.Internal.ValidatePasswordReceive, ) { when (val result = action.result) { - VerifyPasswordResult.Error -> { + ValidatePasswordResult.Error -> { mutableStateFlow.update { it.copy( dialog = VaultItemState.DialogState.Generic( - message = R.string.invalid_master_password.asText(), + message = R.string.generic_error_message.asText(), ), ) } } - is VerifyPasswordResult.Success -> { - onContent { content -> + is ValidatePasswordResult.Success -> { + if (result.isValid) { + onContent { content -> + mutableStateFlow.update { + it.copy( + dialog = null, + viewState = content.copy( + common = content.common.copy(requiresReprompt = false), + ), + ) + } + } + } else { mutableStateFlow.update { it.copy( - dialog = null, - viewState = content.copy( - common = content.common.copy( - requiresReprompt = !result.isVerified, - ), + dialog = VaultItemState.DialogState.Generic( + message = R.string.invalid_master_password.asText(), ), ) } @@ -1198,8 +1199,8 @@ sealed class VaultItemAction { /** * Indicates that the verify password result has been received. */ - data class VerifyPasswordReceive( - val result: VerifyPasswordResult, + data class ValidatePasswordReceive( + val result: ValidatePasswordResult, ) : Internal() /** diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index d238582438..bf2970a09f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -337,46 +338,145 @@ class VaultItemViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `on MasterPasswordSubmit should verify the password`() = runTest { - val loginViewState = createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - ) - val mockCipherView = mockk { - every { - toViewState( - isPremiumUser = true, - totpCodeItemData = null, + fun `on MasterPasswordSubmit should disabled required prompt when validatePassword success with valid password`() = + runTest { + val loginViewState = createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + ) + val mockCipherView = mockk { + every { + toViewState( + isPremiumUser = true, + totpCodeItemData = null, + ) + } returns loginViewState + } + val password = "password" + coEvery { + authRepo.validatePassword(password) + } returns ValidatePasswordResult.Success(isValid = true) + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + + val loginState = DEFAULT_STATE.copy(viewState = loginViewState) + val viewModel = createViewModel(state = loginState) + + viewModel.stateFlow.test { + assertEquals(loginState, awaitItem()) + viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(password)) + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Loading( + message = R.string.loading.asText(), + ), + ), + awaitItem(), ) - } returns loginViewState - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - - val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val viewModel = createViewModel(state = loginState) - - viewModel.stateFlow.test { - assertEquals(loginState, awaitItem()) - viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit("password")) - assertEquals( - loginState.copy( - dialog = VaultItemState.DialogState.Loading( - message = R.string.loading.asText(), + assertEquals( + loginState.copy( + viewState = loginViewState.copy( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + ), ), - ), - awaitItem(), - ) - assertEquals( - loginState.copy( - viewState = loginViewState.copy( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - ), - ), - awaitItem(), - ) + awaitItem(), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `on MasterPasswordSubmit should show incorrect password dialog when validatePassword success with invalid password`() = + runTest { + val loginViewState = createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + ) + val mockCipherView = mockk { + every { + toViewState( + isPremiumUser = true, + totpCodeItemData = null, + ) + } returns loginViewState + } + val password = "password" + coEvery { + authRepo.validatePassword(password) + } returns ValidatePasswordResult.Success(isValid = false) + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + + val loginState = DEFAULT_STATE.copy(viewState = loginViewState) + val viewModel = createViewModel(state = loginState) + + viewModel.stateFlow.test { + assertEquals(loginState, awaitItem()) + viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(password)) + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Loading( + message = R.string.loading.asText(), + ), + ), + awaitItem(), + ) + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Generic( + message = R.string.invalid_master_password.asText(), + ), + ), + awaitItem(), + ) + } + } + + @Test + fun `on MasterPasswordSubmit should show error dialog when validatePassword Error`() = + runTest { + val loginViewState = createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + ) + val mockCipherView = mockk { + every { + toViewState( + isPremiumUser = true, + totpCodeItemData = null, + ) + } returns loginViewState + } + val password = "password" + coEvery { + authRepo.validatePassword(password) + } returns ValidatePasswordResult.Error + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + + val loginState = DEFAULT_STATE.copy(viewState = loginViewState) + val viewModel = createViewModel(state = loginState) + + viewModel.stateFlow.test { + assertEquals(loginState, awaitItem()) + viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(password)) + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Loading( + message = R.string.loading.asText(), + ), + ), + awaitItem(), + ) + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Generic( + message = R.string.generic_error_message.asText(), + ), + ), + awaitItem(), + ) + } } - } @Test fun `on RefreshClick should sync`() = runTest {