diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index f0d7d5c43fc4..73e72be287f1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -8160,7 +8160,7 @@ class BrowserTabViewModelTest { class FakeCustomHeadersProvider( var headers: Map, ) : CustomHeadersProvider { - override fun getCustomHeaders(url: String): Map = headers + override suspend fun getCustomHeaders(url: String): Map = headers } class FakeContentScopeScriptsSubscriptionEventPlugin( diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index 910afd177556..dbd0d6a6f8c0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -167,6 +167,7 @@ class BrowserWebViewClientTest { mockAndroidBrowserConfigFeature, mockFeaturesHeaderProvider, mock(), + coroutinesTestRule.testDispatcherProvider, ) private val mockDuckChat: DuckChat = mock() diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt index 845f161fef40..c532c93f121e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt @@ -109,9 +109,12 @@ class WebViewRequestInterceptorTest { @UiThreadTest @Before - fun setup() { + fun setup() = runTest { configureUserAgent() configureStack() + whenever(mockGpc.canUrlAddHeaders(anyString(), anyMap())).thenReturn(false) + whenever(mockRequest.requestHeaders).thenReturn(mutableMapOf()) + whenever(mockGpc.getHeaders(anyString())).thenReturn(mapOf()) testee = WebViewRequestInterceptor( trackerDetector = mockTrackerDetector, diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 973a115acf81..1ebbb0810224 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1194,7 +1194,9 @@ class BrowserTabViewModel @Inject constructor( } site?.nextUrl = urlToNavigate - command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + viewModelScope.launch { + command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + } } } @@ -1219,7 +1221,7 @@ class BrowserTabViewModel @Inject constructor( currentAutoCompleteViewState().copy(showSuggestions = false, showFavorites = false, searchResults = AutoCompleteResult("", emptyList())) } - private fun getUrlHeaders(url: String?): Map = url?.let { customHeadersProvider.getCustomHeaders(it) } ?: emptyMap() + private suspend fun getUrlHeaders(url: String?): Map = url?.let { customHeadersProvider.getCustomHeaders(it) } ?: emptyMap() private fun extractVerticalParameter(currentUrl: String?): String? { val url = currentUrl ?: return null @@ -2795,7 +2797,9 @@ class BrowserTabViewModel @Inject constructor( if (desktopSiteRequested && uri.isMobileSite) { val desktopUrl = uri.toDesktopUri().toString() logcat(INFO) { "Original URL $url - attempting $desktopUrl with desktop site UA string" } - command.value = NavigationCommand.Navigate(desktopUrl, getUrlHeaders(desktopUrl)) + viewModelScope.launch { + command.value = NavigationCommand.Navigate(desktopUrl, getUrlHeaders(desktopUrl)) + } } else { command.value = NavigationCommand.Refresh } @@ -3165,7 +3169,9 @@ class BrowserTabViewModel @Inject constructor( fun nonHttpAppLinkClicked(appLink: NonHttpAppLink) { if (nonHttpAppLinkChecker.isPermitted(appLink.intent)) { - command.value = HandleNonHttpAppLink(appLink, getUrlHeaders(appLink.fallbackUrl)) + viewModelScope.launch { + command.value = HandleNonHttpAppLink(appLink, getUrlHeaders(appLink.fallbackUrl)) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt index c26640292678..c1fb8d2bb08b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt @@ -363,13 +363,13 @@ class WebViewRequestInterceptor( return WebResourceResponse(null, null, null) } - private fun getHeaders(request: WebResourceRequest): Map { + private suspend fun getHeaders(request: WebResourceRequest): Map { return request.requestHeaders.apply { putAll(gpc.getHeaders(request.url.toString())) } } - private fun shouldAddGcpHeaders(request: WebResourceRequest): Boolean { + private suspend fun shouldAddGcpHeaders(request: WebResourceRequest): Boolean { val existingHeaders = request.requestHeaders return (request.isForMainFrame && request.method == "GET" && gpc.canUrlAddHeaders(request.url.toString(), existingHeaders)) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeader.kt b/app/src/main/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeader.kt index 2c86dcad0171..f669789d1158 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeader.kt @@ -21,9 +21,11 @@ import com.duckduckgo.app.browser.trafficquality.Result.Allowed import com.duckduckgo.app.browser.trafficquality.Result.NotAllowed import com.duckduckgo.app.browser.trafficquality.remote.AndroidFeaturesHeaderProvider import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider.CustomHeadersPlugin import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding +import kotlinx.coroutines.withContext import javax.inject.Inject @ContributesMultibinding(scope = AppScope::class) @@ -33,9 +35,10 @@ class AndroidFeaturesHeaderPlugin @Inject constructor( private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, private val androidFeaturesHeaderProvider: AndroidFeaturesHeaderProvider, private val appVersionProvider: AppVersionHeaderProvider, + private val dispatchers: DispatcherProvider, ) : CustomHeadersPlugin { - override fun getHeaders(url: String): Map { + override suspend fun getHeaders(url: String): Map { if (isFeatureEnabled() && duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { return when (val result = customHeaderAllowedChecker.isAllowed()) { is Allowed -> { @@ -58,9 +61,11 @@ class AndroidFeaturesHeaderPlugin @Inject constructor( } } - private fun isFeatureEnabled(): Boolean { - return androidBrowserConfigFeature.self().isEnabled() && - androidBrowserConfigFeature.featuresRequestHeader().isEnabled() + private suspend fun isFeatureEnabled(): Boolean { + return withContext(dispatchers.io()) { + androidBrowserConfigFeature.self().isEnabled() && + androidBrowserConfigFeature.featuresRequestHeader().isEnabled() + } } companion object { diff --git a/app/src/main/java/com/duckduckgo/app/browser/trafficquality/CustomHeaderAllowedChecker.kt b/app/src/main/java/com/duckduckgo/app/browser/trafficquality/CustomHeaderAllowedChecker.kt index 9e2d086287a9..dcd14f079d78 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/trafficquality/CustomHeaderAllowedChecker.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/trafficquality/CustomHeaderAllowedChecker.kt @@ -29,7 +29,7 @@ import java.time.temporal.ChronoUnit import javax.inject.Inject interface CustomHeaderAllowedChecker { - fun isAllowed(): Result + suspend fun isAllowed(): Result } sealed class Result { @@ -42,7 +42,7 @@ class RealCustomHeaderGracePeriodChecker @Inject constructor( private val appBuildConfig: AppBuildConfig, private val featuresRequestHeaderStore: FeaturesRequestHeaderStore, ) : CustomHeaderAllowedChecker { - override fun isAllowed(): Result { + override suspend fun isAllowed(): Result { val config = featuresRequestHeaderStore.getConfig() val versionConfig = config.find { it.appVersion == appBuildConfig.versionCode } return if (versionConfig != null) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/trafficquality/remote/FeaturesRequestHeaderSettingStore.kt b/app/src/main/java/com/duckduckgo/app/browser/trafficquality/remote/FeaturesRequestHeaderSettingStore.kt index 071b5ada87c0..5ffe761b2171 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/trafficquality/remote/FeaturesRequestHeaderSettingStore.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/trafficquality/remote/FeaturesRequestHeaderSettingStore.kt @@ -17,14 +17,16 @@ package com.duckduckgo.app.browser.trafficquality.remote import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import kotlinx.coroutines.withContext import javax.inject.Inject interface FeaturesRequestHeaderStore { - fun getConfig(): List + suspend fun getConfig(): List } data class TrafficQualitySettingsJson( @@ -49,19 +51,22 @@ data class TrafficQualityAppVersionFeatures( class FeaturesRequestHeaderSettingStore @Inject constructor( private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, private val moshi: Moshi, + private val dispatcherProvider: DispatcherProvider, ) : FeaturesRequestHeaderStore { private val jsonAdapter: JsonAdapter by lazy { moshi.adapter(TrafficQualitySettingsJson::class.java) } - override fun getConfig(): List { - val config = androidBrowserConfigFeature.featuresRequestHeader().getSettings()?.let { - runCatching { - val configJson = jsonAdapter.fromJson(it) - configJson?.versions - }.getOrDefault(emptyList()) - } ?: emptyList() - return config + override suspend fun getConfig(): List { + return withContext(dispatcherProvider.io()) { + val config = androidBrowserConfigFeature.featuresRequestHeader().getSettings()?.let { + runCatching { + val configJson = jsonAdapter.fromJson(it) + configJson?.versions + }.getOrDefault(emptyList()) + } ?: emptyList() + config + } } } diff --git a/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModel.kt b/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModel.kt index b65338bfe523..893f8b2ec64d 100644 --- a/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModel.kt @@ -20,6 +20,7 @@ import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.R import com.duckduckgo.app.pixels.AppPixelName.* @@ -29,6 +30,7 @@ import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.privacy.config.api.PrivacyFeatureName +import kotlinx.coroutines.launch import javax.inject.Inject @ContributesViewModel(ActivityScope::class) @@ -55,10 +57,13 @@ class GlobalPrivacyControlViewModel @Inject constructor( val command: SingleLiveEvent = SingleLiveEvent() init { - _viewState.value = ViewState( - globalPrivacyControlEnabled = gpc.isEnabled(), - globalPrivacyControlFeatureEnabled = featureToggle.isFeatureEnabled(PrivacyFeatureName.GpcFeatureName.value, true), - ) + viewModelScope.launch { + _viewState.value = ViewState( + globalPrivacyControlEnabled = gpc.isEnabled(), + globalPrivacyControlFeatureEnabled = featureToggle.isFeatureEnabled(PrivacyFeatureName.GpcFeatureName.value, true), + ) + } + pixel.fire(SETTINGS_DO_NOT_SELL_SHOWN) } diff --git a/app/src/test/java/com/duckduckgo/app/brokensite/api/BrokenSiteSubmitterTest.kt b/app/src/test/java/com/duckduckgo/app/brokensite/api/BrokenSiteSubmitterTest.kt index fad805296ffb..d81f869ad755 100644 --- a/app/src/test/java/com/duckduckgo/app/brokensite/api/BrokenSiteSubmitterTest.kt +++ b/app/src/test/java/com/duckduckgo/app/brokensite/api/BrokenSiteSubmitterTest.kt @@ -119,7 +119,7 @@ class BrokenSiteSubmitterTest { private lateinit var testee: BrokenSiteSubmitter @Before - fun before() { + fun before() = runTest { whenever(mockAppBuildConfig.deviceLocale).thenReturn(Locale.ENGLISH) whenever(mockAppBuildConfig.sdkInt).thenReturn(1) whenever(mockAppBuildConfig.manufacturer).thenReturn("manufacturer") diff --git a/app/src/test/java/com/duckduckgo/app/browser/AndroidFeaturesHeaderPluginTest.kt b/app/src/test/java/com/duckduckgo/app/browser/AndroidFeaturesHeaderPluginTest.kt index 6729f947a92c..ab52378a5f03 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/AndroidFeaturesHeaderPluginTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/AndroidFeaturesHeaderPluginTest.kt @@ -10,11 +10,13 @@ import com.duckduckgo.app.browser.trafficquality.Result.NotAllowed import com.duckduckgo.app.browser.trafficquality.configEnabledForCurrentVersion import com.duckduckgo.app.browser.trafficquality.remote.AndroidFeaturesHeaderProvider import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.Toggle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.doReturn @@ -23,6 +25,9 @@ import org.mockito.kotlin.whenever class AndroidFeaturesHeaderPluginTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + private lateinit var testee: AndroidFeaturesHeaderPlugin private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock() @@ -37,13 +42,14 @@ class AndroidFeaturesHeaderPluginTest { private val SAMPLE_APP_VERSION_HEADER = "app_version_header" @Before - fun setup() { + fun setup() = runTest { testee = AndroidFeaturesHeaderPlugin( mockDuckDuckGoUrlDetector, mockCustomHeaderGracePeriodChecker, mockAndroidBrowserConfigFeature, mockAndroidFeaturesHeaderProvider, mockAppVersionHeaderProvider, + coroutineRule.testDispatcherProvider, ) whenever(mockCustomHeaderGracePeriodChecker.isAllowed()).thenReturn(Allowed(configEnabledForCurrentVersion)) @@ -79,7 +85,7 @@ class AndroidFeaturesHeaderPluginTest { } @Test - fun whenGetHeadersCalledWithDuckDuckGoUrlAndFeatureDisabledThenReturnEmptyHeaders() { + fun whenGetHeadersCalledWithDuckDuckGoUrlAndFeatureDisabledThenReturnEmptyHeaders() = runTest { val url = "duckduckgo_search_url" whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoQueryUrl(any())).thenReturn(true) whenever(mockAndroidBrowserConfigFeature.self()).thenReturn(mockEnabledToggle) @@ -91,7 +97,7 @@ class AndroidFeaturesHeaderPluginTest { } @Test - fun whenGetHeadersCalledWithDuckDuckGoUrlAndHeaderNotAllowedThenReturnCorrectHeader() { + fun whenGetHeadersCalledWithDuckDuckGoUrlAndHeaderNotAllowedThenReturnCorrectHeader() = runTest { val url = "duckduckgo_search_url" whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoQueryUrl(any())).thenReturn(true) whenever(mockAndroidBrowserConfigFeature.self()).thenReturn(mockEnabledToggle) @@ -105,7 +111,7 @@ class AndroidFeaturesHeaderPluginTest { } @Test - fun whenGetHeadersCalledWithNonDuckDuckGoUrlAndFeatureEnabledThenReturnEmptyMap() { + fun whenGetHeadersCalledWithNonDuckDuckGoUrlAndFeatureEnabledThenReturnEmptyMap() = runTest { val url = "non_duckduckgo_search_url" whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoQueryUrl(any())).thenReturn(false) whenever(mockAndroidBrowserConfigFeature.self()).thenReturn(mockEnabledToggle) @@ -117,7 +123,7 @@ class AndroidFeaturesHeaderPluginTest { } @Test - fun whenGetHeadersCalledWithNonDuckDuckGoUrlAndFeatureDisabledThenReturnEmptyMap() { + fun whenGetHeadersCalledWithNonDuckDuckGoUrlAndFeatureDisabledThenReturnEmptyMap() = runTest { val url = "non_duckduckgo_search_url" whenever(mockDuckDuckGoUrlDetector.isDuckDuckGoQueryUrl(any())).thenReturn(false) whenever(mockAndroidBrowserConfigFeature.self()).thenReturn(mockEnabledToggle) diff --git a/app/src/test/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeaderProviderTest.kt b/app/src/test/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeaderProviderTest.kt index e47146c5d499..dc71e3fa9003 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeaderProviderTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/trafficquality/AndroidFeaturesHeaderProviderTest.kt @@ -61,7 +61,7 @@ class AndroidFeaturesHeaderProviderTest { } @Test - fun whenGPCFeatureEnabledAndGPCDisabledThenValueProvided() { + fun whenGPCFeatureEnabledAndGPCDisabledThenValueProvided() = runTest { whenever(mockGpc.isEnabled()).thenReturn(false) val config = TrafficQualityAppVersion(currentVersion, 5, 5, featuresEnabled(gpc = true)) @@ -71,7 +71,7 @@ class AndroidFeaturesHeaderProviderTest { } @Test - fun whenGPCFeatureEnabledAndGPCEnabledThenValueProvided() { + fun whenGPCFeatureEnabledAndGPCEnabledThenValueProvided() = runTest { whenever(mockGpc.isEnabled()).thenReturn(true) val config = TrafficQualityAppVersion(currentVersion, 5, 5, featuresEnabled(gpc = true)) diff --git a/app/src/test/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModelTest.kt index 113e711f15a3..e9711dc7011f 100644 --- a/app/src/test/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlViewModelTest.kt @@ -20,9 +20,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Observer import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.privacy.config.api.Gpc +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -30,12 +32,15 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.lastValue import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever class GlobalPrivacyControlViewModelTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @@ -52,7 +57,8 @@ class GlobalPrivacyControlViewModelTest { lateinit var testee: GlobalPrivacyControlViewModel @Before - fun setup() { + fun setup() = runTest { + whenever(mockGpc.isEnabled()).thenReturn(false) testee = GlobalPrivacyControlViewModel(mockPixel, mockFeatureToggle, mockGpc) testee.command.observeForever(mockCommandObserver) testee.viewState.observeForever(mockViewStateObserver) diff --git a/app/src/test/java/com/duckduckgo/app/referencetests/brokensites/BrokenSitesReferenceTest.kt b/app/src/test/java/com/duckduckgo/app/referencetests/brokensites/BrokenSitesReferenceTest.kt index 0a2419116874..30e94d040ec3 100644 --- a/app/src/test/java/com/duckduckgo/app/referencetests/brokensites/BrokenSitesReferenceTest.kt +++ b/app/src/test/java/com/duckduckgo/app/referencetests/brokensites/BrokenSitesReferenceTest.kt @@ -50,6 +50,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before import org.junit.Rule @@ -167,7 +168,7 @@ class BrokenSitesReferenceTest(private val testCase: TestCase) { } @Test - fun whenReferenceTestRunsItReturnsTheExpectedResult() { + fun whenReferenceTestRunsItReturnsTheExpectedResult() = runTest { whenever(mockAppBuildConfig.sdkInt).thenReturn(testCase.os?.toInt() ?: 1) whenever(mockAppBuildConfig.manufacturer).thenReturn(testCase.manufacturer) whenever(mockAppBuildConfig.model).thenReturn(testCase.model) diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt index 27a598877e9f..ff202744cf57 100644 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt +++ b/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/headers/CustomHeadersProvider.kt @@ -29,7 +29,7 @@ interface CustomHeadersProvider { * @param url The url of the request. * @return A [Map] of headers. */ - fun getCustomHeaders(url: String): Map + suspend fun getCustomHeaders(url: String): Map /** * A plugin point for custom headers that should be added to all requests. @@ -43,7 +43,7 @@ interface CustomHeadersProvider { * @param url The url of the request. * @return A [Map] of headers. */ - fun getHeaders(url: String): Map + suspend fun getHeaders(url: String): Map } } @@ -52,7 +52,7 @@ class RealCustomHeadersProvider @Inject constructor( private val customHeadersPluginPoint: PluginPoint, ) : CustomHeadersProvider { - override fun getCustomHeaders(url: String): Map { + override suspend fun getCustomHeaders(url: String): Map { val customHeaders = mutableMapOf() customHeadersPluginPoint.getPlugins().forEach { customHeaders.putAll(it.getHeaders(url)) diff --git a/privacy-config/privacy-config-api/src/main/java/com/duckduckgo/privacy/config/api/Gpc.kt b/privacy-config/privacy-config-api/src/main/java/com/duckduckgo/privacy/config/api/Gpc.kt index 307dfa52a1f3..e7c643267355 100644 --- a/privacy-config/privacy-config-api/src/main/java/com/duckduckgo/privacy/config/api/Gpc.kt +++ b/privacy-config/privacy-config-api/src/main/java/com/duckduckgo/privacy/config/api/Gpc.kt @@ -25,21 +25,21 @@ interface Gpc { * configuration value prevails over the user choice. * @return `true` if the feature is enabled and `false` is is not. */ - fun isEnabled(): Boolean + suspend fun isEnabled(): Boolean /** * This method returns a [Map] with the GPC headers IF the url passed allows for them to be * added. * @return a [Map] with the GPC headers or an empty [Map] if the above conditions are not met */ - fun getHeaders(url: String): Map + suspend fun getHeaders(url: String): Map /** * This method takes a [url] and a map with its [existingHeaders] and it then returns `true` if * the given [url] can add the GPC headers * @return a `true` if the given [url] and [existingHeaders] permit the GPC headers to be added */ - fun canUrlAddHeaders( + suspend fun canUrlAddHeaders( url: String, existingHeaders: Map, ): Boolean diff --git a/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPlugin.kt b/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPlugin.kt index b3ddacf8fa73..6e2dfb530936 100644 --- a/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPlugin.kt +++ b/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPlugin.kt @@ -27,7 +27,7 @@ class GpcHeaderPlugin @Inject constructor( private val gpc: Gpc, ) : CustomHeadersPlugin { - override fun getHeaders(url: String): Map { + override suspend fun getHeaders(url: String): Map { return gpc.getHeaders(url) } } diff --git a/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpc.kt b/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpc.kt index f2856fa952ba..0343c9003378 100644 --- a/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpc.kt +++ b/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpc.kt @@ -19,6 +19,7 @@ package com.duckduckgo.privacy.config.impl.features.gpc import androidx.annotation.VisibleForTesting import com.duckduckgo.app.browser.UriString.Companion.sameOrSubdomain import com.duckduckgo.app.privacy.db.UserAllowListRepository +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.privacy.config.api.Gpc @@ -27,6 +28,7 @@ import com.duckduckgo.privacy.config.api.UnprotectedTemporary import com.duckduckgo.privacy.config.store.features.gpc.GpcRepository import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn +import kotlinx.coroutines.withContext import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -36,13 +38,14 @@ class RealGpc @Inject constructor( private val gpcRepository: GpcRepository, private val unprotectedTemporary: UnprotectedTemporary, private val userAllowListRepository: UserAllowListRepository, + private val dispatcherProvider: DispatcherProvider, ) : Gpc { - override fun isEnabled(): Boolean { - return gpcRepository.isGpcEnabled() + override suspend fun isEnabled(): Boolean { + return withContext(dispatcherProvider.io()) { gpcRepository.isGpcEnabled() } } - override fun getHeaders(url: String): Map { + override suspend fun getHeaders(url: String): Map { return if (canGpcBeUsedByUrl(url)) { mapOf(GPC_HEADER to GPC_HEADER_VALUE) } else { @@ -50,7 +53,7 @@ class RealGpc @Inject constructor( } } - override fun canUrlAddHeaders( + override suspend fun canUrlAddHeaders( url: String, existingHeaders: Map, ): Boolean { @@ -70,8 +73,8 @@ class RealGpc @Inject constructor( } @VisibleForTesting - fun canGpcBeUsedByUrl(url: String): Boolean { - return isFeatureEnabled() && isEnabled() && !isAnException(url) + suspend fun canGpcBeUsedByUrl(url: String): Boolean { + return withContext(dispatcherProvider.io()) { isFeatureEnabled() && isEnabled() && !isAnException(url) } } private fun isFeatureEnabled(): Boolean { diff --git a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPluginTest.kt b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPluginTest.kt index 4f651047d2cb..ddf5a1e9fd0b 100644 --- a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPluginTest.kt +++ b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/GpcHeaderPluginTest.kt @@ -3,6 +3,7 @@ package com.duckduckgo.privacy.config.impl.features.gpc import com.duckduckgo.privacy.config.api.Gpc 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 kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Test import org.mockito.kotlin.mock @@ -15,7 +16,7 @@ class GpcHeaderPluginTest { private val mockGpc: Gpc = mock() @Test - fun whenGetHeadersCalledWithUrlThenGpcGetHeadersIsCalledWithTheSameUrlAndHeadersReturned() { + fun whenGetHeadersCalledWithUrlThenGpcGetHeadersIsCalledWithTheSameUrlAndHeadersReturned() = runTest { val url = "url" val gpcHeaders = mapOf(GPC_HEADER to GPC_HEADER_VALUE) whenever(mockGpc.getHeaders(url)).thenReturn(gpcHeaders) diff --git a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpcTest.kt b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpcTest.kt index 79adab770e15..686c55b5b886 100644 --- a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpcTest.kt +++ b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/features/gpc/RealGpcTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.privacy.config.impl.features.gpc import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.app.privacy.db.UserAllowListRepository +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.privacy.config.api.GpcException import com.duckduckgo.privacy.config.api.GpcHeaderEnabledSite @@ -26,8 +27,10 @@ import com.duckduckgo.privacy.config.api.UnprotectedTemporary 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 kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyString @@ -38,6 +41,9 @@ import java.util.concurrent.CopyOnWriteArrayList @RunWith(AndroidJUnit4::class) class RealGpcTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + private val mockGpcRepository: GpcRepository = mock() private val mockFeatureToggle: FeatureToggle = mock() private val mockUnprotectedTemporary: UnprotectedTemporary = mock() @@ -54,11 +60,11 @@ class RealGpcTest { whenever(mockGpcRepository.headerEnabledSites).thenReturn(headers) testee = - RealGpc(mockFeatureToggle, mockGpcRepository, mockUnprotectedTemporary, mockUserAllowListRepository) + RealGpc(mockFeatureToggle, mockGpcRepository, mockUnprotectedTemporary, mockUserAllowListRepository, coroutineRule.testDispatcherProvider) } @Test - fun whenIsEnabledThenIsGpcEnabledCalled() { + fun whenIsEnabledThenIsGpcEnabledCalled() = runTest { testee.isEnabled() verify(mockGpcRepository).isGpcEnabled() } @@ -76,7 +82,7 @@ class RealGpcTest { } @Test - fun whenGetHeadersIfFeatureAndGpcAreEnabledAndUrlIsInExceptionsThenReturnEmptyMap() { + fun whenGetHeadersIfFeatureAndGpcAreEnabledAndUrlIsInExceptionsThenReturnEmptyMap() = runTest { givenFeatureAndGpcAreEnabled() val result = testee.getHeaders(EXCEPTION_URL) @@ -85,7 +91,7 @@ class RealGpcTest { } @Test - fun whenGetHeadersIfFeatureAndGpcAreEnabledAndUrlIsNotInExceptionsThenReturnMapWithHeaders() { + fun whenGetHeadersIfFeatureAndGpcAreEnabledAndUrlIsNotInExceptionsThenReturnMapWithHeaders() = runTest { givenFeatureAndGpcAreEnabled() val result = testee.getHeaders("test.com") @@ -95,7 +101,7 @@ class RealGpcTest { } @Test - fun whenGetHeadersIfFeatureIsEnabledAndGpcIsNotEnabledAndUrlIsNotInExceptionsThenReturnEmptyMap() { + fun whenGetHeadersIfFeatureIsEnabledAndGpcIsNotEnabledAndUrlIsNotInExceptionsThenReturnEmptyMap() = runTest { givenFeatureIsEnabledButGpcIsNot() val result = testee.getHeaders("test.com") @@ -104,7 +110,7 @@ class RealGpcTest { } @Test - fun whenGetHeadersIfFeatureIsNotEnabledAndGpcIsEnabledAndUrlIsNotInExceptionsThenReturnEmptyMap() { + fun whenGetHeadersIfFeatureIsNotEnabledAndGpcIsEnabledAndUrlIsNotInExceptionsThenReturnEmptyMap() = runTest { givenFeatureIsNotEnabledButGpcIsEnabled() val result = testee.getHeaders("test.com") @@ -113,14 +119,14 @@ class RealGpcTest { } @Test - fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInExceptionsThenReturnFalse() { + fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInExceptionsThenReturnFalse() = runTest { givenFeatureAndGpcAreEnabled() assertFalse(testee.canUrlAddHeaders(EXCEPTION_URL, emptyMap())) } @Test - fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInConsumersListsAndHeaderAlreadyExistsThenReturnFalse() { + fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInConsumersListsAndHeaderAlreadyExistsThenReturnFalse() = runTest { givenFeatureAndGpcAreEnabled() assertFalse( @@ -129,21 +135,21 @@ class RealGpcTest { } @Test - fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInConsumersListAndHeaderDoNotExistsThenReturnTrue() { + fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInConsumersListAndHeaderDoNotExistsThenReturnTrue() = runTest { givenFeatureAndGpcAreEnabled() assertTrue(testee.canUrlAddHeaders(VALID_CONSUMER_URL, emptyMap())) } @Test - fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInNotInConsumersListAndHeaderDoNotExistsThenReturnFalse() { + fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndAndUrlIsInNotInConsumersListAndHeaderDoNotExistsThenReturnFalse() = runTest { givenFeatureAndGpcAreEnabled() assertFalse(testee.canUrlAddHeaders("test.com", emptyMap())) } @Test - fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndUrlIsInConsumersButInTheExceptionListThenReturnFalse() { + fun whenCanUrlAddHeadersIfFeatureAndGpcAreEnabledAndUrlIsInConsumersButInTheExceptionListThenReturnFalse() = runTest { val exceptions = CopyOnWriteArrayList().apply { add(GpcException(VALID_CONSUMER_URL)) } whenever(mockGpcRepository.exceptions).thenReturn(exceptions) @@ -153,49 +159,49 @@ class RealGpcTest { } @Test - fun whenCanUrlAddHeadersIfFeatureIsNotEnabledAndGpcIsEnabledAndAndUrlIsInConsumersListAndHeaderDoNotExistsThenReturnFalse() { + fun whenCanUrlAddHeadersIfFeatureIsNotEnabledAndGpcIsEnabledAndAndUrlIsInConsumersListAndHeaderDoNotExistsThenReturnFalse() = runTest { givenFeatureIsNotEnabledButGpcIsEnabled() assertFalse(testee.canUrlAddHeaders(VALID_CONSUMER_URL, emptyMap())) } @Test - fun whenCanUrlAddHeadersIfFeatureIsEnabledAndGpcIsNotEnabledAndAndUrlIsInConsumersListAndHeaderDoNotExistsThenReturnFalse() { + fun whenCanUrlAddHeadersIfFeatureIsEnabledAndGpcIsNotEnabledAndAndUrlIsInConsumersListAndHeaderDoNotExistsThenReturnFalse() = runTest { givenFeatureIsEnabledButGpcIsNot() assertFalse(testee.canUrlAddHeaders(VALID_CONSUMER_URL, emptyMap())) } @Test - fun whenCanGpcBeUsedByUrlIfFeatureAndGpcAreEnabledAnUrlIsNotAnExceptionThenReturnTrue() { + fun whenCanGpcBeUsedByUrlIfFeatureAndGpcAreEnabledAnUrlIsNotAnExceptionThenReturnTrue() = runTest { givenFeatureAndGpcAreEnabled() assertTrue(testee.canGpcBeUsedByUrl("test.com")) } @Test - fun whenCanGpcBeUsedByUrlIfFeatureAndGpcAreEnabledAnUrlIsAnExceptionThenReturnFalse() { + fun whenCanGpcBeUsedByUrlIfFeatureAndGpcAreEnabledAnUrlIsAnExceptionThenReturnFalse() = runTest { givenFeatureAndGpcAreEnabled() assertFalse(testee.canGpcBeUsedByUrl(EXCEPTION_URL)) } @Test - fun whenCanGpcBeUsedByUrlIfFeatureIsEnabledAndGpcIsNotEnabledAnUrlIsNotAnExceptionThenReturnFalse() { + fun whenCanGpcBeUsedByUrlIfFeatureIsEnabledAndGpcIsNotEnabledAnUrlIsNotAnExceptionThenReturnFalse() = runTest { givenFeatureIsEnabledButGpcIsNot() assertFalse(testee.canGpcBeUsedByUrl("test.com")) } @Test - fun whenCanGpcBeUsedByUrlIfFeatureIsNotEnabledAndGpcIsEnabledAnUrlIsNotAnExceptionThenReturnFalse() { + fun whenCanGpcBeUsedByUrlIfFeatureIsNotEnabledAndGpcIsEnabledAnUrlIsNotAnExceptionThenReturnFalse() = runTest { givenFeatureIsNotEnabledButGpcIsEnabled() assertFalse(testee.canGpcBeUsedByUrl("test.com")) } @Test - fun whenCanGpcBeUsedByUrlIfFeatureAndGpcAreEnabledAnUrlIsInUnprotectedTemporaryThenReturnFalse() { + fun whenCanGpcBeUsedByUrlIfFeatureAndGpcAreEnabledAnUrlIsInUnprotectedTemporaryThenReturnFalse() = runTest { givenFeatureAndGpcAreEnabled() whenever(mockUnprotectedTemporary.isAnException(VALID_CONSUMER_URL)).thenReturn(true) diff --git a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/referencetests/gpc/GpcHeaderReferenceTest.kt b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/referencetests/gpc/GpcHeaderReferenceTest.kt index 8d123e89a28c..694c3e364ff8 100644 --- a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/referencetests/gpc/GpcHeaderReferenceTest.kt +++ b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/referencetests/gpc/GpcHeaderReferenceTest.kt @@ -17,6 +17,7 @@ package com.duckduckgo.privacy.config.impl.referencetests.gpc import com.duckduckgo.app.privacy.db.UserAllowListRepository +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.privacy.config.api.Gpc @@ -33,8 +34,10 @@ import com.duckduckgo.privacy.config.store.features.unprotectedtemporary.Unprote import com.duckduckgo.privacy.config.store.toGpcException import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -45,6 +48,9 @@ import java.util.concurrent.CopyOnWriteArrayList @RunWith(ParameterizedRobolectricTestRunner::class) class GpcHeaderReferenceTest(private val testCase: TestCase) { + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + private val mockUnprotectedTemporaryRepository: UnprotectedTemporaryRepository = mock() private val mockGpcRepository: GpcRepository = mock() private val mockFeatureToggle: FeatureToggle = mock() @@ -72,11 +78,17 @@ class GpcHeaderReferenceTest(private val testCase: TestCase) { fun setup() { whenever(mockGpcRepository.isGpcEnabled()).thenReturn(testCase.gpcUserSettingOn) mockGpcPrivacyConfig() - gpc = RealGpc(mockFeatureToggle, mockGpcRepository, RealUnprotectedTemporary(mockUnprotectedTemporaryRepository), mockUserAllowListRepository) + gpc = RealGpc( + mockFeatureToggle, + mockGpcRepository, + RealUnprotectedTemporary(mockUnprotectedTemporaryRepository), + mockUserAllowListRepository, + coroutinesTestRule.testDispatcherProvider, + ) } @Test - fun whenReferenceTestRunsItReturnsTheExpectedResult() { + fun whenReferenceTestRunsItReturnsTheExpectedResult() = runTest { val gpcHeader = gpc.getHeaders(testCase.requestURL)[RealGpc.GPC_HEADER] val gpcHeaderExists = gpcHeader != null assertEquals(testCase.expectGPCHeader, gpcHeaderExists)