PM-26560: Fix cross-origin autofill issues (#5977)

This commit is contained in:
David Perez 2025-10-10 16:06:59 -05:00 committed by GitHub
parent 5706ca2ba3
commit 0604d15d7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 262 additions and 191 deletions

View File

@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.util.buildFilledItemOrNull
import com.x8bit.bitwarden.data.autofill.util.buildUri
import timber.log.Timber
/**
@ -83,6 +84,7 @@ class FilledDataBuilderImpl(
autofillCipher = autofillCipher,
autofillViews = autofillRequest.partition.views,
inlinePresentationSpec = getCipherInlinePresentationOrNull(),
packageName = autofillRequest.packageName,
)
}
}
@ -96,7 +98,9 @@ class FilledDataBuilderImpl(
?.getOrLastOrNull(inlineSuggestionsAdded)
return FilledData(
filledPartitions = filledPartitions.take(n = MAX_FILLED_PARTITIONS_COUNT),
filledPartitions = filledPartitions
.filter { it.filledItems.isNotEmpty() }
.take(n = MAX_FILLED_PARTITIONS_COUNT),
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
originalPartition = autofillRequest.partition,
uri = autofillRequest.uri,
@ -140,16 +144,21 @@ class FilledDataBuilderImpl(
autofillCipher: AutofillCipher.Login,
autofillViews: List<AutofillView.Login>,
inlinePresentationSpec: InlinePresentationSpec?,
packageName: String?,
): FilledPartition {
val filledItems = autofillViews
.mapNotNull { autofillView ->
val value = when (autofillView) {
is AutofillView.Login.Username -> autofillCipher.username
is AutofillView.Login.Password -> autofillCipher.password
if (autofillView.data.website == autofillCipher.website ||
buildUri(packageName.orEmpty(), "androidapp") == autofillCipher.website
) {
val value = when (autofillView) {
is AutofillView.Login.Username -> autofillCipher.username
is AutofillView.Login.Password -> autofillCipher.password
}
autofillView.buildFilledItemOrNull(value = value)
} else {
null
}
autofillView.buildFilledItemOrNull(
value = value,
)
}
return FilledPartition(

View File

@ -66,6 +66,7 @@ sealed class AutofillCipher {
override val subtitle: String,
val password: String,
val username: String,
val website: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = BitwardenDrawable.ic_globe

View File

@ -16,6 +16,7 @@ sealed class AutofillView {
* @param isFocused Whether the view is currently focused.
* @param textValue A text value that represents the input present in the field.
* @param hasPasswordTerms Indicates that the field includes password terms.
* @param website website associated with this view.
*/
data class Data(
val autofillId: AutofillId,
@ -24,6 +25,7 @@ sealed class AutofillView {
val isFocused: Boolean,
val textValue: String?,
val hasPasswordTerms: Boolean,
val website: String?,
)
/**

View File

@ -8,11 +8,9 @@ import android.view.autofill.AutofillId
* @param autofillViews The list of views we care about for autofilling.
* @param idPackage The package id for this view, if there is one.
* @param ignoreAutofillIds The list of [AutofillId]s that should be ignored in the fill response.
* @param website The website that is being displayed in the app, given there is one.
*/
data class ViewNodeTraversalData(
val autofillViews: List<AutofillView>,
val idPackage: String?,
val ignoreAutofillIds: List<AutofillId>,
val website: String?,
)

View File

@ -95,16 +95,21 @@ class AutofillParserImpl(
.firstOrNull { it.data.isFocused }
?: autofillViews.firstOrNull()
if (focusedView == null) {
// The view is unfillable if there are no focused views.
return AutofillRequest.Unfillable
}
val packageName = traversalDataList.buildPackageNameOrNull(
assistStructure = assistStructure,
)
val uri = traversalDataList.buildUriOrNull(
val uri = focusedView.buildUriOrNull(
packageName = packageName,
)
val blockListedURIs = settingsRepository.blockedAutofillUris + BLOCK_LISTED_URIS
if (focusedView == null || blockListedURIs.contains(uri)) {
// The view is unfillable if there are no focused views or the URI is block listed.
if (blockListedURIs.contains(uri)) {
// The view is unfillable if the URI is block listed.
return AutofillRequest.Unfillable
}
@ -165,7 +170,7 @@ private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
.mapNotNull { windowNode ->
windowNode
.rootViewNode
?.traverse()
?.traverse(parentWebsite = null)
?.updateForMissingPasswordFields()
?.updateForMissingUsernameFields()
}
@ -243,16 +248,17 @@ private fun ViewNodeTraversalData.copyAndMapAutofillViews(
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
* data into [ViewNodeTraversalData].
*/
private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
private fun AssistStructure.ViewNode.traverse(
parentWebsite: String?,
): ViewNodeTraversalData {
// Set up mutable lists for collecting valid AutofillViews and ignorable view ids.
val mutableAutofillViewList: MutableList<AutofillView> = mutableListOf()
val mutableIgnoreAutofillIdList: MutableList<AutofillId> = mutableListOf()
var idPackage: String? = this.idPackage
var website: String? = this.website
// Try converting this `ViewNode` into an `AutofillView`. If a valid instance is returned, add
// it to the list. Otherwise, ignore the `AutofillId` associated with this `ViewNode`.
toAutofillView()
toAutofillView(parentWebsite = parentWebsite)
?.run(mutableAutofillViewList::add)
?: autofillId?.run(mutableIgnoreAutofillIdList::add)
@ -260,7 +266,7 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
for (i in 0 until childCount) {
// Extract the traversal data from each child view node and add it to the lists.
getChildAt(i)
.traverse()
.traverse(parentWebsite = website)
.let { viewNodeTraversalData ->
viewNodeTraversalData.autofillViews.forEach(mutableAutofillViewList::add)
viewNodeTraversalData.ignoreAutofillIds.forEach(mutableIgnoreAutofillIdList::add)
@ -273,10 +279,6 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
) {
idPackage = viewNodeTraversalData.idPackage
}
// Get the first non-null website.
if (website == null) {
website = viewNodeTraversalData.website
}
}
}
@ -286,6 +288,5 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
autofillViews = mutableAutofillViewList,
idPackage = idPackage,
ignoreAutofillIds = mutableIgnoreAutofillIdList,
website = website,
)
}

View File

@ -127,6 +127,7 @@ class AutofillCipherProviderImpl(
password = cipherView.login?.password.orEmpty(),
subtitle = cipherView.subtitle.orEmpty(),
username = cipherView.login?.username.orEmpty(),
website = uri,
)
}
}

View File

@ -4,9 +4,15 @@ import android.view.View
import android.view.autofill.AutofillValue
import com.x8bit.bitwarden.data.autofill.model.AutofillView
import com.x8bit.bitwarden.data.autofill.model.FilledItem
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"
/**
* Convert this [AutofillView] into a [FilledItem]. Return null if not possible.
*/
@ -96,3 +102,17 @@ private fun AutofillView.buildListAutofillValueOrNull(
?.let { AutofillValue.forList(it) }
}
}
/**
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
* that fails, try converting [packageName] into an Android app URI.
*/
fun AutofillView.buildUriOrNull(
packageName: String?,
): String? {
// Search list of ViewNodeTraversalData for a website URI.
this.data.website?.let { websiteUri -> return websiteUri }
// If the package name is available, build a URI out of that.
return packageName?.let { buildUri(domain = it, scheme = ANDROID_APP_SCHEME) }
}

View File

@ -41,6 +41,7 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
password = login.password.orEmpty(),
subtitle = subtitle.orEmpty(),
username = login.username.orEmpty(),
website = uri,
),
)
}

View File

@ -49,7 +49,9 @@ private val AssistStructure.ViewNode.isInputField: Boolean
* doesn't contain a valid autofillId, it isn't an a view setup for autofill, so we return null. If
* it doesn't have a supported hint and isn't an input field, we also return null.
*/
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
fun AssistStructure.ViewNode.toAutofillView(
parentWebsite: String?,
): AutofillView? =
this
.autofillId
// We only care about nodes with a valid `AutofillId`.
@ -67,6 +69,7 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
isFocused = this.isFocused,
textValue = this.autofillValue?.extractTextValue(),
hasPasswordTerms = this.hasPasswordTerms(),
website = this.website ?: parentWebsite,
)
buildAutofillView(
autofillOptions = autofillOptions,

View File

@ -4,36 +4,6 @@ import android.app.assist.AssistStructure
import com.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"
/**
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
* that fails, try converting [packageName] into an Android app URI.
*/
fun List<ViewNodeTraversalData>.buildUriOrNull(
packageName: String?,
): String? {
// Search list of ViewNodeTraversalData for a website URI.
this
.firstOrNull { it.website != null }
?.website
?.let { websiteUri ->
return websiteUri
}
// If the package name is available, build a URI out of that.
return packageName
?.let { nonNullPackageName ->
buildUri(
domain = nonNullPackageName,
scheme = ANDROID_APP_SCHEME,
)
}
}
/**
* Try and build a package name. First, try searching traversal data for package names. If that
* fails, try extracting a package name from [assistStructure].

View File

@ -145,6 +145,7 @@ class FillResponseBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
),
),
),
@ -246,6 +247,7 @@ class FillResponseBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
),
),
),

View File

@ -61,16 +61,20 @@ class FilledDataBuilderTest {
password = password,
username = username,
subtitle = "Subtitle",
website = URI,
)
val filledItemPassword: FilledItem = mockk()
val filledItemUsername: FilledItem = mockk()
val autofillViewPassword: AutofillView.Login.Password = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(password) } returns filledItemPassword
}
val autofillViewUsernameOne: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns filledItemUsername
}
val autofillViewUsernameTwo: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns null
}
val autofillPartition = AutofillPartition.Login(
@ -341,15 +345,8 @@ class FilledDataBuilderTest {
partition = autofillPartition,
uri = URI,
)
val filledPartition = FilledPartition(
autofillCipher = autofillCipher,
filledItems = emptyList(),
inlinePresentationSpec = null,
)
val expected = FilledData(
filledPartitions = listOf(
filledPartition,
),
filledPartitions = emptyList(),
ignoreAutofillIds = ignoreAutofillIds,
originalPartition = autofillPartition,
uri = URI,
@ -396,14 +393,17 @@ class FilledDataBuilderTest {
password = password,
username = username,
subtitle = "Subtitle",
website = URI,
)
val filledItemPassword: FilledItem = mockk()
val filledItemUsername: FilledItem = mockk()
val autofillViewPassword: AutofillView.Login.Password = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(password) } returns filledItemPassword
}
val autofillViewUsername: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns filledItemUsername
}
val autofillPartition = AutofillPartition.Login(
@ -490,13 +490,16 @@ class FilledDataBuilderTest {
password = password,
username = username,
subtitle = "Subtitle",
website = URI,
)
val filledItemPassword: FilledItem = mockk()
val filledItemUsername: FilledItem = mockk()
val autofillViewPassword: AutofillView.Login.Password = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(password) } returns filledItemPassword
}
val autofillViewUsername: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns filledItemUsername
}
val autofillPartition = AutofillPartition.Login(

View File

@ -35,6 +35,7 @@ class SaveInfoBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
)
private val autofillIdValid: AutofillId = mockk()
private val autofillViewDataValid = AutofillView.Data(
@ -44,6 +45,7 @@ class SaveInfoBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
)
private val autofillPartitionCard: AutofillPartition.Card = AutofillPartition.Card(
views = listOf(

View File

@ -78,8 +78,9 @@ class AutofillParserTests {
mockkStatic(
FillRequest::getMaxInlineSuggestionsCount,
FillRequest::getInlinePresentationSpecs,
AutofillView::buildUriOrNull,
List<ViewNodeTraversalData>::buildPackageNameOrNull,
)
mockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
every { cardViewNode.website } returns WEBSITE
every { loginViewNode.website } returns WEBSITE
every {
@ -121,7 +122,7 @@ class AutofillParserTests {
every {
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
} returns PACKAGE_NAME
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME) } returns URI
every { any<AutofillView>().buildUriOrNull(PACKAGE_NAME) } returns URI
parser = AutofillParserImpl(
settingsRepository = settingsRepository,
)
@ -134,8 +135,9 @@ class AutofillParserTests {
unmockkStatic(
FillRequest::getMaxInlineSuggestionsCount,
FillRequest::getInlinePresentationSpecs,
AutofillView::buildUriOrNull,
List<ViewNodeTraversalData>::buildPackageNameOrNull,
)
unmockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
}
@Test
@ -181,7 +183,7 @@ class AutofillParserTests {
every { this@mockk.childCount } returns 0
every { this@mockk.idPackage } returns null
every { this@mockk.isFocused } returns false
every { this@mockk.toAutofillView() } returns null
every { this@mockk.toAutofillView(parentWebsite = any()) } returns null
every { this@mockk.website } returns null
}
// `invalidChildViewNode` simulates the OS assigning a node's idPackage to "android", which
@ -194,7 +196,7 @@ class AutofillParserTests {
every { this@mockk.childCount } returns 0
every { this@mockk.idPackage } returns ID_PACKAGE_ANDROID
every { this@mockk.isFocused } returns false
every { this@mockk.toAutofillView() } returns null
every { this@mockk.toAutofillView(parentWebsite = any()) } returns null
every { this@mockk.website } returns null
}
val parentAutofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR
@ -207,6 +209,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -214,7 +217,7 @@ class AutofillParserTests {
every { this@mockk.autofillHints } returns arrayOf(parentAutofillHint)
every { this@mockk.autofillId } returns parentAutofillId
every { this@mockk.idPackage } returns null
every { this@mockk.toAutofillView() } returns parentAutofillView
every { this@mockk.toAutofillView(parentWebsite = any()) } returns parentAutofillView
every { this@mockk.childCount } returns 2
every { this@mockk.getChildAt(0) } returns childViewNode
every { this@mockk.getChildAt(1) } returns invalidChildViewNode
@ -255,10 +258,10 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
verify(exactly = 0) {
any<List<ViewNodeTraversalData>>().buildUriOrNull(ID_PACKAGE_ANDROID)
any<AutofillView>().buildUriOrNull(ID_PACKAGE_ANDROID)
}
}
@ -274,6 +277,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -285,6 +289,7 @@ class AutofillParserTests {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Card(
@ -298,8 +303,8 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
// Test
val actual = parser.parse(
@ -319,7 +324,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -335,6 +340,7 @@ class AutofillParserTests {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -346,6 +352,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Login(
@ -359,8 +366,8 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
// Test
val actual = parser.parse(
@ -380,7 +387,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -398,6 +405,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = true,
website = URI,
),
)
val loginAutofillView: AutofillView.Login = AutofillView.Login.Password(
@ -414,7 +422,7 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { loginViewNode.toAutofillView() } returns unusedAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns unusedAutofillView
// Test
val actual = parser.parse(
@ -434,7 +442,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -481,6 +489,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val loginUsernameAutofillView: AutofillView.Login = AutofillView.Login.Username(
@ -491,6 +500,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val loginPasswordAutofillView: AutofillView.Login = AutofillView.Login.Password(
@ -501,6 +511,7 @@ class AutofillParserTests {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Login(
@ -514,9 +525,13 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { rootViewNode.toAutofillView() } returns null
every { hiddenUserNameViewNode.toAutofillView() } returns unusedAutofillView
every { passwordViewNode.toAutofillView() } returns loginPasswordAutofillView
every { rootViewNode.toAutofillView(parentWebsite = any()) } returns null
every {
hiddenUserNameViewNode.toAutofillView(parentWebsite = any())
} returns unusedAutofillView
every {
passwordViewNode.toAutofillView(parentWebsite = any())
} returns loginPasswordAutofillView
// Test
val actual = parser.parse(
@ -536,7 +551,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -552,6 +567,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -563,6 +579,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Card(
@ -576,8 +593,8 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
// Test
val actual = parser.parse(
@ -597,7 +614,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -614,6 +631,7 @@ class AutofillParserTests {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -625,6 +643,7 @@ class AutofillParserTests {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Card(
@ -638,8 +657,8 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
// Test
val actual = parser.parse(
@ -659,7 +678,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = true,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -676,6 +695,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -687,6 +707,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Card(
@ -700,8 +721,8 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
// Test
val actual = parser.parse(
@ -721,7 +742,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = false,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -738,6 +759,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -749,6 +771,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val autofillPartition = AutofillPartition.Card(
@ -762,8 +785,8 @@ class AutofillParserTests {
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
// Test
val actual = parser.parse(
@ -783,7 +806,7 @@ class AutofillParserTests {
isInlineAutofillEnabled = false,
)
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}
@ -799,6 +822,7 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
monthValue = null,
)
@ -810,22 +834,21 @@ class AutofillParserTests {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = URI,
),
)
val remoteBlockList = listOf(
"blockListedUri.com",
"blockListedAgainUri.com",
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
every { cardViewNode.toAutofillView(parentWebsite = any()) } returns cardAutofillView
every { loginViewNode.toAutofillView(parentWebsite = any()) } returns loginAutofillView
every { settingsRepository.blockedAutofillUris } returns remoteBlockList
// A function for asserting that a block listed URI results in an unfillable request.
fun testBlockListedUri(blockListedUri: String) {
// Setup
every {
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
} returns blockListedUri
every { any<AutofillView>().buildUriOrNull(PACKAGE_NAME) } returns blockListedUri
// Test
val actual = parser.parse(
@ -844,7 +867,7 @@ class AutofillParserTests {
// Verify all tests
verify(exactly = BLOCK_LISTED_URIS.size + remoteBlockList.size) {
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
any<AutofillView>().buildUriOrNull(PACKAGE_NAME)
}
}

View File

@ -604,6 +604,7 @@ private const val LOGIN_NAME = "John's Login"
private const val LOGIN_PASSWORD = "Password123"
private const val LOGIN_SUBTITLE = "John Doe"
private const val LOGIN_USERNAME = "John-Bitwarden"
private const val URI: String = "androidapp://com.x8bit.bitwarden"
private val LOGIN_AUTOFILL_CIPHER_WITH_TOTP = AutofillCipher.Login(
cipherId = LOGIN_WITH_TOTP_CIPHER_ID,
isTotpEnabled = true,
@ -611,6 +612,7 @@ private val LOGIN_AUTOFILL_CIPHER_WITH_TOTP = AutofillCipher.Login(
password = LOGIN_PASSWORD,
subtitle = LOGIN_SUBTITLE,
username = LOGIN_USERNAME,
website = URI,
)
private val LOGIN_AUTOFILL_CIPHER_WITHOUT_TOTP = AutofillCipher.Login(
cipherId = LOGIN_WITHOUT_TOTP_CIPHER_ID,
@ -619,5 +621,5 @@ private val LOGIN_AUTOFILL_CIPHER_WITHOUT_TOTP = AutofillCipher.Login(
password = LOGIN_PASSWORD,
subtitle = LOGIN_SUBTITLE,
username = LOGIN_USERNAME,
website = URI,
)
private const val URI: String = "androidapp://com.x8bit.bitwarden"

View File

@ -16,6 +16,7 @@ class AutofillPartitionExtensionsTest {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = null,
)
private val autofillDataValidText: AutofillView.Data = AutofillView.Data(
autofillId = mockk(),
@ -24,6 +25,7 @@ class AutofillPartitionExtensionsTest {
isFocused = false,
textValue = TEXT_VALUE,
hasPasswordTerms = false,
website = null,
)
//region Card tests

View File

@ -28,6 +28,7 @@ class AutofillViewExtensionsTest {
isFocused = false,
textValue = null,
hasPasswordTerms = false,
website = null,
)
@BeforeEach
@ -349,4 +350,47 @@ class AutofillViewExtensionsTest {
// Verify
assertNull(actual)
}
@Test
fun `buildUriOrNull should return website URI when present`() {
// Setup
val autofillViewData = autofillViewData.copy(website = WEBSITE)
val autofillView = AutofillView.Login.Username(data = autofillViewData)
// Test
val actual = autofillView.buildUriOrNull(packageName = PACKAGE_NAME)
// Verify
assertEquals(WEBSITE, actual)
}
@Test
fun `buildUriOrNull should return package name URI when website is null`() {
// Setup
val autofillViewData = autofillViewData.copy(website = null)
val autofillView = AutofillView.Login.Username(data = autofillViewData)
val expected = "androidapp://$PACKAGE_NAME"
// Test
val actual = autofillView.buildUriOrNull(packageName = PACKAGE_NAME)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return null when website and packageName are null`() {
// Setup
val autofillViewData = autofillViewData.copy(website = null)
val autofillView = AutofillView.Login.Username(data = autofillViewData)
// Test
val actual = autofillView.buildUriOrNull(packageName = null)
// Verify
assertNull(actual)
}
}
private const val PACKAGE_NAME: String = "com.google"
private const val WEBSITE: String = "https://www.google.com"

View File

@ -39,6 +39,7 @@ class CipherViewExtensionsTest {
subtitle = "mockUsername-1",
password = "mockPassword-1",
username = "mockUsername-1",
website = "uri",
),
),
autofillCipherProvider.getLoginAutofillCiphers(uri = "uri"),
@ -71,6 +72,7 @@ class CipherViewExtensionsTest {
subtitle = "mockUsername-1",
password = "mockPassword-1",
username = "mockUsername-1",
website = "uri",
),
),
autofillCipherProvider.getLoginAutofillCiphers(uri = "uri"),

View File

@ -75,6 +75,7 @@ class FilledDataExtensionsTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = "uri",
),
),
),

View File

@ -29,6 +29,7 @@ class ViewNodeExtensionsTest {
isFocused = expectedIsFocused,
textValue = TEXT_VALUE,
hasPasswordTerms = false,
website = null,
)
private val testAutofillValue: AutofillValue = mockk()
private val mockHtmlInfo: HtmlInfo = mockk {
@ -46,10 +47,12 @@ class ViewNodeExtensionsTest {
every { inputType } returns 1
every { isFocused } returns expectedIsFocused
every { htmlInfo } returns mockHtmlInfo
every { website } returns null
}
@BeforeEach
fun setup() {
mockkStatic(AssistStructure.ViewNode::website)
mockkStatic(HtmlInfo::isInputField)
mockkStatic(HtmlInfo::isPasswordField)
mockkStatic(Int::isPasswordInputType)
@ -72,6 +75,7 @@ class ViewNodeExtensionsTest {
@AfterEach
fun teardown() {
unmockkStatic(AssistStructure.ViewNode::website)
unmockkStatic(HtmlInfo::isInputField)
unmockkStatic(HtmlInfo::isPasswordField)
unmockkStatic(Int::isPasswordInputType)
@ -93,7 +97,7 @@ class ViewNodeExtensionsTest {
every { viewNode.autofillHints } returns arrayOf(autofillHint)
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -121,7 +125,7 @@ class ViewNodeExtensionsTest {
} returns monthValue
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -137,7 +141,7 @@ class ViewNodeExtensionsTest {
)
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_EXP_MONTH_HINTS
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -152,7 +156,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARD_EXP_MONTH_HINTS.forEach { idEntry ->
every { viewNode.idEntry } returns idEntry
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for idEntry: $idEntry")
}
@ -167,7 +171,7 @@ class ViewNodeExtensionsTest {
)
SUPPORTED_RAW_CARD_EXP_MONTH_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
}
@ -182,7 +186,7 @@ class ViewNodeExtensionsTest {
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -197,7 +201,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARD_EXP_YEAR_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
@ -213,7 +217,7 @@ class ViewNodeExtensionsTest {
)
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_EXP_YEAR_HINTS
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -227,7 +231,7 @@ class ViewNodeExtensionsTest {
every { viewNode.autofillHints } returns arrayOf(autofillHint)
every { mockHtmlInfo.isInputField } returns true
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -241,7 +245,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARD_EXP_DATE_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
@ -256,7 +260,7 @@ class ViewNodeExtensionsTest {
)
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_EXP_DATE_HINTS
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -271,7 +275,7 @@ class ViewNodeExtensionsTest {
every { viewNode.autofillHints } returns arrayOf(autofillHint)
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -286,7 +290,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARD_NUMBER_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
@ -300,7 +304,7 @@ class ViewNodeExtensionsTest {
)
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_NUMBER_HINTS
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -315,7 +319,7 @@ class ViewNodeExtensionsTest {
every { viewNode.autofillHints } returns arrayOf(autofillHint)
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -330,7 +334,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARD_SECURITY_CODE_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
@ -344,7 +348,7 @@ class ViewNodeExtensionsTest {
data = autofillViewData,
)
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARD_SECURITY_CODE_HINTS
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -357,7 +361,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARDHOLDER_NAME_HINTS.forEach { idEntry ->
every { viewNode.idEntry } returns idEntry
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for idEntry: $idEntry")
}
@ -372,7 +376,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_CARDHOLDER_NAME_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
@ -386,7 +390,7 @@ class ViewNodeExtensionsTest {
data = autofillViewData,
)
every { viewNode.htmlInfo.hints() } returns SUPPORTED_RAW_CARDHOLDER_NAME_HINTS
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -399,7 +403,7 @@ class ViewNodeExtensionsTest {
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -413,7 +417,7 @@ class ViewNodeExtensionsTest {
SUPPORTED_RAW_PASSWORD_HINTS.forEach { hint ->
every { viewNode.hint } returns hint
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual, "Failed for hint: $hint")
}
@ -428,11 +432,50 @@ class ViewNodeExtensionsTest {
)
every { viewNode.htmlInfo.isPasswordField() } returns true
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@Test
fun `toAutofillView should return AutofillView Login Username with internal website`() {
// Setup
val website = "website"
val expected = AutofillView.Login.Username(
data = autofillViewData.copy(website = website),
)
setupUnsupportedInputFieldViewNode()
every { viewNode.website } returns website
every { viewNode.className } returns "android.widget.EditText"
every { any<Int>().isPasswordInputType } returns false
every { any<Int>().isUsernameInputType } returns true
// Test
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
}
@Test
fun `toAutofillView should return AutofillView Login Username with external website`() {
// Setup
val website = "website"
val expected = AutofillView.Login.Username(
data = autofillViewData.copy(website = website),
)
setupUnsupportedInputFieldViewNode()
every { viewNode.className } returns "android.widget.EditText"
every { any<Int>().isPasswordInputType } returns false
every { any<Int>().isUsernameInputType } returns true
// Test
val actual = viewNode.toAutofillView(parentWebsite = website)
// Verify
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toAutofillView should return AutofillView Login Username when is EditText and isUsernameField`() {
@ -446,7 +489,7 @@ class ViewNodeExtensionsTest {
every { any<Int>().isUsernameInputType } returns true
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -465,7 +508,7 @@ class ViewNodeExtensionsTest {
every { any<Int>().isUsernameInputType } returns true
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -480,7 +523,7 @@ class ViewNodeExtensionsTest {
every { viewNode.htmlInfo.isInputField } returns false
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertNull(actual)
@ -494,7 +537,7 @@ class ViewNodeExtensionsTest {
data = autofillViewData,
)
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
assertEquals(expected, actual)
}
@ -511,7 +554,7 @@ class ViewNodeExtensionsTest {
every { viewNode.autofillHints } returns arrayOf(autofillHintOne, autofillHintTwo)
// Test
val actual = viewNode.toAutofillView()
val actual = viewNode.toAutofillView(parentWebsite = null)
// Verify
assertEquals(expected, actual)
@ -1207,6 +1250,7 @@ class ViewNodeExtensionsTest {
every { any<Int>().isPasswordInputType } returns false
every { any<Int>().isUsernameInputType } returns false
every { viewNode.htmlInfo.hints() } returns emptyList()
every { viewNode.website } returns null
}
}

View File

@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class ViewNodeTraversalDataExtensionsTest {
@ -15,64 +14,6 @@ class ViewNodeTraversalDataExtensionsTest {
every { this@mockk.getWindowNodeAt(0) } returns windowNode
}
@Test
fun `buildUriOrNull should return website URI when present`() {
// Setup
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = emptyList(),
idPackage = null,
ignoreAutofillIds = emptyList(),
website = WEBSITE,
)
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
packageName = PACKAGE_NAME,
)
// Verify
assertEquals(WEBSITE, actual)
}
@Test
fun `buildUriOrNull should return package name URI when website is null`() {
// Setup
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = emptyList(),
idPackage = null,
ignoreAutofillIds = emptyList(),
website = null,
)
val expected = "androidapp://$PACKAGE_NAME"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
packageName = PACKAGE_NAME,
)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return null when website and packageName are null`() {
// Setup
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = emptyList(),
idPackage = null,
ignoreAutofillIds = emptyList(),
website = null,
)
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
packageName = null,
)
// Verify
assertNull(actual)
}
@Test
fun `buildPackageNameOrNull should return idPackage when available`() {
// Setup
@ -80,7 +21,6 @@ class ViewNodeTraversalDataExtensionsTest {
autofillViews = emptyList(),
idPackage = ID_PACKAGE,
ignoreAutofillIds = emptyList(),
website = null,
)
// Test
@ -99,7 +39,6 @@ class ViewNodeTraversalDataExtensionsTest {
autofillViews = emptyList(),
idPackage = null,
ignoreAutofillIds = emptyList(),
website = null,
)
val expected = "com.x8bit.bitwarden"
every { windowNode.title } returns "com.x8bit.bitwarden/path.deeper.into.app"
@ -115,5 +54,3 @@ class ViewNodeTraversalDataExtensionsTest {
}
private const val ID_PACKAGE: String = "com.x8bit.bitwarden"
private const val PACKAGE_NAME: String = "com.google"
private const val WEBSITE: String = "https://www.google.com"

View File

@ -12,4 +12,5 @@ fun createMockPasswordCredentialAutofillCipherLogin() = AutofillCipher.Login(
password = "mock-password",
username = "mock-username",
subtitle = "Subtitle",
website = "website",
)

View File

@ -87,6 +87,7 @@ class AutofillUtilsTest {
password = "password",
username = "username",
subtitle = "Subtitle",
website = "website",
),
second = AutofillAppInfo(
context = context,
@ -103,6 +104,7 @@ class AutofillUtilsTest {
password = "password",
username = "username",
subtitle = "AmazonSubtitle",
website = "website",
),
second = AutofillAppInfo(
context = context,