mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-8137] Introduce FIDO 2 user verification to item listing screen (#3449)
This commit is contained in:
parent
4d65230476
commit
53c5d11076
7 changed files with 840 additions and 31 deletions
|
@ -32,11 +32,20 @@ interface BiometricsManager {
|
|||
|
||||
/**
|
||||
* Display a prompt for performing user verification with biometrics or device credentials.
|
||||
*
|
||||
* @param onSuccess Indicates the user was successfully verified.
|
||||
* @param onCancel Indicates the user cancelled verification.
|
||||
* @param onLockOut Indicates there were too many failed verification attempts and must wait
|
||||
* some time before attempting verification again.
|
||||
* @param onError Indicates the user was not verified due to an unexpected error.
|
||||
* @param onNotSupported Indicates the users device is not capable of performing user
|
||||
* verification.
|
||||
*/
|
||||
fun promptUserVerification(
|
||||
onSuccess: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onLockOut: () -> Unit,
|
||||
onError: () -> Unit,
|
||||
onNotSupported: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,14 +50,19 @@ class BiometricsManagerImpl(
|
|||
onCancel: () -> Unit,
|
||||
onLockOut: () -> Unit,
|
||||
onError: () -> Unit,
|
||||
onNotSupported: () -> Unit,
|
||||
) {
|
||||
configureAndDisplayPrompt(
|
||||
onSuccess = { onSuccess() },
|
||||
onCancel = onCancel,
|
||||
onLockOut = onLockOut,
|
||||
onError = onError,
|
||||
cipher = null,
|
||||
)
|
||||
if (isUserVerificationSupported.not()) {
|
||||
onNotSupported()
|
||||
} else {
|
||||
configureAndDisplayPrompt(
|
||||
onSuccess = { onSuccess() },
|
||||
onCancel = onCancel,
|
||||
onLockOut = onLockOut,
|
||||
onError = onError,
|
||||
cipher = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canAuthenticate(authenticators: Int): Boolean =
|
||||
|
|
|
@ -44,11 +44,14 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
|||
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingUserVerificationHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
|
@ -72,11 +75,15 @@ fun VaultItemListingScreen(
|
|||
onNavigateToSearch: (searchType: SearchType) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
fido2CompletionManager: Fido2CompletionManager = LocalFido2CompletionManager.current,
|
||||
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
|
||||
viewModel: VaultItemListingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val userVerificationHandlers = remember(viewModel) {
|
||||
VaultItemListingUserVerificationHandlers.create(viewModel = viewModel)
|
||||
}
|
||||
|
||||
val pullToRefreshState = rememberPullToRefreshState().takeIf { state.isPullToRefreshEnabled }
|
||||
LaunchedEffect(key1 = pullToRefreshState?.isRefreshing) {
|
||||
|
@ -137,6 +144,19 @@ fun VaultItemListingScreen(
|
|||
is VaultItemListingEvent.CompleteFido2Registration -> {
|
||||
fido2CompletionManager.completeFido2Registration(event.result)
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.Fido2UserVerification -> {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = {
|
||||
userVerificationHandlers
|
||||
.onUserVerificationSuccess(event.selectedCipherView)
|
||||
},
|
||||
onCancel = userVerificationHandlers.onUserVerificationCancelled,
|
||||
onLockOut = userVerificationHandlers.onUserVerificationLockOut,
|
||||
onError = userVerificationHandlers.onUserVerificationFail,
|
||||
onNotSupported = userVerificationHandlers.onUserVerificationNotSupported,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions.AuthenticatorSelectionCriteria.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
|
@ -21,6 +23,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
|
|||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
|
@ -195,6 +198,27 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
|
||||
is VaultItemListingsAction.RefreshClick -> handleRefreshClick()
|
||||
is VaultItemListingsAction.RefreshPull -> handleRefreshPull()
|
||||
|
||||
VaultItemListingsAction.UserVerificationLockOut -> {
|
||||
handleUserVerificationLockOut()
|
||||
}
|
||||
|
||||
VaultItemListingsAction.UserVerificationCancelled -> {
|
||||
handleUserVerificationCancelled()
|
||||
}
|
||||
|
||||
VaultItemListingsAction.UserVerificationFail -> {
|
||||
handleUserVerificationFail()
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.UserVerificationSuccess -> {
|
||||
handleUserVerificationSuccess(action)
|
||||
}
|
||||
|
||||
VaultItemListingsAction.UserVerificationNotSupported -> {
|
||||
handleUserVerificationNotSupported()
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
@ -230,6 +254,42 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
vaultRepository.sync()
|
||||
}
|
||||
|
||||
private fun handleUserVerificationLockOut() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleUserVerificationSuccess(
|
||||
action: VaultItemListingsAction.UserVerificationSuccess,
|
||||
) {
|
||||
specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2RequestOrNull()
|
||||
?.let { request ->
|
||||
registerFido2CredentialToCipher(
|
||||
request = request,
|
||||
cipherView = action.selectedCipherView,
|
||||
)
|
||||
}
|
||||
?: showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleUserVerificationFail() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleUserVerificationCancelled() {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationNotSupported() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleCopySendUrlClick(action: ListingItemOverflowAction.SendAction.CopyUrlClick) {
|
||||
clipboardManager.setText(text = action.sendUrl)
|
||||
}
|
||||
|
@ -297,27 +357,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
if (state.isFido2Creation) {
|
||||
val cipherView = getCipherViewOrNull(action.id) ?: return
|
||||
val credentialRequest = state.fido2CredentialRequest ?: return
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Loading(
|
||||
message = R.string.saving.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = fido2CredentialManager.registerFido2Credential(
|
||||
state.activeAccountSummary.userId,
|
||||
fido2CredentialRequest = credentialRequest,
|
||||
selectedCipherView = cipherView,
|
||||
)
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
result = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
handleFido2RegistrationRequestReceive(action)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -333,6 +373,91 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
sendEvent(event)
|
||||
}
|
||||
|
||||
private fun handleFido2RegistrationRequestReceive(action: VaultItemListingsAction.ItemClick) {
|
||||
val cipherView = getCipherViewOrNull(action.id)
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
val credentialRequest = state
|
||||
.fido2CredentialRequest
|
||||
?: run {
|
||||
// This scenario should not occur because `isFido2Creation` is false when
|
||||
// `fido2CredentialRequest` is null. We show the FIDO 2 error dialog to inform
|
||||
// the user and terminate the flow.
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Loading(
|
||||
message = R.string.saving.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
val createOptions = fido2CredentialManager
|
||||
.getPasskeyCreateOptionsOrNull(credentialRequest.requestJson)
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
when (createOptions.authenticatorSelection.userVerification) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
registerFido2CredentialToCipher(
|
||||
request = credentialRequest,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
|
||||
UserVerificationRequirement.PREFERRED -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = false,
|
||||
selectedCipherView = cipherView,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
UserVerificationRequirement.REQUIRED -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = cipherView,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
null -> {
|
||||
// Per WebAuthn spec members should be ignored when invalid. Since the request
|
||||
// violates spec we display an error and terminate the operation.
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerFido2CredentialToCipher(
|
||||
request: Fido2CredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val activeUserId = authRepository.activeUserId
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result: Fido2RegisterCredentialResult =
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = activeUserId,
|
||||
fido2CredentialRequest = request,
|
||||
selectedCipherView = cipherView,
|
||||
)
|
||||
sendAction(
|
||||
VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive(result),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordRepromptSubmit(
|
||||
action: VaultItemListingsAction.MasterPasswordRepromptSubmit,
|
||||
) {
|
||||
|
@ -409,6 +534,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleDismissFido2ErrorDialogClick() {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error,
|
||||
|
@ -943,6 +1069,22 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
)
|
||||
as? DecryptFido2CredentialAutofillViewResult.Success)
|
||||
?.fido2CredentialAutofillViews
|
||||
|
||||
private fun showFido2ErrorDialog() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDialogState() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1368,13 +1510,21 @@ sealed class VaultItemListingEvent {
|
|||
data class ShowToast(val text: Text) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Complete the current FIDO 2 credential creation process.
|
||||
* Complete the current FIDO 2 credential registration process.
|
||||
*
|
||||
* @property result the result of FIDO 2 credential creation.
|
||||
* @property result the result of FIDO 2 credential registration.
|
||||
*/
|
||||
data class CompleteFido2Registration(
|
||||
val result: Fido2RegisterCredentialResult,
|
||||
) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Perform user verification for a FIDO 2 credential operation.
|
||||
*/
|
||||
data class Fido2UserVerification(
|
||||
val isRequired: Boolean,
|
||||
val selectedCipherView: CipherView,
|
||||
) : VaultItemListingEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1473,7 +1623,7 @@ sealed class VaultItemListingsAction {
|
|||
data class FolderClick(val id: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* A master password prompt was encountered when trying to perform a senstive action described
|
||||
* A master password prompt was encountered when trying to perform a sensitive action described
|
||||
* by the given [masterPasswordRepromptData] and the given [password] was submitted.
|
||||
*/
|
||||
data class MasterPasswordRepromptSubmit(
|
||||
|
@ -1486,6 +1636,34 @@ sealed class VaultItemListingsAction {
|
|||
*/
|
||||
data object RefreshPull : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user has too many failed verification attempts for FIDO operations and can no longer
|
||||
* use biometric verification for some time.
|
||||
*/
|
||||
data object UserVerificationLockOut : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user has failed biometric verification for FIDO 2 operations.
|
||||
*/
|
||||
data object UserVerificationFail : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user has successfully verified themself using biometrics.
|
||||
*/
|
||||
data class UserVerificationSuccess(
|
||||
val selectedCipherView: CipherView,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user has cancelled biometric user verification.
|
||||
*/
|
||||
data object UserVerificationCancelled : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* The user cannot perform verification because it is not supported by the device.
|
||||
*/
|
||||
data object UserVerificationNotSupported : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultItemListingViewModel] itself might send.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingViewModel
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
|
||||
|
||||
/**
|
||||
* A collection of handler functions specifically tailored for managing action within the context of
|
||||
* device user verification.
|
||||
*
|
||||
* @property onUserVerificationSuccess Handles the action when biometric verification is
|
||||
* successful.
|
||||
* @property onUserVerificationFail Handles the action when biometric verification fails.
|
||||
* @property onUserVerificationLockOut Handles the action when too many failed verification attempts
|
||||
* locks out the user for a period of time.
|
||||
*/
|
||||
data class VaultItemListingUserVerificationHandlers(
|
||||
val onUserVerificationSuccess: (selectedCipherView: CipherView) -> Unit,
|
||||
val onUserVerificationLockOut: () -> Unit,
|
||||
val onUserVerificationFail: () -> Unit,
|
||||
val onUserVerificationCancelled: () -> Unit,
|
||||
val onUserVerificationNotSupported: () -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates an instance of [VaultItemListingUserVerificationHandlers] by binding
|
||||
* actions to the provided [VaultItemListingViewModel].
|
||||
*/
|
||||
fun create(
|
||||
viewModel: VaultItemListingViewModel,
|
||||
): VaultItemListingUserVerificationHandlers =
|
||||
VaultItemListingUserVerificationHandlers(
|
||||
onUserVerificationSuccess = { selectedCipherView ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
selectedCipherView = selectedCipherView,
|
||||
),
|
||||
)
|
||||
},
|
||||
onUserVerificationFail = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationFail)
|
||||
},
|
||||
onUserVerificationLockOut = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationLockOut)
|
||||
},
|
||||
onUserVerificationCancelled = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
|
||||
},
|
||||
onUserVerificationNotSupported = {
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationNotSupported)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,11 +16,13 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -29,6 +31,7 @@ import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
|||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
|
||||
|
@ -51,6 +54,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
|||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.every
|
||||
import io.mockk.invoke
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
@ -84,6 +88,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
private val fido2CompletionManager: Fido2CompletionManager = mockk {
|
||||
every { completeFido2Registration(any()) } just runs
|
||||
}
|
||||
private val biometricsManager: BiometricsManager = mockk()
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultItemListingEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<VaultItemListingViewModel>(relaxed = true) {
|
||||
|
@ -100,6 +105,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
viewModel = viewModel,
|
||||
intentManager = intentManager,
|
||||
fido2CompletionManager = fido2CompletionManager,
|
||||
biometricsManager = biometricsManager,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVaultItem = { onNavigateToVaultItemId = it },
|
||||
onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true },
|
||||
|
@ -1477,6 +1483,188 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CompleteFido2Registration event should call Fido2CompletionManager with result`() {
|
||||
val result = Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Registration(result))
|
||||
verify {
|
||||
fido2CompletionManager.completeFido2Registration(result)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification event should perform user verification when it is supported`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} just runs
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
),
|
||||
)
|
||||
verify {
|
||||
biometricsManager.promptUserVerification(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `promptForUserVerification onSuccess should send UserVerificationSuccess action`() {
|
||||
val selectedCipherView = createMockCipherView(number = 1)
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = captureLambda(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = selectedCipherView,
|
||||
),
|
||||
)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(selectedCipherView),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `promptForUserVerification onCancel should send UserVerificationCancelled action`() {
|
||||
val selectedCipherView = createMockCipherView(number = 1)
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
any(),
|
||||
onCancel = captureLambda(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = selectedCipherView,
|
||||
),
|
||||
)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationCancelled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `promptForUserVerification onLockOut should send UserVerificationLockOut action`() {
|
||||
val selectedCipherView = createMockCipherView(number = 1)
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
any(),
|
||||
any(),
|
||||
onLockOut = captureLambda(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = selectedCipherView,
|
||||
),
|
||||
)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationLockOut,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `promptForUserVerification onError should send UserVerificationFail action`() {
|
||||
val selectedCipherView = createMockCipherView(number = 1)
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
onError = captureLambda(),
|
||||
any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = selectedCipherView,
|
||||
),
|
||||
)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationFail,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `promptForUserVerification onNotSupported should send UserVerificationNotFailed action`() {
|
||||
val selectedCipherView = createMockCipherView(number = 1)
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
onNotSupported = captureLambda(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = selectedCipherView,
|
||||
),
|
||||
)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationNotSupported,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
|
|
|
@ -10,12 +10,15 @@ 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.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions.AuthenticatorSelectionCriteria.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
|
@ -47,6 +50,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat
|
|||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPublicKeyCredentialCreationOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
|
@ -114,6 +118,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
|
||||
every { lockVault(any()) } just runs
|
||||
every { sync() } just runs
|
||||
coEvery {
|
||||
getDecryptedFido2CredentialAutofillViews(any())
|
||||
} returns DecryptFido2CredentialAutofillViewResult.Error
|
||||
}
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
every { environment } returns Environment.Us
|
||||
|
@ -323,6 +330,162 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item during FIDO 2 registration should show FIDO 2 error dialog when cipherView is null`() {
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item during FIDO 2 registration should show FIDO 2 error dialog when PasskeyCreateOptions is null`() {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
every { fido2CredentialManager.getPasskeyCreateOptionsOrNull(any()) } returns null
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item during FIDO 2 registration should show loading dialog, then request user verification when required`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(any())
|
||||
} returns createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Loading(R.string.saving.asText()),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.DismissPullToRefresh,
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.Fido2UserVerification(
|
||||
isRequired = true,
|
||||
selectedCipherView = cipherView,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item during FIDO 2 registration should skip user verification and perform registration when discouraged`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
val mockFido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockFido2CredentialRequest,
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(any())
|
||||
} returns createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick(cipherView.id.orEmpty()))
|
||||
|
||||
coVerify {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
fido2CredentialRequest = mockFido2CredentialRequest,
|
||||
selectedCipherView = cipherView,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -1886,6 +2049,196 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationLockout should display Fido2ErrorDialog`() {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationLockOut)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationCancelled should clear dialog state and emit CompleteFido2Create with cancelled result`() =
|
||||
runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
|
||||
|
||||
assertNull(viewModel.stateFlow.value.dialogState)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationFail should display Fido2ErrorDialog`() {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationFail)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is null`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockResponse",
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
createMockCipherView(number = 1),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when SpecialCircumstance is invalid`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
AutofillSaveItem.Login(
|
||||
username = "mockUsername",
|
||||
password = "mockPassword",
|
||||
uri = "mockUri",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockResponse",
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when activeUserId is null`() {
|
||||
every { authRepository.activeUserId } returns null
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(createMockFido2CredentialRequest(number = 1))
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should register FIDO 2 credential when registration result is received`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockRequest,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockResponse",
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
),
|
||||
)
|
||||
|
||||
coVerify {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = DEFAULT_ACCOUNT.userId,
|
||||
fido2CredentialRequest = mockRequest,
|
||||
selectedCipherView = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserVerificationNotSupported should display Fido2ErrorDialog`() {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationNotSupported)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.DialogState.Fido2CreationFail(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialogState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType: VaultItemListingType,
|
||||
|
@ -1967,6 +2320,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
shouldFinishOnComplete = false,
|
||||
policyDisablesSend = false,
|
||||
hasMasterPassword = true,
|
||||
fido2CredentialRequest = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue