diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt
index 009ea0aa8976..782797548f8e 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt
@@ -92,7 +92,7 @@ internal class TrackersLottieAssetDelegate(
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
- color = context.getColorFromAttr(com.duckduckgo.mobile.android.R.attr.daxColorContainer)
+ color = context.getColorFromAttr(com.duckduckgo.mobile.android.R.attr.omnibarRoundedFieldBackgroundColor)
typeface = Typeface.SANS_SERIF
}
diff --git a/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt b/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt
index 60675677ca48..2d38bb6935fe 100644
--- a/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt
+++ b/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt
@@ -77,6 +77,7 @@ class GlobalPrivacyControlActivity : DuckDuckGoActivity() {
gpcSpannableString.getSpanFlags(it),
)
removeSpan(it)
+ trim()
}
}
binding.globalPrivacyControlDescription.apply {
diff --git a/app/src/main/res/layout/activity_accessibility_settings.xml b/app/src/main/res/layout/activity_accessibility_settings.xml
index b025536e3616..2c153370df97 100644
--- a/app/src/main/res/layout/activity_accessibility_settings.xml
+++ b/app/src/main/res/layout/activity_accessibility_settings.xml
@@ -14,114 +14,100 @@
~ limitations under the License.
-->
-
+
+ android:id="@+id/includeToolbar"
+ layout="@layout/include_default_toolbar" />
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:ignore="Overdraw">
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/keyline_4">
+ android:id="@+id/appFontSizeToggle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:primaryText="@string/accessibilityTextSizeOverrideTitle"
+ app:secondaryText="@string/accessibilityTextSizeOverrideSubtitle"
+ app:showSwitch="true" />
+ android:id="@+id/fontSizeSettingsGroup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:ignore="Overdraw">
-
+ android:paddingEnd="@dimen/keyline_4">
-
-
-
+ android:layout_gravity="center"
+ android:minWidth="50dp"
+ app:typography="body1" />
+ android:id="@+id/accessibilitySlider"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="@dimen/keyline_4"
+ android:paddingBottom="@dimen/keyline_4"
+ android:stepSize="10"
+ android:valueFrom="70"
+ android:valueTo="170"
+ app:labelBehavior="gone"
+ app:tickVisible="false" />
-
+
+ android:layout_height="wrap_content" />
+ android:id="@+id/forceZoomToggle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:primaryText="@string/accessibilityForceZoomTitle"
+ app:secondaryText="@string/accessibilityForceZoomSubtitle"
+ app:showSwitch="true" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_global_privacy_control.xml b/app/src/main/res/layout/activity_global_privacy_control.xml
index 9aaf97b6b72f..b6fae6a2f7be 100644
--- a/app/src/main/res/layout/activity_global_privacy_control.xml
+++ b/app/src/main/res/layout/activity_global_privacy_control.xml
@@ -17,42 +17,29 @@
-->
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
-
-
-
-
-
-
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/keyline_4"
+ android:text="@string/globalPrivacyControlDescription"
+ app:textType="secondary"
+ app:typography="body2" />
+
+
diff --git a/app/src/main/res/layout/include_new_browser_tab.xml b/app/src/main/res/layout/include_new_browser_tab.xml
index c7ef61f0bc95..9295e366cdd9 100644
--- a/app/src/main/res/layout/include_new_browser_tab.xml
+++ b/app/src/main/res/layout/include_new_browser_tab.xml
@@ -21,7 +21,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
- android:clipChildren="false"
tools:context="com.duckduckgo.app.browser.BrowserActivity"
tools:showIn="@layout/fragment_browser_tab">
diff --git a/autoconsent/autoconsent-impl/build.gradle b/autoconsent/autoconsent-impl/build.gradle
index 64968f7658b9..c33082926551 100644
--- a/autoconsent/autoconsent-impl/build.gradle
+++ b/autoconsent/autoconsent-impl/build.gradle
@@ -41,6 +41,7 @@ dependencies {
implementation AndroidX.appCompat
implementation JakeWharton.timber
implementation KotlinX.coroutines.core
+ implementation Google.android.material
testImplementation (KotlinX.coroutines.test) {
// https://github.com/Kotlin/kotlinx.coroutines/issues/2023
diff --git a/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml b/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml
index e02584d37ed5..db78f23954a0 100644
--- a/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml
+++ b/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml
@@ -15,34 +15,30 @@
-->
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ layout="@layout/include_default_toolbar" />
-
+ app:textType="secondary"
+ app:typography="body2" />
+ android:layout_height="wrap_content"
+ app:primaryText="@string/autoconsentToggle"
+ app:primaryTextTruncated="false"
+ app:showSwitch="true" />
diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/ui/urlmatcher/AutofillUrlMatcher.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/ui/urlmatcher/AutofillUrlMatcher.kt
new file mode 100644
index 000000000000..a5523539f10b
--- /dev/null
+++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/ui/urlmatcher/AutofillUrlMatcher.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2022 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.ui.urlmatcher
+
+interface AutofillUrlMatcher {
+ fun extractUrlPartsForAutofill(originalUrl: String): ExtractedUrlParts
+ fun matchingForAutofill(visitedSite: ExtractedUrlParts, savedSite: ExtractedUrlParts): Boolean
+
+ data class ExtractedUrlParts(val eTldPlus1: String?, val subdomain: String?)
+}
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt
index 4fdad217ffaa..0a2ccc1b248c 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt
@@ -24,6 +24,8 @@ import com.duckduckgo.autofill.store.RealAutofillPrefsStore
import com.duckduckgo.autofill.store.RealInternalTestUserStore
import com.duckduckgo.autofill.store.RealLastUpdatedTimeProvider
import com.duckduckgo.autofill.store.SecureStoreBackedAutofillStore
+import com.duckduckgo.autofill.store.urlmatcher.AutofillDomainNameUrlMatcher
+import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.securestorage.api.SecureStorage
import com.squareup.anvil.annotations.ContributesTo
@@ -44,12 +46,17 @@ class AutofillModule {
secureStorage: SecureStorage,
context: Context,
internalTestUserChecker: InternalTestUserChecker,
+ autofillUrlMatcher: AutofillUrlMatcher,
): AutofillStore {
return SecureStoreBackedAutofillStore(
- secureStorage,
- internalTestUserChecker,
- RealLastUpdatedTimeProvider(),
- RealAutofillPrefsStore(context, internalTestUserChecker),
+ secureStorage = secureStorage,
+ internalTestUserChecker = internalTestUserChecker,
+ lastUpdatedTimeProvider = RealLastUpdatedTimeProvider(),
+ autofillPrefsStore = RealAutofillPrefsStore(context, internalTestUserChecker),
+ autofillUrlMatcher = autofillUrlMatcher,
)
}
+
+ @Provides
+ fun provideAutofillUrlMatcher(): AutofillUrlMatcher = AutofillDomainNameUrlMatcher()
}
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt
index f777e32e7b35..a91a89964405 100644
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt
@@ -16,20 +16,24 @@
package com.duckduckgo.autofill.ui.credential.management.suggestion
-import com.duckduckgo.app.global.extractSchemeAndDomain
import com.duckduckgo.autofill.domain.app.LoginCredentials
+import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher
import javax.inject.Inject
-class SuggestionMatcher @Inject constructor() {
+class SuggestionMatcher @Inject constructor(private val autofillUrlMatcher: AutofillUrlMatcher) {
fun getSuggestions(
currentUrl: String?,
credentials: List,
): List {
if (currentUrl == null) return emptyList()
+ val currentSite = autofillUrlMatcher.extractUrlPartsForAutofill(currentUrl)
+ if (currentSite.eTldPlus1 == null) return emptyList()
return credentials.filter {
- it.domain?.extractSchemeAndDomain() == currentUrl.extractSchemeAndDomain()
+ val storedDomain = it.domain ?: return@filter false
+ val savedSite = autofillUrlMatcher.extractUrlPartsForAutofill(storedDomain)
+ return@filter autofillUrlMatcher.matchingForAutofill(currentSite, savedSite)
}
}
}
diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt
index 788fd43bde6d..6f5d0d84e01b 100644
--- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt
+++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt
@@ -17,6 +17,7 @@
package com.duckduckgo.autofill.ui.credential.management.suggestion
import com.duckduckgo.autofill.domain.app.LoginCredentials
+import com.duckduckgo.autofill.store.urlmatcher.AutofillDomainNameUrlMatcher
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
@@ -25,7 +26,7 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class SuggestionMatcherTest {
- private val testee = SuggestionMatcher()
+ private val testee = SuggestionMatcher(AutofillDomainNameUrlMatcher())
@Test
fun whenUrlIsNullThenNoSuggestions() {
@@ -69,6 +70,13 @@ class SuggestionMatcherTest {
assertEquals(2, suggestions.size)
}
+ @Test
+ fun whenSubdomainIncludedInSavedSiteAndVisitingRootSiteThenSuggestionOffered() {
+ val creds = listOf(creds("https://duckduckgo.com"))
+ val suggestions = testee.getSuggestions("https://test.duckduckgo.com", creds)
+ assertEquals(1, suggestions.size)
+ }
+
private fun creds(domain: String): LoginCredentials {
return LoginCredentials(id = 0, domain = domain, username = "username", password = "password")
}
diff --git a/autofill/autofill-store/build.gradle b/autofill/autofill-store/build.gradle
index bd38a0434874..b61bbe0c903d 100644
--- a/autofill/autofill-store/build.gradle
+++ b/autofill/autofill-store/build.gradle
@@ -15,6 +15,7 @@ dependencies {
implementation KotlinX.coroutines.core
implementation Google.android.material
implementation project(path: ':secure-storage-api')
+ implementation Square.okHttp3.okHttp
implementation Google.dagger
implementation JakeWharton.timber
diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt
index b35ac6af503c..df389f13f2a5 100644
--- a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt
+++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt
@@ -26,6 +26,7 @@ import com.duckduckgo.autofill.InternalTestUserChecker
import com.duckduckgo.autofill.domain.app.LoginCredentials
import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult
import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.NoMatch
+import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher
import com.duckduckgo.securestorage.api.SecureStorage
import com.duckduckgo.securestorage.api.WebsiteLoginDetails
import com.duckduckgo.securestorage.api.WebsiteLoginDetailsWithCredentials
@@ -41,6 +42,7 @@ class SecureStoreBackedAutofillStore(
private val lastUpdatedTimeProvider: LastUpdatedTimeProvider,
private val autofillPrefsStore: AutofillPrefsStore,
private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(),
+ private val autofillUrlMatcher: AutofillUrlMatcher,
) : AutofillStore {
override val autofillAvailable: Boolean
@@ -79,13 +81,21 @@ class SecureStoreBackedAutofillStore(
override suspend fun getCredentials(rawUrl: String): List {
return withContext(dispatcherProvider.io()) {
return@withContext if (autofillEnabled && autofillAvailable) {
- Timber.i("Querying secure store for stored credentials. rawUrl: %s, extractedDomain:%s", rawUrl, rawUrl.extractSchemeAndDomain())
- val url = rawUrl.extractSchemeAndDomain() ?: return@withContext emptyList()
+ Timber.i("Querying secure store for stored credentials. rawUrl: %s", rawUrl)
- val storedCredentials = secureStorage.websiteLoginDetailsWithCredentialsForDomain(url).firstOrNull() ?: emptyList()
- Timber.v("Found %d credentials for %s", storedCredentials.size, url)
+ val visitedSite = autofillUrlMatcher.extractUrlPartsForAutofill(rawUrl)
+ if (visitedSite.eTldPlus1 == null) return@withContext emptyList()
- storedCredentials.map { it.toLoginCredentials() }
+ // first part of domain matching happens at the DB level
+ val storedCredentials =
+ secureStorage.websiteLoginDetailsWithCredentialsForDomain(visitedSite.eTldPlus1!!).firstOrNull() ?: emptyList()
+
+ // second part of domain matching requires filtering at code level
+ storedCredentials.filter {
+ val storedDomain = it.details.domain ?: return@filter false
+ val savedSite = autofillUrlMatcher.extractUrlPartsForAutofill(storedDomain)
+ return@filter autofillUrlMatcher.matchingForAutofill(visitedSite, savedSite)
+ }.map { it.toLoginCredentials() }
} else {
emptyList()
}
diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcher.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcher.kt
new file mode 100644
index 000000000000..6e0ff4d8c458
--- /dev/null
+++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcher.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2022 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.store.urlmatcher
+
+import com.duckduckgo.app.global.extractDomain
+import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher
+import javax.inject.Inject
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import timber.log.Timber
+
+class AutofillDomainNameUrlMatcher @Inject constructor() : AutofillUrlMatcher {
+
+ override fun extractUrlPartsForAutofill(originalUrl: String): AutofillUrlMatcher.ExtractedUrlParts {
+ val normalizedUrl = originalUrl.normalizeScheme()
+ return try {
+ val eTldPlus1 = normalizedUrl.toHttpUrl().topPrivateDomain()
+ val domain = originalUrl.extractDomain()
+ val subdomain = determineSubdomain(domain, eTldPlus1)
+ AutofillUrlMatcher.ExtractedUrlParts(eTldPlus1, subdomain)
+ } catch (e: IllegalArgumentException) {
+ Timber.w("Unable to parse e-tld+1 from $originalUrl")
+ AutofillUrlMatcher.ExtractedUrlParts(null, null)
+ }
+ }
+
+ private fun determineSubdomain(
+ domain: String?,
+ eTldPlus1: String?,
+ ): String? {
+ if (eTldPlus1 == null) return null
+
+ val subdomain = domain?.replace(eTldPlus1 ?: "", "", ignoreCase = true)?.removeSuffix(".")
+ if (subdomain?.isBlank() == true) return null
+
+ return subdomain
+ }
+
+ override fun matchingForAutofill(
+ visitedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ savedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ ): Boolean {
+ // e-tld+1 must match
+ if (!identicalEffectiveTldPlusOne(visitedSite, savedSite)) return false
+
+ // any of these rules can match
+ return identicalSubdomains(visitedSite, savedSite) ||
+ specialHandlingForWwwSubdomainOnSavedSite(visitedSite, savedSite) ||
+ savedSiteHasNoSubdomain(savedSite)
+ }
+
+ private fun identicalEffectiveTldPlusOne(
+ visitedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ savedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ ): Boolean {
+ return visitedSite.eTldPlus1.equals(savedSite.eTldPlus1, ignoreCase = true)
+ }
+
+ private fun identicalSubdomains(
+ visitedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ savedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ ): Boolean {
+ return visitedSite.subdomain.equals(savedSite.subdomain, ignoreCase = true)
+ }
+
+ private fun specialHandlingForWwwSubdomainOnSavedSite(
+ visitedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ savedSite: AutofillUrlMatcher.ExtractedUrlParts,
+ ): Boolean {
+ return (visitedSite.subdomain == null && savedSite.subdomain.equals(WWW, ignoreCase = true))
+ }
+
+ private fun savedSiteHasNoSubdomain(savedSite: AutofillUrlMatcher.ExtractedUrlParts): Boolean {
+ return savedSite.subdomain == null
+ }
+
+ private fun String.normalizeScheme(): String {
+ if (!startsWith("https://") && !startsWith("http://")) {
+ return "https://$this"
+ }
+ return this
+ }
+
+ companion object {
+ private const val WWW = "www"
+ }
+}
diff --git a/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt
index fcd9c0bad22e..1c0a5a9220e3 100644
--- a/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt
+++ b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt
@@ -27,6 +27,8 @@ import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.NoM
import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.UrlOnlyMatch
import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.UsernameMatch
import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.UsernameMissing
+import com.duckduckgo.autofill.store.urlmatcher.AutofillDomainNameUrlMatcher
+import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher
import com.duckduckgo.securestorage.api.SecureStorage
import com.duckduckgo.securestorage.api.WebsiteLoginDetails
import com.duckduckgo.securestorage.api.WebsiteLoginDetailsWithCredentials
@@ -65,6 +67,8 @@ class SecureStoreBackedAutofillStoreTest {
private lateinit var internalTestUserChecker: FakeInternalTestUserChecker
private lateinit var secureStore: FakeSecureStore
+ private val autofillUrlMatcher: AutofillUrlMatcher = AutofillDomainNameUrlMatcher()
+
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
@@ -349,6 +353,75 @@ class SecureStoreBackedAutofillStoreTest {
assertNull(testee.getCredentialsWithId(1))
}
+ @Test
+ fun whenExactUrlMatchesThenCredentialsReturned() = runTest {
+ setupTesteeWithAutofillAvailable()
+ val savedUrl = "https://example.com"
+ val visitedSite = "https://example.com"
+ storeCredentials(1, savedUrl, "username1", "password123")
+
+ val results = testee.getCredentials(visitedSite)
+ assertEquals(1, results.size)
+ assertEquals(1L, results[0].id)
+ }
+
+ @Test
+ fun whenExactSubdomainMatchesThenCredentialsReturned() = runTest {
+ setupTesteeWithAutofillAvailable()
+ val savedUrl = "https://subdomain.example.com"
+ val visitedSite = "https://subdomain.example.com"
+ storeCredentials(1, savedUrl, "username1", "password123")
+
+ val results = testee.getCredentials(visitedSite)
+ assertEquals(1, results.size)
+ assertEquals(1L, results[0].id)
+ }
+
+ @Test
+ fun whenExactWwwSubdomainMatchesThenCredentialsReturned() = runTest {
+ setupTesteeWithAutofillAvailable()
+ val savedUrl = "https://www.example.com"
+ val visitedSite = "https://www.example.com"
+ storeCredentials(1, savedUrl, "username1", "password123")
+
+ val results = testee.getCredentials(visitedSite)
+ assertEquals(1, results.size)
+ assertEquals(1L, results[0].id)
+ }
+
+ @Test
+ fun whenSavedCredentialHasSubdomainAndVisitedPageDoesNotThenCredentialNotMatched() = runTest {
+ setupTesteeWithAutofillAvailable()
+ val savedUrl = "https://test.example.com"
+ val visitedSite = "https://example.com"
+ storeCredentials(1, savedUrl, "username1", "password123")
+
+ val results = testee.getCredentials(visitedSite)
+ assertEquals(0, results.size)
+ }
+
+ @Test
+ fun whenSavedCredentialHasNoSubdomainAndVisitedPageDoesThenCredentialNotMatched() = runTest {
+ setupTesteeWithAutofillAvailable()
+ val savedUrl = "https://example.com"
+ val visitedSite = "https://test.example.com"
+ storeCredentials(1, savedUrl, "username1", "password123")
+
+ val results = testee.getCredentials(visitedSite)
+ assertEquals(1, results.size)
+ }
+
+ @Test
+ fun whenSavedCredentialHasDifferentSubdomainToVisitedPageSubdomainThenCredentialNotMatched() = runTest {
+ setupTesteeWithAutofillAvailable()
+ val savedUrl = "https://test.example.com"
+ val visitedSite = "https://different.example.com"
+ storeCredentials(1, savedUrl, "username1", "password123")
+
+ val results = testee.getCredentials(visitedSite)
+ assertEquals(0, results.size)
+ }
+
private fun List.assertHasNoLoginCredentials(
url: String,
username: String,
@@ -376,7 +449,13 @@ class SecureStoreBackedAutofillStoreTest {
private fun setupTesteeWithAutofillAvailable() {
internalTestUserChecker = FakeInternalTestUserChecker(true)
secureStore = FakeSecureStore(true)
- testee = SecureStoreBackedAutofillStore(secureStore, internalTestUserChecker, lastUpdatedTimeProvider, autofillPrefsStore)
+ testee = SecureStoreBackedAutofillStore(
+ secureStorage = secureStore,
+ internalTestUserChecker = internalTestUserChecker,
+ lastUpdatedTimeProvider = lastUpdatedTimeProvider,
+ autofillPrefsStore = autofillPrefsStore,
+ autofillUrlMatcher = autofillUrlMatcher,
+ )
}
private fun setupTestee(
@@ -386,11 +465,12 @@ class SecureStoreBackedAutofillStoreTest {
internalTestUserChecker = FakeInternalTestUserChecker(isInternalUser)
secureStore = FakeSecureStore(canAccessSecureStorage)
testee = SecureStoreBackedAutofillStore(
- secureStore,
- internalTestUserChecker,
- lastUpdatedTimeProvider,
- autofillPrefsStore,
+ secureStorage = secureStore,
+ internalTestUserChecker = internalTestUserChecker,
+ lastUpdatedTimeProvider = lastUpdatedTimeProvider,
+ autofillPrefsStore = autofillPrefsStore,
dispatcherProvider = coroutineTestRule.testDispatcherProvider,
+ autofillUrlMatcher = autofillUrlMatcher,
)
}
@@ -442,7 +522,7 @@ class SecureStoreBackedAutofillStoreTest {
return flow {
emit(
credentials.filter {
- it.details.domain == domain
+ it.details.domain?.contains(domain) == true
}.map {
it.details
},
@@ -464,7 +544,7 @@ class SecureStoreBackedAutofillStoreTest {
return flow {
emit(
credentials.filter {
- it.details.domain == domain
+ it.details.domain?.contains(domain) == true
},
)
}
diff --git a/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcherTest.kt b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcherTest.kt
new file mode 100644
index 000000000000..e6fcc7df8fc6
--- /dev/null
+++ b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcherTest.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2022 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.store.urlmatcher
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AutofillDomainNameUrlMatcherTest {
+
+ private val testee = AutofillDomainNameUrlMatcher()
+
+ @Test
+ fun whenBasicDomainThenSameReturnedForEtldPlus1() {
+ val inputUrl = "duckduckgo.com"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals(inputUrl, result.eTldPlus1)
+ }
+
+ @Test
+ fun whenUrlIsInvalidMissingTldThenEtldPlusOneIsNull() {
+ val inputUrl = "duckduckgo"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertNull(result.eTldPlus1)
+ }
+
+ @Test
+ fun whenUrlIsInvalidMissingTldThenSubdomainIsNull() {
+ val inputUrl = "duckduckgo"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertNull(result.subdomain)
+ }
+
+ @Test
+ fun whenContainsHttpSchemeThenEtldPlus1Returned() {
+ val inputUrl = "http://duckduckgo.com"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("duckduckgo.com", result.eTldPlus1)
+ }
+
+ @Test
+ fun whenContainsHttpsSchemeThenEtldPlus1Returned() {
+ val inputUrl = "https://duckduckgo.com"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("duckduckgo.com", result.eTldPlus1)
+ }
+
+ @Test
+ fun whenContainsWwwSubdomainThenEtldPlus1Returned() {
+ val inputUrl = "www.duckduckgo.com"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("duckduckgo.com", result.eTldPlus1)
+ }
+
+ @Test
+ fun whenContainsAnotherSubdomainThenEtldPlus1Returned() {
+ val inputUrl = "foo.duckduckgo.com"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("duckduckgo.com", result.eTldPlus1)
+ }
+
+ @Test
+ fun whenTwoPartTldWithNoSubdomainThenEtldPlus1Returned() {
+ val inputUrl = "duckduckgo.co.uk"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("duckduckgo.co.uk", result.eTldPlus1)
+ }
+
+ @Test
+ fun whenTwoPartTldWithSubdomainThenEtldPlus1Returned() {
+ val inputUrl = "www.duckduckgo.co.uk"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("duckduckgo.co.uk", result.eTldPlus1)
+ }
+
+ @Test
+ fun whenSubdomainIsWwwThenCorrectlyIdentifiedAsSubdomain() {
+ val inputUrl = "www.duckduckgo.co.uk"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("www", result.subdomain)
+ }
+
+ @Test
+ fun whenSubdomainIsPresentButNotWwwThenCorrectlyIdentifiedAsSubdomain() {
+ val inputUrl = "test.duckduckgo.co.uk"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("test", result.subdomain)
+ }
+
+ @Test
+ fun whenSubdomainHasTwoLevelsThenCorrectlyIdentifiedAsSubdomain() {
+ val inputUrl = "foo.bar.duckduckgo.co.uk"
+ val result = testee.extractUrlPartsForAutofill(inputUrl)
+ assertEquals("foo.bar", result.subdomain)
+ }
+
+ @Test
+ fun whenUrlsAreIdenticalThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("https://example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("https://example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenUrlsAreIdenticalExceptForUppercaseVisitedSiteThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("https://example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("https://EXAMPLE.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenUrlsAreIdenticalExceptForUppercaseSavedSiteThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("https://EXAMPLE.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("https://example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenBothUrlsContainSameSubdomainThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("test.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("test.example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenBothUrlsContainWwwSubdomainThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("www.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("www.example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteContainsSubdomainAndVisitedSiteDoesNotThenNoMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("foo.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("example.com")
+ assertFalse(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteDoesNotContainSubdomainAndVisitedSiteDoesThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("foo.example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenUrlsHaveDifferentSubdomainsThenNoMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("bar.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("foo.example.com")
+ assertFalse(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteContainsWwwSubdomainAndVisitedSiteDoesNotThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("www.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteContainsUppercaseWwwSubdomainAndVisitedSiteDoesNotThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("WWW.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteDoesNotContainSubdomainAndVisitedSiteDoesContainWwwSubdomainThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("www.example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteDoesNotContainSubdomainAndVisitedSiteDoesContainUppercaseWwwSubdomainThenMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("WWW.example.com")
+ assertTrue(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteContainNestedSubdomainsAndVisitedSiteContainsMatchingRootSubdomainThenNotMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("a.b.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("b.example.com")
+ assertFalse(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteContainSubdomainAndVisitedSiteContainsNestedSubdomainsThenNotMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("b.example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("a.b.example.com")
+ assertFalse(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedSiteHasNoSubdomainAndVisitedMaliciousSitePartiallyContainSavedSiteThenNoMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("example.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("example.com.malicious.com")
+ assertFalse(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+
+ @Test
+ fun whenSavedMaliciousSitePartiallyContainsVisitedSiteThenNoMatchingForAutofill() {
+ val savedSite = testee.extractUrlPartsForAutofill("example.com.malicious.com")
+ val visitedSite = testee.extractUrlPartsForAutofill("example.com")
+ assertFalse(testee.matchingForAutofill(visitedSite, savedSite))
+ }
+}
diff --git a/common-ui/src/main/res/drawable/background_text_view_container.xml b/common-ui/src/main/res/drawable/background_text_view_container.xml
new file mode 100644
index 000000000000..accb8406b0c9
--- /dev/null
+++ b/common-ui/src/main/res/drawable/background_text_view_container.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common-ui/src/main/res/drawable/popup_menu_bg.xml b/common-ui/src/main/res/drawable/popup_menu_bg.xml
index deb447fae7ff..3b8d30ba237a 100644
--- a/common-ui/src/main/res/drawable/popup_menu_bg.xml
+++ b/common-ui/src/main/res/drawable/popup_menu_bg.xml
@@ -15,6 +15,6 @@
-->
-
-
+
+
\ No newline at end of file
diff --git a/common-ui/src/main/res/layout/view_dax_dialog.xml b/common-ui/src/main/res/layout/view_dax_dialog.xml
index a73bad53b379..78aad676f524 100644
--- a/common-ui/src/main/res/layout/view_dax_dialog.xml
+++ b/common-ui/src/main/res/layout/view_dax_dialog.xml
@@ -25,8 +25,7 @@
+ android:layout_height="match_parent">
+
diff --git a/common-ui/src/main/res/values/design-system-theming.xml b/common-ui/src/main/res/values/design-system-theming.xml
index 13194f11254e..8f2a85a61f23 100644
--- a/common-ui/src/main/res/values/design-system-theming.xml
+++ b/common-ui/src/main/res/values/design-system-theming.xml
@@ -137,23 +137,19 @@
- @color/black60
- @color/white
- - ?attr/daxColorSurface
- - ?attr/daxColorSurface
-
-
- - ?attr/daxColorSurface
- - ?attr/daxColorPrimaryIcon
- - ?attr/daxColorPrimaryText
- - ?attr/daxColorSecondaryText
diff --git a/common-ui/src/main/res/values/widgets.xml b/common-ui/src/main/res/values/widgets.xml
index 393fa4b5152b..15bf52a0458b 100644
--- a/common-ui/src/main/res/values/widgets.xml
+++ b/common-ui/src/main/res/values/widgets.xml
@@ -24,12 +24,12 @@
diff --git a/secure-storage/secure-storage-store/build.gradle b/secure-storage/secure-storage-store/build.gradle
index 1e9e1949854c..7f6012ea10db 100644
--- a/secure-storage/secure-storage-store/build.gradle
+++ b/secure-storage/secure-storage-store/build.gradle
@@ -42,6 +42,9 @@ dependencies {
kapt AndroidX.room.compiler
testImplementation Testing.junit4
+ testImplementation project(path: ':common-test')
+ testImplementation Testing.robolectric
+ testImplementation AndroidX.archCore.testing
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
testImplementation (KotlinX.coroutines.test) {
// https://github.com/Kotlin/kotlinx.coroutines/issues/2023
diff --git a/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt b/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt
index a3ac9a3770db..08d0fb22c792 100644
--- a/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt
+++ b/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt
@@ -36,7 +36,7 @@ interface WebsiteLoginCredentialsDao {
@Query("select * from website_login_credentials")
fun websiteLoginCredentials(): Flow>
- @Query("select * from website_login_credentials where domain = :domain")
+ @Query("select * from website_login_credentials where domain like '%' || :domain || '%'")
fun websiteLoginCredentialsByDomain(domain: String): Flow>
@Query("select * from website_login_credentials where id = :id")
diff --git a/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt b/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt
index 8ccf0fbb9615..a4a7fb2f7cf9 100644
--- a/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt
+++ b/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt
@@ -16,97 +16,153 @@
package com.duckduckgo.securestorage.store
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import com.duckduckgo.app.CoroutineTestRule
+import com.duckduckgo.securestorage.store.db.SecureStorageDatabase
import com.duckduckgo.securestorage.store.db.WebsiteLoginCredentialsDao
import com.duckduckgo.securestorage.store.db.WebsiteLoginCredentialsEntity
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
+import org.junit.After
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
-import org.mockito.Mock
+import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
@ExperimentalCoroutinesApi
+@RunWith(RobolectricTestRunner::class)
class RealSecureStorageRepositoryTest {
- @Mock
+
+ @get:Rule
+ @Suppress("unused")
+ val coroutineRule = CoroutineTestRule()
+
+ @get:Rule
+ @Suppress("unused")
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var db: SecureStorageDatabase
+
private lateinit var dao: WebsiteLoginCredentialsDao
private lateinit var testee: RealSecureStorageRepository
- private val testEntity = WebsiteLoginCredentialsEntity(
- id = 1,
- domain = "test.com",
- username = "test",
- password = "pass123",
- passwordIv = "iv",
- notes = "my notes",
- notesIv = "notesIv",
- domainTitle = "test",
- lastUpdatedInMillis = 0L,
- )
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
+ db = Room.inMemoryDatabaseBuilder(RuntimeEnvironment.getApplication(), SecureStorageDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = db.websiteLoginCredentialsDao()
testee = RealSecureStorageRepository(dao)
}
- @Test
- fun whenAddWebsiteLoginCredentialThenCallInsertToDao() {
- runTest {
- testee.addWebsiteLoginCredential(testEntity)
- }
-
- verify(dao).insert(testEntity)
+ @After
+ fun after() {
+ db.close()
}
@Test
- fun whenGetWebsiteLoginCredentialsWithDomainThenCallGetWithDomainFromDao() = runTest {
- whenever(dao.websiteLoginCredentialsByDomain("test")).thenReturn(
- MutableStateFlow(listOf(testEntity)),
- )
+ fun whenRetrievingLoginCredentialByIdThenNullReturnedIfNotExists() = runTest {
+ assertNull(dao.getWebsiteLoginCredentialsById(entity().id))
+ }
- val result: List =
- testee.websiteLoginCredentialsForDomain("test").first()
+ @Test
+ fun whenRetrievingLoginCredentialByIdThenReturnedIfExists() = runTest {
+ val testEntity = entity()
+ dao.insert(entity())
+ val result = testee.getWebsiteLoginCredentialsForId(testEntity.id)
+ assertEquals(testEntity, result)
+ }
+ @Test
+ fun whenRetrievingLoginCredentialByDomainThenReturnedIfDirectMatch() = runTest {
+ val testEntity = entity()
+ dao.insert(testEntity)
+ val result: List = testee.websiteLoginCredentialsForDomain("test.com").first()
assertEquals(testEntity, result[0])
}
@Test
- fun whenGetAllWebsiteLoginCredentialsThenCallGetAllFromDao() = runTest {
- whenever(dao.websiteLoginCredentials()).thenReturn(
- MutableStateFlow(listOf(testEntity)),
- )
+ fun whenRetrievingLoginCredentialByDomainThenEmptyListReturnedIfNoMatches() = runTest {
+ val testEntity = entity()
+ dao.insert(testEntity)
+ val result: List = testee.websiteLoginCredentialsForDomain("no-matches.com").first()
+ assertTrue(result.isEmpty())
+ }
- val result: List =
- testee.websiteLoginCredentials().first()
+ @Test
+ fun whenGetAllWebsiteLoginCredentialsWithSitesThenEmptyListReturned() = runTest {
+ val result: List = testee.websiteLoginCredentials().first()
+ assertTrue(result.isEmpty())
+ }
+ @Test
+ fun whenGetAllWebsiteLoginCredentialsWithASingleSiteThenThatOneIsReturned() = runTest {
+ val testEntity = entity()
+ dao.insert(testEntity)
+ val result: List = testee.websiteLoginCredentials().first()
assertEquals(listOf(testEntity), result)
}
@Test
- fun whenGetWebsiteLoginCredentialsWithIDThenCallGetWithIDFromDao() = runTest {
- whenever(dao.getWebsiteLoginCredentialsById(1)).thenReturn(testEntity)
-
- val result =
- testee.getWebsiteLoginCredentialsForId(1)
-
- assertEquals(testEntity, result)
+ fun whenGetAllWebsiteLoginCredentialsWithMultipleSitesThenThatAllReturned() = runTest {
+ val testEntity = entity()
+ val anotherEntity = entity(id = testEntity.id + 1)
+ dao.insert(testEntity)
+ dao.insert(anotherEntity)
+ val result: List = testee.websiteLoginCredentials().first()
+ assertEquals(listOf(testEntity, anotherEntity), result)
}
@Test
fun whenUpdateWebsiteLoginCredentialsThenCallUpdateToDao() = runTest {
- testee.updateWebsiteLoginCredentials(testEntity)
-
- verify(dao).update(testEntity)
+ val testEntity = entity()
+ dao.insert(testEntity)
+ testee.updateWebsiteLoginCredentials(testEntity.copy(username = "newUsername"))
+ val updated = testee.getWebsiteLoginCredentialsForId(testEntity.id)
+ assertNotNull(updated)
+ assertEquals("newUsername", updated!!.username)
}
@Test
- fun whenDeleteWebsiteLoginCredentialsThenCallDeleteToDao() = runTest {
+ fun whenDeleteWebsiteLoginCredentialsThenEntityRemoved() = runTest {
+ val testEntity = entity()
+ dao.insert(testEntity)
testee.deleteWebsiteLoginCredentials(1)
+ val nowDeleted = dao.getWebsiteLoginCredentialsById(testEntity.id)
+ assertNull(nowDeleted)
+ }
- verify(dao).delete(1)
+ private fun entity(
+ id: Long = 1,
+ domain: String = "test.com",
+ username: String = "test",
+ password: String = "pass123",
+ passwordIv: String = "iv",
+ notes: String = "my notes",
+ notesIv: String = "notesIv",
+ domainTitle: String = "test",
+ lastUpdatedInMillis: Long = 0L,
+ ): WebsiteLoginCredentialsEntity {
+ return WebsiteLoginCredentialsEntity(
+ id = id,
+ domain = domain,
+ username = username,
+ password = password,
+ passwordIv = passwordIv,
+ notes = notes,
+ notesIv = notesIv,
+ domainTitle = domainTitle,
+ lastUpdatedInMillis = lastUpdatedInMillis,
+ )
}
}
diff --git a/versions.properties b/versions.properties
index 9ca4d8b29fdf..9f1b04227c57 100644
--- a/versions.properties
+++ b/versions.properties
@@ -67,7 +67,7 @@ version.com.github.bumptech.glide..okhttp3-integration=4.13.2
version.com.nhaarman.mockitokotlin2..mockito-kotlin=2.2.0
-version.google.android.material=1.6.1
+version.google.android.material=1.7.0
version.google.dagger=2.44.2