BIT-1213: Add real password check to vault item screen (#844)

This commit is contained in:
David Perez 2024-01-29 08:17:16 -06:00 committed by Álison Fernandes
parent 2d652c8a2e
commit 91207df3fa
3 changed files with 164 additions and 82 deletions

View file

@ -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()
}

View file

@ -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()
/**

View file

@ -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<CipherView> {
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<CipherView> {
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<CipherView> {
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<CipherView> {
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 {