mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 11:02:51 -05:00
[PM-41291] feat: Add model and data layer for Identity Autofill (#7232)
This commit is contained in:
1 parent
36bd55adf1
commit
da2ea370dd
25 files changed
+956
-23
No files matched your search
@@ -91,6 +91,12 @@ class FilledDataBuilderImpl(
|
||||
}
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
is AutofillPartition.Identity -> {
|
||||
// Filling an identity partition is wired up in a later phase; this is a no-op
|
||||
// today since nothing yet classifies a view as Identity.
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// Use getOrLastOrNull so if the list has run dry take the last spec.
|
||||
|
||||
@@ -71,4 +71,43 @@ sealed class AutofillCipher {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = BitwardenDrawable.ic_globe
|
||||
}
|
||||
|
||||
/**
|
||||
* The identity [AutofillCipher] model. This contains all the data for building
|
||||
* an identity partition.
|
||||
*
|
||||
* @param fullName The identity's name parts joined for filling a combined full-name field.
|
||||
* @param fullAddress The identity's address parts joined for filling a combined full-address
|
||||
* field.
|
||||
*/
|
||||
data class Identity(
|
||||
override val cipherId: String?,
|
||||
override val name: String,
|
||||
override val subtitle: String,
|
||||
val fullName: String,
|
||||
val fullAddress: String,
|
||||
val title: String,
|
||||
val firstName: String,
|
||||
val middleName: String,
|
||||
val lastName: String,
|
||||
val address1: String,
|
||||
val address2: String,
|
||||
val address3: String,
|
||||
val city: String,
|
||||
val state: String,
|
||||
val postalCode: String,
|
||||
val country: String,
|
||||
val company: String,
|
||||
val email: String,
|
||||
val phone: String,
|
||||
val ssn: String,
|
||||
val passportNumber: String,
|
||||
val licenseNumber: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = BitwardenDrawable.ic_id_card
|
||||
|
||||
override val isTotpEnabled: Boolean
|
||||
get() = false
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,21 @@ enum class AutofillHint {
|
||||
CARD_BRAND,
|
||||
PASSWORD,
|
||||
USERNAME,
|
||||
IDENTITY_PERSON_NAME_FULL,
|
||||
IDENTITY_PERSON_NAME_PREFIX,
|
||||
IDENTITY_PERSON_NAME_GIVEN,
|
||||
IDENTITY_PERSON_NAME_MIDDLE,
|
||||
IDENTITY_PERSON_NAME_FAMILY,
|
||||
IDENTITY_POSTAL_ADDRESS_FULL,
|
||||
IDENTITY_ADDRESS_STREET,
|
||||
IDENTITY_ADDRESS_LOCALITY,
|
||||
IDENTITY_ADDRESS_REGION,
|
||||
IDENTITY_ADDRESS_COUNTRY,
|
||||
IDENTITY_POSTAL_CODE,
|
||||
IDENTITY_PHONE_FULL,
|
||||
IDENTITY_COMPANY,
|
||||
IDENTITY_EMAIL,
|
||||
IDENTITY_SSN,
|
||||
IDENTITY_PASSPORT_NUMBER,
|
||||
IDENTITY_LICENSE_NUMBER,
|
||||
}
|
||||
@@ -10,7 +10,7 @@ sealed class AutofillPartition {
|
||||
|
||||
/**
|
||||
* [AutofillId]s that are optional for save requests. For example, with cards we require a
|
||||
* phone number too trigger the save request, other card data is optional.
|
||||
* phone number to trigger the save request, other card data is optional.
|
||||
*/
|
||||
abstract val optionalSaveIds: List<AutofillId>
|
||||
|
||||
@@ -72,4 +72,20 @@ sealed class AutofillPartition {
|
||||
override val saveType: Int
|
||||
get() = SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||
}
|
||||
|
||||
/**
|
||||
* The identity [AutofillPartition] data. Save requests are never offered for this partition
|
||||
* (there are no [requiredSaveIds]) since capturing new identity data from a filled form is out
|
||||
* of scope.
|
||||
*/
|
||||
data class Identity(
|
||||
override val views: List<AutofillView.Identity>,
|
||||
) : AutofillPartition() {
|
||||
override val optionalSaveIds: List<AutofillId>
|
||||
get() = emptyList()
|
||||
override val requiredSaveIds: List<AutofillId>
|
||||
get() = emptyList()
|
||||
override val saveType: Int
|
||||
get() = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,135 @@ sealed class AutofillView {
|
||||
) : Login()
|
||||
}
|
||||
|
||||
/**
|
||||
* A view that corresponds to the identity data partition for autofill fields.
|
||||
*/
|
||||
sealed class Identity : AutofillView() {
|
||||
|
||||
/**
|
||||
* The full name [AutofillView] for the [Identity] data partition, used for forms with a
|
||||
* single combined name field.
|
||||
*/
|
||||
data class PersonNameFull(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The name prefix (e.g. "Mr.", "Dr.") [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PersonNamePrefix(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The given (first) name [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PersonNameGiven(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The middle name [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PersonNameMiddle(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The family (last) name [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PersonNameFamily(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The full postal address [AutofillView] for the [Identity] data partition, used for forms
|
||||
* with a single combined address field.
|
||||
*/
|
||||
data class PostalAddressFull(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The street address [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class AddressStreet(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The locality (city) [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class AddressLocality(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The region (state/province) [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class AddressRegion(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The country [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class AddressCountry(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The postal code [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PostalCode(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The full phone number [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PhoneFull(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The company [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class Company(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The email [AutofillView] for the [Identity] data partition. This is distinct from
|
||||
* [Login.Email] and may be offered alongside it when a field is ambiguous between the
|
||||
* login and identity partitions.
|
||||
*/
|
||||
data class Email(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The social security number [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class Ssn(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The passport number [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class PassportNumber(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
|
||||
/**
|
||||
* The license number [AutofillView] for the [Identity] data partition.
|
||||
*/
|
||||
data class LicenseNumber(
|
||||
override val data: Data,
|
||||
) : Identity()
|
||||
}
|
||||
|
||||
/**
|
||||
* A view that is an input field but does not correspond to any known autofill field.
|
||||
*/
|
||||
|
||||
@@ -186,6 +186,12 @@ class AutofillParserImpl(
|
||||
)
|
||||
}
|
||||
|
||||
is AutofillView.Identity -> {
|
||||
// Identity partition construction is wired up in a later phase; treating it as
|
||||
// unfillable for now is a no-op since nothing yet classifies a view as Identity.
|
||||
return AutofillRequest.Unfillable
|
||||
}
|
||||
|
||||
is AutofillView.Unused -> {
|
||||
// This will never happen: the heuristic path filters out Unused views, and the
|
||||
// fill-assist path never constructs one (toAutofillViewForFieldKey has no Unused
|
||||
@@ -248,6 +254,9 @@ class AutofillParserImpl(
|
||||
rule.category in LOGIN_FILL_ASSIST_CATEGORIES ||
|
||||
rule.category in CARD_FILL_ASSIST_CATEGORIES
|
||||
}
|
||||
// Fill-assist category coverage for Identity is added in a later phase; this is a
|
||||
// no-op today since nothing yet classifies a view as Identity.
|
||||
is AutofillView.Identity -> false
|
||||
}
|
||||
}
|
||||
if (!coversCurrentPartition) return this
|
||||
@@ -458,6 +467,7 @@ private fun AssistStructure.ViewNode.traverse(
|
||||
* This updates the underlying [AutofillView.data] with the given [website] if it does not already
|
||||
* have a website associated with it.
|
||||
*/
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun AutofillView.updateWebsiteIfNecessary(website: String?): AutofillView {
|
||||
val site = website ?: return this
|
||||
if (this.data.website != null) return this
|
||||
@@ -472,6 +482,59 @@ private fun AutofillView.updateWebsiteIfNecessary(website: String?): AutofillVie
|
||||
is AutofillView.Login.Email -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Login.Password -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Login.Username -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Identity.AddressCountry -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.AddressLocality -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.AddressRegion -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.AddressStreet -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.Company -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Identity.Email -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Identity.LicenseNumber -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PassportNumber -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PersonNameFamily -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PersonNameFull -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PersonNameGiven -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PersonNameMiddle -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PersonNamePrefix -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PhoneFull -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Identity.PostalAddressFull -> {
|
||||
this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
|
||||
is AutofillView.Identity.PostalCode -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Identity.Ssn -> this.copy(data = this.data.copy(website = site))
|
||||
is AutofillView.Unused -> this.copy(data = this.data.copy(website = site))
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -97,12 +97,16 @@ class AutofillProcessorImpl(
|
||||
|
||||
when (autofillRequest) {
|
||||
is AutofillRequest.Fillable -> {
|
||||
val intentSender = createAutofillSavedItemIntentSender(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillSaveItem = autofillRequest.toAutofillSaveItem(),
|
||||
)
|
||||
|
||||
saveCallback.onSuccess(intentSender)
|
||||
autofillRequest
|
||||
.toAutofillSaveItem()
|
||||
?.let { autofillSaveItem ->
|
||||
createAutofillSavedItemIntentSender(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillSaveItem = autofillSaveItem,
|
||||
)
|
||||
}
|
||||
?.let { saveCallback.onSuccess(it) }
|
||||
?: saveCallback.onSuccess()
|
||||
}
|
||||
|
||||
AutofillRequest.Unfillable -> saveCallback.onSuccess()
|
||||
|
||||
+5
@@ -23,4 +23,9 @@ interface AutofillCipherProvider {
|
||||
suspend fun getLoginAutofillCiphers(
|
||||
uri: String,
|
||||
): List<AutofillCipher.Login>
|
||||
|
||||
/**
|
||||
* Get all [AutofillCipher.Identity]s for the current user.
|
||||
*/
|
||||
suspend fun getIdentityAutofillCiphers(): List<AutofillCipher.Identity>
|
||||
}
|
||||
+50
@@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.identityAutofillAddress
|
||||
import com.x8bit.bitwarden.data.platform.util.identityAutofillName
|
||||
import com.x8bit.bitwarden.data.platform.util.isActive
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
@@ -133,6 +135,54 @@ class AutofillCipherProviderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getIdentityAutofillCiphers(): List<AutofillCipher.Identity> {
|
||||
val cipherListViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
|
||||
return cipherListViews
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView
|
||||
// We only care about non-deleted identity ciphers.
|
||||
.takeIf {
|
||||
// Must be identity type.
|
||||
it.type is CipherListViewType.Identity &&
|
||||
// Must still be active.
|
||||
it.isActive &&
|
||||
// Must not require a reprompt.
|
||||
it.reprompt == CipherRepromptType.NONE
|
||||
}
|
||||
?.let { nonNullCipherListView ->
|
||||
nonNullCipherListView.id?.let { cipherId ->
|
||||
decryptCipherOrNull(cipherId = cipherId)?.let { cipherView ->
|
||||
val identityView = cipherView.identity
|
||||
AutofillCipher.Identity(
|
||||
cipherId = cipherView.id,
|
||||
name = cipherView.name,
|
||||
subtitle = cipherView.subtitle.orEmpty(),
|
||||
fullName = identityView?.identityAutofillName.orEmpty(),
|
||||
fullAddress = identityView?.identityAutofillAddress.orEmpty(),
|
||||
title = identityView?.title.orEmpty(),
|
||||
firstName = identityView?.firstName.orEmpty(),
|
||||
middleName = identityView?.middleName.orEmpty(),
|
||||
lastName = identityView?.lastName.orEmpty(),
|
||||
address1 = identityView?.address1.orEmpty(),
|
||||
address2 = identityView?.address2.orEmpty(),
|
||||
address3 = identityView?.address3.orEmpty(),
|
||||
city = identityView?.city.orEmpty(),
|
||||
state = identityView?.state.orEmpty(),
|
||||
postalCode = identityView?.postalCode.orEmpty(),
|
||||
country = identityView?.country.orEmpty(),
|
||||
company = identityView?.company.orEmpty(),
|
||||
email = identityView?.email.orEmpty(),
|
||||
phone = identityView?.phone.orEmpty(),
|
||||
ssn = identityView?.ssn.orEmpty(),
|
||||
passportNumber = identityView?.passportNumber.orEmpty(),
|
||||
licenseNumber = identityView?.licenseNumber.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available [CipherView]s if possible.
|
||||
*/
|
||||
|
||||
+10
-2
@@ -5,9 +5,10 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
|
||||
/**
|
||||
* Convert the [AutofillRequest.Fillable] to an [AutofillSaveItem].
|
||||
* Convert the [AutofillRequest.Fillable] to an [AutofillSaveItem], or null if this partition does
|
||||
* not support save requests.
|
||||
*/
|
||||
fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem? =
|
||||
when (this.partition) {
|
||||
is AutofillPartition.Card -> {
|
||||
AutofillSaveItem.Card(
|
||||
@@ -33,4 +34,11 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
uri = uri,
|
||||
)
|
||||
}
|
||||
|
||||
is AutofillPartition.Identity -> {
|
||||
// Capturing identity data from a filled form is out of scope. This is never actually
|
||||
// reached because AutofillPartition.Identity.canPerformSaveRequest is always false, so
|
||||
// SaveInfo (and therefore a save callback) is never built for it.
|
||||
null
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -51,7 +51,7 @@ fun AutofillView.buildFilledItemOrNull(
|
||||
/**
|
||||
* Build a list [AutofillValue] out of [value] or return null if not possible.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
private fun AutofillView.buildListAutofillValueOrNull(
|
||||
value: String,
|
||||
): AutofillValue? =
|
||||
@@ -93,6 +93,23 @@ private fun AutofillView.buildListAutofillValueOrNull(
|
||||
is AutofillView.Login.Email,
|
||||
is AutofillView.Login.Password,
|
||||
is AutofillView.Login.Username,
|
||||
is AutofillView.Identity.AddressCountry,
|
||||
is AutofillView.Identity.AddressLocality,
|
||||
is AutofillView.Identity.AddressRegion,
|
||||
is AutofillView.Identity.AddressStreet,
|
||||
is AutofillView.Identity.Company,
|
||||
is AutofillView.Identity.Email,
|
||||
is AutofillView.Identity.LicenseNumber,
|
||||
is AutofillView.Identity.PassportNumber,
|
||||
is AutofillView.Identity.PersonNameFamily,
|
||||
is AutofillView.Identity.PersonNameFull,
|
||||
is AutofillView.Identity.PersonNameGiven,
|
||||
is AutofillView.Identity.PersonNameMiddle,
|
||||
is AutofillView.Identity.PersonNamePrefix,
|
||||
is AutofillView.Identity.PhoneFull,
|
||||
is AutofillView.Identity.PostalAddressFull,
|
||||
is AutofillView.Identity.PostalCode,
|
||||
is AutofillView.Identity.Ssn,
|
||||
is AutofillView.Unused,
|
||||
-> {
|
||||
this
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.autofill.util
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.platform.util.identityAutofillAddress
|
||||
import com.x8bit.bitwarden.data.platform.util.identityAutofillName
|
||||
import com.x8bit.bitwarden.data.platform.util.isActive
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
|
||||
@@ -46,6 +48,36 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getIdentityAutofillCiphers(): List<AutofillCipher.Identity> {
|
||||
val identityView = this@toAutofillCipherProvider.identity ?: return emptyList()
|
||||
return listOf(
|
||||
AutofillCipher.Identity(
|
||||
cipherId = id,
|
||||
name = name,
|
||||
subtitle = subtitle.orEmpty(),
|
||||
fullName = identityView.identityAutofillName.orEmpty(),
|
||||
fullAddress = identityView.identityAutofillAddress.orEmpty(),
|
||||
title = identityView.title.orEmpty(),
|
||||
firstName = identityView.firstName.orEmpty(),
|
||||
middleName = identityView.middleName.orEmpty(),
|
||||
lastName = identityView.lastName.orEmpty(),
|
||||
address1 = identityView.address1.orEmpty(),
|
||||
address2 = identityView.address2.orEmpty(),
|
||||
address3 = identityView.address3.orEmpty(),
|
||||
city = identityView.city.orEmpty(),
|
||||
state = identityView.state.orEmpty(),
|
||||
postalCode = identityView.postalCode.orEmpty(),
|
||||
country = identityView.country.orEmpty(),
|
||||
company = identityView.company.orEmpty(),
|
||||
email = identityView.email.orEmpty(),
|
||||
phone = identityView.phone.orEmpty(),
|
||||
ssn = identityView.ssn.orEmpty(),
|
||||
passportNumber = identityView.passportNumber.orEmpty(),
|
||||
licenseNumber = identityView.licenseNumber.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,10 @@ fun FilledData.buildVaultItemDataset(
|
||||
type = when (this.originalPartition) {
|
||||
is AutofillPartition.Card -> AutofillSelectionData.Type.CARD
|
||||
is AutofillPartition.Login -> AutofillSelectionData.Type.LOGIN
|
||||
// AutofillSelectionData.Type has no IDENTITY value yet (deliberately deferred -- it
|
||||
// cascades into the vault item listing/add-edit UI flows). This is a no-op placeholder
|
||||
// today since nothing yet produces an Identity-only partition.
|
||||
is AutofillPartition.Identity -> AutofillSelectionData.Type.LOGIN
|
||||
},
|
||||
uri = this.uri,
|
||||
)
|
||||
|
||||
@@ -252,6 +252,31 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
// Identity hint detection/dispatch is wired up in a later phase; treating these as Unused for
|
||||
// now is a no-op since nothing yet produces an IDENTITY_* hint.
|
||||
AutofillHint.IDENTITY_PERSON_NAME_FULL,
|
||||
AutofillHint.IDENTITY_PERSON_NAME_PREFIX,
|
||||
AutofillHint.IDENTITY_PERSON_NAME_GIVEN,
|
||||
AutofillHint.IDENTITY_PERSON_NAME_MIDDLE,
|
||||
AutofillHint.IDENTITY_PERSON_NAME_FAMILY,
|
||||
AutofillHint.IDENTITY_POSTAL_ADDRESS_FULL,
|
||||
AutofillHint.IDENTITY_ADDRESS_STREET,
|
||||
AutofillHint.IDENTITY_ADDRESS_LOCALITY,
|
||||
AutofillHint.IDENTITY_ADDRESS_REGION,
|
||||
AutofillHint.IDENTITY_ADDRESS_COUNTRY,
|
||||
AutofillHint.IDENTITY_POSTAL_CODE,
|
||||
AutofillHint.IDENTITY_PHONE_FULL,
|
||||
AutofillHint.IDENTITY_COMPANY,
|
||||
AutofillHint.IDENTITY_EMAIL,
|
||||
AutofillHint.IDENTITY_SSN,
|
||||
AutofillHint.IDENTITY_PASSPORT_NUMBER,
|
||||
AutofillHint.IDENTITY_LICENSE_NUMBER,
|
||||
-> {
|
||||
AutofillView.Unused(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.bitwarden.vault.CardView
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.IdentityView
|
||||
|
||||
/**
|
||||
* If someone has multiple AMEX cards, they tend to have the same last 4 digits. So we provide a
|
||||
@@ -80,3 +82,33 @@ private val CardView.subtitleCardNumber: String?
|
||||
*/
|
||||
private val String?.isAmEx: Boolean
|
||||
get() = this?.startsWith("34") == true || this?.startsWith("37") == true
|
||||
|
||||
/**
|
||||
* The full postal address for an [IdentityView] formatted for filling a combined full-address
|
||||
* autofill field. Missing parts are omitted entirely rather than padded with a placeholder, and
|
||||
* parts are joined with a single space -- unlike a display-oriented formatter, this string is
|
||||
* filled verbatim into a form field, so it can't rely on visual affordances like line breaks,
|
||||
* commas, or "-" placeholders that only read sensibly in a UI layout.
|
||||
*/
|
||||
internal val IdentityView.identityAutofillAddress: String?
|
||||
get() = listOfNotNull(
|
||||
address1,
|
||||
address2,
|
||||
address3,
|
||||
city,
|
||||
state,
|
||||
postalCode,
|
||||
country,
|
||||
)
|
||||
.joinToString(" ")
|
||||
.orNullIfBlank()
|
||||
|
||||
/**
|
||||
* The full name for an [IdentityView] formatted for filling a combined full-name autofill field.
|
||||
* Excludes [IdentityView.title] -- a title prefix (Mr/Mrs/Dr) is its own separate autofill field
|
||||
* (`AutofillView.Identity.PersonNamePrefix`).
|
||||
*/
|
||||
internal val IdentityView.identityAutofillName: String?
|
||||
get() = listOfNotNull(firstName, middleName, lastName)
|
||||
.joinToString(" ")
|
||||
.orNullIfBlank()
|
||||
@@ -38,4 +38,9 @@ private fun getAutofillSuggestionCipherType(
|
||||
autofillAppInfo.context,
|
||||
BitwardenString.type_login,
|
||||
)
|
||||
|
||||
is AutofillCipher.Identity -> ContextCompat.getString(
|
||||
autofillAppInfo.context,
|
||||
BitwardenString.type_identity,
|
||||
)
|
||||
}
|
||||
+19
-13
@@ -383,6 +383,22 @@ private val CardView.paymentCardBrandIconRes: Int?
|
||||
-> null
|
||||
}
|
||||
|
||||
private val CardView.cardBrand: VaultCardBrand?
|
||||
get() = brand
|
||||
?.findVaultCardBrandWithNameOrNull()
|
||||
.takeUnless { it == VaultCardBrand.SELECT }
|
||||
|
||||
private val CardView.expiration: String?
|
||||
get() = listOfNotNull(
|
||||
expMonth?.padStart(length = 2, padChar = '0'),
|
||||
expYear,
|
||||
)
|
||||
.joinToString("/")
|
||||
.orNullIfBlank()
|
||||
|
||||
/**
|
||||
* The full postal address for an [IdentityView], joining its address parts.
|
||||
*/
|
||||
private val IdentityView.identityAddress: String?
|
||||
get() = listOfNotNull(
|
||||
address1,
|
||||
@@ -396,6 +412,9 @@ private val IdentityView.identityAddress: String?
|
||||
.joinToString("\n")
|
||||
.orNullIfBlank()
|
||||
|
||||
/**
|
||||
* The full name for an [IdentityView], joining its name parts.
|
||||
*/
|
||||
private val IdentityView.identityName: String?
|
||||
get() = listOfNotNull(
|
||||
title
|
||||
@@ -409,16 +428,3 @@ private val IdentityView.identityName: String?
|
||||
)
|
||||
.joinToString(" ")
|
||||
.orNullIfBlank()
|
||||
|
||||
private val CardView.cardBrand: VaultCardBrand?
|
||||
get() = brand
|
||||
?.findVaultCardBrandWithNameOrNull()
|
||||
.takeUnless { it == VaultCardBrand.SELECT }
|
||||
|
||||
private val CardView.expiration: String?
|
||||
get() = listOfNotNull(
|
||||
expMonth?.padStart(length = 2, padChar = '0'),
|
||||
expYear,
|
||||
)
|
||||
.joinToString("/")
|
||||
.orNullIfBlank()
|
||||
@@ -99,6 +99,30 @@ class SaveInfoBuilderTest {
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should return null for an identity partition since it never supports save requests`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
val autofillPartitionIdentity = AutofillPartition.Identity(
|
||||
views = listOf(
|
||||
AutofillView.Identity.Email(
|
||||
data = autofillViewDataValid,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillPartition = autofillPartitionIdentity,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build should return null if autofill enabled and can't perform autofill`() {
|
||||
// Setup
|
||||
|
||||
+176
@@ -10,6 +10,7 @@ import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.IdentityView
|
||||
import com.bitwarden.vault.LoginListView
|
||||
import com.bitwarden.vault.LoginView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -20,6 +21,8 @@ import com.x8bit.bitwarden.data.autofill.util.card
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.identityAutofillAddress
|
||||
import com.x8bit.bitwarden.data.platform.util.identityAutofillName
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPolicyView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
@@ -132,6 +135,42 @@ class AutofillCipherProviderTest {
|
||||
every { reprompt } returns CipherRepromptType.NONE
|
||||
every { type } returns CipherType.LOGIN
|
||||
}
|
||||
private val identityCipherListView: CipherListView = mockk {
|
||||
every { archivedDate } returns null
|
||||
every { deletedDate } returns null
|
||||
every { id } returns IDENTITY_CIPHER_ID
|
||||
every { name } returns IDENTITY_NAME
|
||||
every { reprompt } returns CipherRepromptType.NONE
|
||||
every { type } returns CipherListViewType.Identity
|
||||
}
|
||||
private val identityView: IdentityView = mockk {
|
||||
every { title } returns IDENTITY_TITLE
|
||||
every { firstName } returns IDENTITY_FIRST_NAME
|
||||
every { middleName } returns IDENTITY_MIDDLE_NAME
|
||||
every { lastName } returns IDENTITY_LAST_NAME
|
||||
every { address1 } returns IDENTITY_ADDRESS_1
|
||||
every { address2 } returns IDENTITY_ADDRESS_2
|
||||
every { address3 } returns IDENTITY_ADDRESS_3
|
||||
every { city } returns IDENTITY_CITY
|
||||
every { state } returns IDENTITY_STATE
|
||||
every { postalCode } returns IDENTITY_POSTAL_CODE
|
||||
every { country } returns IDENTITY_COUNTRY
|
||||
every { company } returns IDENTITY_COMPANY
|
||||
every { email } returns IDENTITY_EMAIL
|
||||
every { phone } returns IDENTITY_PHONE
|
||||
every { ssn } returns IDENTITY_SSN
|
||||
every { passportNumber } returns IDENTITY_PASSPORT_NUMBER
|
||||
every { licenseNumber } returns IDENTITY_LICENSE_NUMBER
|
||||
}
|
||||
private val identityCipherView: CipherView = mockk {
|
||||
every { identity } returns identityView
|
||||
every { archivedDate } returns null
|
||||
every { deletedDate } returns null
|
||||
every { id } returns IDENTITY_CIPHER_ID
|
||||
every { name } returns IDENTITY_NAME
|
||||
every { reprompt } returns CipherRepromptType.NONE
|
||||
every { type } returns CipherType.IDENTITY
|
||||
}
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { activeUserId } returns ACTIVE_USER_ID
|
||||
}
|
||||
@@ -536,6 +575,97 @@ class AutofillCipherProviderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getIdentityAutofillCiphers when unlocked should return empty list when retrieving ciphers times out`() =
|
||||
runTest {
|
||||
coEvery { vaultRepository.isVaultUnlocked(ACTIVE_USER_ID) } returns true
|
||||
mutableCipherListViewsWithFailuresStateFlow.value = DataState.Loading
|
||||
|
||||
// Test
|
||||
val actual = async {
|
||||
autofillCipherProvider.getIdentityAutofillCiphers()
|
||||
}
|
||||
|
||||
testScheduler.runCurrent()
|
||||
assertFalse(actual.isCompleted)
|
||||
testScheduler.advanceTimeBy(delayTimeMillis = 2_000L)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Verify
|
||||
assertTrue(actual.isCompleted)
|
||||
assertEquals(emptyList<AutofillCipher.Identity>(), actual.await())
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getIdentityAutofillCiphers when unlocked should decrypt then return non-null, non-deleted, and non-reprompt identity ciphers`() =
|
||||
runTest {
|
||||
val archivedIdentityCipherView: CipherListView = mockk {
|
||||
every { archivedDate } returns mockk()
|
||||
every { deletedDate } returns null
|
||||
every { type } returns CipherListViewType.Identity
|
||||
}
|
||||
val deletedIdentityCipherView: CipherListView = mockk {
|
||||
every { archivedDate } returns null
|
||||
every { deletedDate } returns mockk()
|
||||
every { type } returns CipherListViewType.Identity
|
||||
}
|
||||
val repromptIdentityCipherView: CipherListView = mockk {
|
||||
every { archivedDate } returns null
|
||||
every { deletedDate } returns null
|
||||
every { reprompt } returns CipherRepromptType.PASSWORD
|
||||
every { type } returns CipherListViewType.Identity
|
||||
}
|
||||
val decryptCipherListViewsResult = DecryptCipherListResult(
|
||||
successes = listOf(
|
||||
identityCipherListView,
|
||||
archivedIdentityCipherView,
|
||||
deletedIdentityCipherView,
|
||||
repromptIdentityCipherView,
|
||||
loginCipherListViewWithTotp,
|
||||
loginCipherListViewWithoutTotp,
|
||||
),
|
||||
failures = emptyList(),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.getCipher(IDENTITY_CIPHER_ID)
|
||||
} returns GetCipherResult.Success(
|
||||
cipherView = identityCipherView,
|
||||
)
|
||||
|
||||
mutableCipherListViewsWithFailuresStateFlow.value = DataState.Loaded(
|
||||
data = decryptCipherListViewsResult,
|
||||
)
|
||||
mutableVaultStateFlow.value = listOf(
|
||||
VaultUnlockData(
|
||||
userId = ACTIVE_USER_ID,
|
||||
status = VaultUnlockData.Status.UNLOCKED,
|
||||
),
|
||||
)
|
||||
every { identityCipherView.subtitle } returns IDENTITY_SUBTITLE
|
||||
every { identityView.identityAutofillName } returns IDENTITY_FULL_NAME
|
||||
every { identityView.identityAutofillAddress } returns IDENTITY_FULL_ADDRESS
|
||||
val expected = listOf(IDENTITY_AUTOFILL_CIPHER)
|
||||
|
||||
// Test & Verify
|
||||
val actual = autofillCipherProvider.getIdentityAutofillCiphers()
|
||||
|
||||
coVerify(exactly = 1) { vaultRepository.getCipher(IDENTITY_CIPHER_ID) }
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getIdentityAutofillCiphers when locked should return an empty list`() = runTest {
|
||||
mutableVaultStateFlow.value = emptyList()
|
||||
|
||||
// Test & Verify
|
||||
val actual = autofillCipherProvider.getIdentityAutofillCiphers()
|
||||
|
||||
assertEquals(emptyList<AutofillCipher.Identity>(), actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getLoginAutofillCiphers when decryption result is CipherNotFound should log the error and omit cipher from results`() =
|
||||
@@ -644,3 +774,49 @@ private val LOGIN_AUTOFILL_CIPHER_WITHOUT_TOTP = AutofillCipher.Login(
|
||||
username = LOGIN_USERNAME,
|
||||
website = URI,
|
||||
)
|
||||
private const val IDENTITY_CIPHER_ID = "IDENTITY_CIPHER_ID"
|
||||
private const val IDENTITY_NAME = "John's Identity"
|
||||
private const val IDENTITY_TITLE = "IDENTITY_TITLE"
|
||||
private const val IDENTITY_FIRST_NAME = "IDENTITY_FIRST_NAME"
|
||||
private const val IDENTITY_MIDDLE_NAME = "IDENTITY_MIDDLE_NAME"
|
||||
private const val IDENTITY_LAST_NAME = "IDENTITY_LAST_NAME"
|
||||
private const val IDENTITY_ADDRESS_1 = "IDENTITY_ADDRESS_1"
|
||||
private const val IDENTITY_ADDRESS_2 = "IDENTITY_ADDRESS_2"
|
||||
private const val IDENTITY_ADDRESS_3 = "IDENTITY_ADDRESS_3"
|
||||
private const val IDENTITY_CITY = "IDENTITY_CITY"
|
||||
private const val IDENTITY_STATE = "IDENTITY_STATE"
|
||||
private const val IDENTITY_POSTAL_CODE = "IDENTITY_POSTAL_CODE"
|
||||
private const val IDENTITY_COUNTRY = "IDENTITY_COUNTRY"
|
||||
private const val IDENTITY_COMPANY = "IDENTITY_COMPANY"
|
||||
private const val IDENTITY_EMAIL = "IDENTITY_EMAIL"
|
||||
private const val IDENTITY_PHONE = "IDENTITY_PHONE"
|
||||
private const val IDENTITY_SSN = "IDENTITY_SSN"
|
||||
private const val IDENTITY_PASSPORT_NUMBER = "IDENTITY_PASSPORT_NUMBER"
|
||||
private const val IDENTITY_LICENSE_NUMBER = "IDENTITY_LICENSE_NUMBER"
|
||||
private const val IDENTITY_SUBTITLE = "IDENTITY_SUBTITLE"
|
||||
private const val IDENTITY_FULL_NAME = "IDENTITY_FULL_NAME"
|
||||
private const val IDENTITY_FULL_ADDRESS = "IDENTITY_FULL_ADDRESS"
|
||||
private val IDENTITY_AUTOFILL_CIPHER = AutofillCipher.Identity(
|
||||
cipherId = IDENTITY_CIPHER_ID,
|
||||
name = IDENTITY_NAME,
|
||||
subtitle = IDENTITY_SUBTITLE,
|
||||
fullName = IDENTITY_FULL_NAME,
|
||||
fullAddress = IDENTITY_FULL_ADDRESS,
|
||||
title = IDENTITY_TITLE,
|
||||
firstName = IDENTITY_FIRST_NAME,
|
||||
middleName = IDENTITY_MIDDLE_NAME,
|
||||
lastName = IDENTITY_LAST_NAME,
|
||||
address1 = IDENTITY_ADDRESS_1,
|
||||
address2 = IDENTITY_ADDRESS_2,
|
||||
address3 = IDENTITY_ADDRESS_3,
|
||||
city = IDENTITY_CITY,
|
||||
state = IDENTITY_STATE,
|
||||
postalCode = IDENTITY_POSTAL_CODE,
|
||||
country = IDENTITY_COUNTRY,
|
||||
company = IDENTITY_COMPANY,
|
||||
email = IDENTITY_EMAIL,
|
||||
phone = IDENTITY_PHONE,
|
||||
ssn = IDENTITY_SSN,
|
||||
passportNumber = IDENTITY_PASSPORT_NUMBER,
|
||||
licenseNumber = IDENTITY_LICENSE_NUMBER,
|
||||
)
|
||||
+53
@@ -418,6 +418,59 @@ class AutofillProcessorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processSaveRequest should invoke empty callback when autofill enabled, has fill contexts, and toAutofillSaveItem returns null`() {
|
||||
// Setup
|
||||
val saveCallback: SaveCallback = mockk {
|
||||
every { onSuccess() } just runs
|
||||
}
|
||||
val assistStructure: AssistStructure = mockk()
|
||||
val fillContext: FillContext = mockk {
|
||||
every { structure } returns assistStructure
|
||||
}
|
||||
val saveRequest: SaveRequest = mockk {
|
||||
every { fillContexts } returns listOf(fillContext)
|
||||
}
|
||||
val autofillPartition: AutofillPartition = mockk()
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk {
|
||||
every { packageName } returns PACKAGE_NAME
|
||||
every { partition } returns autofillPartition
|
||||
every { toAutofillSaveItem() } returns null
|
||||
}
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every {
|
||||
policyManager.getActivePolicies(PolicyType.ORGANIZATION_DATA_OWNERSHIP)
|
||||
} returns emptyList()
|
||||
every {
|
||||
parser.parse(
|
||||
autofillAppInfo = appInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
} returns autofillRequest
|
||||
|
||||
// Test
|
||||
autofillProcessor.processSaveRequest(
|
||||
autofillAppInfo = appInfo,
|
||||
request = saveRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isAutofillSavePromptDisabled
|
||||
policyManager.getActivePolicies(PolicyType.ORGANIZATION_DATA_OWNERSHIP)
|
||||
parser.parse(
|
||||
autofillAppInfo = appInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
saveCallback.onSuccess()
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
createAutofillSavedItemIntentSender(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processSaveRequest should invoke empty callback when autofill enabled, has fill contexts, and parser returns Unfillable`() {
|
||||
|
||||
+16
@@ -9,6 +9,7 @@ import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -103,6 +104,21 @@ class AutofillRequestExtensionsTest {
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillSaveItem should return null when identity partition`() {
|
||||
// Setup
|
||||
val autofillPartition: AutofillPartition.Identity = mockk()
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk {
|
||||
every { partition } returns autofillPartition
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = autofillRequest.toAutofillSaveItem()
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val AUTOFILL_REQUEST_EXTENSIONS_PATH =
|
||||
|
||||
+30
@@ -234,6 +234,36 @@ class AutofillViewExtensionsTest {
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `buildFilledItemOrNull should return index list value when list type and identity view type`() {
|
||||
// Setup
|
||||
val value = "Acme Corp"
|
||||
val autofillViewData = autofillViewData.copy(
|
||||
autofillType = View.AUTOFILL_TYPE_LIST,
|
||||
autofillOptions = listOf("Other Corp", value, "Third Corp"),
|
||||
)
|
||||
val autofillView = AutofillView.Identity.Company(
|
||||
data = autofillViewData,
|
||||
)
|
||||
val expected = FilledItem(
|
||||
autofillId = autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
every { AutofillValue.forList(1) } returns autofillValue
|
||||
|
||||
// Test
|
||||
val actual = autofillView.buildFilledItemOrNull(
|
||||
value = value,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
verify(exactly = 1) {
|
||||
AutofillValue.forList(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildFilledItemOrNull should return text value when text type`() {
|
||||
// Setup
|
||||
|
||||
+57
@@ -113,6 +113,58 @@ class CipherViewExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillCipherProvider should return a provider with the correct data for an Identity type`() =
|
||||
runTest {
|
||||
val cipherView = createMockCipherView(
|
||||
number = 1,
|
||||
cipherType = CipherType.IDENTITY,
|
||||
)
|
||||
|
||||
val autofillCipherProvider = cipherView.toAutofillCipherProvider()
|
||||
|
||||
assertFalse(autofillCipherProvider.isVaultLocked())
|
||||
assertEquals(
|
||||
emptyList<AutofillCipher.Card>(),
|
||||
autofillCipherProvider.getCardAutofillCiphers(),
|
||||
)
|
||||
assertEquals(
|
||||
emptyList<AutofillCipher.Login>(),
|
||||
autofillCipherProvider.getLoginAutofillCiphers(uri = "uri"),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(
|
||||
AutofillCipher.Identity(
|
||||
cipherId = "mockId-1",
|
||||
name = "mockName-1",
|
||||
subtitle = "mockFirstName-1 mockLastName-1",
|
||||
fullName = "mockFirstName-1 mockMiddleName-1 mockLastName-1",
|
||||
fullAddress = "mockAddress1-1 mockAddress2-1 mockAddress3-1 " +
|
||||
"mockCity-1 mockState-1 mockPostalCode-1 mockCountry-1",
|
||||
title = "mockTitle-1",
|
||||
firstName = "mockFirstName-1",
|
||||
middleName = "mockMiddleName-1",
|
||||
lastName = "mockLastName-1",
|
||||
address1 = "mockAddress1-1",
|
||||
address2 = "mockAddress2-1",
|
||||
address3 = "mockAddress3-1",
|
||||
city = "mockCity-1",
|
||||
state = "mockState-1",
|
||||
postalCode = "mockPostalCode-1",
|
||||
country = "mockCountry-1",
|
||||
company = "mockCompany-1",
|
||||
email = "mockEmail-1",
|
||||
phone = "mockPhone-1",
|
||||
ssn = "mockSsn-1",
|
||||
passportNumber = "mockPassportNumber-1",
|
||||
licenseNumber = "mockLicenseNumber-1",
|
||||
),
|
||||
),
|
||||
autofillCipherProvider.getIdentityAutofillCiphers(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toAutofillCipherProvider should return a provider with the correct data for any other type`() =
|
||||
@@ -121,6 +173,7 @@ class CipherViewExtensionsTest {
|
||||
.entries
|
||||
.filterNot { it == CipherType.CARD }
|
||||
.filterNot { it == CipherType.LOGIN }
|
||||
.filterNot { it == CipherType.IDENTITY }
|
||||
.forEach { cipherType ->
|
||||
val autofillCipherProvider = createMockCipherView(
|
||||
number = 1,
|
||||
@@ -137,6 +190,10 @@ class CipherViewExtensionsTest {
|
||||
emptyList<AutofillCipher.Login>(),
|
||||
autofillCipherProvider.getLoginAutofillCiphers(uri = "uri"),
|
||||
)
|
||||
assertEquals(
|
||||
emptyList<AutofillCipher.Identity>(),
|
||||
autofillCipherProvider.getIdentityAutofillCiphers(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+84
@@ -1,9 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.IdentityView
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
@@ -12,6 +15,27 @@ import java.time.ZoneOffset
|
||||
|
||||
class CipherViewExtensionsTest {
|
||||
|
||||
private val emptyIdentityView = IdentityView(
|
||||
title = null,
|
||||
firstName = null,
|
||||
middleName = null,
|
||||
lastName = null,
|
||||
address1 = null,
|
||||
address2 = null,
|
||||
address3 = null,
|
||||
city = null,
|
||||
state = null,
|
||||
postalCode = null,
|
||||
country = null,
|
||||
company = null,
|
||||
email = null,
|
||||
phone = null,
|
||||
ssn = null,
|
||||
username = null,
|
||||
passportNumber = null,
|
||||
licenseNumber = null,
|
||||
)
|
||||
|
||||
private val clock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
@@ -56,4 +80,64 @@ class CipherViewExtensionsTest {
|
||||
|
||||
assertFalse(cipherListView.isActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identityAutofillName should return null when all name parts are null`() {
|
||||
assertNull(emptyIdentityView.identityAutofillName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identityAutofillName should exclude the title and join the remaining name parts`() {
|
||||
val identityView = emptyIdentityView.copy(
|
||||
title = "mr",
|
||||
firstName = "firstName",
|
||||
middleName = "middleName",
|
||||
lastName = "lastName",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"firstName middleName lastName",
|
||||
identityView.identityAutofillName,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identityAutofillAddress should return null when all address parts are null`() {
|
||||
assertNull(emptyIdentityView.identityAutofillAddress)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `identityAutofillAddress should omit missing parts entirely rather than using a placeholder`() {
|
||||
val identityView = emptyIdentityView.copy(
|
||||
address1 = null,
|
||||
address2 = null,
|
||||
address3 = "address3",
|
||||
city = null,
|
||||
state = "state",
|
||||
postalCode = null,
|
||||
country = null,
|
||||
)
|
||||
|
||||
assertEquals("address3 state", identityView.identityAutofillAddress)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `identityAutofillAddress should join all address parts with a single space when fully populated`() {
|
||||
val identityView = emptyIdentityView.copy(
|
||||
address1 = "address1",
|
||||
address2 = "address2",
|
||||
address3 = "address3",
|
||||
city = "city",
|
||||
state = "state",
|
||||
postalCode = "postalCode",
|
||||
country = "country",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"address1 address2 address3 city state postalCode country",
|
||||
identityView.identityAutofillAddress,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ class AutofillUtilsTest {
|
||||
every {
|
||||
ContextCompat.getString(context, BitwardenString.type_login)
|
||||
} returns "Login"
|
||||
every {
|
||||
ContextCompat.getString(context, BitwardenString.type_identity)
|
||||
} returns "Identity"
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -113,6 +116,38 @@ class AutofillUtilsTest {
|
||||
),
|
||||
third = "Autofill suggestion, Login, Amazon, AmazonSubtitle",
|
||||
),
|
||||
Triple(
|
||||
first = AutofillCipher.Identity(
|
||||
cipherId = null,
|
||||
name = "Cipher One",
|
||||
subtitle = "Subtitle",
|
||||
fullName = "fullName",
|
||||
fullAddress = "fullAddress",
|
||||
title = "title",
|
||||
firstName = "firstName",
|
||||
middleName = "middleName",
|
||||
lastName = "lastName",
|
||||
address1 = "address1",
|
||||
address2 = "address2",
|
||||
address3 = "address3",
|
||||
city = "city",
|
||||
state = "state",
|
||||
postalCode = "postalCode",
|
||||
country = "country",
|
||||
company = "company",
|
||||
email = "email",
|
||||
phone = "phone",
|
||||
ssn = "ssn",
|
||||
passportNumber = "passportNumber",
|
||||
licenseNumber = "licenseNumber",
|
||||
),
|
||||
second = AutofillAppInfo(
|
||||
context = context,
|
||||
packageName = "com.x8bit.bitwarden",
|
||||
sdkInt = 34,
|
||||
),
|
||||
third = "Autofill suggestion, Identity, Cipher One, Subtitle",
|
||||
),
|
||||
)
|
||||
.forEach {
|
||||
val result = getAutofillSuggestionContentDescription(
|
||||
|
||||
Reference in new issue
Block a user