Skip to content

Commit 4fc5b1f

Browse files
authored
Update privacy toggle to be per site (#811)
* Adds user whitelisting functionality and a whitelist management screen to the application * Updates the privacy toggle to be per site rather than global, adding sites to the whitelist when toggled on * Updates the privacy toggle to also control https upgrade behavior * Adds "Manage Whitelist" and "Report Broken Site" buttons to the Privacy Dashboard * Adds "Add to Whitelist" or "Remove from Whitelist" option, as applicable, to our Browser menu
1 parent 0195640 commit 4fc5b1f

File tree

90 files changed

+2418
-563
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2418
-563
lines changed

app/schemas/com.duckduckgo.app.global.db.AppDatabase/20.json

Lines changed: 700 additions & 0 deletions
Large diffs are not rendered by default.

app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.duckduckgo.app.bookmarks.db.BookmarksDao
2525
import com.nhaarman.mockitokotlin2.mock
2626
import com.nhaarman.mockitokotlin2.verify
2727
import com.nhaarman.mockitokotlin2.whenever
28+
import org.junit.After
2829
import org.junit.Assert.assertNotNull
2930
import org.junit.Assert.assertTrue
3031
import org.junit.Before
@@ -62,6 +63,12 @@ class BookmarksViewModelTest {
6263
whenever(bookmarksDao.bookmarks()).thenReturn(liveData)
6364
}
6465

66+
@After
67+
fun after() {
68+
testee.viewState.removeObserver(viewStateObserver)
69+
testee.command.removeObserver(commandObserver)
70+
}
71+
6572
@Test
6673
fun whenBookmarkDeletedThenDaoUpdated() {
6774
testee.delete(bookmark)
@@ -94,5 +101,4 @@ class BookmarksViewModelTest {
94101
assertNotNull(captor.value)
95102
assertNotNull(captor.value.bookmarks)
96103
}
97-
98104
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (c) 2020 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.brokensite
18+
19+
import com.duckduckgo.app.global.model.Site
20+
import com.duckduckgo.app.global.model.SiteMonitor
21+
import com.duckduckgo.app.surrogates.SurrogateResponse
22+
import com.duckduckgo.app.trackerdetection.model.TrackingEvent
23+
import org.junit.Assert.*
24+
import org.junit.Test
25+
26+
class BrokenSiteDataTest {
27+
28+
@Test
29+
fun whenSiteIsNullThenDataIsEmptyAndUpgradedIsFalse() {
30+
val data = BrokenSiteData.fromSite(null)
31+
assertTrue(data.url.isEmpty())
32+
assertTrue(data.blockedTrackers.isEmpty())
33+
assertTrue(data.surrogates.isEmpty())
34+
assertFalse(data.upgradedToHttps)
35+
}
36+
37+
@Test
38+
fun whenSiteExistsThenDataContainsUrl() {
39+
val site = buildSite(SITE_URL)
40+
val data = BrokenSiteData.fromSite(site)
41+
assertEquals(SITE_URL, data.url)
42+
}
43+
44+
@Test
45+
fun whenSiteUpgradedThenHttpsUpgradedIsTrue() {
46+
val site = buildSite(SITE_URL, httpsUpgraded = true)
47+
val data = BrokenSiteData.fromSite(site)
48+
assertTrue(data.upgradedToHttps)
49+
}
50+
51+
@Test
52+
fun whenSiteNotUpgradedThenHttpsUpgradedIsFalse() {
53+
val site = buildSite(SITE_URL, httpsUpgraded = false)
54+
val data = BrokenSiteData.fromSite(site)
55+
assertFalse(data.upgradedToHttps)
56+
}
57+
58+
@Test
59+
fun whenSiteHasNoTrackersThenBlockedTrackersIsEmpty() {
60+
val site = buildSite(SITE_URL)
61+
val data = BrokenSiteData.fromSite(site)
62+
assertTrue(data.blockedTrackers.isEmpty())
63+
}
64+
65+
@Test
66+
fun whenSiteHasBlockedTrackersThenBlockedTrackersExist() {
67+
val site = buildSite(SITE_URL)
68+
val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false)
69+
val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.anothertracker.com/tracker.js", emptyList(), null, false)
70+
site.trackerDetected(event)
71+
site.trackerDetected(anotherEvent)
72+
assertEquals("www.tracker.com,www.anothertracker.com", BrokenSiteData.fromSite(site).blockedTrackers)
73+
}
74+
75+
@Test
76+
fun whenSiteHasSameHostBlockedTrackersThenOnlyUniqueTrackersIncludedInData() {
77+
val site = buildSite(SITE_URL)
78+
val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false)
79+
val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.tracker.com/tracker2.js", emptyList(), null, false)
80+
site.trackerDetected(event)
81+
site.trackerDetected(anotherEvent)
82+
assertEquals("www.tracker.com", BrokenSiteData.fromSite(site).blockedTrackers)
83+
}
84+
85+
@Test
86+
fun whenSiteHasNoSurrogatesThenSurrogatesIsEmpty() {
87+
val site = buildSite(SITE_URL)
88+
val data = BrokenSiteData.fromSite(site)
89+
assertTrue(data.surrogates.isEmpty())
90+
}
91+
92+
@Test
93+
fun whenSiteHasSurrogatesThenSurrogatesExist() {
94+
val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "")
95+
val anotherSurrogate = SurrogateResponse(true, "anothersurrogate.com/test.js", "", "")
96+
val site = buildSite(SITE_URL)
97+
site.surrogateDetected(surrogate)
98+
site.surrogateDetected(anotherSurrogate)
99+
assertEquals("surrogate.com,anothersurrogate.com", BrokenSiteData.fromSite(site).surrogates)
100+
}
101+
102+
@Test
103+
fun whenSiteHasSameHostSurrogatesThenOnlyUniqueSurrogateIncludedInData() {
104+
val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "")
105+
val anotherSurrogate = SurrogateResponse(true, "surrogate.com/test2.js", "", "")
106+
val site = buildSite(SITE_URL)
107+
site.surrogateDetected(surrogate)
108+
site.surrogateDetected(anotherSurrogate)
109+
assertEquals("surrogate.com", BrokenSiteData.fromSite(site).surrogates)
110+
}
111+
112+
private fun buildSite(url: String, httpsUpgraded: Boolean = false): Site {
113+
return SiteMonitor(url, "", upgradedHttps = httpsUpgraded)
114+
}
115+
116+
companion object {
117+
private const val SITE_URL = "foo.com"
118+
}
119+
}

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 34 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ import com.duckduckgo.app.cta.model.CtaId
5151
import com.duckduckgo.app.cta.model.DismissedCta
5252
import com.duckduckgo.app.cta.ui.*
5353
import com.duckduckgo.app.global.db.AppDatabase
54-
import com.duckduckgo.app.cta.ui.HomeTopPanelCta
5554
import com.duckduckgo.app.global.install.AppInstallStore
5655
import com.duckduckgo.app.global.model.SiteFactory
5756
import com.duckduckgo.app.onboarding.store.OnboardingStore
5857
import com.duckduckgo.app.onboarding.store.UserStageStore
5958
import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao
59+
import com.duckduckgo.app.privacy.db.UserWhitelistDao
6060
import com.duckduckgo.app.privacy.model.PrivacyPractices
6161
import com.duckduckgo.app.privacy.model.TestEntity
62-
import com.duckduckgo.app.privacy.store.PrivacySettingsStore
62+
import com.duckduckgo.app.privacy.model.UserWhitelistedDomain
6363
import com.duckduckgo.app.runBlocking
6464
import com.duckduckgo.app.settings.db.SettingsDataStore
6565
import com.duckduckgo.app.statistics.VariantManager
@@ -172,10 +172,10 @@ class BrowserTabViewModelTest {
172172
private lateinit var mockWidgetCapabilities: WidgetCapabilities
173173

174174
@Mock
175-
private lateinit var mockPrivacySettingsStore: PrivacySettingsStore
175+
private lateinit var mockUserStageStore: UserStageStore
176176

177177
@Mock
178-
private lateinit var mockUserStageStore: UserStageStore
178+
private lateinit var mockUserWhitelistDao: UserWhitelistDao
179179

180180
private lateinit var mockAutoCompleteApi: AutoCompleteApi
181181

@@ -206,10 +206,10 @@ class BrowserTabViewModelTest {
206206
mockSurveyDao,
207207
mockWidgetCapabilities,
208208
mockDismissedCtaDao,
209+
mockUserWhitelistDao,
209210
mockVariantManager,
210211
mockSettingsStore,
211212
mockOnboardingStore,
212-
mockPrivacySettingsStore,
213213
mockUserStageStore,
214214
coroutineRule.testDispatcherProvider
215215
)
@@ -222,14 +222,15 @@ class BrowserTabViewModelTest {
222222
whenever(mockTabsRepository.retrieveSiteData(any())).thenReturn(MutableLiveData())
223223
whenever(mockPrivacyPractices.privacyPracticesFor(any())).thenReturn(PrivacyPractices.UNKNOWN)
224224
whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1))
225-
whenever(mockPrivacySettingsStore.privacyOn).thenReturn(true)
225+
whenever(mockUserWhitelistDao.contains(anyString())).thenReturn(false)
226226

227227
testee = BrowserTabViewModel(
228228
statisticsUpdater = mockStatisticsUpdater,
229229
queryUrlConverter = mockOmnibarConverter,
230230
duckDuckGoUrlDetector = DuckDuckGoUrlDetector(),
231231
siteFactory = siteFactory,
232232
tabRepository = mockTabsRepository,
233+
userWhitelistDao = mockUserWhitelistDao,
233234
networkLeaderboardDao = mockNetworkLeaderboardDao,
234235
autoComplete = mockAutoCompleteApi,
235236
appSettingsPreferencesStore = mockSettingsStore,
@@ -1074,26 +1075,46 @@ class BrowserTabViewModelTest {
10741075
}
10751076

10761077
@Test
1077-
fun whenUserSelectsToShareLinkThenShareLinkCommandSent() {
1078-
loadUrl("foo.com")
1079-
testee.onShareSelected()
1080-
val command = captureCommands().value as Command.ShareLink
1081-
assertEquals("foo.com", command.url)
1078+
fun whenUserTogglesNonWhitelistedSiteThenSiteAddedToWhitelistAndPixelSentAndPageRefreshed() = coroutineRule.runBlocking {
1079+
whenever(mockUserWhitelistDao.contains("www.example.com")).thenReturn(false)
1080+
loadUrl("http://www.example.com/home.html")
1081+
testee.onWhitelistSelected()
1082+
verify(mockUserWhitelistDao).insert(UserWhitelistedDomain("www.example.com"))
1083+
verify(mockPixel).fire(Pixel.PixelName.BROWSER_MENU_WHITELIST_ADD)
1084+
verify(mockCommandObserver).onChanged(Command.Refresh)
1085+
}
1086+
1087+
@Test
1088+
fun whenUserTogglesWhitelsitedSiteThenSiteRemovedFromWhitelistAndPixelSentAndPageRefreshed() = coroutineRule.runBlocking {
1089+
whenever(mockUserWhitelistDao.contains("www.example.com")).thenReturn(true)
1090+
loadUrl("http://www.example.com/home.html")
1091+
testee.onWhitelistSelected()
1092+
verify(mockUserWhitelistDao).delete(UserWhitelistedDomain("www.example.com"))
1093+
verify(mockPixel).fire(Pixel.PixelName.BROWSER_MENU_WHITELIST_REMOVE)
1094+
verify(mockCommandObserver).onChanged(Command.Refresh)
10821095
}
10831096

10841097
@Test
10851098
fun whenOnSiteAndBrokenSiteSelectedThenBrokenSiteFeedbackCommandSentWithUrl() = coroutineRule.runBlocking {
10861099
loadUrl("foo.com", isBrowserShowing = true)
10871100
testee.onBrokenSiteSelected()
10881101
val command = captureCommands().value as Command.BrokenSiteFeedback
1089-
assertEquals("foo.com", command.url)
1102+
assertEquals("foo.com", command.data.url)
10901103
}
10911104

10921105
@Test
10931106
fun whenNoSiteAndBrokenSiteSelectedThenBrokenSiteFeedbackCommandSentWithoutUrl() {
10941107
testee.onBrokenSiteSelected()
10951108
val command = captureCommands().value as Command.BrokenSiteFeedback
1096-
assertEquals("", command.url)
1109+
assertEquals("", command.data.url)
1110+
}
1111+
1112+
@Test
1113+
fun whenUserSelectsToShareLinkThenShareLinkCommandSent() {
1114+
loadUrl("foo.com")
1115+
testee.onShareSelected()
1116+
val command = captureCommands().value as Command.ShareLink
1117+
assertEquals("foo.com", command.url)
10971118
}
10981119

10991120
@Test
@@ -1589,111 +1610,6 @@ class BrowserTabViewModelTest {
15891610
assertCommandIssued<Command.BrokenSiteFeedback>()
15901611
}
15911612

1592-
@Test
1593-
fun whenOnBrokenSiteSelectedAndNoHttpsUpgradedThenReturnHttpsUpgradedFalse() {
1594-
testee.onBrokenSiteSelected()
1595-
1596-
val command = captureCommands().lastValue
1597-
assertTrue(command is Command.BrokenSiteFeedback)
1598-
1599-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1600-
assertFalse(brokenSiteFeedback.httpsUpgraded)
1601-
}
1602-
1603-
@Test
1604-
fun whenOnBrokenSiteSelectedAndNoTrackersThenReturnBlockedTrackersEmptyString() {
1605-
givenOneActiveTabSelected()
1606-
1607-
testee.onBrokenSiteSelected()
1608-
1609-
val command = captureCommands().lastValue
1610-
assertTrue(command is Command.BrokenSiteFeedback)
1611-
1612-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1613-
assertEquals("", brokenSiteFeedback.blockedTrackers)
1614-
}
1615-
1616-
@Test
1617-
fun whenOnBrokenSiteSelectedAndTrackersBlockedThenReturnBlockedTrackers() {
1618-
givenOneActiveTabSelected()
1619-
val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false)
1620-
val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.anothertracker.com/tracker.js", emptyList(), null, false)
1621-
1622-
testee.trackerDetected(event)
1623-
testee.trackerDetected(anotherEvent)
1624-
testee.onBrokenSiteSelected()
1625-
1626-
val command = captureCommands().lastValue
1627-
assertTrue(command is Command.BrokenSiteFeedback)
1628-
1629-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1630-
assertEquals("www.tracker.com,www.anothertracker.com", brokenSiteFeedback.blockedTrackers)
1631-
}
1632-
1633-
@Test
1634-
fun whenOnBrokenSiteSelectedAndSameHostTrackersBlockedThenDoNotReturnDuplicatedBlockedTrackers() {
1635-
givenOneActiveTabSelected()
1636-
val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false)
1637-
val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.tracker.com/tracker2.js", emptyList(), null, false)
1638-
1639-
testee.trackerDetected(event)
1640-
testee.trackerDetected(anotherEvent)
1641-
testee.onBrokenSiteSelected()
1642-
1643-
val command = captureCommands().lastValue
1644-
assertTrue(command is Command.BrokenSiteFeedback)
1645-
1646-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1647-
assertEquals("www.tracker.com", brokenSiteFeedback.blockedTrackers)
1648-
}
1649-
1650-
@Test
1651-
fun whenOnBrokenSiteSelectedAndNoSurrogatesThenReturnSurrogatesEmptyString() {
1652-
givenOneActiveTabSelected()
1653-
1654-
testee.onBrokenSiteSelected()
1655-
1656-
val command = captureCommands().lastValue
1657-
assertTrue(command is Command.BrokenSiteFeedback)
1658-
1659-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1660-
assertEquals("", brokenSiteFeedback.surrogates)
1661-
}
1662-
1663-
@Test
1664-
fun whenOnBrokenSiteSelectedAndSurrogatesThenReturnSurrogates() {
1665-
givenOneActiveTabSelected()
1666-
val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "")
1667-
val anotherSurrogate = SurrogateResponse(true, "anothersurrogate.com/test.js", "", "")
1668-
1669-
testee.surrogateDetected(surrogate)
1670-
testee.surrogateDetected(anotherSurrogate)
1671-
testee.onBrokenSiteSelected()
1672-
1673-
val command = captureCommands().lastValue
1674-
assertTrue(command is Command.BrokenSiteFeedback)
1675-
1676-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1677-
assertEquals("surrogate.com,anothersurrogate.com", brokenSiteFeedback.surrogates)
1678-
}
1679-
1680-
@Test
1681-
fun whenOnBrokenSiteSelectedAndSameHostSurrogatesThenDoNotReturnDuplicatedSurrogates() {
1682-
givenOneActiveTabSelected()
1683-
val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "")
1684-
val anotherSurrogate = SurrogateResponse(true, "surrogate.com/test2.js", "", "")
1685-
1686-
testee.surrogateDetected(surrogate)
1687-
testee.surrogateDetected(anotherSurrogate)
1688-
testee.onBrokenSiteSelected()
1689-
1690-
val command = captureCommands().lastValue
1691-
assertTrue(command is Command.BrokenSiteFeedback)
1692-
1693-
val brokenSiteFeedback = command as Command.BrokenSiteFeedback
1694-
assertEquals("surrogate.com", brokenSiteFeedback.surrogates)
1695-
}
1696-
16971613
private inline fun <reified T : Command> assertCommandIssued(instanceAssertions: T.() -> Unit = {}) {
16981614
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
16991615
val issuedCommand = commandCaptor.allValues.find { it is T }

0 commit comments

Comments
 (0)