BIT-1687: Password reprompt for items (#936)

This commit is contained in:
Shannon Draeker 2024-02-01 00:35:05 -07:00 committed by Álison Fernandes
parent 2e3200f53d
commit c5e8faccc3
9 changed files with 252 additions and 3 deletions

View file

@ -65,12 +65,19 @@ sealed class ListingItemOverflowAction : Parcelable {
* Represents the vault actions. * Represents the vault actions.
*/ */
sealed class VaultAction : ListingItemOverflowAction() { sealed class VaultAction : ListingItemOverflowAction() {
/**
* Whether the action requires a master password re-prompt if that
* setting is enabled for the selected item.
*/
abstract val requiresPasswordReprompt: Boolean
/** /**
* Click on the view cipher overflow option. * Click on the view cipher overflow option.
*/ */
@Parcelize @Parcelize
data class ViewClick(val cipherId: String) : VaultAction() { data class ViewClick(val cipherId: String) : VaultAction() {
override val title: Text get() = R.string.view.asText() override val title: Text get() = R.string.view.asText()
override val requiresPasswordReprompt: Boolean get() = false
} }
/** /**
@ -79,6 +86,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class EditClick(val cipherId: String) : VaultAction() { data class EditClick(val cipherId: String) : VaultAction() {
override val title: Text get() = R.string.edit.asText() override val title: Text get() = R.string.edit.asText()
override val requiresPasswordReprompt: Boolean get() = true
} }
/** /**
@ -87,6 +95,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class CopyUsernameClick(val username: String) : VaultAction() { data class CopyUsernameClick(val username: String) : VaultAction() {
override val title: Text get() = R.string.copy_username.asText() override val title: Text get() = R.string.copy_username.asText()
override val requiresPasswordReprompt: Boolean get() = false
} }
/** /**
@ -95,6 +104,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class CopyPasswordClick(val password: String) : VaultAction() { data class CopyPasswordClick(val password: String) : VaultAction() {
override val title: Text get() = R.string.copy_password.asText() override val title: Text get() = R.string.copy_password.asText()
override val requiresPasswordReprompt: Boolean get() = true
} }
/** /**
@ -103,6 +113,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class CopyTotpClick(val totpCode: String) : VaultAction() { data class CopyTotpClick(val totpCode: String) : VaultAction() {
override val title: Text get() = R.string.copy_totp.asText() override val title: Text get() = R.string.copy_totp.asText()
override val requiresPasswordReprompt: Boolean get() = false
} }
/** /**
@ -111,6 +122,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class CopyNumberClick(val number: String) : VaultAction() { data class CopyNumberClick(val number: String) : VaultAction() {
override val title: Text get() = R.string.copy_number.asText() override val title: Text get() = R.string.copy_number.asText()
override val requiresPasswordReprompt: Boolean get() = true
} }
/** /**
@ -119,6 +131,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class CopySecurityCodeClick(val securityCode: String) : VaultAction() { data class CopySecurityCodeClick(val securityCode: String) : VaultAction() {
override val title: Text get() = R.string.copy_security_code.asText() override val title: Text get() = R.string.copy_security_code.asText()
override val requiresPasswordReprompt: Boolean get() = true
} }
/** /**
@ -127,6 +140,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class CopyNoteClick(val notes: String) : VaultAction() { data class CopyNoteClick(val notes: String) : VaultAction() {
override val title: Text get() = R.string.copy_notes.asText() override val title: Text get() = R.string.copy_notes.asText()
override val requiresPasswordReprompt: Boolean get() = false
} }
/** /**
@ -135,6 +149,7 @@ sealed class ListingItemOverflowAction : Parcelable {
@Parcelize @Parcelize
data class LaunchClick(val url: String) : VaultAction() { data class LaunchClick(val url: String) : VaultAction() {
override val title: Text get() = R.string.launch.asText() override val title: Text get() = R.string.launch.asText()
override val requiresPasswordReprompt: Boolean get() = false
} }
} }
} }

View file

@ -17,6 +17,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenGroupItem import com.x8bit.bitwarden.ui.platform.components.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -28,6 +29,7 @@ import kotlinx.collections.immutable.toPersistentList
fun VaultContent( fun VaultContent(
state: VaultState.ViewState.Content, state: VaultState.ViewState.Content,
vaultHandlers: VaultHandlers, vaultHandlers: VaultHandlers,
onOverflowOptionClick: (action: ListingItemOverflowAction.VaultAction) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
@ -86,7 +88,15 @@ fun VaultContent(
supportingLabel = favoriteItem.supportingLabel?.invoke(), supportingLabel = favoriteItem.supportingLabel?.invoke(),
onClick = { vaultHandlers.vaultItemClick(favoriteItem) }, onClick = { vaultHandlers.vaultItemClick(favoriteItem) },
overflowOptions = favoriteItem.overflowOptions, overflowOptions = favoriteItem.overflowOptions,
onOverflowOptionClick = vaultHandlers.overflowOptionClick, onOverflowOptionClick = { action ->
if (favoriteItem.shouldShowMasterPasswordReprompt &&
action.requiresPasswordReprompt
) {
onOverflowOptionClick(action)
} else {
vaultHandlers.overflowOptionClick(action)
}
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
@ -245,7 +255,15 @@ fun VaultContent(
supportingLabel = noFolderItem.supportingLabel?.invoke(), supportingLabel = noFolderItem.supportingLabel?.invoke(),
onClick = { vaultHandlers.vaultItemClick(noFolderItem) }, onClick = { vaultHandlers.vaultItemClick(noFolderItem) },
overflowOptions = noFolderItem.overflowOptions, overflowOptions = noFolderItem.overflowOptions,
onOverflowOptionClick = vaultHandlers.overflowOptionClick, onOverflowOptionClick = { action ->
if (noFolderItem.shouldShowMasterPasswordReprompt &&
action.requiresPasswordReprompt
) {
onOverflowOptionClick(action)
} else {
vaultHandlers.overflowOptionClick(action)
}
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),

View file

@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
@ -55,6 +56,7 @@ import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager import com.x8bit.bitwarden.ui.platform.theme.LocalExitManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -190,6 +192,21 @@ private fun VaultScreenScaffold(
) )
} }
var masterPasswordRepromptAction by remember {
mutableStateOf<ListingItemOverflowAction.VaultAction?>(null)
}
masterPasswordRepromptAction?.let { action ->
BitwardenMasterPasswordDialog(
onConfirmClick = { password ->
masterPasswordRepromptAction = null
vaultHandlers.masterPasswordRepromptSubmit(action, password)
},
onDismissRequest = {
masterPasswordRepromptAction = null
},
)
}
BitwardenScaffold( BitwardenScaffold(
topBar = { topBar = {
BitwardenMediumTopAppBar( BitwardenMediumTopAppBar(
@ -276,6 +293,7 @@ private fun VaultScreenScaffold(
is VaultState.ViewState.Content -> VaultContent( is VaultState.ViewState.Content -> VaultContent(
state = viewState, state = viewState,
vaultHandlers = vaultHandlers, vaultHandlers = vaultHandlers,
onOverflowOptionClick = { masterPasswordRepromptAction = it },
modifier = innerModifier, modifier = innerModifier,
) )

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.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -144,6 +145,11 @@ class VaultViewModel @Inject constructor(
is VaultAction.DialogDismiss -> handleDialogDismiss() is VaultAction.DialogDismiss -> handleDialogDismiss()
is VaultAction.RefreshPull -> handleRefreshPull() is VaultAction.RefreshPull -> handleRefreshPull()
is VaultAction.OverflowOptionClick -> handleOverflowOptionClick(action) is VaultAction.OverflowOptionClick -> handleOverflowOptionClick(action)
is VaultAction.MasterPasswordRepromptSubmit -> {
handleMasterPasswordRepromptSubmit(action)
}
is VaultAction.Internal -> handleInternalAction(action) is VaultAction.Internal -> handleInternalAction(action)
} }
} }
@ -326,6 +332,20 @@ class VaultViewModel @Inject constructor(
} }
} }
private fun handleMasterPasswordRepromptSubmit(
action: VaultAction.MasterPasswordRepromptSubmit,
) {
viewModelScope.launch {
val result = authRepository.validatePassword(action.password)
sendAction(
VaultAction.Internal.ValidatePasswordResultReceive(
overflowAction = action.overflowAction,
result = result,
),
)
}
}
private fun handleCopyNoteClick(action: ListingItemOverflowAction.VaultAction.CopyNoteClick) { private fun handleCopyNoteClick(action: ListingItemOverflowAction.VaultAction.CopyNoteClick) {
clipboardManager.setText(action.notes) clipboardManager.setText(action.notes)
} }
@ -390,6 +410,10 @@ class VaultViewModel @Inject constructor(
is VaultAction.Internal.IconLoadingSettingReceive -> handleIconLoadingSettingReceive( is VaultAction.Internal.IconLoadingSettingReceive -> handleIconLoadingSettingReceive(
action, action,
) )
is VaultAction.Internal.ValidatePasswordResultReceive -> {
handleValidatePasswordResultReceive(action)
}
} }
} }
@ -525,6 +549,39 @@ class VaultViewModel @Inject constructor(
} }
} }
private fun handleValidatePasswordResultReceive(
action: VaultAction.Internal.ValidatePasswordResultReceive,
) {
when (val result = action.result) {
ValidatePasswordResult.Error -> {
mutableStateFlow.update {
it.copy(
dialog = VaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
)
}
}
is ValidatePasswordResult.Success -> {
if (!result.isValid) {
mutableStateFlow.update {
it.copy(
dialog = VaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_master_password.asText(),
),
)
}
return
}
// Complete the overflow action.
trySendAction(VaultAction.OverflowOptionClick(action.overflowAction))
}
}
}
//endregion VaultAction Handlers //endregion VaultAction Handlers
} }
@ -721,6 +778,11 @@ data class VaultState(
*/ */
abstract val overflowOptions: List<ListingItemOverflowAction.VaultAction> abstract val overflowOptions: List<ListingItemOverflowAction.VaultAction>
/**
* Whether to prompt the user for their password when they select an overflow option.
*/
abstract val shouldShowMasterPasswordReprompt: Boolean
/** /**
* Represents a login item within the vault. * Represents a login item within the vault.
* *
@ -733,6 +795,7 @@ data class VaultState(
override val startIcon: IconData = IconData.Local(R.drawable.ic_login_item), override val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
override val extraIconList: List<IconRes> = emptyList(), override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>, override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
override val shouldShowMasterPasswordReprompt: Boolean,
val username: Text?, val username: Text?,
) : VaultItem() { ) : VaultItem() {
override val supportingLabel: Text? get() = username override val supportingLabel: Text? get() = username
@ -751,6 +814,7 @@ data class VaultState(
override val startIcon: IconData = IconData.Local(R.drawable.ic_card_item), override val startIcon: IconData = IconData.Local(R.drawable.ic_card_item),
override val extraIconList: List<IconRes> = emptyList(), override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>, override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
override val shouldShowMasterPasswordReprompt: Boolean,
val brand: Text? = null, val brand: Text? = null,
val lastFourDigits: Text? = null, val lastFourDigits: Text? = null,
) : VaultItem() { ) : VaultItem() {
@ -779,6 +843,7 @@ data class VaultState(
override val startIcon: IconData = IconData.Local(R.drawable.ic_identity_item), override val startIcon: IconData = IconData.Local(R.drawable.ic_identity_item),
override val extraIconList: List<IconRes> = emptyList(), override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>, override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
override val shouldShowMasterPasswordReprompt: Boolean,
val firstName: Text?, val firstName: Text?,
) : VaultItem() { ) : VaultItem() {
override val supportingLabel: Text? get() = firstName override val supportingLabel: Text? get() = firstName
@ -795,6 +860,7 @@ data class VaultState(
override val startIcon: IconData = IconData.Local(R.drawable.ic_secure_note_item), override val startIcon: IconData = IconData.Local(R.drawable.ic_secure_note_item),
override val extraIconList: List<IconRes> = emptyList(), override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>, override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
override val shouldShowMasterPasswordReprompt: Boolean,
) : VaultItem() { ) : VaultItem() {
override val supportingLabel: Text? get() = null override val supportingLabel: Text? get() = null
} }
@ -1033,6 +1099,15 @@ sealed class VaultAction {
val overflowAction: ListingItemOverflowAction.VaultAction, val overflowAction: ListingItemOverflowAction.VaultAction,
) : VaultAction() ) : VaultAction()
/**
* User submitted their master password to authenticate before continuing with
* the selected overflow action.
*/
data class MasterPasswordRepromptSubmit(
val overflowAction: ListingItemOverflowAction.VaultAction,
val password: String,
) : VaultAction()
/** /**
* Models actions that the [VaultViewModel] itself might send. * Models actions that the [VaultViewModel] itself might send.
*/ */
@ -1070,6 +1145,14 @@ sealed class VaultAction {
data class VaultDataReceive( data class VaultDataReceive(
val vaultData: DataState<VaultData>, val vaultData: DataState<VaultData>,
) : Internal() ) : Internal()
/**
* Indicates that a result for verifying the user's master password has been received.
*/
data class ValidatePasswordResultReceive(
val overflowAction: ListingItemOverflowAction.VaultAction,
val result: ValidatePasswordResult,
) : Internal()
} }
} }

View file

@ -33,6 +33,7 @@ data class VaultHandlers(
val tryAgainClick: () -> Unit, val tryAgainClick: () -> Unit,
val dialogDismiss: () -> Unit, val dialogDismiss: () -> Unit,
val overflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit, val overflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit,
val masterPasswordRepromptSubmit: (ListingItemOverflowAction.VaultAction, String) -> Unit,
) { ) {
companion object { companion object {
/** /**
@ -79,6 +80,14 @@ data class VaultHandlers(
overflowOptionClick = { overflowOptionClick = {
viewModel.trySendAction(VaultAction.OverflowOptionClick(it)) viewModel.trySendAction(VaultAction.OverflowOptionClick(it))
}, },
masterPasswordRepromptSubmit = { action, password ->
viewModel.trySendAction(
VaultAction.MasterPasswordRepromptSubmit(
overflowAction = action,
password = password,
),
)
},
) )
} }
} }

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util package com.x8bit.bitwarden.ui.vault.feature.vault.util
import android.net.Uri import android.net.Uri
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView import com.bitwarden.core.CollectionView
@ -160,6 +161,7 @@ private fun CipherView.toVaultItemOrNull(
), ),
overflowOptions = toOverflowActions(), overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(), extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
) )
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote( CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
@ -167,6 +169,7 @@ private fun CipherView.toVaultItemOrNull(
name = name.asText(), name = name.asText(),
overflowOptions = toOverflowActions(), overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(), extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
) )
CipherType.CARD -> VaultState.ViewState.VaultItem.Card( CipherType.CARD -> VaultState.ViewState.VaultItem.Card(
@ -178,6 +181,7 @@ private fun CipherView.toVaultItemOrNull(
?.asText(), ?.asText(),
overflowOptions = toOverflowActions(), overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(), extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
) )
CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity( CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity(
@ -186,6 +190,7 @@ private fun CipherView.toVaultItemOrNull(
firstName = identity?.firstName?.asText(), firstName = identity?.firstName?.asText(),
overflowOptions = toOverflowActions(), overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(), extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
) )
} }
} }

View file

@ -386,7 +386,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `MasterPasswordRepromptSubmit for a request Success with a valid password should should post to the AutofillSelectionManager`() = fun `MasterPasswordRepromptSubmit for a request Success with a valid password should post to the AutofillSelectionManager`() =
runTest { runTest {
setupMockUri() setupMockUri()
val cipherView = createMockCipherView(number = 1) val cipherView = createMockCipherView(number = 1)

View file

@ -736,6 +736,7 @@ class VaultScreenTest : BaseComposeTest() {
name = itemText.asText(), name = itemText.asText(),
username = username.asText(), username = username.asText(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
shouldShowMasterPasswordReprompt = false,
) )
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
@ -857,6 +858,7 @@ class VaultScreenTest : BaseComposeTest() {
name = itemText.asText(), name = itemText.asText(),
username = userName.asText(), username = userName.asText(),
overflowOptions = emptyList(), overflowOptions = emptyList(),
shouldShowMasterPasswordReprompt = false,
) )
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(

View file

@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState 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.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@ -1328,6 +1329,104 @@ class VaultViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `MasterPasswordRepromptSubmit for a request Error should show a generic error dialog`() =
runTest {
val password = "password"
coEvery {
authRepository.validatePassword(password = password)
} returns ValidatePasswordResult.Error
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
viewModel.trySendAction(
VaultAction.MasterPasswordRepromptSubmit(
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = password,
),
password = password,
),
)
assertEquals(
DEFAULT_STATE.copy(
dialog = VaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `MasterPasswordRepromptSubmit for a request Success with an invalid password should show an invalid password dialog`() =
runTest {
val password = "password"
coEvery {
authRepository.validatePassword(password = password)
} returns ValidatePasswordResult.Success(isValid = false)
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
viewModel.trySendAction(
VaultAction.MasterPasswordRepromptSubmit(
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = password,
),
password = password,
),
)
assertEquals(
DEFAULT_STATE.copy(
dialog = VaultState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_master_password.asText(),
),
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `MasterPasswordRepromptSubmit for a request Success with a valid password should continue the action`() =
runTest {
val password = "password"
coEvery {
authRepository.validatePassword(password = password)
} returns ValidatePasswordResult.Success(isValid = true)
val viewModel = createViewModel()
viewModel.trySendAction(
VaultAction.MasterPasswordRepromptSubmit(
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
password = password,
),
password = password,
),
)
verify(exactly = 1) {
clipboardManager.setText(password)
}
}
private fun createViewModel(): VaultViewModel = private fun createViewModel(): VaultViewModel =
VaultViewModel( VaultViewModel(
authRepository = authRepository, authRepository = authRepository,