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 add edit item (#3450)
This commit is contained in:
parent
b0ff0b9185
commit
291af8d017
5 changed files with 930 additions and 150 deletions
|
@ -39,10 +39,12 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
|
|||
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.LocalExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
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.permissions.PermissionsManager
|
||||
|
@ -52,6 +54,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
|
|||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers
|
||||
|
||||
/**
|
||||
* Top level composable for the vault add item screen.
|
||||
|
@ -67,6 +70,7 @@ fun VaultAddEditScreen(
|
|||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
fido2CompletionManager: Fido2CompletionManager = LocalFido2CompletionManager.current,
|
||||
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
|
||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
|
||||
onNavigateToAttachments: (cipherId: String) -> Unit,
|
||||
|
@ -75,6 +79,9 @@ fun VaultAddEditScreen(
|
|||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val userVerificationHandlers = remember(viewModel) {
|
||||
VaultAddEditUserVerificationHandlers.create(viewModel = viewModel)
|
||||
}
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
|
@ -115,6 +122,16 @@ fun VaultAddEditScreen(
|
|||
is VaultAddEditEvent.CompleteFido2Registration -> {
|
||||
fido2CompletionManager.completeFido2Registration(event.result)
|
||||
}
|
||||
|
||||
is VaultAddEditEvent.Fido2UserVerification -> {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = userVerificationHandlers.onUserVerificationSuccess,
|
||||
onCancel = userVerificationHandlers.onUserVerificationCancelled,
|
||||
onError = userVerificationHandlers.onUserVerificationFail,
|
||||
onLockOut = userVerificationHandlers.onUserVerificationLockOut,
|
||||
onNotSupported = userVerificationHandlers.onUserVerificationNotSupported,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,6 +165,9 @@ fun VaultAddEditScreen(
|
|||
onAutofillDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultAddEditAction.Common.InitialAutofillDialogDismissed) }
|
||||
},
|
||||
onFido2ErrorDismiss = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultAddEditAction.Common.Fido2ErrorDialogDismissed) }
|
||||
},
|
||||
)
|
||||
|
||||
if (pendingDeleteCipher) {
|
||||
|
@ -281,6 +301,7 @@ private fun VaultAddEditItemDialogs(
|
|||
dialogState: VaultAddEditState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onAutofillDismissRequest: () -> Unit,
|
||||
onFido2ErrorDismiss: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is VaultAddEditState.DialogState.Loading -> {
|
||||
|
@ -309,6 +330,16 @@ private fun VaultAddEditItemDialogs(
|
|||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.DialogState.Fido2Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = onFido2ErrorDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.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
|
||||
|
@ -254,6 +255,30 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
is VaultAddEditAction.Common.HiddenFieldVisibilityChange -> {
|
||||
handleHiddenFieldVisibilityChange(action)
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.UserVerificationSuccess -> {
|
||||
handleUserVerificationSuccess()
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.UserVerificationLockOut -> {
|
||||
handleUserVerificationLockOut()
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.UserVerificationFail -> {
|
||||
handleUserVerificationFail()
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.UserVerificationCancelled -> {
|
||||
handleUserVerificationCancelled()
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.Fido2ErrorDialogDismissed -> {
|
||||
handleFido2ErrorDialogDismissed()
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.UserVerificationNotSupported -> {
|
||||
handleUserVerificationNotSupported()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,21 +398,49 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
request: Fido2CredentialRequest,
|
||||
content: VaultAddEditState.ViewState.Content,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val activeUserId = authRepository.activeUserId
|
||||
val createOptions = fido2CredentialManager
|
||||
.getPasskeyCreateOptionsOrNull(request.requestJson)
|
||||
?: run {
|
||||
sendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
result = Fido2RegisterCredentialResult.Error,
|
||||
),
|
||||
)
|
||||
showFido2ErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
when (createOptions.authenticatorSelection.userVerification) {
|
||||
UserVerificationRequirement.DISCOURAGED -> {
|
||||
registerFido2CredentialToCipher(request, content.toCipherView())
|
||||
}
|
||||
|
||||
UserVerificationRequirement.PREFERRED -> {
|
||||
sendEvent(VaultAddEditEvent.Fido2UserVerification(isRequired = false))
|
||||
}
|
||||
|
||||
UserVerificationRequirement.REQUIRED -> {
|
||||
sendEvent(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val userId = authRepository.activeUserId
|
||||
?: run {
|
||||
showFido2ErrorDialog()
|
||||
return@launch
|
||||
}
|
||||
val result: Fido2RegisterCredentialResult =
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = activeUserId,
|
||||
userId = userId,
|
||||
fido2CredentialRequest = request,
|
||||
selectedCipherView = content.toCipherView(),
|
||||
selectedCipherView = cipherView,
|
||||
)
|
||||
sendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(result),
|
||||
|
@ -460,6 +513,56 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleUserVerificationLockOut() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleUserVerificationSuccess() {
|
||||
specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2RequestOrNull()
|
||||
?.let { request ->
|
||||
onContent { content ->
|
||||
registerFido2CredentialToCipher(
|
||||
request = request,
|
||||
cipherView = content.toCipherView(),
|
||||
)
|
||||
}
|
||||
}
|
||||
?: showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleUserVerificationFail() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleFido2ErrorDialogDismissed() {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationCancelled() {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationNotSupported() {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleAddNewCustomFieldClick(
|
||||
action: VaultAddEditAction.Common.AddNewCustomFieldClick,
|
||||
) {
|
||||
|
@ -1376,6 +1479,17 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
private fun showFido2ErrorDialog() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGenericErrorDialog(
|
||||
message: Text = R.string.generic_error_message.asText(),
|
||||
) {
|
||||
|
@ -1922,6 +2036,12 @@ data class VaultAddEditState(
|
|||
*/
|
||||
@Parcelize
|
||||
data object InitialAutofillPrompt : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a FIDO 2 operation error dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2Error(val message: Text) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1992,11 +2112,20 @@ sealed class VaultAddEditEvent {
|
|||
/**
|
||||
* 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,
|
||||
) : VaultAddEditEvent()
|
||||
|
||||
/**
|
||||
* Perform user verification for a FIDO 2 credential operation.
|
||||
*
|
||||
* @param isRequired When `false`, user verification can be bypassed if it is not supported.
|
||||
*/
|
||||
data class Fido2UserVerification(
|
||||
val isRequired: Boolean,
|
||||
) : VaultAddEditEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2143,6 +2272,37 @@ sealed class VaultAddEditAction {
|
|||
* @property isVisible the new visibility state of the hidden field.
|
||||
*/
|
||||
data class HiddenFieldVisibilityChange(val isVisible: Boolean) : Common()
|
||||
|
||||
/**
|
||||
* The user has too many failed verification attempts for FIDO operations and can no longer
|
||||
* use biometric or device credential verification for some time.
|
||||
*/
|
||||
data object UserVerificationLockOut : Common()
|
||||
|
||||
/**
|
||||
* The user has failed verification for FIDO 2 operations.
|
||||
*/
|
||||
data object UserVerificationFail : Common()
|
||||
|
||||
/**
|
||||
* The user has successfully verified themself using device biometrics or credentials.
|
||||
*/
|
||||
data object UserVerificationSuccess : Common()
|
||||
|
||||
/**
|
||||
* The user has cancelled device verification.
|
||||
*/
|
||||
data object UserVerificationCancelled : Common()
|
||||
|
||||
/**
|
||||
* The user has dismissed the FIDO 2 credential error dialog.
|
||||
*/
|
||||
data object Fido2ErrorDialogDismissed : Common()
|
||||
|
||||
/**
|
||||
* User verification cannot be performed with device biometrics or credentials.
|
||||
*/
|
||||
data object UserVerificationNotSupported : Common()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions specifically tailored for managing action within the context of
|
||||
* biometric 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.
|
||||
* @property onUserVerificationCancelled Handles the action when verification is explicitly
|
||||
* cancelled by the user.
|
||||
* @property onUserVerificationNotSupported Handles the action when device biometric and credential
|
||||
* verification cannot be performed.
|
||||
*/
|
||||
data class VaultAddEditUserVerificationHandlers(
|
||||
val onUserVerificationSuccess: () -> Unit,
|
||||
val onUserVerificationLockOut: () -> Unit,
|
||||
val onUserVerificationFail: () -> Unit,
|
||||
val onUserVerificationCancelled: () -> Unit,
|
||||
val onUserVerificationNotSupported: () -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates an instance of [VaultAddEditUserVerificationHandlers] by binding actions
|
||||
* to the provided [VaultAddEditViewModel].
|
||||
*/
|
||||
fun create(
|
||||
viewModel: VaultAddEditViewModel,
|
||||
): VaultAddEditUserVerificationHandlers =
|
||||
VaultAddEditUserVerificationHandlers(
|
||||
onUserVerificationSuccess = {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
},
|
||||
onUserVerificationFail = {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail)
|
||||
},
|
||||
onUserVerificationLockOut = {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut)
|
||||
},
|
||||
onUserVerificationCancelled = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.UserVerificationCancelled,
|
||||
)
|
||||
},
|
||||
onUserVerificationNotSupported = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.UserVerificationNotSupported,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ 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
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
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.permissions.FakePermissionManager
|
||||
|
@ -59,6 +60,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection
|
|||
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import io.mockk.every
|
||||
import io.mockk.invoke
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
|
@ -98,6 +100,9 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
private val fido2CompletionManager: Fido2CompletionManager = mockk {
|
||||
every { completeFido2Registration(any()) } just runs
|
||||
}
|
||||
private val biometricsManager: BiometricsManager = mockk {
|
||||
every { isUserVerificationSupported } returns true
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
|
@ -116,6 +121,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
exitManager = exitManager,
|
||||
intentManager = intentManager,
|
||||
fido2CompletionManager = fido2CompletionManager,
|
||||
biometricsManager = biometricsManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +199,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on CompleteFido2Create even should invoke Fido2CompletionManager`() {
|
||||
fun `on CompleteFido2Create event should invoke Fido2CompletionManager`() {
|
||||
val result = Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockRegistrationResponse",
|
||||
)
|
||||
|
@ -201,6 +207,33 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
verify { fido2CompletionManager.completeFido2Registration(result) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2Error dialog should display based on state`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("mockMessage")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking dismiss dialog on Fido2Error dialog should send Fido2ErrorDialogDismissed action`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(VaultAddEditAction.Common.Fido2ErrorDialogDismissed) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close button should update according to state`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").assertIsDisplayed()
|
||||
|
@ -2878,6 +2911,104 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification event should prompt for user verification`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = any(),
|
||||
onCancel = any(),
|
||||
onLockOut = any(),
|
||||
onError = any(),
|
||||
onNotSupported = any(),
|
||||
)
|
||||
} just runs
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.Fido2UserVerification(true))
|
||||
verify {
|
||||
biometricsManager.promptUserVerification(any(), any(), any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification onSuccess should send UserVerificationSuccess action`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = captureLambda(),
|
||||
onCancel = any(),
|
||||
onLockOut = any(),
|
||||
onError = any(),
|
||||
onNotSupported = any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
verify { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification onCancel should send UserVerificationCancelled action`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = any(),
|
||||
onCancel = captureLambda(),
|
||||
onLockOut = any(),
|
||||
onError = any(),
|
||||
onNotSupported = any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
verify { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification onLockout should send UserVerificationLockOut action`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = any(),
|
||||
onCancel = any(),
|
||||
onLockOut = captureLambda(),
|
||||
onError = any(),
|
||||
onNotSupported = any(),
|
||||
)
|
||||
} answers {
|
||||
lambda<() -> Unit>().invoke()
|
||||
}
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
verify { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification onError should send UserVerificationFail action`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = any(),
|
||||
onCancel = any(),
|
||||
onLockOut = any(),
|
||||
onError = captureLambda(),
|
||||
onNotSupported = any(),
|
||||
)
|
||||
} answers { lambda<() -> Unit>().invoke() }
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
verify { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2UserVerification onNotSupported should send UserVerificationNotSupported action`() {
|
||||
every {
|
||||
biometricsManager.promptUserVerification(
|
||||
onSuccess = any(),
|
||||
onCancel = any(),
|
||||
onLockOut = any(),
|
||||
onError = any(),
|
||||
onNotSupported = captureLambda(),
|
||||
)
|
||||
} answers { lambda<() -> Unit>().invoke() }
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.Fido2UserVerification(isRequired = true))
|
||||
verify { viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported) }
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
private fun updateLoginType(
|
||||
|
|
|
@ -15,9 +15,11 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
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.createMockFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
|
@ -684,7 +686,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should show dialog, register credential, show toast once an item is saved, and emit ExitApp`() =
|
||||
fun `in add mode during fido2 registration, SaveClick should show saving dialog, and request user verification when required`() =
|
||||
runTest {
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = "mockUserId",
|
||||
|
@ -697,7 +699,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
val stateWithDialog = createVaultAddItemState(
|
||||
val stateWithSavingDialog = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
dialogState = VaultAddEditState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
|
@ -708,25 +710,32 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
val stateWithName = createVaultAddItemState(
|
||||
val stateWithNewLogin = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
data = createVaultData(),
|
||||
)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
state = stateWithNewLogin,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
val mockCreateResult = Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockRegistrationResponse",
|
||||
)
|
||||
val mockCreateOptions = createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
|
@ -734,8 +743,91 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
} returns mockCreateResult
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns mockCreateOptions
|
||||
every { authRepository.activeUserId } returns "mockUserId"
|
||||
|
||||
turbineScope {
|
||||
val stateTurbine = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
assertEquals(stateWithNewLogin, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithSavingDialog, stateTurbine.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.Fido2UserVerification(isRequired = true),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should show saving dialog, remove it once item is saved, skip user verification when not required, and emit ExitApp`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = mockUserId,
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
origin = null,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
val stateWithSavingDialog = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
dialogState = VaultAddEditState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
val mockCreateResult = Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
val mockCreateOptions = createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.DISCOURAGED,
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = mockUserId,
|
||||
selectedCipherView = any(),
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
} returns mockCreateResult
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns mockCreateOptions
|
||||
every { authRepository.activeUserId } returns mockUserId
|
||||
|
||||
turbineScope {
|
||||
val stateTurbine = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
|
||||
|
@ -743,100 +835,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithDialog, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithSavingDialog, stateTurbine.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.item_updated.asText()),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(result = mockCreateResult),
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockCreateResult),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
selectedCipherView = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should show dialog, register credential, show toast on error, and emit ExitApp when result is Error`() =
|
||||
runTest {
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = "mockUserId",
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
origin = null,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
val stateWithDialog = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
dialogState = VaultAddEditState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
val mockCreateResult = Fido2RegisterCredentialResult.Error
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
selectedCipherView = any(),
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
} returns mockCreateResult
|
||||
every { authRepository.activeUserId } returns "mockUserId"
|
||||
|
||||
turbineScope {
|
||||
val stateTurbine = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithDialog, stateTurbine.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText()),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(result = mockCreateResult),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
userId = mockUserId,
|
||||
selectedCipherView = any(),
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
|
@ -846,10 +857,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should show saving dialog, register credential, dismiss dialog, show toast on error, and emit ExitApp when activeUserId is null`() =
|
||||
fun `in add mode during fido2, SaveClick should show fido2 error dialog when create options are null`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = "mockUserId",
|
||||
userId = mockUserId,
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
|
@ -859,17 +871,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
val stateWithDialog = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
dialogState = VaultAddEditState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
|
@ -877,6 +878,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns null
|
||||
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
|
@ -886,41 +894,121 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
val mockCreateResult = Fido2RegisterCredentialResult.Error
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
selectedCipherView = any(),
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
} returns mockCreateResult
|
||||
every { authRepository.activeUserId } returns null
|
||||
|
||||
turbineScope {
|
||||
val stateTurbine = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithDialog, stateTurbine.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText()),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(result = mockCreateResult),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = "mockUserId",
|
||||
selectedCipherView = any(),
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = mockUserId,
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
origin = null,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
)
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
turbineScope {
|
||||
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.Fido2UserVerification(isRequired = false),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2, SaveClick should emit fido user verification as required when request user verification option is REQUIRED`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
val fido2CredentialRequest = Fido2CredentialRequest(
|
||||
userId = mockUserId,
|
||||
requestJson = "mockRequestJson",
|
||||
packageName = "mockPackageName",
|
||||
signingInfo = mockk<SigningInfo>(),
|
||||
origin = null,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
)
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
name = "mockName-1",
|
||||
),
|
||||
)
|
||||
.copy(shouldExitOnSave = true)
|
||||
|
||||
every {
|
||||
fido2CredentialManager.getPasskeyCreateOptionsOrNull(
|
||||
requestJson = fido2CredentialRequest.requestJson,
|
||||
)
|
||||
} returns createMockPublicKeyCredentialCreationOptions(
|
||||
number = 1,
|
||||
userVerificationRequirement = UserVerificationRequirement.REQUIRED,
|
||||
)
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
turbineScope {
|
||||
val eventTurbine = viewModel.eventFlow.testIn(backgroundScope)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.Fido2UserVerification(isRequired = true),
|
||||
eventTurbine.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1287,6 +1375,34 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 create`() =
|
||||
runTest {
|
||||
val errorState = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
dialogState = VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = errorState,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.Fido2ErrorDialogDismissed)
|
||||
viewModel.eventFlow.test {
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TypeOptionSelect LOGIN should switch to LoginItem`() = runTest {
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
|
@ -2254,15 +2370,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
viewModel = VaultAddEditViewModel(
|
||||
savedStateHandle = secureNotesInitialSavedStateHandle,
|
||||
clipboardManager = clipboardManager,
|
||||
vaultRepository = vaultRepository,
|
||||
generatorRepository = generatorRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
policyManager = policyManager,
|
||||
resourceManager = resourceManager,
|
||||
authRepository = authRepository,
|
||||
clipboardManager = clipboardManager,
|
||||
policyManager = policyManager,
|
||||
vaultRepository = vaultRepository,
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
generatorRepository = generatorRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
organizationEventManager = organizationEventManager,
|
||||
)
|
||||
|
@ -2750,6 +2866,290 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationLockout should display Fido2ErrorDialog`() {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationCancelled should clear dialog state and emit CompleteFido2Create with cancelled result`() =
|
||||
runTest {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled)
|
||||
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Cancelled,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationFail should display Fido2ErrorDialog`() {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@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",
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when Fido2Request is null`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
AutofillSaveItem.Login(
|
||||
username = "mockUsername",
|
||||
password = "mockPassword",
|
||||
uri = "mockUri",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockResponse",
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when activeUserId is null`() {
|
||||
every { authRepository.activeUserId } returns null
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(createMockFido2CredentialRequest(number = 1))
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
message = R.string.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserVerificationSuccess should register FIDO 2 credential`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockResponse",
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockRequest,
|
||||
)
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
coVerify {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
userId = any(),
|
||||
fido2CredentialRequest = mockRequest,
|
||||
selectedCipherView = any(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.item_updated.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationNotSupported should clear dialog state and send CompleteFido2Registration event with Error`() =
|
||||
runTest {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = Fido2RegisterCredentialResult.Error,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Error
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockRequest,
|
||||
)
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
mockResult,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.an_error_has_occurred.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Success(
|
||||
registrationResponse = "mockResponse",
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockRequest,
|
||||
)
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
mockResult,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowToast(R.string.item_updated.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Cancelled should emit CompleteFido2Registration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockFido2CredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Cancelled
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = mockRequest,
|
||||
)
|
||||
every { authRepository.activeUserId } returns "activeUserId"
|
||||
coEvery {
|
||||
fido2CredentialManager.registerFido2Credential(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} returns mockResult
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Internal.Fido2RegisterCredentialResultReceive(
|
||||
mockResult,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(mockResult),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//region Helper functions
|
||||
|
||||
|
@ -2859,15 +3259,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
): VaultAddEditViewModel =
|
||||
VaultAddEditViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
clipboardManager = bitwardenClipboardManager,
|
||||
vaultRepository = vaultRepo,
|
||||
generatorRepository = generatorRepo,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
policyManager = policyManager,
|
||||
resourceManager = bitwardenResourceManager,
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
authRepository = authRepository,
|
||||
clipboardManager = bitwardenClipboardManager,
|
||||
policyManager = policyManager,
|
||||
vaultRepository = vaultRepo,
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
generatorRepository = generatorRepo,
|
||||
settingsRepository = settingsRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
resourceManager = bitwardenResourceManager,
|
||||
clock = clock,
|
||||
organizationEventManager = organizationEventManager,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue