Skip to content

Commit

Permalink
[Implementation]: Android - Send a custom test header on search reque…
Browse files Browse the repository at this point in the history
…st (#5266)

Task/Issue URL:
https://app.asana.com/0/1200581511062568/1208723599553913/f

### Description
Added a custom test header.

### Steps to test this PR
See **Test scenarios** section in the description of the Asana task ->
https://app.asana.com/0/1200581511062568/1208723599553913/f

### NO UI changes
  • Loading branch information
anikiki authored Nov 13, 2024
1 parent 7a238c8 commit d13de5b
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import com.duckduckgo.app.autocomplete.api.AutoCompleteApi
import com.duckduckgo.app.autocomplete.api.AutoCompleteScorer
import com.duckduckgo.app.autocomplete.api.AutoCompleteService
import com.duckduckgo.app.autocomplete.impl.AutoCompleteRepository
import com.duckduckgo.app.browser.AndroidFeaturesHeaderPlugin.Companion.TEST_VALUE
import com.duckduckgo.app.browser.AndroidFeaturesHeaderPlugin.Companion.X_DUCKDUCKGO_ANDROID_HEADER
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.DownloadFile
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.OpenInNewTab
Expand Down Expand Up @@ -190,6 +192,7 @@ import com.duckduckgo.common.test.InstantSchedulersRule
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider
import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
Expand All @@ -206,7 +209,6 @@ import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.history.api.HistoryEntry.VisitedPage
Expand All @@ -215,14 +217,9 @@ import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
import com.duckduckgo.privacy.config.api.AmpLinkInfo
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.privacy.config.api.ContentBlocking
import com.duckduckgo.privacy.config.api.GpcException
import com.duckduckgo.privacy.config.api.PrivacyFeatureName
import com.duckduckgo.privacy.config.api.TrackingParameters
import com.duckduckgo.privacy.config.api.UnprotectedTemporary
import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc
import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER
import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER_VALUE
import com.duckduckgo.privacy.config.store.features.gpc.GpcRepository
import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin
import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin
import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels
Expand Down Expand Up @@ -252,7 +249,6 @@ import java.security.cert.X509Certificate
import java.security.interfaces.RSAPublicKey
import java.time.LocalDateTime
import java.util.UUID
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
Expand Down Expand Up @@ -377,12 +373,6 @@ class BrowserTabViewModelTest {

private val mockAppLinksHandler: AppLinksHandler = mock()

private val mockFeatureToggle: FeatureToggle = mock()

private val mockGpcRepository: GpcRepository = mock()

private val mockUnprotectedTemporary: UnprotectedTemporary = mock()

private val mockAmpLinks: AmpLinks = mock()

private val mockTrackingParameters: TrackingParameters = mock()
Expand Down Expand Up @@ -499,6 +489,7 @@ class BrowserTabViewModelTest {
private val protectionTogglePluginPoint = FakePluginPoint(protectionTogglePlugin)
private var fakeAndroidConfigBrowserFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java)
private val mockAutocompleteTabsFeature: AutocompleteTabsFeature = mock()
private val fakeCustomHeadersPlugin = FakeCustomHeadersProvider(emptyMap())

@Before
fun before() = runTest {
Expand Down Expand Up @@ -625,7 +616,6 @@ class BrowserTabViewModelTest {
navigationAwareLoginDetector = mockNavigationAwareLoginDetector,
userEventsStore = mockUserEventsStore,
fileDownloader = mockFileDownloader,
gpc = RealGpc(mockFeatureToggle, mockGpcRepository, mockUnprotectedTemporary, mockUserAllowListRepository),
fireproofDialogsEventHandler = fireproofDialogsEventHandler,
emailManager = mockEmailManager,
appCoroutineScope = TestScope(),
Expand Down Expand Up @@ -664,6 +654,7 @@ class BrowserTabViewModelTest {
highlightsOnboardingExperimentManager = mockHighlightsOnboardingExperimentManager,
privacyProtectionTogglePlugin = protectionTogglePluginPoint,
showOnAppLaunchOptionHandler = mockShowOnAppLaunchHandler,
customHeadersProvider = fakeCustomHeadersPlugin,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -3243,7 +3234,7 @@ class BrowserTabViewModelTest {

@Test
fun whenUserSubmittedQueryIfGpcIsEnabledAndUrlIsValidThenAddHeaderToUrl() {
givenUrlCanUseGpc()
givenCustomHeadersProviderReturnsGpcHeader()
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com")

testee.onUserSubmittedQuery("foo")
Expand All @@ -3254,9 +3245,9 @@ class BrowserTabViewModelTest {
}

@Test
fun whenUserSubmittedQueryIfGpcIsEnabledAndUrlIsNotValidThenDoNotAddHeaderToUrl() {
fun whenUserSubmittedQueryIfGpcReturnsNoHeaderThenDoNotAddHeaderToUrl() {
val url = "foo.com"
givenUrlCannotUseGpc(url)
givenCustomHeadersProviderReturnsNoHeaders()
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn(url)

testee.onUserSubmittedQuery("foo")
Expand All @@ -3267,20 +3258,8 @@ class BrowserTabViewModelTest {
}

@Test
fun whenUserSubmittedQueryIfGpcIsDisabledThenDoNotAddHeaderToUrl() {
givenGpcIsDisabled()
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com")

testee.onUserSubmittedQuery("foo")
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())

val command = commandCaptor.lastValue as Navigate
assertTrue(command.headers.isEmpty())
}

@Test
fun whenOnDesktopSiteModeToggledIfGpcIsEnabledAndUrlIsValidThenAddHeaderToUrl() {
givenUrlCanUseGpc()
fun whenOnDesktopSiteModeToggledIfGpcReturnsHeaderThenAddHeaderToUrl() {
givenCustomHeadersProviderReturnsGpcHeader()
loadUrl("http://m.example.com")
setDesktopBrowsingMode(false)
testee.onChangeBrowserModeClicked()
Expand All @@ -3291,32 +3270,8 @@ class BrowserTabViewModelTest {
}

@Test
fun whenOnDesktopSiteModeToggledIfGpcIsEnabledAndUrlIsNotValidThenDoNotAddHeaderToUrl() {
givenUrlCannotUseGpc("example.com")
loadUrl("http://m.example.com")
setDesktopBrowsingMode(false)
testee.onChangeBrowserModeClicked()
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())

val command = commandCaptor.lastValue as Navigate
assertTrue(command.headers.isEmpty())
}

@Test
fun whenOnDesktopSiteModeToggledIfGpcIsDisabledThenDoNotAddHeaderToUrl() {
givenGpcIsDisabled()
loadUrl("http://m.example.com")
setDesktopBrowsingMode(false)
testee.onChangeBrowserModeClicked()
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())

val command = commandCaptor.lastValue as Navigate
assertTrue(command.headers.isEmpty())
}

@Test
fun whenExternalAppLinkClickedIfGpcIsEnabledAndUrlIsValidThenAddHeaderToUrl() {
givenUrlCanUseGpc()
fun whenExternalAppLinkClickedIfGpcReturnsHeaderThenAddHeaderToUrl() {
givenCustomHeadersProviderReturnsGpcHeader()
val intentType = SpecialUrlDetector.UrlType.NonHttpAppLink("query", mock(), "fallback")

testee.nonHttpAppLinkClicked(intentType)
Expand All @@ -3327,8 +3282,8 @@ class BrowserTabViewModelTest {
}

@Test
fun whenExternalAppLinkClickedIfGpcIsEnabledAndFallbackUrlIsNullThenDoNotAddHeaderToUrl() {
givenUrlCanUseGpc()
fun whenExternalAppLinkClickedIfGpcReturnsNoHeaderThenDoNotAddHeaderToUrl() {
givenCustomHeadersProviderReturnsNoHeaders()
val intentType = SpecialUrlDetector.UrlType.NonHttpAppLink("query", mock(), null)

testee.nonHttpAppLinkClicked(intentType)
Expand All @@ -3339,27 +3294,26 @@ class BrowserTabViewModelTest {
}

@Test
fun whenExternalAppLinkClickedIfGpcIsEnabledAndUrlIsNotValidThenDoNotAddHeaderToUrl() {
val url = "fallback"
givenUrlCannotUseGpc(url)
val intentType = SpecialUrlDetector.UrlType.NonHttpAppLink("query", mock(), url)
fun whenUserSubmittedQueryIfAndroidFeaturesReturnsHeaderThenAddHeaderToUrl() {
givenCustomHeadersProviderReturnsAndroidFeaturesHeader()
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com")

testee.nonHttpAppLinkClicked(intentType)
testee.onUserSubmittedQuery("foo")
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())

val command = commandCaptor.lastValue as Command.HandleNonHttpAppLink
assertTrue(command.headers.isEmpty())
val command = commandCaptor.lastValue as Navigate
assertEquals(TEST_VALUE, command.headers[X_DUCKDUCKGO_ANDROID_HEADER])
}

@Test
fun whenExternalAppLinkClickedIfGpcIsDisabledThenDoNotAddHeaderToUrl() {
givenGpcIsDisabled()
val intentType = SpecialUrlDetector.UrlType.NonHttpAppLink("query", mock(), "fallback")
fun whenUserSubmittedQueryIfAndroidFeaturesReturnsNoHeaderThenDoNotAddHeaderToUrl() {
givenCustomHeadersProviderReturnsNoHeaders()
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com")

testee.nonHttpAppLinkClicked(intentType)
testee.onUserSubmittedQuery("foo")
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())

val command = commandCaptor.lastValue as Command.HandleNonHttpAppLink
val command = commandCaptor.lastValue as Navigate
assertTrue(command.headers.isEmpty())
}

Expand Down Expand Up @@ -5809,22 +5763,16 @@ class BrowserTabViewModelTest {
testee.navigationStateChanged(buildWebNavigation(navigationHistory = history))
}

private fun givenUrlCanUseGpc() {
whenever(mockFeatureToggle.isFeatureEnabled(any(), any())).thenReturn(true)
whenever(mockGpcRepository.isGpcEnabled()).thenReturn(true)
whenever(mockGpcRepository.exceptions).thenReturn(CopyOnWriteArrayList())
private fun givenCustomHeadersProviderReturnsGpcHeader() {
fakeCustomHeadersPlugin.headers = mapOf(GPC_HEADER to GPC_HEADER_VALUE)
}

private fun givenUrlCannotUseGpc(url: String) {
val exceptions = CopyOnWriteArrayList<GpcException>().apply { add(GpcException(url)) }
whenever(mockFeatureToggle.isFeatureEnabled(eq(PrivacyFeatureName.GpcFeatureName.value), any())).thenReturn(true)
whenever(mockGpcRepository.isGpcEnabled()).thenReturn(true)
whenever(mockGpcRepository.exceptions).thenReturn(exceptions)
private fun givenCustomHeadersProviderReturnsNoHeaders() {
fakeCustomHeadersPlugin.headers = emptyMap()
}

private fun givenGpcIsDisabled() {
whenever(mockFeatureToggle.isFeatureEnabled(any(), any())).thenReturn(true)
whenever(mockGpcRepository.isGpcEnabled()).thenReturn(false)
private fun givenCustomHeadersProviderReturnsAndroidFeaturesHeader() {
fakeCustomHeadersPlugin.headers = mapOf(X_DUCKDUCKGO_ANDROID_HEADER to TEST_VALUE)
}

private suspend fun givenFireButtonPulsing() {
Expand Down Expand Up @@ -6058,4 +6006,10 @@ class BrowserTabViewModelTest {
toggleOn++
}
}

class FakeCustomHeadersProvider(var headers: Map<String, String>) : CustomHeadersProvider {
override fun getCustomHeaders(url: String): Map<String, String> {
return headers
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class BrowserWebViewClientTest {
private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock()
private val openInNewTabFlow: MutableSharedFlow<OpenDuckPlayerInNewTab> = MutableSharedFlow()
private val mockUriLoadedManager: UriLoadedManager = mock()
private val mockAndroidFeaturesHeaderPlugin = AndroidFeaturesHeaderPlugin(mockDuckDuckGoUrlDetector, mock())

@UiThreadTest
@Before
Expand Down Expand Up @@ -184,6 +185,7 @@ class BrowserWebViewClientTest {
mockDuckPlayer,
mockDuckDuckGoUrlDetector,
mockUriLoadedManager,
mockAndroidFeaturesHeaderPlugin,
)
testee.webViewClientListener = listener
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 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.app.browser

import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider.CustomHeadersPlugin
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject

@ContributesMultibinding(scope = AppScope::class)
class AndroidFeaturesHeaderPlugin @Inject constructor(
private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector,
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
) : CustomHeadersPlugin {

override fun getHeaders(url: String): Map<String, String> {
if (androidBrowserConfigFeature.self().isEnabled() &&
androidBrowserConfigFeature.featuresRequestHeader().isEnabled() &&
duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)
) {
return mapOf(
X_DUCKDUCKGO_ANDROID_HEADER to TEST_VALUE,
)
}
return emptyMap()
}

companion object {
internal const val X_DUCKDUCKGO_ANDROID_HEADER = "x-duckduckgo-android"
internal const val TEST_VALUE = "test"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.extensions.asLocationPermissionOrigin
import com.duckduckgo.common.utils.isMobileSite
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider
import com.duckduckgo.common.utils.toDesktopUri
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.downloads.api.DownloadCommand
Expand All @@ -277,7 +278,6 @@ import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
import com.duckduckgo.privacy.config.api.AmpLinkInfo
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.privacy.config.api.ContentBlocking
import com.duckduckgo.privacy.config.api.Gpc
import com.duckduckgo.privacy.config.api.TrackingParameters
import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin
import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin
Expand Down Expand Up @@ -387,7 +387,6 @@ class BrowserTabViewModel @Inject constructor(
private val dispatchers: DispatcherProvider,
private val userEventsStore: UserEventsStore,
private val fileDownloader: FileDownloader,
private val gpc: Gpc,
private val fireproofDialogsEventHandler: FireproofDialogsEventHandler,
private val emailManager: EmailManager,
private val accessibilitySettingsDataStore: AccessibilitySettingsDataStore,
Expand Down Expand Up @@ -425,6 +424,7 @@ class BrowserTabViewModel @Inject constructor(
private val highlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager,
private val privacyProtectionTogglePlugin: PluginPoint<PrivacyProtectionTogglePlugin>,
private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler,
private val customHeadersProvider: CustomHeadersProvider,
) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -1061,10 +1061,7 @@ class BrowserTabViewModel @Inject constructor(
}

private fun getUrlHeaders(url: String?): Map<String, String> {
url?.let {
return gpc.getHeaders(url)
}
return emptyMap()
return url?.let { customHeadersProvider.getCustomHeaders(it) } ?: emptyMap()
}

private fun extractVerticalParameter(currentUrl: String?): String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class BrowserWebViewClient @Inject constructor(
private val duckPlayer: DuckPlayer,
private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector,
private val uriLoadedManager: UriLoadedManager,
private val androidFeaturesHeaderPlugin: AndroidFeaturesHeaderPlugin,
) : WebViewClient() {

var webViewClientListener: WebViewClientListener? = null
Expand Down Expand Up @@ -345,6 +346,11 @@ class BrowserWebViewClient @Inject constructor(
webViewClientListener?.openLinkInNewTab(url)
return true
} else {
val headers = androidFeaturesHeaderPlugin.getHeaders(url.toString())
if (headers.isNotEmpty()) {
loadUrl(webView, url.toString(), headers)
return true
}
return false
}
}
Expand Down Expand Up @@ -382,6 +388,14 @@ class BrowserWebViewClient @Inject constructor(
}
}

private fun loadUrl(
webView: WebView,
url: String,
headers: Map<String, String>,
) {
webView.loadUrl(url, headers)
}

@UiThread
override fun onPageStarted(
webView: WebView,
Expand Down
Loading

0 comments on commit d13de5b

Please sign in to comment.