Bug 1823432 - Verify mobile messaging for native app and extension communication r=geckoview-reviewers,extension-reviewers,robwu,amejiamarmol

Differential Revision: https://phabricator.services.mozilla.com/D179607
This commit is contained in:
Cathy Lu 2023-06-07 16:20:50 +00:00
parent 21d11bee1a
commit d715b3d12a
2 changed files with 425 additions and 0 deletions

View File

@ -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();
});

View File

@ -4,4 +4,5 @@ firefox-appdir = browser
tags = webextensions in-process-webextensions
skip-if = os != "android"
[test_ext_native_messaging_geckoview.js]
[test_ext_native_messaging_permissions.js]