/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ package org.mozilla.geckoview.test import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.greaterThan import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.StringEndsWith.endsWith import org.json.JSONObject import org.junit.Assert.* // ktlint-disable no-wildcard-imports import org.junit.Assume.assumeThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.geckoview.* // ktlint-disable no-wildcard-imports import org.mozilla.geckoview.GeckoSession.NavigationDelegate import org.mozilla.geckoview.GeckoSession.PermissionDelegate import org.mozilla.geckoview.GeckoSession.ProgressDelegate import org.mozilla.geckoview.WebExtension.* // ktlint-disable no-wildcard-imports import org.mozilla.geckoview.WebExtension.BrowsingDataDelegate.Type.* // ktlint-disable no-wildcard-imports import org.mozilla.geckoview.WebExtensionController.EnableSource import org.mozilla.geckoview.test.rule.GeckoSessionTestRule import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.RejectedPromiseException import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay import org.mozilla.geckoview.test.util.RuntimeCreator import org.mozilla.geckoview.test.util.UiThreadUtils import java.nio.charset.Charset import java.util.* // ktlint-disable no-wildcard-imports import java.util.concurrent.CancellationException import kotlin.collections.HashMap @RunWith(AndroidJUnit4::class) @MediumTest class WebExtensionTest : BaseSessionTest() { companion object { private const val TABS_CREATE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-create/" private const val TABS_CREATE_2_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-create-2/" private const val TABS_CREATE_REMOVE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-create-remove/" private const val TABS_ACTIVATE_REMOVE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-activate-remove/" private const val TABS_REMOVE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-remove/" private const val MESSAGING_BACKGROUND: String = "resource://android/assets/web_extensions/messaging/" private const val MESSAGING_CONTENT: String = "resource://android/assets/web_extensions/messaging-content/" private const val OPENOPTIONSPAGE_1_BACKGROUND: String = "resource://android/assets/web_extensions/openoptionspage-1/" private const val OPENOPTIONSPAGE_2_BACKGROUND: String = "resource://android/assets/web_extensions/openoptionspage-2/" private const val EXTENSION_PAGE_RESTORE: String = "resource://android/assets/web_extensions/extension-page-restore/" private const val BROWSING_DATA: String = "resource://android/assets/web_extensions/browsing-data-built-in/" } private val controller get() = sessionRule.runtime.webExtensionController @Before fun setup() { sessionRule.setPrefsUntilTestEnd(mapOf("extensions.isembedded" to true)) sessionRule.runtime.webExtensionController.setTabActive(mainSession, true) } @Test fun installBuiltIn() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") // Load the WebExtension that will add a border to the body val borderify = sessionRule.waitForResult( controller.installBuiltIn( "resource://android/assets/web_extensions/borderify/", ), ) assertTrue(borderify.isBuiltIn) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("red") // Check some of the metadata assertEquals(borderify.metaData.incognito, "spanning") // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(borderify)) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not applied after being uninstalled assertBodyBorderEqualTo("") } private fun assertBodyBorderEqualTo(expected: String) { val color = mainSession.evaluateJS("document.body.style.borderColor") assertThat( "The border color should be '$expected'", color as String, equalTo(expected), ) } private fun checkDisabledState( extension: WebExtension, userDisabled: Boolean = false, appDisabled: Boolean = false, blocklistDisabled: Boolean = false, signatureDisabled: Boolean = false, appVersionDisabled: Boolean = false, ) { val enabled = !userDisabled && !appDisabled && !blocklistDisabled && !signatureDisabled && !appVersionDisabled mainSession.reload() sessionRule.waitForPageStop() if (!enabled) { // Border should be empty because the extension is disabled assertBodyBorderEqualTo("") } else { assertBodyBorderEqualTo("red") } assertThat( "enabled should match", extension.metaData.enabled, equalTo(enabled), ) assertThat( "userDisabled should match", extension.metaData.disabledFlags and DisabledFlags.USER > 0, equalTo(userDisabled), ) assertThat( "appDisabled should match", extension.metaData.disabledFlags and DisabledFlags.APP > 0, equalTo(appDisabled), ) assertThat( "blocklistDisabled should match", extension.metaData.disabledFlags and DisabledFlags.BLOCKLIST > 0, equalTo(blocklistDisabled), ) assertThat( "signatureDisabled should match", extension.metaData.disabledFlags and DisabledFlags.SIGNATURE > 0, equalTo(signatureDisabled), ) assertThat( "appVersionDisabled should match", extension.metaData.disabledFlags and DisabledFlags.APP_VERSION > 0, equalTo(appVersionDisabled), ) } @Test fun noDelegateErrorMessage() { try { sessionRule.evaluateExtensionJS( """ const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); await browser.tabs.update(tab.id, { url: "www.google.com" }); """, ) assertThat("tabs.update should not succeed", true, equalTo(false)) } catch (ex: RejectedPromiseException) { assertThat( "Error message matches", ex.message, equalTo("Error: tabs.update is not supported"), ) } try { sessionRule.evaluateExtensionJS( """ const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); await browser.tabs.remove(tab.id); """, ) assertThat("tabs.remove should not succeed", true, equalTo(false)) } catch (ex: RejectedPromiseException) { assertThat( "Error message matches", ex.message, equalTo("Error: tabs.remove is not supported"), ) } try { sessionRule.evaluateExtensionJS( """ await browser.runtime.openOptionsPage(); """, ) assertThat( "runtime.openOptionsPage should not succeed", true, equalTo(false), ) } catch (ex: RejectedPromiseException) { assertThat( "Error message matches", ex.message, equalTo("Error: runtime.openOptionsPage is not supported"), ) } } @Test fun enableDisable() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) sessionRule.addExternalDelegateUntilTestEnd( WebExtensionController.AddonManagerDelegate::class, { delegate -> controller.setAddonManagerDelegate(delegate) }, { controller.setAddonManagerDelegate(null) }, object : WebExtensionController.AddonManagerDelegate { @AssertCalled(count = 3) override fun onEnabling(extension: WebExtension) {} @AssertCalled(count = 3) override fun onEnabled(extension: WebExtension) {} @AssertCalled(count = 3) override fun onDisabling(extension: WebExtension) {} @AssertCalled(count = 3) override fun onDisabled(extension: WebExtension) {} @AssertCalled(count = 1) override fun onUninstalling(extension: WebExtension) {} @AssertCalled(count = 1) override fun onUninstalled(extension: WebExtension) {} @AssertCalled(count = 1) override fun onInstalling(extension: WebExtension) {} @AssertCalled(count = 1) override fun onInstalled(extension: WebExtension) {} }, ) // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") var borderify = sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ), ) checkDisabledState(borderify, userDisabled = false, appDisabled = false) borderify = sessionRule.waitForResult(controller.disable(borderify, EnableSource.USER)) checkDisabledState(borderify, userDisabled = true, appDisabled = false) borderify = sessionRule.waitForResult(controller.disable(borderify, EnableSource.APP)) checkDisabledState(borderify, userDisabled = true, appDisabled = true) borderify = sessionRule.waitForResult(controller.enable(borderify, EnableSource.APP)) checkDisabledState(borderify, userDisabled = true, appDisabled = false) borderify = sessionRule.waitForResult(controller.enable(borderify, EnableSource.USER)) checkDisabledState(borderify, userDisabled = false, appDisabled = false) borderify = sessionRule.waitForResult(controller.disable(borderify, EnableSource.APP)) checkDisabledState(borderify, userDisabled = false, appDisabled = true) borderify = sessionRule.waitForResult(controller.enable(borderify, EnableSource.APP)) checkDisabledState(borderify, userDisabled = false, appDisabled = false) sessionRule.waitForResult(controller.uninstall(borderify)) mainSession.reload() sessionRule.waitForPageStop() // Border should be empty because the extension is not installed anymore assertBodyBorderEqualTo("") } @Test fun installWebExtension() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals( extension.metaData.description, "Adds a red border to all webpages matching example.com.", ) assertEquals(extension.metaData.name, "Borderify") assertEquals(extension.metaData.version, "1.0") assertEquals(extension.isBuiltIn, false) assertEquals(extension.metaData.enabled, false) assertEquals( extension.metaData.signedState, WebExtension.SignedStateFlags.SIGNED, ) assertEquals( extension.metaData.blocklistState, WebExtension.BlocklistStateFlags.NOT_BLOCKED, ) assertEquals(extension.metaData.incognito, "spanning") return GeckoResult.allow() } }) val borderify = sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("red") var list = extensionsMap(sessionRule.waitForResult(controller.list())) assertEquals(list.size, 2) assertTrue(list.containsKey(borderify.id)) assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID)) // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(borderify)) list = extensionsMap(sessionRule.waitForResult(controller.list())) assertEquals(list.size, 1) assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID)) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not applied after being uninstalled assertBodyBorderEqualTo("") } @Test @Setting.List(Setting(key = Setting.Key.USE_PRIVATE_MODE, value = "true")) fun runInPrivateBrowsing() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // Make sure border is empty before running the extension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 1) override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) var borderify = sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ), ) // Make sure private mode is enabled assertTrue(mainSession.settings.usePrivateMode) assertFalse(borderify.metaData.allowedInPrivateBrowsing) // Check that the WebExtension was not applied to a private mode page assertBodyBorderEqualTo("") borderify = sessionRule.waitForResult( controller.setAllowedInPrivateBrowsing(borderify, true), ) assertTrue(borderify.metaData.allowedInPrivateBrowsing) // Check that the WebExtension was applied to a private mode page now that the extension // is enabled in private mode mainSession.reload() sessionRule.waitForPageStop() assertBodyBorderEqualTo("red") borderify = sessionRule.waitForResult( controller.setAllowedInPrivateBrowsing(borderify, false), ) assertFalse(borderify.metaData.allowedInPrivateBrowsing) // Check that the WebExtension was not applied to a private mode page after being // not allowed to run in private mode mainSession.reload() sessionRule.waitForPageStop() assertBodyBorderEqualTo("") // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(borderify)) mainSession.reload() sessionRule.waitForPageStop() assertBodyBorderEqualTo("") } @Test fun optionsPageMetadata() { // dummy.xpi is not signed, but it could be sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, ), ) // Wait for the onReady AddonManagerDelegate method to be called, and assert // that the baseUrl and optionsPageUrl are both available as expected. val onReadyResult = GeckoResult() sessionRule.addExternalDelegateUntilTestEnd( WebExtensionController.AddonManagerDelegate::class, { delegate -> controller.setAddonManagerDelegate(delegate) }, { controller.setAddonManagerDelegate(null) }, object : WebExtensionController.AddonManagerDelegate { @AssertCalled(count = 1) override fun onReady(extension: WebExtension) { assertNotNull(extension.metaData.baseUrl) assertTrue(extension.metaData.baseUrl.matches("^moz-extension://[0-9a-f\\-]*/$".toRegex())) assertNotNull(extension.metaData.optionsPageUrl) assertTrue((extension.metaData.optionsPageUrl ?: "").matches("^moz-extension://[0-9a-f\\-]*/options.html$".toRegex())) onReadyResult.complete(null) super.onReady(extension) } }, ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 1) override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val dummy = sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/dummy.xpi", null, ), ) // In the onReady AddonManagerDelegate optionsPageUrl metadata is asserted again // and expected to not be empty anymore. assertNull(dummy.metaData.optionsPageUrl) sessionRule.waitForResult(onReadyResult) sessionRule.waitForResult(controller.uninstall(dummy)) } @Test fun installMultiple() { // dummy.xpi is not signed, but it could be sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, ), ) // First, make sure the list only contains the test support extension var list = extensionsMap(sessionRule.waitForResult(controller.list())) assertEquals(list.size, 1) assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID)) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 2) override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) // Install in parallell borderify and dummy val borderifyResult = controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ) val dummyResult = controller.install( "resource://android/assets/web_extensions/dummy.xpi", null, ) val (borderify, dummy) = sessionRule.waitForResult( GeckoResult.allOf(borderifyResult, dummyResult), ) // Make sure the list is updated accordingly list = extensionsMap(sessionRule.waitForResult(controller.list())) assertTrue(list.containsKey(borderify.id)) assertTrue(list.containsKey(dummy.id)) assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID)) assertEquals(list.size, 3) // Uninstall borderify and verify that it's not in the list anymore sessionRule.waitForResult(controller.uninstall(borderify)) list = extensionsMap(sessionRule.waitForResult(controller.list())) assertEquals(list.size, 2) assertTrue(list.containsKey(dummy.id)) assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID)) assertFalse(list.containsKey(borderify.id)) // Uninstall dummy and make sure the list is now empty sessionRule.waitForResult(controller.uninstall(dummy)) list = extensionsMap(sessionRule.waitForResult(controller.list())) assertEquals(list.size, 1) assertTrue(list.containsKey(RuntimeCreator.TEST_SUPPORT_EXTENSION_ID)) } private fun testInstallError( name: String, expectedError: Int, expectedExtensionID: String?, expectedExtension: Boolean = true, ) { sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 0) override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) sessionRule.addExternalDelegateUntilTestEnd( WebExtensionController.AddonManagerDelegate::class, { delegate -> controller.setAddonManagerDelegate(delegate) }, { controller.setAddonManagerDelegate(null) }, object : WebExtensionController.AddonManagerDelegate { @AssertCalled(count = 1) override fun onInstallationFailed( extension: WebExtension?, installException: InstallException, ) { // Make sure the extension is present when it should be. assertEquals(expectedExtension, extension != null) assertEquals(expectedExtensionID, extension?.id) assertEquals(installException.code, expectedError) } }, ) sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/$name", null, ) .accept({ // We should not be able to install an extension here. assertTrue(false) }, { exception -> val installException = exception as WebExtension.InstallException assertEquals(installException.code, expectedError) }), ) } private fun extensionsMap(extensionList: List): Map { val map = HashMap() for (extension in extensionList) { map.put(extension.id, extension) } return map } private fun testInstallUnsignedExtensionSignatureNotRequired( extensionArchiveURL: String, extensionName: String, ) { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, ), ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val borderify = sessionRule.waitForResult( controller.install(extensionArchiveURL, null) .then { extension -> assertEquals( extension!!.metaData.signedState, WebExtension.SignedStateFlags.MISSING, ) assertEquals( extension.metaData.blocklistState, WebExtension.BlocklistStateFlags.NOT_BLOCKED, ) assertEquals(extension.metaData.name, extensionName) GeckoResult.fromValue(extension) }, ) sessionRule.waitForResult(controller.uninstall(borderify)) } @Test fun installUnsignedExtensionSignatureNotRequired() { testInstallUnsignedExtensionSignatureNotRequired( extensionArchiveURL = "resource://android/assets/web_extensions/borderify-unsigned.xpi", extensionName = "Borderify", ) } @Test fun installUnsignedExtensionAsZipFile() { testInstallUnsignedExtensionSignatureNotRequired( extensionArchiveURL = "resource://android/assets/web_extensions/borderify-unsigned.zip", extensionName = "Borderify", ) } @Test fun installUnsignedExtensionSignatureRequired() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to true, ), ) testInstallError( name = "borderify-unsigned.xpi", expectedError = InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED, expectedExtensionID = null, expectedExtension = false, ) } @Test fun installUnsignedExtensionSignatureRequiredAsZipFile() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to true, ), ) testInstallError( name = "borderify-unsigned.zip", expectedError = InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED, expectedExtensionID = null, expectedExtension = false, ) } @Test fun installExtensionFileNotFound() { testInstallError( name = "file-not-found.xpi", expectedError = InstallException.ErrorCodes.ERROR_NETWORK_FAILURE, expectedExtensionID = null, expectedExtension = false, ) } @Test fun installExtensionMissingId() { testInstallError( name = "borderify-missing-id.xpi", expectedError = InstallException.ErrorCodes.ERROR_CORRUPT_FILE, expectedExtensionID = null, expectedExtension = false, ) } @Test fun corruptFileErrorWillNotReturnAnWebExtensionWithoutId() { sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 0) override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) sessionRule.delegateDuringNextWait(object : WebExtensionController.AddonManagerDelegate { @AssertCalled(count = 1) override fun onInstallationFailed( extension: WebExtension?, installException: InstallException, ) { assertNull(extension) } }) sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify-missing-id.xpi", null, ) .accept({ // We should not be able to install extensions without an id. assertTrue(false) }, { exception -> val installException = exception as WebExtension.InstallException assertEquals(installException.code, InstallException.ErrorCodes.ERROR_CORRUPT_FILE) }), ) } @Test fun installExtensionIncompatible() { testInstallError( name = "dummy-incompatible.xpi", expectedError = InstallException.ErrorCodes.ERROR_INCOMPATIBLE, expectedExtensionID = "dummy@tests.mozilla.org", expectedExtension = true, ) } @Test fun installAddonUnsupportedType() { testInstallError( name = "langpack_signed.xpi", expectedError = InstallException.ErrorCodes.ERROR_UNSUPPORTED_ADDON_TYPE, expectedExtensionID = "langpack-klingon@firefox.mozilla.org", expectedExtension = true, ) } @Test fun installDeny() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // Ensure border is empty to start. assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 1) override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.deny() } }) sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ).accept({ // We should not be able to install the extension. assertTrue(false) }, { exception -> assertTrue(exception is WebExtension.InstallException) val installException = exception as WebExtension.InstallException assertEquals(installException.code, WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED) }), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not installed and the border is still empty. assertBodyBorderEqualTo("") } @Test fun createNotification() { sessionRule.delegateUntilTestEnd(object : WebNotificationDelegate { @AssertCalled override fun onShowNotification(notification: WebNotification) { } }) val extension = sessionRule.waitForResult( controller.installBuiltIn("resource://android/assets/web_extensions/notification-test/"), ) sessionRule.waitUntilCalled(object : WebNotificationDelegate { @AssertCalled(count = 1) override fun onShowNotification(notification: WebNotification) { assertEquals(notification.title, "Time for cake!") assertEquals(notification.text, "Something something cake") assertEquals(notification.imageUrl, "https://example.com/img.svg") // This should be filled out, Bug 1589693 assertEquals(notification.source, null) } }) sessionRule.waitForResult( controller.uninstall(extension), ) } // This test // - Registers a web extension // - Listens for messages and waits for a message // - Sends a response to the message and waits for a second message // - Verify that the second message has the correct value // // When `background == true` the test will be run using background messaging, otherwise the // test will use content script messaging. private fun testOnMessage(background: Boolean) { val messageResult = GeckoResult() val prefix = if (background) "testBackground" else "testContent" val messageDelegate = object : WebExtension.MessageDelegate { var awaitingResponse = false var completed = false override fun onConnect(port: WebExtension.Port) { // Ignored for this test } override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { checkSender(nativeApp, sender, background) if (!awaitingResponse) { assertThat( "We should receive a message from the WebExtension", message as String, equalTo("${prefix}BrowserMessage"), ) awaitingResponse = true return GeckoResult.fromValue("${prefix}MessageResponse") } else if (!completed) { assertThat( "The background script should receive our message and respond", message as String, equalTo("response: ${prefix}MessageResponse"), ) messageResult.complete(null) completed = true } return null } } val messaging = installWebExtension(background, messageDelegate) sessionRule.waitForResult(messageResult) sessionRule.waitForResult(controller.uninstall(messaging)) } // This test // - Listen for a new tab request from a web extension // - Registers a web extension // - Waits for onNewTab request // - Verify that request came from right extension @Test fun testBrowserTabsCreate() { val tabsCreateResult = GeckoResult() var tabsExtension: WebExtension? = null val tabDelegate = object : WebExtension.TabDelegate { override fun onNewTab(source: WebExtension, details: WebExtension.CreateTabDetails): GeckoResult { assertEquals(details.url, "https://www.mozilla.org/en-US/") assertEquals(details.active, true) assertEquals(tabsExtension!!, source) tabsCreateResult.complete(null) return GeckoResult.fromValue(null) } } tabsExtension = sessionRule.waitForResult(controller.installBuiltIn(TABS_CREATE_BACKGROUND)) tabsExtension.setTabDelegate(tabDelegate) sessionRule.waitForResult(tabsCreateResult) sessionRule.waitForResult(controller.uninstall(tabsExtension)) } // This test // - Listen for a new tab request from a web extension // - Registers a web extension // - Extension requests creation of new tab with a cookie store id. // - Waits for onNewTab request // - Verify that request came from right extension @Test fun testBrowserTabsCreateWithCookieStoreId() { sessionRule.setPrefsUntilTestEnd(mapOf("privacy.userContext.enabled" to true)) val tabsCreateResult = GeckoResult() var tabsExtension: WebExtension? = null val tabDelegate = object : WebExtension.TabDelegate { override fun onNewTab(source: WebExtension, details: WebExtension.CreateTabDetails): GeckoResult { assertEquals(details.url, "https://www.mozilla.org/en-US/") assertEquals(details.active, true) assertEquals(details.cookieStoreId, "1") assertEquals(tabsExtension!!.id, source.id) tabsCreateResult.complete(null) return GeckoResult.fromValue(null) } } tabsExtension = sessionRule.waitForResult(controller.installBuiltIn(TABS_CREATE_2_BACKGROUND)) tabsExtension.setTabDelegate(tabDelegate) sessionRule.waitForResult(tabsCreateResult) sessionRule.waitForResult(controller.uninstall(tabsExtension)) } // This test // - Create and assign WebExtension TabDelegate to handle creation and closing of tabs // - Registers a WebExtension // - Extension requests creation of new tab // - TabDelegate handles creation of new tab // - Extension requests removal of newly created tab // - TabDelegate handles closing of newly created tab // - Verify that close request came from right extension and targeted session @Test fun testBrowserTabsCreateBrowserTabsRemove() { val onCloseRequestResult = GeckoResult() val tabsExtension = sessionRule.waitForResult( controller.installBuiltIn(TABS_CREATE_REMOVE_BACKGROUND), ) tabsExtension.tabDelegate = object : WebExtension.TabDelegate { override fun onNewTab(source: WebExtension, details: WebExtension.CreateTabDetails): GeckoResult { val extensionCreatedSession = sessionRule.createClosedSession(mainSession.settings) extensionCreatedSession.webExtensionController.setTabDelegate( tabsExtension, object : WebExtension.SessionTabDelegate { override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult { assertEquals(tabsExtension.id, source!!.id) assertEquals(details.active, true) assertNotEquals(null, extensionCreatedSession) assertEquals(extensionCreatedSession, session) onCloseRequestResult.complete(null) return GeckoResult.allow() } }, ) return GeckoResult.fromValue(extensionCreatedSession) } } sessionRule.waitForResult(onCloseRequestResult) sessionRule.waitForResult(controller.uninstall(tabsExtension)) } // This test // - Create and assign WebExtension TabDelegate to handle creation and closing of tabs // - Create and opens a new GeckoSession // - Set the main session as active tab // - Registers a WebExtension // - Extension listens for activated tab changes // - Set the main session as inactive tab // - Set the newly created GeckoSession as active tab // - Extension requests removal of newly created tab if tabs.query({active: true}) // contains only the newly activated tab // - TabDelegate handles closing of newly created tab // - Verify that close request came from right extension and targeted session @Test fun testSetTabActive() { val onCloseRequestResult = GeckoResult() val tabsExtension = sessionRule.waitForResult( controller.installBuiltIn(TABS_ACTIVATE_REMOVE_BACKGROUND), ) val newTabSession = sessionRule.createOpenSession(mainSession.settings) sessionRule.addExternalDelegateUntilTestEnd( WebExtension.SessionTabDelegate::class, { delegate -> newTabSession.webExtensionController.setTabDelegate(tabsExtension, delegate) }, { newTabSession.webExtensionController.setTabDelegate(tabsExtension, null) }, object : WebExtension.SessionTabDelegate { override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult { assertEquals(tabsExtension, source) assertEquals(newTabSession, session) onCloseRequestResult.complete(null) return GeckoResult.allow() } }, ) controller.setTabActive(mainSession, false) controller.setTabActive(newTabSession, true) sessionRule.waitForResult(onCloseRequestResult) sessionRule.waitForResult(controller.uninstall(tabsExtension)) } private fun browsingDataMessage( port: WebExtension.Port, type: String, since: Long? = null, ): GeckoResult { val message = JSONObject( "{" + "\"type\": \"$type\"" + "}", ) if (since != null) { message.put("since", since) } return browsingDataCall(port, message) } private fun browsingDataCall( port: WebExtension.Port, json: JSONObject, ): GeckoResult { val uuid = UUID.randomUUID().toString() json.put("uuid", uuid) port.postMessage(json) val response = GeckoResult() port.setDelegate(object : WebExtension.PortDelegate { override fun onPortMessage(message: Any, port: WebExtension.Port) { assertThat( "Response ID Matches.", (message as JSONObject).getString("uuid"), equalTo(uuid), ) response.complete(message) } }) return response } @Test fun testBrowsingDataDelegateBuiltIn() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) val extension = sessionRule.waitForResult( controller.installBuiltIn(BROWSING_DATA), ) val portResult = GeckoResult() extension.setMessageDelegate( object : WebExtension.MessageDelegate { override fun onConnect(port: WebExtension.Port) { portResult.complete(port) } }, "browser", ) val TEST_SINCE_VALUE = 59294 sessionRule.addExternalDelegateUntilTestEnd( WebExtension.BrowsingDataDelegate::class, { delegate -> extension.browsingDataDelegate = delegate }, { extension.browsingDataDelegate = null }, object : WebExtension.BrowsingDataDelegate { override fun onGetSettings(): GeckoResult? { return GeckoResult.fromValue( WebExtension.BrowsingDataDelegate.Settings( TEST_SINCE_VALUE, CACHE or COOKIES or DOWNLOADS or HISTORY or LOCAL_STORAGE, CACHE or COOKIES or HISTORY, ), ) } }, ) val port = sessionRule.waitForResult(portResult) // Test browsingData.removeDownloads sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearDownloads(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(1234L), ) return null } }) sessionRule.waitForResult(browsingDataMessage(port, "clear-downloads", 1234)) // Test browsingData.removeFormData sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearFormData(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(1234L), ) return null } }) sessionRule.waitForResult(browsingDataMessage(port, "clear-form-data", 1234)) // Test browsingData.removeHistory sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(1234L), ) return null } }) sessionRule.waitForResult(browsingDataMessage(port, "clear-history", 1234)) // Test browsingData.removePasswords sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(1234L), ) return null } }) sessionRule.waitForResult(browsingDataMessage(port, "clear-passwords", 1234)) // Test browsingData.remove({ indexedDB: true, localStorage: true, passwords: true }) sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return null } }) var response = sessionRule.waitForResult( browsingDataCall( port, JSONObject( "{" + "\"type\": \"clear\"," + "\"removalOptions\": {}," + "\"dataTypes\": {\"indexedDB\": true, \"localStorage\": true, \"passwords\": true}" + "}", ), ), ) assertThat( "browsingData.remove should succeed", response.getString("type"), equalTo("response"), ) // Test browsingData.remove({ indexedDB: true, history: true, passwords: true }) sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return null } @AssertCalled override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return null } }) response = sessionRule.waitForResult( browsingDataCall( port, JSONObject( "{" + "\"type\": \"clear\"," + "\"removalOptions\": {}," + "\"dataTypes\": {\"indexedDB\": true, \"history\": true, \"passwords\": true}" + "}", ), ), ) assertThat( "browsingData.remove should succeed", response.getString("type"), equalTo("response"), ) // Test browsingData.remove({ indexedDB: true, history: true, passwords: true }) // with failure sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return null } @AssertCalled override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return GeckoResult.fromException(RuntimeException("Not authorized.")) } }) response = sessionRule.waitForResult( browsingDataCall( port, JSONObject( "{" + "\"type\": \"clear\"," + "\"removalOptions\": {}," + "\"dataTypes\": {\"indexedDB\": true, \"history\": true, \"passwords\": true}" + "}", ), ), ) assertThat( "browsingData.remove returns expected error.", response.getString("error"), equalTo("Not authorized."), ) // Test browsingData.remove({ indexedDB: true, history: true, passwords: true }) // with multiple failures sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { @AssertCalled override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return GeckoResult.fromException(RuntimeException("Not authorized passwords.")) } @AssertCalled override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult? { assertThat( "timestamp should match", sinceUnixTimestamp, equalTo(0L), ) return GeckoResult.fromException(RuntimeException("Not authorized history.")) } }) response = sessionRule.waitForResult( browsingDataCall( port, JSONObject( "{" + "\"type\": \"clear\"," + "\"removalOptions\": {}," + "\"dataTypes\": {\"indexedDB\": true, \"history\": true, \"passwords\": true}" + "}", ), ), ) val error = response.getString("error") assertThat( "browsingData.remove returns expected error.", error == "Not authorized passwords." || error == "Not authorized history.", equalTo(true), ) // Test browsingData.settings() response = sessionRule.waitForResult( browsingDataMessage(port, "get-settings"), ) val settings = response.getJSONObject("result") val dataToRemove = settings.getJSONObject("dataToRemove") val options = settings.getJSONObject("options") assertThat( "Since should be correct", options.getInt("since"), equalTo(TEST_SINCE_VALUE), ) for (key in listOf("cache", "cookies", "history")) { assertThat( "Data to remove should be correct", dataToRemove.getBoolean(key), equalTo(true), ) } for (key in listOf("downloads", "localStorage")) { assertThat( "Data to remove should be correct", dataToRemove.getBoolean(key), equalTo(false), ) } val dataRemovalPermitted = settings.getJSONObject("dataRemovalPermitted") for (key in listOf("cache", "cookies", "downloads", "history", "localStorage")) { assertThat( "Data removal permitted should be correct", dataRemovalPermitted.getBoolean(key), equalTo(true), ) } // Test browsingData.settings() with no delegate sessionRule.delegateDuringNextWait(object : WebExtension.BrowsingDataDelegate { override fun onGetSettings(): GeckoResult? { return null } }) response = sessionRule.waitForResult( browsingDataMessage(port, "get-settings"), ) assertThat( "browsingData.settings returns expected error.", response.getString("error"), equalTo("browsingData.settings is not supported"), ) sessionRule.waitForResult(controller.uninstall(extension)) } @Test fun testBrowsingDataDelegate() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val extension = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/browsing-data.xpi", null), ) val accumulator = mutableListOf() val result = GeckoResult>() extension.browsingDataDelegate = object : WebExtension.BrowsingDataDelegate { fun register(type: String, timestamp: Long) { accumulator.add("$type $timestamp") if (accumulator.size >= 5) { result.complete(accumulator) } } override fun onClearDownloads(sinceUnixTimestamp: Long): GeckoResult { register("downloads", sinceUnixTimestamp) return GeckoResult.fromValue(null) } override fun onClearFormData(sinceUnixTimestamp: Long): GeckoResult { register("formData", sinceUnixTimestamp) return GeckoResult.fromValue(null) } override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult { register("history", sinceUnixTimestamp) return GeckoResult.fromValue(null) } override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult { register("passwords", sinceUnixTimestamp) return GeckoResult.fromValue(null) } } val actual = sessionRule.waitForResult(result) assertThat( "Delegate methods get called in the right order", actual, equalTo( listOf( "downloads 10001", "formData 10002", "history 10003", "passwords 10004", "downloads 10005", ), ), ) sessionRule.waitForResult(controller.uninstall(extension)) } // Same as testSetTabActive when the extension is not allowed in private browsing @Test fun testSetTabActiveNotAllowedInPrivateBrowsing() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) val onCloseRequestResult = GeckoResult() sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val tabsExtension = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/tabs-activate-remove.xpi", null), ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) var tabsExtensionPB = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/tabs-activate-remove-2.xpi", null), ) tabsExtensionPB = sessionRule.waitForResult( controller.setAllowedInPrivateBrowsing(tabsExtensionPB, true), ) val newTabSession = sessionRule.createOpenSession(mainSession.settings) val newPrivateSession = sessionRule.createOpenSession( GeckoSessionSettings.Builder().usePrivateMode(true).build(), ) val privateBrowsingNewTabSession = GeckoResult() class TabDelegate( val result: GeckoResult, val extension: WebExtension, val expectedSession: GeckoSession, ) : WebExtension.SessionTabDelegate { override fun onCloseTab( source: WebExtension?, session: GeckoSession, ): GeckoResult { assertEquals(extension.id, source!!.id) assertEquals(expectedSession, session) result.complete(null) return GeckoResult.allow() } } newTabSession.webExtensionController.setTabDelegate( tabsExtensionPB, TabDelegate(privateBrowsingNewTabSession, tabsExtensionPB, newTabSession), ) newTabSession.webExtensionController.setTabDelegate( tabsExtension, TabDelegate(onCloseRequestResult, tabsExtension, newTabSession), ) val privateBrowsingPrivateSession = GeckoResult() newPrivateSession.webExtensionController.setTabDelegate( tabsExtensionPB, TabDelegate(privateBrowsingPrivateSession, tabsExtensionPB, newPrivateSession), ) // tabsExtension is not allowed in private browsing and shouldn't get this event newPrivateSession.webExtensionController.setTabDelegate( tabsExtension, object : WebExtension.SessionTabDelegate { override fun onCloseTab( source: WebExtension?, session: GeckoSession, ): GeckoResult { privateBrowsingPrivateSession.completeExceptionally( RuntimeException("Should never happen"), ) return GeckoResult.allow() } }, ) controller.setTabActive(mainSession, false) controller.setTabActive(newPrivateSession, true) sessionRule.waitForResult(privateBrowsingPrivateSession) controller.setTabActive(newPrivateSession, false) controller.setTabActive(newTabSession, true) sessionRule.waitForResult(onCloseRequestResult) sessionRule.waitForResult(privateBrowsingNewTabSession) sessionRule.waitForResult( sessionRule.runtime.webExtensionController.uninstall(tabsExtension), ) sessionRule.waitForResult( sessionRule.runtime.webExtensionController.uninstall(tabsExtensionPB), ) newTabSession.close() newPrivateSession.close() } // Verifies that the following messages are received from an extension page loaded in the session // - HELLO_FROM_PAGE_1 from nativeApp browser1 // - HELLO_FROM_PAGE_2 from nativeApp browser2 // - connection request from browser1 // - HELLO_FROM_PORT from the port opened at the above step private fun testExtensionMessages(extension: WebExtension, session: GeckoSession) { val messageResult2 = GeckoResult() session.webExtensionController.setMessageDelegate( extension, object : WebExtension.MessageDelegate { override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { messageResult2.complete(message as String) return null } }, "browser2", ) val message2 = sessionRule.waitForResult(messageResult2) assertThat( "Message is received correctly", message2, equalTo("HELLO_FROM_PAGE_2"), ) val messageResult1 = GeckoResult() val portResult = GeckoResult() session.webExtensionController.setMessageDelegate( extension, object : WebExtension.MessageDelegate { override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { messageResult1.complete(message as String) return null } override fun onConnect(port: WebExtension.Port) { portResult.complete(port) } }, "browser1", ) val message1 = sessionRule.waitForResult(messageResult1) assertThat( "Message is received correctly", message1, equalTo("HELLO_FROM_PAGE_1"), ) val port = sessionRule.waitForResult(portResult) val portMessageResult = GeckoResult() port.setDelegate(object : WebExtension.PortDelegate { override fun onPortMessage(message: Any, port: WebExtension.Port) { portMessageResult.complete(message as String) } }) val portMessage = sessionRule.waitForResult(portMessageResult) assertThat( "Message is received correctly", portMessage, equalTo("HELLO_FROM_PORT"), ) } // This test: // - loads an extension that tries to send some messages when loading tab.html // - verifies that the messages are received when loading the tab normally // - verifies that the messages are received when restoring the tab in a fresh session @Test fun testRestoringExtensionPagePreservesMessages() { // TODO: Bug 1837551 assumeThat(sessionRule.env.isFission, equalTo(false)) val extension = sessionRule.waitForResult( controller.installBuiltIn(EXTENSION_PAGE_RESTORE), ) mainSession.loadUri("${extension.metaData.baseUrl}tab.html") sessionRule.waitForPageStop() var savedState: GeckoSession.SessionState? = null sessionRule.waitUntilCalled(object : ProgressDelegate { @AssertCalled(count = 1) override fun onSessionStateChange(session: GeckoSession, state: GeckoSession.SessionState) { savedState = state } }) // Test that messages are received in the main session testExtensionMessages(extension, mainSession) val newSession = sessionRule.createOpenSession() newSession.restoreState(savedState!!) newSession.waitForPageStop() // Test that messages are received in a restored state testExtensionMessages(extension, newSession) sessionRule.waitForResult(controller.uninstall(extension)) } // This test // - Create and assign WebExtension TabDelegate to handle closing of tabs // - Create new GeckoSession for WebExtension to close // - Load url that will allow extension to identify the tab // - Registers a WebExtension // - Extension finds the tab by url and removes it // - TabDelegate handles closing of the tab // - Verify that request targets previously created GeckoSession @Test fun testBrowserTabsRemove() { val onCloseRequestResult = GeckoResult() val existingSession = sessionRule.createOpenSession() existingSession.loadTestPath("$HELLO_HTML_PATH?tabToClose") existingSession.waitForPageStop() val tabsExtension = sessionRule.waitForResult( controller.installBuiltIn(TABS_REMOVE_BACKGROUND), ) sessionRule.addExternalDelegateUntilTestEnd( WebExtension.SessionTabDelegate::class, { delegate -> existingSession.webExtensionController.setTabDelegate(tabsExtension, delegate) }, { existingSession.webExtensionController.setTabDelegate(tabsExtension, null) }, object : WebExtension.SessionTabDelegate { override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult { assertEquals(existingSession, session) onCloseRequestResult.complete(null) return GeckoResult.allow() } }, ) sessionRule.waitForResult(onCloseRequestResult) sessionRule.waitForResult(controller.uninstall(tabsExtension)) } private fun installWebExtension( background: Boolean, messageDelegate: WebExtension.MessageDelegate, ): WebExtension { val webExtension: WebExtension if (background) { webExtension = sessionRule.waitForResult( controller.installBuiltIn(MESSAGING_BACKGROUND), ) webExtension.setMessageDelegate(messageDelegate, "browser") } else { webExtension = sessionRule.waitForResult( controller.installBuiltIn(MESSAGING_CONTENT), ) mainSession.webExtensionController .setMessageDelegate(webExtension, messageDelegate, "browser") } return webExtension } @Test fun contentMessaging() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() testOnMessage(false) } @Test fun backgroundMessaging() { testOnMessage(true) } // This test // - installs a web extension // - waits for the web extension to connect to the browser // - on connect it will start listening on the port for a message // - When the message is received it sends a message in response and waits for another message // - When the second message is received it verifies it contains the expected value // // When `background == true` the test will be run using background messaging, otherwise the // test will use content script messaging. private fun testPortMessage(background: Boolean) { val result = GeckoResult() val prefix = if (background) "testBackground" else "testContent" val portDelegate = object : WebExtension.PortDelegate { var awaitingResponse = false override fun onPortMessage(message: Any, port: WebExtension.Port) { assertEquals(port.name, "browser") if (!awaitingResponse) { assertThat( "We should receive a message from the WebExtension", message as String, equalTo("${prefix}PortMessage"), ) port.postMessage(JSONObject("{\"message\": \"${prefix}PortMessageResponse\"}")) awaitingResponse = true } else { assertThat( "The background script should receive our message and respond", message as String, equalTo("response: ${prefix}PortMessageResponse"), ) result.complete(null) } } override fun onDisconnect(port: WebExtension.Port) { // ignored } } val messageDelegate = object : WebExtension.MessageDelegate { override fun onConnect(port: WebExtension.Port) { checkSender(port.name, port.sender, background) assertEquals(port.name, "browser") port.setDelegate(portDelegate) } override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { // Ignored for this test return null } } val messaging = installWebExtension(background, messageDelegate) sessionRule.waitForResult(result) sessionRule.waitForResult(controller.uninstall(messaging)) } @Test fun contentPortMessaging() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() testPortMessage(false) } @Test fun backgroundPortMessaging() { testPortMessage(true) } // This test // - Registers a web extension // - Awaits for the web extension to connect to the browser // - When connected, it triggers a disconnection from the other side and verifies that // the browser is notified of the port being disconnected. // // When `background == true` the test will be run using background messaging, otherwise the // test will use content script messaging. // // When `refresh == true` the disconnection will be triggered by refreshing the page, otherwise // it will be triggered by sending a message to the web extension. private fun testPortDisconnect(background: Boolean, refresh: Boolean) { val result = GeckoResult() var messaging: WebExtension? = null var messagingPort: WebExtension.Port? = null val portDelegate = object : WebExtension.PortDelegate { override fun onPortMessage( message: Any, port: WebExtension.Port, ) { assertEquals(port, messagingPort) } override fun onDisconnect(port: WebExtension.Port) { assertEquals(messaging!!.id, port.sender.webExtension.id) assertEquals(port, messagingPort) // We successfully received a disconnection result.complete(null) } } val messageDelegate = object : WebExtension.MessageDelegate { override fun onConnect(port: WebExtension.Port) { assertEquals(messaging!!.id, port.sender.webExtension.id) checkSender(port.name, port.sender, background) assertEquals(port.name, "browser") messagingPort = port port.setDelegate(portDelegate) if (refresh) { // Refreshing the page should disconnect the port mainSession.reload() } else { // Let's ask the web extension to disconnect this port val message = JSONObject() message.put("action", "disconnect") port.postMessage(message) } } override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { assertEquals(messaging!!.id, sender.webExtension.id) // Ignored for this test return null } } messaging = installWebExtension(background, messageDelegate) sessionRule.waitForResult(result) sessionRule.waitForResult(controller.uninstall(messaging)) } @Test fun contentPortDisconnect() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() testPortDisconnect(background = false, refresh = false) } @Test fun backgroundPortDisconnect() { testPortDisconnect(background = true, refresh = false) } @Test fun contentPortDisconnectAfterRefresh() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() testPortDisconnect(background = false, refresh = true) } fun checkSender(nativeApp: String, sender: WebExtension.MessageSender, background: Boolean) { assertEquals("nativeApp should always be 'browser'", nativeApp, "browser") if (background) { // For background scripts we only want messages from the extension, this should never // happen and it's a bug if we get here. assertEquals( "Called from content script with background-only delegate.", sender.environmentType, WebExtension.MessageSender.ENV_TYPE_EXTENSION, ) assertTrue( "Unexpected sender url", sender.url.endsWith("/_generated_background_page.html"), ) } else { assertEquals( "Called from background script, expecting only content scripts", sender.environmentType, WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT, ) assertTrue("Expecting only top level senders.", sender.isTopLevel) assertEquals("Unexpected sender url", sender.url, "https://example.com/") } } // This test // - Register a web extension and waits for connections // - When connected it disconnects the port from the app side // - Awaits for a message from the web extension confirming the web extension was notified of // port being closed. // // When `background == true` the test will be run using background messaging, otherwise the // test will use content script messaging. private fun testPortDisconnectFromApp(background: Boolean) { val result = GeckoResult() var messaging: WebExtension? = null val messageDelegate = object : WebExtension.MessageDelegate { override fun onConnect(port: WebExtension.Port) { assertEquals(messaging!!.id, port.sender.webExtension.id) checkSender(port.name, port.sender, background) port.disconnect() } override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { assertEquals(messaging!!.id, sender.webExtension.id) checkSender(nativeApp, sender, background) if (message is JSONObject) { if (message.getString("type") == "portDisconnected") { result.complete(null) } } return null } } messaging = installWebExtension(background, messageDelegate) sessionRule.waitForResult(result) sessionRule.waitForResult(controller.uninstall(messaging)) } @Test fun contentPortDisconnectFromApp() { mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() testPortDisconnectFromApp(false) } @Test fun backgroundPortDisconnectFromApp() { testPortDisconnectFromApp(true) } // This test checks that scripts running in a iframe have the `isTopLevel` property set to false. private fun testIframeTopLevel() { val portTopLevel = GeckoResult() val portIframe = GeckoResult() val messageTopLevel = GeckoResult() val messageIframe = GeckoResult() var messaging: WebExtension? = null val messageDelegate = object : WebExtension.MessageDelegate { override fun onConnect(port: WebExtension.Port) { assertEquals(messaging!!.id, port.sender.webExtension.id) assertEquals( WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT, port.sender.environmentType, ) when (port.sender.url) { "$TEST_ENDPOINT$HELLO_IFRAME_HTML_PATH" -> { assertTrue(port.sender.isTopLevel) portTopLevel.complete(null) } "$TEST_ENDPOINT$HELLO_HTML_PATH" -> { assertFalse(port.sender.isTopLevel) portIframe.complete(null) } else -> // We shouldn't get other messages fail() } port.disconnect() } override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { assertEquals(messaging!!.id, sender.webExtension.id) assertEquals( WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT, sender.environmentType, ) when (sender.url) { "$TEST_ENDPOINT$HELLO_IFRAME_HTML_PATH" -> { assertTrue(sender.isTopLevel) messageTopLevel.complete(null) } "$TEST_ENDPOINT$HELLO_HTML_PATH" -> { assertFalse(sender.isTopLevel) messageIframe.complete(null) } else -> // We shouldn't get other messages fail() } return null } } messaging = sessionRule.waitForResult( controller.installBuiltIn( "resource://android/assets/web_extensions/messaging-iframe/", ), ) mainSession.webExtensionController .setMessageDelegate(messaging, messageDelegate, "browser") sessionRule.waitForResult(portTopLevel) sessionRule.waitForResult(portIframe) sessionRule.waitForResult(messageTopLevel) sessionRule.waitForResult(messageIframe) sessionRule.waitForResult(controller.uninstall(messaging)) } @Test fun iframeTopLevel() { mainSession.loadTestPath(HELLO_IFRAME_HTML_PATH) sessionRule.waitForPageStop() testIframeTopLevel() } @Test fun redirectToExtensionResource() { val result = GeckoResult() val messageDelegate = object : WebExtension.MessageDelegate { override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { assertEquals(message, "setupReadyStartTest") result.complete(null) return null } } val extension = sessionRule.waitForResult( controller.installBuiltIn( "resource://android/assets/web_extensions/redirect-to-android-resource/", ), ) extension.setMessageDelegate(messageDelegate, "browser") sessionRule.waitForResult(result) // Extension has set up some webRequest listeners to redirect requests. // Open the test page and verify that the extension has redirected the // scripts as expected. mainSession.loadTestPath(TRACKERS_PATH) sessionRule.waitForPageStop() val textContent = mainSession.evaluateJS("document.body.textContent.replace(/\\s/g, '')") assertThat( "The extension should have rewritten the script requests and the body", textContent as String, equalTo("start,extension-was-here,end"), ) sessionRule.waitForResult(controller.uninstall(extension)) } @Test fun loadWebExtensionPage() { val result = GeckoResult() var extension: WebExtension? = null val messageDelegate = object : WebExtension.MessageDelegate { override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { assertEquals(extension!!.id, sender.webExtension.id) assertEquals( WebExtension.MessageSender.ENV_TYPE_EXTENSION, sender.environmentType, ) result.complete(message as String) return null } } extension = sessionRule.waitForResult( controller.ensureBuiltIn( "resource://android/assets/web_extensions/extension-page-update/", "extension-page-update@tests.mozilla.org", ), ) val sessionController = mainSession.webExtensionController sessionController.setMessageDelegate(extension, messageDelegate, "browser") sessionController.setTabDelegate( extension, object : WebExtension.SessionTabDelegate { override fun onUpdateTab( extension: WebExtension, session: GeckoSession, details: WebExtension.UpdateTabDetails, ): GeckoResult { return GeckoResult.allow() } }, ) mainSession.loadUri("https://example.com") mainSession.waitUntilCalled(object : NavigationDelegate, ProgressDelegate { @GeckoSessionTestRule.AssertCalled(count = 1) override fun onLocationChange(session: GeckoSession, url: String?, perms: MutableList) { assertThat( "Url should load example.com first", url, equalTo("https://example.com/"), ) } @GeckoSessionTestRule.AssertCalled(count = 1) override fun onPageStop(session: GeckoSession, success: Boolean) { assertThat( "Page should load successfully.", success, equalTo(true), ) } }) var page: String? = null val pageStop = GeckoResult() mainSession.delegateUntilTestEnd(object : NavigationDelegate, ProgressDelegate { override fun onLocationChange(session: GeckoSession, url: String?, perms: MutableList) { page = url } override fun onPageStop(session: GeckoSession, success: Boolean) { if (success && page != null && page!!.endsWith("/tab.html")) { pageStop.complete(true) } } }) // If ensureBuiltIn works correctly, this will not re-install the extension. // We can verify that it won't reinstall because that would cause the extension page to // close prematurely, making the test fail. val ensure = sessionRule.waitForResult( controller.ensureBuiltIn( "resource://android/assets/web_extensions/extension-page-update/", "extension-page-update@tests.mozilla.org", ), ) assertThat("ID match", ensure.id, equalTo(extension.id)) assertThat("version match", ensure.metaData.version, equalTo(extension.metaData.version)) // Make sure the page loaded successfully sessionRule.waitForResult(pageStop) assertThat("Url should load WebExtension page", page, endsWith("/tab.html")) assertThat( "WebExtension page should have access to privileged APIs", sessionRule.waitForResult(result), equalTo("HELLO_FROM_PAGE"), ) // Test that after uninstalling an extension, all its pages get closed sessionRule.addExternalDelegateUntilTestEnd( WebExtension.SessionTabDelegate::class, { delegate -> mainSession.webExtensionController.setTabDelegate(extension, delegate) }, { mainSession.webExtensionController.setTabDelegate(extension, null) }, object : WebExtension.SessionTabDelegate {}, ) val uninstall = controller.uninstall(extension) sessionRule.waitUntilCalled(object : WebExtension.SessionTabDelegate { @AssertCalled override fun onCloseTab( source: WebExtension?, session: GeckoSession, ): GeckoResult { assertEquals(extension.id, source!!.id) assertEquals(mainSession, session) return GeckoResult.allow() } }) sessionRule.waitForResult(uninstall) } @Test fun badUrl() { testInstallBuiltInError("invalid url", "Could not parse uri") } @Test fun badHost() { testInstallBuiltInError("resource://gre/", "Only resource://android") } @Test fun dontAllowRemoteUris() { testInstallBuiltInError("https://example.com/extension/", "Only resource://android") } @Test fun badFileType() { testInstallBuiltInError( "resource://android/bad/location/error", "does not point to a folder", ) } @Test fun badLocationXpi() { testInstallBuiltInError( "resource://android/bad/location/error.xpi", "does not point to a folder", ) } @Test fun testInstallBuiltInError() { testInstallBuiltInError( "resource://android/bad/location/error/", "does not contain a valid manifest", ) } private fun testInstallBuiltInError(location: String, expectedError: String) { try { sessionRule.waitForResult(controller.installBuiltIn(location)) } catch (ex: Exception) { // Let's make sure the error message contains the expected error message assertTrue(ex.message!!.contains(expectedError)) return } fail("The above code should throw.") } // Test web extension permission.request. @WithDisplay(width = 100, height = 100) @Test fun permissionRequest() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) val extension = sessionRule.waitForResult( controller.ensureBuiltIn( "resource://android/assets/web_extensions/permission-request/", "permissions@example.com", ), ) mainSession.loadUri("${extension.metaData.baseUrl}clickToRequestPermission.html") sessionRule.waitForPageStop() // click triggers permissions.request mainSession.synthesizeTap(50, 50) sessionRule.delegateUntilTestEnd(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 2) override fun onOptionalPrompt(extension: WebExtension, permissions: Array, origins: Array): GeckoResult { val expected = arrayOf("geolocation") assertThat("Permissions should match the requested permissions", permissions, equalTo(expected)) assertThat("Origins should match the requested origins", origins, equalTo(arrayOf("*://example.com/*"))) return forEachCall(GeckoResult.deny(), GeckoResult.allow()) } }) var result = GeckoResult() mainSession.webExtensionController.setMessageDelegate( extension, object : WebExtension.MessageDelegate { override fun onMessage( nativeApp: String, message: Any, sender: WebExtension.MessageSender, ): GeckoResult? { result.complete(message as String) return null } }, "browser", ) val message = sessionRule.waitForResult(result) assertThat("Permission request should first be denied.", message, equalTo("false")) mainSession.synthesizeTap(50, 50) result = GeckoResult() val message2 = sessionRule.waitForResult(result) assertThat("Permission request should be accepted.", message2, equalTo("true")) mainSession.synthesizeTap(50, 50) result = GeckoResult() val message3 = sessionRule.waitForResult(result) assertThat("Permission request should already be accepted.", message3, equalTo("true")) sessionRule.waitForResult(controller.uninstall(extension)) } // Test the basic update extension flow with no new permissions. @Test fun update() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.update.enabled" to true, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, "extensions.getAddons.cache.enabled" to true, "extensions.getAddons.cache.lastUpdate" to 0, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "1.0") return GeckoResult.allow() } }) val update1 = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/update-1.xpi", null), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("red") val update2 = sessionRule.waitForResult(controller.update(update1)) assertEquals(update2.metaData.version, "2.0") mainSession.reload() sessionRule.waitForPageStop() // Check that updated extension changed the border color. assertBodyBorderEqualTo("blue") // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(update2)) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not applied after being uninstalled assertBodyBorderEqualTo("") // This pref should have been updated because we expect the cached // metadata to have been refreshed. val geckoPrefs = sessionRule.getPrefs( "extensions.getAddons.cache.lastUpdate", ) assumeThat(geckoPrefs[0] as Int, greaterThan(0)) } @Test fun updateDisabled() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, // This is the important change here: "extensions.update.enabled" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 1) override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "1.0") return GeckoResult.allow() } }) // Install an extension that can be updated. val update1 = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/update-1.xpi", null), ) // Attempt to update the extension, which should not be possible since // we set the pref to `false` above. val update2 = sessionRule.waitForResult(controller.update(update1)) assertNull(update2) // Cleanup. sessionRule.waitForResult(controller.uninstall(update1)) } @Test fun updateWithMetadataNotStale() { val now = (System.currentTimeMillis() / 1000).toInt() sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.update.enabled" to true, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, "extensions.getAddons.cache.enabled" to true, "extensions.getAddons.cache.lastUpdate" to now, ), ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "1.0") return GeckoResult.allow() } }) // 1. Install val update1 = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/update-1.xpi", null), ) // 2. Update val update2 = sessionRule.waitForResult(controller.update(update1)) // 3. Uninstall sessionRule.waitForResult(controller.uninstall(update2)) // This pref should not have been updated because the cache isn't stale // (we set the pref to the current time at the top of this test case). val geckoPrefs = sessionRule.getPrefs( "extensions.getAddons.cache.lastUpdate", ) assumeThat(geckoPrefs[0] as Int, equalTo(now)) } // Test extension updating when the new extension has different permissions. @Test fun updateWithPerms() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.update.enabled" to true, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "1.0") return GeckoResult.allow() } }) val update1 = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/update-with-perms-1.xpi", null), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("red") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onUpdatePrompt( currentlyInstalled: WebExtension, updatedExtension: WebExtension, newPermissions: Array, newOrigins: Array, ): GeckoResult { assertEquals(currentlyInstalled.metaData.version, "1.0") assertEquals(updatedExtension.metaData.version, "2.0") assertEquals(newPermissions.size, 1) assertEquals(newPermissions[0], "tabs") return GeckoResult.allow() } }) val update2 = sessionRule.waitForResult(controller.update(update1)) assertEquals(update2.metaData.version, "2.0") mainSession.reload() sessionRule.waitForPageStop() // Check that updated extension changed the border color. assertBodyBorderEqualTo("blue") // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(update2)) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not applied after being uninstalled assertBodyBorderEqualTo("") } // Ensure update extension works as expected when there is no update available. @Test fun updateNotAvailable() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "2.0") return GeckoResult.allow() } }) val update1 = sessionRule.waitForResult( controller.install( "https://example.org/tests/junit/update-2.xpi", null, ), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("blue") val update2 = sessionRule.waitForResult(controller.update(update1)) assertNull(update2) // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(update1)) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not applied after being uninstalled assertBodyBorderEqualTo("") } // Test denying an extension update. @Test fun updateDenyPerms() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.update.enabled" to true, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "1.0") return GeckoResult.allow() } }) val update1 = sessionRule.waitForResult( controller.install("https://example.org/tests/junit/update-with-perms-1.xpi", null), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("red") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onUpdatePrompt( currentlyInstalled: WebExtension, updatedExtension: WebExtension, newPermissions: Array, newOrigins: Array, ): GeckoResult { assertEquals(currentlyInstalled.metaData.version, "1.0") assertEquals(updatedExtension.metaData.version, "2.0") return GeckoResult.deny() } }) sessionRule.waitForResult( controller.update(update1).accept({ // We should not be able to update the extension. assertTrue(false) }, { exception -> assertTrue(exception is WebExtension.InstallException) val installException = exception as WebExtension.InstallException assertEquals(installException.code, WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED) }), ) mainSession.reload() sessionRule.waitForPageStop() // Check that updated extension changed the border color. assertBodyBorderEqualTo("red") // Uninstall WebExtension and check again sessionRule.waitForResult(controller.uninstall(update1)) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was not applied after being uninstalled assertBodyBorderEqualTo("") } @Test(expected = CancellationException::class) fun cancelInstall() { val install = controller.install("$TEST_ENDPOINT/stall/test.xpi", null) val cancel = sessionRule.waitForResult(install.cancel()) assertTrue(cancel) sessionRule.waitForResult(install) } @Test fun cancelInstallFailsAfterInstalled() { sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val install = controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ) val borderify = sessionRule.waitForResult(install) val cancel = sessionRule.waitForResult(install.cancel()) assertFalse(cancel) sessionRule.waitForResult(controller.uninstall(borderify)) } @Test fun updatePostpone() { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.update.enabled" to true, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, "extensions.webextensions.warnings-as-errors" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // First let's check that the color of the border is empty before loading // the WebExtension assertBodyBorderEqualTo("") sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.version, "1.0") return GeckoResult.allow() } }) val update1 = sessionRule.waitForResult( controller.install( "https://example.org/tests/junit/update-postpone-1.xpi", null, ), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension was applied by checking the border color assertBodyBorderEqualTo("red") sessionRule.waitForResult( controller.update(update1).accept({ // We should not be able to update the extension. assertTrue(false) }, { exception -> assertTrue(exception is WebExtension.InstallException) val installException = exception as WebExtension.InstallException assertEquals(installException.code, WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED) }), ) mainSession.reload() sessionRule.waitForPageStop() // Check that the WebExtension is still the first extension. assertBodyBorderEqualTo("red") sessionRule.waitForResult(controller.uninstall(update1)) } /* This function installs a web extension, disables it, updates it and uninstalls it @param source: Int - represents a logical type; can be EnableSource.APP or EnableSource.USER */ private fun testUpdatingExtensionDisabledBy(source: Int) { sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.update.enabled" to true, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) sessionRule.addExternalDelegateUntilTestEnd( WebExtensionController.AddonManagerDelegate::class, { delegate -> controller.setAddonManagerDelegate(delegate) }, { controller.setAddonManagerDelegate(null) }, object : WebExtensionController.AddonManagerDelegate { @AssertCalled(count = 0) override fun onEnabling(extension: WebExtension) {} @AssertCalled(count = 0) override fun onEnabled(extension: WebExtension) {} @AssertCalled(count = 1) override fun onDisabling(extension: WebExtension) {} @AssertCalled(count = 1) override fun onDisabled(extension: WebExtension) {} @AssertCalled(count = 1) override fun onUninstalling(extension: WebExtension) {} @AssertCalled(count = 1) override fun onUninstalled(extension: WebExtension) {} // We expect onInstalling/onInstalled to be invoked twice // because we first install the extension and then we update // it, which results in a second install. @AssertCalled(count = 2) override fun onInstalling(extension: WebExtension) {} @AssertCalled(count = 2) override fun onInstalled(extension: WebExtension) {} }, ) val webExtension = sessionRule.waitForResult( controller.install( "https://example.org/tests/junit/update-1.xpi", null, ), ) mainSession.reload() sessionRule.waitForPageStop() val disabledWebExtension = sessionRule.waitForResult(controller.disable(webExtension, source)) when (source) { EnableSource.APP -> checkDisabledState(disabledWebExtension, appDisabled = true) EnableSource.USER -> checkDisabledState(disabledWebExtension, userDisabled = true) } val updatedWebExtension = sessionRule.waitForResult(controller.update(disabledWebExtension)) mainSession.reload() sessionRule.waitForPageStop() sessionRule.waitForResult(controller.uninstall(updatedWebExtension)) } @Test fun updateDisabledByUser() { testUpdatingExtensionDisabledBy(EnableSource.USER) } @Test fun updateDisabledByApp() { testUpdatingExtensionDisabledBy(EnableSource.APP) } // This test // - Listen for a newTab request from a web extension // - Registers a web extension // - Waits for onNewTab request // - Verify that request came from right extension @Test fun testBrowserRuntimeOpenOptionsPageInNewTab() { val tabsCreateResult = GeckoResult() var optionsExtension: WebExtension? = null val tabDelegate = object : WebExtension.TabDelegate { @AssertCalled(count = 1) override fun onNewTab( source: WebExtension, details: WebExtension.CreateTabDetails, ): GeckoResult { assertThat(details.url, endsWith("options.html")) assertEquals(details.active, true) assertEquals(optionsExtension!!.id, source.id) tabsCreateResult.complete(null) return GeckoResult.fromValue(null) } } optionsExtension = sessionRule.waitForResult( controller.installBuiltIn(OPENOPTIONSPAGE_1_BACKGROUND), ) optionsExtension.setTabDelegate(tabDelegate) sessionRule.waitForResult(tabsCreateResult) sessionRule.waitForResult(controller.uninstall(optionsExtension)) } // This test // - Listen for an openOptionsPage request from a web extension // - Registers a web extension // - Waits for onOpenOptionsPage request // - Verify that request came from right extension @Test fun testBrowserRuntimeOpenOptionsPageDelegate() { val openOptionsPageResult = GeckoResult() var optionsExtension: WebExtension? = null val tabDelegate = object : WebExtension.TabDelegate { @AssertCalled(count = 1) override fun onOpenOptionsPage(source: WebExtension) { assertThat( source.metaData.optionsPageUrl, endsWith("options.html"), ) assertEquals(optionsExtension!!.id, source.id) openOptionsPageResult.complete(null) } } optionsExtension = sessionRule.waitForResult( controller.installBuiltIn(OPENOPTIONSPAGE_2_BACKGROUND), ) optionsExtension.setTabDelegate(tabDelegate) sessionRule.waitForResult(openOptionsPageResult) sessionRule.waitForResult(controller.uninstall(optionsExtension)) } // This test checks if the request from Web Extension is processed correctly in Java // the Boolean flags are true, other options have non-default values @Test fun testDownloadsFlagsTrue() { val uri = createTestUrl("/assets/www/images/test.gif") sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val webExtension = sessionRule.waitForResult( controller.install( "https://example.org/tests/junit/download-flags-true.xpi", null, ), ) val assertOnDownloadCalled = GeckoResult() val downloadDelegate = object : DownloadDelegate { override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult? { assertEquals(webExtension!!.id, source.id) assertEquals(uri, request.request.uri) assertEquals("POST", request.request.method) request.request.body?.rewind() val result = Charset.forName("UTF-8").decode(request.request.body!!).toString() assertEquals("postbody", result) assertEquals("Mozilla Firefox", request.request.headers.get("User-Agent")) assertEquals("banana.gif", request.filename) assertTrue(request.allowHttpErrors) assertTrue(request.saveAs) assertEquals(GeckoWebExecutor.FETCH_FLAGS_PRIVATE, request.downloadFlags) assertEquals(DownloadRequest.CONFLICT_ACTION_OVERWRITE, request.conflictActionFlag) val download = controller.createDownload(1) assertOnDownloadCalled.complete(download) val downloadInfo = object : Download.Info {} val initialData = DownloadInitData(download, downloadInfo) return GeckoResult.fromValue(initialData) } } webExtension.setDownloadDelegate(downloadDelegate) mainSession.reload() sessionRule.waitForPageStop() try { sessionRule.waitForResult(assertOnDownloadCalled) } catch (exception: UiThreadUtils.TimeoutException) { controller.setAllowedInPrivateBrowsing(webExtension, true) val downloadCreated = sessionRule.waitForResult(assertOnDownloadCalled) assertNotNull(downloadCreated.id) sessionRule.waitForResult(controller.uninstall(webExtension)) } } // This test checks if the request from Web Extension is processed correctly in Java // the Boolean flags are absent/false, other options have default values @Test fun testDownloadsFlagsFalse() { val uri = createTestUrl("/assets/www/images/test.gif") sessionRule.setPrefsUntilTestEnd( mapOf( "xpinstall.signatures.required" to false, "extensions.install.requireBuiltInCerts" to false, "extensions.update.requireBuiltInCerts" to false, ), ) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { return GeckoResult.allow() } }) val webExtension = sessionRule.waitForResult( controller.install( "https://example.org/tests/junit/download-flags-false.xpi", null, ), ) val assertOnDownloadCalled = GeckoResult() val downloadDelegate = object : DownloadDelegate { override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult? { assertEquals(webExtension!!.id, source.id) assertEquals(uri, request.request.uri) assertEquals("GET", request.request.method) assertNull(request.request.body) assertEquals(0, request.request.headers.size) assertNull(request.filename) assertFalse(request.allowHttpErrors) assertFalse(request.saveAs) assertEquals(GeckoWebExecutor.FETCH_FLAGS_NONE, request.downloadFlags) assertEquals(DownloadRequest.CONFLICT_ACTION_UNIQUIFY, request.conflictActionFlag) val download = controller.createDownload(2) assertOnDownloadCalled.complete(download) val downloadInfo = object : Download.Info {} val initialData = DownloadInitData(download, downloadInfo) return GeckoResult.fromValue(initialData) } } webExtension.setDownloadDelegate(downloadDelegate) mainSession.reload() sessionRule.waitForPageStop() val downloadCreated = sessionRule.waitForResult(assertOnDownloadCalled) assertNotNull(downloadCreated.id) sessionRule.waitForResult(controller.uninstall(webExtension)) } @Test fun testOnChanged() { val uri = createTestUrl("/assets/www/images/test.gif") val downloadId = 4 val unfinishedDownloadSize = 5L val finishedDownloadSize = 25L val expectedFilename = "test.gif" val expectedMime = "image/gif" val expectedEndTime = Date().time val expectedFilesize = 48L // first and second update val downloadData = object : Download.Info { var endTime: Long? = null val startTime = Date().time - 50000 var fileExists = false var totalBytes: Long = -1 var mime = "" var fileSize: Long = -1 var filename = "" var state = Download.STATE_IN_PROGRESS override fun state(): Int { return state } override fun endTime(): Long? { return endTime } override fun startTime(): Long { return startTime } override fun fileExists(): Boolean { return fileExists } override fun totalBytes(): Long { return totalBytes } override fun mime(): String { return mime } override fun fileSize(): Long { return fileSize } override fun filename(): String { return filename } } val webExtension = sessionRule.waitForResult( controller.installBuiltIn("resource://android/assets/web_extensions/download-onChanged/"), ) val assertOnDownloadCalled = GeckoResult() val downloadDelegate = object : DownloadDelegate { override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult? { assertEquals(webExtension!!.id, source.id) assertEquals(uri, request.request.uri) val download = controller.createDownload(downloadId) assertOnDownloadCalled.complete(download) return GeckoResult.fromValue(DownloadInitData(download, downloadData)) } } val updates = mutableListOf() val thirdUpdateReceived = GeckoResult() val messageDelegate = object : MessageDelegate { override fun onMessage(nativeApp: String, message: Any, sender: MessageSender): GeckoResult? { val current = (message as JSONObject).getJSONObject("current") updates.add(message) // Once we get the size finished download, that means we got the last update if (current.getLong("totalBytes") == finishedDownloadSize) { thirdUpdateReceived.complete(message) } return GeckoResult.fromValue(message) } } webExtension.setDownloadDelegate(downloadDelegate) webExtension.setMessageDelegate(messageDelegate, "browser") mainSession.reload() sessionRule.waitForPageStop() val downloadCreated = sessionRule.waitForResult(assertOnDownloadCalled) assertEquals(downloadId, downloadCreated.id) // first and second update (they are identical) downloadData.filename = expectedFilename downloadData.mime = expectedMime downloadData.totalBytes = unfinishedDownloadSize downloadCreated.update(downloadData) downloadCreated.update(downloadData) downloadData.fileSize = expectedFilesize downloadData.endTime = expectedEndTime downloadData.totalBytes = finishedDownloadSize downloadData.state = Download.STATE_COMPLETE downloadCreated.update(downloadData) sessionRule.waitForResult(thirdUpdateReceived) // The second update should not be there because the data was identical assertEquals(2, updates.size) val firstUpdateCurrent = updates[0].getJSONObject("current") val firstUpdatePrevious = updates[0].getJSONObject("previous") assertEquals(3, firstUpdateCurrent.length()) assertEquals(3, firstUpdatePrevious.length()) assertEquals(expectedMime, firstUpdateCurrent.getString("mime")) assertEquals("", firstUpdatePrevious.getString("mime")) assertEquals(expectedFilename, firstUpdateCurrent.getString("filename")) assertEquals("", firstUpdatePrevious.getString("filename")) assertEquals(unfinishedDownloadSize, firstUpdateCurrent.getLong("totalBytes")) assertEquals(-1, firstUpdatePrevious.getLong("totalBytes")) val secondUpdateCurrent = updates[1].getJSONObject("current") val secondUpdatePrevious = updates[1].getJSONObject("previous") assertEquals(4, secondUpdateCurrent.length()) assertEquals(4, secondUpdatePrevious.length()) assertEquals(finishedDownloadSize, secondUpdateCurrent.getLong("totalBytes")) assertEquals(firstUpdateCurrent.getLong("totalBytes"), secondUpdatePrevious.getLong("totalBytes")) assertEquals("complete", secondUpdateCurrent.get("state").toString()) assertEquals("in_progress", secondUpdatePrevious.get("state").toString()) assertEquals(expectedEndTime.toString(), secondUpdateCurrent.getString("endTime")) assertEquals("null", secondUpdatePrevious.getString("endTime")) assertEquals(expectedFilesize, secondUpdateCurrent.getLong("fileSize")) assertEquals(-1, secondUpdatePrevious.getLong("fileSize")) sessionRule.waitForResult(controller.uninstall(webExtension)) } @Test fun testOnChangedWrongId() { val uri = createTestUrl("/assets/www/images/test.gif") val downloadId = 5 val webExtension = sessionRule.waitForResult( controller.installBuiltIn("resource://android/assets/web_extensions/download-onChanged/"), ) val assertOnDownloadCalled = GeckoResult() val downloadDelegate = object : DownloadDelegate { override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult? { assertEquals(webExtension!!.id, source.id) assertEquals(uri, request.request.uri) val download = controller.createDownload(downloadId) assertOnDownloadCalled.complete(download) return GeckoResult.fromValue(DownloadInitData(download, object : Download.Info {})) } } val onMessageCalled = GeckoResult() val messageDelegate = object : MessageDelegate { override fun onMessage(nativeApp: String, message: Any, sender: MessageSender): GeckoResult? { onMessageCalled.complete(message as String) return GeckoResult.fromValue(message) } } webExtension.setDownloadDelegate(downloadDelegate) webExtension.setMessageDelegate(messageDelegate, "browser") mainSession.reload() sessionRule.waitForPageStop() val updateData = object : WebExtension.Download.Info { override fun state(): Int { return WebExtension.Download.STATE_COMPLETE } } val randomDownload = controller.createDownload(25) val r = randomDownload!!.update(updateData) try { sessionRule.waitForResult(r!!) } catch (ex: Exception) { val a = ex.message!! assertEquals("Error: Trying to update unknown download", a) sessionRule.waitForResult(controller.uninstall(webExtension)) return } } @Test fun testMozAddonManagerDisabledByDefault() { // Assert the expected precondition (the pref to be set to false by default). val geckoPrefs = sessionRule.getPrefs( "extensions.webapi.enabled", ) assumeThat(geckoPrefs[0] as Boolean, equalTo(false)) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() // This pref normally exposes the mozAddonManager API to `example.com`. sessionRule.setPrefsUntilTestEnd(mapOf("extensions.webapi.testing" to true)) assertThat( "mozAddonManager is not exposed", mainSession.evaluateJS("typeof navigator.mozAddonManager") as String, equalTo("undefined"), ) } @Test fun testMozAddonManagerCanBeEnabledByPref() { // TODO: Bug 1837551 assumeThat(sessionRule.env.isFission, equalTo(false)) mainSession.loadUri("https://example.com") sessionRule.waitForPageStop() sessionRule.setPrefsUntilTestEnd( mapOf( "extensions.webapi.enabled" to true, // We still need this pref to be set to allow the API on `example.com`. "extensions.webapi.testing" to true, ), ) assertThat( "mozAddonManager is exposed", mainSession.evaluateJS("typeof navigator.mozAddonManager") as String, equalTo("object"), ) assertThat( "mozAddonManager.abuseReportPanelEnabled should be false", mainSession.evaluateJS("navigator.mozAddonManager.abuseReportPanelEnabled") as Boolean, equalTo(false), ) // Install an add-on, then assert results got from `mozAddonManager.getAddonByID()`. var addonId = "" sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled override fun onInstallPrompt(extension: WebExtension): GeckoResult { assertEquals(extension.metaData.name, "Borderify") assertEquals(extension.metaData.version, "1.0") assertEquals(extension.isBuiltIn, false) addonId = extension.id return GeckoResult.allow() } }) val borderify = sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ), ) var jsCode = """ navigator.mozAddonManager.getAddonByID("$addonId").then( addon => [addon.name, addon.version, addon.type].join(":") ); """ assertThat( "mozAddonManager.getAddonByID() resolved to the expected result", mainSession.evaluateJS(jsCode) as String, equalTo("Borderify:1.0:extension"), ) // Uninstall the add-on before exiting the test. sessionRule.waitForResult(controller.uninstall(borderify)) } @Test fun testMozAddonManagerSetting() { val settings = GeckoRuntimeSettings.Builder().build() assertThat( "Extension web API setting should be set to false", settings.extensionsWebAPIEnabled, equalTo(false), ) val geckoPrefs = sessionRule.getPrefs("extensions.webapi.enabled") assertThat( "extensionsWebAPIEnabled matches Gecko pref value", settings.extensionsWebAPIEnabled, equalTo(geckoPrefs[0] as Boolean), ) } @Test fun testExtensionsProcessDisabledByDefault() { val settings = GeckoRuntimeSettings.Builder() .build() assertThat( "extensionsProcessEnabled setting default should be null", settings.extensionsProcessEnabled, equalTo(null), ) val geckoPrefs = sessionRule.getPrefs( "extensions.webextensions.remote", ) assertThat( "extensions.webextensions.remote pref default value should be false", geckoPrefs[0] as Boolean, equalTo(false), ) } @Test fun testExtensionsProcessControlledFromSettings() { val settings = GeckoRuntimeSettings.Builder() .extensionsProcessEnabled(true) .build() assertThat( "extensionsProcessEnabled setting should be set to true", settings.extensionsProcessEnabled, equalTo(true), ) } @Test fun testExtensionProcessCrashThresholdsControlledFromSettings() { var crashThreshold = 1 var timeframe = 60000L val settings = GeckoRuntimeSettings.Builder() .extensionsProcessCrashThreshold(crashThreshold) .extensionsProcessCrashTimeframe(timeframe) .build() assertThat( "extensionProcessCrashThresholdMaxCount should be set to $crashThreshold", settings.extensionsProcessCrashThreshold, equalTo(crashThreshold), ) assertThat( "extensionsProcessCrashThresholdTimeframeSeconds should be set to $timeframe", settings.extensionsProcessCrashTimeframe, equalTo(timeframe), ) // Update with setters and check that settings have updated crashThreshold = 5 timeframe = 120000L settings.setExtensionsProcessCrashThreshold(crashThreshold) settings.setExtensionsProcessCrashTimeframe(timeframe) assertThat( "extensionProcessCrashThresholdMaxCount should be updated to $crashThreshold", settings.extensionsProcessCrashThreshold, equalTo(crashThreshold), ) assertThat( "extensionsProcessCrashThresholdTimeframeSeconds should be updated to $timeframe", settings.extensionsProcessCrashTimeframe, equalTo(timeframe), ) } @Test fun testExtensionProcessCrash() { sessionRule.setPrefsUntilTestEnd( mapOf( "extensions.webextensions.remote" to true, "dom.ipc.keepProcessesAlive.extension" to 1, "xpinstall.signatures.required" to false, ), ) sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 1) override fun onInstallPrompt(extension: WebExtension): GeckoResult? { return GeckoResult.allow() } }) sessionRule.addExternalDelegateUntilTestEnd( WebExtensionController.ExtensionProcessDelegate::class, { delegate -> controller.setExtensionProcessDelegate(delegate) }, { controller.setExtensionProcessDelegate(null) }, object : WebExtensionController.ExtensionProcessDelegate { @AssertCalled(count = 1) override fun onDisabledProcessSpawning() {} }, ) val borderify = sessionRule.waitForResult( controller.install( "resource://android/assets/web_extensions/borderify.xpi", null, ), ) val list = extensionsMap(sessionRule.waitForResult(controller.list())) assertTrue(list.containsKey(borderify.id)) mainSession.loadUri("about:crashextensions") sessionRule.waitForResult(controller.uninstall(borderify)) } }