From eb771e9dfabe0cd137d548c0556ea7ec8cc2971a Mon Sep 17 00:00:00 2001 From: Shannon Draeker <125921730+shannon-livefront@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:48:25 -0600 Subject: [PATCH] PM-9439: Update cipher list item for passkeys (#3422) --- .../data/vault/repository/VaultRepository.kt | 8 ++ .../vault/repository/VaultRepositoryImpl.kt | 33 ++++--- ...ecryptFido2CredentialAutofillViewResult.kt | 20 +++++ .../data/vault/repository/model/VaultData.kt | 3 + .../components/listitem/BitwardenListItem.kt | 16 ++++ .../search/util/SearchTypeDataExtensions.kt | 1 + .../itemlisting/VaultItemListingContent.kt | 2 + .../itemlisting/VaultItemListingViewModel.kt | 25 ++++++ .../util/VaultItemListingDataExtensions.kt | 47 ++++++++-- .../feature/vault/util/VaultDataExtensions.kt | 14 ++- .../VerificationCodeViewModel.kt | 1 + .../res/drawable/ic_login_item_passkey.xml | 7 ++ .../datasource/sdk/model/CipherViewUtil.kt | 16 ++++ .../vault/repository/VaultRepositoryTest.kt | 90 +++++++++++++++++++ .../itemlisting/VaultItemListingScreenTest.kt | 4 + .../VaultItemListingViewModelTest.kt | 78 +++++++++++++--- .../VaultItemListingDataExtensionsTest.kt | 44 +++++++-- .../util/VaultItemListingDataUtil.kt | 12 +++ .../vault/util/VaultDataExtensionsTest.kt | 81 +++++++++++++++-- .../VerificationCodeViewModelTest.kt | 2 + 20 files changed, 457 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DecryptFido2CredentialAutofillViewResult.kt create mode 100644 app/src/main/res/drawable/ic_login_item_passkey.xml diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 01be99c4f..98bc3e1f3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -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.repository.model.CreateFolderResult 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.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData @@ -144,6 +145,13 @@ interface VaultRepository : CipherManager, VaultLockManager { */ fun getAuthCodesFlow(): StateFlow>> + /** + * Get the decrypted list of fido credentials for the current ciphers and user id. + */ + suspend fun getDecryptedFido2CredentialAutofillViews( + cipherViewList: List, + ): DecryptFido2CredentialAutofillViewResult + /** * Emits the totp code result flow to listeners. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index d84f82bf9..ce93353e5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -6,7 +6,6 @@ import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.crypto.Kdf import com.bitwarden.exporters.ExportFormat -import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.send.Send import com.bitwarden.send.SendType 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.repository.model.CreateFolderResult 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.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData @@ -183,6 +183,7 @@ class VaultRepositoryImpl( ) { ciphersData, foldersData, collectionsData, sendsData -> VaultData( cipherViewList = ciphersData, + fido2CredentialAutofillViewList = null, folderViewList = foldersData, collectionViewList = collectionsData, sendViewList = sendsData.sendViewList, @@ -523,6 +524,20 @@ class VaultRepositoryImpl( ) } + override suspend fun getDecryptedFido2CredentialAutofillViews( + cipherViewList: List, + ): 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) { 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.filterMatchingCredentials( - credentialIds: List, - relyingPartyId: String, - ): List { - 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 * key. This indicates a scenario in which a user has requested PIN unlocking but requires diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DecryptFido2CredentialAutofillViewResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DecryptFido2CredentialAutofillViewResult.kt new file mode 100644 index 000000000..3b34bc525 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DecryptFido2CredentialAutofillViewResult.kt @@ -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, + ) : DecryptFido2CredentialAutofillViewResult() + + /** + * Generic error while decrypting credentials. + */ + data object Error : DecryptFido2CredentialAutofillViewResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultData.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultData.kt index 0f18bc52a..6cc9e5664 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultData.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultData.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.repository.model +import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.send.SendView import com.bitwarden.vault.CipherView import com.bitwarden.vault.CollectionView @@ -12,10 +13,12 @@ import com.bitwarden.vault.FolderView * @param collectionViewList List of decrypted collections. * @param folderViewList List of decrypted folders. * @param sendViewList List of decrypted sends. + * @param fido2CredentialAutofillViewList List of decrypted fido 2 credentials. */ data class VaultData( val cipherViewList: List, val collectionViewList: List, val folderViewList: List, val sendViewList: List, + val fido2CredentialAutofillViewList: List? = null, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt index 04cba601b..6432cb702 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt @@ -53,6 +53,9 @@ import kotlinx.collections.immutable.persistentListOf * This allows the caller to specify things like padding, size, etc. * @param labelTestTag The optional test tag for the [label]. * @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 supportingLabelTestTag The optional test tag for the [supportingLabel]. * @param startIconTestTag The optional test tag for the [startIcon]. @@ -68,6 +71,8 @@ fun BitwardenListItem( modifier: Modifier = Modifier, labelTestTag: String? = null, optionsTestTag: String? = null, + secondSupportingLabel: String? = null, + secondSupportingLabelTestTag: String? = null, supportingLabel: String? = null, supportingLabelTestTag: 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 -> Text( text = supportLabel, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt index 399886049..b6956d21f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/util/SearchTypeDataExtensions.kt @@ -222,6 +222,7 @@ private fun CipherView.toIconData( login?.uris.toLoginIconData( baseIconUrl = baseIconUrl, isIconLoadingDisabled = isIconLoadingDisabled, + usePasskeyDefaultIcon = false, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index cdfdf7f10..2e560d88a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -198,6 +198,8 @@ fun VaultItemListingContent( startIconTestTag = it.iconTestTag, label = it.title, labelTestTag = it.titleTestTag, + secondSupportingLabel = it.secondSubtitle, + secondSupportingLabelTestTag = it.secondSubtitleTestTag, supportingLabel = it.subtitle, supportingLabelTestTag = it.subtitleTestTag, optionsTestTag = it.optionsTestTag, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 5858bb2a5..6c2b19f63 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.fido.Fido2CredentialAutofillView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository 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.manager.AutofillSelectionManager 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.SpecialCircumstanceManager 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.vault.datasource.network.model.PolicyTypeJson 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.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult @@ -857,6 +860,8 @@ class VaultItemListingViewModel @Inject constructor( isIconLoadingDisabled = state.isIconLoadingDisabled, autofillSelectionData = state.autofillSelectionData, fido2CreationData = state.fido2CredentialRequest, + fido2CredentialAutofillViews = vaultData + .fido2CredentialAutofillViewList, ) } @@ -899,6 +904,7 @@ class VaultItemListingViewModel @Inject constructor( ciphers = vaultData.cipherViewList, matchUri = matchUri, ), + fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(), ) } } @@ -919,9 +925,24 @@ class VaultItemListingViewModel @Inject constructor( ciphers = vaultData.cipherViewList, matchUri = matchUri, ), + fido2CredentialAutofillViewList = vaultData.toFido2CredentialAutofillViews(), ) } } + + /** + * Decrypt and filter the fido 2 autofill credentials. + */ + @Suppress("MaxLineLength") + private suspend fun VaultData.toFido2CredentialAutofillViews(): List? = + (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 title title of the item. * @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 subtitleTestTag The test tag associated with the [subtitle]. * @property iconData data for the icon to be displayed (nullable). @@ -1104,6 +1127,8 @@ data class VaultItemListingState( val id: String, val title: String, val titleTestTag: String, + val secondSubtitle: String?, + val secondSubtitleTestTag: String?, val subtitle: String?, val subtitleTestTag: String, val iconData: IconData, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt index 5d30cb86f..56ac3a0b0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util import androidx.annotation.DrawableRes +import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.send.SendType import com.bitwarden.send.SendView import com.bitwarden.vault.CipherRepromptType @@ -13,6 +14,7 @@ import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest 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.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -101,6 +103,7 @@ fun VaultData.toViewState( isIconLoadingDisabled: Boolean, autofillSelectionData: AutofillSelectionData?, fido2CreationData: Fido2CredentialRequest?, + fido2CredentialAutofillViews: List?, ): VaultItemListingState.ViewState { val filteredCipherViewList = cipherViewList .filter { cipherView -> @@ -129,6 +132,7 @@ fun VaultData.toViewState( isIconLoadingDisabled = isIconLoadingDisabled, isAutofill = autofillSelectionData != null, isFido2Creation = fido2CreationData != null, + fido2CredentialAutofillViews = fido2CredentialAutofillViews, ), displayFolderList = folderList.map { folderView -> VaultItemListingState.FolderDisplayItem( @@ -259,12 +263,14 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary( is VaultItemListingState.ItemListingType.Send.SendText -> this } +@Suppress("LongParameterList") private fun List.toDisplayItemList( baseIconUrl: String, hasMasterPassword: Boolean, isIconLoadingDisabled: Boolean, isAutofill: Boolean, isFido2Creation: Boolean, + fido2CredentialAutofillViews: List?, ): List = this.map { it.toDisplayItem( @@ -273,6 +279,10 @@ private fun List.toDisplayItemList( isIconLoadingDisabled = isIconLoadingDisabled, isAutofill = isAutofill, isFido2Creation = isFido2Creation, + fido2CredentialAutofillView = fido2CredentialAutofillViews + ?.firstOrNull { fido2CredentialAutofillView -> + fido2CredentialAutofillView.cipherId == it.id + }, ) } @@ -287,32 +297,55 @@ private fun List.toDisplayItemList( ) } +@Suppress("LongParameterList") private fun CipherView.toDisplayItem( baseIconUrl: String, hasMasterPassword: Boolean, isIconLoadingDisabled: Boolean, isAutofill: Boolean, isFido2Creation: Boolean, + fido2CredentialAutofillView: Fido2CredentialAutofillView?, ): VaultItemListingState.DisplayItem = VaultItemListingState.DisplayItem( id = id.orEmpty(), title = name, titleTestTag = "CipherNameLabel", - subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + secondSubtitle = this.toSecondSubtitle(fido2CredentialAutofillView?.rpId), + secondSubtitleTestTag = "PasskeySite", + subtitle = this.subtitle, + subtitleTestTag = this.toSubtitleTestTag( + isAutofill = isAutofill, + isFido2Creation = isFido2Creation, + ), iconData = this.toIconData( baseIconUrl = baseIconUrl, isIconLoadingDisabled = isIconLoadingDisabled, + usePasskeyDefaultIcon = (isAutofill || isFido2Creation) && + this.isActiveWithFido2Credentials, ), - iconTestTag = toIconTestTag(), - extraIconList = toLabelIcons(), - overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword), + iconTestTag = this.toIconTestTag(), + extraIconList = this.toLabelIcons(), + overflowOptions = this.toOverflowActions(hasMasterPassword = hasMasterPassword), optionsTestTag = "CipherOptionsButton", isAutofill = isAutofill, isFido2Creation = isFido2Creation, 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 = when (type) { CipherType.LOGIN -> "LoginCipherIcon" @@ -324,12 +357,14 @@ private fun CipherView.toIconTestTag(): String = private fun CipherView.toIconData( baseIconUrl: String, isIconLoadingDisabled: Boolean, + usePasskeyDefaultIcon: Boolean, ): IconData { return when (this.type) { CipherType.LOGIN -> { login?.uris.toLoginIconData( baseIconUrl = baseIconUrl, isIconLoadingDisabled = isIconLoadingDisabled, + usePasskeyDefaultIcon = usePasskeyDefaultIcon, ) } @@ -347,6 +382,8 @@ private fun SendView.toDisplayItem( id = id.orEmpty(), title = name, titleTestTag = "SendNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = deletionDate.toFormattedPattern(DELETION_DATE_PATTERN, clock), subtitleTestTag = "SendDateLabel", iconData = IconData.Local( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt index 5c577b63a..cb0888754 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt @@ -146,13 +146,18 @@ fun VaultData.toViewState( fun List?.toLoginIconData( isIconLoadingDisabled: Boolean, baseIconUrl: String, + usePasskeyDefaultIcon: Boolean, ): 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 ?.map { it.uri } ?.firstOrNull { uri -> uri?.contains(".") == true } - ?: return localIconData + ?: return IconData.Local(defaultIconRes) if (uri.startsWith(ANDROID_URI)) { return IconData.Local(R.drawable.ic_android) @@ -163,7 +168,7 @@ fun List?.toLoginIconData( } if (isIconLoadingDisabled) { - return localIconData + return IconData.Local(defaultIconRes) } if (!uri.contains("://")) { @@ -177,7 +182,7 @@ fun List?.toLoginIconData( return IconData.Network( uri = url, - fallbackIconRes = R.drawable.ic_login_item, + fallbackIconRes = defaultIconRes, ) } @@ -199,6 +204,7 @@ private fun CipherView.toVaultItemOrNull( startIcon = login?.uris.toLoginIconData( isIconLoadingDisabled = isIconLoadingDisabled, baseIconUrl = baseIconUrl, + usePasskeyDefaultIcon = false, ), overflowOptions = toOverflowActions(hasMasterPassword = hasMasterPassword), extraIconList = toLabelIcons(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt index d29e564e3..529191a7a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt @@ -299,6 +299,7 @@ class VerificationCodeViewModel @Inject constructor( startIcon = item.uriLoginViewList.toLoginIconData( baseIconUrl = state.baseIconUrl, isIconLoadingDisabled = state.isIconLoadingDisabled, + usePasskeyDefaultIcon = false, ), ) }, diff --git a/app/src/main/res/drawable/ic_login_item_passkey.xml b/app/src/main/res/drawable/ic_login_item_passkey.xml new file mode 100644 index 000000000..305f7c68a --- /dev/null +++ b/app/src/main/res/drawable/ic_login_item_passkey.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index cdc318b87..d1150f96a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.model +import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.vault.AttachmentView import com.bitwarden.vault.CardView import com.bitwarden.vault.CipherRepromptType @@ -138,6 +139,21 @@ fun createMockSdkFido2Credential( 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]. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 95c3126b1..b1ececd3c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -8,6 +8,7 @@ import com.bitwarden.core.DateTime import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.exporters.ExportFormat +import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.send.SendType import com.bitwarden.send.SendView 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.repository.model.CreateFolderResult 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.DeleteSendResult 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>() + 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 /** diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index feb32028c..a7ed193e1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -1536,6 +1536,8 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem = id = "mockId-$number", title = "mockTitle-$number", titleTestTag = "SendNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = "mockSubtitle-$number", subtitleTestTag = "SendDateLabel", iconData = IconData.Local(R.drawable.ic_card_item), @@ -1580,6 +1582,8 @@ private fun createCipherDisplayItem(number: Int): VaultItemListingState.DisplayI id = "mockId-$number", title = "mockTitle-$number", titleTestTag = "CipherNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = "mockSubtitle-$number", subtitleTestTag = "CipherSubTitleLabel", iconData = IconData.Local(R.drawable.ic_vault), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 080748419..786120555 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -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.createMockSendView 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.GenerateTotpResult 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.concat import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary +import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayItemForCipher @@ -284,6 +286,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { runTest { setupMockUri() val cipherView = createMockCipherView(number = 1) + coEvery { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView), + ) + } returns DecryptFido2CredentialAutofillViewResult.Error specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AutofillSelection( autofillSelectionData = AutofillSelectionData( @@ -309,6 +316,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { awaitItem(), ) } + coVerify { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView), + ) + } } @Test @@ -354,7 +366,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), displayItemList = listOf( - createMockDisplayItemForCipher(number = 1), + createMockDisplayItemForCipher(number = 1) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), @@ -404,7 +417,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), displayItemList = listOf( - createMockDisplayItemForCipher(number = 1), + createMockDisplayItemForCipher(number = 1) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), @@ -928,7 +942,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), displayItemList = listOf( - createMockDisplayItemForCipher(number = 1), + createMockDisplayItemForCipher(number = 1) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), @@ -946,6 +961,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { val cipherView1 = createMockCipherView(number = 1) val cipherView2 = createMockCipherView(number = 2) + coEvery { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView1, cipherView2), + ) + } returns DecryptFido2CredentialAutofillViewResult.Success(emptyList()) + // Set up the data to be filtered mockFilteredCiphers = listOf(cipherView1) @@ -976,7 +997,15 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), 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(), ), @@ -987,6 +1016,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) + coVerify { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView1, cipherView2), + ) + } } @Suppress("MaxLineLength") @@ -995,13 +1029,18 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { runTest { setupMockUri() + val cipherView1 = createMockCipherView(number = 1) + val cipherView2 = createMockCipherView(number = 2) + + coEvery { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView1, cipherView2), + ) + } returns DecryptFido2CredentialAutofillViewResult.Success(emptyList()) coEvery { fido2CredentialManager.validateOrigin(any()) } returns Fido2ValidateOriginResult.Success - val cipherView1 = createMockCipherView(number = 1) - val cipherView2 = createMockCipherView(number = 2) - mockFilteredCiphers = listOf(cipherView1) val fido2CredentialRequest = Fido2CredentialRequest( @@ -1035,7 +1074,15 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { displayCollectionList = emptyList(), displayItemList = listOf( 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(), ), @@ -1046,6 +1093,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) + coVerify { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView1, cipherView2), + ) + fido2CredentialManager.validateOrigin(any()) + } } @Test @@ -1139,7 +1192,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), displayItemList = listOf( - createMockDisplayItemForCipher(number = 1), + createMockDisplayItemForCipher(number = 1) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), @@ -1249,7 +1303,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), displayItemList = listOf( - createMockDisplayItemForCipher(number = 1), + createMockDisplayItemForCipher(number = 1) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), @@ -1369,7 +1424,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { viewState = VaultItemListingState.ViewState.Content( displayCollectionList = emptyList(), displayItemList = listOf( - createMockDisplayItemForCipher(number = 1), + createMockDisplayItemForCipher(number = 1) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt index 183f651f3..0b568c150 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt @@ -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.vault.datasource.sdk.model.createMockCipherView 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.createMockSendView import com.x8bit.bitwarden.data.vault.repository.model.VaultData 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.vault.model.VaultFilterType import io.mockk.every @@ -397,6 +399,7 @@ class VaultItemListingDataExtensionsTest { autofillSelectionData = null, fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ) assertEquals( @@ -408,22 +411,28 @@ class VaultItemListingDataExtensionsTest { cipherType = CipherType.LOGIN, subtitle = null, ) - .copy(shouldShowMasterPasswordReprompt = true), + .copy( + secondSubtitleTestTag = "PasskeySite", + shouldShowMasterPasswordReprompt = true, + ), createMockDisplayItemForCipher( number = 2, cipherType = CipherType.CARD, subtitle = null, - ), + ) + .copy(secondSubtitleTestTag = "PasskeySite"), createMockDisplayItemForCipher( number = 3, cipherType = CipherType.SECURE_NOTE, subtitle = null, - ), + ) + .copy(secondSubtitleTestTag = "PasskeySite"), createMockDisplayItemForCipher( number = 4, cipherType = CipherType.IDENTITY, subtitle = null, - ), + ) + .copy(secondSubtitleTestTag = "PasskeySite"), ), displayFolderList = emptyList(), ), @@ -455,6 +464,12 @@ class VaultItemListingDataExtensionsTest { folderId = "mockId-1", ), ) + val fido2CredentialAutofillViews = listOf( + createMockFido2CredentialAutofillView( + cipherId = "mockId-1", + number = 1, + ), + ) val result = VaultData( cipherViewList = cipherViewList, @@ -472,6 +487,7 @@ class VaultItemListingDataExtensionsTest { ), fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = fido2CredentialAutofillViews, ) assertEquals( @@ -484,6 +500,13 @@ class VaultItemListingDataExtensionsTest { subtitle = null, ) .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, shouldShowMasterPasswordReprompt = true, ), @@ -492,7 +515,11 @@ class VaultItemListingDataExtensionsTest { cipherType = CipherType.CARD, subtitle = null, ) - .copy(isAutofill = true), + .copy( + secondSubtitleTestTag = "PasskeySite", + subtitleTestTag = "PasswordName", + isAutofill = true, + ), ), displayFolderList = emptyList(), ), @@ -525,6 +552,7 @@ class VaultItemListingDataExtensionsTest { autofillSelectionData = null, fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ), ) @@ -545,6 +573,7 @@ class VaultItemListingDataExtensionsTest { autofillSelectionData = null, fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ), ) @@ -563,6 +592,7 @@ class VaultItemListingDataExtensionsTest { autofillSelectionData = null, fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ), ) @@ -584,6 +614,7 @@ class VaultItemListingDataExtensionsTest { ), fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ), ) @@ -608,6 +639,7 @@ class VaultItemListingDataExtensionsTest { origin = "https://www.test.com", ), hasMasterPassword = true, + fido2CredentialAutofillViews = null, ), ) } @@ -748,6 +780,7 @@ class VaultItemListingDataExtensionsTest { vaultFilterType = VaultFilterType.AllVaults, fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ) assertEquals( @@ -789,6 +822,7 @@ class VaultItemListingDataExtensionsTest { vaultFilterType = VaultFilterType.AllVaults, fido2CreationData = null, hasMasterPassword = true, + fido2CredentialAutofillViews = null, ) assertEquals( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt index 333f8e438..c41d6cca7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt @@ -23,6 +23,8 @@ fun createMockDisplayItemForCipher( id = "mockId-$number", title = "mockName-$number", titleTestTag = "CipherNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = subtitle, subtitleTestTag = "CipherSubTitleLabel", iconData = IconData.Network( @@ -75,6 +77,8 @@ fun createMockDisplayItemForCipher( id = "mockId-$number", title = "mockName-$number", titleTestTag = "CipherNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = subtitle, subtitleTestTag = "CipherSubTitleLabel", iconData = IconData.Local(R.drawable.ic_secure_note_item), @@ -113,6 +117,8 @@ fun createMockDisplayItemForCipher( id = "mockId-$number", title = "mockName-$number", titleTestTag = "CipherNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = subtitle, subtitleTestTag = "CipherSubTitleLabel", iconData = IconData.Local(R.drawable.ic_card_item), @@ -157,6 +163,8 @@ fun createMockDisplayItemForCipher( id = "mockId-$number", title = "mockName-$number", titleTestTag = "CipherNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = subtitle, subtitleTestTag = "CipherSubTitleLabel", iconData = IconData.Local(R.drawable.ic_identity_item), @@ -202,6 +210,8 @@ fun createMockDisplayItemForSend( id = "mockId-$number", title = "mockName-$number", titleTestTag = "SendNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = "Oct 27, 2023, 12:00 PM", subtitleTestTag = "SendDateLabel", iconData = IconData.Local(R.drawable.ic_send_file), @@ -241,6 +251,8 @@ fun createMockDisplayItemForSend( id = "mockId-$number", title = "mockName-$number", titleTestTag = "SendNameLabel", + secondSubtitle = null, + secondSubtitleTestTag = null, subtitle = "Oct 27, 2023, 12:00 PM", subtitleTestTag = "SendDateLabel", iconData = IconData.Local(R.drawable.ic_send_text), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt index 89aea2371..a6609d86b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt @@ -27,6 +27,7 @@ import java.time.Clock import java.time.Instant import java.time.ZoneOffset +@Suppress("LargeClass") class VaultDataExtensionsTest { private val clock: Clock = Clock.fixed( @@ -352,7 +353,6 @@ class VaultDataExtensionsTest { ) } - @Suppress("MaxLineLength") @Test fun `toViewState should omit non org related totp codes when user does not have premium`() { val vaultData = VaultData( @@ -390,7 +390,6 @@ class VaultDataExtensionsTest { ) } - @Suppress("MaxLineLength") @Test fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true`() { val actual = @@ -403,6 +402,7 @@ class VaultDataExtensionsTest { .toLoginIconData( isIconLoadingDisabled = true, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + usePasskeyDefaultIcon = false, ) val expected = IconData.Local(iconRes = R.drawable.ic_login_item) @@ -411,6 +411,26 @@ class VaultDataExtensionsTest { } @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 fun `toLoginIconData should return a IconData Local type if no valid uris are found`() { val actual = listOf( @@ -423,6 +443,7 @@ class VaultDataExtensionsTest { .toLoginIconData( isIconLoadingDisabled = false, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + usePasskeyDefaultIcon = false, ) val expected = IconData.Local(iconRes = R.drawable.ic_login_item) @@ -431,6 +452,26 @@ class VaultDataExtensionsTest { } @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 fun `toLoginIconData should return a IconData Local type if an Android uri is detected`() { val actual = listOf( @@ -443,6 +484,7 @@ class VaultDataExtensionsTest { .toLoginIconData( isIconLoadingDisabled = false, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + usePasskeyDefaultIcon = false, ) val expected = IconData.Local(iconRes = R.drawable.ic_android) @@ -450,7 +492,6 @@ class VaultDataExtensionsTest { assertEquals(expected, actual) } - @Suppress("MaxLineLength") @Test fun `toLoginIconData should return a IconData Local type if an iOS uri is detected`() { val actual = listOf( @@ -463,6 +504,7 @@ class VaultDataExtensionsTest { .toLoginIconData( isIconLoadingDisabled = false, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + usePasskeyDefaultIcon = false, ) val expected = IconData.Local(iconRes = R.drawable.ic_ios) @@ -470,7 +512,6 @@ class VaultDataExtensionsTest { assertEquals(expected, actual) } - @Suppress("MaxLineLength") @Test fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false`() { mockkStatic(Uri::class) @@ -488,6 +529,7 @@ class VaultDataExtensionsTest { .toLoginIconData( isIconLoadingDisabled = false, baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + usePasskeyDefaultIcon = false, ) val expected = IconData.Network( @@ -501,6 +543,36 @@ class VaultDataExtensionsTest { } @Suppress("MaxLineLength") + @Test + fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false and usePasskeyDefaultIcon`() { + mockkStatic(Uri::class) + val uriMock = mockk() + 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 fun `toViewState should only count deleted items for the trash count`() { val vaultData = VaultData( @@ -539,7 +611,6 @@ class VaultDataExtensionsTest { ) } - @Suppress("MaxLineLength") @Test fun `toViewState with over 100 no folder items should show no folder option`() { mockkStatic(Uri::class) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt index 42b215146..de0be3083 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt @@ -546,6 +546,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() { startIcon = cipherView.login?.uris.toLoginIconData( isIconLoadingDisabled = initialState.isIconLoadingDisabled, baseIconUrl = initialState.baseIconUrl, + usePasskeyDefaultIcon = false, ), ) }, @@ -566,6 +567,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() { startIcon = cipherView.login?.uris.toLoginIconData( isIconLoadingDisabled = initialState.isIconLoadingDisabled, baseIconUrl = initialState.baseIconUrl, + usePasskeyDefaultIcon = false, ), ) },