diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 11:10:00 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 11:10:00 -0500 |
commit | f164d9124708b50789dbb6959e1de96cc5697c48 (patch) | |
tree | 6dffd12e08c5383130df0252fb69cd6d6330794f /toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js | |
parent | 30de4018913f0cdaea19d1dd12ecd8209e2ed08e (diff) | |
download | UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar.gz UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar.lz UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar.xz UXP-f164d9124708b50789dbb6959e1de96cc5697c48.zip |
Rename Toolkit's webextensions component directory to better reflect what it is.
Diffstat (limited to 'toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js')
-rw-r--r-- | toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js b/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js new file mode 100644 index 000000000..5a6b628f5 --- /dev/null +++ b/toolkit/components/webextensions/test/xpcshell/test_ext_native_messaging.js @@ -0,0 +1,514 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +/* globals chrome */ + +const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; +const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; + +const ECHO_BODY = String.raw` + import struct + import sys + + while True: + rawlen = sys.stdin.read(4) + if len(rawlen) == 0: + sys.exit(0) + msglen = struct.unpack('@I', rawlen)[0] + msg = sys.stdin.read(msglen) + + sys.stdout.write(struct.pack('@I', msglen)) + sys.stdout.write(msg) +`; + +const INFO_BODY = String.raw` + import json + import os + import struct + import sys + + msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()}) + sys.stdout.write(struct.pack('@I', len(msg))) + sys.stdout.write(msg) + sys.exit(0) +`; + +const STDERR_LINES = ["hello stderr", "this should be a separate line"]; +let STDERR_MSG = STDERR_LINES.join("\\n"); + +const STDERR_BODY = String.raw` + import sys + sys.stderr.write("${STDERR_MSG}") +`; + +const SCRIPTS = [ + { + name: "echo", + description: "a native app that echoes back messages it receives", + script: ECHO_BODY.replace(/^ {2}/gm, ""), + }, + { + name: "info", + description: "a native app that gives some info about how it was started", + script: INFO_BODY.replace(/^ {2}/gm, ""), + }, + { + name: "stderr", + description: "a native app that writes to stderr and then exits", + script: STDERR_BODY.replace(/^ {2}/gm, ""), + }, +]; + +add_task(function* setup() { + yield setupHosts(SCRIPTS); +}); + +// Test the basic operation of native messaging with a simple +// script that echoes back whatever message is sent to it. +add_task(function* test_happy_path() { + function background() { + let port = browser.runtime.connectNative("echo"); + port.onMessage.addListener(msg => { + browser.test.sendMessage("message", msg); + }); + browser.test.onMessage.addListener((what, payload) => { + if (what == "send") { + if (payload._json) { + let json = payload._json; + payload.toJSON = () => json; + delete payload._json; + } + port.postMessage(payload); + } + }); + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + const tests = [ + { + data: "this is a string", + what: "simple string", + }, + { + data: "Это юникода", + what: "unicode string", + }, + { + data: {test: "hello"}, + what: "simple object", + }, + { + data: { + what: "An object with a few properties", + number: 123, + bool: true, + nested: {what: "another object"}, + }, + what: "object with several properties", + }, + + { + data: { + ignoreme: true, + _json: {data: "i have a tojson method"}, + }, + expected: {data: "i have a tojson method"}, + what: "object with toJSON() method", + }, + ]; + for (let test of tests) { + extension.sendMessage("send", test.data); + let response = yield extension.awaitMessage("message"); + let expected = test.expected || test.data; + deepEqual(response, expected, `Echoed a message of type ${test.what}`); + } + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is still running"); + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; +}); + +if (AppConstants.platform == "win") { + // "relative.echo" has a relative path in the host manifest. + add_task(function* test_relative_path() { + function background() { + let port = browser.runtime.connectNative("relative.echo"); + let MSG = "test relative echo path"; + port.onMessage.addListener(msg => { + browser.test.assertEq(MSG, msg, "Got expected message back"); + browser.test.sendMessage("done"); + }); + port.postMessage(MSG); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("done"); + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is still running"); + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; + }); +} + +// Test sendNativeMessage() +add_task(function* test_sendNativeMessage() { + async function background() { + let MSG = {test: "hello world"}; + + // Check error handling + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("nonexistent", MSG), + /Attempt to postMessage on disconnected port/, + "sendNativeMessage() to a nonexistent app failed"); + + // Check regular message exchange + let reply = await browser.runtime.sendNativeMessage("echo", MSG); + + let expected = JSON.stringify(MSG); + let received = JSON.stringify(reply); + browser.test.assertEq(expected, received, "Received echoed native message"); + + browser.test.sendMessage("finished"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + + // With sendNativeMessage(), the subprocess should be disconnected + // after exchanging a single message. + yield waitForSubprocessExit(); + + yield extension.unload(); +}); + +// Test calling Port.disconnect() +add_task(function* test_disconnect() { + function background() { + let port = browser.runtime.connectNative("echo"); + port.onMessage.addListener((msg, msgPort) => { + browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument"); + browser.test.sendMessage("message", msg); + }); + port.onDisconnect.addListener(msgPort => { + browser.test.fail("onDisconnect should not be called for disconnect()"); + }); + browser.test.onMessage.addListener((what, payload) => { + if (what == "send") { + if (payload._json) { + let json = payload._json; + payload.toJSON = () => json; + delete payload._json; + } + port.postMessage(payload); + } else if (what == "disconnect") { + try { + port.disconnect(); + browser.test.sendMessage("disconnect-result", {success: true}); + } catch (err) { + browser.test.sendMessage("disconnect-result", { + success: false, + errmsg: err.message, + }); + } + } + }); + browser.test.sendMessage("ready"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + extension.sendMessage("send", "test"); + let response = yield extension.awaitMessage("message"); + equal(response, "test", "Echoed a string"); + + let procCount = yield getSubprocessCount(); + equal(procCount, 1, "subprocess is running"); + + extension.sendMessage("disconnect"); + response = yield extension.awaitMessage("disconnect-result"); + equal(response.success, true, "disconnect succeeded"); + + do_print("waiting for subprocess to exit"); + yield waitForSubprocessExit(); + procCount = yield getSubprocessCount(); + equal(procCount, 0, "subprocess is no longer running"); + + extension.sendMessage("disconnect"); + response = yield extension.awaitMessage("disconnect-result"); + equal(response.success, true, "second call to disconnect silently ignored"); + + yield extension.unload(); +}); + +// Test the limit on message size for writing +add_task(function* test_write_limit() { + Services.prefs.setIntPref(PREF_MAX_WRITE, 10); + function clearPref() { + Services.prefs.clearUserPref(PREF_MAX_WRITE); + } + do_register_cleanup(clearPref); + + function background() { + const PAYLOAD = "0123456789A"; + let port = browser.runtime.connectNative("echo"); + try { + port.postMessage(PAYLOAD); + browser.test.sendMessage("result", null); + } catch (ex) { + browser.test.sendMessage("result", ex.message); + } + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let errmsg = yield extension.awaitMessage("result"); + notEqual(errmsg, null, "native postMessage() failed for overly large message"); + + yield extension.unload(); + yield waitForSubprocessExit(); + + clearPref(); +}); + +// Test the limit on message size for reading +add_task(function* test_read_limit() { + Services.prefs.setIntPref(PREF_MAX_READ, 10); + function clearPref() { + Services.prefs.clearUserPref(PREF_MAX_READ); + } + do_register_cleanup(clearPref); + + function background() { + const PAYLOAD = "0123456789A"; + let port = browser.runtime.connectNative("echo"); + port.onDisconnect.addListener(msgPort => { + browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); + browser.test.assertEq("Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.", port.error && port.error.message); + browser.test.sendMessage("result", "disconnected"); + }); + port.onMessage.addListener(msg => { + browser.test.sendMessage("result", "message"); + }); + port.postMessage(PAYLOAD); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let result = yield extension.awaitMessage("result"); + equal(result, "disconnected", "native port disconnected on receiving large message"); + + yield extension.unload(); + yield waitForSubprocessExit(); + + clearPref(); +}); + +// Test that an extension without the nativeMessaging permission cannot +// use native messaging. +add_task(function* test_ext_permission() { + function background() { + browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission"); + browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission"); + browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission"); + browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission"); + browser.test.sendMessage("finished"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: {}, + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + yield extension.unload(); +}); + +// Test that an extension that is not listed in allowed_extensions for +// a native application cannot use that application. +add_task(function* test_app_permission() { + function background() { + let port = browser.runtime.connectNative("echo"); + port.onDisconnect.addListener(msgPort => { + browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); + browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message); + browser.test.sendMessage("result", "disconnected"); + }); + port.onMessage.addListener(msg => { + browser.test.sendMessage("result", "message"); + }); + port.postMessage({test: "test"}); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + permissions: ["nativeMessaging"], + }, + }, "somethingelse@tests.mozilla.org"); + + yield extension.startup(); + + let result = yield extension.awaitMessage("result"); + equal(result, "disconnected", "connectNative() failed without native app permission"); + + yield extension.unload(); + + let procCount = yield getSubprocessCount(); + equal(procCount, 0, "No child process was started"); +}); + +// Test that the command-line arguments and working directory for the +// native application are as expected. +add_task(function* test_child_process() { + function background() { + let port = browser.runtime.connectNative("info"); + port.onMessage.addListener(msg => { + browser.test.sendMessage("result", msg); + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + + let msg = yield extension.awaitMessage("result"); + equal(msg.args.length, 2, "Received one command line argument"); + equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest"); + equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path, + "Working directory is the directory containing the native appliation"); + + let exitPromise = waitForSubprocessExit(); + yield extension.unload(); + yield exitPromise; +}); + +add_task(function* test_stderr() { + function background() { + let port = browser.runtime.connectNative("stderr"); + port.onDisconnect.addListener(msgPort => { + browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument"); + browser.test.assertEq(null, port.error, "Normal application exit is not an error"); + browser.test.sendMessage("finished"); + }); + } + + let {messages} = yield promiseConsoleOutput(function* () { + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("finished"); + yield extension.unload(); + + yield waitForSubprocessExit(); + }); + + let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line))); + notEqual(lines[0], -1, "Saw first line of stderr output on the console"); + notEqual(lines[1], -1, "Saw second line of stderr output on the console"); + notEqual(lines[0], lines[1], "Stderr output lines are separated in the console"); +}); + +// Test that calling connectNative() multiple times works +// (bug 1313980 was a previous regression in this area) +add_task(function* test_multiple_connects() { + async function background() { + function once() { + return new Promise(resolve => { + let MSG = "hello"; + let port = browser.runtime.connectNative("echo"); + + port.onMessage.addListener(msg => { + browser.test.assertEq(MSG, msg, "Got expected message back"); + port.disconnect(); + resolve(); + }); + port.postMessage(MSG); + }); + } + + await once(); + await once(); + browser.test.notifyPass("multiple-connect"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { + applications: {gecko: {id: ID}}, + permissions: ["nativeMessaging"], + }, + }); + + yield extension.startup(); + yield extension.awaitFinish("multiple-connect"); + yield extension.unload(); +}); |