Skip to content

Commit

Permalink
Integrate with remote config to determine if autofill is on by default
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed Nov 7, 2023
1 parent 5cd0f2a commit 2f94aa0
Show file tree
Hide file tree
Showing 14 changed files with 504 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,11 @@ interface AutofillFeature {
*/
@Toggle.DefaultValue(false)
fun canAccessCredentialManagement(): Toggle

/**
* @return `true` when the remote config has the global "onByDefault" autofill sub-feature flag enabled
* If the remote feature is not present defaults to `false`
*/
@Toggle.DefaultValue(false)
fun onByDefault(): Toggle
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.room.Room
import com.duckduckgo.anvil.annotations.ContributesPluginPoint
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.global.DispatcherProvider
import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
import com.duckduckgo.autofill.api.InternalTestUserChecker
import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizer
Expand All @@ -35,12 +36,15 @@ import com.duckduckgo.autofill.store.LastUpdatedTimeProvider
import com.duckduckgo.autofill.store.RealAutofillPrefsStore
import com.duckduckgo.autofill.store.RealInternalTestUserStore
import com.duckduckgo.autofill.store.RealLastUpdatedTimeProvider
import com.duckduckgo.autofill.store.feature.AutofillDefaultStateDecider
import com.duckduckgo.autofill.store.feature.AutofillFeatureRepository
import com.duckduckgo.autofill.store.feature.RealAutofillDefaultStateDecider
import com.duckduckgo.autofill.store.feature.RealAutofillFeatureRepository
import com.duckduckgo.autofill.store.feature.email.incontext.ALL_MIGRATIONS as EmailInContextMigrations
import com.duckduckgo.autofill.store.feature.email.incontext.EmailProtectionInContextDatabase
import com.duckduckgo.autofill.store.feature.email.incontext.EmailProtectionInContextFeatureRepository
import com.duckduckgo.autofill.store.feature.email.incontext.RealEmailProtectionInContextFeatureRepository
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
Expand All @@ -63,9 +67,22 @@ class AutofillModule {
@Provides
fun provideAutofillPrefsStore(
context: Context,
internalTestUserChecker: InternalTestUserChecker,
autofillDefaultStateDecider: AutofillDefaultStateDecider,
): AutofillPrefsStore {
return RealAutofillPrefsStore(context, internalTestUserChecker)
return RealAutofillPrefsStore(context, autofillDefaultStateDecider)
}

@Provides
fun providerAutofillDefaultStateProvider(
userBrowserProperties: UserBrowserProperties,
autofillFeature: AutofillFeature,
internalTestUserChecker: InternalTestUserChecker,
): AutofillDefaultStateDecider {
return RealAutofillDefaultStateDecider(
userBrowserProperties = userBrowserProperties,
autofillFeature = autofillFeature,
internalTestUserChecker = internalTestUserChecker,
)
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@
package com.duckduckgo.autofill.impl

import com.duckduckgo.app.CoroutineTestRule
import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.api.InternalTestUserChecker
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.feature.toggles.api.toggle.AutofillTestFeature
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
Expand Down Expand Up @@ -136,13 +135,13 @@ class AutofillCapabilityCheckerImplTest {
canGeneratePassword: Boolean = false,
canAccessCredentialManagement: Boolean = false,
) {
val autofillFeature = AutofillTestFeature(
topLevelFeatureEnabled = topLevelFeatureEnabled,
canInjectCredentials = canInjectCredentials,
canSaveCredentials = canSaveCredentials,
canGeneratePassword = canGeneratePassword,
canAccessCredentialManagement = canAccessCredentialManagement,
)
val autofillFeature = AutofillTestFeature().also {
it.topLevelFeatureEnabled = topLevelFeatureEnabled
it.canInjectCredentials = canInjectCredentials
it.canGeneratePassword = canGeneratePassword
it.canSaveCredentials = canSaveCredentials
it.canAccessCredentialManagement = canAccessCredentialManagement
}

whenever(autofillGlobalCapabilityChecker.isSecureAutofillAvailable()).thenReturn(true)
whenever(autofillGlobalCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true)
Expand All @@ -157,64 +156,6 @@ class AutofillCapabilityCheckerImplTest {
)
}

private class AutofillTestFeature(
private val topLevelFeatureEnabled: Boolean,
private val canInjectCredentials: Boolean,
private val canSaveCredentials: Boolean,
private val canGeneratePassword: Boolean,
private val canAccessCredentialManagement: Boolean,
) : AutofillFeature {
override fun self(): Toggle {
return object : Toggle {
override fun isEnabled(): Boolean = topLevelFeatureEnabled
override fun setEnabled(state: State) {}
override fun getRawStoredState(): State? {
TODO("Not yet implemented")
}
}
}

override fun canInjectCredentials(): Toggle {
return object : Toggle {
override fun isEnabled(): Boolean = canInjectCredentials
override fun setEnabled(state: State) {}
override fun getRawStoredState(): State? {
TODO("Not yet implemented")
}
}
}

override fun canSaveCredentials(): Toggle {
return object : Toggle {
override fun isEnabled(): Boolean = canSaveCredentials
override fun setEnabled(state: State) {}
override fun getRawStoredState(): State? {
TODO("Not yet implemented")
}
}
}

override fun canGeneratePasswords(): Toggle {
return object : Toggle {
override fun isEnabled(): Boolean = canGeneratePassword
override fun setEnabled(state: State) {}
override fun getRawStoredState(): State? {
TODO("Not yet implemented")
}
}
}

override fun canAccessCredentialManagement(): Toggle {
return object : Toggle {
override fun isEnabled(): Boolean = canAccessCredentialManagement
override fun setEnabled(state: State) {}
override fun getRawStoredState(): State? {
TODO("Not yet implemented")
}
}
}
}

companion object {
private const val URL = "https://example.com"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.duckduckgo.autofill.impl.AutofillGlobalCapabilityChecker
import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupFeature
import com.duckduckgo.autofill.impl.email.remoteconfig.EmailProtectionInContextExceptions
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.feature.toggles.api.toggle.TestToggle
import java.util.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -115,12 +115,6 @@ class RealEmailProtectionInContextAvailabilityRulesTest {
override fun self(): Toggle = TestToggle(enabled)
}

private open class TestToggle(val enabled: Boolean) : Toggle {
override fun getRawStoredState(): State? = null
override fun setEnabled(state: State) {}
override fun isEnabled() = enabled
}

companion object {
private const val ALLOWED_URL = "https://duckduckgo.com"
private const val DISALLOWED_URL = "https://example.com"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.api.store.AutofillStore
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.Command
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.Command.ExitCredentialMode
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.Command.ExitListMode
Expand Down Expand Up @@ -112,6 +114,18 @@ class AutofillSettingsViewModelTest {
}
}

@Test
fun whenUserEnablesAutofillThenCorrectPixelFired() {
testee.onEnableAutofill()
verify(pixel).fire(AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED)
}

@Test
fun whenUserDisablesAutofillThenCorrectPixelFired() {
testee.onDisableAutofill()
verify(pixel).fire(AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED)
}

@Test
fun whenUserCopiesPasswordThenCommandIssuedToShowChange() = runTest {
testee.onCopyPassword("hello")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.duckduckgo.autofill.store

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.duckduckgo.autofill.store.feature.AutofillDefaultStateDecider
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class RealAutofillPrefsStoreTest {

private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val defaultStateDecider: AutofillDefaultStateDecider = mock()

private val testee = RealAutofillPrefsStore(
applicationContext = context,
defaultStateDecider = defaultStateDecider,
)

@Test
fun whenAutofillStateNeverSetManuallyThenDefaultStateDeciderUsed() {
testee.isEnabled
verify(defaultStateDecider).defaultState()
}

@Test
fun whenAutofillStateWasManuallySetToEnabledThenDefaultStateDeciderNotUsed() {
testee.isEnabled = true
testee.isEnabled
verify(defaultStateDecider, never()).defaultState()
}

@Test
fun whenAutofillStateWasManuallySetToDisabledThenDefaultStateDeciderNotUsed() {
testee.isEnabled = false
testee.isEnabled
verify(defaultStateDecider, never()).defaultState()
}

@Test
fun whenDeterminedEnabledByDefaultOnceThenNotDecidedAgain() {
// first call will decide default state should be enabled
whenever(defaultStateDecider.defaultState()).thenReturn(true)
assertTrue(testee.isEnabled)
verify(defaultStateDecider).defaultState()

// second call should not invoke decider again
assertTrue(testee.isEnabled)
verifyNoMoreInteractions(defaultStateDecider)
}

@Test
fun whenDeterminedNotEnabledByDefaultOnceThenWillCallToDeciderAgain() {
// first call will decide default state should not be enabled
whenever(defaultStateDecider.defaultState()).thenReturn(false)
assertFalse(testee.isEnabled)

// second call should invoke decider again
assertFalse(testee.isEnabled)
verify(defaultStateDecider, times(2)).defaultState()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.duckduckgo.autofill.store.feature

import com.duckduckgo.autofill.api.InternalTestUserChecker
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.feature.toggles.api.toggle.AutofillTestFeature
import org.junit.Assert.*
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class RealAutofillDefaultStateDeciderTest {

private val userBrowserProperties: UserBrowserProperties = mock()
private val autofillFeature = AutofillTestFeature()
private val internalTestUserChecker: InternalTestUserChecker = mock()
private val testee = RealAutofillDefaultStateDecider(
userBrowserProperties = userBrowserProperties,
autofillFeature = autofillFeature,
internalTestUserChecker = internalTestUserChecker,
)

@Test
fun whenRemoteFeatureDisabledThenNumberOfDaysInstalledIsIrrelevant() {
configureRemoteFeatureEnabled(false)

configureDaysInstalled(0)
assertFalse(testee.defaultState())

configureDaysInstalled(1000)
assertFalse(testee.defaultState())
}

@Test
fun whenNumberOfDaysInstalledIsNotZeroThenFeatureFlagIsIrrelevant() {
configureDaysInstalled(0)

configureRemoteFeatureEnabled(false)
assertFalse(testee.defaultState())

configureRemoteFeatureEnabled(false)
assertFalse(testee.defaultState())
}

@Test
fun whenInternalTesterThenAlwaysEnabledByDefault() {
configureDaysInstalled(100)
configureRemoteFeatureEnabled(false)
configureAsInternalTester()
assertTrue(testee.defaultState())
}

@Test
fun whenInstalledSameDayAndFeatureFlagEnabledThenEnabledByDefault() {
configureDaysInstalled(0)
configureRemoteFeatureEnabled(true)
assertTrue(testee.defaultState())
}

private fun configureAsInternalTester() {
whenever(internalTestUserChecker.isInternalTestUser).thenReturn(true)
}

private fun configureRemoteFeatureEnabled(enabled: Boolean) {
autofillFeature.onByDefault = enabled
}

private fun configureDaysInstalled(daysInstalled: Long) {
whenever(userBrowserProperties.daysSinceInstalled()).thenReturn(daysInstalled)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 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.feature.toggles.api.toggle

import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.feature.toggles.api.Toggle

class AutofillTestFeature : AutofillFeature {
var topLevelFeatureEnabled: Boolean = false
var canInjectCredentials: Boolean = false
var canSaveCredentials: Boolean = false
var canGeneratePassword: Boolean = false
var canAccessCredentialManagement: Boolean = false
var onByDefault: Boolean = false

override fun self(): Toggle = TestToggle(topLevelFeatureEnabled)
override fun canInjectCredentials(): Toggle = TestToggle(canInjectCredentials)
override fun canSaveCredentials(): Toggle = TestToggle(canSaveCredentials)
override fun canGeneratePasswords(): Toggle = TestToggle(canGeneratePassword)
override fun canAccessCredentialManagement(): Toggle = TestToggle(canAccessCredentialManagement)
override fun onByDefault(): Toggle = TestToggle(onByDefault)
}

open class TestToggle(val enabled: Boolean) : Toggle {
override fun getRawStoredState(): Toggle.State? = null
override fun setEnabled(state: Toggle.State) {}
override fun isEnabled(): Boolean = enabled
}
Loading

0 comments on commit 2f94aa0

Please sign in to comment.