mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1646, BIT-1647: Launch action after password validation (#852)
This commit is contained in:
parent
20dd839923
commit
d12776483d
4 changed files with 592 additions and 140 deletions
|
@ -114,7 +114,14 @@ fun VaultItemScreen(
|
|||
{ viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick) }
|
||||
},
|
||||
onSubmitMasterPassword = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(it)) }
|
||||
{ masterPassword, action ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = masterPassword,
|
||||
action = action,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -271,7 +278,7 @@ fun VaultItemScreen(
|
|||
private fun VaultItemDialogs(
|
||||
dialog: VaultItemState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSubmitMasterPassword: (String) -> Unit,
|
||||
onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit,
|
||||
) {
|
||||
when (dialog) {
|
||||
is VaultItemState.DialogState.Generic -> BitwardenBasicDialog(
|
||||
|
@ -286,9 +293,9 @@ private fun VaultItemDialogs(
|
|||
visibilityState = LoadingDialogState.Shown(text = dialog.message),
|
||||
)
|
||||
|
||||
VaultItemState.DialogState.MasterPasswordDialog -> {
|
||||
is VaultItemState.DialogState.MasterPasswordDialog -> {
|
||||
BitwardenMasterPasswordDialog(
|
||||
onConfirmClick = onSubmitMasterPassword,
|
||||
onConfirmClick = { onSubmitMasterPassword(it, dialog.action) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ private const val KEY_STATE = "state"
|
|||
/**
|
||||
* ViewModel responsible for handling user interactions in the vault item screen
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class VaultItemViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
@ -141,7 +141,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.EditClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
|
@ -160,7 +164,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.validatePassword(action.masterPassword)
|
||||
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result))
|
||||
sendAction(VaultItemAction.Internal.ValidatePasswordReceive(result, action.action))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +179,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(action.field),
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
|
@ -195,7 +203,14 @@ class VaultItemViewModel @Inject constructor(
|
|||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
||||
field = action.field,
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
|
@ -218,27 +233,69 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleAttachmentsClick() {
|
||||
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
||||
}
|
||||
|
||||
private fun handleCloneClick() {
|
||||
sendEvent(
|
||||
VaultItemEvent.NavigateToAddEdit(
|
||||
itemId = state.vaultItemId,
|
||||
isClone = true,
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentsClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToAttachments(itemId = state.vaultItemId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloneClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CloneClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToAddEdit(itemId = state.vaultItemId, isClone = true))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMoveToOrganizationClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.MoveToOrganizationClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.NavigateToMoveToOrganization(itemId = state.vaultItemId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCollectionsClick() {
|
||||
sendEvent(VaultItemEvent.NavigateToCollections(itemId = state.vaultItemId))
|
||||
}
|
||||
|
||||
private fun handleConfirmDeleteClick() {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
|
@ -246,11 +303,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
onContent { content ->
|
||||
content
|
||||
.common
|
||||
.currentCipher
|
||||
?.let { cipher ->
|
||||
content.common.currentCipher?.let { cipher ->
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
VaultItemAction.Internal.DeleteCipherReceive(
|
||||
|
@ -347,13 +400,17 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
private fun handleCopyPasswordClick() {
|
||||
onLoginContent { content, login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = password),
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
clipboardManager.setText(text = password)
|
||||
}
|
||||
}
|
||||
|
@ -370,13 +427,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleCopyUsernameClick() {
|
||||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
onLoginContent { _, login ->
|
||||
val username = requireNotNull(login.username)
|
||||
clipboardManager.setText(text = username)
|
||||
}
|
||||
|
@ -392,7 +443,11 @@ class VaultItemViewModel @Inject constructor(
|
|||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.PasswordHistoryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
|
@ -406,7 +461,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewPasswordClick(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
|
@ -437,26 +498,34 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
private fun handleCopyNumberClick() {
|
||||
onCardContent { content, card ->
|
||||
val number = requireNotNull(card.number)
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = number),
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onCardContent
|
||||
}
|
||||
val number = requireNotNull(card.number)
|
||||
clipboardManager.setText(text = number)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopySecurityCodeClick() {
|
||||
onCardContent { content, card ->
|
||||
val securityCode = requireNotNull(card.securityCode)
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = securityCode),
|
||||
),
|
||||
)
|
||||
}
|
||||
return@onCardContent
|
||||
}
|
||||
val securityCode = requireNotNull(card.securityCode)
|
||||
clipboardManager.setText(text = securityCode)
|
||||
}
|
||||
}
|
||||
|
@ -467,6 +536,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
||||
when (action) {
|
||||
is VaultItemAction.Internal.CopyValue -> handleCopyValue(action)
|
||||
is VaultItemAction.Internal.PasswordBreachReceive -> handlePasswordBreachReceive(action)
|
||||
is VaultItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
is VaultItemAction.Internal.ValidatePasswordReceive -> handleValidatePasswordReceive(
|
||||
|
@ -478,6 +548,10 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCopyValue(action: VaultItemAction.Internal.CopyValue) {
|
||||
clipboardManager.setText(action.value)
|
||||
}
|
||||
|
||||
private fun handlePasswordBreachReceive(
|
||||
action: VaultItemAction.Internal.PasswordBreachReceive,
|
||||
) {
|
||||
|
@ -592,6 +666,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
trySendAction(action.repromptAction.vaultItemAction)
|
||||
}
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
|
@ -949,7 +1024,9 @@ data class VaultItemState(
|
|||
* Displays the master password dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data object MasterPasswordDialog : DialogState()
|
||||
data class MasterPasswordDialog(
|
||||
val action: PasswordRepromptAction,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1054,6 +1131,7 @@ sealed class VaultItemAction {
|
|||
*/
|
||||
data class MasterPasswordSubmit(
|
||||
val masterPassword: String,
|
||||
val action: PasswordRepromptAction,
|
||||
) : Common()
|
||||
|
||||
/**
|
||||
|
@ -1181,6 +1259,13 @@ sealed class VaultItemAction {
|
|||
* Models actions that the [VaultItemViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : VaultItemAction() {
|
||||
/**
|
||||
* Copies the given [value] to the clipboard.
|
||||
*/
|
||||
data class CopyValue(
|
||||
val value: String,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the password breach results have been received.
|
||||
*/
|
||||
|
@ -1201,6 +1286,7 @@ sealed class VaultItemAction {
|
|||
*/
|
||||
data class ValidatePasswordReceive(
|
||||
val result: ValidatePasswordResult,
|
||||
val repromptAction: PasswordRepromptAction,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
|
@ -1218,3 +1304,117 @@ sealed class VaultItemAction {
|
|||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents all the actions that can be taken after being prompted to a master password check.
|
||||
*/
|
||||
sealed class PasswordRepromptAction : Parcelable {
|
||||
/**
|
||||
* The Vault action that should be sent when password validation has completed.
|
||||
*/
|
||||
abstract val vaultItemAction: VaultItemAction
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Common.EditClick] upon password
|
||||
* validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object EditClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.EditClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.ItemType.Login.PasswordHistoryClick]
|
||||
* upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object PasswordHistoryClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.ItemType.Login.PasswordHistoryClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Common.AttachmentsClick] upon password
|
||||
* validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object AttachmentsClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.AttachmentsClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Common.CloneClick] upon password
|
||||
* validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object CloneClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.CloneClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Common.MoveToOrganizationClick] upon
|
||||
* password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object MoveToOrganizationClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.MoveToOrganizationClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Common.ConfirmDeleteClick] upon
|
||||
* password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data object DeleteClick : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.ConfirmDeleteClick
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Internal.CopyValue] upon password
|
||||
* validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CopyClick(
|
||||
val value: String,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Internal.CopyValue(
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the
|
||||
* [VaultItemAction.ItemType.Login.PasswordVisibilityClicked] upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewPasswordClick(
|
||||
val isVisible: Boolean,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.ItemType.Login.PasswordVisibilityClicked(
|
||||
isVisible = isVisible,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that we should launch the [VaultItemAction.Common.HiddenFieldVisibilityClicked]
|
||||
* upon password validation.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewHiddenFieldClicked(
|
||||
val field: VaultItemState.ViewState.Content.Common.Custom.HiddenField,
|
||||
val isVisible: Boolean,
|
||||
) : PasswordRepromptAction() {
|
||||
override val vaultItemAction: VaultItemAction
|
||||
get() = VaultItemAction.Common.HiddenFieldVisibilityClicked(
|
||||
field = this.field,
|
||||
isVisible = this.isVisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,7 +186,11 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithText("Master password confirmation").assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -198,8 +202,13 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
@Test
|
||||
fun `Ok click on master password dialog should emit DismissDialogClick`() {
|
||||
val enteredPassword = "pass1234"
|
||||
val passwordRepromptAction = PasswordRepromptAction.EditClick
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
it.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = passwordRepromptAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Master password").performTextInput(enteredPassword)
|
||||
|
@ -209,7 +218,12 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(enteredPassword))
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = enteredPassword,
|
||||
action = passwordRepromptAction,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.item
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import app.cash.turbine.turbineScope
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
|
@ -42,6 +43,7 @@ import org.junit.jupiter.api.BeforeEach
|
|||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||
|
@ -124,17 +126,54 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(initialState.copy(dialog = null), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConfirmDeleteClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.ConfirmDeleteClick)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `ConfirmDeleteClick with DeleteCipherResult Success should should ShowToast and NavigateBack`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
|
@ -166,13 +205,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Suppress("MaxLineLength")
|
||||
fun `ConfirmDeleteClick with DeleteCipherResult Failure should should Show generic error`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
@ -189,7 +231,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE,
|
||||
viewState = loginViewState,
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
|
@ -303,7 +345,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.trySendAction(VaultItemAction.Common.EditClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.EditClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
@ -363,16 +409,23 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(password))
|
||||
turbineScope {
|
||||
val stateFlow = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventFlow = viewModel.eventFlow.testIn(backgroundScope)
|
||||
assertEquals(loginState, stateFlow.awaitItem())
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = password,
|
||||
action = PasswordRepromptAction.EditClick,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
message = R.string.loading.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
|
@ -380,7 +433,14 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemEvent.NavigateToAddEdit(
|
||||
itemId = DEFAULT_STATE.vaultItemId,
|
||||
isClone = false,
|
||||
),
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -412,7 +472,12 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(password))
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = password,
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
|
@ -458,7 +523,12 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(password))
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.MasterPasswordSubmit(
|
||||
masterPassword = password,
|
||||
action = PasswordRepromptAction.DeleteClick,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.Loading(
|
||||
|
@ -509,7 +579,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick("field"))
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(value = "field"),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
@ -566,6 +640,12 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `on HiddenFieldVisibilityClicked should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val field = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
|
@ -580,17 +660,19 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.HiddenFieldVisibilityClicked(
|
||||
field = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
field = field,
|
||||
isVisible = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewHiddenFieldClicked(
|
||||
field = field,
|
||||
isVisible = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
@ -661,8 +743,60 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on AttachmentsClick should emit NavigateToAttachments`() = runTest {
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
fun `on AttachmentsClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.AttachmentsClick)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.AttachmentsClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AttachmentsClick should emit NavigateToAttachments when re-prompt is not required`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.AttachmentsClick)
|
||||
assertEquals(
|
||||
|
@ -673,8 +807,59 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on CloneClick should emit NavigateToAddEdit with isClone set to true`() = runTest {
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
fun `on CloneClick should show password dialog when re-prompt is required`() = runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.CloneClick)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CloneClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CloneClick should emit NavigateToAddEdit when re-prompt is not required`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CloneClick)
|
||||
assertEquals(
|
||||
|
@ -688,8 +873,60 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on MoveToOrganizationClick should emit NavigateToMoveToOrganization`() = runTest {
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
fun `on MoveToOrganizationClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.MoveToOrganizationClick)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.MoveToOrganizationClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on MoveToOrganizationClick should emit NavigateToMoveToOrganization when re-prompt is not required`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.MoveToOrganizationClick)
|
||||
assertEquals(
|
||||
|
@ -794,7 +1031,13 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(
|
||||
value = DEFAULT_LOGIN_PASSWORD,
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
@ -862,39 +1105,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyUsernameClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyUsernameClick should call setText on ClipboardManager when re-prompt is not required`() {
|
||||
fun `on CopyUsernameClick should call setText on ClipboardManager`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(
|
||||
|
@ -946,7 +1157,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.PasswordHistoryClick,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
@ -1015,11 +1230,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Login.PasswordVisibilityClicked(
|
||||
true,
|
||||
isVisible = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.ViewPasswordClick(isVisible = true),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
@ -1109,7 +1328,13 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(cardState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
assertEquals(
|
||||
cardState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
cardState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(
|
||||
value = requireNotNull(DEFAULT_CARD_TYPE.number),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
@ -1168,7 +1393,13 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(cardState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
assertEquals(
|
||||
cardState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
cardState.copy(
|
||||
dialog = VaultItemState.DialogState.MasterPasswordDialog(
|
||||
action = PasswordRepromptAction.CopyClick(
|
||||
value = requireNotNull(DEFAULT_CARD_TYPE.securityCode),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue