summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/feature/accounts-push
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/feature/accounts-push')
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/build.gradle3
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsFeature.kt93
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsUseCases.kt63
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt8
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsFeatureTest.kt136
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsUseCasesTest.kt100
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabReceivedEventsObserverTest.kt (renamed from mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt)6
-rw-r--r--mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabsClosedEventsObserverTest.kt183
8 files changed, 585 insertions, 7 deletions
diff --git a/mobile/android/android-components/components/feature/accounts-push/build.gradle b/mobile/android/android-components/components/feature/accounts-push/build.gradle
index c87fa7582a..17bc5d8313 100644
--- a/mobile/android/android-components/components/feature/accounts-push/build.gradle
+++ b/mobile/android/android-components/components/feature/accounts-push/build.gradle
@@ -37,6 +37,7 @@ tasks.withType(KotlinCompile).configureEach {
}
dependencies {
+ implementation project(':browser-state')
implementation project(':service-firefox-accounts')
implementation project(':support-ktx')
implementation project(':support-base')
@@ -47,7 +48,9 @@ dependencies {
implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.kotlin_coroutines
+ testImplementation project(':concept-engine')
testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
testImplementation ComponentsDependencies.androidx_test_core
testImplementation ComponentsDependencies.androidx_test_junit
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsFeature.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsFeature.kt
new file mode 100644
index 0000000000..8767064867
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsFeature.kt
@@ -0,0 +1,93 @@
+/* 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 mozilla.components.feature.accounts.push
+
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.AccountEventsObserver
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.service.fxa.manager.FxaAccountManager
+
+/**
+ * A feature for closing tabs on this device from other devices
+ * in the [DeviceConstellation].
+ *
+ * This feature receives commands to close tabs using the [FxaAccountManager].
+ *
+ * See [CloseTabsUseCases] for the ability to close tabs that are open on
+ * other devices from this device.
+ *
+ * @param browserStore The [BrowserStore] that holds the currently open tabs.
+ * @param accountManager The account manager.
+ * @param owner The Android lifecycle owner for the observers. Defaults to
+ * the [ProcessLifecycleOwner].
+ * @param autoPause Whether or not the observer should automatically be
+ * paused/resumed with the bound lifecycle.
+ * @param onTabsClosed The callback invoked when one or more tabs are closed.
+ */
+class CloseTabsFeature(
+ private val browserStore: BrowserStore,
+ private val accountManager: FxaAccountManager,
+ private val owner: LifecycleOwner = ProcessLifecycleOwner.get(),
+ private val autoPause: Boolean = false,
+ onTabsClosed: (Device?, List<String>) -> Unit,
+) {
+ @VisibleForTesting internal val observer = TabsClosedEventsObserver { device, urls ->
+ val tabsToRemove = getTabsToRemove(urls)
+ if (tabsToRemove.isNotEmpty()) {
+ browserStore.dispatch(TabListAction.RemoveTabsAction(tabsToRemove.map { it.id }))
+ onTabsClosed(device, tabsToRemove.map { it.content.url })
+ }
+ }
+
+ /**
+ * Begins observing the [accountManager] for "tabs closed" events.
+ */
+ fun observe() {
+ accountManager.registerForAccountEvents(observer, owner, autoPause)
+ }
+
+ private fun getTabsToRemove(remotelyClosedUrls: List<String>): List<TabSessionState> {
+ // The user might have the same URL open in multiple tabs on this device, and might want
+ // to remotely close some or all of those tabs. Synced tabs don't carry enough
+ // information to know which duplicates the user meant to close, so we use a heuristic:
+ // if a URL appears N times in the remotely closed URLs list, we'll close up to
+ // N instances of that URL.
+ val countsByUrl = remotelyClosedUrls.groupingBy { it }.eachCount()
+ return browserStore.state.tabs
+ .groupBy { it.content.url }
+ .asSequence()
+ .mapNotNull { (url, tabs) ->
+ countsByUrl[url]?.let { count -> tabs.take(count) }
+ }
+ .flatten()
+ .toList()
+ }
+}
+
+internal class TabsClosedEventsObserver(
+ internal val onTabsClosed: (Device?, List<String>) -> Unit,
+) : AccountEventsObserver {
+ override fun onEvents(events: List<AccountEvent>) {
+ // Group multiple commands from the same device, so that we can close
+ // more tabs at once.
+ events.asSequence()
+ .filterIsInstance<AccountEvent.DeviceCommandIncoming>()
+ .map { it.command }
+ .filterIsInstance<DeviceCommandIncoming.TabsClosed>()
+ .groupingBy { it.from }
+ .fold(emptyList<String>()) { urls, command -> urls + command.urls }
+ .forEach { (device, urls) ->
+ onTabsClosed(device, urls)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsUseCases.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsUseCases.kt
new file mode 100644
index 0000000000..845ae27a98
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/CloseTabsUseCases.kt
@@ -0,0 +1,63 @@
+/* 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 mozilla.components.feature.accounts.push
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceCommandOutgoing
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.service.fxa.manager.FxaAccountManager
+
+/**
+ * Use cases for closing tabs that are open on other devices in the [DeviceConstellation].
+ *
+ * The use cases send commands to close tabs using the [FxaAccountManager].
+ *
+ * See [CloseTabsFeature] for the ability to close tabs on this device from
+ * other devices.
+ *
+ * @param accountManager The account manager.
+ */
+class CloseTabsUseCases(private val accountManager: FxaAccountManager) {
+ /**
+ * Closes a tab that's currently open on another device.
+ *
+ * @param deviceId The ID of the device on which the tab is currently open.
+ * @param url The URL of the tab to close.
+ * @return Whether the command to close the tab was sent to the device.
+ */
+ @WorkerThread
+ suspend fun close(deviceId: String, url: String): Boolean {
+ filterCloseTabsDevices(accountManager) { constellation, devices ->
+ val device = devices.firstOrNull { it.id == deviceId }
+ device?.let {
+ return constellation.sendCommandToDevice(
+ device.id,
+ DeviceCommandOutgoing.CloseTab(listOf(url)),
+ )
+ }
+ }
+
+ return false
+ }
+}
+
+@VisibleForTesting
+internal inline fun filterCloseTabsDevices(
+ accountManager: FxaAccountManager,
+ block: (DeviceConstellation, Collection<Device>) -> Unit,
+) {
+ val constellation = accountManager.authenticatedAccount()?.deviceConstellation() ?: return
+
+ constellation.state()?.let { state ->
+ state.otherDevices.filter {
+ it.capabilities.contains(DeviceCapability.CLOSE_TABS)
+ }.let { devices ->
+ block(constellation, devices)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt
index 4f049a790b..931dcf59a6 100644
--- a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt
+++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/SendTabFeature.kt
@@ -24,7 +24,7 @@ import mozilla.components.support.base.log.logger.Logger
*
* See [SendTabUseCases] for the ability to send tabs to other devices.
*
- * @param accountManager Firefox account manager.
+ * @param accountManager Account manager.
* @param owner Android lifecycle owner for the observers. Defaults to the [ProcessLifecycleOwner]
* so that we can always observe events throughout the application lifecycle.
* @param autoPause whether or not the observer should automatically be
@@ -38,7 +38,7 @@ class SendTabFeature(
onTabsReceived: (Device?, List<TabData>) -> Unit,
) {
init {
- val observer = EventsObserver(onTabsReceived)
+ val observer = TabReceivedEventsObserver(onTabsReceived)
// Observe the account for all account events, although we'll ignore
// non send-tab command events.
@@ -46,10 +46,10 @@ class SendTabFeature(
}
}
-internal class EventsObserver(
+internal class TabReceivedEventsObserver(
private val onTabsReceived: (Device?, List<TabData>) -> Unit,
) : AccountEventsObserver {
- private val logger = Logger("EventsObserver")
+ private val logger = Logger("TabReceivedEventsObserver")
override fun onEvents(events: List<AccountEvent>) {
events.asSequence()
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsFeatureTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsFeatureTest.kt
new file mode 100644
index 0000000000..7b18681dce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsFeatureTest.kt
@@ -0,0 +1,136 @@
+/* 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 mozilla.components.feature.accounts.push
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class CloseTabsFeatureTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val device123 = Device(
+ id = "123",
+ displayName = "Charcoal",
+ deviceType = DeviceType.DESKTOP,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(DeviceCapability.CLOSE_TABS),
+ subscriptionExpired = true,
+ subscription = null,
+ )
+
+ @Test
+ fun `GIVEN a notification to close multiple URLs WHEN all URLs are open in tabs THEN all tabs are closed and the callback is invoked`() {
+ val urls = listOf(
+ "https://mozilla.org",
+ "https://getfirefox.com",
+ "https://example.org",
+ "https://getthunderbird.com",
+ )
+ val browserStore = BrowserStore(
+ BrowserState(
+ tabs = urls.map { createTab(it) },
+ ),
+ )
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val feature = CloseTabsFeature(
+ browserStore,
+ accountManager = mock(),
+ owner = mock(),
+ onTabsClosed = callback,
+ )
+
+ feature.observer.onTabsClosed(device123, urls)
+
+ browserStore.waitUntilIdle()
+
+ assertTrue(browserStore.state.tabs.isEmpty())
+ verify(callback).invoke(eq(device123), eq(urls))
+ }
+
+ @Test
+ fun `GIVEN a notification to close a URL WHEN the URL is not open in a tab THEN the callback is not invoked`() {
+ val browserStore = BrowserStore()
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val feature = CloseTabsFeature(
+ browserStore,
+ accountManager = mock(),
+ owner = mock(),
+ onTabsClosed = callback,
+ )
+
+ feature.observer.onTabsClosed(device123, listOf("https://mozilla.org"))
+
+ browserStore.waitUntilIdle()
+
+ verify(callback, never()).invoke(any(), any())
+ }
+
+ @Test
+ fun `GIVEN a notification to close duplicate URLs WHEN the duplicate URLs are open in tabs THEN the number of tabs closed matches the number of URLs and the callback is invoked`() {
+ val browserStore = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab("https://mozilla.org", id = "1"),
+ createTab("https://mozilla.org", id = "2"),
+ createTab("https://getfirefox.com", id = "3"),
+ createTab("https://getfirefox.com", id = "4"),
+ createTab("https://getfirefox.com", id = "5"),
+ createTab("https://getthunderbird.com", id = "6"),
+ createTab("https://example.org", id = "7"),
+ ),
+ ),
+ )
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val feature = CloseTabsFeature(
+ browserStore,
+ accountManager = mock(),
+ owner = mock(),
+ onTabsClosed = callback,
+ )
+
+ feature.observer.onTabsClosed(
+ device123,
+ listOf(
+ "https://mozilla.org",
+ "https://getfirefox.com",
+ "https://getfirefox.com",
+ "https://example.org",
+ "https://example.org",
+ ),
+ )
+
+ browserStore.waitUntilIdle()
+
+ assertEquals(listOf("2", "5", "6"), browserStore.state.tabs.map { it.id })
+ verify(callback).invoke(
+ eq(device123),
+ eq(
+ listOf(
+ "https://mozilla.org",
+ "https://getfirefox.com",
+ "https://getfirefox.com",
+ "https://example.org",
+ ),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsUseCasesTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsUseCasesTest.kt
new file mode 100644
index 0000000000..4aae8c84f6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/CloseTabsUseCasesTest.kt
@@ -0,0 +1,100 @@
+/* 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 mozilla.components.feature.accounts.push
+
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class CloseTabsUseCasesTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private val device123 = Device(
+ id = "123",
+ displayName = "Charcoal",
+ deviceType = DeviceType.DESKTOP,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(DeviceCapability.CLOSE_TABS),
+ subscriptionExpired = true,
+ subscription = null,
+ )
+
+ private val device1234 = Device(
+ id = "1234",
+ displayName = "Ruby",
+ deviceType = DeviceType.DESKTOP,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = emptyList(),
+ subscriptionExpired = true,
+ subscription = null,
+ )
+
+ private val manager: FxaAccountManager = mock()
+ private val account: OAuthAccount = mock()
+ private val constellation: DeviceConstellation = mock()
+ private val state: ConstellationState = mock()
+
+ @Before
+ fun setUp() {
+ `when`(manager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+ }
+
+ @Test
+ fun `GIVEN a list of devices WHEN one device supports the close tabs command THEN filtering returns that device`() {
+ val deviceIds = mutableListOf<String>()
+ `when`(state.otherDevices).thenReturn(listOf(device123, device1234))
+ filterCloseTabsDevices(manager) { _, devices ->
+ deviceIds.addAll(devices.map { it.id })
+ }
+
+ assertEquals(listOf("123"), deviceIds)
+ }
+
+ @Test
+ fun `GIVEN a constellation with one capable device WHEN sending a close tabs command to that device THEN the command is sent`() = runTestOnMain {
+ val useCases = CloseTabsUseCases(manager)
+
+ `when`(state.otherDevices).thenReturn(listOf(device123))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(true)
+
+ useCases.close("123", "http://example.com")
+
+ verify(constellation).sendCommandToDevice(any(), any())
+ }
+
+ @Test
+ fun `GIVEN a constellation with one incapable device WHEN sending a close tabs command to that device THEN the command is not sent`() = runTestOnMain {
+ val useCases = CloseTabsUseCases(manager)
+
+ `when`(state.otherDevices).thenReturn(listOf(device1234))
+ `when`(constellation.sendCommandToDevice(any(), any()))
+ .thenReturn(false)
+
+ useCases.close("1234", "http://example.com")
+
+ verify(constellation, never()).sendCommandToDevice(any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabReceivedEventsObserverTest.kt
index 6de8ff42f6..1b8128d4ba 100644
--- a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/EventsObserverTest.kt
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabReceivedEventsObserverTest.kt
@@ -15,11 +15,11 @@ import org.junit.Test
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-class EventsObserverTest {
+class TabReceivedEventsObserverTest {
@Test
fun `events are delivered successfully`() {
val callback: (Device?, List<TabData>) -> Unit = mock()
- val observer = EventsObserver(callback)
+ val observer = TabReceivedEventsObserver(callback)
val events = listOf(AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock())))
observer.onEvents(events)
@@ -34,7 +34,7 @@ class EventsObserverTest {
@Test
fun `only TabReceived commands are delivered`() {
val callback: (Device?, List<TabData>) -> Unit = mock()
- val observer = EventsObserver(callback)
+ val observer = TabReceivedEventsObserver(callback)
val events = listOf(
AccountEvent.ProfileUpdated,
AccountEvent.DeviceCommandIncoming(command = DeviceCommandIncoming.TabReceived(mock(), mock())),
diff --git a/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabsClosedEventsObserverTest.kt b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabsClosedEventsObserverTest.kt
new file mode 100644
index 0000000000..8d51d0b812
--- /dev/null
+++ b/mobile/android/android-components/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/TabsClosedEventsObserverTest.kt
@@ -0,0 +1,183 @@
+/* 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 mozilla.components.feature.accounts.push
+
+import mozilla.components.concept.sync.AccountEvent
+import mozilla.components.concept.sync.Device
+import mozilla.components.concept.sync.DeviceCapability
+import mozilla.components.concept.sync.DeviceCommandIncoming
+import mozilla.components.concept.sync.DeviceType
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class TabsClosedEventsObserverTest {
+ private val device123 = Device(
+ id = "123",
+ displayName = "Charcoal",
+ deviceType = DeviceType.DESKTOP,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(DeviceCapability.CLOSE_TABS),
+ subscriptionExpired = true,
+ subscription = null,
+ )
+
+ private val device1234 = Device(
+ id = "1234",
+ displayName = "Emerald",
+ deviceType = DeviceType.MOBILE,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(DeviceCapability.CLOSE_TABS),
+ subscriptionExpired = true,
+ subscription = null,
+ )
+
+ private val device12345 = Device(
+ id = "12345",
+ displayName = "Sapphire",
+ deviceType = DeviceType.MOBILE,
+ isCurrentDevice = false,
+ lastAccessTime = null,
+ capabilities = listOf(DeviceCapability.CLOSE_TABS),
+ subscriptionExpired = true,
+ subscription = null,
+ )
+
+ @Test
+ fun `GIVEN a tabs closed command WHEN the observer is notified THEN the callback is invoked`() {
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val observer = TabsClosedEventsObserver(callback)
+ val events = listOf(
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ null,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ )
+
+ observer.onEvents(events)
+
+ verify(callback).invoke(eq(null), eq(listOf("https://mozilla.org")))
+ }
+
+ @Test
+ fun `GIVEN a tabs closed command from a device WHEN the observer is notified THEN the callback is invoked`() {
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val observer = TabsClosedEventsObserver(callback)
+ val events = listOf(
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device123,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ )
+
+ observer.onEvents(events)
+
+ verify(callback).invoke(eq(device123), eq(listOf("https://mozilla.org")))
+ }
+
+ @Test
+ fun `GIVEN multiple commands WHEN the observer is notified THEN the callback is only invoked for the tabs closed commands`() {
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val observer = TabsClosedEventsObserver(callback)
+ val events = listOf(
+ AccountEvent.ProfileUpdated,
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device123,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ )
+
+ observer.onEvents(events)
+
+ verify(callback, times(1)).invoke(eq(device123), eq(listOf("https://mozilla.org")))
+ }
+
+ @Test
+ fun `GIVEN multiple tabs closed commands from the same device WHEN the observer is notified THEN the callback is invoked once`() {
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val observer = TabsClosedEventsObserver(callback)
+ val events = listOf(
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device123,
+ listOf("https://mozilla.org", "https://getfirefox.com"),
+ ),
+ ),
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device123,
+ listOf("https://example.org"),
+ ),
+ ),
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device123,
+ listOf("https://getthunderbird.com"),
+ ),
+ ),
+ )
+
+ observer.onEvents(events)
+
+ verify(callback, times(1)).invoke(
+ eq(device123),
+ eq(
+ listOf(
+ "https://mozilla.org",
+ "https://getfirefox.com",
+ "https://example.org",
+ "https://getthunderbird.com",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN multiple tabs closed commands from different devices WHEN the observer is notified THEN the callback is invoked once per device`() {
+ val callback: (Device?, List<String>) -> Unit = mock()
+ val observer = TabsClosedEventsObserver(callback)
+ val events = listOf(
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ null,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device123,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device1234,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ AccountEvent.DeviceCommandIncoming(
+ command = DeviceCommandIncoming.TabsClosed(
+ device12345,
+ listOf("https://mozilla.org"),
+ ),
+ ),
+ )
+
+ observer.onEvents(events)
+
+ verify(callback, times(4)).invoke(any(), eq(listOf("https://mozilla.org")))
+ }
+}