diff options
Diffstat (limited to 'mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt')
-rw-r--r-- | mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt | 1001 |
1 files changed, 1001 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt b/mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt new file mode 100644 index 0000000000..9c30bd8c0e --- /dev/null +++ b/mobile/android/android-components/components/feature/addons/src/test/java/AddonManagerTest.kt @@ -0,0 +1,1001 @@ +/* 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.addons + +import android.graphics.Bitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.WebExtensionAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.WebExtensionState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.webextension.ActionHandler +import mozilla.components.concept.engine.webextension.DisabledFlags +import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.APP_SUPPORT +import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.APP_VERSION +import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.BLOCKLIST +import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.SIGNATURE +import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.USER +import mozilla.components.concept.engine.webextension.EnableSource +import mozilla.components.concept.engine.webextension.InstallationMethod +import mozilla.components.concept.engine.webextension.Metadata +import mozilla.components.concept.engine.webextension.WebExtension +import mozilla.components.feature.addons.ui.translateName +import mozilla.components.feature.addons.update.AddonUpdater.Status +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.rule.runTestOnMain +import mozilla.components.support.test.whenever +import mozilla.components.support.webextensions.WebExtensionSupport +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AddonManagerTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + + @Before + fun setup() { + WebExtensionSupport.installedExtensions.clear() + } + + @After + fun after() { + WebExtensionSupport.installedExtensions.clear() + } + + @Test + fun `getAddons - queries addons from provider and updates installation state`() = runTestOnMain { + // Prepare addons provider + // addon1 (ext1) is a featured extension that is already installed. + // addon2 (ext2) is a featured extension that is not installed. + // addon3 (ext3) is a featured extension that is marked as disabled. + // addon4 (ext4) and addon5 (ext5) are not featured extensions but they are installed. + val addonsProvider: AddonsProvider = mock() + + whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(Addon(id = "ext1"), Addon(id = "ext2"), Addon(id = "ext3"))) + + // Prepare engine + val engine: Engine = mock() + val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() + whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { + callbackCaptor.value.invoke(emptyList()) + } + val store = BrowserStore( + BrowserState( + extensions = mapOf( + "ext1" to WebExtensionState("ext1", "url"), + "ext4" to WebExtensionState("ext4", "url"), + "ext5" to WebExtensionState("ext5", "url"), + // ext6 is a temporarily loaded extension. + "ext6" to WebExtensionState("ext6", "url"), + // ext7 is a built-in extension. + "ext7" to WebExtensionState("ext7", "url"), + ), + ), + ) + + WebExtensionSupport.initialize(engine, store) + val ext1: WebExtension = mock() + whenever(ext1.id).thenReturn("ext1") + whenever(ext1.isEnabled()).thenReturn(true) + WebExtensionSupport.installedExtensions["ext1"] = ext1 + + // Make `ext3` an extension that is disabled because it wasn't supported. + val newlySupportedExtension: WebExtension = mock() + val metadata: Metadata = mock() + whenever(newlySupportedExtension.isEnabled()).thenReturn(false) + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT)) + whenever(metadata.optionsPageUrl).thenReturn("http://options-page.moz") + whenever(metadata.openOptionsPageInTab).thenReturn(true) + whenever(newlySupportedExtension.id).thenReturn("ext3") + whenever(newlySupportedExtension.url).thenReturn("site_url") + whenever(newlySupportedExtension.getMetadata()).thenReturn(metadata) + WebExtensionSupport.installedExtensions["ext3"] = newlySupportedExtension + + val ext4: WebExtension = mock() + whenever(ext4.id).thenReturn("ext4") + whenever(ext4.isEnabled()).thenReturn(true) + val ext4Metadata: Metadata = mock() + whenever(ext4Metadata.temporary).thenReturn(false) + whenever(ext4.getMetadata()).thenReturn(ext4Metadata) + WebExtensionSupport.installedExtensions["ext4"] = ext4 + + val ext5: WebExtension = mock() + whenever(ext5.id).thenReturn("ext5") + whenever(ext5.isEnabled()).thenReturn(true) + val ext5Metadata: Metadata = mock() + whenever(ext5Metadata.temporary).thenReturn(false) + whenever(ext5.getMetadata()).thenReturn(ext5Metadata) + WebExtensionSupport.installedExtensions["ext5"] = ext5 + + val ext6: WebExtension = mock() + whenever(ext6.id).thenReturn("ext6") + whenever(ext6.url).thenReturn("some url") + whenever(ext6.isEnabled()).thenReturn(true) + val ext6Metadata: Metadata = mock() + whenever(ext6Metadata.name).thenReturn("temporarily loaded extension - ext6") + whenever(ext6Metadata.temporary).thenReturn(true) + whenever(ext6.getMetadata()).thenReturn(ext6Metadata) + WebExtensionSupport.installedExtensions["ext6"] = ext6 + + val ext7: WebExtension = mock() + whenever(ext7.id).thenReturn("ext7") + whenever(ext7.isEnabled()).thenReturn(true) + whenever(ext7.isBuiltIn()).thenReturn(true) + WebExtensionSupport.installedExtensions["ext7"] = ext7 + + // Verify add-ons were updated with state provided by the engine/store. + val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons() + assertEquals(6, addons.size) + + // ext1 should be installed. + val addon1 = addons.find { it.id == "ext1" }!! + + assertEquals("ext1", addon1.id) + assertNotNull(addon1.installedState) + assertEquals("ext1", addon1.installedState!!.id) + assertTrue(addon1.isEnabled()) + assertFalse(addon1.isDisabledAsUnsupported()) + assertNull(addon1.installedState!!.optionsPageUrl) + assertFalse(addon1.installedState!!.openOptionsPageInTab) + + // ext2 should not be installed. + val addon2 = addons.find { it.id == "ext2" }!! + assertEquals("ext2", addon2.id) + assertNull(addon2.installedState) + + // ext3 should now be marked as supported but still be disabled as unsupported. + val addon3 = addons.find { it.id == "ext3" }!! + assertEquals("ext3", addon3.id) + assertNotNull(addon3.installedState) + assertEquals("ext3", addon3.installedState!!.id) + assertTrue(addon3.isSupported()) + assertFalse(addon3.isEnabled()) + assertTrue(addon3.isDisabledAsUnsupported()) + assertEquals("http://options-page.moz", addon3.installedState!!.optionsPageUrl) + assertTrue(addon3.installedState!!.openOptionsPageInTab) + + // ext4 should be installed. + val addon4 = addons.find { it.id == "ext4" }!! + assertEquals("ext4", addon4.id) + assertNotNull(addon4.installedState) + assertEquals("ext4", addon4.installedState!!.id) + assertTrue(addon4.isEnabled()) + assertFalse(addon4.isDisabledAsUnsupported()) + assertNull(addon4.installedState!!.optionsPageUrl) + assertFalse(addon4.installedState!!.openOptionsPageInTab) + + // ext5 should be installed. + val addon5 = addons.find { it.id == "ext5" }!! + assertEquals("ext5", addon5.id) + assertNotNull(addon5.installedState) + assertEquals("ext5", addon5.installedState!!.id) + assertTrue(addon5.isEnabled()) + assertFalse(addon5.isDisabledAsUnsupported()) + assertNull(addon5.installedState!!.optionsPageUrl) + assertFalse(addon5.installedState!!.openOptionsPageInTab) + + // ext6 should be installed. + val addon6 = addons.find { it.id == "ext6" }!! + assertEquals("ext6", addon6.id) + assertNotNull(addon6.installedState) + assertEquals("ext6", addon6.installedState!!.id) + assertTrue(addon6.isEnabled()) + assertFalse(addon6.isDisabledAsUnsupported()) + assertNull(addon6.installedState!!.optionsPageUrl) + assertFalse(addon6.installedState!!.openOptionsPageInTab) + } + + @Test + fun `getAddons - returns temporary add-ons as supported`() = runTestOnMain { + val addonsProvider: AddonsProvider = mock() + whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf()) + + // Prepare engine + val engine: Engine = mock() + val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() + whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { + callbackCaptor.value.invoke(emptyList()) + } + + val store = BrowserStore() + WebExtensionSupport.initialize(engine, store) + + // Add temporary extension + val temporaryExtension: WebExtension = mock() + val temporaryExtensionIcon: Bitmap = mock() + val temporaryExtensionMetadata: Metadata = mock() + whenever(temporaryExtensionMetadata.temporary).thenReturn(true) + whenever(temporaryExtensionMetadata.name).thenReturn("name") + whenever(temporaryExtension.id).thenReturn("temp_ext") + whenever(temporaryExtension.url).thenReturn("site_url") + whenever(temporaryExtension.getMetadata()).thenReturn(temporaryExtensionMetadata) + WebExtensionSupport.installedExtensions["temp_ext"] = temporaryExtension + + val addonManager = spy(AddonManager(store, mock(), addonsProvider, mock())) + + whenever(addonManager.loadIcon(temporaryExtension)).thenReturn(temporaryExtensionIcon) + + val addons = addonManager.getAddons() + assertEquals(1, addons.size) + + // Temporary extension should be returned and marked as supported + assertEquals("temp_ext", addons[0].id) + assertEquals(1, addons[0].translatableName.size) + assertNotNull(addons[0].translatableName[addons[0].defaultLocale]) + assertTrue(addons[0].translatableName.containsValue("name")) + assertNotNull(addons[0].installedState) + assertTrue(addons[0].isSupported()) + assertEquals(temporaryExtensionIcon, addons[0].installedState!!.icon) + } + + @Test + fun `getAddons - filters unneeded locales on featured add-ons`() = runTestOnMain { + val addon = Addon( + id = "addon1", + translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"), + translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "invalid1" to "Beschreibung", "invalid2" to "descripciĆ³n"), + translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "invalid1" to "Kurzfassung", "invalid2" to "resumen"), + ) + + val store = BrowserStore() + + val engine: Engine = mock() + val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() + whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { + callbackCaptor.value.invoke(emptyList()) + } + + val addonsProvider: AddonsProvider = mock() + whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon)) + WebExtensionSupport.initialize(engine, store) + + val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons() + assertEquals(1, addons[0].translatableName.size) + assertTrue(addons[0].translatableName.contains(addons[0].defaultLocale)) + assertEquals(1, addons[0].translatableDescription.size) + assertTrue(addons[0].translatableDescription.contains(addons[0].defaultLocale)) + assertEquals(1, addons[0].translatableSummary.size) + assertTrue(addons[0].translatableSummary.contains(addons[0].defaultLocale)) + } + + @Test + fun `getAddons - filters unneeded locales on non-featured installed add-ons`() = runTestOnMain { + val addon = Addon( + id = "addon1", + translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"), + translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "invalid1" to "Beschreibung", "invalid2" to "descripciĆ³n"), + translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "invalid1" to "Kurzfassung", "invalid2" to "resumen"), + ) + + val store = BrowserStore() + + val engine: Engine = mock() + val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() + whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { + callbackCaptor.value.invoke(emptyList()) + } + + val addonsProvider: AddonsProvider = mock() + whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(emptyList()) + WebExtensionSupport.initialize(engine, store) + val extension: WebExtension = mock() + whenever(extension.id).thenReturn(addon.id) + whenever(extension.isEnabled()).thenReturn(true) + whenever(extension.getMetadata()).thenReturn(mock()) + WebExtensionSupport.installedExtensions[addon.id] = extension + + val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons() + assertEquals(1, addons[0].translatableName.size) + assertTrue(addons[0].translatableName.contains(addons[0].defaultLocale)) + assertEquals(1, addons[0].translatableDescription.size) + assertTrue(addons[0].translatableDescription.contains(addons[0].defaultLocale)) + assertEquals(1, addons[0].translatableSummary.size) + assertTrue(addons[0].translatableSummary.contains(addons[0].defaultLocale)) + } + + @Test + fun `getAddons - suspends until pending actions are completed`() = runTestOnMain { + val addon = Addon( + id = "ext1", + installedState = Addon.InstalledState("ext1", "1.0", "", true), + ) + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + + val store = BrowserStore() + + val engine: Engine = mock() + val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() + whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { + callbackCaptor.value.invoke(emptyList()) + } + val addonsProvider: AddonsProvider = mock() + + whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon)) + WebExtensionSupport.initialize(engine, store) + WebExtensionSupport.installedExtensions[addon.id] = extension + + val addonManager = AddonManager(store, mock(), addonsProvider, mock()) + addonManager.installAddon(url = addon.downloadUrl) + addonManager.enableAddon(addon) + addonManager.disableAddon(addon) + addonManager.uninstallAddon(addon) + assertEquals(4, addonManager.pendingAddonActions.size) + + var getAddonsResult: List<Addon>? = null + val nonSuspendingJob = CoroutineScope(Dispatchers.IO).launch { + getAddonsResult = addonManager.getAddons(waitForPendingActions = false) + } + + nonSuspendingJob.join() + assertNotNull(getAddonsResult) + + getAddonsResult = null + val suspendingJob = CoroutineScope(Dispatchers.IO).launch { + getAddonsResult = addonManager.getAddons(waitForPendingActions = true) + } + + addonManager.pendingAddonActions.forEach { it.complete(Unit) } + + suspendingJob.join() + assertNotNull(getAddonsResult) + } + + @Test + fun `getAddons - passes on allowCache parameter`() = runTestOnMain { + val store = BrowserStore() + + val engine: Engine = mock() + val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() + whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { + callbackCaptor.value.invoke(emptyList()) + } + WebExtensionSupport.initialize(engine, store) + + val addonsProvider: AddonsProvider = mock() + whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(emptyList()) + val addonsManager = AddonManager(store, mock(), addonsProvider, mock()) + + addonsManager.getAddons() + verify(addonsProvider).getFeaturedAddons(eq(true), eq(null), language = anyString()) + + addonsManager.getAddons(allowCache = false) + verify(addonsProvider).getFeaturedAddons(eq(false), eq(null), language = anyString()) + Unit + } + + @Test + fun `updateAddon - when a extension is updated successfully`() { + val engine: Engine = mock() + val engineSession: EngineSession = mock() + val store = spy( + BrowserStore( + BrowserState( + tabs = listOf( + createTab(id = "1", url = "https://www.mozilla.org", engineSession = engineSession), + ), + extensions = mapOf("extensionId" to mock()), + ), + ), + ) + val onSuccessCaptor = argumentCaptor<((WebExtension?) -> Unit)>() + var updateStatus: Status? = null + val manager = AddonManager(store, engine, mock(), mock()) + + val updatedExt: WebExtension = mock() + whenever(updatedExt.id).thenReturn("extensionId") + whenever(updatedExt.url).thenReturn("url") + whenever(updatedExt.supportActions).thenReturn(true) + + WebExtensionSupport.installedExtensions["extensionId"] = mock() + + val oldExt = WebExtensionSupport.installedExtensions["extensionId"] + + manager.updateAddon("extensionId") { status -> + updateStatus = status + } + + val actionHandlerCaptor = argumentCaptor<ActionHandler>() + val actionCaptor = argumentCaptor<WebExtensionAction.UpdateWebExtensionAction>() + + // Verifying we returned the right status + verify(engine).updateWebExtension(any(), onSuccessCaptor.capture(), any()) + onSuccessCaptor.value.invoke(updatedExt) + assertEquals(Status.SuccessfullyUpdated, updateStatus) + + // Verifying we updated the extension in WebExtensionSupport + assertNotEquals(oldExt, WebExtensionSupport.installedExtensions["extensionId"]) + assertEquals(updatedExt, WebExtensionSupport.installedExtensions["extensionId"]) + + // Verifying we updated the extension in the store + verify(store).dispatch(actionCaptor.capture()) + assertEquals( + WebExtensionState(updatedExt.id, updatedExt.url, updatedExt.getMetadata()?.name, updatedExt.isEnabled()), + actionCaptor.allValues.last().updatedExtension, + ) + + // Verify that we registered an action handler for all existing sessions on the extension + verify(updatedExt).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture()) + actionHandlerCaptor.value.onBrowserAction(updatedExt, engineSession, mock()) + } + + @Test + fun `updateAddon - when extension is not installed`() { + var updateStatus: Status? = null + + val manager = AddonManager(mock(), mock(), mock(), mock()) + + manager.updateAddon("extensionId") { status -> + updateStatus = status + } + + assertEquals(Status.NotInstalled, updateStatus) + } + + @Test + fun `updateAddon - when extension is not supported`() { + var updateStatus: Status? = null + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("unsupportedExt") + + val metadata: Metadata = mock() + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT)) + whenever(extension.getMetadata()).thenReturn(metadata) + + WebExtensionSupport.installedExtensions["extensionId"] = extension + + val manager = AddonManager(mock(), mock(), mock(), mock()) + manager.updateAddon("extensionId") { status -> + updateStatus = status + } + + assertEquals(Status.NotInstalled, updateStatus) + } + + @Test + fun `updateAddon - when an error happens while updating`() { + val engine: Engine = mock() + val onErrorCaptor = argumentCaptor<((String, Throwable) -> Unit)>() + var updateStatus: Status? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + + WebExtensionSupport.installedExtensions["extensionId"] = mock() + + manager.updateAddon("extensionId") { status -> + updateStatus = status + } + + // Verifying we returned the right status + verify(engine).updateWebExtension(any(), any(), onErrorCaptor.capture()) + onErrorCaptor.value.invoke("message", Exception()) + assertTrue(updateStatus is Status.Error) + } + + @Test + fun `updateAddon - when there is not new updates for the extension`() { + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<((WebExtension?) -> Unit)>() + var updateStatus: Status? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + + WebExtensionSupport.installedExtensions["extensionId"] = mock() + manager.updateAddon("extensionId") { status -> + updateStatus = status + } + + verify(engine).updateWebExtension(any(), onSuccessCaptor.capture(), any()) + onSuccessCaptor.value.invoke(null) + assertEquals(Status.NoUpdateAvailable, updateStatus) + } + + @Test + fun `installAddon successfully`() { + val addon = Addon(id = "ext1") + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() + + var installedAddon: Addon? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.installAddon( + url = addon.downloadUrl, + installationMethod = InstallationMethod.MANAGER, + onSuccess = { + installedAddon = it + }, + ) + + verify(engine).installWebExtension( + any(), + eq(InstallationMethod.MANAGER), + onSuccessCaptor.capture(), + any(), + ) + + val metadata: Metadata = mock() + val extension: WebExtension = mock() + whenever(metadata.name).thenReturn("nameFromMetadata") + whenever(extension.id).thenReturn("ext1") + whenever(extension.getMetadata()).thenReturn(metadata) + onSuccessCaptor.value.invoke(extension) + assertNotNull(installedAddon) + assertEquals(addon.id, installedAddon!!.id) + assertEquals("nameFromMetadata", installedAddon!!.translateName(testContext)) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `installAddon failure`() { + val addon = Addon(id = "ext1") + val engine: Engine = mock() + val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>() + + var throwable: Throwable? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.installAddon( + url = addon.downloadUrl, + installationMethod = InstallationMethod.FROM_FILE, + onError = { caught -> + throwable = caught + }, + ) + + verify(engine).installWebExtension( + url = any(), + installationMethod = eq(InstallationMethod.FROM_FILE), + onSuccess = any(), + onError = onErrorCaptor.capture(), + ) + + onErrorCaptor.value.invoke(IllegalStateException("test")) + assertNotNull(throwable!!) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `uninstallAddon successfully`() { + val installedAddon = Addon( + id = "ext1", + installedState = Addon.InstalledState("ext1", "1.0", "", true), + ) + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[installedAddon.id] = extension + + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<(() -> Unit)>() + + var successCallbackInvoked = false + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.uninstallAddon( + installedAddon, + onSuccess = { + successCallbackInvoked = true + }, + ) + verify(engine).uninstallWebExtension(eq(extension), onSuccessCaptor.capture(), any()) + + onSuccessCaptor.value.invoke() + assertTrue(successCallbackInvoked) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `uninstallAddon failure cases`() { + val addon = Addon(id = "ext1") + val engine: Engine = mock() + val onErrorCaptor = argumentCaptor<((String, Throwable) -> Unit)>() + var throwable: Throwable? = null + var msg: String? = null + val errorCallback = { errorMsg: String, caught: Throwable -> + throwable = caught + msg = errorMsg + } + val manager = AddonManager(mock(), engine, mock(), mock()) + + // Extension is not installed so we're invoking the error callback and never the engine + manager.uninstallAddon(addon, onError = errorCallback) + verify(engine, never()).uninstallWebExtension(any(), any(), onErrorCaptor.capture()) + assertNotNull(throwable!!) + assertEquals("Addon is not installed", throwable!!.localizedMessage) + + // Install extension and try again + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + manager.uninstallAddon(addon, onError = errorCallback) + verify(engine, never()).uninstallWebExtension(any(), any(), onErrorCaptor.capture()) + + // Make sure engine error is forwarded to caller + val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true)) + manager.uninstallAddon(installedAddon, onError = errorCallback) + verify(engine).uninstallWebExtension(eq(extension), any(), onErrorCaptor.capture()) + onErrorCaptor.value.invoke(addon.id, IllegalStateException("test")) + assertNotNull(throwable!!) + assertEquals("test", throwable!!.localizedMessage) + assertEquals(msg, addon.id) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `add optional permissions successfully`() { + val permission = listOf("permission1") + val origin = listOf("origin") + val addon = Addon( + id = "ext1", + installedState = Addon.InstalledState("ext1", "1.0", "", true), + ) + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() + + var updateAddon: Addon? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.addOptionalPermission( + addon, + permission, + origin, + onSuccess = { + updateAddon = it + }, + ) + + verify(engine).addOptionalPermissions(eq(extension.id), any(), any(), onSuccessCaptor.capture(), any()) + onSuccessCaptor.value.invoke(extension) + assertNotNull(updateAddon) + assertEquals(addon.id, updateAddon!!.id) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `add optional with empty permissions and origins`() { + var onErrorWasExecuted = false + val manager = AddonManager(mock(), mock(), mock(), mock()) + + manager.addOptionalPermission( + mock(), + emptyList(), + emptyList(), + onError = { + onErrorWasExecuted = true + }, + ) + + assertTrue(onErrorWasExecuted) + } + + @Test + fun `remove optional permissions successfully`() { + val permission = listOf("permission1") + val origins = listOf("origin") + val addon = Addon( + id = "ext1", + installedState = Addon.InstalledState("ext1", "1.0", "", true), + ) + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() + + var updateAddon: Addon? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.removeOptionalPermission( + addon, + permission, + origins, + onSuccess = { + updateAddon = it + }, + ) + + verify(engine).removeOptionalPermissions(eq(extension.id), any(), any(), onSuccessCaptor.capture(), any()) + onSuccessCaptor.value.invoke(extension) + assertNotNull(updateAddon) + assertEquals(addon.id, updateAddon!!.id) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `remove optional with empty permissions and origins`() { + var onErrorWasExecuted = false + val manager = AddonManager(mock(), mock(), mock(), mock()) + + manager.removeOptionalPermission( + mock(), + emptyList(), + emptyList(), + onError = { + onErrorWasExecuted = true + }, + ) + + assertTrue(onErrorWasExecuted) + } + + @Test + fun `enableAddon successfully`() { + val addon = Addon( + id = "ext1", + installedState = Addon.InstalledState("ext1", "1.0", "", true), + ) + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() + + var enabledAddon: Addon? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.enableAddon( + addon, + onSuccess = { + enabledAddon = it + }, + ) + + verify(engine).enableWebExtension(eq(extension), any(), onSuccessCaptor.capture(), any()) + onSuccessCaptor.value.invoke(extension) + assertNotNull(enabledAddon) + assertEquals(addon.id, enabledAddon!!.id) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `enableAddon failure cases`() { + val addon = Addon(id = "ext1") + val engine: Engine = mock() + val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>() + var throwable: Throwable? = null + val errorCallback = { caught: Throwable -> + throwable = caught + } + val manager = AddonManager(mock(), engine, mock(), mock()) + + // Extension is not installed so we're invoking the error callback and never the engine + manager.enableAddon(addon, onError = errorCallback) + verify(engine, never()).enableWebExtension(any(), any(), any(), onErrorCaptor.capture()) + assertNotNull(throwable!!) + assertEquals("Addon is not installed", throwable!!.localizedMessage) + + // Install extension and try again + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + manager.enableAddon(addon, onError = errorCallback) + verify(engine, never()).enableWebExtension(any(), any(), any(), onErrorCaptor.capture()) + + // Make sure engine error is forwarded to caller + val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true)) + manager.enableAddon(installedAddon, source = EnableSource.APP_SUPPORT, onError = errorCallback) + verify(engine).enableWebExtension(eq(extension), eq(EnableSource.APP_SUPPORT), any(), onErrorCaptor.capture()) + onErrorCaptor.value.invoke(IllegalStateException("test")) + assertNotNull(throwable!!) + assertEquals("test", throwable!!.localizedMessage) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `disableAddon successfully`() { + val addon = Addon( + id = "ext1", + installedState = Addon.InstalledState("ext1", "1.0", "", true), + ) + + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + + val engine: Engine = mock() + val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() + + var disabledAddon: Addon? = null + val manager = AddonManager(mock(), engine, mock(), mock()) + manager.disableAddon( + addon, + source = EnableSource.APP_SUPPORT, + onSuccess = { + disabledAddon = it + }, + ) + + verify(engine).disableWebExtension(eq(extension), eq(EnableSource.APP_SUPPORT), onSuccessCaptor.capture(), any()) + onSuccessCaptor.value.invoke(extension) + assertNotNull(disabledAddon) + assertEquals(addon.id, disabledAddon!!.id) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `disableAddon failure cases`() { + val addon = Addon(id = "ext1") + val engine: Engine = mock() + val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>() + var throwable: Throwable? = null + val errorCallback = { caught: Throwable -> + throwable = caught + } + val manager = AddonManager(mock(), engine, mock(), mock()) + + // Extension is not installed so we're invoking the error callback and never the engine + manager.disableAddon(addon, onError = errorCallback) + verify(engine, never()).disableWebExtension(any(), any(), any(), onErrorCaptor.capture()) + assertNotNull(throwable!!) + assertEquals("Addon is not installed", throwable!!.localizedMessage) + + // Install extension and try again + val extension: WebExtension = mock() + whenever(extension.id).thenReturn("ext1") + WebExtensionSupport.installedExtensions[addon.id] = extension + manager.disableAddon(addon, onError = errorCallback) + verify(engine, never()).disableWebExtension(any(), any(), any(), onErrorCaptor.capture()) + + // Make sure engine error is forwarded to caller + val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true)) + manager.disableAddon(installedAddon, onError = errorCallback) + verify(engine).disableWebExtension(eq(extension), any(), any(), onErrorCaptor.capture()) + onErrorCaptor.value.invoke(IllegalStateException("test")) + assertNotNull(throwable!!) + assertEquals("test", throwable!!.localizedMessage) + assertTrue(manager.pendingAddonActions.isEmpty()) + } + + @Test + fun `toInstalledState read from icon cache`() { + val extension: WebExtension = mock() + val metadata: Metadata = mock() + + val manager = spy(AddonManager(mock(), mock(), mock(), mock())) + + manager.iconsCache["ext1"] = mock() + whenever(extension.id).thenReturn("ext1") + whenever(extension.getMetadata()).thenReturn(metadata) + whenever(extension.isEnabled()).thenReturn(true) + whenever(extension.getDisabledReason()).thenReturn(null) + whenever(extension.isAllowedInPrivateBrowsing()).thenReturn(true) + whenever(metadata.version).thenReturn("version") + whenever(metadata.optionsPageUrl).thenReturn("optionsPageUrl") + whenever(metadata.openOptionsPageInTab).thenReturn(true) + + val installedExtension = manager.toInstalledState(extension) + + assertEquals(manager.iconsCache["ext1"], installedExtension.icon) + assertEquals("version", installedExtension.version) + assertEquals("optionsPageUrl", installedExtension.optionsPageUrl) + assertNull(installedExtension.disabledReason) + assertTrue(installedExtension.openOptionsPageInTab) + assertTrue(installedExtension.enabled) + assertTrue(installedExtension.allowedInPrivateBrowsing) + + verify(manager, times(0)).loadIcon(eq(extension)) + } + + @Test + fun `toInstalledState load icon when cache is not available`() { + val extension: WebExtension = mock() + val metadata: Metadata = mock() + + val manager = spy(AddonManager(mock(), mock(), mock(), mock())) + + whenever(extension.id).thenReturn("ext1") + whenever(extension.getMetadata()).thenReturn(metadata) + whenever(extension.isEnabled()).thenReturn(true) + whenever(extension.getDisabledReason()).thenReturn(null) + whenever(extension.isAllowedInPrivateBrowsing()).thenReturn(true) + whenever(metadata.version).thenReturn("version") + whenever(metadata.optionsPageUrl).thenReturn("optionsPageUrl") + whenever(metadata.openOptionsPageInTab).thenReturn(true) + + val installedExtension = manager.toInstalledState(extension) + + assertEquals(manager.iconsCache["ext1"], installedExtension.icon) + assertEquals("version", installedExtension.version) + assertEquals("optionsPageUrl", installedExtension.optionsPageUrl) + assertNull(installedExtension.disabledReason) + assertTrue(installedExtension.openOptionsPageInTab) + assertTrue(installedExtension.enabled) + assertTrue(installedExtension.allowedInPrivateBrowsing) + + verify(manager).loadIcon(extension) + } + + @Test + fun `loadIcon try to load the icon from extension`() = runTestOnMain { + val extension: WebExtension = mock() + + val manager = spy(AddonManager(mock(), mock(), mock(), mock())) + + whenever(extension.loadIcon(AddonManager.ADDON_ICON_SIZE)).thenReturn(mock()) + + val icon = manager.loadIcon(extension) + + assertNotNull(icon) + } + + @Test + fun `loadIcon calls tryLoadIconInBackground when TimeoutCancellationException`() = + runTestOnMain { + val extension: WebExtension = mock() + + val manager = spy(AddonManager(mock(), mock(), mock(), mock())) + doNothing().`when`(manager).tryLoadIconInBackground(extension) + + doThrow(mock<TimeoutCancellationException>()).`when`(extension) + .loadIcon(AddonManager.ADDON_ICON_SIZE) + + val icon = manager.loadIcon(extension) + + assertNull(icon) + verify(manager).loadIcon(extension) + } + + @Test + fun `getDisabledReason cases`() { + val extension: WebExtension = mock() + val metadata: Metadata = mock() + whenever(extension.getMetadata()).thenReturn(metadata) + + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(BLOCKLIST)) + assertEquals(Addon.DisabledReason.BLOCKLISTED, extension.getDisabledReason()) + + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT)) + assertEquals(Addon.DisabledReason.UNSUPPORTED, extension.getDisabledReason()) + + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(USER)) + assertEquals(Addon.DisabledReason.USER_REQUESTED, extension.getDisabledReason()) + + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(SIGNATURE)) + assertEquals(Addon.DisabledReason.NOT_CORRECTLY_SIGNED, extension.getDisabledReason()) + + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_VERSION)) + assertEquals(Addon.DisabledReason.INCOMPATIBLE, extension.getDisabledReason()) + + whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(0)) + assertNull(extension.getDisabledReason()) + } +} |