Bug 1197420 Part 5 Tests for optional permissions r=kmag draft
authorAndrew Swan <aswan@mozilla.com>
Thu, 16 Mar 2017 14:36:26 -0700
changeset 551366 2d897f5c1353e69b1aab17713627960806c159fe
parent 538632 40f53e5f34f02749a5027aa324cf0843c5d2c837
child 621530 69b840fa867fe2aa2b5e42f7c9ba8547215a270c
push id51036
push useraswan@mozilla.com
push dateSat, 25 Mar 2017 19:56:26 +0000
reviewerskmag
bugs1197420
milestone55.0a1
Bug 1197420 Part 5 Tests for optional permissions r=kmag MozReview-Commit-ID: 8uViXB1Jgz3
toolkit/components/extensions/ExtensionPermissions.jsm
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_permissions.html
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- 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]