for #23069: add blocklist middleware for home
parent
0b26bac220
commit
2cc9ca3773
@ -0,0 +1,18 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.ext
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a [RecentTab.Tab] from a list of [RecentTab]. [RecentTab.SearchGroup]s will not be filtered.
|
||||||
|
*
|
||||||
|
* @param tab [RecentTab] to remove from the list
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun List<RecentTab>.filterOutTab(tab: RecentTab): List<RecentTab> = filterNot {
|
||||||
|
it is RecentTab.Tab && tab is RecentTab.Tab && it.state.id == tab.state.id
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.blocklist
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.support.ktx.kotlin.sha1
|
||||||
|
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for interacting with the a blocklist stored in [settings].
|
||||||
|
* The blocklist is a set of SHA1 hashed URLs, which are stripped
|
||||||
|
* of protocols and common subdomains like "www" or "mobile".
|
||||||
|
*/
|
||||||
|
class BlocklistHandler(private val settings: Settings) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an URL to the blocklist. The URL will be stripped and hashed,
|
||||||
|
* so no pre-formatted is required.
|
||||||
|
*/
|
||||||
|
fun addUrlToBlocklist(url: String) {
|
||||||
|
val updatedBlocklist = settings.homescreenBlocklist + url.stripAndHash()
|
||||||
|
settings.homescreenBlocklist = updatedBlocklist
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a list of recent bookmarks by the blocklist. Requires this class to be contextually
|
||||||
|
* in a scope.
|
||||||
|
*/
|
||||||
|
@JvmName("filterRecentBookmark")
|
||||||
|
fun List<RecentBookmark>.filteredByBlocklist(): List<RecentBookmark> =
|
||||||
|
settings.homescreenBlocklist.let { blocklist ->
|
||||||
|
filterNot {
|
||||||
|
it.url?.let { url -> blocklistContainsUrl(blocklist, url) } ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a list of recent tabs by the blocklist. Requires this class to be contextually
|
||||||
|
* in a scope.
|
||||||
|
*/
|
||||||
|
@JvmName("filterRecentTab")
|
||||||
|
fun List<RecentTab>.filteredByBlocklist(): List<RecentTab> =
|
||||||
|
settings.homescreenBlocklist.let { blocklist ->
|
||||||
|
filterNot {
|
||||||
|
it is RecentTab.Tab && blocklistContainsUrl(blocklist, it.state.content.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter a list of recent history items by the blocklist. Requires this class to be contextually
|
||||||
|
* in a scope.
|
||||||
|
*/
|
||||||
|
@JvmName("filterRecentHistory")
|
||||||
|
fun List<RecentlyVisitedItem>.filteredByBlocklist(): List<RecentlyVisitedItem> =
|
||||||
|
settings.homescreenBlocklist.let { blocklist ->
|
||||||
|
filterNot {
|
||||||
|
it is RecentlyVisitedItem.RecentHistoryHighlight &&
|
||||||
|
blocklistContainsUrl(blocklist, it.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun blocklistContainsUrl(blocklist: Set<String>, url: String): Boolean =
|
||||||
|
blocklist.any { it == url.stripAndHash() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal fun String.stripAndHash(): String =
|
||||||
|
this.stripProtocolAndCommonSubdomains().sha1()
|
||||||
|
|
||||||
|
// Eventually, this should be standardize in A-C and this can then be removed
|
||||||
|
// https://github.com/mozilla-mobile/android-components/issues/11743
|
||||||
|
private fun String.stripProtocolAndCommonSubdomains(): String {
|
||||||
|
val stripped = this.substringAfter("://").dropLastWhile { it == '/' }
|
||||||
|
// This kind of stripping allows us to match "twitter" to "mobile.twitter.com".
|
||||||
|
// Borrowed from DomainMatcher in A-C
|
||||||
|
val domainsToStrip = listOf("www", "mobile", "m")
|
||||||
|
|
||||||
|
domainsToStrip.forEach { domain ->
|
||||||
|
if (stripped.startsWith("$domain.")) {
|
||||||
|
return stripped.substring(domain.length + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stripped
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.blocklist
|
||||||
|
|
||||||
|
import mozilla.components.lib.state.Middleware
|
||||||
|
import mozilla.components.lib.state.MiddlewareContext
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentAction
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentState
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [Middleware] reacts to item removals from the home screen, adding them to a blocklist.
|
||||||
|
* Additionally, it reacts to state changes in order to filter them by the blocklist.
|
||||||
|
*
|
||||||
|
* @param settings Blocklist is stored here as a string set
|
||||||
|
*/
|
||||||
|
class BlocklistMiddleware(
|
||||||
|
private val blocklistHandler: BlocklistHandler
|
||||||
|
) : Middleware<HomeFragmentState, HomeFragmentAction> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will filter "Change" actions using the blocklist and use "Remove" actions to update
|
||||||
|
* the blocklist.
|
||||||
|
*/
|
||||||
|
override fun invoke(
|
||||||
|
context: MiddlewareContext<HomeFragmentState, HomeFragmentAction>,
|
||||||
|
next: (HomeFragmentAction) -> Unit,
|
||||||
|
action: HomeFragmentAction
|
||||||
|
) {
|
||||||
|
next(getUpdatedAction(context.state, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUpdatedAction(
|
||||||
|
state: HomeFragmentState,
|
||||||
|
action: HomeFragmentAction
|
||||||
|
) = with(blocklistHandler) {
|
||||||
|
when (action) {
|
||||||
|
is HomeFragmentAction.Change -> {
|
||||||
|
action.copy(
|
||||||
|
recentBookmarks = action.recentBookmarks.filteredByBlocklist(),
|
||||||
|
recentTabs = action.recentTabs.filteredByBlocklist(),
|
||||||
|
recentHistory = action.recentHistory.filteredByBlocklist()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.RecentTabsChange -> {
|
||||||
|
action.copy(
|
||||||
|
recentTabs = action.recentTabs.filteredByBlocklist()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.RecentBookmarksChange -> {
|
||||||
|
action.copy(
|
||||||
|
recentBookmarks = action.recentBookmarks.filteredByBlocklist()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.RecentHistoryChange -> {
|
||||||
|
action.copy(recentHistory = action.recentHistory.filteredByBlocklist())
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.RemoveRecentTab -> {
|
||||||
|
if (action.recentTab is RecentTab.Tab) {
|
||||||
|
addUrlToBlocklist(action.recentTab.state.content.url)
|
||||||
|
state.toActionFilteringAllState(this)
|
||||||
|
} else {
|
||||||
|
action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.RemoveRecentBookmark -> {
|
||||||
|
action.recentBookmark.url?.let { url ->
|
||||||
|
addUrlToBlocklist(url)
|
||||||
|
state.toActionFilteringAllState(this)
|
||||||
|
} ?: action
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.RemoveRecentHistoryHighlight -> {
|
||||||
|
addUrlToBlocklist(action.highlightUrl)
|
||||||
|
state.toActionFilteringAllState(this)
|
||||||
|
}
|
||||||
|
else -> action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an item is removed from any part of the state, it should also be removed from any other
|
||||||
|
// relevant parts that contain it.
|
||||||
|
// This is a candidate for refactoring once context receivers lands in Kotlin 1.6.20
|
||||||
|
// https://blog.jetbrains.com/kotlin/2022/02/kotlin-1-6-20-m1-released/#prototype-of-context-receivers-for-kotlin-jvm
|
||||||
|
private fun HomeFragmentState.toActionFilteringAllState(blocklistHandler: BlocklistHandler) =
|
||||||
|
with(blocklistHandler) {
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
recentTabs = recentTabs.filteredByBlocklist(),
|
||||||
|
recentBookmarks = recentBookmarks.filteredByBlocklist(),
|
||||||
|
recentHistory = recentHistory.filteredByBlocklist(),
|
||||||
|
topSites = topSites,
|
||||||
|
mode = mode,
|
||||||
|
collections = collections,
|
||||||
|
tip = tip,
|
||||||
|
showCollectionPlaceholder = showCollectionPlaceholder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.recentbookmarks.view
|
||||||
|
|
||||||
|
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menu item in the recent bookmarks dropdown menu.
|
||||||
|
*
|
||||||
|
* @property title The menu item title.
|
||||||
|
* @property onClick Invoked when the user clicks on the menu item.
|
||||||
|
*/
|
||||||
|
data class RecentBookmarksMenuItem(
|
||||||
|
val title: String,
|
||||||
|
val onClick: (RecentBookmark) -> Unit
|
||||||
|
)
|
@ -0,0 +1,18 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.recenttabs.view
|
||||||
|
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menu item in the recent tab dropdown menu.
|
||||||
|
*
|
||||||
|
* @property title The menu item title.
|
||||||
|
* @property onClick Invoked when the user clicks on the menu item.
|
||||||
|
*/
|
||||||
|
class RecentTabMenuItem(
|
||||||
|
val title: String,
|
||||||
|
val onClick: (RecentTab.Tab) -> Unit
|
||||||
|
)
|
@ -0,0 +1,34 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.ext
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import mozilla.components.browser.state.state.TabSessionState
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
|
||||||
|
class RecentTabsTest {
|
||||||
|
@Test
|
||||||
|
fun `Test filtering out tab`() {
|
||||||
|
val filteredId = "id"
|
||||||
|
val mockSessionState: TabSessionState = mockk()
|
||||||
|
every { mockSessionState.id } returns filteredId
|
||||||
|
val tab = RecentTab.Tab(mockSessionState)
|
||||||
|
val searchGroup = RecentTab.SearchGroup(
|
||||||
|
tabId = filteredId,
|
||||||
|
searchTerm = "",
|
||||||
|
url = "",
|
||||||
|
thumbnail = null,
|
||||||
|
count = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
val recentTabs = listOf(tab, searchGroup)
|
||||||
|
val result = recentTabs.filterOutTab(tab)
|
||||||
|
|
||||||
|
assertEquals(listOf(searchGroup), result)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package org.mozilla.fenix.home.blocklist
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import mozilla.components.browser.state.state.ContentState
|
||||||
|
import mozilla.components.browser.state.state.TabSessionState
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
class BlocklistHandlerTest {
|
||||||
|
private val mockSettings: Settings = mockk()
|
||||||
|
|
||||||
|
private lateinit var blocklistHandler: BlocklistHandler
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
blocklistHandler = BlocklistHandler(mockSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN url added to blocklist THEN settings updated with hash`() {
|
||||||
|
val addedUrl = "url"
|
||||||
|
val updateSlot = slot<Set<String>>()
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf()
|
||||||
|
every { mockSettings.homescreenBlocklist = capture(updateSlot) } returns Unit
|
||||||
|
|
||||||
|
blocklistHandler.addUrlToBlocklist(addedUrl)
|
||||||
|
|
||||||
|
assertEquals(setOf(addedUrl.stripAndHash()), updateSlot.captured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN bookmark is not in blocklist THEN will not be filtered`() {
|
||||||
|
val bookmarks = listOf(RecentBookmark(url = "test"))
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf()
|
||||||
|
|
||||||
|
val filtered = with(blocklistHandler) {
|
||||||
|
bookmarks.filteredByBlocklist()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(bookmarks, filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN bookmark is in blocklist THEN will be filtered`() {
|
||||||
|
val blockedUrl = "test"
|
||||||
|
val bookmarks = listOf(RecentBookmark(url = blockedUrl))
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
|
||||||
|
|
||||||
|
val filtered = with(blocklistHandler) {
|
||||||
|
bookmarks.filteredByBlocklist()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(listOf<String>(), filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN recent history is not in blocklist THEN will not be filtered`() {
|
||||||
|
val recentHistory = listOf(RecentlyVisitedItem.RecentHistoryHighlight(url = "test", title = ""))
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf()
|
||||||
|
|
||||||
|
val filtered = with(blocklistHandler) {
|
||||||
|
recentHistory.filteredByBlocklist()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(recentHistory, filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN recent history is in blocklist THEN will be filtered`() {
|
||||||
|
val blockedUrl = "test"
|
||||||
|
val recentHistory = listOf(RecentlyVisitedItem.RecentHistoryHighlight(url = blockedUrl, title = ""))
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
|
||||||
|
|
||||||
|
val filtered = with(blocklistHandler) {
|
||||||
|
recentHistory.filteredByBlocklist()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(listOf<String>(), filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN recent tab is not in blocklist THEN will not be filtered`() {
|
||||||
|
val mockSessionState: TabSessionState = mockk()
|
||||||
|
val mockContent: ContentState = mockk()
|
||||||
|
val tabs = listOf(RecentTab.Tab(mockSessionState))
|
||||||
|
every { mockSessionState.content } returns mockContent
|
||||||
|
every { mockContent.url } returns "test"
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf()
|
||||||
|
|
||||||
|
val filtered = with(blocklistHandler) {
|
||||||
|
tabs.filteredByBlocklist()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(tabs, filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN recent tab is in blocklist THEN will be filtered`() {
|
||||||
|
val blockedUrl = "test"
|
||||||
|
val mockSessionState: TabSessionState = mockk()
|
||||||
|
val mockContent: ContentState = mockk()
|
||||||
|
val tabs = listOf(RecentTab.Tab(mockSessionState))
|
||||||
|
every { mockSessionState.content } returns mockContent
|
||||||
|
every { mockContent.url } returns blockedUrl
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
|
||||||
|
|
||||||
|
val filtered = with(blocklistHandler) {
|
||||||
|
tabs.filteredByBlocklist()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(listOf<String>(), filtered)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,308 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.blocklist
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import mozilla.components.browser.state.state.createTab
|
||||||
|
import mozilla.components.support.test.ext.joinBlocking
|
||||||
|
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentAction
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentState
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentStore
|
||||||
|
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||||
|
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
class BlocklistMiddlewareTest {
|
||||||
|
private val mockSettings: Settings = mockk()
|
||||||
|
private val blocklistHandler = BlocklistHandler(mockSettings)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN empty blocklist WHEN action intercepted THEN unchanged by middleware`() {
|
||||||
|
val updatedBookmark = RecentBookmark(url = "https://www.mozilla.org/")
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf()
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertEquals(updatedBookmark, store.state.recentBookmarks[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-empty blocklist WHEN action intercepted with no matching elements THEN unchanged by middleware`() {
|
||||||
|
val updatedBookmark = RecentBookmark(url = "https://www.mozilla.org/")
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf("https://www.github.org/".stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertEquals(updatedBookmark, store.state.recentBookmarks[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-empty blocklist with specific pages WHEN action intercepted with matching host THEN unchanged by middleware`() {
|
||||||
|
val updatedBookmark = RecentBookmark(url = "https://github.com/")
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf("https://github.com/mozilla-mobile/fenix".stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertEquals(updatedBookmark, store.state.recentBookmarks[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-empty blocklist WHEN action intercepted with matching elements THEN filtered by middleware`() {
|
||||||
|
val updatedBookmark = RecentBookmark(url = "https://www.mozilla.org/")
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf("https://www.mozilla.org/".stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertTrue(store.state.recentBookmarks.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-empty blocklist WHEN action intercepted with matching elements THEN all relevant sections filtered by middleware`() {
|
||||||
|
val blockedUrl = "https://www.mozilla.org/"
|
||||||
|
val updatedBookmarks = listOf(RecentBookmark(url = blockedUrl))
|
||||||
|
val updatedRecentTabs = listOf(RecentTab.Tab(createTab(url = blockedUrl)))
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = updatedRecentTabs,
|
||||||
|
recentBookmarks = updatedBookmarks,
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertTrue(store.state.recentBookmarks.isEmpty())
|
||||||
|
assertTrue(store.state.recentTabs.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN non-empty blocklist WHEN action intercepted with matching elements THEN only matching urls removed`() {
|
||||||
|
val blockedUrl = "https://www.mozilla.org/"
|
||||||
|
val unblockedUrl = "https://www.github.org/"
|
||||||
|
val unblockedBookmark = RecentBookmark(unblockedUrl)
|
||||||
|
val updatedBookmarks = listOf(
|
||||||
|
RecentBookmark(url = blockedUrl), unblockedBookmark
|
||||||
|
)
|
||||||
|
val unblockedRecentTab = RecentTab.Tab(createTab(url = unblockedUrl))
|
||||||
|
val updatedRecentTabs =
|
||||||
|
listOf(RecentTab.Tab(createTab(url = blockedUrl)), unblockedRecentTab)
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf(blockedUrl.stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = updatedRecentTabs,
|
||||||
|
recentBookmarks = updatedBookmarks,
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertEquals(unblockedBookmark, store.state.recentBookmarks[0])
|
||||||
|
assertEquals(unblockedRecentTab, store.state.recentTabs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN remove action intercepted THEN hashed url added to blocklist and Change action dispatched`() {
|
||||||
|
val captureMiddleware = CaptureActionsMiddleware<HomeFragmentState, HomeFragmentAction>()
|
||||||
|
val removedUrl = "https://www.mozilla.org/"
|
||||||
|
val removedBookmark = RecentBookmark(url = removedUrl)
|
||||||
|
|
||||||
|
val updateSlot = slot<Set<String>>()
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf() andThen setOf(removedUrl.stripAndHash())
|
||||||
|
every { mockSettings.homescreenBlocklist = capture(updateSlot) } returns Unit
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(recentBookmarks = listOf(removedBookmark)),
|
||||||
|
middlewares = listOf(middleware, captureMiddleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.RemoveRecentBookmark(removedBookmark)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
val capturedAction = captureMiddleware.findFirstAction(HomeFragmentAction.Change::class)
|
||||||
|
assertEquals(emptyList<RecentBookmark>(), capturedAction.recentBookmarks)
|
||||||
|
assertEquals(setOf(removedUrl.stripAndHash()), updateSlot.captured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN urls are compared to blocklist THEN protocols are stripped`() {
|
||||||
|
val host = "www.mozilla.org/"
|
||||||
|
val updatedBookmark = RecentBookmark(url = "http://$host")
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf("https://$host".stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertTrue(store.state.recentBookmarks.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN urls are compared to blocklist THEN common subdomains are stripped`() {
|
||||||
|
val host = "mozilla.org/"
|
||||||
|
val updatedBookmark = RecentBookmark(url = host)
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf(host.stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertTrue(store.state.recentBookmarks.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN urls are compared to blocklist THEN trailing slashes are stripped`() {
|
||||||
|
val host = "www.mozilla.org"
|
||||||
|
val updatedBookmark = RecentBookmark(url = "http://$host/")
|
||||||
|
|
||||||
|
every { mockSettings.homescreenBlocklist } returns setOf("https://$host".stripAndHash())
|
||||||
|
val middleware = BlocklistMiddleware(blocklistHandler)
|
||||||
|
val store = HomeFragmentStore(
|
||||||
|
HomeFragmentState(),
|
||||||
|
middlewares = listOf(middleware)
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
topSites = store.state.topSites,
|
||||||
|
mode = store.state.mode,
|
||||||
|
collections = store.state.collections,
|
||||||
|
tip = store.state.tip,
|
||||||
|
showCollectionPlaceholder = store.state.showCollectionPlaceholder,
|
||||||
|
recentTabs = store.state.recentTabs,
|
||||||
|
recentBookmarks = listOf(updatedBookmark),
|
||||||
|
recentHistory = store.state.recentHistory
|
||||||
|
)
|
||||||
|
).joinBlocking()
|
||||||
|
|
||||||
|
assertTrue(store.state.recentBookmarks.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue