diff --git a/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt b/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt index 021d4881b64a..bc166e120648 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt @@ -25,6 +25,7 @@ import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegra import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection @@ -91,6 +92,7 @@ class RealExemptedUrlsHolder @Inject constructor() : ExemptedUrlsHolder { class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( private val maliciousSiteProtection: MaliciousSiteProtection, private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, + private val settingsDataStore: SettingsDataStore, private val dispatchers: DispatcherProvider, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val exemptedUrlsHolder: ExemptedUrlsHolder, @@ -101,6 +103,8 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( val processedUrls = mutableListOf() private var isFeatureEnabled = false + private val isSettingEnabled: Boolean + get() = settingsDataStore.maliciousSiteProtectionEnabled private var currentCheckId = AtomicInteger(0) init { @@ -130,7 +134,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( documentUri: Uri?, confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit, ): IsMaliciousViewData { - if (!isFeatureEnabled) { + if (!isEnabled()) { return IsMaliciousViewData.Safe } val url = request.url.let { @@ -185,7 +189,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit, ): IsMaliciousViewData { return runBlocking { - if (!isFeatureEnabled) { + if (!isEnabled()) { return@runBlocking IsMaliciousViewData.Safe } val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase() @@ -249,6 +253,10 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( request.url.path?.contains("/iframe/") == true || request.requestHeaders["Accept"]?.contains("text/html") == true + private fun isEnabled(): Boolean { + return isFeatureEnabled && isSettingEnabled + } + override fun onPageLoadStarted() { processedUrls.clear() } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt index 4de78cf371cc..370a28d7cded 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsActivity.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.generalsettings import android.os.Bundle +import android.view.View import android.view.View.OnClickListener import android.widget.CompoundButton import androidx.core.view.isVisible @@ -29,13 +30,17 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ActivityGeneralSettingsBinding import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen +import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.OpenMaliciousLearnMore import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchScreenNoParams import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage import com.duckduckgo.app.global.view.fadeTransitionConfig +import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.spans.DuckDuckGoClickableSpan +import com.duckduckgo.common.ui.view.addClickableSpan import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.navigation.api.GlobalActivityStarter @@ -61,6 +66,10 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { viewModel.onAutocompleteRecentlyVisitedSitesSettingChanged(isChecked) } + private val maliciousSiteProtectionToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> + viewModel.onMaliciousSiteProtectionSettingChanged(isChecked) + } + private val voiceSearchChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> viewModel.onVoiceSearchChanged(isChecked) } @@ -75,6 +84,17 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { setContentView(binding.root) setupToolbar(binding.includeToolbar.toolbar) + binding.maliciousLearnMore.addClickableSpan( + textSequence = getText(R.string.maliciousSiteSettingLearnMore), + spans = listOf( + "learn_more_link" to object : DuckDuckGoClickableSpan() { + override fun onClick(widget: View) { + viewModel.maliciousSiteLearnMoreClicked() + } + }, + ), + ) + configureUiEventHandlers() observeViewModel() } @@ -84,6 +104,7 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { binding.autocompleteRecentlyVisitedSitesToggle.setOnCheckedChangeListener(autocompleteRecentlyVisitedSitesToggleListener) binding.voiceSearchToggle.setOnCheckedChangeListener(voiceSearchChangeListener) binding.showOnAppLaunchButton.setOnClickListener(showOnAppLaunchClickListener) + binding.maliciousToggle.setOnCheckedChangeListener(maliciousSiteProtectionToggleListener) } private fun observeViewModel() { @@ -105,6 +126,11 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { } else { binding.autocompleteRecentlyVisitedSitesToggle.isVisible = false } + binding.maliciousDisabledMessage.isVisible = !it.maliciousSiteProtectionEnabled + binding.maliciousToggle.quietlySetIsChecked( + newCheckedState = it.maliciousSiteProtectionEnabled, + changeListener = maliciousSiteProtectionToggleListener, + ) if (it.showVoiceSearch) { binding.voiceSearchToggle.isVisible = true binding.voiceSearchToggle.quietlySetIsChecked(viewState.voiceSearchEnabled, voiceSearchChangeListener) @@ -135,6 +161,19 @@ class GeneralSettingsActivity : DuckDuckGoActivity() { LaunchShowOnAppLaunchScreen -> { globalActivityStarter.start(this, ShowOnAppLaunchScreenNoParams, fadeTransitionConfig()) } + OpenMaliciousLearnMore -> { + globalActivityStarter.start( + this, + WebViewActivityWithParams( + url = MALICIOUS_SITE_LEARN_MORE_URL, + screenTitle = getString(R.string.maliciousSiteLearnMoreTitle), + ), + ) + } } } + + companion object { + private const val MALICIOUS_SITE_LEARN_MORE_URL = "https://duckduckgo.com/duckduckgo-help-pages/privacy/phishing-and-malware-protection/" + } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt index 99dfabb924b7..769dc7e32001 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt @@ -69,10 +69,12 @@ class GeneralSettingsViewModel @Inject constructor( val voiceSearchEnabled: Boolean, val isShowOnAppLaunchOptionVisible: Boolean, val showOnAppLaunchSelectedOption: ShowOnAppLaunchOption, + val maliciousSiteProtectionEnabled: Boolean, ) sealed class Command { data object LaunchShowOnAppLaunchScreen : Command() + data object OpenMaliciousLearnMore : Command() } private val _viewState = MutableStateFlow(null) @@ -95,6 +97,7 @@ class GeneralSettingsViewModel @Inject constructor( voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable, isShowOnAppLaunchOptionVisible = showOnAppLaunchFeature.self().isEnabled(), showOnAppLaunchSelectedOption = showOnAppLaunchOptionDataStore.optionFlow.first(), + maliciousSiteProtectionEnabled = settingsDataStore.maliciousSiteProtectionEnabled, ) } @@ -151,6 +154,24 @@ class GeneralSettingsViewModel @Inject constructor( pixel.fire(AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED) } + fun onMaliciousSiteProtectionSettingChanged(enabled: Boolean) { + Timber.i("User changed malicious site setting, is now enabled: $enabled") + viewModelScope.launch(dispatcherProvider.io()) { + settingsDataStore.maliciousSiteProtectionEnabled = enabled + pixel.fire( + AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED, + mapOf(NEW_STATE to enabled.toString()), + ) + _viewState.value = _viewState.value?.copy( + maliciousSiteProtectionEnabled = enabled, + ) + } + } + + fun maliciousSiteLearnMoreClicked() { + sendCommand(Command.OpenMaliciousLearnMore) + } + private fun observeShowOnAppLaunchOption() { showOnAppLaunchOptionDataStore.optionFlow .onEach { showOnAppLaunchOption -> @@ -163,4 +184,8 @@ class GeneralSettingsViewModel @Inject constructor( _commands.send(newCommand) } } + + companion object { + private const val NEW_STATE = "newState" + } } diff --git a/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt b/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt index f01221520ebd..d97f05e1ecc6 100644 --- a/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt @@ -94,6 +94,7 @@ object PixelInterceptorPixelsRequiringDataCleaning : PixelParamRemovalPlugin { SITE_NOT_WORKING_SHOWN.pixelName to PixelParameter.removeAtb(), SITE_NOT_WORKING_WEBSITE_BROKEN.pixelName to PixelParameter.removeAtb(), AppPixelName.APP_VERSION_AT_SEARCH_TIME.pixelName to PixelParameter.removeAll(), + AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED.pixelName to PixelParameter.removeAtb(), ) } } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index 969c3b2d30b0..f909754f8e9e 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -363,6 +363,8 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { DUCK_PLAYER_SETTING_NEVER_OVERLAY_YOUTUBE("duckplayer_setting_never_overlay_youtube"), DUCK_PLAYER_SETTING_ALWAYS_DUCK_PLAYER("duckplayer_setting_always_duck-player"), + MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED("m_malicious-site-protection_feature-toggled"), + ADD_BOOKMARK_CONFIRM_EDITED("m_add_bookmark_confirm_edit"), REFERRAL_INSTALL_UTM_CAMPAIGN("m_android_install"), diff --git a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt index d72ff1604765..635f1e915c54 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt @@ -39,6 +39,7 @@ interface SettingsDataStore { @Deprecated(message = "hideTips variable is deprecated and no longer available in onboarding") var hideTips: Boolean var autoCompleteSuggestionsEnabled: Boolean + var maliciousSiteProtectionEnabled: Boolean var appIcon: AppIcon var selectedFireAnimation: FireAnimation val fireAnimationEnabled: Boolean @@ -117,6 +118,10 @@ class SettingsSharedPreferences @Inject constructor( get() = preferences.getBoolean(KEY_AUTOCOMPLETE_ENABLED, true) set(enabled) = preferences.edit { putBoolean(KEY_AUTOCOMPLETE_ENABLED, enabled) } + override var maliciousSiteProtectionEnabled: Boolean + get() = preferences.getBoolean(KEY_MALICIOUS_SITE_PROTECTION_ENABLED, true) + set(enabled) = preferences.edit { putBoolean(KEY_MALICIOUS_SITE_PROTECTION_ENABLED, enabled) } + override var appLoginDetection: Boolean get() = preferences.getBoolean("KEY_LOGIN_DETECTION_ENABLED", true) set(enabled) = preferences.edit { putBoolean("KEY_LOGIN_DETECTION_ENABLED", enabled) } @@ -252,6 +257,7 @@ class SettingsSharedPreferences @Inject constructor( const val FILENAME = "com.duckduckgo.app.settings_activity.settings" const val KEY_BACKGROUND_JOB_ID = "BACKGROUND_JOB_ID" const val KEY_AUTOCOMPLETE_ENABLED = "AUTOCOMPLETE_ENABLED" + const val KEY_MALICIOUS_SITE_PROTECTION_ENABLED = "MALICIOUS_SITE_PROTECTION_ENABLED" const val KEY_AUTOMATIC_FIREPROOF_SETTING = "KEY_AUTOMATIC_FIREPROOF_SETTING" const val KEY_AUTOMATICALLY_CLEAR_WHAT_OPTION = "AUTOMATICALLY_CLEAR_WHAT_OPTION" const val KEY_AUTOMATICALLY_CLEAR_WHEN_OPTION = "AUTOMATICALLY_CLEAR_WHEN_OPTION" diff --git a/app/src/main/res/layout/activity_general_settings.xml b/app/src/main/res/layout/activity_general_settings.xml index 698f152bc700..93b7160f8f49 100644 --- a/app/src/main/res/layout/activity_general_settings.xml +++ b/app/src/main/res/layout/activity_general_settings.xml @@ -80,6 +80,48 @@ app:primaryText="@string/showOnAppLaunchOptionTitle" tools:secondaryText="Last Opened Tab" /> + + + + + + + + + + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 928d4671bc97..71c9df6f3995 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -44,6 +44,11 @@ Accept Risk and Visit Site]]> Report a site incorrectly flagged as malicious Learn more + Site Safety Warnings + Warn me on sites flagged for phishing or malware + Learn More + Disabling this feature can put your information at risk. + What site are you signing in to? (required) diff --git a/app/src/test/java/com/duckduckgo/app/Fakes.kt b/app/src/test/java/com/duckduckgo/app/Fakes.kt index a882a653e03f..04a49125e7ab 100644 --- a/app/src/test/java/com/duckduckgo/app/Fakes.kt +++ b/app/src/test/java/com/duckduckgo/app/Fakes.kt @@ -41,6 +41,10 @@ class FakeSettingsDataStore : SettingsDataStore { get() = store["autoCompleteSuggestionsEnabled"] as Boolean? ?: true set(value) { store["autoCompleteSuggestionsEnabled"] = value } + override var maliciousSiteProtectionEnabled: Boolean + get() = store["maliciousSiteProtectionEnabled"] as Boolean? ?: true + set(value) { store["maliciousSiteProtectionEnabled"] = value } + @Deprecated("Not used anymore after adding automatic fireproof", replaceWith = ReplaceWith("automaticFireproofSetting")) override var appLoginDetection: Boolean get() = store["appLoginDetection"] as Boolean? ?: true diff --git a/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt b/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt index be2920d1c0fb..0842ac86129b 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt @@ -7,6 +7,7 @@ import com.duckduckgo.app.browser.webview.ExemptedUrlsHolder.ExemptedUrl import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.MaliciousSite import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.Safe import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State @@ -38,6 +39,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { var coroutineRule = CoroutineTestRule() private val maliciousSiteProtection: MaliciousSiteProtection = mock(MaliciousSiteProtection::class.java) + private val mockSettingsDataStore: SettingsDataStore = mock(SettingsDataStore::class.java) private val mockExemptedUrlsHolder = mock(ExemptedUrlsHolder::class.java) private val fakeAndroidBrowserConfigFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java) private val maliciousUri = "http://malicious.com".toUri() @@ -45,6 +47,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { private val testee = RealMaliciousSiteBlockerWebViewIntegration( maliciousSiteProtection, androidBrowserConfigFeature = fakeAndroidBrowserConfigFeature, + mockSettingsDataStore, dispatchers = coroutineRule.testDispatcherProvider, appCoroutineScope = coroutineRule.testScope, isMainProcess = true, @@ -54,6 +57,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { @Before fun setup() { updateFeatureEnabled(true) + whenever(mockSettingsDataStore.maliciousSiteProtectionEnabled).thenReturn(true) whenever(mockExemptedUrlsHolder.exemptedMaliciousUrls).thenReturn(emptySet()) } @@ -65,6 +69,14 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { assertEquals(Safe, result) } + @Test + fun `shouldOverrideUrlLoading returns safe when setting is disabled by user`() = runTest { + whenever(mockSettingsDataStore.maliciousSiteProtectionEnabled).thenReturn(false) + + val result = testee.shouldOverrideUrlLoading(exampleUri, true) {} + assertEquals(Safe, result) + } + @Test fun `shouldInterceptRequest returns safe when feature is disabled`() = runTest { val request = mock(WebResourceRequest::class.java) @@ -75,6 +87,16 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { assertEquals(Safe, result) } + @Test + fun `shouldInterceptRequest returns safe when setting is disabled by user`() = runTest { + val request = mock(WebResourceRequest::class.java) + whenever(request.url).thenReturn(exampleUri) + whenever(mockSettingsDataStore.maliciousSiteProtectionEnabled).thenReturn(false) + + val result = testee.shouldIntercept(request, null) {} + assertEquals(Safe, result) + } + @Test fun `shouldOverrideUrlLoading returns safe when url is already processed`() = runTest { testee.processedUrls.add(exampleUri.toString()) @@ -84,7 +106,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldInterceptRequest returns result when feature is enabled, is malicious, and is mainframe`() = runTest { + fun `shouldInterceptRequest returns result when feature is enabled, setting is enabled, is malicious, and is mainframe`() = runTest { val request = mock(WebResourceRequest::class.java) whenever(request.url).thenReturn(maliciousUri) whenever(request.isForMainFrame).thenReturn(true) @@ -95,7 +117,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldInterceptRequest returns result when feature is enabled, is malicious, and is iframe`() = runTest { + fun `shouldInterceptRequest returns result when feature is enabled, setting is enabled, is malicious, and is iframe`() = runTest { val request = mock(WebResourceRequest::class.java) whenever(request.url).thenReturn(maliciousUri) whenever(request.isForMainFrame).thenReturn(true) @@ -107,7 +129,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldInterceptRequest returns safe when feature is enabled, is malicious, and is not mainframe nor iframe`() = runTest { + fun `shouldInterceptRequest returns safe when feature is enabled, setting is enabled, is malicious, and is not mainframe nor iframe`() = runTest { val request = mock(WebResourceRequest::class.java) whenever(request.url).thenReturn(maliciousUri) whenever(request.isForMainFrame).thenReturn(false) @@ -118,7 +140,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldOverride returns safe when feature is enabled, is malicious, and is not mainframe`() = runTest { + fun `shouldOverride returns safe when feature is enabled, setting is enabled, is malicious, and is not mainframe`() = runTest { whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(ConfirmedResult(Malicious(MALWARE))) val result = testee.shouldOverrideUrlLoading(maliciousUri, false) {} @@ -126,7 +148,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldOverride returns malicious when feature is enabled, is malicious, and is mainframe`() = runTest { + fun `shouldOverride returns malicious when feature is enabled,, setting is enabled, is malicious, and is mainframe`() = runTest { whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(ConfirmedResult(Malicious(MALWARE))) val result = testee.shouldOverrideUrlLoading(maliciousUri, true) {} @@ -134,7 +156,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldOverride returns safe when feature is enabled, is malicious, and not mainframe nor iframe`() = runTest { + fun `shouldOverride returns safe when feature is enabled, setting is enabled, is malicious, and not mainframe nor iframe`() = runTest { whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(ConfirmedResult(Malicious(MALWARE))) val result = testee.shouldOverrideUrlLoading(maliciousUri, false) {} @@ -142,7 +164,7 @@ class RealMaliciousSiteBlockerWebViewIntegrationTest { } @Test - fun `shouldIntercept returns safe when feature is enabled, is malicious, and is mainframe but webView has different host`() = runTest { + fun `shouldIntercept returns safe when feature and setting enabled, is malicious, and is mainframe but webView has different host`() = runTest { whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(ConfirmedResult(Malicious(MALWARE))) val request = mock(WebResourceRequest::class.java) whenever(request.url).thenReturn(maliciousUri) diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt index 0f28c3fefddc..c84011b6a607 100644 --- a/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt @@ -20,13 +20,16 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import com.duckduckgo.app.FakeSettingsDataStore import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen +import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.OpenMaliciousLearnMore import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage import com.duckduckgo.app.generalsettings.showonapplaunch.store.FakeShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle @@ -45,6 +48,9 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.reset import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -293,6 +299,61 @@ internal class GeneralSettingsViewModelTest { } } + @Test + fun whenMaliciousSiteProtectionEnabledThenViewStateEmittedSettingOnAndPixelFired() = runTest { + fakeAppSettingsDataStore.autoCompleteSuggestionsEnabled = true + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) + + val viewState = defaultViewState() + val paramsCaptor = argumentCaptor>() + + initTestee() + + testee.onMaliciousSiteProtectionSettingChanged(true) + testee.viewState.test { + assertEquals(viewState.copy(maliciousSiteProtectionEnabled = true), awaitItem()) + cancelAndConsumeRemainingEvents() + } + + assertTrue(fakeAppSettingsDataStore.maliciousSiteProtectionEnabled) + verify(mockPixel).fire(eq(AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED), paramsCaptor.capture(), any(), eq(Count)) + val params = paramsCaptor.firstValue + assertEquals("true", params["newState"]) + } + + @Test + fun whenMaliciousSiteProtectionDisabledThenViewStateEmittedSettingOffAndPixelFired() = runTest { + fakeAppSettingsDataStore.autoCompleteSuggestionsEnabled = true + fakeShowOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(LastOpenedTab) + + val viewState = defaultViewState() + val paramsCaptor = argumentCaptor>() + + initTestee() + + testee.onMaliciousSiteProtectionSettingChanged(false) + testee.viewState.test { + assertEquals(viewState.copy(maliciousSiteProtectionEnabled = false), awaitItem()) + cancelAndConsumeRemainingEvents() + } + + assertFalse(fakeAppSettingsDataStore.maliciousSiteProtectionEnabled) + verify(mockPixel).fire(eq(AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED), paramsCaptor.capture(), any(), eq(Count)) + val params = paramsCaptor.firstValue + assertEquals("false", params["newState"]) + } + + @Test + fun whenMaliciousSiteLearnMoreClickedThenOpenMaliciousLearnMoreCommandEmitted() = runTest { + initTestee() + + testee.maliciousSiteLearnMoreClicked() + + testee.commands.test { + assertEquals(OpenMaliciousLearnMore, awaitItem()) + } + } + private fun defaultViewState() = GeneralSettingsViewModel.ViewState( autoCompleteSuggestionsEnabled = true, autoCompleteRecentlyVisitedSitesSuggestionsUserEnabled = true, @@ -301,6 +362,7 @@ internal class GeneralSettingsViewModelTest { voiceSearchEnabled = false, isShowOnAppLaunchOptionVisible = fakeShowOnAppLaunchFeatureToggle.self().isEnabled(), showOnAppLaunchSelectedOption = LastOpenedTab, + maliciousSiteProtectionEnabled = true, ) private fun initTestee() {