Skip to content

Commit

Permalink
Refactor tracking parameter removal (#4879)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1200204095367872/1208009677259118/f

### Description
As mentioned in #4871, this
adds some unit tests and cleans up the code in this class to make it a
bit more readable.

### Steps to test this PR
- [x] Visit http://privacy-test-pages.site/
- [x] Go to "Privacy Protections Tests” > “Query parameters"
- [x] Tap on the links
- [x] Verify that the parameters are stripped as expected
  • Loading branch information
joshliebe authored Aug 8, 2024
1 parent 01bf909 commit ffdc501
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,42 +62,66 @@ class RealTrackingParameters @Inject constructor(
if (!featureToggle.isFeatureEnabled(PrivacyFeatureName.TrackingParametersFeatureName.value)) return null
if (isAnException(initiatingUrl, url)) return null

val trackingParameters = trackingParametersRepository.parameters

val parsedUri = Uri.parse(url)

// In some instances, particularly with ads, the query may represent a different URL (without encoding),
// making it difficult to detect accurately.
val query = parsedUri.query
val queryUri = query?.toUri()
val uri = if (queryUri?.isValid() == true) queryUri else parsedUri

try {
val queryParameters = uri.queryParameterNames
return if (queryUri?.isValid() == true) {
cleanQueryUriParameters(url, query, queryUri)
} else {
cleanParsedUriParameters(parsedUri)
}
}

private fun cleanParsedUriParameters(uri: Uri): String? {
return cleanUri(uri) { cleanedUrl ->
cleanedUrl
}
}

if (queryParameters.isEmpty()) {
return null
}
val preservedParameters = getPreservedParameters(queryParameters, trackingParameters)
if (preservedParameters.size == queryParameters.size) {
return null
}
private fun cleanQueryUriParameters(url: String, query: String, queryUri: Uri): String? {
return cleanUri(queryUri) { interimCleanedUrl ->
url.replace(query, interimCleanedUrl)
}
}

private fun cleanUri(uri: Uri, buildCleanedUrl: (String) -> String): String? {
val trackingParameters = trackingParametersRepository.parameters

return try {
val preservedParameters = getPreservedParameters(uri, trackingParameters) ?: return null
val interimCleanedUrl = uri.replaceQueryParameters(preservedParameters).toString()
val cleanedUrl = if (queryUri?.isValid() == true) url.replace(query, interimCleanedUrl) else interimCleanedUrl
val cleanedUrl = buildCleanedUrl(interimCleanedUrl)

lastCleanedUrl = cleanedUrl

return cleanedUrl
cleanedUrl
} catch (exception: UnsupportedOperationException) {
Timber.e("Tracking Parameter Removal: ${exception.message}")
null
}
}

private fun getPreservedParameters(uri: Uri, trackingParameters: List<String>): List<String>? {
val queryParameters = uri.queryParameterNames
if (queryParameters.isEmpty()) {
return null
}
val preservedParameters = filterNonTrackingParameters(queryParameters, trackingParameters)
return if (preservedParameters.size == queryParameters.size) {
null
} else {
preservedParameters
}
}

private fun Uri?.isValid(): Boolean {
return this?.isAbsolute == true && this.isHierarchical
}

private fun getPreservedParameters(
private fun filterNonTrackingParameters(
queryParameters: MutableSet<String>,
trackingParameters: List<String>,
) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,25 @@

package com.duckduckgo.privacy.config.impl.features.trackingparameters

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.app.privacy.db.UserAllowListRepository
import com.duckduckgo.feature.toggles.api.FeatureExceptions.FeatureException
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.privacy.config.api.PrivacyFeatureName
import com.duckduckgo.privacy.config.api.UnprotectedTemporary
import com.duckduckgo.privacy.config.store.features.trackingparameters.TrackingParametersRepository
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class RealTrackingParametersTest {

private lateinit var testee: RealTrackingParameters
Expand All @@ -37,12 +45,89 @@ class RealTrackingParametersTest {

@Before
fun setup() {
givenFeatureEnabled(true)
whenever(mockTrackingParametersRepository.exceptions).thenReturn(emptyList())
whenever(mockTrackingParametersRepository.parameters).thenReturn(listOf(TRACKING_PARAMETER))
whenever(mockUserAllowListRepository.isUrlInUserAllowList(anyString())).thenReturn(false)
whenever(mockUnprotectedTemporary.isAnException(anyString())).thenReturn(false)
testee = RealTrackingParameters(mockTrackingParametersRepository, mockFeatureToggle, mockUnprotectedTemporary, mockUserAllowListRepository)
}

@Test
fun whenIsExceptionCalledAndDomainIsInUserAllowListThenReturnTrue() {
fun whenCleanTrackingParametersAndFeatureIsDisabledThenReturnNull() {
givenFeatureEnabled(false)
assertNull(testee.cleanTrackingParameters(null, URL))
}

@Test
fun whenCleanTrackingParametersAndUrlIsInAllowListThenReturnNull() {
whenever(mockUserAllowListRepository.isUrlInUserAllowList(anyString())).thenReturn(true)
assertTrue(testee.isAnException("foo.com", "test.com"))
assertNull(testee.cleanTrackingParameters(null, URL))
}

@Test
fun whenCleanTrackingParametersAndUrlIsUnprotectedTemporaryExceptionThenReturnNull() {
whenever(mockUnprotectedTemporary.isAnException(anyString())).thenReturn(true)
assertNull(testee.cleanTrackingParameters(null, URL))
}

@Test
fun whenCleanTrackingParametersAndUrlDomainIsExceptionThenReturnNull() {
whenever(mockTrackingParametersRepository.exceptions).thenReturn(listOf(FeatureException(domain = "example.com", reason = "reason")))
assertNull(testee.cleanTrackingParameters(null, URL))
}

@Test
fun whenCleanTrackingParametersAndUrlSubdomainIsExceptionThenReturnNull() {
whenever(mockTrackingParametersRepository.exceptions).thenReturn(listOf(FeatureException(domain = "example.com", reason = "reason")))
assertNull(testee.cleanTrackingParameters(null, "https://sub.example.com?tracking_param=value&other=value"))
}

@Test
fun whenCleanTrackingParametersAndInitiatingUrlDomainIsExceptionThenReturnNull() {
whenever(mockTrackingParametersRepository.exceptions).thenReturn(listOf(FeatureException(domain = "foo.com", reason = "reason")))
assertNull(testee.cleanTrackingParameters("https://foo.com", URL))
}

@Test
fun whenCleanTrackingParametersAndInitiatingUrlSubdomainIsExceptionThenReturnNull() {
whenever(mockTrackingParametersRepository.exceptions).thenReturn(listOf(FeatureException(domain = "foo.com", reason = "reason")))
assertNull(testee.cleanTrackingParameters("https://sub.foo.com", URL))
}

@Test
fun whenCleanTrackingParametersAndUrlHasTrackingParametersThenReturnCleanedUrl() {
val result = testee.cleanTrackingParameters(null, URL)
assertEquals(CLEANED_URL, result)
}

@Test
fun whenCleanTrackingParametersAndUrlDoesNotHaveTrackingParametersThenReturnNull() {
val result = testee.cleanTrackingParameters(null, CLEANED_URL)
assertNull(result)
}

@Test
fun whenCleanTrackingParametersAndQueryIsValidUrlThenReturnCleanedUrl() {
val expectedCleanedUrl = "https://example.com?$CLEANED_URL"
val result = testee.cleanTrackingParameters(null, "https://example.com?$URL")
assertEquals(expectedCleanedUrl, result)
}

@Test
fun whenCleanTrackingParametersThenSetLastCleanedUrl() {
assertNull(testee.lastCleanedUrl)
testee.cleanTrackingParameters(null, URL)
assertEquals(CLEANED_URL, testee.lastCleanedUrl)
}

private fun givenFeatureEnabled(enabled: Boolean) {
whenever(mockFeatureToggle.isFeatureEnabled(eq(PrivacyFeatureName.TrackingParametersFeatureName.value), any())).thenReturn(enabled)
}

companion object {
private const val URL = "https://example.com?tracking_param=value&other=value"
private const val CLEANED_URL = "https://example.com?other=value"
private const val TRACKING_PARAMETER = "tracking_param"
}
}

0 comments on commit ffdc501

Please sign in to comment.