Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* Any copyright is dedicated to the Public Domain.
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
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 =
private const val TABS_CREATE_2_BACKGROUND: String =
private const val TABS_CREATE_REMOVE_BACKGROUND: String =
private const val TABS_ACTIVATE_REMOVE_BACKGROUND: String =
private const val TABS_REMOVE_BACKGROUND: String =
private const val MESSAGING_BACKGROUND: String =
private const val MESSAGING_CONTENT: String =
private const val OPENOPTIONSPAGE_1_BACKGROUND: String =
private const val OPENOPTIONSPAGE_2_BACKGROUND: String =
private const val EXTENSION_PAGE_RESTORE: String =
private const val BROWSING_DATA: String =
}
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(
),
)
assertTrue(borderify.isBuiltIn)
assertArrayEquals(
arrayOf("*://developer.mozilla.org/*"),
borderify.metaData.optionalOrigins,
)
assertArrayEquals(
arrayOf("clipboardRead"),
borderify.metaData.optionalPermissions,
)
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("")
}
@Test
fun verifyOptionalAndOriginsPermissionsMV3() {
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(
),
)
assertArrayEquals(
arrayOf("clipboardRead"),
borderify.metaData.optionalPermissions,
)
val expectedOptionalOrigins = arrayOf(
"*://*.example.com/*",
"*://opt-host-perm.example.com/*",
"*://host-perm.example.com/*",
)
expectedOptionalOrigins.sort()
borderify.metaData.optionalOrigins.sort()
assertArrayEquals(
expectedOptionalOrigins,
borderify.metaData.optionalOrigins,
)
mainSession.reload()
sessionRule.waitForPageStop()
// Check that the WebExtension was applied by checking the border color
assertBodyBorderEqualTo("red")
// 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("")
}
@WithDisplay(width = 100, height = 100)
@Test
fun grantedOptionalPermissions() {
sessionRule.setPrefsUntilTestEnd(
mapOf(
"xpinstall.signatures.required" to false,
"extensions.install.requireBuiltInCerts" to false,
"extensions.update.requireBuiltInCerts" to false,
),
)
val extension = sessionRule.waitForResult(
controller.ensureBuiltIn(
"optional-permission-request@example.com",
),
)
assertEquals("optional-permission-request@example.com", extension.id)
mainSession.loadUri("${extension.metaData.baseUrl}clickToRequestPermission.html")
sessionRule.waitForPageStop()
var grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
var grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
// click triggers permissions.request
mainSession.synthesizeTap(50, 50)
sessionRule.delegateUntilTestEnd(object : WebExtensionController.PromptDelegate {
override fun onOptionalPrompt(
extension: WebExtension,
permissions: Array<String>,
origins: Array<String>,
): GeckoResult<AllowOrDeny> {
return GeckoResult.allow()
}
})
var result = GeckoResult<String>()
mainSession.webExtensionController.setMessageDelegate(
extension,
object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender,
): GeckoResult<Any>? {
result.complete(message as String)
return null
}
},
"browser",
)
result = GeckoResult<String>()
val message = sessionRule.waitForResult(result)
assertThat("Permission request should be accepted.", message, equalTo("true"))
val updatedExtension = sessionRule.waitForResult(
// Adds "internal:privateBrowsingAllowed"
controller.setAllowedInPrivateBrowsing(extension, true),
)
grantedOptionalPermissions = updatedExtension.metaData.grantedOptionalPermissions
grantedOptionalOrigins = updatedExtension.metaData.grantedOptionalOrigins
assertThat(
"grantedOptionalPermissions must be 1.",
grantedOptionalPermissions.size,
equalTo(1),
)
assertThat("grantedOptionalOrigins must be 1.", grantedOptionalOrigins.size, equalTo(1))
assertThat(
"grantedOptionalOrigins must be *://example.com/*.",
grantedOptionalOrigins.first(),
equalTo("*://example.com/*"),
)
// geolocation is part of the manifest but not requested/granted.
assertFalse(grantedOptionalPermissions.contains("geolocation"))
// "internal:privateBrowsingAllowed" must not be part of grantedOptionalPermissions.
assertThat(
"grantedOptionalPermissions must be activeTab.",
grantedOptionalPermissions.first(),
equalTo("activeTab"),
)
sessionRule.waitForResult(controller.uninstall(extension))
}
@Test
fun updateOptionalPermissions() {
var extension = sessionRule.waitForResult(
controller.ensureBuiltIn(
"optional-permission-request@example.com",
),
)
assertEquals("optional-permission-request@example.com", extension.id)
var grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
var grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
// Without adding any optional permissions.
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
// Only adding an origin permission.
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf(),
arrayOf("*://example.com/*"),
),
)
grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 1.", grantedOptionalOrigins.size, equalTo(1))
assertThat(
"grantedOptionalOrigins must be *://example.com/*.",
grantedOptionalOrigins.first(),
equalTo("*://example.com/*"),
)
// Adding "nothing" to verify that nothing gets changed.
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf(),
arrayOf(),
),
)
assertThat("grantedOptionalOrigins must be 1.", grantedOptionalOrigins.size, equalTo(1))
assertThat(
"grantedOptionalOrigins must be *://example.com/*.",
grantedOptionalOrigins.first(),
equalTo("*://example.com/*"),
)
// Adding an optional permission.
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf("activeTab"),
arrayOf(),
),
)
grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
// Both optional and origin permissions must be granted.
assertThat(
"grantedOptionalPermissions must be 1.",
grantedOptionalPermissions.size,
equalTo(1),
)
assertThat(
"grantedOptionalPermissions must be activeTab.",
grantedOptionalPermissions.first(),
equalTo("activeTab"),
)
assertThat("grantedOptionalOrigins must be 1.", grantedOptionalOrigins.size, equalTo(1))
assertThat(
"grantedOptionalOrigins must be *://example.com/*.",
grantedOptionalOrigins.first(),
equalTo("*://example.com/*"),
)
// Removing "nothing" to verify that nothing gets changed.
extension = sessionRule.waitForResult(
controller.removeOptionalPermissions(
extension.id,
arrayOf(),
arrayOf(),
),
)
assertThat("grantedOptionalOrigins must be 1.", grantedOptionalOrigins.size, equalTo(1))
assertThat(
"grantedOptionalOrigins must be *://example.com/*.",
grantedOptionalOrigins.first(),
equalTo("*://example.com/*"),
)
// Remove an activeTab optional permission.
extension = sessionRule.waitForResult(
controller.removeOptionalPermissions(
extension.id,
arrayOf("activeTab"),
arrayOf(),
),
)
grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 1.", grantedOptionalOrigins.size, equalTo(1))
assertThat(
"grantedOptionalOrigins must be *://example.com/*.",
grantedOptionalOrigins.first(),
equalTo("*://example.com/*"),
)
// Remove an `*://example.com/*` origin permission.
extension = sessionRule.waitForResult(
controller.removeOptionalPermissions(
extension.id,
arrayOf(),
arrayOf("*://example.com/*"),
),
)
grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
// Missing origins from the manifest.
// Must throw!
try {
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf(),
arrayOf("*://missing-origins.com/*"),
),
)
fail()
} catch (_: Exception) {
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
}
// Permission no in the manifest.
// Must throw!
try {
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf("clipboardRead"),
arrayOf(),
),
)
fail()
} catch (_: Exception) {
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
}
// Missing origins from the manifest.
// Must throw!
try {
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf(),
arrayOf("<all_urls>"),
),
)
fail()
} catch (_: Exception) {
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
}
sessionRule.waitForResult(controller.uninstall(extension))
}
@Test
fun onOptionalPermissionsChanged() {
var extension = sessionRule.waitForResult(
controller.ensureBuiltIn(
"optional-permission-request@example.com",
),
)
assertEquals("optional-permission-request@example.com", extension.id)
var grantedOptionalPermissions = extension.metaData.grantedOptionalPermissions
var grantedOptionalOrigins = extension.metaData.grantedOptionalOrigins
assertThat(
"grantedOptionalPermissions must be 0.",
grantedOptionalPermissions.size,
equalTo(0),
)
assertThat("grantedOptionalOrigins must be 0.", grantedOptionalOrigins.size, equalTo(0))
sessionRule.delegateDuringNextWait(object : WebExtensionController.AddonManagerDelegate {
@AssertCalled(count = 1)
override fun onOptionalPermissionsChanged(updatedExtension: WebExtension) {
grantedOptionalPermissions = updatedExtension.metaData.grantedOptionalPermissions
grantedOptionalOrigins = updatedExtension.metaData.grantedOptionalOrigins
assertNull(updatedExtension)
assertArrayEquals(
"grantedOptionalPermissions must be [activeTab, geolocation].",
arrayOf("activeTab", "geolocation"),
grantedOptionalPermissions,
)
assertArrayEquals(
"grantedOptionalPermissions must be [*://example.com/*].",
arrayOf("*://example.com/*"),
grantedOptionalOrigins,
)
}
})
extension = sessionRule.waitForResult(
controller.addOptionalPermissions(
extension.id,
arrayOf("activeTab", "geolocation"),
arrayOf("*://example.com/*"),
),
)
sessionRule.waitForResult(controller.uninstall(extension))
}
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<AllowOrDeny> {
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(
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<AllowOrDeny> {
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(
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<AllowOrDeny> {
return GeckoResult.allow()
}
})
var borderify = sessionRule.waitForResult(
controller.install(
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 optionsUIPageMetadata() {
// 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<Void>()
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()))
assertEquals(true, extension.metaData.openOptionsPageInTab)
onReadyResult.complete(null)
super.onReady(extension)
}
},
)
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled(count = 1)
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.allow()
}
})
val dummy = sessionRule.waitForResult(
controller.install(
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 optionsPageAliasMetadata() {
// NOTE: This test case tests options_page is considered an alternative alias for
// options_ui.page and the metadata to be set so that it is opened in a tab.
// Wait for the onReady AddonManagerDelegate method to be called, and assert
// that the baseUrl and optionsPageUrl are both available as expected.
val onReadyResult = GeckoResult<Void>()
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()))
assertEquals("${extension.metaData.baseUrl}dummy.html", extension.metaData.optionsPageUrl)
assertEquals(true, extension.metaData.openOptionsPageInTab)
onReadyResult.complete(null)
super.onReady(extension)
}
},
)
val testExt = sessionRule.waitForResult(
controller.installBuiltIn(
),
)
sessionRule.waitForResult(onReadyResult)
sessionRule.waitForResult(controller.uninstall(testExt))
}
@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<AllowOrDeny> {
return GeckoResult.allow()
}
})
// Install in parallell borderify and dummy
val borderifyResult = controller.install(
null,
)
val dummyResult = controller.install(
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<AllowOrDeny> {
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(
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<WebExtension>): Map<String, WebExtension> {
val map = HashMap<String, WebExtension>()
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<AllowOrDeny> {
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(
extensionName = "Borderify",
)
}
@Test
fun installUnsignedExtensionAsZipFile() {
testInstallUnsignedExtensionSignatureNotRequired(
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<AllowOrDeny> {
return GeckoResult.allow()
}
})
sessionRule.delegateDuringNextWait(object : WebExtensionController.AddonManagerDelegate {
@AssertCalled(count = 1)
override fun onInstallationFailed(
extension: WebExtension?,
installException: InstallException,
) {
assertNull(extension)
}
})
sessionRule.waitForResult(
controller.install(
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<AllowOrDeny> {
return GeckoResult.deny()
}
})
sessionRule.waitForResult(
controller.install(
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(
)
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<Void>()
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<Any>? {
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<Void>()
var tabsExtension: WebExtension? = null
val tabDelegate = object : WebExtension.TabDelegate {
override fun onNewTab(source: WebExtension, details: WebExtension.CreateTabDetails): GeckoResult<GeckoSession> {
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<Void>()
var tabsExtension: WebExtension? = null
val tabDelegate = object : WebExtension.TabDelegate {
override fun onNewTab(source: WebExtension, details: WebExtension.CreateTabDetails): GeckoResult<GeckoSession> {
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<Void>()
val tabsExtension = sessionRule.waitForResult(
controller.installBuiltIn(TABS_CREATE_REMOVE_BACKGROUND),
)
tabsExtension.tabDelegate = object : WebExtension.TabDelegate {
override fun onNewTab(source: WebExtension, details: WebExtension.CreateTabDetails): GeckoResult<GeckoSession> {
val extensionCreatedSession = sessionRule.createClosedSession(mainSession.settings)
extensionCreatedSession.webExtensionController.setTabDelegate(
tabsExtension,
object : WebExtension.SessionTabDelegate {
override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult<AllowOrDeny> {
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<Void>()
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<AllowOrDeny> {
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<JSONObject> {
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<JSONObject> {
val uuid = UUID.randomUUID().toString()
json.put("uuid", uuid)
port.postMessage(json)
val response = GeckoResult<JSONObject>()
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<WebExtension.Port>()
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<WebExtension.BrowsingDataDelegate.Settings>? {
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<Void>? {
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<Void>? {
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<Void>? {
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<Void>? {
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<Void>? {
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<Void>? {
assertThat(
"timestamp should match",
sinceUnixTimestamp,
equalTo(0L),
)
return null
}
@AssertCalled
override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult<Void>? {
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<Void>? {
assertThat(
"timestamp should match",
sinceUnixTimestamp,
equalTo(0L),
)
return null
}
@AssertCalled
override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult<Void>? {
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<Void>? {
assertThat(
"timestamp should match",
sinceUnixTimestamp,
equalTo(0L),
)
return GeckoResult.fromException(RuntimeException("Not authorized passwords."))
}
@AssertCalled
override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult<Void>? {
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<WebExtension.BrowsingDataDelegate.Settings>? {
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<AllowOrDeny> {
return GeckoResult.allow()
}
})
val extension = sessionRule.waitForResult(
)
val accumulator = mutableListOf<String>()
val result = GeckoResult<List<String>>()
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<Void> {
register("downloads", sinceUnixTimestamp)
return GeckoResult.fromValue(null)
}
override fun onClearFormData(sinceUnixTimestamp: Long): GeckoResult<Void> {
register("formData", sinceUnixTimestamp)
return GeckoResult.fromValue(null)
}
override fun onClearHistory(sinceUnixTimestamp: Long): GeckoResult<Void> {
register("history", sinceUnixTimestamp)
return GeckoResult.fromValue(null)
}
override fun onClearPasswords(sinceUnixTimestamp: Long): GeckoResult<Void> {
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<Void>()
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.allow()
}
})
val tabsExtension = sessionRule.waitForResult(
)
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
@AssertCalled
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
return GeckoResult.allow()
}
})
var tabsExtensionPB = sessionRule.waitForResult(
)
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<Void>()
class TabDelegate(
val result: GeckoResult<Void>,
val extension: WebExtension,
val expectedSession: GeckoSession,
) :
WebExtension.SessionTabDelegate {
override fun onCloseTab(
source: WebExtension?,
session: GeckoSession,
): GeckoResult<AllowOrDeny> {
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<Void>()
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<AllowOrDeny> {
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<String>()
session.webExtensionController.setMessageDelegate(
extension,
object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender,
): GeckoResult<Any>? {
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<String>()
val portResult = GeckoResult<WebExtension.Port>()
session.webExtensionController.setMessageDelegate(
extension,
object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender,
): GeckoResult<Any>? {
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<String>()
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 1884334
val geckoPrefs = sessionRule.getPrefs(
"fission.disableSessionHistoryInParent",
)
assumeThat(geckoPrefs[0] as Boolean, Matchers.equalTo(true))
// 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<Void>()
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<AllowOrDeny> {
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<Void>()
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<Any>? {
// 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<Void>()
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<Any>? {
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<Void>()
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<Any>? {
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<Void>()
val portIframe = GeckoResult<Void>()
val messageTopLevel = GeckoResult<Void>()
val messageIframe = GeckoResult<Void>()
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<Any>? {
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(
),
)
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<String>()
val messageDelegate = object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender,
): GeckoResult<Any>? {
assertEquals(message, "setupReadyStartTest")
result.complete(null)
return null
}
}
val extension = sessionRule.waitForResult(
controller.installBuiltIn(
),
)
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<String>()
var extension: WebExtension? = null
val messageDelegate = object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender,
): GeckoResult<Any>? {
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(
"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<AllowOrDeny> {
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<PermissionDelegate.ContentPermission>, hasUserGesture: Boolean) {
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<Boolean>()
mainSession.delegateUntilTestEnd(object : NavigationDelegate, ProgressDelegate {
override fun onLocationChange(session: GeckoSession, url: String?, perms: MutableList<PermissionDelegate.ContentPermission>, hasUserGesture: Boolean) {
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(
"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<AllowOrDeny> {
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(
"does not point to a folder",
)
}
@Test
fun badLocationXpi() {
testInstallBuiltInError(
"does not point to a folder",
)
}
@Test
fun testInstallBuiltInError() {
testInstallBuiltInError(
"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(
"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<String>, origins: Array<String>): GeckoResult<AllowOrDeny> {
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<String>()
mainSession.webExtensionController.setMessageDelegate(
extension,
object : WebExtension.MessageDelegate {
override fun onMessage(
nativeApp: String,
message: Any,
sender: WebExtension.MessageSender,
): GeckoResult<Any>? {
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<String>()
val message2 = sessionRule.waitForResult(result)
assertThat("Permission request should be accepted.", message2, equalTo("true"))
mainSession.synthesizeTap(50, 50)
result = GeckoResult<String>()
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<AllowOrDeny> {
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<AllowOrDeny> {
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<AllowOrDeny> {
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<AllowOrDeny> {
assertEquals(extension.metaData.version, "1.0")
return GeckoResult.allow()
}
})
val update1 = sessionRule.waitForResult(
)
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<String>,
newOrigins: Array<String>,
): GeckoResult<AllowOrDeny> {
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<AllowOrDeny> {
assertEquals(extension.metaData.version, "2.0")
return GeckoResult.allow()
}
})
val update1 = sessionRule.waitForResult(
controller.install(
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<AllowOrDeny> {
assertEquals(extension.metaData.version, "1.0")
return GeckoResult.allow()
}
})
val update1 = sessionRule.waitForResult(
)
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<String>,
newOrigins: Array<String>,
): GeckoResult<AllowOrDeny> {
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<AllowOrDeny> {
return GeckoResult.allow()
}
})
val install = controller.install(
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<AllowOrDeny> {
assertEquals(extension.metaData.version, "1.0")
return GeckoResult.allow()
}
})
val update1 = sessionRule.waitForResult(
controller.install(
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<AllowOrDeny> {
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(
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<Void>()
var optionsExtension: WebExtension? = null
val tabDelegate = object : WebExtension.TabDelegate {
@AssertCalled(count = 1)
override fun onNewTab(
source: WebExtension,
details: WebExtension.CreateTabDetails,
): GeckoResult<GeckoSession> {
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<Void>()
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<AllowOrDeny> {
return GeckoResult.allow()
}
})
val webExtension = sessionRule.waitForResult(
controller.install(
null,
),
)
val assertOnDownloadCalled = GeckoResult<WebExtension.Download>()
val downloadDelegate = object : DownloadDelegate {
override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult<DownloadInitData>? {
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<AllowOrDeny> {
return GeckoResult.allow()
}
})
val webExtension = sessionRule.waitForResult(
controller.install(
null,
),
)
val assertOnDownloadCalled = GeckoResult<WebExtension.Download>()
val downloadDelegate = object : DownloadDelegate {
override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult<DownloadInitData>? {
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(
)
val assertOnDownloadCalled = GeckoResult<Download>()
val downloadDelegate = object : DownloadDelegate {
override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult<WebExtension.DownloadInitData>? {
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<JSONObject>()
val thirdUpdateReceived = GeckoResult<JSONObject>()
val messageDelegate = object : MessageDelegate {
override fun onMessage(nativeApp: String, message: Any, sender: MessageSender): GeckoResult<Any>? {
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(
)
val assertOnDownloadCalled = GeckoResult<WebExtension.Download>()
val downloadDelegate = object : DownloadDelegate {
override fun onDownload(source: WebExtension, request: DownloadRequest): GeckoResult<WebExtension.DownloadInitData>? {
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<String>()
val messageDelegate = object : MessageDelegate {
override fun onMessage(nativeApp: String, message: Any, sender: MessageSender): GeckoResult<Any>? {
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"),
)
// 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<AllowOrDeny> {
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(
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<AllowOrDeny>? {
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(
null,
),
)
val list = extensionsMap(sessionRule.waitForResult(controller.list()))
assertTrue(list.containsKey(borderify.id))
mainSession.loadUri("about:crashextensions")
sessionRule.waitForResult(controller.uninstall(borderify))
}
}