[PM-24148] add credential manager provider for create passwords (#5579)

Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
This commit is contained in:
Nailik 2025-11-19 23:00:53 +01:00 committed by GitHub
parent 839e9e8a1a
commit 5ec0a1986d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 879 additions and 336 deletions

View File

@ -86,6 +86,7 @@
<intent-filter>
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />

View File

@ -58,6 +58,13 @@ interface CredentialManagerPendingIntentManager {
userId: String,
): PendingIntent
/**
* Creates a pending intent to use when providing options for Password credential creation.
*/
fun createPasswordCreationPendingIntent(
userId: String,
): PendingIntent
/**
* Creates a pending intent to use when providing options for Password credential filling.
*/

View File

@ -75,6 +75,24 @@ class CredentialManagerPendingIntentManagerImpl(
)
}
/**
* Creates a pending intent to use when providing options for FIDO 2 credential creation.
*/
override fun createPasswordCreationPendingIntent(
userId: String,
): PendingIntent {
val intent = Intent(CREATE_PASSWORD_ACTION)
.setPackage(context.packageName)
.putExtra(EXTRA_KEY_USER_ID, userId)
return PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ Random.nextInt(),
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
)
}
/**
* Creates a pending intent to use when providing options for Password credential filling.
*/
@ -101,4 +119,5 @@ class CredentialManagerPendingIntentManagerImpl(
private const val CREATE_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY"
private const val UNLOCK_ACCOUNT_ACTION = "com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT"
private const val GET_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY"
private const val CREATE_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD"
private const val GET_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD"

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.model
import android.os.Bundle
import android.os.Parcelable
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest
@ -48,6 +49,15 @@ data class CreateCredentialRequest(
providerRequest.callingRequest as? CreatePublicKeyCredentialRequest
}
/**
* The [CreatePasswordRequest] of the [providerRequest], or null if the calling
* request is not a [CreatePasswordRequest].
*/
@IgnoredOnParcel
val createPasswordCredentialRequest: CreatePasswordRequest? by lazy {
providerRequest.callingRequest as? CreatePasswordRequest
}
/**
* The [requestJson] of the [createPublicKeyCredentialRequest], or null if the calling request
* is not a [CreatePublicKeyCredentialRequest].

View File

@ -19,6 +19,7 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.AuthenticationAction
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
@ -69,7 +70,7 @@ class CredentialProviderProcessorImpl(
}
val createCredentialJob = ioScope.launch {
processCreateCredentialRequest(request = request)
(handleCreatePasskeyQuery(request) ?: handleCreatePasswordQuery(request))
?.let { callback.onResult(it) }
?: callback.onError(CreateCredentialUnknownException())
}
@ -137,21 +138,11 @@ class CredentialProviderProcessorImpl(
callback.onError(ClearCredentialUnsupportedException())
}
private fun processCreateCredentialRequest(
private fun handleCreatePasskeyQuery(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
return when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}
if (request !is BeginCreatePublicKeyCredentialRequest) return null
else -> null
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request
.candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
@ -161,14 +152,19 @@ class CredentialProviderProcessorImpl(
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
.setCreateEntries(
userState.accounts.toCreatePasskeyEntry(userState.activeUserId),
)
.build()
}
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
private fun List<UserState.Account>.toCreatePasskeyEntry(
activeUserId: String,
): List<CreateEntry> = map { it.toCreatePasskeyEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
private fun UserState.Account.toCreatePasskeyEntry(
isActive: Boolean,
): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
@ -196,6 +192,54 @@ class CredentialProviderProcessorImpl(
return entryBuilder.build()
}
private fun handleCreatePasswordQuery(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
if (request !is BeginCreatePasswordCredentialRequest) return null
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(
userState.accounts.toCreatePasswordEntry(userState.activeUserId),
)
.build()
}
private fun List<UserState.Account>.toCreatePasswordEntry(
activeUserId: String,
) = map { it.toCreatePasswordEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreatePasswordEntry(
isActive: Boolean,
): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
accountName = accountName,
pendingIntent = pendingIntentManager.createPasswordCreationPendingIntent(
userId = userId,
),
)
.setDescription(
context.getString(
BitwardenString.your_password_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.setAutoSelectAllowed(true)
if (isVaultUnlocked) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
return entryBuilder.build()
}
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): CreateEntry.Builder {

View File

@ -1,19 +1,19 @@
package com.x8bit.bitwarden.ui.credentials.manager
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
/**
* A manager for completing the FIDO 2 creation process.
* A manager for completing the credential creation process.
*/
interface CredentialProviderCompletionManager {
/**
* Completes the FIDO 2 registration process with the provided [result].
* Completes the credential registration process with the provided [result].
*/
fun completeFido2Registration(result: RegisterFido2CredentialResult)
fun completeCredentialRegistration(result: CreateCredentialResult)
/**
* Complete the FIDO 2 credential assertion process with the provided [result].

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential
@ -15,9 +16,9 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.PendingIntentHandler
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
/**
* Primary implementation of [CredentialProviderCompletionManager] when the build version is
@ -28,11 +29,11 @@ class CredentialProviderCompletionManagerImpl(
private val activity: Activity,
) : CredentialProviderCompletionManager {
override fun completeFido2Registration(result: RegisterFido2CredentialResult) {
override fun completeCredentialRegistration(result: CreateCredentialResult) {
activity.also {
val intent = Intent()
when (result) {
is RegisterFido2CredentialResult.Error -> {
is CreateCredentialResult.Error -> {
PendingIntentHandler
.setCreateCredentialException(
intent = intent,
@ -42,7 +43,7 @@ class CredentialProviderCompletionManagerImpl(
)
}
is RegisterFido2CredentialResult.Success -> {
is CreateCredentialResult.Success.Fido2CredentialRegistered -> {
PendingIntentHandler
.setCreateCredentialResponse(
intent = intent,
@ -52,7 +53,15 @@ class CredentialProviderCompletionManagerImpl(
)
}
is RegisterFido2CredentialResult.Cancelled -> {
is CreateCredentialResult.Success.PasswordCreated -> {
PendingIntentHandler
.setCreateCredentialResponse(
intent = intent,
response = CreatePasswordResponse(),
)
}
is CreateCredentialResult.Cancelled -> {
PendingIntentHandler
.setCreateCredentialException(
intent = intent,

View File

@ -3,9 +3,9 @@ package com.x8bit.bitwarden.ui.credentials.manager
import androidx.credentials.CredentialProvider
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
/**
* A no-op implementation of [CredentialProviderCompletionManagerImpl] provided when the build
@ -13,7 +13,7 @@ import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialR
*/
@OmitFromCoverage
object CredentialProviderCompletionManagerUnsupportedApiImpl : CredentialProviderCompletionManager {
override fun completeFido2Registration(result: RegisterFido2CredentialResult) = Unit
override fun completeCredentialRegistration(result: CreateCredentialResult) = Unit
override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit

View File

@ -0,0 +1,34 @@
package com.x8bit.bitwarden.ui.credentials.manager.model
import com.bitwarden.ui.util.Text
/**
* Represents the result of a credential creation attempt.
*/
sealed class CreateCredentialResult {
/**
* Represents a successful credential creation attempt.
*/
sealed class Success : CreateCredentialResult() {
/**
* Indicates that the FIDO2 registration was successful.
*/
data class Fido2CredentialRegistered(val responseJson: String) : Success()
/**
* Indicates that the Password creation was successful.
*/
data object PasswordCreated : Success()
}
/**
* Indicates that an error occurred during credential creation.
*/
data class Error(val message: Text) : CreateCredentialResult()
/**
* Indicates that credential creation was cancelled by the user.
*/
data object Cancelled : CreateCredentialResult()
}

View File

@ -1,23 +0,0 @@
package com.x8bit.bitwarden.ui.credentials.manager.model
import com.bitwarden.ui.util.Text
/**
* Represents the result of a FIDO2 credential registration attempt.
*/
sealed class RegisterFido2CredentialResult {
/**
* Indicates that the registration was successful.
*/
data class Success(val responseJson: String) : RegisterFido2CredentialResult()
/**
* Indicates that an error occurred during registration.
*/
data class Error(val message: Text) : RegisterFido2CredentialResult()
/**
* Indicates that the registration was cancelled by the user.
*/
data object Cancelled : RegisterFido2CredentialResult()
}

View File

@ -6,21 +6,22 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* A reusable dialog for confirming whether or not the user wants to overwrite an existing FIDO 2
* credential.
* A reusable dialog for confirming whether or not the user wants to overwrite an existing credential.
*
* @param onConfirmClick A callback for when the overwrite confirmation button is clicked.
* @param onDismissRequest A callback for when the dialog is requesting dismissal.
*/
@Suppress("MaxLineLength")
@Composable
fun BitwardenOverwritePasskeyConfirmationDialog(
fun BitwardenOverwriteCredentialConfirmationDialog(
title: String,
message: String,
onConfirmClick: () -> Unit,
onDismissRequest: () -> Unit,
) {
BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.overwrite_passkey),
message = stringResource(id = BitwardenString.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey),
title = title,
message = message,
confirmButtonText = stringResource(id = BitwardenString.okay),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onConfirmClick,

View File

@ -71,6 +71,7 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.navigateToExportItemsGra
import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.navigateToVerifyPassword
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import java.util.concurrent.atomic.AtomicReference
@ -141,6 +142,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForFido2Assertion,
is RootNavState.VaultUnlockedForPasswordGet,
is RootNavState.VaultUnlockedForProviderGetCredentials,
is RootNavState.VaultUnlockedForCreatePasswordRequest,
-> VaultUnlockedGraphRoute
is RootNavState.CredentialExchangeExport,
@ -245,6 +247,17 @@ fun RootNavScreen(
)
}
is RootNavState.VaultUnlockedForCreatePasswordRequest -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultAddEdit(
args = VaultAddEditArgs(
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
navOptions = rootNavOptions,
)
}
is RootNavState.VaultUnlockedForAutofillSelection -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultItemListingAsRoot(

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.util.parseJwtTokenDataOrNull
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.util.toAndroidAppUriString
import com.bitwarden.ui.platform.manager.share.model.ShareData
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -144,11 +145,31 @@ class RootNavViewModel @Inject constructor(
}
is SpecialCircumstance.ProviderCreateCredential -> {
RootNavState.VaultUnlockedForFido2Save(
activeUserId = userState.activeUserId,
createCredentialRequest =
specialCircumstance.createCredentialRequest,
)
val request = specialCircumstance.createCredentialRequest
val publicKeyRequest = request.createPublicKeyCredentialRequest
val passwordRequest = request.createPasswordCredentialRequest
when {
publicKeyRequest != null -> {
RootNavState.VaultUnlockedForFido2Save(
activeUserId = userState.activeUserId,
createCredentialRequest = request,
)
}
passwordRequest != null -> {
RootNavState.VaultUnlockedForCreatePasswordRequest(
username = passwordRequest.id,
password = passwordRequest.password,
uri = request
.callingAppInfo
.packageName
.toAndroidAppUriString(),
)
}
else -> throw IllegalStateException("Should not have entered here.")
}
}
is SpecialCircumstance.Fido2Assertion -> {
@ -336,6 +357,21 @@ sealed class RootNavState : Parcelable {
val createCredentialRequest: CreateCredentialRequest,
) : RootNavState()
/**
* App should show an add item screen for a user to complete the saving of data collected by
* the credential manager framework.
*
* @param username The username of the user.
* @param password The password of the user.
* @param uri The URI to associate this credential with.
*/
@Parcelize
data class VaultUnlockedForCreatePasswordRequest(
val username: String,
val password: String,
val uri: String,
) : RootNavState()
/**
* App should perform FIDO 2 credential assertion for the user.
*/

View File

@ -71,7 +71,7 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.Text
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwriteCredentialConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
@ -161,8 +161,8 @@ fun VaultAddEditScreen(
)
}
is VaultAddEditEvent.CompleteFido2Registration -> {
credentialProviderCompletionManager.completeFido2Registration(
is VaultAddEditEvent.CompleteCredentialRegistration -> {
credentialProviderCompletionManager.completeCredentialRegistration(
result = event.result,
)
}
@ -227,10 +227,14 @@ fun VaultAddEditScreen(
onAutofillDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.InitialAutofillDialogDismissed) }
},
onFido2ErrorDismiss = remember(viewModel) {
onCredentialErrorDismiss = remember(viewModel) {
{ errorMessage ->
viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(message = errorMessage),
VaultAddEditAction
.Common
.CredentialErrorDialogDismissed(
message = errorMessage,
),
)
}
},
@ -463,7 +467,7 @@ private fun VaultAddEditItemDialogs(
dialogState: VaultAddEditState.DialogState?,
onDismissRequest: () -> Unit,
onAutofillDismissRequest: () -> Unit,
onFido2ErrorDismiss: (Text) -> Unit,
onCredentialErrorDismiss: (Text) -> Unit,
onConfirmOverwriteExistingPasskey: () -> Unit,
onSubmitMasterPasswordFido2Verification: (password: String) -> Unit,
onRetryFido2PasswordVerification: () -> Unit,
@ -495,16 +499,22 @@ private fun VaultAddEditItemDialogs(
)
}
is VaultAddEditState.DialogState.Fido2Error -> {
is VaultAddEditState.DialogState.CredentialError -> {
BitwardenBasicDialog(
title = stringResource(id = BitwardenString.an_error_has_occurred),
message = dialogState.message(),
onDismissRequest = { onFido2ErrorDismiss(dialogState.message) },
onDismissRequest = { onCredentialErrorDismiss(dialogState.message) },
)
}
is VaultAddEditState.DialogState.OverwritePasskeyConfirmationPrompt -> {
BitwardenOverwritePasskeyConfirmationDialog(
@Suppress("MaxLineLength")
BitwardenOverwriteCredentialConfirmationDialog(
title = stringResource(id = BitwardenString.overwrite_passkey),
message = stringResource(
id = BitwardenString
.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey,
),
onConfirmClick = onConfirmOverwriteExistingPasskey,
onDismissRequest = onDismissRequest,
)

View File

@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
import android.os.Parcelable
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.DateTime
@ -34,7 +33,6 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.credentials.util.getCreatePasskeyCredentialRequestOrNull
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@ -59,7 +57,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
@ -150,7 +148,7 @@ class VaultAddEditViewModel @Inject constructor(
val autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull()
// Check for totp data to pre-populate
val totpData = specialCircumstance?.toTotpDataOrNull()
// Check for Fido2 data to pre-populate
// Check for Fido2 or Password credential data to pre-populate
val providerCreateCredentialRequest =
specialCircumstance?.toCreateCredentialRequestOrNull()
val fido2AttestationOptions = providerCreateCredentialRequest?.requestJson
@ -348,8 +346,8 @@ class VaultAddEditViewModel @Inject constructor(
handleUserVerificationCancelled()
}
is VaultAddEditAction.Common.Fido2ErrorDialogDismissed -> {
handleFido2ErrorDialogDismissed(action)
is VaultAddEditAction.Common.CredentialErrorDialogDismissed -> {
handleCredentialErrorDialogDismissed(action)
}
VaultAddEditAction.Common.UserVerificationNotSupported -> {
@ -401,31 +399,7 @@ class VaultAddEditViewModel @Inject constructor(
@Suppress("LongMethod")
private fun handleSaveClick() = onContent { content ->
if (content.common.name.isBlank()) {
showGenericErrorDialog(
message = BitwardenString.validation_field_required
.asText(BitwardenString.name.asText()),
)
return@onContent
} else if (
content.common.selectedOwnerId != null &&
content.common.selectedOwner?.collections?.all { !it.isSelected } == true
) {
showGenericErrorDialog(
message = BitwardenString.select_one_collection.asText(),
)
return@onContent
} else if (
!networkConnectionManager.isNetworkConnected
) {
showDialog(
dialogState = VaultAddEditState.DialogState.Generic(
title = BitwardenString.internet_connection_required_title.asText(),
message = BitwardenString.internet_connection_required_message.asText(),
),
)
return@onContent
}
if (hasValidationErrors(content)) return@onContent
mutableStateFlow.update {
it.copy(
@ -435,14 +409,17 @@ class VaultAddEditViewModel @Inject constructor(
)
}
state.createCredentialRequest
?.let { request ->
handleProviderCreateCredentialRequest(
request.providerRequest,
content.toCipherView(),
)
return@onContent
}
state.createCredentialRequest?.run {
createPublicKeyCredentialRequest
?.let { createPublicKeyCredentialRequest ->
handleCreatePublicKeyCredentialRequest(
request = createPublicKeyCredentialRequest,
callingAppInfo = this.callingAppInfo,
cipherView = content.toCipherView(),
)
return@onContent
}
}
viewModelScope.launch {
when (val vaultAddEditType = state.vaultAddEditType) {
@ -467,15 +444,34 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleProviderCreateCredentialRequest(
request: ProviderCreateCredentialRequest,
cipherView: CipherView,
) {
request
.getCreatePasskeyCredentialRequestOrNull()
?.let { handleCreatePublicKeyCredentialRequest(request.callingAppInfo, it, cipherView) }
?: run { handleUnsupportedProviderCreateCredentialRequest() }
}
private fun hasValidationErrors(content: VaultAddEditState.ViewState.Content): Boolean =
if (content.common.name.isBlank()) {
showGenericErrorDialog(
message = BitwardenString.validation_field_required
.asText(BitwardenString.name.asText()),
)
true
} else if (
content.common.selectedOwnerId != null &&
content.common.selectedOwner?.collections?.all { !it.isSelected } == true
) {
showGenericErrorDialog(
message = BitwardenString.select_one_collection.asText(),
)
true
} else if (
!networkConnectionManager.isNetworkConnected
) {
showDialog(
dialogState = VaultAddEditState.DialogState.Generic(
title = BitwardenString.internet_connection_required_title.asText(),
message = BitwardenString.internet_connection_required_message.asText(),
),
)
true
} else {
false
}
private fun handleCreatePublicKeyCredentialRequest(
callingAppInfo: CallingAppInfo,
@ -520,7 +516,7 @@ class VaultAddEditViewModel @Inject constructor(
viewModelScope.launch {
val userId = authRepository.activeUserId
?: run {
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
)
@ -539,12 +535,6 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleUnsupportedProviderCreateCredentialRequest() {
showFido2ErrorDialog(
BitwardenString.passkey_operation_failed_because_the_request_is_unsupported.asText(),
)
}
private fun handleAttachmentsClick() {
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
}
@ -613,63 +603,69 @@ class VaultAddEditViewModel @Inject constructor(
private fun handleConfirmOverwriteExistingPasskeyClick() {
state
.createCredentialRequest
?.providerRequest
?.let { request ->
onContent { content ->
handleProviderCreateCredentialRequest(
request,
content.toCipherView(),
)
}
request.createPublicKeyCredentialRequest
?.let { createPublicKeyCredentialRequest ->
onContent { content ->
handleCreatePublicKeyCredentialRequest(
request = createPublicKeyCredentialRequest,
callingAppInfo = request.callingAppInfo,
cipherView = content.toCipherView(),
)
}
}
}
?: showFido2ErrorDialog(
?: showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_the_request_is_invalid.asText(),
)
}
private fun handleUserVerificationLockOut() {
bitwardenCredentialManager.isUserVerified = false
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
}
private fun handleUserVerificationSuccess() {
bitwardenCredentialManager.isUserVerified = true
getRequestAndRegisterCredential()
getRequestAndRegisterFido2Credential()
}
private fun getRequestAndRegisterCredential() =
private fun getRequestAndRegisterFido2Credential() =
state.createCredentialRequest
?.providerRequest
?.let { request ->
onContent { content ->
handleProviderCreateCredentialRequest(
request = request,
cipherView = content.toCipherView(),
)
}
request.createPublicKeyCredentialRequest
?.let { createPublicKeyCredentialRequest ->
onContent { content ->
handleCreatePublicKeyCredentialRequest(
request = createPublicKeyCredentialRequest,
callingAppInfo = request.callingAppInfo,
cipherView = content.toCipherView(),
)
}
}
}
?: showFido2ErrorDialog(
?: showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
.asText(),
)
private fun handleUserVerificationFail() {
bitwardenCredentialManager.isUserVerified = false
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
)
}
private fun handleFido2ErrorDialogDismissed(
action: VaultAddEditAction.Common.Fido2ErrorDialogDismissed,
private fun handleCredentialErrorDialogDismissed(
action: VaultAddEditAction.Common.CredentialErrorDialogDismissed,
) {
bitwardenCredentialManager.isUserVerified = false
clearDialogState()
sendEvent(
VaultAddEditEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Error(action.message),
VaultAddEditEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Error(action.message),
),
)
}
@ -678,8 +674,8 @@ class VaultAddEditViewModel @Inject constructor(
bitwardenCredentialManager.isUserVerified = false
clearDialogState()
sendEvent(
VaultAddEditEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Cancelled,
VaultAddEditEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Cancelled,
),
)
}
@ -692,7 +688,7 @@ class VaultAddEditViewModel @Inject constructor(
.value
?.activeAccount
?: run {
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
)
@ -780,7 +776,7 @@ class VaultAddEditViewModel @Inject constructor(
}
private fun handleDismissFido2VerificationDialogClick() {
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
)
@ -1657,7 +1653,13 @@ class VaultAddEditViewModel @Inject constructor(
if (state.shouldClearSpecialCircumstance) {
specialCircumstanceManager.specialCircumstance = null
}
if (state.shouldExitOnSave) {
if (state.createCredentialRequest?.createPasswordCredentialRequest != null) {
sendEvent(
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.PasswordCreated,
),
)
} else if (state.shouldExitOnSave) {
sendEvent(event = VaultAddEditEvent.ExitApp)
} else {
snackbarRelayManager.sendSnackbarData(
@ -1970,8 +1972,8 @@ class VaultAddEditViewModel @Inject constructor(
// Use toast here because we are closing the activity.
toastManager.show(BitwardenString.an_error_has_occurred)
sendEvent(
VaultAddEditEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Error(
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Error(
action.result.messageResourceId.asText(),
),
),
@ -1982,8 +1984,10 @@ class VaultAddEditViewModel @Inject constructor(
// Use toast here because we are closing the activity.
toastManager.show(BitwardenString.item_updated)
sendEvent(
VaultAddEditEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Success(action.result.responseJson),
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = action.result.responseJson,
),
),
)
}
@ -1997,7 +2001,7 @@ class VaultAddEditViewModel @Inject constructor(
when (action.result) {
is ValidatePasswordResult.Error -> {
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
)
@ -2022,7 +2026,7 @@ class VaultAddEditViewModel @Inject constructor(
when (action.result) {
is ValidatePinResult.Error -> {
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
)
@ -2047,7 +2051,7 @@ class VaultAddEditViewModel @Inject constructor(
it.copy(dialog = errorDialogState)
}
} else {
showFido2ErrorDialog(
showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
)
@ -2058,7 +2062,7 @@ class VaultAddEditViewModel @Inject constructor(
bitwardenCredentialManager.isUserVerified = true
bitwardenCredentialManager.authenticationAttempts = 0
getRequestAndRegisterCredential()
getRequestAndRegisterFido2Credential()
}
private fun handleAuthenticatorHelpToolTipClick() {
@ -2072,10 +2076,10 @@ class VaultAddEditViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = null) }
}
private fun showFido2ErrorDialog(message: Text) {
private fun showCredentialErrorDialog(message: Text) {
mutableStateFlow.update {
it.copy(
dialog = VaultAddEditState.DialogState.Fido2Error(message),
dialog = VaultAddEditState.DialogState.CredentialError(message),
)
}
}
@ -2754,10 +2758,10 @@ data class VaultAddEditState(
data object InitialAutofillPrompt : DialogState()
/**
* Displays a FIDO 2 operation error dialog to the user.
* Displays a credential operation error dialog to the user.
*/
@Parcelize
data class Fido2Error(val message: Text) : DialogState()
data class CredentialError(val message: Text) : DialogState()
/**
* Displays the overwrite passkey confirmation prompt to the user.
@ -2888,12 +2892,12 @@ sealed class VaultAddEditEvent {
) : VaultAddEditEvent()
/**
* Complete the current FIDO 2 credential registration process.
* Complete the current credential registration process.
*
* @property result the result of FIDO 2 credential registration.
*/
data class CompleteFido2Registration(
val result: RegisterFido2CredentialResult,
data class CompleteCredentialRegistration(
val result: CreateCredentialResult,
) : BackgroundEvent, VaultAddEditEvent()
/**
@ -3083,9 +3087,9 @@ sealed class VaultAddEditAction {
data object UserVerificationCancelled : Common()
/**
* The user has dismissed the FIDO 2 credential error dialog.
* The user has dismissed the credential error dialog.
*/
data class Fido2ErrorDialogDismissed(val message: Text) : Common()
data class CredentialErrorDialogDismissed(val message: Text) : Common()
/**
* User verification cannot be performed with device biometrics or credentials.

View File

@ -10,7 +10,7 @@ import java.util.UUID
/**
* Returns pre-filled content that may be used for an "add" type
* [VaultAddEditState.ViewState.Content] during FIDO 2 credential creation.
* [VaultAddEditState.ViewState.Content] during FIDO 2 or Password credential creation.
*/
fun CreateCredentialRequest.toDefaultAddTypeContent(
attestationOptions: PasskeyAttestationOptions?,
@ -26,11 +26,17 @@ fun CreateCredentialRequest.toDefaultAddTypeContent(
val rpName = attestationOptions
?.relyingParty
?.name
.orEmpty()
?: callingAppInfo.packageName
val username = attestationOptions
?.user
?.name
?: createPasswordCredentialRequest
?.id
.orEmpty()
val password = createPasswordCredentialRequest
?.password
.orEmpty()
return VaultAddEditState.ViewState.Content(
@ -40,6 +46,7 @@ fun CreateCredentialRequest.toDefaultAddTypeContent(
isIndividualVaultDisabled = isIndividualVaultDisabled,
type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = username,
password = password,
uriList = listOf(
UriItem(
id = UUID.randomUUID().toString(),

View File

@ -50,7 +50,7 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwriteCredentialConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
@ -179,8 +179,8 @@ fun VaultItemListingScreen(
onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId))
}
is VaultItemListingEvent.CompleteFido2Registration -> {
credentialProviderCompletionManager.completeFido2Registration(event.result)
is VaultItemListingEvent.CompleteCredentialRegistration -> {
credentialProviderCompletionManager.completeCredentialRegistration(event.result)
}
is VaultItemListingEvent.CredentialManagerUserVerification -> {
@ -392,7 +392,13 @@ private fun VaultItemListingDialogs(
)
is VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt -> {
BitwardenOverwritePasskeyConfirmationDialog(
@Suppress("MaxLineLength")
BitwardenOverwriteCredentialConfirmationDialog(
title = stringResource(id = BitwardenString.overwrite_passkey),
message = stringResource(
id = BitwardenString
.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey,
),
onConfirmClick = { onConfirmOverwriteExistingPasskey(dialogState.cipherViewId) },
onDismissRequest = onDismissRequest,
)

View File

@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.CredentialEntry
@ -77,9 +79,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
@ -168,9 +170,11 @@ class VaultItemListingViewModel @Inject constructor(
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
dialogState = providerCreateCredentialRequest?.let {
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText())
},
dialogState = providerCreateCredentialRequest
?.createPublicKeyCredentialRequest
?.let {
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText())
},
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
@ -426,8 +430,8 @@ class VaultItemListingViewModel @Inject constructor(
state.createCredentialRequest
?.let {
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Cancelled,
VaultItemListingEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Cancelled,
),
)
}
@ -973,7 +977,7 @@ class VaultItemListingViewModel @Inject constructor(
createCredentialRequest
.providerRequest
.getCreatePasskeyCredentialRequestOrNull()
?.let { createPasskeyCredentialRequest ->
?.let {
handleItemClickForCreatePublicKeyCredentialRequest(
cipherId = action.id,
cipherView = cipherView,
@ -984,7 +988,7 @@ class VaultItemListingViewModel @Inject constructor(
VaultItemListingsAction.Internal.CredentialOperationFailureReceive(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString
.passkey_operation_failed_because_the_request_is_unsupported
.credential_operation_failed_because_the_request_is_unsupported
.asText(),
error = null,
),
@ -1085,6 +1089,27 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun registerCredentialToCipher(
cipherView: CipherView,
providerRequest: ProviderCreateCredentialRequest,
) {
when (providerRequest.callingRequest) {
is CreatePublicKeyCredentialRequest -> {
registerFido2CredentialToCipher(
cipherView = cipherView,
providerRequest = providerRequest,
)
}
else -> {
showCredentialManagerErrorDialog(
BitwardenString.credential_operation_failed_because_the_request_is_invalid
.asText(),
)
}
}
}
private fun registerFido2CredentialToCipher(
cipherView: CipherView,
providerRequest: ProviderCreateCredentialRequest,
@ -1326,8 +1351,8 @@ class VaultItemListingViewModel @Inject constructor(
when {
state.createCredentialRequest != null -> {
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Error(action.message),
VaultItemListingEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Error(action.message),
),
)
}
@ -1530,7 +1555,7 @@ class VaultItemListingViewModel @Inject constructor(
}
is VaultItemListingsAction.Internal.CreateCredentialRequestReceive -> {
handleRegisterFido2CredentialRequestReceive(action)
handleRegisterCredentialRequestReceive(action)
}
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
@ -1900,7 +1925,7 @@ class VaultItemListingViewModel @Inject constructor(
state.createCredentialRequest
?.providerRequest
?.let { request ->
registerFido2CredentialToCipher(
registerCredentialToCipher(
cipherView = cipherView,
providerRequest = request,
)
@ -2012,6 +2037,32 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun handleRegisterCredentialRequestReceive(
action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive,
) {
when (action.request.providerRequest.callingRequest) {
is CreatePublicKeyCredentialRequest -> {
handleRegisterFido2CredentialRequestReceive(action)
}
is CreatePasswordRequest -> {
observeVaultData()
}
else -> {
mutableStateFlow.update {
it.copy(
dialogState =
VaultItemListingState.DialogState.CredentialManagerOperationFail(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.generic_error_message.asText(),
),
)
}
}
}
}
private fun handleRegisterFido2CredentialRequestReceive(
action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive,
) {
@ -2062,8 +2113,10 @@ class VaultItemListingViewModel @Inject constructor(
// user to have time to see the message.
toastManager.show(messageId = BitwardenString.item_updated)
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Success(action.result.responseJson),
VaultItemListingEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = action.result.responseJson,
),
),
)
}
@ -2077,8 +2130,8 @@ class VaultItemListingViewModel @Inject constructor(
// user to have time to see the message.
toastManager.show(messageId = BitwardenString.an_error_has_occurred)
sendEvent(
VaultItemListingEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Error(
VaultItemListingEvent.CompleteCredentialRegistration(
CreateCredentialResult.Error(
message = error.messageResourceId.asText(),
),
),
@ -3240,12 +3293,12 @@ sealed class VaultItemListingEvent {
}
/**
* Complete the current FIDO 2 credential registration process.
* Complete the current credential registration process.
*
* @property result The result of FIDO 2 credential registration.
* @property result The result of the credential registration.
*/
data class CompleteFido2Registration(
val result: RegisterFido2CredentialResult,
data class CompleteCredentialRegistration(
val result: CreateCredentialResult,
) : BackgroundEvent, VaultItemListingEvent()
/**

View File

@ -11,7 +11,7 @@ fun createMockCreateCredentialRequest(
isUserPreVerified: Boolean = false,
requestData: Bundle = bundleOf(),
): CreateCredentialRequest = CreateCredentialRequest(
userId = "mockUserId-$number",
userId = "mockUserIdx-$number",
isUserPreVerified = isUserPreVerified,
requestData = requestData,
)

View File

@ -125,7 +125,7 @@ class CredentialProviderProcessorTest {
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with error on password create request`() {
fun `processCreateCredentialRequest should invoke callback with error on password create request when userState is null`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
@ -148,6 +148,198 @@ class CredentialProviderProcessorTest {
assert(captureSlot.captured is CreateCredentialUnknownException)
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with result on password create request with valid userState`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
every { context.packageName } returns "com.x8bit.bitwarden"
every { context.getString(any(), any()) } returns "mockDescription"
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { callback.onError(any()) }
assertEquals(DEFAULT_USER_STATE.accounts.size, captureSlot.captured.createEntries.size)
val capturedEntry = captureSlot.captured.createEntries[0]
assertEquals(DEFAULT_USER_STATE.accounts[0].email, capturedEntry.accountName)
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should generate correct password entries based on state`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
every { context.packageName } returns "com.x8bit.bitwarden.dev"
every { context.getString(any(), any()) } returns "mockDescription"
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk<Cipher>()
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { callback.onError(any()) }
assertEquals(DEFAULT_USER_STATE.accounts.size, captureSlot.captured.createEntries.size)
// Verify only the active account entry has a lastUsedTime
assertEquals(
1,
captureSlot.captured.createEntries.filter { it.lastUsedTime != null }.size,
)
DEFAULT_USER_STATE.accounts.forEachIndexed { index, mockAccount ->
assertEquals(mockAccount.email, captureSlot.captured.createEntries[index].accountName)
}
// Verify all entries have biometric prompt data when feature flag is enabled
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData != null }) {
"Expected all entries to have biometric prompt data."
}
// Verify entries have the correct authenticators when cipher is not null
assertTrue(
captureSlot.captured
.createEntries
.all {
it.biometricPromptData?.allowedAuthenticators ==
BiometricManager.Authenticators.BIOMETRIC_STRONG
},
) { "Expected all entries to have BIOMETRIC_STRONG authenticators." }
// Verify entries have no biometric prompt data when cipher is null
every { biometricsEncryptionManager.getOrCreateCipher(any()) } returns null
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
assertTrue(
captureSlot.captured.createEntries.all { it.biometricPromptData == null },
) { "Expected all entries to have null biometric prompt data." }
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should not add biometric data to password entries on pre-V devices`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
every { context.packageName } returns "com.x8bit.bitwarden"
every { context.getString(any(), any()) } returns "mockDescription"
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns false
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
// Verify entries have no biometric prompt data on older devices
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData == null }) {
"Expected all entries to have null biometric prompt data on pre-V devices."
}
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should not add biometric data to password entries when vault is locked`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map { it.copy(isVaultUnlocked = false) },
)
every { context.packageName } returns "com.x8bit.bitwarden"
every { context.getString(any(), any()) } returns "mockDescription"
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { biometricsEncryptionManager.getOrCreateCipher(any()) }
// Verify entries have no biometric prompt data when vault is locked
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData == null }) {
"Expected all entries to have null biometric prompt data when vault is locked."
}
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with error when json is null or empty`() {

View File

@ -48,7 +48,7 @@ fun createMockCipherView(
organizationId: String? = "mockOrganizationId-$number",
folderId: String? = "mockId-$number",
notes: String? = "mockNotes-$number",
password: String = "mockPassword-$number",
password: String? = "mockPassword-$number",
clock: Clock = FIXED_CLOCK,
fido2Credentials: List<Fido2Credential>? = null,
sshKey: SshKeyView? = createMockSshKeyView(number = number),

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.graphics.drawable.Icon
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginGetCredentialResponse
@ -16,9 +17,9 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2Creden
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPasswordCredentialAutofillCipherLogin
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import io.mockk.Called
import io.mockk.MockKVerificationScope
import io.mockk.Ordering
@ -60,9 +61,11 @@ class CredentialProviderCompletionManagerTest {
}
@Test
fun `completeFido2Registration should perform no operations`() {
val mockRegistrationResult = mockk<RegisterFido2CredentialResult>()
credentialProviderCompletionManager.completeFido2Registration(mockRegistrationResult)
fun `completeCredentialRegistration should perform no operations`() {
val mockRegistrationResult = mockk<CreateCredentialResult>()
credentialProviderCompletionManager.completeCredentialRegistration(
mockRegistrationResult,
)
verify {
mockRegistrationResult wasNot Called
mockActivity wasNot Called
@ -132,10 +135,10 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength")
@Test
fun `completeFido2Registration should set CreateCredentialResponse, set activity result, then finish activity when result is Success`() {
fun `completeCredentialRegistration should set CreateCredentialResponse, set activity result, then finish activity when result is SuccessFido2`() {
credentialProviderCompletionManager
.completeFido2Registration(
RegisterFido2CredentialResult.Success(
.completeCredentialRegistration(
CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "registrationResponse",
),
)
@ -147,9 +150,24 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength")
@Test
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Error`() {
fun `completeCredentialRegistration should set CreateCredentialResponse, set activity result, then finish activity when result is SuccessPassword`() {
credentialProviderCompletionManager.completeCredentialRegistration(
CreateCredentialResult.Success.PasswordCreated,
)
verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setCreateCredentialResponse(
intent = any(),
response = any<CreatePasswordResponse>(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeCredentialRegistration should set CreateCredentialException, set activity result, then finish activity when result is Error`() {
credentialProviderCompletionManager
.completeFido2Registration(RegisterFido2CredentialResult.Error("".asText()))
.completeCredentialRegistration(CreateCredentialResult.Error("".asText()))
verifyActivityResultIsSetAndFinishedAfter {
mockActivity.resources
@ -159,9 +177,9 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength")
@Test
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Cancelled`() {
fun `completeCredentialRegistration should set CreateCredentialException, set activity result, then finish activity when result is Cancelled`() {
credentialProviderCompletionManager
.completeFido2Registration(RegisterFido2CredentialResult.Cancelled)
.completeCredentialRegistration(CreateCredentialResult.Cancelled)
verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setCreateCredentialException(any(), any())

View File

@ -313,6 +313,31 @@ class RootNavScreenTest : BitwardenComposeTest() {
}
}
// Make sure navigating to vault unlocked for create password request works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForCreatePasswordRequest(
username = "activeUserId",
password = "mockPassword",
uri = "mockUri",
)
composeTestRule.runOnIdle {
verify {
mockNavHostController.navigate(
route = VaultUnlockedGraphRoute,
navOptions = expectedNavOptions,
)
mockNavHostController.navigate(
route = VaultAddEditRoute(
vaultAddEditMode = VaultAddEditMode.ADD,
vaultItemId = null,
vaultItemCipherType = VaultItemCipherType.LOGIN,
selectedFolderId = null,
selectedCollectionId = null,
),
navOptions = expectedNavOptions,
)
}
}
// Make sure navigating to vault unlocked for CreateCredentialRequest works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Save(
activeUserId = "activeUserId",

View File

@ -1,6 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.core.os.bundleOf
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.ProviderCreateCredentialRequest
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.data.repository.model.Environment
@ -28,7 +31,9 @@ import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkObject
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.AfterEach
@ -64,6 +69,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
@AfterEach
fun tearDown() {
unmockkStatic(::parseJwtTokenDataOrNull)
unmockkObject(ProviderCreateCredentialRequest.Companion)
}
@Test
@ -682,11 +688,18 @@ class RootNavViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is a Fido2Save special circumstance the nav state should be VaultUnlockedForFido2Save`() {
mockkObject(ProviderCreateCredentialRequest.Companion)
val createCredentialRequest = CreateCredentialRequest(
userId = "activeUserId",
isUserPreVerified = false,
requestData = bundleOf(),
)
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk {
every { callingRequest } returns mockk<CreatePublicKeyCredentialRequest>()
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(createCredentialRequest)
mutableUserStateFlow.tryEmit(
@ -728,6 +741,73 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is a ProviderCreateCredential with password request the nav state should be VaultUnlockedForCreatePasswordRequest`() {
mockkObject(ProviderCreateCredentialRequest.Companion)
val mockUsername = "testUser"
val mockPassword = "testPassword123"
val mockPackageName = "com.example.app"
val createCredentialRequest = CreateCredentialRequest(
userId = "activeUserId",
isUserPreVerified = false,
requestData = bundleOf(),
)
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk {
every { callingRequest } returns mockk<CreatePasswordRequest> {
every { id } returns mockUsername
every { password } returns mockPassword
}
every { callingAppInfo } returns mockk {
every { packageName } returns mockPackageName
}
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(createCredentialRequest)
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
needsMasterPassword = false,
trustedDevice = null,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(
showImportLoginsCard = true,
),
isExportable = true,
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.VaultUnlockedForCreatePasswordRequest(
username = mockUsername,
password = mockPassword,
uri = "androidapp://$mockPackageName",
),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is a Fido2Assertion special circumstance the nav state should be VaultUnlockedForFido2Assertion`() {

View File

@ -57,7 +57,7 @@ import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
@ -112,7 +112,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
every { launchUri(any()) } just runs
}
private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk {
every { completeFido2Registration(any()) } just runs
every { completeCredentialRegistration(any()) } just runs
}
private val biometricsManager: BiometricsManager = mockk {
every { isUserVerificationSupported } returns true
@ -233,18 +233,18 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
}
@Test
fun `on CompleteFido2Create event should invoke Fido2CompletionManager`() {
val result = RegisterFido2CredentialResult.Success(
fun `on CompleteCredentialCreate event should invoke CredentialProviderCompletionManager`() {
val result = CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockRegistrationResponse",
)
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Registration(result = result))
verify { credentialProviderCompletionManager.completeFido2Registration(result) }
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteCredentialRegistration(result = result))
verify { credentialProviderCompletionManager.completeCredentialRegistration(result) }
}
@Test
fun `Fido2Error dialog should display based on state`() {
fun `CredentialError dialog should display based on state`() {
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()),
dialog = VaultAddEditState.DialogState.CredentialError("mockMessage".asText()),
)
composeTestRule
@ -464,7 +464,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
@Test
fun `clicking dismiss dialog on Fido2Error dialog should send Fido2ErrorDialogDismissed action`() {
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()),
dialog = VaultAddEditState.DialogState.CredentialError("mockMessage".asText()),
)
composeTestRule
@ -474,7 +474,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
verify {
viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed("mockMessage".asText()),
VaultAddEditAction.Common.CredentialErrorDialogDismissed("mockMessage".asText()),
)
}
}

View File

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.core.os.bundleOf
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest
@ -78,7 +77,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
@ -1085,8 +1084,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
assertEquals(stateWithSavingDialog, stateFlow.awaitItem())
assertEquals(stateWithName, stateFlow.awaitItem())
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Success(
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockResponse",
),
),
@ -1183,55 +1182,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `in add mode during fido2, SaveClick should show fido2 error dialog when request type is not supported`() =
runTest {
val fido2CredentialRequest = createMockCreateCredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
createCredentialRequest = fido2CredentialRequest,
)
val stateWithName = createVaultAddItemState(
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
createCredentialRequest = fido2CredentialRequest,
)
.copy(shouldExitOnSave = true)
val mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest =
mockk<ProviderCreateCredentialRequest>(relaxed = true) {
every { callingAppInfo } returns mockk(relaxed = true)
every { callingRequest } returns mockk<CreatePasswordRequest>(relaxed = true)
}
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(),
)
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
)
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
message = BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
.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`() =
@ -2276,7 +2226,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 create`() =
runTest {
val errorState = createVaultAddItemState(
dialogState = VaultAddEditState.DialogState.Fido2Error(
dialogState = VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
)
@ -2288,15 +2238,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
),
)
viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(
VaultAddEditAction.Common.CredentialErrorDialogDismissed(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
)
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialog)
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Error(
VaultAddEditEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Error(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
@ -4034,12 +3984,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationLockout should set isUserVerified to false and display Fido2ErrorDialog`() {
fun `UserVerificationLockout should set isUserVerified to false and display CredentialErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut)
verify { bitwardenCredentialManager.isUserVerified = false }
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
viewModel.stateFlow.value.dialog,
@ -4048,7 +3998,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Create with cancelled result`() =
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteCredentialRegistration with cancelled result`() =
runTest {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled)
@ -4056,8 +4006,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
assertNull(viewModel.stateFlow.value.dialog)
viewModel.eventFlow.test {
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Cancelled,
VaultAddEditEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Cancelled,
),
awaitItem(),
)
@ -4066,12 +4016,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationFail should set isUserVerified to false, and display Fido2ErrorDialog`() {
fun `UserVerificationFail should set isUserVerified to false, and display CredentialErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail)
verify { bitwardenCredentialManager.isUserVerified = false }
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
viewModel.stateFlow.value.dialog,
@ -4080,12 +4030,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationNotSupported should display Fido2ErrorDialog when active account not found`() {
fun `UserVerificationNotSupported should display CredentialErrorDialog when active account not found`() {
mutableUserStateFlow.value = null
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
verify { bitwardenCredentialManager.isUserVerified = false }
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
),
viewModel.stateFlow.value.dialog,
@ -4181,7 +4131,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `MasterPasswordFido2VerificationSubmit should display Fido2Error when password verification fails`() {
fun `MasterPasswordFido2VerificationSubmit should display CredentialError when password verification fails`() {
val password = "password"
coEvery {
authRepository.validatePassword(password = password)
@ -4194,7 +4144,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
@ -4230,7 +4180,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `MasterPasswordFido2VerificationSubmit should display Fido2Error when user has no retries remaining`() {
fun `MasterPasswordFido2VerificationSubmit should display CredentialError when user has no retries remaining`() {
val password = "password"
every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false
coEvery {
@ -4244,7 +4194,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
@ -4286,7 +4236,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `PinFido2VerificationSubmit should display Fido2Error when Pin verification fails`() {
fun `PinFido2VerificationSubmit should display CredentialError when Pin verification fails`() {
val pin = "PIN"
coEvery {
authRepository.validatePin(pin = pin)
@ -4299,7 +4249,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
@ -4335,7 +4285,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `PinFido2VerificationSubmit should display Fido2Error when user has no retries remaining`() {
fun `PinFido2VerificationSubmit should display CredentialError when user has no retries remaining`() {
val pin = "PIN"
every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false
coEvery {
@ -4349,7 +4299,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
),
@ -4430,13 +4380,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}
@Test
fun `DismissFido2VerificationDialogClick should display Fido2ErrorDialog`() {
fun `DismissFido2VerificationDialogClick should display CredentialErrorDialog`() {
viewModel.trySendAction(
VaultAddEditAction.Common.DismissFido2VerificationDialogClick,
)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString
.passkey_operation_failed_because_user_could_not_be_verified
.asText(),
@ -4447,7 +4397,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationSuccess should display Fido2ErrorDialog when request is invalid`() {
fun `UserVerificationSuccess should display CredentialErrorDialog when request is invalid`() {
every { authRepository.activeUserId } returns null
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
@ -4459,7 +4409,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
.asText(),
),
@ -4499,7 +4449,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteCredentialRegistration result`() =
runTest {
val mockRequest = createMockCreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Error.InternalError
@ -4527,8 +4477,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Error(
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Error(
BitwardenString.passkey_registration_failed_due_to_an_internal_error
.asText(),
),
@ -4543,7 +4493,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteCredentialRegistration result`() =
runTest {
val mockRequest = createMockCreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Success(
@ -4572,8 +4522,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
assertEquals(
VaultAddEditEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Success(
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockResponse",
),
),

View File

@ -49,9 +49,9 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
@ -106,7 +106,7 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
every { launchUri(any()) } just runs
}
private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk {
every { completeFido2Registration(any()) } just runs
every { completeCredentialRegistration(any()) } just runs
every { completeFido2Assertion(any()) } just runs
every { completePasswordGet(any()) } just runs
every { completeProviderGetCredentialsRequest(any()) } just runs
@ -1966,11 +1966,11 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `CompleteFido2Registration event should call CredentialProviderCompletionManager with result`() {
val result = RegisterFido2CredentialResult.Success("mockResponse")
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Registration(result))
fun `CompleteCredentialRegistration event should call CredentialProviderCompletionManager with result`() {
val result = CreateCredentialResult.Success.Fido2CredentialRegistered("mockResponse")
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteCredentialRegistration(result))
verify {
credentialProviderCompletionManager.completeFido2Registration(result)
credentialProviderCompletionManager.completeCredentialRegistration(result)
}
}

View File

@ -99,9 +99,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
@ -261,10 +261,18 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
private val mockGetPublicKeyCredentialOption = mockk<GetPublicKeyCredentialOption> {
every { requestJson } returns "mockRequestJson"
}
private val mockCreatePublicKeyCredentialOption = mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "mockRequestJson"
every { origin } returns "mockOrigin"
}
private val mockProviderGetCredentialRequest = mockk<ProviderGetCredentialRequest> {
every { credentialOptions } returns listOf(mockGetPublicKeyCredentialOption)
every { callingAppInfo } returns mockCallingAppInfo
}
private val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
every { callingRequest } returns mockCreatePublicKeyCredentialOption
every { callingAppInfo } returns mockCallingAppInfo
}
private val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption> {
every { requestJson } returns "mockRequestJson"
}
@ -272,14 +280,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { beginGetCredentialOptions } returns listOf(mockBeginGetPublicKeyCredentialOption)
every { callingAppInfo } returns mockCallingAppInfo
}
private val mockCreatePublicKeyCredentialRequest = mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "mockRequestJson"
every { origin } returns "mockOrigin"
}
private val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
every { callingRequest } returns mockCreatePublicKeyCredentialRequest
every { callingAppInfo } returns mockCallingAppInfo
}
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
bufferedMutableSharedFlow()
private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay> = mockk {
@ -303,12 +304,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
ProviderGetCredentialRequest.Companion,
BeginGetCredentialRequest.Companion,
)
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
every {
ProviderGetCredentialRequest.fromBundle(any())
} returns mockProviderGetCredentialRequest
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
every {
BeginGetCredentialRequest.fromBundle(any())
} returns mockBeginGetCredentialRequest
@ -348,6 +350,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
coEvery {
originManager.validateOrigin(any(), any())
} returns ValidateOriginResult.Success(null)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.stateFlow.test {
@ -511,6 +515,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
shouldFinishWhenComplete = false,
)
val searchType = SearchType.Vault.All
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemListingsAction.SearchIconClick)
@ -677,7 +682,7 @@ 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`() {
fun `ItemClick for vault item during credential registration should show credential error dialog when cipherView is null`() {
val cipherView = createMockCipherView(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
@ -755,6 +760,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.ItemClick(
@ -810,6 +816,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.ItemClick(
@ -881,6 +888,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.ItemClick(
@ -934,6 +942,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.ItemClick(
@ -2313,6 +2322,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
mutableVaultDataStateFlow.value = dataState
@ -2990,6 +3000,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `icon loading state updates should update isIconLoadingDisabled`() = runTest {
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
@ -3065,6 +3076,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns ValidateOriginResult.Success("mockOrigin")
setupFido2CreateRequest()
createVaultItemListingViewModel()
coVerify(ordering = Ordering.ORDERED) {
@ -3075,9 +3087,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `ValidateOriginResult should update dialog state on Unknown error`() = runTest {
val mockCredentialsRequest = createMockCreateCredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
createMockCreateCredentialRequest(number = 1),
mockCredentialsRequest,
)
coEvery {
originManager.validateOrigin(
@ -3086,6 +3099,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns ValidateOriginResult.Error.Unknown
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
assertEquals(
@ -3111,6 +3125,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
assertEquals(
@ -3139,6 +3154,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
assertEquals(
@ -3165,6 +3181,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns ValidateOriginResult.Error.PasskeyNotSupportedForApp
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
assertEquals(
@ -3191,6 +3208,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns ValidateOriginResult.Error.AssetLinkNotFound
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
assertEquals(
@ -3204,7 +3222,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteCredentialRegistration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Error.InternalError
@ -3217,8 +3235,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Error(
VaultItemListingEvent.CompleteCredentialRegistration(
CreateCredentialResult.Error(
BitwardenString.passkey_registration_failed_due_to_an_internal_error.asText(),
),
),
@ -3232,7 +3250,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteCredentialRegistration result`() =
runTest {
val mockResult = Fido2RegisterCredentialResult.Success(
responseJson = "mockResponse",
@ -3247,8 +3265,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(
RegisterFido2CredentialResult.Success(
VaultItemListingEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockResponse",
),
),
@ -3268,6 +3286,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
SpecialCircumstance.ProviderCreateCredential(
createMockCreateCredentialRequest(number = 1),
)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.DismissCredentialManagerErrorDialogClick(
@ -3277,8 +3296,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialogState)
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Error(
VaultItemListingEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Error(
"".asText(),
),
),
@ -4130,12 +4149,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Registration with cancelled result`() =
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteCredentialRegistration with cancelled result`() =
runTest {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
createMockCreateCredentialRequest(number = 1),
)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
@ -4143,8 +4163,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertNull(viewModel.stateFlow.value.dialogState)
viewModel.eventFlow.test {
assertEquals(
VaultItemListingEvent.CompleteFido2Registration(
result = RegisterFido2CredentialResult.Cancelled,
VaultItemListingEvent.CompleteCredentialRegistration(
result = CreateCredentialResult.Cancelled,
),
awaitItem(),
)
@ -4277,6 +4297,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.UserVerificationSuccess(
@ -4314,6 +4335,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
responseJson = "mockResponse",
)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.UserVerificationSuccess(
@ -5013,6 +5035,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
} returns UserVerificationRequirement.REQUIRED
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
@ -5339,6 +5362,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
runTest {
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.Internal.InternetConnectionErrorReceived,
@ -5372,6 +5396,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
originManager.validateOrigin(any(), any())
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
@ -5403,6 +5428,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
mockCallingAppInfo.getSignatureFingerprintAsHexString()
} returns null
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
@ -5791,6 +5817,24 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
private fun setupFido2CreateRequest(
mockCallingAppInfo: CallingAppInfo = this.mockCallingAppInfo,
mockCreatePublicKeyCredentialRequest: CreatePublicKeyCredentialRequest =
mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "mockRequestJson"
every { origin } returns "mockOrigin"
},
mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest =
mockk<ProviderCreateCredentialRequest> {
every { callingAppInfo } returns mockCallingAppInfo
every { callingRequest } returns mockCreatePublicKeyCredentialRequest
},
) {
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
}
private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType,
): SavedStateHandle = SavedStateHandle().apply {

View File

@ -691,6 +691,7 @@ Do you want to switch to this account?</string>
<string name="follow_the_steps_from_duo_to_finish_logging_in">Follow the steps from Duo to finish logging in.</string>
<string name="launch_duo">Launch Duo</string>
<string name="your_passkey_will_be_saved_to_your_bitwarden_vault_for_x">Your passkey will be saved to your Bitwarden vault for %1$s</string>
<string name="your_password_will_be_saved_to_your_bitwarden_vault_for_x">Your password will be saved to your Bitwarden vault for %1$s</string>
<string name="passkeys_not_supported_for_this_app">Passkeys not supported for this app</string>
<string name="passkey_operation_failed_because_browser_is_not_privileged">Passkey operation failed because browser is not privileged</string>
<string name="passkey_operation_failed_because_browser_signature_does_not_match">Passkey operation failed because browser signature does not match</string>
@ -917,6 +918,8 @@ Do you want to switch to this account?</string>
<string name="passkey_operation_failed_because_the_request_is_unsupported">Passkey operation failed because the request is unsupported.</string>
<string name="password_operation_failed_because_the_selected_item_does_not_exist">Password operation failed because the selected item does not exist.</string>
<string name="password_operation_failed_because_no_item_was_selected">Password operation failed because no item was selected.</string>
<string name="credential_operation_failed_because_the_request_is_invalid">Credential operation failed because the request is invalid.</string>
<string name="credential_operation_failed_because_the_request_is_unsupported">Credential operation failed because the request is unsupported.</string>
<string name="credential_operation_failed_because_user_verification_attempts_exceeded">Credential operation failed because user verification attempts exceeded.</string>
<string name="credential_operation_failed_because_user_is_locked_out">Credential operation failed because user is locked out.</string>
<string name="credential_operation_failed_because_the_selected_item_does_not_exist">Credential operation failed because the selected item does not exist.</string>