Bug 1317590 AddonManager support for permissions r=rhelmer
authorAndrew Swan <aswan@mozilla.com>
Wed, 16 Nov 2016 08:53:56 -0800
changeset 440388 d35e32293f2173bd6480d9d0a0788e29cd2cb198
parent 440387 ef895cb3fb7d0b9c0d09e2fd773951e696d69d0f
child 440389 ea17d4edd453337325be6140c4e9e8b1dbfd31fd
push id36216
push userbmo:ato@mozilla.com
push dateThu, 17 Nov 2016 13:57:13 +0000
reviewersrhelmer
bugs1317590
milestone53.0a1
Bug 1317590 AddonManager support for permissions r=rhelmer MozReview-Commit-ID: 6I6BTb0TJR2
toolkit/components/extensions/Extension.jsm
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -358,16 +358,32 @@ this.ExtensionData = class {
           resolve(JSON.parse(text));
         } catch (e) {
           reject(e);
         }
       });
     });
   }
 
+  // This method should return a structured representation of any
+  // capabilities this extension has access to, as derived from the
+  // manifest.  The current implementation just returns the contents
+  // of the permissions attribute, if we add things like url_overrides,
+  // they should also be added here.
+  userPermissions() {
+    let result = {
+      hosts: this.whiteListedHosts.pat,
+      apis: [...this.apiNames],
+    };
+    const EXP_PATTERN = /^experiments\.\w+/;
+    result.permissions = [...this.permissions]
+      .filter(p => !result.hosts.includes(p) && !EXP_PATTERN.test(p));
+    return result;
+  }
+
   // Reads the extension's |manifest.json| file, and stores its
   // parsed contents in |this.manifest|.
   readManifest() {
     return Promise.all([
       this.readJSON("manifest.json"),
       Management.lazyInit(),
     ]).then(([manifest]) => {
       this.manifest = manifest;
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1865,23 +1865,22 @@ var AddonManagerInternal = {
    * @param  aHash
    *         An optional hash of the add-on
    * @param  aName
    *         An optional placeholder name while the add-on is being downloaded
    * @param  aIcons
    *         Optional placeholder icons while the add-on is being downloaded
    * @param  aVersion
    *         An optional placeholder version while the add-on is being downloaded
-   * @param  aLoadGroup
-   *         An optional nsILoadGroup to associate any network requests with
+   * @param  aBrowser
+   *         An optional <browser> element for download permissions prompts.
    * @throws if the aUrl, aCallback or aMimetype arguments are not specified
    */
-  getInstallForURL: function(aUrl, aCallback, aMimetype,
-                                                  aHash, aName, aIcons,
-                                                  aVersion, aBrowser) {
+  getInstallForURL: function(aUrl, aCallback, aMimetype, aHash, aName,
+                             aIcons, aVersion, aBrowser) {
     if (!gStarted)
       throw Components.Exception("AddonManager is not initialized",
                                  Cr.NS_ERROR_NOT_INITIALIZED);
 
     if (!aUrl || typeof aUrl != "string")
       throw Components.Exception("aURL must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -3194,26 +3193,32 @@ this.AddonManager = {
     // The install is being downloaded.
     ["STATE_DOWNLOADING",  1],
     // The install is checking for compatibility information.
     ["STATE_CHECKING", 2],
     // The install is downloaded and ready to install.
     ["STATE_DOWNLOADED", 3],
     // The download failed.
     ["STATE_DOWNLOAD_FAILED", 4],
+    // The install may not proceed until the user accepts permissions
+    ["STATE_AWAITING_PERMISSIONS", 5],
+    // Any permission prompts are done
+    ["STATE_PERMISSION_GRANTED", 6],
     // The install has been postponed.
-    ["STATE_POSTPONED", 5],
+    ["STATE_POSTPONED", 7],
+    // The install is ready to be applied.
+    ["STATE_READY", 8],
     // The add-on is being installed.
-    ["STATE_INSTALLING", 6],
+    ["STATE_INSTALLING", 9],
     // The add-on has been installed.
-    ["STATE_INSTALLED", 7],
+    ["STATE_INSTALLED", 10],
     // The install failed.
-    ["STATE_INSTALL_FAILED", 8],
+    ["STATE_INSTALL_FAILED", 11],
     // The install has been cancelled.
-    ["STATE_CANCELLED", 9],
+    ["STATE_CANCELLED", 12],
   ]),
 
   // Constants representing different types of errors while downloading an
   // add-on.
   // These will show up as AddonManager.ERROR_* (eg, ERROR_NETWORK_FAILURE)
   _errors: new Map([
     // The download failed due to network problems.
     ["ERROR_NETWORK_FAILURE", -1],
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -989,16 +989,17 @@ var loadManifestFromWebManifest = Task.a
     else
       addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER;
   }
 
   // WebExtensions don't use iconURLs
   addon.iconURL = null;
   addon.icon64URL = null;
   addon.icons = manifest.icons || {};
+  addon.userPermissions = extension.userPermissions();
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
   function getLocale(aLocale) {
     // Use the raw manifest, here, since we need values with their
     // localization placeholders still in place.
     let rawManifest = extension.rawManifest;
 
@@ -1327,16 +1328,17 @@ let loadManifestFromRDF = Task.async(fun
   if (addon.type == "experiment") {
     addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
     addon.updateURL = null;
     addon.updateKey = null;
   }
 
   // icons will be filled by the calling function
   addon.icons = {};
+  addon.userPermissions = null;
 
   return addon;
 });
 
 function defineSyncGUID(aAddon) {
   // Define .syncGUID as a lazy property which is also settable
   Object.defineProperty(aAddon, "syncGUID", {
     get: () => {
@@ -3978,19 +3980,34 @@ this.XPIProvider = {
    *         A version for the install
    * @param  aBrowser
    *         The browser performing the install
    * @param  aCallback
    *         A callback to pass the AddonInstall to
    */
   getInstallForURL: function(aUrl, aHash, aName, aIcons, aVersion, aBrowser,
                              aCallback) {
-    createDownloadInstall(function(aInstall) {
-      aCallback(aInstall.wrapper);
-    }, aUrl, aHash, aName, aIcons, aVersion, aBrowser);
+    let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+    let url = NetUtil.newURI(aUrl);
+
+    let options = {
+      hash: aHash,
+      browser: aBrowser,
+      name: aName,
+      icons: aIcons,
+      version: aVersion,
+    };
+
+    if (url instanceof Ci.nsIFileURL) {
+      let install = new LocalAddonInstall(location, url, options);
+      install.init().then(() => { aCallback(install.wrapper); });
+    } else {
+      let install = new DownloadAddonInstall(location, url, options);
+      aCallback(install.wrapper);
+    }
   },
 
   /**
    * Called to get an AddonInstall to install an add-on from a local file.
    *
    * @param  aFile
    *         The file to be installed
    * @param  aCallback
@@ -5338,55 +5355,68 @@ function getHashStringForCrypto(aCrypto)
 /**
  * Base class for objects that manage the installation of an addon.
  * This class isn't instantiated directly, see the derived classes below.
  */
 class AddonInstall {
   /**
    * Instantiates an AddonInstall.
    *
-   * @param  aInstallLocation
+   * @param  installLocation
    *         The install location the add-on will be installed into
-   * @param  aUrl
+   * @param  url
    *         The nsIURL to get the add-on from. If this is an nsIFileURL then
    *         the add-on will not need to be downloaded
-   * @param  aHash
+   * @param  options
+   *         Additional options for the install
+   * @param  options.hash
    *         An optional hash for the add-on
-   * @param  aExistingAddon
+   * @param  options.existingAddon
    *         The add-on this install will update if known
-   */
-  constructor(aInstallLocation, aUrl, aHash, aExistingAddon) {
+   * @param  options.name
+   *         An optional name for the add-on
+   * @param  options.type
+   *         An optional type for the add-on
+   * @param  options.icons
+   *         Optional icons for the add-on
+   * @param  options.version
+   *         An optional version for the add-on
+   * @param  options.permHandler
+   *         A callback to present permissions to the user before installing.
+   */
+  constructor(installLocation, url, options = {}) {
     this.wrapper = new AddonInstallWrapper(this);
-    this.installLocation = aInstallLocation;
-    this.sourceURI = aUrl;
-
-    if (aHash) {
-      let hashSplit = aHash.toLowerCase().split(":");
+    this.installLocation = installLocation;
+    this.sourceURI = url;
+
+    if (options.hash) {
+      let hashSplit = options.hash.toLowerCase().split(":");
       this.originalHash = {
         algorithm: hashSplit[0],
         data: hashSplit[1]
       };
     }
     this.hash = this.originalHash;
-    this.existingAddon = aExistingAddon;
+    this.existingAddon = options.existingAddon || null;
+    this.permHandler = options.permHandler || (() => Promise.resolve());
     this.releaseNotesURI = null;
 
     this.listeners = [];
-    this.icons = {};
+    this.icons = options.icons || {};
     this.error = 0;
 
     this.progress = 0;
     this.maxProgress = -1;
 
     // Giving each instance of AddonInstall a reference to the logger.
     this.logger = logger;
 
-    this.name = null;
-    this.type = null;
-    this.version = null;
+    this.name = options.name || null;
+    this.type = options.type || null;
+    this.version = options.version || null;
 
     this.file = null;
     this.ownsTempFile = null;
     this.certificate = null;
     this.certName = null;
 
     this.linkedInstalls = null;
     this.addon = null;
@@ -5402,16 +5432,22 @@ class AddonInstall {
    * Note this method is overridden to handle additional state in
    * the subclassses below.
    *
    * @throws if installation cannot proceed from the current state
    */
   install() {
     switch (this.state) {
     case AddonManager.STATE_DOWNLOADED:
+      this.checkPermissions();
+      break;
+    case AddonManager.STATE_PERMISSION_GRANTED:
+      this.checkForBlockers();
+      break;
+    case AddonManager.STATE_READY:
       this.startInstall();
       break;
     case AddonManager.STATE_POSTPONED:
       logger.debug(`Postponing install of ${this.addon.id}`);
       break;
     case AddonManager.STATE_DOWNLOADING:
     case AddonManager.STATE_CHECKING:
     case AddonManager.STATE_INSTALLING:
@@ -5772,16 +5808,71 @@ class AddonInstall {
       this.addon.compatibilityOverrides = repoAddon ?
         repoAddon.compatibilityOverrides :
         null;
       this.addon.appDisabled = !isUsableAddon(this.addon);
       return undefined;
     }).bind(this));
   }
 
+  /**
+   * This method should be called when the XPI is ready to be installed,
+   * i.e., when a download finishes or when a local file has been verified.
+   * It should only be called from install() when the install is in
+   * STATE_DOWNLOADED (which actually means that the file is available
+   * and has been verified).
+   */
+  checkPermissions() {
+    Task.spawn((function*() {
+      if (this.permHandler) {
+        let info = {
+          existinAddon: this.existingAddon,
+          addon: this.addon,
+        };
+
+        try {
+          yield this.permHandler(info);
+        } catch (err) {
+          logger.info(`Install of ${this.addon.id} cancelled since user declined permissions`);
+          this.state = AddonManager.STATE_CANCELLED;
+          XPIProvider.removeActiveInstall(this);
+          AddonManagerPrivate.callInstallListeners("onInstallCancelled",
+                                                   this.listeners, this.wrapper);
+          return;
+        }
+      }
+      this.state = AddonManager.STATE_PERMISSION_GRANTED;
+      this.install();
+    }).bind(this));
+  }
+
+  /**
+   * This method should be called when we have the XPI and any needed
+   * permissions prompts have been completed.  If there are any upgrade
+   * listeners, they are invoked and the install moves into STATE_POSTPONED.
+   * Otherwise, the install moves into STATE_INSTALLING
+   */
+  checkForBlockers() {
+    // If an upgrade listener is registered for this add-on, pass control
+    // over the upgrade to the add-on.
+    if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
+      logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
+      let resumeFn = () => {
+        logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
+        this.state = AddonManager.STATE_READY;
+        this.install();
+      }
+      this.postpone(resumeFn);
+      return;
+    }
+
+    this.state = AddonManager.STATE_READY;
+    this.install();
+  }
+
   // TODO This relies on the assumption that we are always installing into the
   // highest priority install location so the resulting add-on will be visible
   // overriding any existing copy in another install location (bug 557710).
   /**
    * Installs the add-on into the install location.
    */
   startInstall() {
     this.state = AddonManager.STATE_INSTALLING;
@@ -6184,43 +6275,42 @@ class LocalAddonInstall extends AddonIns
 class DownloadAddonInstall extends AddonInstall {
   /**
    * Instantiates a DownloadAddonInstall
    *
    * @param  installLocation
    *         The InstallLocation the add-on will be installed into
    * @param  url
    *         The nsIURL to get the add-on from
-   * @param  name
-   *         An optional name for the add-on
-   * @param  hash
+   * @param  options
+   *         Additional options for the install
+   * @param  options.hash
    *         An optional hash for the add-on
-   * @param  existingAddon
+   * @param  options.existingAddon
    *         The add-on this install will update if known
-   * @param  browser
+   * @param  options.browser
    *         The browser performing the install, used to display
    *         authentication prompts.
-   * @param  type
+   * @param  options.name
+   *         An optional name for the add-on
+   * @param  options.type
    *         An optional type for the add-on
-   * @param  icons
+   * @param  options.icons
    *         Optional icons for the add-on
-   * @param  version
+   * @param  options.version
    *         An optional version for the add-on
-   */
-  constructor(installLocation, url, hash, existingAddon, browser,
-              name, type, icons, version) {
-    super(installLocation, url, hash, existingAddon);
-
-    this.browser = browser;
+   * @param  options.permHandler
+   *         A callback to present permissions to the user before installing.
+   */
+  constructor(installLocation, url, options={}) {
+    super(installLocation, url, options);
+
+    this.browser = options.browser;
 
     this.state = AddonManager.STATE_AVAILABLE;
-    this.name = name;
-    this.type = type;
-    this.version = version;
-    this.icons = icons;
 
     this.stream = null;
     this.crypto = null;
     this.badCertHandler = null;
     this.restartDownload = false;
 
     AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
                                             this.wrapper);
@@ -6566,34 +6656,22 @@ class DownloadAddonInstall extends Addon
 
       if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
                                                    this.listeners,
                                                    this.wrapper)) {
         // If a listener changed our state then do not proceed with the install
         if (this.state != AddonManager.STATE_DOWNLOADED)
           return;
 
-        // If an upgrade listener is registered for this add-on, pass control
-        // over the upgrade to the add-on.
-        if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
-          logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
-          let resumeFn = () => {
-            logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
-            this.state = AddonManager.STATE_DOWNLOADED;
-            this.install();
-          }
-          this.postpone(resumeFn);
-        } else {
-          // no upgrade listener present, so proceed with normal install
-          this.install();
-          if (this.linkedInstalls) {
-            for (let install of this.linkedInstalls) {
-              if (install.state == AddonManager.STATE_DOWNLOADED)
-                install.install();
-            }
+        // proceed with the install state machine.
+        this.install();
+        if (this.linkedInstalls) {
+          for (let install of this.linkedInstalls) {
+            if (install.state == AddonManager.STATE_DOWNLOADED)
+              install.install();
           }
         }
       }
     });
   }
 
   getInterface(iid) {
     if (iid.equals(Ci.nsIAuthPrompt2)) {
@@ -6670,73 +6748,43 @@ function createLocalInstall(file, locati
   catch (e) {
     logger.error("Error creating install", e);
     XPIProvider.removeActiveInstall(this);
     return Promise.resolve(null);
   }
 }
 
 /**
- * Creates a new AddonInstall to download and install a URL.
- *
- * @param  aCallback
- *         The callback to pass the new AddonInstall to
- * @param  aUri
- *         The URI to download
- * @param  aHash
- *         A hash for the add-on
- * @param  aName
- *         A name for the add-on
- * @param  aIcons
- *         An icon URLs for the add-on
- * @param  aVersion
- *         A version for the add-on
- * @param  aBrowser
- *         The browser performing the install
- */
-function createDownloadInstall(aCallback, aUri, aHash, aName, aIcons,
-                               aVersion, aBrowser) {
-  let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
-  let url = NetUtil.newURI(aUri);
-
-  if (url instanceof Ci.nsIFileURL) {
-    let install = new LocalAddonInstall(location, url, aHash);
-    install.init().then(() => { aCallback(install); });
-  } else {
-    let install = new DownloadAddonInstall(location, url, aHash, null,
-                                           aBrowser, aName, null, aIcons,
-                                           aVersion);
-    aCallback(install);
-  }
-}
-
-/**
  * Creates a new AddonInstall for an update.
  *
  * @param  aCallback
  *         The callback to pass the new AddonInstall to
  * @param  aAddon
  *         The add-on being updated
  * @param  aUpdate
  *         The metadata about the new version from the update manifest
  */
 function createUpdate(aCallback, aAddon, aUpdate) {
   let url = NetUtil.newURI(aUpdate.updateURL);
 
   Task.spawn(function*() {
+    let opts = {
+      hash: aUpdate.updateHash,
+      existingAddon: aAddon,
+      name: aAddon.selectedLocale.name,
+      type: aAddon.type,
+      icons: aAddon.icons,
+      version: aUpdate.version,
+    };
     let install;
     if (url instanceof Ci.nsIFileURL) {
-      install = new LocalAddonInstall(aAddon._installLocation, url,
-                                      aUpdate.updateHash, aAddon);
+      install = new LocalAddonInstall(aAddon._installLocation, url, opts);
       yield install.init();
     } else {
-      install = new DownloadAddonInstall(aAddon._installLocation, url,
-                                         aUpdate.updateHash, aAddon, null,
-                                         aAddon.selectedLocale.name, aAddon.type,
-                                         aAddon.icons, aUpdate.version);
+      install = new DownloadAddonInstall(aAddon._installLocation, url, opts);
     }
     try {
       if (aUpdate.updateInfoURL)
         install.releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
     }
     catch (e) {
       // If the releaseNotesURI cannot be parsed then just ignore it.
     }
@@ -6789,16 +6837,20 @@ AddonInstallWrapper.prototype = {
 
   get linkedInstalls() {
     let install = installFor(this);
     if (!install.linkedInstalls)
       return null;
     return install.linkedInstalls.map(i => i.wrapper);
   },
 
+  set _permHandler(handler) {
+    installFor(this).permHandler = handler;
+  },
+
   install: function() {
     installFor(this).install();
   },
 
   cancel: function() {
     installFor(this).cancel();
   },
 
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -42,16 +42,18 @@ const { OS } = Components.utils.import("
 Components.utils.import("resource://gre/modules/AsyncShutdown.jsm");
 
 Components.utils.import("resource://testing-common/AddonTestUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
                                   "resource://testing-common/ExtensionXPCShellUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
+                                  "resource://testing-common/ExtensionTestCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "MockAsyncShutdown",
                                   "resource://testing-common/AddonTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
                                   "resource://testing-common/MockRegistrar.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
                                   "resource://testing-common/MockRegistry.jsm");
@@ -65,16 +67,17 @@ const {
   getFileForAddon,
   manuallyInstall,
   manuallyUninstall,
   promiseAddonByID,
   promiseAddonEvent,
   promiseAddonsByIDs,
   promiseAddonsWithOperationsByTypes,
   promiseCompleteAllInstalls,
+  promiseCompleteInstall,
   promiseConsoleOutput,
   promiseFindAddonUpdates,
   promiseInstallAllFiles,
   promiseInstallFile,
   promiseRestartManager,
   promiseSetExtensionModifiedTime,
   promiseShutdownManager,
   promiseStartupManager,
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
@@ -471,8 +471,81 @@ add_task(function* test_strict_min_max()
   addon = yield promiseAddonByID(newId);
 
   notEqual(addon, null, "Add-on is installed");
   equal(addon.id, newId, "Installed add-on has the expected ID");
 
   yield extension.unload();
   AddonManager.checkCompatibility = savedCheckCompatibilityValue;
 });
+
+// Check permissions prompt
+add_task(function* test_permissions() {
+  const manifest = {
+    name: "permissions test",
+    description: "permissions test",
+    manifest_version: 2,
+    version: "1.0",
+
+    permissions: ["tabs", "storage", "https://*.example.com/*", "<all_urls>", "experiments.test"],
+  };
+
+  let xpi = ExtensionTestCommon.generateXPI({manifest});
+
+  let install = yield new Promise(resolve => {
+    AddonManager.getInstallForFile(xpi, resolve);
+  });
+
+  let perminfo;
+  install._permHandler = info => {
+    perminfo = info;
+    return Promise.resolve();
+  };
+
+  yield promiseCompleteInstall(install);
+
+  notEqual(perminfo, undefined, "Permission handler was invoked");
+  equal(perminfo.existingAddon, null, "Permission info does not include an existing addon");
+  notEqual(perminfo.addon, null, "Permission info includes the new addon");
+  let perms = perminfo.addon.userPermissions;
+  deepEqual(perms.permissions, ["tabs", "storage"], "API permissions are correct");
+  deepEqual(perms.hosts, ["https://*.example.com/*", "<all_urls>"], "Host permissions are correct");
+  deepEqual(perms.apis, ["test"], "Experiments permissions are correct");
+
+  let addon = yield promiseAddonByID(perminfo.addon.id);
+  notEqual(addon, null, "Extension was installed");
+
+  addon.uninstall();
+  yield OS.File.remove(xpi.path);
+});
+
+// Check permissions prompt cancellation
+add_task(function* test_permissions() {
+  const manifest = {
+    name: "permissions test",
+    description: "permissions test",
+    manifest_version: 2,
+    version: "1.0",
+
+    permissions: ["webRequestBlocking"],
+  };
+
+  let xpi = ExtensionTestCommon.generateXPI({manifest});
+
+  let install = yield new Promise(resolve => {
+    AddonManager.getInstallForFile(xpi, resolve);
+  });
+
+  let perminfo;
+  install._permHandler = info => {
+    perminfo = info;
+    return Promise.reject();
+  };
+
+  yield promiseCompleteInstall(install);
+
+  notEqual(perminfo, undefined, "Permission handler was invoked");
+
+  let addon = yield promiseAddonByID(perminfo.addon.id);
+  equal(addon, null, "Extension was not installed");
+
+  yield OS.File.remove(xpi.path);
+});