PM-9439: Update cipher list item for passkeys (#3422)

This commit is contained in:
Shannon Draeker 2024-07-11 18:48:25 -06:00 committed by GitHub
parent a84694b100
commit eb771e9dfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 457 additions and 47 deletions

View file

@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
@ -144,6 +145,13 @@ interface VaultRepository : CipherManager, VaultLockManager {
*/ */
fun getAuthCodesFlow(): StateFlow<DataState<List<VerificationCodeItem>>> fun getAuthCodesFlow(): StateFlow<DataState<List<VerificationCodeItem>>>
/**
* Get the decrypted list of fido credentials for the current ciphers and user id.
*/
suspend fun getDecryptedFido2CredentialAutofillViews(
cipherViewList: List<CipherView>,
): DecryptFido2CredentialAutofillViewResult
/** /**
* Emits the totp code result flow to listeners. * Emits the totp code result flow to listeners.
*/ */

View file

@ -6,7 +6,6 @@ import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.crypto.Kdf import com.bitwarden.crypto.Kdf
import com.bitwarden.exporters.ExportFormat import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.send.Send import com.bitwarden.send.Send
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
@ -57,6 +56,7 @@ import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
@ -183,6 +183,7 @@ class VaultRepositoryImpl(
) { ciphersData, foldersData, collectionsData, sendsData -> ) { ciphersData, foldersData, collectionsData, sendsData ->
VaultData( VaultData(
cipherViewList = ciphersData, cipherViewList = ciphersData,
fido2CredentialAutofillViewList = null,
folderViewList = foldersData, folderViewList = foldersData,
collectionViewList = collectionsData, collectionViewList = collectionsData,
sendViewList = sendsData.sendViewList, sendViewList = sendsData.sendViewList,
@ -523,6 +524,20 @@ class VaultRepositoryImpl(
) )
} }
override suspend fun getDecryptedFido2CredentialAutofillViews(
cipherViewList: List<CipherView>,
): DecryptFido2CredentialAutofillViewResult {
return vaultSdkSource
.decryptFido2CredentialAutofillViews(
userId = activeUserId ?: return DecryptFido2CredentialAutofillViewResult.Error,
cipherViews = cipherViewList.toTypedArray(),
)
.fold(
onFailure = { DecryptFido2CredentialAutofillViewResult.Error },
onSuccess = { DecryptFido2CredentialAutofillViewResult.Success(it) },
)
}
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) { override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
mutableTotpCodeResultFlow.tryEmit(totpCodeResult) mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
} }
@ -861,22 +876,6 @@ class VaultRepositoryImpl(
) )
} }
/**
* Return a filtered list containing elements that match the given [relyingPartyId] and a
* credential ID contained in [credentialIds].
*/
private fun List<Fido2CredentialAutofillView>.filterMatchingCredentials(
credentialIds: List<ByteArray>,
relyingPartyId: String,
): List<Fido2CredentialAutofillView> {
val skipCredentialIdFiltering = credentialIds.isEmpty()
return filter { fido2CredentialView ->
fido2CredentialView.rpId == relyingPartyId &&
(skipCredentialIdFiltering ||
credentialIds.contains(fido2CredentialView.credentialId))
}
}
/** /**
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user * Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
* key. This indicates a scenario in which a user has requested PIN unlocking but requires * key. This indicates a scenario in which a user has requested PIN unlocking but requires

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.vault.repository.model
import com.bitwarden.fido.Fido2CredentialAutofillView
/**
* Models result of decrypting the fido2 credential autofill views.
*/
sealed class DecryptFido2CredentialAutofillViewResult {
/**
* Credentials decrypted successfully.
*/
data class Success(
val fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
) : DecryptFido2CredentialAutofillViewResult()
/**
* Generic error while decrypting credentials.
*/
data object Error : DecryptFido2CredentialAutofillViewResult()
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.repository.model package com.x8bit.bitwarden.data.vault.repository.model
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.bitwarden.vault.CollectionView import com.bitwarden.vault.CollectionView
@ -12,10 +13,12 @@ import com.bitwarden.vault.FolderView
* @param collectionViewList List of decrypted collections. * @param collectionViewList List of decrypted collections.
* @param folderViewList List of decrypted folders. * @param folderViewList List of decrypted folders.
* @param sendViewList List of decrypted sends. * @param sendViewList List of decrypted sends.
* @param fido2CredentialAutofillViewList List of decrypted fido 2 credentials.
*/ */
data class VaultData( data class VaultData(
val cipherViewList: List<CipherView>, val cipherViewList: List<CipherView>,
val collectionViewList: List<CollectionView>, val collectionViewList: List<CollectionView>,
val folderViewList: List<FolderView>, val folderViewList: List<FolderView>,
val sendViewList: List<SendView>, val sendViewList: List<SendView>,
val fido2CredentialAutofillViewList: List<Fido2CredentialAutofillView>? = null,
) )

View file

@ -53,6 +53,9 @@ import kotlinx.collections.immutable.persistentListOf
* This allows the caller to specify things like padding, size, etc. * This allows the caller to specify things like padding, size, etc.
* @param labelTestTag The optional test tag for the [label]. * @param labelTestTag The optional test tag for the [label].
* @param optionsTestTag The optional test tag for the options button. * @param optionsTestTag The optional test tag for the options button.
* @param secondSupportingLabel An additional optional text label to display beneath the label and
* above the optional supporting label.
* @param secondSupportingLabelTestTag The optional test tag for the [secondSupportingLabel].
* @param supportingLabel An optional secondary text label to display beneath the label. * @param supportingLabel An optional secondary text label to display beneath the label.
* @param supportingLabelTestTag The optional test tag for the [supportingLabel]. * @param supportingLabelTestTag The optional test tag for the [supportingLabel].
* @param startIconTestTag The optional test tag for the [startIcon]. * @param startIconTestTag The optional test tag for the [startIcon].
@ -68,6 +71,8 @@ fun BitwardenListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
labelTestTag: String? = null, labelTestTag: String? = null,
optionsTestTag: String? = null, optionsTestTag: String? = null,
secondSupportingLabel: String? = null,
secondSupportingLabelTestTag: String? = null,
supportingLabel: String? = null, supportingLabel: String? = null,
supportingLabelTestTag: String? = null, supportingLabelTestTag: String? = null,
startIconTestTag: String? = null, startIconTestTag: String? = null,
@ -124,6 +129,17 @@ fun BitwardenListItem(
} }
} }
secondSupportingLabel?.let { secondSupportLabel ->
Text(
text = secondSupportLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.semantics {
secondSupportingLabelTestTag?.let { testTag = it }
},
)
}
supportingLabel?.let { supportLabel -> supportingLabel?.let { supportLabel ->
Text( Text(
text = supportLabel, text = supportLabel,

View file

@ -222,6 +222,7 @@ private fun CipherView.toIconData(
login?.uris.toLoginIconData( login?.uris.toLoginIconData(
baseIconUrl = baseIconUrl, baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
usePasskeyDefaultIcon = false,
) )
} }

View file

@ -198,6 +198,8 @@ fun VaultItemListingContent(
startIconTestTag = it.iconTestTag, startIconTestTag = it.iconTestTag,
label = it.title, label = it.title,
labelTestTag = it.titleTestTag, labelTestTag = it.titleTestTag,
secondSupportingLabel = it.secondSubtitle,
secondSupportingLabelTestTag = it.secondSubtitleTestTag,
supportingLabel = it.subtitle, supportingLabel = it.subtitle,
supportingLabelTestTag = it.subtitleTestTag, supportingLabelTestTag = it.subtitleTestTag,
optionsTestTag = it.optionsTestTag, optionsTestTag = it.optionsTestTag,

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
@ -12,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResu
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
@ -28,6 +30,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.map
import com.x8bit.bitwarden.data.platform.util.getFido2RpIdOrNull import com.x8bit.bitwarden.data.platform.util.getFido2RpIdOrNull
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult 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.RemovePasswordSendResult
@ -857,6 +860,8 @@ class VaultItemListingViewModel @Inject constructor(
isIconLoadingDisabled = state.isIconLoadingDisabled, isIconLoadingDisabled = state.isIconLoadingDisabled,
autofillSelectionData = state.autofillSelectionData, autofillSelectionData = state.autofillSelectionData,
fido2CreationData = state.fido2CredentialRequest, fido2CreationData = state.fido2CredentialRequest,
fido2CredentialAutofillViews = vaultData
.fido2CredentialAutofillViewList,
) )
} }
@ -899,6 +904,7 @@ class VaultItemListingViewModel @Inject constructor(
ciphers = vaultData.cipherViewList, ciphers = vaultData.cipherViewList,
matchUri = matchUri, matchUri = matchUri,
), ),
fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(),
) )
} }
} }
@ -919,9 +925,24 @@ class VaultItemListingViewModel @Inject constructor(
ciphers = vaultData.cipherViewList, ciphers = vaultData.cipherViewList,
matchUri = matchUri, matchUri = matchUri,
), ),
fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(),
) )
} }
} }
/**
* Decrypt and filter the fido 2 autofill credentials.
*/
@Suppress("MaxLineLength")
private suspend fun VaultData.toFido2CredentialAutofillViews(): List<Fido2CredentialAutofillView>? =
(vaultRepository
.getDecryptedFido2CredentialAutofillViews(
cipherViewList = this
.cipherViewList
.filter { it.isActiveWithFido2Credentials },
)
as? DecryptFido2CredentialAutofillViewResult.Success)
?.fido2CredentialAutofillViews
} }
/** /**
@ -1089,6 +1110,8 @@ data class VaultItemListingState(
* @property id the id of the item. * @property id the id of the item.
* @property title title of the item. * @property title title of the item.
* @property titleTestTag The test tag associated with the [title]. * @property titleTestTag The test tag associated with the [title].
* @property secondSubtitle The second subtitle of the item (nullable).
* @property secondSubtitleTestTag The test tag associated with the [secondSubtitle].
* @property subtitle subtitle of the item (nullable). * @property subtitle subtitle of the item (nullable).
* @property subtitleTestTag The test tag associated with the [subtitle]. * @property subtitleTestTag The test tag associated with the [subtitle].
* @property iconData data for the icon to be displayed (nullable). * @property iconData data for the icon to be displayed (nullable).
@ -1104,6 +1127,8 @@ data class VaultItemListingState(
val id: String, val id: String,
val title: String, val title: String,
val titleTestTag: String, val titleTestTag: String,
val secondSubtitle: String?,
val secondSubtitleTestTag: String?,
val subtitle: String?, val subtitle: String?,
val subtitleTestTag: String, val subtitleTestTag: String,
val iconData: IconData, val iconData: IconData,

View file

@ -3,6 +3,7 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherRepromptType
@ -13,6 +14,7 @@ import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.util.subtitle import com.x8bit.bitwarden.data.platform.util.subtitle
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -101,6 +103,7 @@ fun VaultData.toViewState(
isIconLoadingDisabled: Boolean, isIconLoadingDisabled: Boolean,
autofillSelectionData: AutofillSelectionData?, autofillSelectionData: AutofillSelectionData?,
fido2CreationData: Fido2CredentialRequest?, fido2CreationData: Fido2CredentialRequest?,
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
): VaultItemListingState.ViewState { ): VaultItemListingState.ViewState {
val filteredCipherViewList = cipherViewList val filteredCipherViewList = cipherViewList
.filter { cipherView -> .filter { cipherView ->
@ -129,6 +132,7 @@ fun VaultData.toViewState(
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = autofillSelectionData != null, isAutofill = autofillSelectionData != null,
isFido2Creation = fido2CreationData != null, isFido2Creation = fido2CreationData != null,
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
), ),
displayFolderList = folderList.map { folderView -> displayFolderList = folderList.map { folderView ->
VaultItemListingState.FolderDisplayItem( VaultItemListingState.FolderDisplayItem(
@ -259,12 +263,14 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
is VaultItemListingState.ItemListingType.Send.SendText -> this is VaultItemListingState.ItemListingType.Send.SendText -> this
} }
@Suppress("LongParameterList")
private fun List<CipherView>.toDisplayItemList( private fun List<CipherView>.toDisplayItemList(
baseIconUrl: String, baseIconUrl: String,
hasMasterPassword: Boolean, hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean, isIconLoadingDisabled: Boolean,
isAutofill: Boolean, isAutofill: Boolean,
isFido2Creation: Boolean, isFido2Creation: Boolean,
fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>?,
): List<VaultItemListingState.DisplayItem> = ): List<VaultItemListingState.DisplayItem> =
this.map { this.map {
it.toDisplayItem( it.toDisplayItem(
@ -273,6 +279,10 @@ private fun List<CipherView>.toDisplayItemList(
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
isAutofill = isAutofill, isAutofill = isAutofill,
isFido2Creation = isFido2Creation, isFido2Creation = isFido2Creation,
fido2CredentialAutofillView = fido2CredentialAutofillViews
?.firstOrNull { fido2CredentialAutofillView ->
fido2CredentialAutofillView.cipherId == it.id
},
) )
} }
@ -287,32 +297,55 @@ private fun List<SendView>.toDisplayItemList(
) )
} }
@Suppress("LongParameterList")
private fun CipherView.toDisplayItem( private fun CipherView.toDisplayItem(
baseIconUrl: String, baseIconUrl: String,
hasMasterPassword: Boolean, hasMasterPassword: Boolean,
isIconLoadingDisabled: Boolean, isIconLoadingDisabled: Boolean,
isAutofill: Boolean, isAutofill: Boolean,
isFido2Creation: Boolean, isFido2Creation: Boolean,
fido2CredentialAutofillView: Fido2CredentialAutofillView?,
): VaultItemListingState.DisplayItem = ): VaultItemListingState.DisplayItem =
VaultItemListingState.DisplayItem( VaultItemListingState.DisplayItem(
id = id.orEmpty(), id = id.orEmpty(),
title = name, title = name,
titleTestTag = "CipherNameLabel", titleTestTag = "CipherNameLabel",
subtitle = subtitle, secondSubtitle = this.toSecondSubtitle(fido2CredentialAutofillView?.rpId),
subtitleTestTag = "CipherSubTitleLabel", secondSubtitleTestTag = "PasskeySite",
subtitle = this.subtitle,
subtitleTestTag = this.toSubtitleTestTag(
isAutofill = isAutofill,
isFido2Creation = isFido2Creation,
),
iconData = this.toIconData( iconData = this.toIconData(
baseIconUrl = baseIconUrl, baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
usePasskeyDefaultIcon = (isAutofill || isFido2Creation) &&
this.isActiveWithFido2Credentials,
), ),
iconTestTag = toIconTestTag(), iconTestTag = this.toIconTestTag(),
extraIconList = toLabelIcons(), extraIconList = this.toLabelIcons(),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword), overflowOptions = this.toOverflowActions(hasMasterPassword = hasMasterPassword),
optionsTestTag = "CipherOptionsButton", optionsTestTag = "CipherOptionsButton",
isAutofill = isAutofill, isAutofill = isAutofill,
isFido2Creation = isFido2Creation, isFido2Creation = isFido2Creation,
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD, shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
) )
private fun CipherView.toSecondSubtitle(fido2CredentialRpId: String?): String? =
fido2CredentialRpId
?.takeIf { this.type == CipherType.LOGIN && it.isNotEmpty() && it != this.name }
private fun CipherView.toSubtitleTestTag(
isAutofill: Boolean,
isFido2Creation: Boolean,
): String =
if ((isAutofill || isFido2Creation)) {
if (this.isActiveWithFido2Credentials) "PasskeyName" else "PasswordName"
} else {
"CipherSubTitleLabel"
}
private fun CipherView.toIconTestTag(): String = private fun CipherView.toIconTestTag(): String =
when (type) { when (type) {
CipherType.LOGIN -> "LoginCipherIcon" CipherType.LOGIN -> "LoginCipherIcon"
@ -324,12 +357,14 @@ private fun CipherView.toIconTestTag(): String =
private fun CipherView.toIconData( private fun CipherView.toIconData(
baseIconUrl: String, baseIconUrl: String,
isIconLoadingDisabled: Boolean, isIconLoadingDisabled: Boolean,
usePasskeyDefaultIcon: Boolean,
): IconData { ): IconData {
return when (this.type) { return when (this.type) {
CipherType.LOGIN -> { CipherType.LOGIN -> {
login?.uris.toLoginIconData( login?.uris.toLoginIconData(
baseIconUrl = baseIconUrl, baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
usePasskeyDefaultIcon = usePasskeyDefaultIcon,
) )
} }
@ -347,6 +382,8 @@ private fun SendView.toDisplayItem(
id = id.orEmpty(), id = id.orEmpty(),
title = name, title = name,
titleTestTag = "SendNameLabel", titleTestTag = "SendNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock), subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock),
subtitleTestTag = "SendDateLabel", subtitleTestTag = "SendDateLabel",
iconData = IconData.Local( iconData = IconData.Local(

View file

@ -146,13 +146,18 @@ fun VaultData.toViewState(
fun List<LoginUriView>?.toLoginIconData( fun List<LoginUriView>?.toLoginIconData(
isIconLoadingDisabled: Boolean, isIconLoadingDisabled: Boolean,
baseIconUrl: String, baseIconUrl: String,
usePasskeyDefaultIcon: Boolean,
): IconData { ): IconData {
val localIconData = IconData.Local(R.drawable.ic_login_item) val defaultIconRes = if (usePasskeyDefaultIcon) {
R.drawable.ic_login_item_passkey
} else {
R.drawable.ic_login_item
}
var uri = this var uri = this
?.map { it.uri } ?.map { it.uri }
?.firstOrNull { uri -> uri?.contains(".") == true } ?.firstOrNull { uri -> uri?.contains(".") == true }
?: return localIconData ?: return IconData.Local(defaultIconRes)
if (uri.startsWith(ANDROID_URI)) { if (uri.startsWith(ANDROID_URI)) {
return IconData.Local(R.drawable.ic_android) return IconData.Local(R.drawable.ic_android)
@ -163,7 +168,7 @@ fun List<LoginUriView>?.toLoginIconData(
} }
if (isIconLoadingDisabled) { if (isIconLoadingDisabled) {
return localIconData return IconData.Local(defaultIconRes)
} }
if (!uri.contains("://")) { if (!uri.contains("://")) {
@ -177,7 +182,7 @@ fun List<LoginUriView>?.toLoginIconData(
return IconData.Network( return IconData.Network(
uri = url, uri = url,
fallbackIconRes = R.drawable.ic_login_item, fallbackIconRes = defaultIconRes,
) )
} }
@ -199,6 +204,7 @@ private fun CipherView.toVaultItemOrNull(
startIcon = login?.uris.toLoginIconData( startIcon = login?.uris.toLoginIconData(
isIconLoadingDisabled = isIconLoadingDisabled, isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl, baseIconUrl = baseIconUrl,
usePasskeyDefaultIcon = false,
), ),
overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword), overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword),
extraIconList = toLabelIcons(), extraIconList = toLabelIcons(),

View file

@ -299,6 +299,7 @@ class VerificationCodeViewModel @Inject constructor(
startIcon = item.uriLoginViewList.toLoginIconData( startIcon = item.uriLoginViewList.toLoginIconData(
baseIconUrl = state.baseIconUrl, baseIconUrl = state.baseIconUrl,
isIconLoadingDisabled = state.isIconLoadingDisabled, isIconLoadingDisabled = state.isIconLoadingDisabled,
usePasskeyDefaultIcon = false,
), ),
) )
}, },

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="23" android:viewportWidth="24" android:width="25.043478dp">
<path android:fillColor="#175DDC" android:pathData="M4.019,18.894C3.344,18.894 2.797,18.347 2.797,17.672C2.797,16.997 3.344,16.45 4.019,16.45C4.694,16.45 5.241,16.997 5.241,17.672C5.241,18.347 4.694,18.894 4.019,18.894Z"/>
<path android:fillColor="#175DDC" android:fillType="evenOdd" android:pathData="M7.872,11.322C6.193,10.258 5.079,8.384 5.079,6.25C5.079,2.936 7.765,0.25 11.078,0.25C14.392,0.25 17.079,2.936 17.079,6.25C17.079,8.384 15.964,10.258 14.285,11.322C15.731,11.858 17.002,12.745 17.991,13.879C18.256,14.174 18.489,14.441 18.65,14.669L21.328,14.667L23.991,17.229L20.241,20.575L18.741,19.075L17.241,20.575L15.741,19.075L14.241,20.515L9.761,20.509C8.767,21.896 7.107,22.788 5.246,22.749C2.281,22.687 -0.071,20.287 -0.008,17.388C0.056,14.489 2.51,12.189 5.475,12.251C5.643,12.255 5.81,12.266 5.975,12.284C6.123,12.188 6.291,12.085 6.482,11.975C6.923,11.72 7.387,11.502 7.872,11.322ZM9.888,14.677L16.663,14.671C16.465,14.454 16.201,14.19 15.808,13.879C14.51,12.859 12.866,12.25 11.078,12.25C9.968,12.25 8.914,12.485 7.963,12.907C8.746,13.332 9.408,13.943 9.888,14.677ZM6.579,6.25C6.579,8.735 8.593,10.75 11.078,10.75C13.564,10.75 15.578,8.735 15.578,6.25C15.578,3.765 13.564,1.75 11.078,1.75C8.593,1.75 6.579,3.765 6.579,6.25ZM8.991,19.008L8.542,19.635C7.829,20.629 6.632,21.277 5.277,21.249C3.11,21.204 1.448,19.459 1.492,17.421C1.536,15.381 3.275,13.706 5.443,13.751C6.798,13.779 7.964,14.476 8.632,15.497L9.077,16.178L20.724,16.167L21.785,17.187L20.3,18.512L18.741,16.954L17.241,18.454L15.763,16.975L13.639,19.014L8.991,19.008Z"/>
</vector>

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.vault.AttachmentView import com.bitwarden.vault.AttachmentView
import com.bitwarden.vault.CardView import com.bitwarden.vault.CardView
import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherRepromptType
@ -138,6 +139,21 @@ fun createMockSdkFido2Credential(
creationDate = clock.instant(), creationDate = clock.instant(),
) )
/**
* Create a mock [Fido2CredentialAutofillView] with a given [number] and optional [cipherId].
*/
fun createMockFido2CredentialAutofillView(
number: Int,
cipherId: String? = null,
): Fido2CredentialAutofillView =
Fido2CredentialAutofillView(
credentialId = "mockCredentialId-$number".encodeToByteArray(),
cipherId = cipherId ?: "mockCipherId-$number",
rpId = "mockRpId-$number",
userNameForUi = "mockUserNameForUi-$number",
userHandle = "mockUserHandle-$number".encodeToByteArray(),
)
/** /**
* Create a mock [LoginUriView] with a given [number]. * Create a mock [LoginUriView] with a given [number].
*/ */

View file

@ -8,6 +8,7 @@ import com.bitwarden.core.DateTime
import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.exporters.ExportFormat import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
@ -77,6 +78,7 @@ import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
@ -4166,6 +4168,94 @@ class VaultRepositoryTest {
) )
} }
@Test
fun `getDecryptedFido2CredentialAutofillViews should return error when userId not found`() =
runTest {
fakeAuthDiskSource.userState = null
val expected = DecryptFido2CredentialAutofillViewResult.Error
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(createMockCipherView(number = 1)),
)
assertEquals(
expected,
result,
)
coVerify(exactly = 0) {
vaultSdkSource.decryptFido2CredentialAutofillViews(any(), any())
}
}
@Test
fun `getDecryptedFido2CredentialAutofillViews should return error when decryption fails`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val cipherViewList = listOf(createMockCipherView(number = 1))
coEvery {
vaultSdkSource.decryptFido2CredentialAutofillViews(
userId = MOCK_USER_STATE.activeUserId,
cipherViews = cipherViewList.toTypedArray(),
)
} returns Throwable().asFailure()
turbineScope {
val expected = DecryptFido2CredentialAutofillViewResult.Error
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(
cipherViewList = cipherViewList,
)
assertEquals(
expected,
result,
)
}
coVerify {
vaultSdkSource.decryptFido2CredentialAutofillViews(
userId = MOCK_USER_STATE.activeUserId,
cipherViews = cipherViewList.toTypedArray(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `getDecryptedFido2CredentialAutofillViews should return correct results when decryption succeeds`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val cipherViewList = listOf(createMockCipherView(number = 1))
val autofillViewList = mockk<List<Fido2CredentialAutofillView>>()
val expected = DecryptFido2CredentialAutofillViewResult.Success(
fido2CredentialAutofillViews = autofillViewList,
)
coEvery {
vaultSdkSource.decryptFido2CredentialAutofillViews(
userId = MOCK_USER_STATE.activeUserId,
cipherViews = cipherViewList.toTypedArray(),
)
} returns autofillViewList.asSuccess()
turbineScope {
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(
cipherViewList = cipherViewList,
)
assertEquals(
expected,
result,
)
}
coVerify {
vaultSdkSource.decryptFido2CredentialAutofillViews(
userId = MOCK_USER_STATE.activeUserId,
cipherViews = cipherViewList.toTypedArray(),
)
}
}
//region Helper functions //region Helper functions
/** /**

View file

@ -1536,6 +1536,8 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
id = "mockId-$number", id = "mockId-$number",
title = "mockTitle-$number", title = "mockTitle-$number",
titleTestTag = "SendNameLabel", titleTestTag = "SendNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = "mockSubtitle-$number", subtitle = "mockSubtitle-$number",
subtitleTestTag = "SendDateLabel", subtitleTestTag = "SendDateLabel",
iconData = IconData.Local(R.drawable.ic_card_item), iconData = IconData.Local(R.drawable.ic_card_item),
@ -1580,6 +1582,8 @@ private fun createCipherDisplayItem(number: Int): VaultItemListingState.DisplayI
id = "mockId-$number", id = "mockId-$number",
title = "mockTitle-$number", title = "mockTitle-$number",
titleTestTag = "CipherNameLabel", titleTestTag = "CipherNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = "mockSubtitle-$number", subtitle = "mockSubtitle-$number",
subtitleTestTag = "CipherSubTitleLabel", subtitleTestTag = "CipherSubTitleLabel",
iconData = IconData.Local(R.drawable.ic_vault), iconData = IconData.Local(R.drawable.ic_vault),

View file

@ -36,6 +36,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionV
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult 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.RemovePasswordSendResult
@ -44,6 +45,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher
@ -284,6 +286,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
runTest { runTest {
setupMockUri() setupMockUri()
val cipherView = createMockCipherView(number = 1) val cipherView = createMockCipherView(number = 1)
coEvery {
vaultRepository.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(cipherView),
)
} returns DecryptFido2CredentialAutofillViewResult.Error
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AutofillSelection( SpecialCircumstance.AutofillSelection(
autofillSelectionData = AutofillSelectionData( autofillSelectionData = AutofillSelectionData(
@ -309,6 +316,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
awaitItem(), awaitItem(),
) )
} }
coVerify {
vaultRepository.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(cipherView),
)
}
} }
@Test @Test
@ -354,7 +366,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1), createMockDisplayItemForCipher(number = 1)
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -404,7 +417,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1), createMockDisplayItemForCipher(number = 1)
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -928,7 +942,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1), createMockDisplayItemForCipher(number = 1)
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -946,6 +961,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
val cipherView1 = createMockCipherView(number = 1) val cipherView1 = createMockCipherView(number = 1)
val cipherView2 = createMockCipherView(number = 2) val cipherView2 = createMockCipherView(number = 2)
coEvery {
vaultRepository.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(cipherView1, cipherView2),
)
} returns DecryptFido2CredentialAutofillViewResult.Success(emptyList())
// Set up the data to be filtered // Set up the data to be filtered
mockFilteredCiphers = listOf(cipherView1) mockFilteredCiphers = listOf(cipherView1)
@ -976,7 +997,15 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1).copy(isAutofill = true), createMockDisplayItemForCipher(number = 1).copy(
secondSubtitleTestTag = "PasskeySite",
subtitleTestTag = "PasskeyName",
iconData = IconData.Network(
uri = "https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item_passkey,
),
isAutofill = true,
),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -987,6 +1016,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
coVerify {
vaultRepository.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(cipherView1, cipherView2),
)
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -995,13 +1029,18 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
runTest { runTest {
setupMockUri() setupMockUri()
val cipherView1 = createMockCipherView(number = 1)
val cipherView2 = createMockCipherView(number = 2)
coEvery {
vaultRepository.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(cipherView1, cipherView2),
)
} returns DecryptFido2CredentialAutofillViewResult.Success(emptyList())
coEvery { coEvery {
fido2CredentialManager.validateOrigin(any()) fido2CredentialManager.validateOrigin(any())
} returns Fido2ValidateOriginResult.Success } returns Fido2ValidateOriginResult.Success
val cipherView1 = createMockCipherView(number = 1)
val cipherView2 = createMockCipherView(number = 2)
mockFilteredCiphers = listOf(cipherView1) mockFilteredCiphers = listOf(cipherView1)
val fido2CredentialRequest = Fido2CredentialRequest( val fido2CredentialRequest = Fido2CredentialRequest(
@ -1035,7 +1074,15 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1) createMockDisplayItemForCipher(number = 1)
.copy(isFido2Creation = true), .copy(
secondSubtitleTestTag = "PasskeySite",
subtitleTestTag = "PasskeyName",
iconData = IconData.Network(
uri = "https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item_passkey,
),
isFido2Creation = true,
),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -1046,6 +1093,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
coVerify {
vaultRepository.getDecryptedFido2CredentialAutofillViews(
cipherViewList = listOf(cipherView1, cipherView2),
)
fido2CredentialManager.validateOrigin(any())
}
} }
@Test @Test
@ -1139,7 +1192,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1), createMockDisplayItemForCipher(number = 1)
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -1249,7 +1303,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1), createMockDisplayItemForCipher(number = 1)
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -1369,7 +1424,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewState = VaultItemListingState.ViewState.Content( viewState = VaultItemListingState.ViewState.Content(
displayCollectionList = emptyList(), displayCollectionList = emptyList(),
displayItemList = listOf( displayItemList = listOf(
createMockDisplayItemForCipher(number = 1), createMockDisplayItemForCipher(number = 1)
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),

View file

@ -16,10 +16,12 @@ import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.platform.util.subtitle import com.x8bit.bitwarden.data.platform.util.subtitle
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import io.mockk.every import io.mockk.every
@ -397,6 +399,7 @@ class VaultItemListingDataExtensionsTest {
autofillSelectionData = null, autofillSelectionData = null,
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
) )
assertEquals( assertEquals(
@ -408,22 +411,28 @@ class VaultItemListingDataExtensionsTest {
cipherType = CipherType.LOGIN, cipherType = CipherType.LOGIN,
subtitle = null, subtitle = null,
) )
.copy(shouldShowMasterPasswordReprompt = true), .copy(
secondSubtitleTestTag = "PasskeySite",
shouldShowMasterPasswordReprompt = true,
),
createMockDisplayItemForCipher( createMockDisplayItemForCipher(
number = 2, number = 2,
cipherType = CipherType.CARD, cipherType = CipherType.CARD,
subtitle = null, subtitle = null,
), )
.copy(secondSubtitleTestTag = "PasskeySite"),
createMockDisplayItemForCipher( createMockDisplayItemForCipher(
number = 3, number = 3,
cipherType = CipherType.SECURE_NOTE, cipherType = CipherType.SECURE_NOTE,
subtitle = null, subtitle = null,
), )
.copy(secondSubtitleTestTag = "PasskeySite"),
createMockDisplayItemForCipher( createMockDisplayItemForCipher(
number = 4, number = 4,
cipherType = CipherType.IDENTITY, cipherType = CipherType.IDENTITY,
subtitle = null, subtitle = null,
), )
.copy(secondSubtitleTestTag = "PasskeySite"),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -455,6 +464,12 @@ class VaultItemListingDataExtensionsTest {
folderId = "mockId-1", folderId = "mockId-1",
), ),
) )
val fido2CredentialAutofillViews = listOf(
createMockFido2CredentialAutofillView(
cipherId = "mockId-1",
number = 1,
),
)
val result = VaultData( val result = VaultData(
cipherViewList = cipherViewList, cipherViewList = cipherViewList,
@ -472,6 +487,7 @@ class VaultItemListingDataExtensionsTest {
), ),
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = fido2CredentialAutofillViews,
) )
assertEquals( assertEquals(
@ -484,6 +500,13 @@ class VaultItemListingDataExtensionsTest {
subtitle = null, subtitle = null,
) )
.copy( .copy(
secondSubtitle = "mockRpId-1",
secondSubtitleTestTag = "PasskeySite",
subtitleTestTag = "PasskeyName",
iconData = IconData.Network(
uri = "https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item_passkey,
),
isAutofill = true, isAutofill = true,
shouldShowMasterPasswordReprompt = true, shouldShowMasterPasswordReprompt = true,
), ),
@ -492,7 +515,11 @@ class VaultItemListingDataExtensionsTest {
cipherType = CipherType.CARD, cipherType = CipherType.CARD,
subtitle = null, subtitle = null,
) )
.copy(isAutofill = true), .copy(
secondSubtitleTestTag = "PasskeySite",
subtitleTestTag = "PasswordName",
isAutofill = true,
),
), ),
displayFolderList = emptyList(), displayFolderList = emptyList(),
), ),
@ -525,6 +552,7 @@ class VaultItemListingDataExtensionsTest {
autofillSelectionData = null, autofillSelectionData = null,
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
), ),
) )
@ -545,6 +573,7 @@ class VaultItemListingDataExtensionsTest {
autofillSelectionData = null, autofillSelectionData = null,
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
), ),
) )
@ -563,6 +592,7 @@ class VaultItemListingDataExtensionsTest {
autofillSelectionData = null, autofillSelectionData = null,
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
), ),
) )
@ -584,6 +614,7 @@ class VaultItemListingDataExtensionsTest {
), ),
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
), ),
) )
@ -608,6 +639,7 @@ class VaultItemListingDataExtensionsTest {
origin = "https://www.test.com", origin = "https://www.test.com",
), ),
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
), ),
) )
} }
@ -748,6 +780,7 @@ class VaultItemListingDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
) )
assertEquals( assertEquals(
@ -789,6 +822,7 @@ class VaultItemListingDataExtensionsTest {
vaultFilterType = VaultFilterType.AllVaults, vaultFilterType = VaultFilterType.AllVaults,
fido2CreationData = null, fido2CreationData = null,
hasMasterPassword = true, hasMasterPassword = true,
fido2CredentialAutofillViews = null,
) )
assertEquals( assertEquals(

View file

@ -23,6 +23,8 @@ fun createMockDisplayItemForCipher(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
titleTestTag = "CipherNameLabel", titleTestTag = "CipherNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = subtitle, subtitle = subtitle,
subtitleTestTag = "CipherSubTitleLabel", subtitleTestTag = "CipherSubTitleLabel",
iconData = IconData.Network( iconData = IconData.Network(
@ -75,6 +77,8 @@ fun createMockDisplayItemForCipher(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
titleTestTag = "CipherNameLabel", titleTestTag = "CipherNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = subtitle, subtitle = subtitle,
subtitleTestTag = "CipherSubTitleLabel", subtitleTestTag = "CipherSubTitleLabel",
iconData = IconData.Local(R.drawable.ic_secure_note_item), iconData = IconData.Local(R.drawable.ic_secure_note_item),
@ -113,6 +117,8 @@ fun createMockDisplayItemForCipher(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
titleTestTag = "CipherNameLabel", titleTestTag = "CipherNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = subtitle, subtitle = subtitle,
subtitleTestTag = "CipherSubTitleLabel", subtitleTestTag = "CipherSubTitleLabel",
iconData = IconData.Local(R.drawable.ic_card_item), iconData = IconData.Local(R.drawable.ic_card_item),
@ -157,6 +163,8 @@ fun createMockDisplayItemForCipher(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
titleTestTag = "CipherNameLabel", titleTestTag = "CipherNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = subtitle, subtitle = subtitle,
subtitleTestTag = "CipherSubTitleLabel", subtitleTestTag = "CipherSubTitleLabel",
iconData = IconData.Local(R.drawable.ic_identity_item), iconData = IconData.Local(R.drawable.ic_identity_item),
@ -202,6 +210,8 @@ fun createMockDisplayItemForSend(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
titleTestTag = "SendNameLabel", titleTestTag = "SendNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = "Oct 27, 2023, 12:00 PM", subtitle = "Oct 27, 2023, 12:00 PM",
subtitleTestTag = "SendDateLabel", subtitleTestTag = "SendDateLabel",
iconData = IconData.Local(R.drawable.ic_send_file), iconData = IconData.Local(R.drawable.ic_send_file),
@ -241,6 +251,8 @@ fun createMockDisplayItemForSend(
id = "mockId-$number", id = "mockId-$number",
title = "mockName-$number", title = "mockName-$number",
titleTestTag = "SendNameLabel", titleTestTag = "SendNameLabel",
secondSubtitle = null,
secondSubtitleTestTag = null,
subtitle = "Oct 27, 2023, 12:00 PM", subtitle = "Oct 27, 2023, 12:00 PM",
subtitleTestTag = "SendDateLabel", subtitleTestTag = "SendDateLabel",
iconData = IconData.Local(R.drawable.ic_send_text), iconData = IconData.Local(R.drawable.ic_send_text),

View file

@ -27,6 +27,7 @@ import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset import java.time.ZoneOffset
@Suppress("LargeClass")
class VaultDataExtensionsTest { class VaultDataExtensionsTest {
private val clock: Clock = Clock.fixed( private val clock: Clock = Clock.fixed(
@ -352,7 +353,6 @@ class VaultDataExtensionsTest {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `toViewState should omit non org related totp codes when user does not have premium`() { fun `toViewState should omit non org related totp codes when user does not have premium`() {
val vaultData = VaultData( val vaultData = VaultData(
@ -390,7 +390,6 @@ class VaultDataExtensionsTest {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true`() { fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true`() {
val actual = val actual =
@ -403,6 +402,7 @@ class VaultDataExtensionsTest {
.toLoginIconData( .toLoginIconData(
isIconLoadingDisabled = true, isIconLoadingDisabled = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = false,
) )
val expected = IconData.Local(iconRes = R.drawable.ic_login_item) val expected = IconData.Local(iconRes = R.drawable.ic_login_item)
@ -411,6 +411,26 @@ class VaultDataExtensionsTest {
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true and usePasskeyDefaultIcon true`() {
val actual =
createMockCipherView(
number = 1,
cipherType = CipherType.LOGIN,
)
.login
?.uris
.toLoginIconData(
isIconLoadingDisabled = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = true,
)
val expected = IconData.Local(iconRes = R.drawable.ic_login_item_passkey)
assertEquals(expected, actual)
}
@Test @Test
fun `toLoginIconData should return a IconData Local type if no valid uris are found`() { fun `toLoginIconData should return a IconData Local type if no valid uris are found`() {
val actual = listOf( val actual = listOf(
@ -423,6 +443,7 @@ class VaultDataExtensionsTest {
.toLoginIconData( .toLoginIconData(
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = false,
) )
val expected = IconData.Local(iconRes = R.drawable.ic_login_item) val expected = IconData.Local(iconRes = R.drawable.ic_login_item)
@ -431,6 +452,26 @@ class VaultDataExtensionsTest {
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if no valid uris are found and usePasskeyDefaultIcon true`() {
val actual = listOf(
LoginUriView(
uri = "",
match = UriMatchType.HOST,
uriChecksum = null,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = true,
)
val expected = IconData.Local(iconRes = R.drawable.ic_login_item_passkey)
assertEquals(expected, actual)
}
@Test @Test
fun `toLoginIconData should return a IconData Local type if an Android uri is detected`() { fun `toLoginIconData should return a IconData Local type if an Android uri is detected`() {
val actual = listOf( val actual = listOf(
@ -443,6 +484,7 @@ class VaultDataExtensionsTest {
.toLoginIconData( .toLoginIconData(
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = false,
) )
val expected = IconData.Local(iconRes = R.drawable.ic_android) val expected = IconData.Local(iconRes = R.drawable.ic_android)
@ -450,7 +492,6 @@ class VaultDataExtensionsTest {
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@Suppress("MaxLineLength")
@Test @Test
fun `toLoginIconData should return a IconData Local type if an iOS uri is detected`() { fun `toLoginIconData should return a IconData Local type if an iOS uri is detected`() {
val actual = listOf( val actual = listOf(
@ -463,6 +504,7 @@ class VaultDataExtensionsTest {
.toLoginIconData( .toLoginIconData(
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = false,
) )
val expected = IconData.Local(iconRes = R.drawable.ic_ios) val expected = IconData.Local(iconRes = R.drawable.ic_ios)
@ -470,7 +512,6 @@ class VaultDataExtensionsTest {
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@Suppress("MaxLineLength")
@Test @Test
fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false`() { fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false`() {
mockkStatic(Uri::class) mockkStatic(Uri::class)
@ -488,6 +529,7 @@ class VaultDataExtensionsTest {
.toLoginIconData( .toLoginIconData(
isIconLoadingDisabled = false, isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = false,
) )
val expected = IconData.Network( val expected = IconData.Network(
@ -501,6 +543,36 @@ class VaultDataExtensionsTest {
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false and usePasskeyDefaultIcon`() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri1.com"
val actual =
createMockCipherView(
number = 1,
cipherType = CipherType.LOGIN,
)
.login
?.uris
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
usePasskeyDefaultIcon = true,
)
val expected = IconData.Network(
uri = "https://vault.bitwarden.com/icons/www.mockuri1.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item_passkey,
)
assertEquals(expected, actual)
unmockkStatic(Uri::class)
}
@Test @Test
fun `toViewState should only count deleted items for the trash count`() { fun `toViewState should only count deleted items for the trash count`() {
val vaultData = VaultData( val vaultData = VaultData(
@ -539,7 +611,6 @@ class VaultDataExtensionsTest {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `toViewState with over 100 no folder items should show no folder option`() { fun `toViewState with over 100 no folder items should show no folder option`() {
mockkStatic(Uri::class) mockkStatic(Uri::class)

View file

@ -546,6 +546,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
startIcon = cipherView.login?.uris.toLoginIconData( startIcon = cipherView.login?.uris.toLoginIconData(
isIconLoadingDisabled = initialState.isIconLoadingDisabled, isIconLoadingDisabled = initialState.isIconLoadingDisabled,
baseIconUrl = initialState.baseIconUrl, baseIconUrl = initialState.baseIconUrl,
usePasskeyDefaultIcon = false,
), ),
) )
}, },
@ -566,6 +567,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
startIcon = cipherView.login?.uris.toLoginIconData( startIcon = cipherView.login?.uris.toLoginIconData(
isIconLoadingDisabled = initialState.isIconLoadingDisabled, isIconLoadingDisabled = initialState.isIconLoadingDisabled,
baseIconUrl = initialState.baseIconUrl, baseIconUrl = initialState.baseIconUrl,
usePasskeyDefaultIcon = false,
), ),
) )
}, },