diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModel.kt index 8fb560350eb7..c5274e18c607 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModel.kt @@ -19,17 +19,19 @@ package com.duckduckgo.autofill.impl.service import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.autofill.impl.securestorage.SecureStorage import com.duckduckgo.autofill.impl.securestorage.WebsiteLoginDetailsWithCredentials import com.duckduckgo.autofill.impl.service.AutofillProviderChooseViewModel.Command.AutofillLogin import com.duckduckgo.autofill.impl.service.AutofillProviderChooseViewModel.Command.ContinueWithoutAuthentication import com.duckduckgo.autofill.impl.service.AutofillProviderChooseViewModel.Command.ForceFinish import com.duckduckgo.autofill.impl.service.AutofillProviderChooseViewModel.Command.RequestAuthentication +import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.Command import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -41,8 +43,9 @@ import timber.log.Timber @ContributesViewModel(ActivityScope::class) class AutofillProviderChooseViewModel @Inject constructor( private val autofillProviderDeviceAuth: AutofillProviderDeviceAuth, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, - private val secureStorage: SecureStorage, + private val autofillStore: InternalAutofillStore, ) : ViewModel() { private val command = Channel(1, BufferOverflow.DROP_OLDEST) @@ -72,15 +75,23 @@ class AutofillProviderChooseViewModel @Inject constructor( fun continueAfterAuthentication(credentialId: Long) { Timber.i("DDGAutofillService request to autofill login with credentialId: $credentialId") viewModelScope.launch(dispatchers.io()) { - secureStorage.getWebsiteLoginDetailsWithCredentials(credentialId)?.toLoginCredentials()?.let { + autofillStore.getCredentialsWithId(credentialId)?.let { loginCredential -> + loginCredential.updateLastUsedTimestamp() Timber.i("DDGAutofillService $credentialId found, autofilling") - command.send(AutofillLogin(it)) + command.send(AutofillLogin(loginCredential)) } ?: run { command.send(ForceFinish) } } } + private fun LoginCredentials.updateLastUsedTimestamp() { + appCoroutineScope.launch(dispatchers.io()) { + val updated = this@updateLastUsedTimestamp.copy(lastUsedMillis = System.currentTimeMillis()) + autofillStore.updateCredentials(updated, refreshLastUpdatedTimestamp = false) + } + } + private fun WebsiteLoginDetailsWithCredentials.toLoginCredentials(): LoginCredentials { return LoginCredentials( id = details.id, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModel.kt index e95f66dc238b..2a4ca4ab1afb 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModel.kt @@ -19,6 +19,7 @@ package com.duckduckgo.autofill.impl.service import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.store.InternalAutofillStore @@ -26,6 +27,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.searching.Credentia import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -40,6 +42,7 @@ class AutofillProviderCredentialsListViewModel @Inject constructor( private val pixel: Pixel, private val dispatchers: DispatcherProvider, private val credentialListFilter: CredentialListFilter, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -70,6 +73,17 @@ class AutofillProviderCredentialsListViewModel @Inject constructor( _viewState.value = _viewState.value.copy(credentialSearchQuery = searchText) } + fun onCredentialSelected(credentials: LoginCredentials) { + credentials.updateLastUsedTimestamp() + } + + private fun LoginCredentials.updateLastUsedTimestamp() { + appCoroutineScope.launch(dispatchers.io()) { + val updated = this@updateLastUsedTimestamp.copy(lastUsedMillis = System.currentTimeMillis()) + autofillStore.updateCredentials(updated, refreshLastUpdatedTimestamp = false) + } + } + data class ViewState( val logins: List? = null, val credentialSearchQuery: String = "", diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderSuggestions.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderSuggestions.kt index 7b516b2d8cf2..ca29cc2a7e67 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderSuggestions.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillProviderSuggestions.kt @@ -28,14 +28,12 @@ import android.view.autofill.AutofillValue import androidx.annotation.RequiresApi import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.autofill.api.store.AutofillStore import com.duckduckgo.autofill.impl.service.AutofillFieldType.UNKNOWN import com.duckduckgo.autofill.impl.service.AutofillFieldType.USERNAME import com.duckduckgo.autofill.impl.service.AutofillProviderChooseActivity.Companion.FILL_REQUEST_AUTOFILL_CREDENTIAL_ID_EXTRAS import com.duckduckgo.autofill.impl.service.AutofillProviderChooseActivity.Companion.FILL_REQUEST_AUTOFILL_ID_EXTRAS import com.duckduckgo.autofill.impl.service.AutofillProviderChooseActivity.Companion.FILL_REQUEST_PACKAGE_ID_EXTRAS import com.duckduckgo.autofill.impl.service.AutofillProviderChooseActivity.Companion.FILL_REQUEST_URL_EXTRAS -import com.duckduckgo.autofill.impl.service.mapper.AppCredentialProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn @@ -55,10 +53,9 @@ interface AutofillProviderSuggestions { @ContributesBinding(AppScope::class) class RealAutofillProviderSuggestions @Inject constructor( private val appBuildConfig: AppBuildConfig, - private val autofillStore: AutofillStore, private val viewProvider: AutofillServiceViewProvider, private val suggestionsFormatter: AutofillServiceSuggestionCredentialFormatter, - private val appCredentialProvider: AppCredentialProvider, + private val autofillSuggestions: AutofillSuggestions, ) : AutofillProviderSuggestions { companion object { @@ -210,18 +207,16 @@ class RealAutofillProviderSuggestions @Inject constructor( AutofillValue.forText(credential?.password ?: "password") } - private suspend fun loginCredentials(node: AutofillRootNode): List? { - val crendentialsForDomain = node.website.takeUnless { it.isNullOrBlank() }?.let { - autofillStore.getCredentials(it) + private suspend fun loginCredentials(node: AutofillRootNode): List { + val credentialsForDomain = node.website.takeUnless { it.isNullOrBlank() }?.let { nonEmptyWebsite -> + autofillSuggestions.getSiteSuggestions(nonEmptyWebsite) } ?: emptyList() - val crendentialsForPackage = node.packageId.takeUnless { it.isNullOrBlank() }?.let { - appCredentialProvider.getCredentials(it) + val credentialsForPackage = node.packageId.takeUnless { it.isNullOrBlank() }?.let { nonEmptyPackageId -> + autofillSuggestions.getAppSuggestions(nonEmptyPackageId) } ?: emptyList() - Timber.v("DDGAutofillService credentials for domain: $crendentialsForDomain") - Timber.v("DDGAutofillService credentials for package: $crendentialsForPackage") - return crendentialsForDomain.plus(crendentialsForPackage).distinct() + return credentialsForDomain.plus(credentialsForPackage).distinct() } private fun createAutofillSelectionIntent(context: Context, url: String, packageId: String): PendingIntent { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSimpleCredentialsListFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSimpleCredentialsListFragment.kt index 2fac3f1fb51e..324fa4489aee 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSimpleCredentialsListFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSimpleCredentialsListFragment.kt @@ -241,6 +241,7 @@ class AutofillSimpleCredentialsListFragment : DuckDuckGoFragment(R.layout.fragme } private fun onCredentialsSelected(credentials: LoginCredentials) { + viewModel.onCredentialSelected(credentials) parentActivity()?.autofillLogin(credentials) ?: run { activity?.finish() } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSuggestions.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSuggestions.kt new file mode 100644 index 000000000000..3df9728d704d --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/service/AutofillSuggestions.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.service + +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.store.AutofillStore +import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator +import com.duckduckgo.autofill.impl.service.mapper.AppCredentialProvider +import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsGrouper +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.withContext +import timber.log.Timber + +interface AutofillSuggestions { + suspend fun getSiteSuggestions(website: String): List + suspend fun getAppSuggestions(packageId: String): List +} + +@SingleInstanceIn(AppScope::class) +@ContributesBinding(AppScope::class) +class AutofillServiceSuggestions @Inject constructor( + private val autofillStore: AutofillStore, + private val loginDeduplicator: AutofillLoginDeduplicator, + private val grouper: AutofillSelectCredentialsGrouper, + private val appCredentialProvider: AppCredentialProvider, + private val dispatcherProvider: DispatcherProvider, +) : AutofillSuggestions { + + override suspend fun getSiteSuggestions(website: String): List { + return withContext(dispatcherProvider.io()) { + val credentials = autofillStore.getCredentials(website) + loginDeduplicator.deduplicate(website, credentials).let { dedupLogins -> + grouper.group(website, dedupLogins).let { groups -> + groups.perfectMatches + .plus(groups.partialMatches.values.flatten()) + .plus(groups.shareableCredentials.values.flatten()) + } + }.also { + Timber.v("DDGAutofillService credentials for domain: $it") + } + } + } + + override suspend fun getAppSuggestions(packageId: String): List { + return withContext(dispatcherProvider.io()) { + appCredentialProvider.getCredentials(packageId).also { + Timber.v("DDGAutofillService credentials for package: $it") + } + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/sorting/CredentialListSorterByTitleAndDomain.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/sorting/CredentialListSorterByTitleAndDomain.kt index da05ab90d08c..0b9145e47153 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/sorting/CredentialListSorterByTitleAndDomain.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/sorting/CredentialListSorterByTitleAndDomain.kt @@ -20,7 +20,7 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher.ExtractedUrlParts import com.duckduckgo.common.utils.extractDomain -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import java.text.Collator import javax.inject.Inject @@ -30,7 +30,7 @@ interface CredentialListSorter { fun comparator(): Collator } -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) class CredentialListSorterByTitleAndDomain @Inject constructor( private val autofillUrlMatcher: AutofillUrlMatcher, ) : CredentialListSorter { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt index f0c5c78d2034..8f3f4580addd 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt @@ -20,7 +20,7 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialListSorter import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsGrouper.Groups import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import java.util.* import javax.inject.Inject @@ -40,7 +40,7 @@ interface AutofillSelectCredentialsGrouper { ) } -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) class RealAutofillSelectCredentialsGrouper @Inject constructor( private val autofillUrlMatcher: AutofillUrlMatcher, private val sorter: CredentialListSorter, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt index 0f97c59e2777..a25a89c2ee5b 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt @@ -17,14 +17,14 @@ package com.duckduckgo.autofill.impl.ui.credential.selecting import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import javax.inject.Named interface TimestampBasedLoginSorter : Comparator -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) @Named("LastUsedCredentialSorter") class LastUsedCredentialSorter @Inject constructor() : TimestampBasedLoginSorter { @@ -54,7 +54,7 @@ class LastUsedCredentialSorter @Inject constructor() : TimestampBasedLoginSorter } } -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) @Named("LastUpdatedCredentialSorter") class LastUpdatedCredentialSorter @Inject constructor() : TimestampBasedLoginSorter { diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/AutofillStoreFixtures.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/AutofillStoreFixtures.kt new file mode 100644 index 000000000000..cd5d62fb0eea --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/AutofillStoreFixtures.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill + +import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.impl.FakePasswordStoreEventPlugin +import com.duckduckgo.autofill.impl.SecureStoreBackedAutofillStore +import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator +import com.duckduckgo.autofill.impl.encoding.TestUrlUnicodeNormalizer +import com.duckduckgo.autofill.impl.securestorage.SecureStorage +import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsGrouper +import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsGrouper.Groups +import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher +import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher +import com.duckduckgo.autofill.store.AutofillPrefsStore +import com.duckduckgo.autofill.store.LastUpdatedTimeProvider +import com.duckduckgo.autofill.sync.CredentialsSyncMetadata +import com.duckduckgo.autofill.sync.SyncCredentialsListener +import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase +import com.duckduckgo.common.utils.DispatcherProvider +import kotlinx.coroutines.CoroutineScope + +fun fakeAutofillStore( + secureStorage: SecureStorage = fakeStorage(), + lastUpdatedTimeProvider: LastUpdatedTimeProvider = lastUpdatedTimeProvider(), + autofillPrefsStore: AutofillPrefsStore, + autofillUrlMatcher: AutofillUrlMatcher = autofillDomainNameUrlMatcher(), + autofillFeature: AutofillFeature, + dispatcherProvider: DispatcherProvider, + coroutineScope: CoroutineScope, +) = SecureStoreBackedAutofillStore( + secureStorage = secureStorage, + lastUpdatedTimeProvider = lastUpdatedTimeProvider, + autofillPrefsStore = autofillPrefsStore, + dispatcherProvider = dispatcherProvider, + autofillUrlMatcher = autofillUrlMatcher, + passwordStoreEventListenersPlugins = FakePasswordStoreEventPlugin(), + syncCredentialsListener = SyncCredentialsListener( + CredentialsSyncMetadata(inMemoryAutofillDatabase().credentialsSyncDao()), + dispatcherProvider, + coroutineScope, + ), + autofillFeature = autofillFeature, +) + +fun fakeStorage( + canAccessSecureStorage: Boolean = true, + urlMatcher: AutofillUrlMatcher = autofillDomainNameUrlMatcher(), +) = FakeSecureStore( + canAccessSecureStorage = canAccessSecureStorage, + urlMatcher = urlMatcher, +) + +fun lastUpdatedTimeProvider() = object : LastUpdatedTimeProvider { + override fun getInMillis(): Long = 10000L +} + +fun autofillDomainNameUrlMatcher() = AutofillDomainNameUrlMatcher(TestUrlUnicodeNormalizer()) + +fun noopDeduplicator() = object : AutofillLoginDeduplicator { + override fun deduplicate( + originalUrl: String, + logins: List, + ): List = logins +} + +fun noopGroupBuilder() = object : AutofillSelectCredentialsGrouper { + override fun group( + originalUrl: String, + unsortedCredentials: List, + ): Groups { + return Groups(unsortedCredentials, emptyMap(), emptyMap()) + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/FakeSecureStorage.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/FakeSecureStorage.kt new file mode 100644 index 000000000000..e3f3d260ce71 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/FakeSecureStorage.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill + +import com.duckduckgo.autofill.impl.securestorage.SecureStorage +import com.duckduckgo.autofill.impl.securestorage.WebsiteLoginDetails +import com.duckduckgo.autofill.impl.securestorage.WebsiteLoginDetailsWithCredentials +import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow + +class FakeSecureStore( + private val canAccessSecureStorage: Boolean, + private val urlMatcher: AutofillUrlMatcher, +) : SecureStorage { + + private val credentials = mutableListOf() + + override suspend fun addWebsiteLoginDetailsWithCredentials( + websiteLoginDetailsWithCredentials: WebsiteLoginDetailsWithCredentials, + ): WebsiteLoginDetailsWithCredentials { + val id = websiteLoginDetailsWithCredentials.details.id ?: (credentials.size.toLong() + 1) + val credentialWithId: WebsiteLoginDetailsWithCredentials = websiteLoginDetailsWithCredentials.copy( + details = websiteLoginDetailsWithCredentials.details.copy(id = id), + ) + credentials.add(credentialWithId) + return credentialWithId + } + + override suspend fun addWebsiteLoginDetailsWithCredentials(credentials: List): List { + credentials.forEach { addWebsiteLoginDetailsWithCredentials(it) } + return credentials.map { it.details.id!! } + } + + override suspend fun websiteLoginDetailsForDomain(domain: String): Flow> { + return flow { + emit( + domainLookup(domain).map { it.details }, + ) + } + } + + override suspend fun websiteLoginDetails(): Flow> { + return flow { + emit(credentials.map { it.details }) + } + } + + override suspend fun getWebsiteLoginDetailsWithCredentials(id: Long): WebsiteLoginDetailsWithCredentials? { + return credentials.firstOrNull() { it.details.id == id } + } + + override suspend fun websiteLoginDetailsWithCredentialsForDomain(domain: String): Flow> { + return flow { + emit( + domainLookup(domain), + ) + } + } + + private fun domainLookup(domain: String) = credentials + .filter { it.details.domain?.contains(domain) == true } + .filter { + val visitedSite = urlMatcher.extractUrlPartsForAutofill(domain) + val savedSite = urlMatcher.extractUrlPartsForAutofill(it.details.domain) + urlMatcher.matchingForAutofill(visitedSite, savedSite) + } + + override suspend fun websiteLoginDetailsWithCredentials(): Flow> { + return flow { + emit(credentials) + } + } + + override suspend fun updateWebsiteLoginDetailsWithCredentials( + websiteLoginDetailsWithCredentials: WebsiteLoginDetailsWithCredentials, + ): WebsiteLoginDetailsWithCredentials { + credentials.indexOfFirst { it.details.id == websiteLoginDetailsWithCredentials.details.id }.also { + credentials[it] = websiteLoginDetailsWithCredentials + } + return websiteLoginDetailsWithCredentials + } + + override suspend fun deleteWebsiteLoginDetailsWithCredentials(id: Long) { + credentials.removeAll { it.details.id == id } + } + + override suspend fun deleteWebSiteLoginDetailsWithCredentials(ids: List) { + credentials.removeAll { ids.contains(it.details.id) } + } + + override suspend fun addToNeverSaveList(domain: String) { + } + + override suspend fun clearNeverSaveList() { + } + + override suspend fun neverSaveListCount(): Flow = emptyFlow() + override suspend fun isInNeverSaveList(domain: String): Boolean = false + + override suspend fun canAccessSecureStorage(): Boolean = canAccessSecureStorage +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt index 1e990966a7df..389ecb80b2ad 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt @@ -55,6 +55,7 @@ import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppress import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.UpdateMatchingUsernames import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver +import com.duckduckgo.autofill.noopDeduplicator import com.duckduckgo.common.test.CoroutineTestRule import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.TestScope @@ -98,7 +99,7 @@ class AutofillStoredBackJavascriptInterfaceTest { private val inContextDataStore: EmailProtectionInContextDataStore = mock() private val recentInstallChecker: EmailProtectionInContextRecentInstallChecker = mock() private val testWebView = WebView(getApplicationContext()) - private val loginDeduplicator: AutofillLoginDeduplicator = NoopDeduplicator() + private val loginDeduplicator: AutofillLoginDeduplicator = noopDeduplicator() private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() private val partialCredentialSaveStore: PartialCredentialSaveStore = mock() @@ -606,13 +607,6 @@ class AutofillStoredBackJavascriptInterfaceTest { } } - private class NoopDeduplicator : AutofillLoginDeduplicator { - override fun deduplicate( - originalUrl: String, - logins: List, - ): List = logins - } - private companion object { private const val EXAMPLE_DOT_COM_URL = "https://example.com" } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt index ac8b205c239a..ccbb77181686 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.autofill.impl import android.annotation.SuppressLint import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.FakeSecureStore import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType.Password @@ -30,7 +31,6 @@ import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCrede import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.UsernameMissing import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.encoding.TestUrlUnicodeNormalizer -import com.duckduckgo.autofill.impl.securestorage.SecureStorage import com.duckduckgo.autofill.impl.securestorage.WebsiteLoginDetails import com.duckduckgo.autofill.impl.securestorage.WebsiteLoginDetailsWithCredentials import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher @@ -44,10 +44,7 @@ import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -740,98 +737,6 @@ class SecureStoreBackedAutofillStoreTest { return secureStore.addWebsiteLoginDetailsWithCredentials(credentials).toLoginCredentials() } - private class FakeSecureStore( - private val canAccessSecureStorage: Boolean, - private val urlMatcher: AutofillUrlMatcher, - ) : SecureStorage { - - private val credentials = mutableListOf() - - override suspend fun addWebsiteLoginDetailsWithCredentials( - websiteLoginDetailsWithCredentials: WebsiteLoginDetailsWithCredentials, - ): WebsiteLoginDetailsWithCredentials { - val id = websiteLoginDetailsWithCredentials.details.id ?: (credentials.size.toLong() + 1) - val credentialWithId: WebsiteLoginDetailsWithCredentials = websiteLoginDetailsWithCredentials.copy( - details = websiteLoginDetailsWithCredentials.details.copy(id = id), - ) - credentials.add(credentialWithId) - return credentialWithId - } - - override suspend fun addWebsiteLoginDetailsWithCredentials(credentials: List): List { - credentials.forEach { addWebsiteLoginDetailsWithCredentials(it) } - return credentials.map { it.details.id!! } - } - - override suspend fun websiteLoginDetailsForDomain(domain: String): Flow> { - return flow { - emit( - domainLookup(domain).map { it.details }, - ) - } - } - - override suspend fun websiteLoginDetails(): Flow> { - return flow { - emit(credentials.map { it.details }) - } - } - - override suspend fun getWebsiteLoginDetailsWithCredentials(id: Long): WebsiteLoginDetailsWithCredentials? { - return credentials.firstOrNull() { it.details.id == id } - } - - override suspend fun websiteLoginDetailsWithCredentialsForDomain(domain: String): Flow> { - return flow { - emit( - domainLookup(domain), - ) - } - } - - private fun domainLookup(domain: String) = credentials - .filter { it.details.domain?.contains(domain) == true } - .filter { - val visitedSite = urlMatcher.extractUrlPartsForAutofill(domain) - val savedSite = urlMatcher.extractUrlPartsForAutofill(it.details.domain) - urlMatcher.matchingForAutofill(visitedSite, savedSite) - } - - override suspend fun websiteLoginDetailsWithCredentials(): Flow> { - return flow { - emit(credentials) - } - } - - override suspend fun updateWebsiteLoginDetailsWithCredentials( - websiteLoginDetailsWithCredentials: WebsiteLoginDetailsWithCredentials, - ): WebsiteLoginDetailsWithCredentials { - credentials.indexOfFirst { it.details.id == websiteLoginDetailsWithCredentials.details.id }.also { - credentials[it] = websiteLoginDetailsWithCredentials - } - return websiteLoginDetailsWithCredentials - } - - override suspend fun deleteWebsiteLoginDetailsWithCredentials(id: Long) { - credentials.removeAll { it.details.id == id } - } - - override suspend fun deleteWebSiteLoginDetailsWithCredentials(ids: List) { - credentials.removeAll { ids.contains(it.details.id) } - } - - override suspend fun addToNeverSaveList(domain: String) { - } - - override suspend fun clearNeverSaveList() { - } - - override suspend fun neverSaveListCount(): Flow = emptyFlow() - override suspend fun isInNeverSaveList(domain: String): Boolean = false - - override suspend fun canAccessSecureStorage(): Boolean = canAccessSecureStorage - } - companion object { private const val DEFAULT_INITIAL_LAST_UPDATED = 200L private const val UPDATED_INITIAL_LAST_UPDATED = 10000L diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModelTest.kt index 3ad9cc487125..ab8c7896e7de 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderChooseViewModelTest.kt @@ -2,12 +2,16 @@ package com.duckduckgo.autofill.impl.service import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.test +import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.fakeAutofillStore +import com.duckduckgo.autofill.fakeStorage import com.duckduckgo.autofill.impl.securestorage.SecureStorage +import com.duckduckgo.autofill.sync.CredentialsFixtures.toLoginCredentials import com.duckduckgo.autofill.sync.CredentialsFixtures.toWebsiteLoginCredentials import com.duckduckgo.autofill.sync.CredentialsFixtures.twitterCredentials -import com.duckduckgo.autofill.sync.FakeSecureStorage import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Rule @@ -25,12 +29,21 @@ class AutofillProviderChooseViewModelTest { private val autofillProviderDeviceAuth: AutofillProviderDeviceAuth = mock() - private val secureStorage: SecureStorage = FakeSecureStorage() + private val secureStorage: SecureStorage = fakeStorage() + + val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) private val testee = AutofillProviderChooseViewModel( autofillProviderDeviceAuth = autofillProviderDeviceAuth, dispatchers = coroutineRule.testDispatcherProvider, - secureStorage = secureStorage, + autofillStore = fakeAutofillStore( + secureStorage = secureStorage, + autofillPrefsStore = mock(), + dispatcherProvider = coroutineRule.testDispatcherProvider, + coroutineScope = coroutineRule.testScope, + autofillFeature = autofillFeature, + ), + appCoroutineScope = coroutineRule.testScope, ) @Test @@ -81,6 +94,20 @@ class AutofillProviderChooseViewModelTest { } } + @Test + fun whenContinueAfterAuthenticationThenUpdateLastUsedTimestamp() = runTest { + givenLocalCredentials(twitterCredentials) + whenever(autofillProviderDeviceAuth.isAuthRequired()).thenReturn(true) + + testee.commands().test { + awaitItem() // requires auth + testee.continueAfterAuthentication(twitterCredentials.id!!) + awaitItem() + val updatedCredential = secureStorage.getWebsiteLoginDetailsWithCredentials(twitterCredentials.id!!)!!.toLoginCredentials() + assertFalse(twitterCredentials.lastUsedMillis == updatedCredential.lastUsedMillis) + } + } + private suspend fun givenLocalCredentials(vararg credentials: LoginCredentials) { credentials.forEach { secureStorage.addWebsiteLoginDetailsWithCredentials(it.toWebsiteLoginCredentials()) diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModelTest.kt index d6a032a8cb39..acb877256dcd 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillProviderCredentialsListViewModelTest.kt @@ -15,7 +15,10 @@ import org.junit.Assert.assertNotNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @@ -31,6 +34,7 @@ class AutofillProviderCredentialsListViewModelTest { pixel = mock(), dispatchers = coroutineRule.testDispatcherProvider, credentialListFilter = FakeDomainListFilter(), + appCoroutineScope = coroutineRule.testScope, ) @Test @@ -63,6 +67,13 @@ class AutofillProviderCredentialsListViewModelTest { } } + @Test + fun whenCredentialSelectedThenUpdateLastUsedTimestamp() = runTest { + val credentials = twitterCredentials + testee.onCredentialSelected(credentials) + verify(autofillStore).updateCredentials(any(), refreshLastUpdatedTimestamp = eq(false)) + } + private class FakeDomainListFilter : CredentialListFilter { override suspend fun filter( originalList: List, diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillServiceSuggestionsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillServiceSuggestionsTest.kt new file mode 100644 index 000000000000..e213ea94d247 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/AutofillServiceSuggestionsTest.kt @@ -0,0 +1,158 @@ +package com.duckduckgo.autofill.impl.service + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator +import com.duckduckgo.autofill.impl.service.mapper.fakes.FakeAppCredentialsProvider +import com.duckduckgo.autofill.impl.service.mapper.fakes.FakeAutofillStore +import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsGrouper +import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsGrouper.Groups +import com.duckduckgo.autofill.noopDeduplicator +import com.duckduckgo.autofill.noopGroupBuilder +import com.duckduckgo.autofill.sync.CredentialsFixtures.amazonCredentials +import com.duckduckgo.autofill.sync.CredentialsFixtures.spotifyCredentials +import com.duckduckgo.autofill.sync.CredentialsFixtures.twitterCredentials +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AutofillServiceSuggestionsTest { + @get:Rule var coroutineRule = CoroutineTestRule() + + private val fakeAutofillStore = FakeAutofillStore( + listOf( + twitterCredentials, + spotifyCredentials, + amazonCredentials, + ), + ) + + private val fakeAppCredentialsProvider = FakeAppCredentialsProvider( + listOf( + twitterCredentials, + spotifyCredentials, + amazonCredentials, + ), + ) + + @Test + fun whenGetSiteSuggestionsThenReturnsSiteSuggestions() = runTest { + val testee = AutofillServiceSuggestions( + autofillStore = fakeAutofillStore, + loginDeduplicator = noopDeduplicator(), + grouper = noopGroupBuilder(), + appCredentialProvider = fakeAppCredentialsProvider, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + val result = testee.getSiteSuggestions(twitterCredentials.domain!!) + assertEquals(1, result.size) + assertEquals(twitterCredentials, result.first()) + } + + @Test + fun whenGetSiteSuggestionsDoesNotHaveResultsThenReturnsEmtpyList() = runTest { + val testee = AutofillServiceSuggestions( + autofillStore = fakeAutofillStore, + loginDeduplicator = noopDeduplicator(), + grouper = noopGroupBuilder(), + appCredentialProvider = fakeAppCredentialsProvider, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + val result = testee.getSiteSuggestions("random.com") + assertEquals(0, result.size) + } + + @Test + fun whenGetAppSuggestionsThenReturnsAppSuggestions() = runTest { + val testee = AutofillServiceSuggestions( + autofillStore = fakeAutofillStore, + loginDeduplicator = noopDeduplicator(), + grouper = noopGroupBuilder(), + appCredentialProvider = fakeAppCredentialsProvider, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + val result = testee.getAppSuggestions(twitterCredentials.domain!!) + assertEquals(1, result.size) + assertEquals(twitterCredentials, result.first()) + } + + @Test + fun whenGetAppSuggestionsDoesNotHaveResultsThenReturnsEmtpyList() = runTest { + val testee = AutofillServiceSuggestions( + autofillStore = fakeAutofillStore, + loginDeduplicator = noopDeduplicator(), + grouper = noopGroupBuilder(), + appCredentialProvider = fakeAppCredentialsProvider, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + val result = testee.getAppSuggestions("random.package") + assertEquals(0, result.size) + } + + @Test + fun whenGetSuggestionsThenGroupedCredentialsAreFlattened() = runTest { + val grouper = object : AutofillSelectCredentialsGrouper { + override fun group( + originalUrl: String, + unsortedCredentials: List, + ): Groups { + return Groups( + listOf(twitterCredentials), + mapOf(spotifyCredentials.domain!! to listOf(spotifyCredentials)), + mapOf(amazonCredentials.domain!! to listOf(amazonCredentials)), + ) + } + } + + val testee = AutofillServiceSuggestions( + autofillStore = fakeAutofillStore, + loginDeduplicator = noopDeduplicator(), + grouper = grouper, + appCredentialProvider = fakeAppCredentialsProvider, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + testee.getSiteSuggestions(twitterCredentials.domain!!).let { + assertEquals(3, it.size) + assertTrue(it.contains(twitterCredentials)) + assertTrue(it.contains(spotifyCredentials)) + assertTrue(it.contains(amazonCredentials)) + } + } + + @Test + fun whenGetSiteSuggestionsThenDedupedResultAreProcessed() = runTest { + val testee = AutofillServiceSuggestions( + autofillStore = FakeAutofillStore( + listOf( + twitterCredentials, + twitterCredentials.copy(username = "other"), + twitterCredentials.copy(username = "another"), + ), + ), + loginDeduplicator = object : AutofillLoginDeduplicator { + override fun deduplicate( + originalUrl: String, + logins: List, + ): List { + return listOf(logins.first()) + } + }, + grouper = noopGroupBuilder(), + appCredentialProvider = fakeAppCredentialsProvider, + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + + val result = testee.getSiteSuggestions(twitterCredentials.domain!!) + + assertEquals(1, result.size) + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/RealAutofillProviderSuggestionsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/RealAutofillProviderSuggestionsTest.kt index 086fd096838d..ecbdfc9acdb0 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/RealAutofillProviderSuggestionsTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/RealAutofillProviderSuggestionsTest.kt @@ -24,6 +24,8 @@ import com.duckduckgo.autofill.api.store.AutofillStore import com.duckduckgo.autofill.impl.service.AutofillFieldType.PASSWORD import com.duckduckgo.autofill.impl.service.AutofillFieldType.USERNAME import com.duckduckgo.autofill.impl.service.mapper.AppCredentialProvider +import com.duckduckgo.autofill.noopDeduplicator +import com.duckduckgo.autofill.noopGroupBuilder import com.duckduckgo.common.test.CoroutineTestRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -57,12 +59,19 @@ class RealAutofillProviderSuggestionsTest { whenever(this.getOpenDuckDuckGoSuggestionSpecs()).thenReturn(SuggestionUISpecs("Search in DuckDuckGo", "", 0)) } + private val autofillSuggestions = AutofillServiceSuggestions( + autofillStore = autofillStore, + appCredentialProvider = appCredentialProvider, + loginDeduplicator = noopDeduplicator(), + grouper = noopGroupBuilder(), + dispatcherProvider = coroutineRule.testDispatcherProvider, + ) + private val testee = RealAutofillProviderSuggestions( appBuildConfig = appBuildConfig, - autofillStore = autofillStore, viewProvider = mockViewProvider, suggestionsFormatter = suggestionFormatter, - appCredentialProvider = appCredentialProvider, + autofillSuggestions = autofillSuggestions, ) @Test diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/mapper/fakes/FakeAppCredentialsProvider.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/mapper/fakes/FakeAppCredentialsProvider.kt new file mode 100644 index 000000000000..5fe26a07e9ce --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/service/mapper/fakes/FakeAppCredentialsProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.service.mapper.fakes + +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.impl.service.mapper.AppCredentialProvider + +class FakeAppCredentialsProvider constructor( + private val credentials: List, +) : AppCredentialProvider { + override suspend fun getCredentials(appPackage: String): List { + return credentials.filter { it.domain!!.equals(appPackage) } + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsFixtures.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsFixtures.kt index a3edf84fef5c..e020468c621f 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsFixtures.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsFixtures.kt @@ -77,6 +77,7 @@ object CredentialsFixtures { domainTitle = details.domainTitle, notes = notes, lastUpdatedMillis = details.lastUpdatedMillis, + lastUsedMillis = details.lastUsedInMillis, ) } fun LoginCredentials.toWebsiteLoginCredentials(): WebsiteLoginDetailsWithCredentials { @@ -87,6 +88,7 @@ object CredentialsFixtures { id = id, domainTitle = domainTitle, lastUpdatedMillis = lastUpdatedMillis, + lastUsedInMillis = lastUsedMillis, ), password = password, notes = notes, diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsInvalidItemsViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsInvalidItemsViewModelTest.kt index 41bd630abf6b..9cad94f20c93 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsInvalidItemsViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsInvalidItemsViewModelTest.kt @@ -39,7 +39,7 @@ class CredentialsInvalidItemsViewModelTest { val coroutineRule = CoroutineTestRule() private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSync = CredentialsSync( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncTest.kt index 931178cca55d..1a1aee7856a5 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncTest.kt @@ -44,7 +44,7 @@ import org.junit.runner.RunWith internal class CredentialsSyncTest { private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSync = CredentialsSync( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/FakeSecureStorage.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/SyncFakeSecureStorage.kt similarity index 98% rename from autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/FakeSecureStorage.kt rename to autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/SyncFakeSecureStorage.kt index b467677d44b7..71677c41cb73 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/FakeSecureStorage.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/SyncFakeSecureStorage.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf -internal class FakeSecureStorage : SecureStorage { +class SyncFakeSecureStorage : SecureStorage { private val entities = mutableListOf() override suspend fun canAccessSecureStorage(): Boolean = true diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLastModifiedWinsStrategyTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLastModifiedWinsStrategyTest.kt index 78fb34d961d8..2ff8e4e16e8a 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLastModifiedWinsStrategyTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLastModifiedWinsStrategyTest.kt @@ -30,7 +30,7 @@ import com.duckduckgo.autofill.sync.CredentialsSyncMapper import com.duckduckgo.autofill.sync.CredentialsSyncMetadata import com.duckduckgo.autofill.sync.FakeCredentialsSyncStore import com.duckduckgo.autofill.sync.FakeCrypto -import com.duckduckgo.autofill.sync.FakeSecureStorage +import com.duckduckgo.autofill.sync.SyncFakeSecureStorage import com.duckduckgo.autofill.sync.credentialsSyncEntries import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase import com.duckduckgo.autofill.sync.provider.CredentialsSyncLocalValidationFeature @@ -56,7 +56,7 @@ internal class CredentialsLastModifiedWinsStrategyTest { val coroutineRule = CoroutineTestRule() private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSync = CredentialsSync( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLocalWinsStrategyTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLocalWinsStrategyTest.kt index facfe42a126f..0c18877ebb21 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLocalWinsStrategyTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsLocalWinsStrategyTest.kt @@ -30,7 +30,7 @@ import com.duckduckgo.autofill.sync.CredentialsSyncMapper import com.duckduckgo.autofill.sync.CredentialsSyncMetadata import com.duckduckgo.autofill.sync.FakeCredentialsSyncStore import com.duckduckgo.autofill.sync.FakeCrypto -import com.duckduckgo.autofill.sync.FakeSecureStorage +import com.duckduckgo.autofill.sync.SyncFakeSecureStorage import com.duckduckgo.autofill.sync.credentialsSyncEntries import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase import com.duckduckgo.autofill.sync.provider.CredentialsSyncLocalValidationFeature @@ -55,7 +55,7 @@ internal class CredentialsLocalWinsStrategyTest { val coroutineRule = CoroutineTestRule() private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSync = CredentialsSync( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsRemoteWinsStrategyTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsRemoteWinsStrategyTest.kt index 093cccb65320..59ac55bb6258 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsRemoteWinsStrategyTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CredentialsRemoteWinsStrategyTest.kt @@ -30,7 +30,7 @@ import com.duckduckgo.autofill.sync.CredentialsSyncMapper import com.duckduckgo.autofill.sync.CredentialsSyncMetadata import com.duckduckgo.autofill.sync.FakeCredentialsSyncStore import com.duckduckgo.autofill.sync.FakeCrypto -import com.duckduckgo.autofill.sync.FakeSecureStorage +import com.duckduckgo.autofill.sync.SyncFakeSecureStorage import com.duckduckgo.autofill.sync.credentialsSyncEntries import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase import com.duckduckgo.autofill.sync.provider.CredentialsSyncLocalValidationFeature @@ -55,7 +55,7 @@ internal class CredentialsRemoteWinsStrategyTest { val coroutineRule = CoroutineTestRule() private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSync = CredentialsSync( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CrendentialsDedupStrategyTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CrendentialsDedupStrategyTest.kt index a6a80713230d..5081a9d52332 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CrendentialsDedupStrategyTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/persister/CrendentialsDedupStrategyTest.kt @@ -30,7 +30,7 @@ import com.duckduckgo.autofill.sync.CredentialsSyncMapper import com.duckduckgo.autofill.sync.CredentialsSyncMetadata import com.duckduckgo.autofill.sync.FakeCredentialsSyncStore import com.duckduckgo.autofill.sync.FakeCrypto -import com.duckduckgo.autofill.sync.FakeSecureStorage +import com.duckduckgo.autofill.sync.SyncFakeSecureStorage import com.duckduckgo.autofill.sync.credentialsSyncEntries import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase import com.duckduckgo.autofill.sync.provider.CredentialsSyncLocalValidationFeature @@ -55,7 +55,7 @@ internal class CredentialsDedupStrategyTest { val coroutineRule = CoroutineTestRule() private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSync = CredentialsSync( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProviderTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProviderTest.kt index f7d5333c2b70..b1559fc7cced 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProviderTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProviderTest.kt @@ -28,7 +28,7 @@ import com.duckduckgo.autofill.sync.CredentialsSync import com.duckduckgo.autofill.sync.CredentialsSyncMetadata import com.duckduckgo.autofill.sync.FakeCredentialsSyncStore import com.duckduckgo.autofill.sync.FakeCrypto -import com.duckduckgo.autofill.sync.FakeSecureStorage +import com.duckduckgo.autofill.sync.SyncFakeSecureStorage import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.formatters.time.DatabaseDateFormatter @@ -52,7 +52,7 @@ internal class CredentialsSyncDataProviderTest { val coroutineRule = CoroutineTestRule() private val db = inMemoryAutofillDatabase() - private val secureStorage = FakeSecureStorage() + private val secureStorage = SyncFakeSecureStorage() private val credentialsSyncMetadata = CredentialsSyncMetadata(db.credentialsSyncDao()) private val credentialsSyncStore = FakeCredentialsSyncStore() private val credentialsSync = CredentialsSync(