summaryrefslogtreecommitdiffstats
path: root/mobile/android/components/extensions/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/components/extensions/test/xpcshell')
-rw-r--r--mobile/android/components/extensions/test/xpcshell/.eslintrc.js6
-rw-r--r--mobile/android/components/extensions/test/xpcshell/head.js24
-rw-r--r--mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js424
-rw-r--r--mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js167
-rw-r--r--mobile/android/components/extensions/test/xpcshell/xpcshell.toml9
5 files changed, 630 insertions, 0 deletions
diff --git a/mobile/android/components/extensions/test/xpcshell/.eslintrc.js b/mobile/android/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..2e6d214f4b
--- /dev/null
+++ b/mobile/android/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ extends:
+ "../../../../../../toolkit/components/extensions/test/xpcshell/.eslintrc.js",
+};
diff --git a/mobile/android/components/extensions/test/xpcshell/head.js b/mobile/android/components/extensions/test/xpcshell/head.js
new file mode 100644
index 0000000000..e79781fba6
--- /dev/null
+++ b/mobile/android/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,24 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ ExtensionTestUtils:
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
+});
+
+// Remove this pref once bug 1535365 is fixed.
+Services.prefs.setBoolPref("extensions.webextensions.remote", false);
+
+// https_first automatically upgrades http to https, but the tests are not
+// designed to expect that. And it is not easy to change that because
+// nsHttpServer does not support https (bug 1742061). So disable https_first.
+Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ExtensionTestUtils.init(this);
+
+Services.io.offline = true;
+
+var createHttpServer = (...args) => {
+ AddonTestUtils.maybeInit(this);
+ return AddonTestUtils.createHttpServer(...args);
+};
diff --git a/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js
new file mode 100644
index 0000000000..3ba2e26139
--- /dev/null
+++ b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js
@@ -0,0 +1,424 @@
+"use strict";
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerPathHandler("/", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write("<!DOCTYPE html><html></html>");
+});
+
+ChromeUtils.defineESModuleGetters(this, {
+ GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
+});
+
+// Save reference to original implementations to restore later.
+const { sendMessage, onConnect } = GeckoViewConnection.prototype;
+add_setup(async () => {
+ // This file replaces the implementation of GeckoViewConnection;
+ // make sure that it is restored upon test completion.
+ registerCleanupFunction(() => {
+ GeckoViewConnection.prototype.sendMessage = sendMessage;
+ GeckoViewConnection.prototype.onConnect = onConnect;
+ });
+});
+
+// Mock the embedder communication port
+class EmbedderPort {
+ constructor(portId, messenger) {
+ this.id = portId;
+ this.messenger = messenger;
+ }
+ close() {
+ Assert.ok(false, "close not expected to be called");
+ }
+ onPortDisconnect() {
+ Assert.ok(false, "onPortDisconnect not expected to be called");
+ }
+ onPortMessage(holder) {
+ Assert.ok(false, "onPortMessage not expected to be called");
+ }
+ triggerPortDisconnect() {
+ this.messenger.sendPortDisconnect(this.id);
+ }
+}
+
+function stubConnectNative() {
+ let port;
+ const firstCallPromise = new Promise(resolve => {
+ let callCount = 0;
+ GeckoViewConnection.prototype.onConnect = (portId, messenger) => {
+ Assert.equal(++callCount, 1, "onConnect called once");
+ port = new EmbedderPort(portId, messenger);
+ resolve();
+ return port;
+ };
+ });
+ const triggerPortDisconnect = () => {
+ if (!port) {
+ Assert.ok(false, "Undefined port, connection must be established first");
+ }
+ port.triggerPortDisconnect();
+ };
+ const restore = () => {
+ GeckoViewConnection.prototype.onConnect = onConnect;
+ };
+ return { firstCallPromise, triggerPortDisconnect, restore };
+}
+
+function stubSendNativeMessage() {
+ let sendResponse;
+ const returnPromise = new Promise(resolve => {
+ sendResponse = resolve;
+ });
+ const firstCallPromise = new Promise(resolve => {
+ let callCount = 0;
+ GeckoViewConnection.prototype.sendMessage = data => {
+ Assert.equal(++callCount, 1, "sendMessage called once");
+ resolve(data);
+ return returnPromise;
+ };
+ });
+ const restore = () => {
+ GeckoViewConnection.prototype.sendMessage = sendMessage;
+ };
+ return { firstCallPromise, sendResponse, restore };
+}
+
+function promiseExtensionEvent(wrapper, event) {
+ return new Promise(resolve => {
+ wrapper.extension.once(event, (...args) => resolve(args));
+ });
+}
+
+// verify that when background sends a native message,
+// the background will not be terminated to allow native messaging
+add_task(async function test_sendNativeMessage_event_page() {
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["geckoViewAddons", "nativeMessaging"],
+ background: { persistent: false },
+ },
+ async background() {
+ const res = await browser.runtime.sendNativeMessage("fake", "msg");
+ browser.test.assertEq("myResp", res, "expected response");
+ browser.test.sendMessage("done");
+ browser.runtime.onSuspend.addListener(async () => {
+ browser.test.assertFail("unexpected onSuspend");
+ });
+ },
+ });
+
+ const stub = stubSendNativeMessage();
+ await extension.startup();
+ info("Wait for sendNativeMessage to be received");
+ Assert.equal(
+ (await stub.firstCallPromise).deserialize({}),
+ "msg",
+ "expected message"
+ );
+
+ info("Trigger background script idle timeout and expect to be reset");
+ const promiseResetIdle = promiseExtensionEvent(
+ extension,
+ "background-script-reset-idle"
+ );
+ await extension.terminateBackground();
+ info("Wait for 'background-script-reset-idle' event to be emitted");
+ await promiseResetIdle;
+
+ stub.sendResponse("myResp");
+
+ info("Wait for extension to verify sendNativeMessage response");
+ await extension.awaitMessage("done");
+ await extension.unload();
+
+ stub.restore();
+});
+
+// verify that when an extension tab sends a native message,
+// the background will terminate as expected
+add_task(async function test_sendNativeMessage_tab() {
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["geckoViewAddons", "nativeMessaging"],
+ background: { persistent: false },
+ },
+ async background() {
+ browser.runtime.onSuspend.addListener(async () => {
+ browser.test.sendMessage("onSuspend_called");
+ });
+ },
+ files: {
+ "tab.html": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <script src="tab.js"></script>
+ `,
+ "tab.js": async () => {
+ const res = await browser.runtime.sendNativeMessage("fake", "msg");
+ browser.test.assertEq("myResp", res, "expected response");
+ browser.test.sendMessage("content_done");
+ },
+ },
+ });
+
+ const stub = stubSendNativeMessage();
+ await extension.startup();
+
+ const tab = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/tab.html?tab`,
+ { extension }
+ );
+
+ info("Wait for sendNativeMessage to be received");
+ Assert.equal(
+ (await stub.firstCallPromise).deserialize({}),
+ "msg",
+ "expected message"
+ );
+
+ info("Terminate extension");
+ await extension.terminateBackground();
+ await extension.awaitMessage("onSuspend_called");
+
+ stub.sendResponse("myResp");
+
+ info("Wait for extension to verify sendNativeMessage response");
+ await extension.awaitMessage("content_done");
+ await tab.close();
+ await extension.unload();
+
+ stub.restore();
+});
+
+// verify that when a content script sends a native message,
+// the background will terminate as expected
+add_task(async function test_sendNativeMessage_content_script() {
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent",
+ ],
+ background: { persistent: false },
+ content_scripts: [
+ {
+ run_at: "document_end",
+ js: ["test.js"],
+ matches: ["http://example.com/"],
+ },
+ ],
+ },
+ files: {
+ "test.js": async () => {
+ const res = await browser.runtime.sendNativeMessage("fake", "msg");
+ browser.test.assertEq("myResp", res, "expected response");
+ browser.test.sendMessage("content_done");
+ },
+ },
+ async background() {
+ browser.runtime.onSuspend.addListener(async () => {
+ browser.test.sendMessage("onSuspend_called");
+ });
+ },
+ });
+
+ const stub = stubSendNativeMessage();
+ await extension.startup();
+
+ info("Load content page");
+ const page = await ExtensionTestUtils.loadContentPage("http://example.com/");
+
+ info("Wait for message from extension");
+ Assert.equal(
+ (await stub.firstCallPromise).deserialize({}),
+ "msg",
+ "expected message"
+ );
+
+ info("Terminate extension");
+ await extension.terminateBackground();
+ await extension.awaitMessage("onSuspend_called");
+
+ stub.sendResponse("myResp");
+
+ info("Wait for extension to verify sendNativeMessage response");
+ await extension.awaitMessage("content_done");
+ await page.close();
+ await extension.unload();
+
+ stub.restore();
+});
+
+// verify that when native messaging ports are open, the background will not be terminated
+// and once the ports disconnect, onSuspend can be called
+add_task(async function test_connectNative_event_page() {
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["geckoViewAddons", "nativeMessaging"],
+ background: { persistent: false },
+ },
+ async background() {
+ const port = browser.runtime.connectNative("test");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+
+ browser.runtime.onSuspend.addListener(async () => {
+ browser.test.sendMessage("onSuspend_called");
+ });
+ },
+ });
+
+ const stub = stubConnectNative();
+ await extension.startup();
+ info("Waiting for connectNative request");
+ await stub.firstCallPromise;
+
+ info("Trigger background script idle timeout and expect to be reset");
+ const promiseResetIdle = promiseExtensionEvent(
+ extension,
+ "background-script-reset-idle"
+ );
+
+ await extension.terminateBackground();
+ info("Wait for 'background-script-reset-idle' event to be emitted");
+ await promiseResetIdle;
+
+ info("Trigger port disconnect, terminate background, and expect onSuspend()");
+ stub.triggerPortDisconnect();
+ await extension.awaitMessage("port_disconnected");
+
+ info("Terminate extension");
+ await extension.terminateBackground();
+ await extension.awaitMessage("onSuspend_called");
+
+ await extension.unload();
+ stub.restore();
+});
+
+// verify that when an extension tab opens native messaging ports,
+// the background will terminate as expected
+add_task(async function test_connectNative_tab() {
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: ["geckoViewAddons", "nativeMessaging"],
+ background: { persistent: false },
+ },
+ async background() {
+ browser.runtime.onSuspend.addListener(async () => {
+ browser.test.sendMessage("onSuspend_called");
+ });
+ },
+ files: {
+ "tab.html": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <script src="tab.js"></script>
+ `,
+ "tab.js": async () => {
+ const port = browser.runtime.connectNative("test");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ browser.test.sendMessage("content_done");
+ },
+ },
+ });
+
+ const stub = stubConnectNative();
+ await extension.startup();
+
+ const tab = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/tab.html?tab`,
+ { extension }
+ );
+ await extension.awaitMessage("content_done");
+ await stub.firstCallPromise;
+
+ info("Terminate extension");
+ await extension.terminateBackground();
+ await extension.awaitMessage("onSuspend_called");
+
+ stub.triggerPortDisconnect();
+ await extension.awaitMessage("port_disconnected");
+ await tab.close();
+ await extension.unload();
+
+ stub.restore();
+});
+
+// verify that when a content script opens native messaging ports,
+// the background will terminate as expected
+add_task(async function test_connectNative_content_script() {
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ manifest: {
+ permissions: [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent",
+ ],
+ background: { persistent: false },
+ content_scripts: [
+ {
+ run_at: "document_end",
+ js: ["test.js"],
+ matches: ["http://example.com/"],
+ },
+ ],
+ },
+ files: {
+ "test.js": async () => {
+ const port = browser.runtime.connectNative("test");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ browser.test.sendMessage("content_done");
+ },
+ },
+ async background() {
+ browser.runtime.onSuspend.addListener(async () => {
+ browser.test.sendMessage("onSuspend_called");
+ });
+ },
+ });
+
+ const stub = stubConnectNative();
+ await extension.startup();
+
+ info("Load content page");
+ const page = await ExtensionTestUtils.loadContentPage("http://example.com/");
+ await extension.awaitMessage("content_done");
+ await stub.firstCallPromise;
+
+ info("Terminate extension");
+ await extension.terminateBackground();
+ await extension.awaitMessage("onSuspend_called");
+
+ stub.triggerPortDisconnect();
+ await extension.awaitMessage("port_disconnected");
+ await page.close();
+ await extension.unload();
+
+ stub.restore();
+});
diff --git a/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js
new file mode 100644
index 0000000000..63f64b487e
--- /dev/null
+++ b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js
@@ -0,0 +1,167 @@
+"use strict";
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerPathHandler("/dum", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write("<!DOCTYPE html><html></html>");
+});
+
+async function testNativeMessaging({
+ isPrivileged = false,
+ permissions,
+ testBackground,
+ testContent,
+}) {
+ async function runTest(testFn, completionMessage) {
+ try {
+ dump(`Running test before sending ${completionMessage}\n`);
+ await testFn();
+ } catch (e) {
+ browser.test.fail(`Unexpected error: ${e}`);
+ }
+ browser.test.sendMessage(completionMessage);
+ }
+ const extension = ExtensionTestUtils.loadExtension({
+ isPrivileged,
+ background: `(${runTest})(${testBackground}, "background_done");`,
+ manifest: {
+ content_scripts: [
+ {
+ run_at: "document_end",
+ js: ["test.js"],
+ matches: ["http://example.com/dummy"],
+ },
+ ],
+ permissions,
+ },
+ files: {
+ "test.js": `(${runTest})(${testContent}, "content_done");`,
+ },
+ });
+
+ // Run background script.
+ await extension.startup();
+ await extension.awaitMessage("background_done");
+
+ // Run content script.
+ const page = await ExtensionTestUtils.loadContentPage(
+ "http://example.com/dummy"
+ );
+ await extension.awaitMessage("content_done");
+ await page.close();
+
+ await extension.unload();
+}
+
+// Checks that unprivileged extensions cannot use any of the nativeMessaging
+// APIs on Android.
+add_task(async function test_nativeMessaging_unprivileged() {
+ function testScript() {
+ browser.test.assertEq(
+ browser.runtime.connectNative,
+ undefined,
+ "connectNative should not be available in unprivileged extensions"
+ );
+ browser.test.assertEq(
+ browser.runtime.sendNativeMessage,
+ undefined,
+ "sendNativeMessage should not be available in unprivileged extensions"
+ );
+ }
+
+ const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await testNativeMessaging({
+ isPrivileged: false,
+ permissions: [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent",
+ ],
+ testBackground: testScript,
+ testContent: testScript,
+ });
+ });
+ AddonTestUtils.checkMessages(messages, {
+ expected: [
+ { message: /Invalid extension permission: geckoViewAddons/ },
+ { message: /Invalid extension permission: nativeMessaging/ },
+ { message: /Invalid extension permission: nativeMessagingFromContent/ },
+ ],
+ });
+});
+
+// Checks that privileged extensions can still not use native messaging without
+// the geckoViewAddons permission.
+add_task(async function test_geckoViewAddons_missing() {
+ const ERROR_NATIVE_MESSAGE_FROM_BACKGROUND =
+ "Native manifests are not supported on android";
+ const ERROR_NATIVE_MESSAGE_FROM_CONTENT =
+ /^Native messaging not allowed: \{.*"envType":"content_child","url":"http:\/\/example\.com\/dummy"\}$/;
+
+ async function testBackground() {
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"),
+ // Redacted error: ERROR_NATIVE_MESSAGE_FROM_BACKGROUND
+ "An unexpected error occurred",
+ "Background script cannot use nativeMessaging without geckoViewAddons"
+ );
+ }
+ async function testContent() {
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"),
+ // Redacted error: ERROR_NATIVE_MESSAGE_FROM_CONTENT
+ "An unexpected error occurred",
+ "Content script cannot use nativeMessaging without geckoViewAddons"
+ );
+ }
+
+ const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await testNativeMessaging({
+ isPrivileged: true,
+ permissions: ["nativeMessaging", "nativeMessagingFromContent"],
+ testBackground,
+ testContent,
+ });
+ });
+ AddonTestUtils.checkMessages(messages, {
+ expected: [
+ { errorMessage: ERROR_NATIVE_MESSAGE_FROM_BACKGROUND },
+ { errorMessage: ERROR_NATIVE_MESSAGE_FROM_CONTENT },
+ ],
+ });
+});
+
+// Checks that privileged extensions cannot use native messaging from content
+// without the nativeMessagingFromContent permission.
+add_task(async function test_nativeMessagingFromContent_missing() {
+ const ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM =
+ /^Unexpected messaging sender: \{.*"envType":"content_child","url":"http:\/\/example\.com\/dummy"\}$/;
+ function testBackground() {
+ // sendNativeMessage / connectNative are expected to succeed, but we
+ // are not testing that here because XpcshellTestRunnerService does not
+ // have a WebExtension.MessageDelegate that handles the message.
+ // There are plenty of mochitests that rely on connectNative, so we are
+ // not testing that here.
+ }
+ async function testContent() {
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"),
+ // Redacted error: ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM
+ "An unexpected error occurred",
+ "Trying to get through to native messaging but without luck"
+ );
+ }
+
+ const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await testNativeMessaging({
+ isPrivileged: true,
+ permissions: ["geckoViewAddons", "nativeMessaging"],
+ testBackground,
+ testContent,
+ });
+ });
+ AddonTestUtils.checkMessages(messages, {
+ expected: [{ errorMessage: ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM }],
+ });
+});
diff --git a/mobile/android/components/extensions/test/xpcshell/xpcshell.toml b/mobile/android/components/extensions/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..c9486971a0
--- /dev/null
+++ b/mobile/android/components/extensions/test/xpcshell/xpcshell.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+head = "head.js"
+firefox-appdir = "browser"
+tags = "webextensions in-process-webextensions"
+run-if = ["os == 'android'"]
+
+["test_ext_native_messaging_geckoview.js"]
+
+["test_ext_native_messaging_permissions.js"]