Skip to content

Commit f610a2a

Browse files
authored
Android: Site-level "fireproof" feature to preserve logins (#829)
* Fireproof websites UI (#796) * Create new database table to persist sites where cookies should be preserved * Removed divider from bookmarks list * bookmarks title show in single line * background favicon compatible with dark theme * introduce fireproof site option menu * Fireproof option menu reacts to database state. * Fireproof website screen created * Remove cookies preserving fireproof websites (#808) * Implement logic to remove cookies preserving the ones related to fireproof website. - Try to directly remove cookies from WebView database, preserving the ones with hosts related to a fireproof website - If process fails, fallback to remove all the cookies to avoid any leak - Send pixels in the following scenarios: - database path not found - database can't be opened - delete query fails - database corruption * Fireproof websites empty state and Feature pixels (#810) * Empty state for fireproof websites screen * Feature pixels - User clicks on "fireproof a website" - User undo "fireproof website" action (confirmation snackbar after fireproffing a website) - User removed a website from "fireproof websites"
1 parent 084e859 commit f610a2a

File tree

54 files changed

+2723
-140
lines changed

Some content is hidden

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

54 files changed

+2723
-140
lines changed

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

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

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

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import com.duckduckgo.app.cta.db.DismissedCtaDao
5050
import com.duckduckgo.app.cta.model.CtaId
5151
import com.duckduckgo.app.cta.model.DismissedCta
5252
import com.duckduckgo.app.cta.ui.*
53+
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao
54+
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
5355
import com.duckduckgo.app.global.db.AppDatabase
5456
import com.duckduckgo.app.global.install.AppInstallStore
5557
import com.duckduckgo.app.global.model.SiteFactory
@@ -188,6 +190,8 @@ class BrowserTabViewModelTest {
188190

189191
private lateinit var testee: BrowserTabViewModel
190192

193+
private lateinit var fireproofWebsiteDao: FireproofWebsiteDao
194+
191195
private val selectedTabLiveData = MutableLiveData<TabEntity>()
192196

193197
@Before
@@ -197,6 +201,7 @@ class BrowserTabViewModelTest {
197201
db = Room.inMemoryDatabaseBuilder(getInstrumentation().targetContext, AppDatabase::class.java)
198202
.allowMainThreadQueries()
199203
.build()
204+
fireproofWebsiteDao = db.fireproofWebsiteDao()
200205

201206
mockAutoCompleteApi = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao)
202207

@@ -243,7 +248,8 @@ class BrowserTabViewModelTest {
243248
ctaViewModel = ctaViewModel,
244249
searchCountDao = mockSearchCountDao,
245250
pixel = mockPixel,
246-
dispatchers = coroutineRule.testDispatcherProvider
251+
dispatchers = coroutineRule.testDispatcherProvider,
252+
fireproofWebsiteDao = fireproofWebsiteDao
247253
)
248254

249255
testee.loadData("abc", null, false)
@@ -1085,7 +1091,7 @@ class BrowserTabViewModelTest {
10851091
}
10861092

10871093
@Test
1088-
fun whenUserTogglesWhitelsitedSiteThenSiteRemovedFromWhitelistAndPixelSentAndPageRefreshed() = coroutineRule.runBlocking {
1094+
fun whenUserTogglesWhitelsitedSiteThenSiteRemovedFromWhitelistAndPixelSentAndPageRefreshed() = coroutineRule.runBlocking {
10891095
whenever(mockUserWhitelistDao.contains("www.example.com")).thenReturn(true)
10901096
loadUrl("http://www.example.com/home.html")
10911097
testee.onWhitelistSelected()
@@ -1259,6 +1265,7 @@ class BrowserTabViewModelTest {
12591265
assertFalse(browserViewState().canGoForward)
12601266
assertFalse(browserViewState().canReportSite)
12611267
assertFalse(browserViewState().canChangeBrowsingMode)
1268+
assertFalse(browserViewState().canFireproofSite)
12621269
assertFalse(findInPageViewState().canFindInPage)
12631270
}
12641271

@@ -1610,6 +1617,100 @@ class BrowserTabViewModelTest {
16101617
assertCommandIssued<Command.BrokenSiteFeedback>()
16111618
}
16121619

1620+
@Test
1621+
fun whenHomeShowingByPressingBackThenFireproofWebsiteOptionMenuDisabled() {
1622+
setupNavigation(isBrowsing = true)
1623+
testee.onUserPressedBack()
1624+
assertFalse(browserViewState().canFireproofSite)
1625+
}
1626+
1627+
@Test
1628+
fun whenUserLoadsNotFireproofWebsiteThenFireproofWebsiteOptionMenuEnabled() {
1629+
loadUrl("http://www.example.com/path", isBrowserShowing = true)
1630+
assertTrue(browserViewState().canFireproofSite)
1631+
}
1632+
1633+
@Test
1634+
fun whenUserLoadsFireproofWebsiteThenFireproofWebsiteOptionMenuDisabled() {
1635+
givenFireproofWebsiteDomain("www.example.com")
1636+
loadUrl("http://www.example.com/path", isBrowserShowing = true)
1637+
assertFalse(browserViewState().canFireproofSite)
1638+
}
1639+
1640+
@Test
1641+
fun whenUserLoadsFireproofWebsiteSubDomainThenFireproofWebsiteOptionMenuEnabled() {
1642+
givenFireproofWebsiteDomain("example.com")
1643+
loadUrl("http://mobile.example.com/path", isBrowserShowing = true)
1644+
assertTrue(browserViewState().canFireproofSite)
1645+
}
1646+
1647+
@Test
1648+
fun whenUrlClearedThenFireproofWebsiteOptionMenuDisabled() {
1649+
loadUrl("http://www.example.com/path")
1650+
assertTrue(browserViewState().canFireproofSite)
1651+
loadUrl(null)
1652+
assertFalse(browserViewState().canFireproofSite)
1653+
}
1654+
1655+
@Test
1656+
fun whenUrlIsUpdatedWithNonFireproofWebsiteThenFireproofWebsiteOptionMenuEnabled() {
1657+
givenFireproofWebsiteDomain("www.example.com")
1658+
loadUrl("http://www.example.com/", isBrowserShowing = true)
1659+
updateUrl("http://www.example.com/", "http://twitter.com/explore", true)
1660+
assertTrue(browserViewState().canFireproofSite)
1661+
}
1662+
1663+
@Test
1664+
fun whenUrlIsUpdatedWithFireproofWebsiteThenFireproofWebsiteOptionMenuDisabled() {
1665+
givenFireproofWebsiteDomain("twitter.com")
1666+
loadUrl("http://example.com/", isBrowserShowing = true)
1667+
updateUrl("http://example.com/", "http://twitter.com/explore", true)
1668+
assertFalse(browserViewState().canFireproofSite)
1669+
}
1670+
1671+
@Test
1672+
fun whenUserClicksFireproofWebsiteOptionMenuThenShowConfirmationIsIssued() {
1673+
loadUrl("http://mobile.example.com/", isBrowserShowing = true)
1674+
testee.onFireproofWebsiteClicked()
1675+
assertCommandIssued<Command.ShowFireproofWebSiteConfirmation> {
1676+
assertEquals("mobile.example.com", this.fireproofWebsiteEntity.domain)
1677+
}
1678+
}
1679+
1680+
@Test
1681+
fun whenUserClicksFireproofWebsiteOptionMenuThenFireproofWebsiteOptionMenuDisabled() {
1682+
loadUrl("http://example.com/", isBrowserShowing = true)
1683+
testee.onFireproofWebsiteClicked()
1684+
assertFalse(browserViewState().canFireproofSite)
1685+
}
1686+
1687+
@Test
1688+
fun whenFireproofWebsiteAddedThenPixelSent() {
1689+
loadUrl("http://example.com/", isBrowserShowing = true)
1690+
testee.onFireproofWebsiteClicked()
1691+
verify(mockPixel).fire(Pixel.PixelName.FIREPROOF_WEBSITE_ADDED)
1692+
}
1693+
1694+
@Test
1695+
fun whenUserClicksOnFireproofWebsiteSnackbarUndoActionThenFireproofWebsiteIsRemoved() {
1696+
loadUrl("http://example.com/", isBrowserShowing = true)
1697+
testee.onFireproofWebsiteClicked()
1698+
assertCommandIssued<Command.ShowFireproofWebSiteConfirmation> {
1699+
testee.onFireproofWebsiteSnackbarUndoClicked(this.fireproofWebsiteEntity)
1700+
}
1701+
assertTrue(browserViewState().canFireproofSite)
1702+
}
1703+
1704+
@Test
1705+
fun whenUserClicksOnFireproofWebsiteSnackbarUndoActionThenPixelSent() {
1706+
loadUrl("http://example.com/", isBrowserShowing = true)
1707+
testee.onFireproofWebsiteClicked()
1708+
assertCommandIssued<Command.ShowFireproofWebSiteConfirmation> {
1709+
testee.onFireproofWebsiteSnackbarUndoClicked(this.fireproofWebsiteEntity)
1710+
}
1711+
verify(mockPixel).fire(Pixel.PixelName.FIREPROOF_WEBSITE_UNDO)
1712+
}
1713+
16131714
private inline fun <reified T : Command> assertCommandIssued(instanceAssertions: T.() -> Unit = {}) {
16141715
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
16151716
val issuedCommand = commandCaptor.allValues.find { it is T }
@@ -1644,6 +1745,12 @@ class BrowserTabViewModelTest {
16441745
testee.loadData("TAB_ID", "https://example.com", false)
16451746
}
16461747

1748+
private fun givenFireproofWebsiteDomain(vararg fireproofWebsitesDomain: String) {
1749+
fireproofWebsitesDomain.forEach {
1750+
fireproofWebsiteDao.insert(FireproofWebsiteEntity(domain = it))
1751+
}
1752+
}
1753+
16471754
private fun setBrowserShowing(isBrowsing: Boolean) {
16481755
testee.browserViewState.value = browserViewState().copy(browserShowing = isBrowsing)
16491756
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.fire
18+
19+
import androidx.room.Room
20+
import androidx.test.platform.app.InstrumentationRegistry
21+
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
22+
import com.duckduckgo.app.global.db.AppDatabase
23+
import org.junit.Assert.assertEquals
24+
import org.junit.Assert.assertTrue
25+
import org.junit.Test
26+
27+
class GetCookieHostsToPreserveTest {
28+
29+
private val context = InstrumentationRegistry.getInstrumentation().targetContext
30+
private val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
31+
private val fireproofWebsiteDao = db.fireproofWebsiteDao()
32+
private val getHostsToPreserve = GetCookieHostsToPreserve(fireproofWebsiteDao)
33+
34+
@Test
35+
fun whenSubDomainFireproofWebsiteThenExpectedListReturned() {
36+
givenFireproofWebsitesStored(FireproofWebsiteEntity("mobile.twitter.com"))
37+
val expectedList = listOf(
38+
".mobile.twitter.com",
39+
"mobile.twitter.com",
40+
".twitter.com",
41+
".com"
42+
)
43+
44+
val hostsToPreserve = getHostsToPreserve()
45+
46+
assertTrue(expectedList.all { hostsToPreserve.contains(it) })
47+
}
48+
49+
@Test
50+
fun whenFireproofWebsiteThenExpectedListReturned() {
51+
givenFireproofWebsitesStored(FireproofWebsiteEntity("twitter.com"))
52+
val expectedList = listOf("twitter.com", ".twitter.com", ".com")
53+
54+
val hostsToPreserve = getHostsToPreserve()
55+
56+
assertTrue(expectedList.all { hostsToPreserve.contains(it) })
57+
}
58+
59+
@Test
60+
fun whenMultipleFireproofWebsiteWithSameTopLevelThenExpectedListReturned() {
61+
givenFireproofWebsitesStored(FireproofWebsiteEntity("twitter.com"))
62+
givenFireproofWebsitesStored(FireproofWebsiteEntity("example.com"))
63+
val expectedList = listOf(
64+
".example.com",
65+
"example.com",
66+
"twitter.com",
67+
".twitter.com",
68+
".com"
69+
)
70+
71+
val hostsToPreserve = getHostsToPreserve()
72+
73+
assertEquals(expectedList.size, hostsToPreserve.size)
74+
assertTrue(expectedList.all { hostsToPreserve.contains(it) })
75+
}
76+
77+
private fun givenFireproofWebsitesStored(fireproofWebsiteEntity: FireproofWebsiteEntity) {
78+
fireproofWebsiteDao.insert(fireproofWebsiteEntity)
79+
}
80+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.fire
18+
19+
import com.nhaarman.mockitokotlin2.mock
20+
import com.nhaarman.mockitokotlin2.verify
21+
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
22+
import com.nhaarman.mockitokotlin2.whenever
23+
import kotlinx.coroutines.test.runBlockingTest
24+
import org.junit.Test
25+
26+
class RemoveCookiesTest {
27+
28+
private val selectiveCookieRemover = mock<CookieRemover>()
29+
private val cookieManagerRemover = mock<CookieRemover>()
30+
private val removeCookies = RemoveCookies(cookieManagerRemover, selectiveCookieRemover)
31+
32+
@Test
33+
fun whenSelectiveCookieRemoverSucceedsThenNoMoreInteractions() = runBlockingTest {
34+
selectiveCookieRemover.succeeds()
35+
36+
removeCookies.removeCookies()
37+
38+
verifyZeroInteractions(cookieManagerRemover)
39+
}
40+
41+
@Test
42+
fun whenSelectiveCookieRemoverFailsThenFallbackToCookieManagerRemover() = runBlockingTest {
43+
selectiveCookieRemover.fails()
44+
45+
removeCookies.removeCookies()
46+
47+
verify(cookieManagerRemover).removeCookies()
48+
}
49+
50+
private suspend fun CookieRemover.succeeds() {
51+
whenever(this.removeCookies()).thenReturn(true)
52+
}
53+
54+
private suspend fun CookieRemover.fails() {
55+
whenever(this.removeCookies()).thenReturn(false)
56+
}
57+
}

0 commit comments

Comments
 (0)