Bug 1197420 Part 5 Tests for optional permissions r=kmag
MozReview-Commit-ID: 8uViXB1Jgz3
--- a/toolkit/components/extensions/ExtensionPermissions.jsm
+++ b/toolkit/components/extensions/ExtensionPermissions.jsm
@@ -103,9 +103,21 @@ this.ExtensionPermissions = {
}
},
async removeAll(extension) {
await lazyInit();
delete prefs.data[extension.id];
prefs.saveSoon();
},
+
+ // This is meant for tests only
+ async _uninit() {
+ if (!_initPromise) {
+ return;
+ }
+
+ await _initPromise;
+ await prefs.finalize();
+ prefs = null;
+ _initPromise = null;
+ },
};
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -12,32 +12,34 @@ support-files =
oauth.html
redirect_auto.sjs
tags = webextensions in-process-webextensions
[test_chrome_ext_background_debug_global.html]
skip-if = (os == 'android') # android doesn't have devtools
[test_chrome_ext_background_page.html]
skip-if = (toolkit == 'android') # android doesn't have devtools
-[test_chrome_ext_eventpage_warning.html]
[test_chrome_ext_contentscript_data_uri.html]
[test_chrome_ext_contentscript_unrecognizedprop_warning.html]
+[test_chrome_ext_downloads_saveAs.html]
+[test_chrome_ext_eventpage_warning.html]
[test_chrome_ext_hybrid_addons.html]
+[test_chrome_ext_idle.html]
+[test_chrome_ext_identity.html]
+skip-if = os == 'android' # unsupported.
+[test_chrome_ext_permissions.html]
+skip-if = os == 'android' # Bug 1350559
+[test_chrome_ext_storage_cleanup.html]
+[test_chrome_ext_trackingprotection.html]
[test_chrome_ext_trustworthy_origin.html]
[test_chrome_ext_webnavigation_resolved_urls.html]
+[test_chrome_ext_webrequest_background_events.html]
+[test_chrome_ext_webrequest_host_permissions.html]
[test_chrome_native_messaging_paths.html]
skip-if = os != "mac" && os != "linux"
[test_ext_cookies_expiry.html]
[test_ext_cookies_permissions_bad.html]
[test_ext_cookies_permissions_good.html]
[test_ext_cookies_containers.html]
[test_ext_jsversion.html]
[test_ext_schema.html]
[test_ext_protocolHandlers.html]
skip-if = (toolkit == 'android') # bug 1342577
-[test_chrome_ext_storage_cleanup.html]
-[test_chrome_ext_idle.html]
-[test_chrome_ext_identity.html]
-skip-if = os == 'android' # unsupported.
-[test_chrome_ext_downloads_saveAs.html]
-[test_chrome_ext_webrequest_background_events.html]
-[test_chrome_ext_webrequest_host_permissions.html]
-[test_chrome_ext_trackingprotection.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for permissions</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function makeTest(manifestPermissions, optionalPermissions, checkFetch = true) {
+ return async function() {
+ function pageScript() {
+ /* global PERMISSIONS */
+ /* eslint-disable mozilla/balanced-listeners */
+ window.addEventListener("keypress", () => {
+ browser.permissions.request(PERMISSIONS).then(result => {
+ browser.test.sendMessage("request.result", result);
+ }, {once: true});
+ });
+ /* eslint-enable mozilla/balanced-listeners */
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "set-cookie") {
+ try {
+ await browser.cookies.set({
+ url: "http://example.com/",
+ name: "COOKIE",
+ value: "NOM NOM",
+ });
+ browser.test.sendMessage("set-cookie.result", {success: true});
+ } catch (err) {
+ dump(`set cookie failed with ${err.message}\n`);
+ browser.test.sendMessage("set-cookie.result",
+ {success: false, message: err.message});
+ }
+ } else if (msg == "remove") {
+ browser.permissions.remove(PERMISSIONS).then(result => {
+ browser.test.sendMessage("remove.result", result);
+ });
+ }
+ });
+
+ browser.test.sendMessage("page-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+
+ manifest: {
+ permissions: manifestPermissions,
+ optional_permissions: [...(optionalPermissions.permissions || []),
+ ...(optionalPermissions.origins || [])],
+
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["content_script.js"],
+ }],
+ },
+
+ files: {
+ "content_script.js": async () => {
+ let url = new URL(window.location.pathname, "http://example.com/");
+ fetch(url, {}).then(response => {
+ browser.test.sendMessage("fetch.result", response.ok);
+ }).catch(err => {
+ browser.test.sendMessage("fetch.result", false);
+ });
+ },
+
+ "page.html": `<html><head>
+ <script src="page.js"><\/script>
+ </head></html>`,
+
+ "page.js": `const PERMISSIONS = ${JSON.stringify(optionalPermissions)}; (${pageScript})();`,
+ },
+ });
+
+ await extension.startup();
+
+ function call(method) {
+ extension.sendMessage(method);
+ return extension.awaitMessage(`${method}.result`);
+ }
+
+ let base = window.location.href.replace(/^chrome:\/\/mochitests\/content/,
+ "http://mochi.test:8888");
+ let file = new URL("file_sample.html", base);
+
+ async function testContentScript() {
+ let win = window.open(file);
+ let result = await extension.awaitMessage("fetch.result");
+ win.close();
+ return result;
+ }
+
+ let url = await extension.awaitMessage("ready");
+ let win = window.open(url);
+ await extension.awaitMessage("page-ready");
+
+ // Using the cookies API from an extension page should fail
+ let result = await call("set-cookie");
+ is(result.success, false, "setting cookie failed");
+ if (manifestPermissions.includes("cookies")) {
+ ok(/^Permission denied/.test(result.message),
+ "setting cookie failed with an appropriate error due to missing host permission");
+ } else {
+ ok(/browser\.cookies is undefined/.test(result.message),
+ "setting cookie failed since cookies API is not present");
+ }
+
+ // Making a cross-origin request from a content script should fail
+ if (checkFetch) {
+ result = await testContentScript();
+ is(result, false, "fetch() failed from content script due to lack of host permission");
+ }
+
+ // Request some permissions
+ let winutils = SpecialPowers.getDOMWindowUtils(win);
+ winutils.sendKeyEvent("keypress", KeyEvent.DOM_VK_A, 0, 0);
+ result = await extension.awaitMessage("request.result");
+ is(result, true, "permissions.request() succeeded");
+
+ // Using the cookies API from an extension page should succeed
+ result = await call("set-cookie");
+ is(result.success, true, "setting cookie succeeded");
+
+ // Making a cross-origin request from a content script should succeed
+ if (checkFetch) {
+ result = await testContentScript();
+ is(result, true, "fetch() succeeded from content script due to lack of host permission");
+ }
+
+ // Now revoke our permissions
+ result = await call("remove");
+
+ // The cookies API should once again fail
+ result = await call("set-cookie");
+ is(result.success, false, "setting cookie failed");
+
+ // As should the cross-origin request from a content script
+ if (checkFetch) {
+ result = await testContentScript();
+ is(result, false, "fetch() failed from content script due to lack of host permission");
+ }
+
+ await extension.unload();
+ };
+}
+
+const ORIGIN = "*://example.com/";
+add_task(makeTest([], {
+ permissions: ["cookies"],
+ origins: [ORIGIN],
+}));
+
+add_task(makeTest(["cookies"], {origins: [ORIGIN]}));
+add_task(makeTest([ORIGIN], {permissions: ["cookies"]}, false));
+
+</script>
+
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -0,0 +1,262 @@
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "ExtensionManager", () => {
+ const {ExtensionManager}
+ = Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
+ return ExtensionManager;
+});
+Cu.import("resource://gre/modules/ExtensionPermissions.jsm");
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+// Find the DOMWindowUtils for the background page for the given
+// extension (wrapper)
+function findWinUtils(extension) {
+ let extensionChild = ExtensionManager.extensions.get(extension.extension.id);
+ let bgwin = null;
+ for (let view of extensionChild.views) {
+ if (view.viewType == "background") {
+ bgwin = view.contentWindow;
+ }
+ }
+ notEqual(bgwin, null, "Found background window for the test extension");
+ return bgwin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+}
+
+add_task(async function test_permissions() {
+ const REQUIRED_PERMISSIONS = ["downloads"];
+ const REQUIRED_ORIGINS = ["*://site.com/", "*://*.domain.com/"];
+ const OPTIONAL_PERMISSIONS = ["idle", "clipboardWrite"];
+ const OPTIONAL_ORIGINS = ["http://optionalsite.com/", "https://*.optionaldomain.com/"];
+
+ let acceptPrompt = false;
+ const observer = {
+ observe(subject, topic, data) {
+ if (topic == "webextension-optional-permission-prompt") {
+ let {resolve} = subject.wrappedJSObject;
+ resolve(acceptPrompt);
+ }
+ },
+ };
+
+ Services.prefs.setBoolPref("extensions.webextOptionalPermissionPrompts", true);
+ Services.obs.addObserver(observer, "webextension-optional-permission-prompt", false);
+ do_register_cleanup(() => {
+ Services.obs.removeObserver(observer, "webextension-optional-permission-prompt");
+ Services.prefs.clearUserPref("extensions.webextOptionalPermissionPrompts");
+ });
+
+ await AddonTestUtils.promiseStartupManager();
+
+ function background() {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ if (method == "getAll") {
+ let perms = await browser.permissions.getAll();
+ browser.test.sendMessage("getAll.result", perms);
+ } else if (method == "contains") {
+ let result = await browser.permissions.contains(arg);
+ browser.test.sendMessage("contains.result", result);
+ } else if (method == "request") {
+ try {
+ let result = await browser.permissions.request(arg);
+ browser.test.sendMessage("request.result", {status: "success", result});
+ } catch (err) {
+ browser.test.sendMessage("request.result", {status: "error", message: err.message});
+ }
+ } else if (method == "remove") {
+ let result = await browser.permissions.remove(arg);
+ browser.test.sendMessage("remove.result", result);
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: [...REQUIRED_PERMISSIONS, ...REQUIRED_ORIGINS],
+ optional_permissions: [...OPTIONAL_PERMISSIONS, ...OPTIONAL_ORIGINS],
+ },
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+ let winUtils = findWinUtils(extension);
+
+ function call(method, arg) {
+ extension.sendMessage(method, arg);
+ return extension.awaitMessage(`${method}.result`);
+ }
+
+ let result = await call("getAll");
+ deepEqual(result.permissions, REQUIRED_PERMISSIONS);
+ deepEqual(result.origins, REQUIRED_ORIGINS);
+
+ for (let perm of REQUIRED_PERMISSIONS) {
+ result = await call("contains", {permissions: [perm]});
+ equal(result, true, `contains() returns true for fixed permission ${perm}`);
+ }
+ for (let origin of REQUIRED_ORIGINS) {
+ result = await call("contains", {origins: [origin]});
+ equal(result, true, `contains() returns true for fixed origin ${origin}`);
+ }
+
+ // None of the optional permissions should be available yet
+ for (let perm of OPTIONAL_PERMISSIONS) {
+ result = await call("contains", {permissions: [perm]});
+ equal(result, false, `contains() returns false for permission ${perm}`);
+ }
+ for (let origin of OPTIONAL_ORIGINS) {
+ result = await call("contains", {origins: [origin]});
+ equal(result, false, `conains() returns false for origin ${origin}`);
+ }
+
+ result = await call("contains", {
+ permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS],
+ });
+ equal(result, false, "contains() returns false for a mix of available and unavailable permissions");
+
+ let perm = OPTIONAL_PERMISSIONS[0];
+ result = await call("request", {permissions: [perm]});
+ equal(result.status, "error", "request() fails if not called from an event handler");
+ ok(/May only request permissions from a user input handler/.test(result.message),
+ "error message for calling request() outside an event handler is reasonable");
+ result = await call("contains", {permissions: [perm]});
+ equal(result, false, "Permission requested outside an event handler was not granted");
+
+ let userInputHandle = winUtils.setHandlingUserInput(true);
+
+ result = await call("request", {permissions: ["notifications"]});
+ equal(result.status, "error", "request() for permission not in optional_permissions should fail");
+ ok(/since it was not declared in optional_permissions/.test(result.message),
+ "error message for undeclared optional_permission is reasonable");
+
+ // Check request() when the prompt is canceled.
+ acceptPrompt = false;
+ result = await call("request", {permissions: [perm]});
+ equal(result.status, "success", "request() returned cleanly");
+ equal(result.result, false, "request() returned false for rejected permission");
+
+ result = await call("contains", {permissions: [perm]});
+ equal(result, false, "Rejected permission was not granted");
+
+ // Call request() and accept the prompt
+ acceptPrompt = true;
+ let allOptional = {
+ permissions: OPTIONAL_PERMISSIONS,
+ origins: OPTIONAL_ORIGINS,
+ };
+ result = await call("request", allOptional);
+ equal(result.status, "success", "request() returned cleanly");
+ equal(result.result, true, "request() returned true for accepted permissions");
+ userInputHandle.destruct();
+
+ let allPermissions = {
+ permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS],
+ origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS],
+ };
+
+ result = await call("getAll");
+ deepEqual(result, allPermissions, "getAll() returns required and runtime requested permissions");
+
+ result = await call("contains", allPermissions);
+ equal(result, true, "contains() returns true for runtime requested permissions");
+
+ // Restart, verify permissions are still present
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ result = await call("getAll");
+ deepEqual(result, allPermissions, "Runtime requested permissions are still present after restart");
+
+ // Check remove()
+ result = await call("remove", {permissions: OPTIONAL_PERMISSIONS});
+ equal(result, true, "remove() succeeded");
+
+ let perms = {
+ permissions: REQUIRED_PERMISSIONS,
+ origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS],
+ };
+ result = await call("getAll");
+ deepEqual(result, perms, "Expected permissions remain after removing some");
+
+ result = await call("remove", {origins: OPTIONAL_ORIGINS});
+ equal(result, true, "remove() succeeded");
+
+ perms.origins = REQUIRED_ORIGINS;
+ result = await call("getAll");
+ deepEqual(result, perms, "Back to default permissions after removing more");
+
+ await extension.unload();
+});
+
+add_task(async function test_startup() {
+ async function background() {
+ browser.test.onMessage.addListener(async (perms) => {
+ await browser.permissions.request(perms);
+ browser.test.sendMessage("requested");
+ });
+
+ let all = await browser.permissions.getAll();
+ browser.test.sendMessage("perms", all);
+ }
+
+ const PERMS1 = {
+ permissions: ["clipboardRead", "tabs"],
+ };
+ const PERMS2 = {
+ origins: ["https://site2.com/"],
+ };
+
+ let extension1 = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {optional_permissions: PERMS1.permissions},
+ useAddonManager: "permanent",
+ });
+ let extension2 = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {optional_permissions: PERMS2.origins},
+ useAddonManager: "permanent",
+ });
+
+ await extension1.startup();
+ await extension2.startup();
+
+ let perms = await extension1.awaitMessage("perms");
+ dump(`perms1 ${JSON.stringify(perms)}\n`);
+ perms = await extension2.awaitMessage("perms");
+ dump(`perms2 ${JSON.stringify(perms)}\n`);
+
+ let winUtils = findWinUtils(extension1);
+ let handle = winUtils.setHandlingUserInput(true);
+ extension1.sendMessage(PERMS1);
+ await extension1.awaitMessage("requested");
+ handle.destruct();
+
+ winUtils = findWinUtils(extension2);
+ handle = winUtils.setHandlingUserInput(true);
+ extension2.sendMessage(PERMS2);
+ await extension2.awaitMessage("requested");
+ handle.destruct();
+
+ // Restart everything, and force the permissions store to be
+ // re-read on startup
+ ExtensionPermissions._uninit();
+ await AddonTestUtils.promiseRestartManager();
+ await extension1.awaitStartup();
+ await extension2.awaitStartup();
+
+ async function checkPermissions(extension, permissions) {
+ perms = await extension.awaitMessage("perms");
+ let expect = Object.assign({permissions: [], origins: []}, permissions);
+ deepEqual(perms, expect, "Extension got correct permissions on startup");
+ }
+
+ await checkPermissions(extension1, PERMS1);
+ await checkPermissions(extension2, PERMS2);
+
+ await extension1.unload();
+ await extension2.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -47,16 +47,18 @@ skip-if = release_or_beta
[test_ext_management.js]
[test_ext_management_uninstall_self.js]
[test_ext_manifest_content_security_policy.js]
[test_ext_manifest_incognito.js]
[test_ext_manifest_minimum_chrome_version.js]
[test_ext_manifest_themes.js]
[test_ext_onmessage_removelistener.js]
skip-if = true # This test no longer tests what it is meant to test.
+[test_ext_permissions.js]
+skip-if = "android" # Bug 1350559
[test_ext_privacy.js]
[test_ext_privacy_disable.js]
[test_ext_privacy_update.js]
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getBrowserInfo.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_onInstalled_and_onStartup.js]
[test_ext_runtime_sendMessage.js]