diff options
Diffstat (limited to 'mobile/android/components/extensions/test/xpcshell')
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"] |