Bug 1294811: Move AddonManager test helpers to a shared test module. r?rhelmer draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 13 Aug 2016 20:13:28 -0700
changeset 400482 63da0f601d80ecbde780a0c333288e69dc29231c
parent 399874 4c4fcb84ee3930af35eb6cb83c044d6ab8a08abb
child 528216 a951767ea7fe7b32cf541b7aa3e5346e3e4c7869
push id26162
push usermaglione.k@gmail.com
push dateSun, 14 Aug 2016 03:30:23 +0000
reviewersrhelmer
bugs1294811
milestone51.0a1
Bug 1294811: Move AddonManager test helpers to a shared test module. r?rhelmer Most of the AddonTestUtils code is simply moved from head_addons.js, but I did significantly refactor some of the especially crufty parts. MozReview-Commit-ID: K4vIqnI1qhY
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/moz.build
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js
toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js
toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -0,0 +1,1141 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var EXPORTED_SYMBOLS = ["AddonTestUtils", "MockAsyncShutdown"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
+const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
+
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
+const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required";
+
+
+Cu.importGlobalProperties(["fetch", "TextEncoder"]);
+
+Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+                                  "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
+                                  "resource://testing-common/MockRegistrar.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
+                                  "resource://testing-common/MockRegistry.jsm");
+
+
+XPCOMUtils.defineLazyServiceGetter(this, "rdfService",
+                                   "@mozilla.org/rdf/rdf-service;1", "nsIRDFService");
+
+
+XPCOMUtils.defineLazyGetter(this, "AppInfo", () => {
+  let AppInfo = {};
+  Cu.import("resource://testing-common/AppInfo.jsm", AppInfo);
+  return AppInfo;
+});
+
+
+const ArrayBufferInputStream = Components.Constructor(
+  "@mozilla.org/io/arraybuffer-input-stream;1",
+  "nsIArrayBufferInputStream", "setData");
+
+const nsFile = Components.Constructor(
+  "@mozilla.org/file/local;1",
+  "nsIFile", "initWithPath");
+
+const RDFXMLParser = Components.Constructor(
+  "@mozilla.org/rdf/xml-parser;1",
+  "nsIRDFXMLParser", "parseString");
+
+const RDFDataSource = Components.Constructor(
+  "@mozilla.org/rdf/datasource;1?name=in-memory-datasource",
+  "nsIRDFDataSource");
+
+const ZipReader = Components.Constructor(
+  "@mozilla.org/libjar/zip-reader;1",
+  "nsIZipReader", "open");
+
+const ZipWriter = Components.Constructor(
+  "@mozilla.org/zipwriter;1",
+  "nsIZipWriter", "open");
+
+
+// We need some internal bits of AddonManager
+var AMscope = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+var {AddonManager, AddonManagerInternal, AddonManagerPrivate} = AMscope;
+
+
+// Mock out AddonManager's reference to the AsyncShutdown module so we can shut
+// down AddonManager from the test
+var MockAsyncShutdown = {
+  hook: null,
+  status: null,
+  profileBeforeChange: {
+    addBlocker: function(aName, aBlocker, aOptions) {
+      // do_print("Mock profileBeforeChange blocker for '" + aName + "'");
+      MockAsyncShutdown.hook = aBlocker;
+      MockAsyncShutdown.status = aOptions.fetchState;
+    }
+  },
+  // We can use the real Barrier
+  Barrier: AsyncShutdown.Barrier
+};
+
+AMscope.AsyncShutdown = MockAsyncShutdown;
+
+
+/**
+ * Escapes any occurances of &, ", < or > with XML entities.
+ *
+ * @param {string} str
+ *        The string to escape.
+ * @return {string} The escaped string.
+ */
+function escapeXML(str) {
+  let replacements = {"&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;"};
+  return String(str).replace(/[&"<>]/g, m => replacements[m]);
+}
+
+/**
+ * A tagged template function which escapes any XML metacharacters in
+ * interpolated values.
+ */
+function escaped(strings, ...values) {
+  let result = [];
+
+  for (let [i, string] of strings.entries()) {
+    result.push(string);
+    if (i < values.length)
+      result.push(escapeXML(values[i]));
+  }
+
+  return result.join("");
+}
+
+
+class AddonsList {
+  constructor(extensionsINI) {
+    this.multiprocessIncompatibleIDs = new Set();
+
+    if (!extensionsINI.exists()) {
+      this.extensions = [];
+      this.themes = [];
+      return;
+    }
+
+    let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+                  .getService(Ci.nsIINIParserFactory);
+
+    let parser = factory.createINIParser(extensionsINI);
+
+    function readDirectories(section) {
+      var dirs = [];
+      var keys = parser.getKeys(section);
+      for (let key of XPCOMUtils.IterStringEnumerator(keys)) {
+        let descriptor = parser.getString(section, key);
+
+        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        try {
+          file.persistentDescriptor = descriptor;
+        } catch (e) {
+          // Throws if the directory doesn't exist, we can ignore this since the
+          // platform will too.
+          continue;
+        }
+        dirs.push(file);
+      }
+      return dirs;
+    }
+
+    this.extensions = readDirectories("ExtensionDirs");
+    this.themes = readDirectories("ThemeDirs");
+
+    var keys = parser.getKeys("MultiprocessIncompatibleExtensions");
+    for (let key of XPCOMUtils.IterStringEnumerator(keys)) {
+      let id = parser.getString("MultiprocessIncompatibleExtensions", key);
+      this.multiprocessIncompatibleIDs.add(id);
+    }
+  }
+
+  hasItem(aType, aDir, aId) {
+    var path = aDir.clone();
+    path.append(aId);
+
+    var xpiPath = aDir.clone();
+    xpiPath.append(aId + ".xpi");
+
+    return this[aType].some(file => {
+      if (!file.exists())
+        throw new Error("Non-existant path found in extensions.ini: " + file.path)
+
+      if (file.isDirectory())
+        return file.equals(path);
+      if (file.isFile())
+        return file.equals(xpiPath);
+      return false;
+    });
+  }
+
+  isMultiprocessIncompatible(aId) {
+    return this.multiprocessIncompatibleIDs.has(aId);
+  }
+
+  hasTheme(aDir, aId) {
+    return this.hasItem("themes", aDir, aId);
+  }
+
+  hasExtension(aDir, aId) {
+    return this.hasItem("extensions", aDir, aId);
+  }
+}
+
+var AddonTestUtils = {
+  addonIntegrationService: null,
+  addonsList: null,
+  appInfo: null,
+  extensionsINI: null,
+  testUnpacked: false,
+  useRealCertChecks: false,
+
+  init(testScope) {
+    this.testScope = testScope;
+
+    // Get the profile directory for tests to use.
+    this.profileDir = testScope.do_get_profile();
+
+    this.extensionsINI = this.profileDir.clone();
+    this.extensionsINI.append("extensions.ini");
+
+    // Register a temporary directory for the tests.
+    this.tempDir = this.profileDir.clone();
+    this.tempDir.append("temp");
+    this.tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    this.registerDirectory("TmpD", this.tempDir);
+
+    // Create a replacement app directory for the tests.
+    const appDirForAddons = this.profileDir.clone();
+    appDirForAddons.append("appdir-addons");
+    appDirForAddons.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    this.registerDirectory("XREAddonAppDir", appDirForAddons);
+
+
+    // Enable more extensive EM logging
+    Services.prefs.setBoolPref("extensions.logging.enabled", true);
+
+    // By default only load extensions from the profile install location
+    Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE);
+
+    // By default don't disable add-ons from any scope
+    Services.prefs.setIntPref("extensions.autoDisableScopes", 0);
+
+    // By default, don't cache add-ons in AddonRepository.jsm
+    Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
+
+    // Disable the compatibility updates window by default
+    Services.prefs.setBoolPref("extensions.showMismatchUI", false);
+
+    // Point update checks to the local machine for fast failures
+    Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL");
+    Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL");
+    Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
+    Services.prefs.setCharPref("services.settings.server", "http://localhost/dummy-kinto/v1");
+
+    // By default ignore bundled add-ons
+    Services.prefs.setBoolPref("extensions.installDistroAddons", false);
+
+    // By default don't check for hotfixes
+    Services.prefs.setCharPref("extensions.hotfix.id", "");
+
+    // Ensure signature checks are enabled by default
+    Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+
+
+    // Write out an empty blocklist.xml file to the profile to ensure nothing
+    // is blocklisted by default
+    var blockFile = OS.Path.join(this.profileDir.path, "blocklist.xml");
+
+    var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+               "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">\n" +
+               "</blocklist>\n";
+
+    this.awaitPromise(OS.File.writeAtomic(blockFile, new TextEncoder().encode(data)));
+
+
+    // Make sure that a given path does not exist
+    function pathShouldntExist(file) {
+      if (file.exists()) {
+        throw new Error(`Test cleanup: path ${file.path} exists when it should not`);
+      }
+    }
+
+    testScope.do_register_cleanup(() => {
+      for (let file of this.tempXPIs) {
+        if (file.exists())
+          file.remove(false);
+      }
+
+      // Check that the temporary directory is empty
+      var dirEntries = this.tempDir.directoryEntries
+                           .QueryInterface(Ci.nsIDirectoryEnumerator);
+      var entry;
+      while ((entry = dirEntries.nextFile)) {
+        throw new Error(`Found unexpected file in temporary directory: ${entry.leafName}`);
+      }
+      dirEntries.close();
+
+      try {
+        appDirForAddons.remove(true);
+      } catch (ex) {
+        testScope.do_print("Got exception removing addon app dir, " + ex);
+      }
+
+      var testDir = this.profileDir.clone();
+      testDir.append("extensions");
+      testDir.append("trash");
+      pathShouldntExist(testDir);
+
+      testDir.leafName = "staged";
+      pathShouldntExist(testDir);
+
+      // Clear commonly set prefs.
+      try {
+        Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
+      } catch (e) {}
+      try {
+        Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY);
+      } catch (e) {}
+
+      return this.promiseShutdownManager();
+    });
+  },
+
+  /**
+   * Helper to spin the event loop until a promise resolves or rejects
+   *
+   * @param {Promise} promise
+   * @returns {*} The promise's resolution value.
+   * @throws The promise's rejection value, if it rejects.
+   */
+  awaitPromise(promise) {
+    let done = false;
+    let result;
+    let error;
+    promise.then(
+      val => { result = val; },
+      err => { error = err; }
+    ).then(() => {
+      done = true;
+    });
+
+    while (!done)
+      Services.tm.mainThread.processNextEvent(true);
+
+    if (error !== undefined)
+      throw error;
+    return result;
+  },
+
+  createAppInfo(ID, name, version, platformVersion = "1.0") {
+    AppInfo.updateAppInfo({
+      ID, name, version, platformVersion,
+      crashReporter: true,
+      extraProps: {
+        browserTabsRemoteAutostart: false,
+      },
+    });
+    this.appInfo = AppInfo.getAppInfo();
+  },
+
+  getManifestURI(file) {
+    if (file.isDirectory()) {
+      file.append("install.rdf");
+      if (file.exists()) {
+        return NetUtil.newURI(file);
+      }
+
+      file.leafName = "manifest.json";
+      if (file.exists())
+        return NetUtil.newURI(file);
+
+      throw new Error("No manifest file present");
+    }
+
+    let zip = ZipReader(file);
+    try {
+      let uri = NetUtil.newURI(file);
+
+      if (zip.hasEntry("install.rdf")) {
+        return NetUtil.newURI(`jar:${uri.spec}!/install.rdf`);
+      }
+
+      if (zip.hasEntry("manifest.json")) {
+        return NetUtil.newURI(`jar:${uri.spec}!/manifest.json`);
+      }
+
+      throw new Error("No manifest file present");
+    } finally {
+      zip.close();
+
+      // Make sure to close the open zip file or it will be locked.
+      Services.obs.notifyObservers(file, "flush-cache-entry", "cert-override");
+    }
+  },
+
+  getIDFromManifest: Task.async(function*(manifestURI) {
+    let body = yield fetch(manifestURI.spec);
+
+    if (manifestURI.spec.endsWith(".rdf")) {
+      let data = yield body.text();
+
+      let ds = new RDFDataSource();
+      let rdfParser = new RDFXMLParser(ds, manifestURI, data);
+
+      let rdfID = ds.GetTarget(rdfService.GetResource("urn:mozilla:install-manifest"),
+                               rdfService.GetResource("http://www.mozilla.org/2004/em-rdf#id"),
+                               true);
+      return rdfID.QueryInterface(Ci.nsIRDFLiteral).Value;
+    }
+
+    let manifest = yield body.json();
+    return manifest.applications.gecko.id;
+  }),
+
+  overrideCertDB() {
+    // Unregister the real database. This only works because the add-ons manager
+    // hasn't started up and grabbed the certificate database yet.
+    let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+    let factory = registrar.getClassObject(CERTDB_CID, Ci.nsIFactory);
+    registrar.unregisterFactory(CERTDB_CID, factory);
+
+    // Get the real DB
+    let realCertDB = factory.createInstance(null, Ci.nsIX509CertDB);
+
+
+    let verifyCert = Task.async(function*(file, result, cert, callback) {
+      if (result == Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED &&
+          !this.useRealCertChecks && callback.wrappedJSObject) {
+        // Bypassing XPConnect allows us to create a fake x509 certificate from JS
+        callback = callback.wrappedJSObject;
+
+        try {
+          let manifestURI = this.getManifestURI(file);
+
+          let id = yield this.getIDFromManifest(manifestURI);
+
+          let fakeCert = {commonName: id};
+
+          return [callback, Cr.NS_OK, fakeCert];
+        } catch (e) {
+          // If there is any error then just pass along the original results
+        }
+      }
+
+      return [callback, result, cert];
+    }).bind(this);
+
+
+    function FakeCertDB() {
+      for (let property of Object.keys(realCertDB)) {
+        if (property in this)
+          continue;
+
+        if (typeof realCertDB[property] == "function")
+          this[property] = realCertDB[property].bind(realCertDB);
+      }
+    }
+    FakeCertDB.prototype = {
+      openSignedAppFileAsync(root, file, callback) {
+        // First try calling the real cert DB
+        realCertDB.openSignedAppFileAsync(root, file, (result, zipReader, cert) => {
+          verifyCert(file.clone(), result, cert, callback)
+            .then(([callback, result, cert]) => {
+              callback.openSignedAppFileFinished(result, zipReader, cert);
+            });
+        });
+      },
+
+      verifySignedDirectoryAsync(root, dir, callback) {
+        // First try calling the real cert DB
+        realCertDB.verifySignedDirectoryAsync(root, dir, (result, cert) => {
+          verifyCert(dir.clone(), result, cert, callback)
+            .then(([callback, result, cert]) => {
+              callback.verifySignedDirectoryFinished(result, cert);
+            });
+        });
+      },
+
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIX509CertDB]),
+    };
+
+    let certDBFactory = XPCOMUtils.generateSingletonFactory(FakeCertDB);
+    registrar.registerFactory(CERTDB_CID, "CertDB",
+                              CERTDB_CONTRACTID, certDBFactory);
+  },
+
+  /**
+   * Starts up the add-on manager as if it was started by the application.
+   *
+   * @param {boolean} [appChanged = true]
+   *        An optional boolean parameter to simulate the case where the
+   *        application has changed version since the last run. If not passed it
+   *        defaults to true
+   */
+  promiseStartupManager(appChanged = true) {
+    if (this.addonIntegrationService)
+      throw new Error("Attempting to startup manager that was already started.");
+
+    if (appChanged) {
+      if (this.extensionsINI.exists())
+        this.extensionsINI.remove(true);
+    }
+
+    this.addonIntegrationService = Cc["@mozilla.org/addons/integration;1"]
+          .getService(Ci.nsIObserver);
+
+    this.addonIntegrationService.observe(null, "addons-startup", null);
+
+    this.emit("addon-manager-started");
+
+    // Load the add-ons list as it was after extension registration
+    this.loadAddonsList();
+
+    return Promise.resolve();
+  },
+
+  promiseShutdownManager() {
+    if (!this.addonIntegrationService) {
+      return Promise.resolve(false);
+    }
+
+    Services.obs.notifyObservers(null, "quit-application-granted", null);
+    return MockAsyncShutdown.hook()
+      .then(() => {
+        this.emit("addon-manager-shutdown");
+
+        this.addonIntegrationService = null;
+
+        // Load the add-ons list as it was after application shutdown
+        this.loadAddonsList();
+
+        // Clear any crash report annotations
+        this.appInfo.annotations = {};
+
+        // Force the XPIProvider provider to reload to better
+        // simulate real-world usage.
+        let XPIscope = Cu.import("resource://gre/modules/addons/XPIProvider.jsm");
+        // This would be cleaner if I could get it as the rejection reason from
+        // the AddonManagerInternal.shutdown() promise
+        let shutdownError = XPIscope.XPIProvider._shutdownError;
+
+        AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
+        Cu.unload("resource://gre/modules/addons/XPIProvider.jsm");
+
+        if (shutdownError)
+          throw shutdownError;
+
+        return true;
+      });
+  },
+
+  promiseRestartManager(newVersion) {
+    return this.promiseShutdownManager()
+      .then(() => {
+        if (newVersion)
+          this.appInfo.version = newVersion;
+
+        return this.promiseStartupManager(!!newVersion);
+      });
+  },
+
+  loadAddonsList() {
+    this.addonsList = new AddonsList(this.extensionsINI);
+  },
+
+  /**
+   * Creates an update.rdf structure as a string using for the update data passed.
+   *
+   * @param {Object} aData
+   *        The update data as a JS object. Each property name is an add-on ID,
+   *        the property value is an array of each version of the add-on. Each
+   *        array value is a JS object containing the data for the version, at
+   *        minimum a "version" and "targetApplications" property should be
+   *        included to create a functional update manifest.
+   * @return {string} The update.rdf structure as a string.
+   */
+  createUpdateRDF(aData) {
+    var rdf = '<?xml version="1.0"?>\n';
+    rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
+           '     xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
+
+    for (let addon in aData) {
+      rdf += escaped`  <Description about="urn:mozilla:extension:${addon}"><em:updates><Seq>\n`;
+
+      for (let versionData of aData[addon]) {
+        rdf += '    <li><Description>\n';
+
+        rdf += this._writeProps(versionData, ["version", "multiprocessCompatible"],
+                                `      `);
+
+        for (let app of versionData.targetApplications || []) {
+          rdf += "      <em:targetApplication><Description>\n";
+          rdf += this._writeProps(app, ["id", "minVersion", "maxVersion", "updateLink", "updateHash"],
+                                  `        `)
+          rdf += "      </Description></em:targetApplication>\n";
+        }
+
+        rdf += '    </Description></li>\n';
+      }
+
+      rdf += '  </Seq></em:updates></Description>\n'
+    }
+    rdf += "</RDF>\n";
+
+    return rdf;
+  },
+
+  _writeProps(obj, props, indent = "  ") {
+    let items = [];
+    for (let prop of props) {
+      if (prop in obj)
+        items.push(escaped`${indent}<em:${prop}>${obj[prop]}</em:${prop}>\n`);
+    }
+    return items.join("");
+  },
+
+  _writeArrayProps(obj, props, indent = "  ") {
+    let items = [];
+    for (let prop of props) {
+      for (let val of obj[prop] || [])
+        items.push(escaped`${indent}<em:${prop}>${val}</em:${prop}>\n`);
+    }
+    return items.join("");
+  },
+
+  _writeLocaleStrings(aData) {
+    let items = [];
+
+    items.push(this._writeProps(aData, ["name", "description", "creator", "homepageURL"]));
+
+    items.push(this._writeArrayProps(aData, ["developer", "translator", "contributor"]));
+
+    return items.join("");
+  },
+
+  createInstallRDF(aData) {
+    var rdf = '<?xml version="1.0"?>\n';
+    rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
+           '     xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
+
+    rdf += '<Description about="urn:mozilla:install-manifest">\n';
+
+    let props = ["id", "version", "type", "internalName", "updateURL", "updateKey",
+                 "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
+                 "skinnable", "bootstrap", "unpack", "strictCompatibility", "multiprocessCompatible"];
+    rdf += this._writeProps(aData, props);
+
+    rdf += this._writeLocaleStrings(aData);
+
+    for (let platform of aData.targetPlatforms || [])
+      rdf += escaped`<em:targetPlatform>${platform}</em:targetPlatform>\n`;
+
+    for (let app of aData.targetApplications || []) {
+      rdf += "<em:targetApplication><Description>\n";
+      rdf += this._writeProps(app, ["id", "minVersion", "maxVersion"]);
+      rdf += "</Description></em:targetApplication>\n";
+    }
+
+    for (let localized of aData.localized || []) {
+      rdf += "<em:localized><Description>\n";
+      for (let localeName of localized.locale || [])
+        rdf += escaped`<em:locale>${localeName}</em:locale>\n`;
+      rdf += this._writeLocaleStrings(localized);
+      rdf += "</Description></em:localized>\n";
+    }
+
+    for (let dep of aData.dependencies || [])
+      rdf += escaped`<em:dependency><Description em:id="${dep}"/></em:dependency>\n`;
+
+    rdf += "</Description>\n</RDF>\n";
+    return rdf;
+  },
+
+  /**
+   * Recursively create all directories upto and including the given
+   * path, if they do not exist.
+   *
+   * @param {string} path The path of the directory to create.
+   * @returns {Promise}
+   */
+  recursiveMakeDir(path) {
+    let paths = [];
+    for (; path; path = OS.Path.dirname(path)) {
+      paths.push(path);
+    }
+
+    return Promise.all(paths.reverse() .map(path =>
+      OS.File.makeDir(path, {ignoreExisting: true})));
+  },
+
+  /**
+   * Writes the given data to a file in the given zip file.
+   *
+   * @param {string|nsIFile} zipFile
+   *        The zip file to write to.
+   * @param {Object} files
+   *        An object containing filenames and the data to write to the
+   *        corresponding paths in the zip file.
+   * @param {integer} [flags = 0]
+   *        Additional flags to open the file with.
+   */
+  writeFilesToZip(zipFile, files, flags = 0) {
+    if (typeof zipFile == "string")
+      zipFile = nsFile(zipFile);
+
+    var zipW = ZipWriter(zipFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | flags);
+
+    for (let [path, data] of Object.entries(files)) {
+      if (!(data instanceof ArrayBuffer))
+        data = new TextEncoder("utf-8").encode(data).buffer;
+
+      let stream = ArrayBufferInputStream(data, 0, data.byteLength);
+
+      // Note these files are being created in the XPI archive with date "0" which is 1970-01-01.
+      zipW.addEntryStream(path, 0, Ci.nsIZipWriter.COMPRESSION_NONE,
+                          stream, false);
+    }
+
+    zipW.close();
+  },
+
+  promiseWriteFilesToZip: Task.async(function*(zip, files, flags) {
+    yield this.recursiveMakeDir(OS.Path.dirname(zip));
+
+    this.writeFilesToZip(zip, files, flags);
+
+    return Promise.resolve(nsFile(zip));
+  }),
+
+  promiseWriteFilesToDir: Task.async(function*(dir, files) {
+    yield this.recursiveMakeDir(dir);
+
+    for (let [path, data] of Object.entries(files)) {
+      path = path.split("/");
+      let dirPath = dir;
+      let leafName = path.pop();
+
+      // Create parent directories, if necessary.
+      for (let subDir of path) {
+        dirPath = OS.Path.join(dirPath, subDir);
+        yield OS.Path.makeDir(dirPath, {ignoreExisting: true});
+      }
+
+      if (typeof data == "string")
+        data = new TextEncoder("utf-8").encode(data);
+
+      yield OS.File.writeAtomic(OS.Path.join(dirPath, leafName), data);
+    }
+
+    return nsFile(dir);
+  }),
+
+  promiseWriteFilesToExtension(dir, id, files, unpacked = this.testUnpacked) {
+    if ("install.rdf" in files && typeof files["install.rdf"] !== "string") {
+      files["install.rdf"] = this.createInstallRDF(files["install.rdf"]);
+    }
+
+    if (unpacked) {
+      let path = OS.Path.join(dir, id);
+
+      return this.promiseWriteFilesToDir(path, files);
+    }
+
+    let xpi = OS.Path.join(dir, `${id}.xpi`);
+
+    return this.promiseWriteFilesToZip(xpi, files);
+  },
+
+  tempXPIs: [],
+  /**
+   * Creates an XPI file for some manifest data in the temporary directory and
+   * returns the nsIFile for it. The file will be deleted when the test completes.
+   *
+   * @param {object} files
+   *          The object holding data about the add-on
+   * @return {nsIFile} A file pointing to the created XPI file
+   */
+  createTempXPIFile(files) {
+    var file = this.tempDir.clone();
+    file.append("foo.xpi");
+    do {
+      file.leafName = Math.floor(Math.random() * 1000000) + ".xpi";
+    } while (file.exists());
+
+    this.tempXPIs.push(file);
+
+    if ("install.rdf" in files && typeof files["install.rdf"] !== "string") {
+      files["install.rdf"] = this.createInstallRDF(files["install.rdf"]);
+    }
+
+    this.writeFilesToZip(file.path, files);
+    return file;
+  },
+
+  /**
+   * Creates an XPI file for some WebExtension data in the temporary directory and
+   * returns the nsIFile for it. The file will be deleted when the test completes.
+   *
+   * @param {Object} data
+   *        The object holding data about the add-on, as expected by
+   *        |Extension.generateXPI|.
+   * @return {nsIFile} A file pointing to the created XPI file
+   */
+  createTempWebExtensionFile(data) {
+    let file = Extension.generateXPI(data);
+    this.tempXPIs.push(file);
+    return file;
+  },
+
+  /**
+   * Creates an extension proxy file.
+   * See: https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment#Firefox_extension_proxy_file
+   * @param {nsIFile} aDir
+   *        The directory to add the proxy file to.
+   * @param {nsIFile} aAddon
+   *        An nsIFile for the add-on file that this is a proxy file for.
+   * @param {string} aId
+   *        A string to use for the add-on ID.
+   */
+  promiseWriteProxyFileToDir(aDir, aAddon, aId) {
+    let files = {
+      [aId]: aAddon.path,
+    };
+
+    return this.promiseWriteFilesToDir(aDir.path, files);
+  },
+
+  /**
+   * Manually installs an XPI file into an install location by either copying the
+   * XPI there or extracting it depending on whether unpacking is being tested
+   * or not.
+   *
+   * @param {nsIFile} aXPIFile
+   *        The XPI file to install.
+   * @param {nsIFile} aInstallLocation
+   *        The install location (an nsIFile) to install into.
+   * @param {string} aID
+   *        The ID to install as.
+   * @param {boolean} [unpacked = this.testUnpacked]
+   *        If true, install as an unpacked directory, rather than a
+   *        packed XPI.
+   */
+  manuallyInstall(aXPIFile, aInstallLocation, aID, unpacked = this.testUnpacked) {
+    if (unpacked) {
+      let dir = aInstallLocation.clone();
+      dir.append(aID);
+      dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+      let zip = ZipReader(aXPIFile);
+      let entries = zip.findEntries(null);
+      while (entries.hasMore()) {
+        let entry = entries.getNext();
+        let target = dir.clone();
+        entry.split("/").forEach(function(aPart) {
+          target.append(aPart);
+        });
+        zip.extract(entry, target);
+      }
+      zip.close();
+
+      return dir;
+    }
+
+    let target = aInstallLocation.clone();
+    target.append(aID + ".xpi");
+    aXPIFile.copyTo(target.parent, target.leafName);
+    return target;
+  },
+
+  /**
+   * Manually uninstalls an add-on by removing its files from the install
+   * location.
+   *
+   * @param {nsIFile} aInstallLocation
+   *        The nsIFile of the install location to remove from.
+   * @param {string} aID
+   *        The ID of the add-on to remove.
+   * @param {boolean} [unpacked = this.testUnpacked]
+   *        If true, uninstall an unpacked directory, rather than a
+   *        packed XPI.
+   */
+  manuallyUninstall(aInstallLocation, aID, unpacked = this.testUnpacked) {
+    let file = this.getFileForAddon(aInstallLocation, aID, unpacked);
+
+    // In reality because the app is restarted a flush isn't necessary for XPIs
+    // removed outside the app, but for testing we must flush manually.
+    if (file.isFile())
+      Services.obs.notifyObservers(file, "flush-cache-entry", null);
+
+    file.remove(true);
+  },
+
+  /**
+   * Gets the nsIFile for where an add-on is installed. It may point to a file or
+   * a directory depending on whether add-ons are being installed unpacked or not.
+   *
+   * @param {nsIFile} aDir
+   *         The nsIFile for the install location
+   * @param {string} aId
+   *        The ID of the add-on
+   * @param {boolean} [unpacked = this.testUnpacked]
+   *        If true, return the path to an unpacked directory, rather than a
+   *        packed XPI.
+   * @return {nsIFile}
+   */
+  getFileForAddon(aDir, aId, unpacked = this.testUnpacked) {
+    var dir = aDir.clone();
+    if (unpacked)
+      dir.append(aId);
+    else
+      dir.append(`${aId}.xpi`);
+    return dir;
+  },
+
+  /**
+   * Sets the last modified time of the extension, usually to trigger an update
+   * of its metadata. If the extension is unpacked, this function assumes that
+   * the extension contains only the install.rdf file.
+   *
+   * @param {nsIFile} aExt A file pointing to either the packed extension or its unpacked directory.
+   * @param {number} aTime The time to which we set the lastModifiedTime of the extension
+   *
+   * @deprecated Please use promiseSetExtensionModifiedTime instead
+   */
+  setExtensionModifiedTime(aExt, aTime) {
+    aExt.lastModifiedTime = aTime;
+    if (aExt.isDirectory()) {
+      let entries = aExt.directoryEntries
+                        .QueryInterface(Ci.nsIDirectoryEnumerator);
+      while (entries.hasMoreElements())
+        this.setExtensionModifiedTime(entries.nextFile, aTime);
+      entries.close();
+    }
+  },
+
+  promiseSetExtensionModifiedTime: Task.async(function*(aPath, aTime) {
+    yield OS.File.setDates(aPath, aTime, aTime);
+
+    let entries, iterator;
+    try {
+      let iterator = new OS.File.DirectoryIterator(aPath);
+      entries = yield iterator.nextBatch();
+    } catch (ex) {
+      if (!(ex instanceof OS.File.Error))
+        throw ex;
+      return;
+    } finally {
+      if (iterator) {
+        iterator.close();
+      }
+    }
+    for (let entry of entries) {
+      yield this.promiseSetExtensionModifiedTime(entry.path, aTime);
+    }
+  }),
+
+  registerDirectory(aKey, aDir) {
+    var dirProvider = {
+      getFile(aProp, aPersistent) {
+        aPersistent.value = false;
+        if (aProp == aKey)
+          return aDir.clone();
+        return null;
+      },
+
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+    };
+    Services.dirsvc.registerProvider(dirProvider);
+  },
+
+  /**
+   * Returns a promise that resolves when the given add-on event is fired. The
+   * resolved value is an array of arguments passed for the event.
+   *
+   * @param {string} event
+   * @returns {Promise}
+   */
+  promiseAddonEvent(event) {
+    return new Promise(resolve => {
+      let listener = {
+        [event](...args) {
+          AddonManager.removeAddonListener(listener);
+          resolve(args);
+        },
+      };
+
+      AddonManager.addAddonListener(listener);
+    });
+  },
+
+  /**
+   * A helper method to install AddonInstall and wait for completion.
+   *
+   * @param {AddonInstall} aInstall
+   *        The add-on to install.
+   * @returns {Promise}
+   */
+  promiseCompleteInstall(aInstall) {
+    let listener;
+    return new Promise(resolve => {
+      listener = {
+        onDownloadFailed: resolve,
+        onDownloadCancelled: resolve,
+        onInstallFailed: resolve,
+        onInstallCancelled: resolve,
+        onInstallEnded: resolve,
+        onInstallPostponed: resolve,
+      };
+
+      aInstall.addListener(listener);
+      aInstall.install();
+    }).then(() => {
+      aInstall.removeListener(listener);
+    });
+  },
+
+  /**
+   * A helper method to install a file.
+   *
+   * @param {nsIFile} aFile
+   *        The file to install
+   * @param {boolean} [aIgnoreIncompatible = false]
+   *        Optional parameter to ignore add-ons that are incompatible
+   *        with the application
+   * @returns {Promise}
+   *        Resolves when the install has completed.
+   */
+  promiseInstallFile(aFile, aIgnoreIncompatible = false) {
+    return new Promise((resolve, reject) => {
+      AddonManager.getInstallForFile(aFile, (aInstall) => {
+        if (!aInstall)
+          reject(new Error(`No AddonInstall created for ${aFile.path}`));
+        else if (aInstall.state != AddonManager.STATE_DOWNLOADED)
+          reject(new Error(`Expected file to be downloaded for install of ${aFile.path}`));
+        else if (aIgnoreIncompatible && aInstall.addon.appDisabled)
+          resolve();
+        else
+          resolve(this.promiseCompleteInstall(aInstall));
+      });
+    });
+  },
+
+  /**
+   * A helper method to install an array of files.
+   *
+   * @param {Iterable<nsIFile>} aFiles
+   *        The files to install
+   * @param {boolean} [aIgnoreIncompatible = false]
+   *        Optional parameter to ignore add-ons that are incompatible
+   *        with the application
+   * @returns {Promise}
+   *        Resolves when the installs have completed.
+   */
+  promiseInstallAllFiles(aFiles, aIgnoreIncompatible = false) {
+    return Promise.all(Array.from(
+      aFiles,
+      file => this.promiseInstallFile(file, aIgnoreIncompatible)));
+  },
+
+  promiseCompleteAllInstalls(aInstalls) {
+    return Promise.all(Array.from(aInstalls, this.promiseCompleteInstall));
+  },
+
+  /**
+   * A promise-based variant of AddonManager.getAddonsByIDs.
+   *
+   * @param {Array<string>} list As the first argument of AddonManager.getAddonsByIDs
+   * @return {Promise<Array<Addon>>}
+   */
+  promiseAddonsByIDs(list) {
+    return new Promise(resolve => AddonManager.getAddonsByIDs(list, resolve));
+  },
+
+  /**
+   * A promise-based variant of AddonManager.getAddonByID.
+   *
+   * @param {string} aId The ID of the add-on.
+   * @return {Promise<Addon>}
+   */
+  promiseAddonByID(aId) {
+    return new Promise(resolve => AddonManager.getAddonByID(aId, resolve));
+  },
+
+  /**
+   * A promise-based variant of AddonManager.getAddonsWithOperationsByTypes
+   *
+   * @param {Array<string>} aTypes The first argument to
+   *                       AddonManager.getAddonsWithOperationsByTypes
+   * @return {Promise<Array<Addon>>}
+   */
+  promiseAddonsWithOperationsByTypes(aTypes) {
+    return new Promise(resolve => AddonManager.getAddonsWithOperationsByTypes(aTypes, resolve));
+  },
+
+  /**
+   * Monitors console output for the duration of a task, and returns a promise
+   * which resolves to a tuple containing a list of all console messages
+   * generated during the task's execution, and the result of the task itself.
+   *
+   * @param {function} aTask
+   *                   The task to run while monitoring console output. May be
+   *                   either a generator function, per Task.jsm, or an ordinary
+   *                   function which returns promose.
+   * @return {Promise<[Array<nsIConsoleMessage>, *]>}
+   */
+  promiseConsoleOutput: Task.async(function*(aTask) {
+    const DONE = "=== xpcshell test console listener done ===";
+
+    let listener, messages = [];
+    let awaitListener = new Promise(resolve => {
+      listener = msg => {
+        if (msg == DONE) {
+          resolve();
+        } else {
+          msg instanceof Ci.nsIScriptError;
+          messages.push(msg);
+        }
+      }
+    });
+
+    Services.console.registerListener(listener);
+    try {
+      let result = yield aTask();
+
+      Services.console.logStringMessage(DONE);
+      yield awaitListener;
+
+      return { messages, result };
+    }
+    finally {
+      Services.console.unregisterListener(listener);
+    }
+  }),
+};
+
+for (let [key, val] of Object.entries(AddonTestUtils)) {
+  if (typeof val == "function")
+    AddonTestUtils[key] = val.bind(AddonTestUtils);
+}
+
+EventEmitter.decorate(AddonTestUtils);
--- a/toolkit/mozapps/extensions/internal/moz.build
+++ b/toolkit/mozapps/extensions/internal/moz.build
@@ -16,16 +16,20 @@ EXTRA_JS_MODULES.addons += [
     'LightweightThemeImageOptimizer.jsm',
     'ProductAddonChecker.jsm',
     'SpellCheckDictionaryBootstrap.js',
     'WebExtensionBootstrap.js',
     'XPIProvider.jsm',
     'XPIProviderUtils.js',
 ]
 
+TESTING_JS_MODULES += [
+    'AddonTestUtils.jsm',
+]
+
 # Don't ship unused providers on Android
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
     EXTRA_JS_MODULES.addons += [
         'PluginProvider.jsm',
     ]
 
 EXTRA_PP_JS_MODULES.addons += [
     'AddonConstants.jsm',
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -36,57 +36,85 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 Components.utils.import("resource://gre/modules/Promise.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 const { OS } = Components.utils.import("resource://gre/modules/osfile.jsm", {});
 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, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
                                   "resource://testing-common/MockRegistrar.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
                                   "resource://testing-common/MockRegistry.jsm");
 
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+Object.defineProperty(this, "gAppInfo", {
+  get() {
+    return AddonTestUtils.appInfo;
+  },
+});
+
+Object.defineProperty(this, "gExtensionsINI", {
+  get() {
+    return AddonTestUtils.extensionsINI;
+  },
+});
+
+Object.defineProperty(this, "gInternalManager", {
+  get() {
+    return AddonTestUtils.addonIntegrationService.QueryInterface(AM_Ci.nsITimerCallback);
+  },
+});
+
+Object.defineProperty(this, "gProfD", {
+  get() {
+    return AddonTestUtils.profileDir;
+  },
+});
+
+Object.defineProperty(this, "gTmpD", {
+  get() {
+    return AddonTestUtils.profileDir;
+  },
+});
+
+Object.defineProperty(this, "gUseRealCertChecks", {
+  get() {
+    return AddonTestUtils.useRealCertChecks;
+  },
+  set(val) {
+   return AddonTestUtils.useRealCertChecks = val;
+  },
+});
+
+Object.defineProperty(this, "TEST_UNPACKED", {
+  get() {
+    return AddonTestUtils.testUnpacked;
+  },
+  set(val) {
+   return AddonTestUtils.testUnpacked = val;
+  },
+});
 
 // We need some internal bits of AddonManager
 var AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
 var { AddonManager, AddonManagerInternal, AddonManagerPrivate } = AMscope;
 
-// Mock out AddonManager's reference to the AsyncShutdown module so we can shut
-// down AddonManager from the test
-var MockAsyncShutdown = {
-  hook: null,
-  status: null,
-  profileBeforeChange: {
-    addBlocker: function(aName, aBlocker, aOptions) {
-      do_print("Mock profileBeforeChange blocker for '" + aName + "'");
-      MockAsyncShutdown.hook = aBlocker;
-      MockAsyncShutdown.status = aOptions.fetchState;
-    }
-  },
-  // We can use the real Barrier
-  Barrier: AsyncShutdown.Barrier
-};
-
-AMscope.AsyncShutdown = MockAsyncShutdown;
-
-var gInternalManager = null;
-var gAppInfo = null;
-var gAddonsList;
-
 var gPort = null;
 var gUrlToFileMap = {};
 
-var TEST_UNPACKED = false;
-
 // Map resource://xpcshell-data/ to the data directory
 var resHandler = Services.io.getProtocolHandler("resource")
                          .QueryInterface(AM_Ci.nsISubstitutingProtocolHandler);
 // Allow non-existent files because of bug 1207735
 var dataURI = NetUtil.newURI(do_get_file("data", true));
 resHandler.setSubstitution("xpcshell-data", dataURI);
 
 function isManifestRegistered(file) {
@@ -253,209 +281,29 @@ this.BootstrapMonitor = {
 
       for (let resolve of this.startupPromises)
         resolve();
       this.startupPromises = [];
     }
   }
 }
 
+AddonTestUtils.on("addon-manager-shutdown", () => BootstrapMonitor.shutdownCheck());
+
 function isNightlyChannel() {
   var channel = "default";
   try {
     channel = Services.prefs.getCharPref("app.update.channel");
   }
   catch (e) { }
 
   return channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr";
 }
 
-function createAppInfo(ID, name, version, platformVersion="1.0") {
-  let tmp = {};
-  AM_Cu.import("resource://testing-common/AppInfo.jsm", tmp);
-  tmp.updateAppInfo({
-    ID, name, version, platformVersion,
-    crashReporter: true,
-    extraProps: {
-      browserTabsRemoteAutostart: false,
-    },
-  });
-  gAppInfo = tmp.getAppInfo();
-}
-
-function getManifestURIForBundle(file) {
-  if (file.isDirectory()) {
-    file.append("install.rdf");
-    if (file.exists()) {
-      return NetUtil.newURI(file);
-    }
-
-    file.leafName = "manifest.json";
-    if (file.exists()) {
-      return NetUtil.newURI(file);
-    }
-
-    throw new Error("No manifest file present");
-  }
-
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(file);
-  try {
-    let uri = NetUtil.newURI(file);
-
-    if (zip.hasEntry("install.rdf")) {
-      return NetUtil.newURI("jar:" + uri.spec + "!/" + "install.rdf");
-    }
-
-    if (zip.hasEntry("manifest.json")) {
-      return NetUtil.newURI("jar:" + uri.spec + "!/" + "manifest.json");
-    }
-
-    throw new Error("No manifest file present");
-  }
-  finally {
-    zip.close();
-  }
-}
-
-let getIDForManifest = Task.async(function*(manifestURI) {
-  // Load it
-  let inputStream = yield new Promise((resolve, reject) => {
-    NetUtil.asyncFetch({
-      uri: manifestURI,
-      loadUsingSystemPrincipal: true,
-    }, (inputStream, status) => {
-      if (status != Components.results.NS_OK)
-        reject(status);
-      resolve(inputStream);
-    });
-  });
-
-  // Get the data as a string
-  let data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
-
-  if (manifestURI.spec.endsWith(".rdf")) {
-    let rdfParser = AM_Cc["@mozilla.org/rdf/xml-parser;1"].
-                    createInstance(AM_Ci.nsIRDFXMLParser)
-    let ds = AM_Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
-             createInstance(AM_Ci.nsIRDFDataSource);
-    rdfParser.parseString(ds, manifestURI, data);
-
-    let rdfService = AM_Cc["@mozilla.org/rdf/rdf-service;1"].
-                     getService(AM_Ci.nsIRDFService);
-
-    let rdfID = ds.GetTarget(rdfService.GetResource("urn:mozilla:install-manifest"),
-                             rdfService.GetResource("http://www.mozilla.org/2004/em-rdf#id"),
-                             true);
-    return rdfID.QueryInterface(AM_Ci.nsIRDFLiteral).Value;
-  }
-  let manifest = JSON.parse(data);
-  return manifest.applications.gecko.id;
-});
-
-let gUseRealCertChecks = false;
-function overrideCertDB(handler) {
-  // Unregister the real database. This only works because the add-ons manager
-  // hasn't started up and grabbed the certificate database yet.
-  let registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
-  let factory = registrar.getClassObject(CERTDB_CID, AM_Ci.nsIFactory);
-  registrar.unregisterFactory(CERTDB_CID, factory);
-
-  // Get the real DB
-  let realCertDB = factory.createInstance(null, AM_Ci.nsIX509CertDB);
-
-  let verifyCert = Task.async(function*(caller, file, result, cert, callback) {
-    // If this isn't a callback we can get directly to through JS then just
-    // pass on the results
-    if (!callback.wrappedJSObject) {
-      caller(callback, result, cert);
-      return;
-    }
-
-    // Bypassing XPConnect allows us to create a fake x509 certificate from
-    // JS
-    callback = callback.wrappedJSObject;
-
-    if (gUseRealCertChecks || result != Components.results.NS_ERROR_SIGNED_JAR_NOT_SIGNED) {
-      // If the real DB found a useful result of some kind then pass it on.
-      caller(callback, result, cert);
-      return;
-    }
-
-    try {
-      let manifestURI = getManifestURIForBundle(file);
-
-      let id = yield getIDForManifest(manifestURI);
-
-      // Make sure to close the open zip file or it will be locked.
-      if (file.isFile()) {
-        Services.obs.notifyObservers(file, "flush-cache-entry", "cert-override");
-      }
-
-      let fakeCert = {
-        commonName: id
-      }
-      caller(callback, Components.results.NS_OK, fakeCert);
-    }
-    catch (e) {
-      // If there is any error then just pass along the original results
-      caller(callback, result, cert);
-    }
-  });
-
-  let fakeCertDB = {
-    openSignedAppFileAsync(root, file, callback) {
-      // First try calling the real cert DB
-      realCertDB.openSignedAppFileAsync(root, file, (result, zipReader, cert) => {
-        function call(callback, result, cert) {
-          callback.openSignedAppFileFinished(result, zipReader, cert);
-        }
-
-        verifyCert(call, file.clone(), result, cert, callback);
-      });
-    },
-
-    verifySignedDirectoryAsync(root, dir, callback) {
-      // First try calling the real cert DB
-      realCertDB.verifySignedDirectoryAsync(root, dir, (result, cert) => {
-        function call(callback, result, cert) {
-          callback.verifySignedDirectoryFinished(result, cert);
-        }
-
-        verifyCert(call, dir.clone(), result, cert, callback);
-      });
-    },
-
-    QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIX509CertDB])
-  };
-
-  for (let property of Object.keys(realCertDB)) {
-    if (property in fakeCertDB) {
-      continue;
-    }
-
-    if (typeof realCertDB[property] == "function") {
-      fakeCertDB[property] = realCertDB[property].bind(realCertDB);
-    }
-  }
-
-  let certDBFactory = {
-    createInstance: function(outer, iid) {
-      if (outer != null) {
-        throw Components.results.NS_ERROR_NO_AGGREGATION;
-      }
-      return fakeCertDB.QueryInterface(iid);
-    }
-  };
-  registrar.registerFactory(CERTDB_CID, "CertDB",
-                            CERTDB_CONTRACTID, certDBFactory);
-}
-
-overrideCertDB();
+var {createAppInfo} = AddonTestUtils;
 
 /**
  * Tests that an add-on does appear in the crash report annotations, if
  * crash reporting is enabled. The test will fail if the add-on is not in the
  * annotation.
  * @param  aId
  *         The ID of the add-on
  * @param  aVersion
@@ -705,344 +553,65 @@ function do_check_compatibilityoverride(
 }
 
 function do_check_icons(aActual, aExpected) {
   for (var size in aExpected) {
     do_check_eq(remove_port(aActual[size]), remove_port(aExpected[size]));
   }
 }
 
-// Record the error (if any) from trying to save the XPI
-// database at shutdown time
-var gXPISaveError = null;
-
-/**
- * Starts up the add-on manager as if it was started by the application.
- *
- * @param  aAppChanged
- *         An optional boolean parameter to simulate the case where the
- *         application has changed version since the last run. If not passed it
- *         defaults to true
- */
-function startupManager(aAppChanged) {
-  if (gInternalManager)
-    do_throw("Test attempt to startup manager that was already started.");
-
-  if (aAppChanged || aAppChanged === undefined) {
-    if (gExtensionsINI.exists())
-      gExtensionsINI.remove(true);
-  }
+var {promiseStartupManager} = AddonTestUtils;
+var {promiseRestartManager} = AddonTestUtils;
+var {promiseShutdownManager} = AddonTestUtils;
+var {awaitPromise} = AddonTestUtils;
 
-  gInternalManager = AM_Cc["@mozilla.org/addons/integration;1"].
-                     getService(AM_Ci.nsIObserver).
-                     QueryInterface(AM_Ci.nsITimerCallback);
-
-  gInternalManager.observe(null, "addons-startup", null);
-
-  // Load the add-ons list as it was after extension registration
-  loadAddonsList();
-}
-
-/**
- * Helper to spin the event loop until a promise resolves or rejects
- */
-function loopUntilPromise(aPromise) {
-  let done = false;
-  aPromise.then(
-    () => done = true,
-    err => {
-      do_report_unexpected_exception(err);
-      done = true;
-    });
-
-  let thr = Services.tm.mainThread;
-
-  while (!done) {
-    thr.processNextEvent(true);
-  }
+function startupManager(aAppChanged) {
+  promiseStartupManager(aAppChanged);
 }
 
 /**
  * Restarts the add-on manager as if the host application was restarted.
  *
  * @param  aNewVersion
  *         An optional new version to use for the application. Passing this
  *         will change nsIXULAppInfo.version and make the startup appear as if
  *         the application version has changed.
  */
 function restartManager(aNewVersion) {
-  loopUntilPromise(promiseRestartManager(aNewVersion));
-}
-
-function promiseRestartManager(aNewVersion) {
-  return promiseShutdownManager()
-    .then(null, err => do_report_unexpected_exception(err))
-    .then(() => {
-      if (aNewVersion) {
-        gAppInfo.version = aNewVersion;
-        startupManager(true);
-      }
-      else {
-        startupManager(false);
-      }
-    });
+  awaitPromise(promiseRestartManager(aNewVersion));
 }
 
 function shutdownManager() {
-  loopUntilPromise(promiseShutdownManager());
-}
-
-function promiseShutdownManager() {
-  if (!gInternalManager) {
-    return Promise.resolve(false);
-  }
-
-  let hookErr = null;
-  Services.obs.notifyObservers(null, "quit-application-granted", null);
-  return MockAsyncShutdown.hook()
-    .then(null, err => hookErr = err)
-    .then( () => {
-      BootstrapMonitor.shutdownCheck();
-      gInternalManager = null;
-
-      // Load the add-ons list as it was after application shutdown
-      loadAddonsList();
-
-      // Clear any crash report annotations
-      gAppInfo.annotations = {};
-
-      // Force the XPIProvider provider to reload to better
-      // simulate real-world usage.
-      let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
-      // This would be cleaner if I could get it as the rejection reason from
-      // the AddonManagerInternal.shutdown() promise
-      gXPISaveError = XPIscope.XPIProvider._shutdownError;
-      do_print("gXPISaveError set to: " + gXPISaveError);
-      AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
-      Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm");
-      if (hookErr) {
-        throw hookErr;
-      }
-    });
-}
-
-function loadAddonsList() {
-  function readDirectories(aSection) {
-    var dirs = [];
-    var keys = parser.getKeys(aSection);
-    while (keys.hasMore()) {
-      let descriptor = parser.getString(aSection, keys.getNext());
-      try {
-        let file = AM_Cc["@mozilla.org/file/local;1"].
-                   createInstance(AM_Ci.nsIFile);
-        file.persistentDescriptor = descriptor;
-        dirs.push(file);
-      }
-      catch (e) {
-        // Throws if the directory doesn't exist, we can ignore this since the
-        // platform will too.
-      }
-    }
-    return dirs;
-  }
-
-  gAddonsList = {
-    extensions: [],
-    themes: [],
-    mpIncompatible: new Set()
-  };
-
-  if (!gExtensionsINI.exists())
-    return;
-
-  var factory = AM_Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
-                getService(AM_Ci.nsIINIParserFactory);
-  var parser = factory.createINIParser(gExtensionsINI);
-  gAddonsList.extensions = readDirectories("ExtensionDirs");
-  gAddonsList.themes = readDirectories("ThemeDirs");
-  var keys = parser.getKeys("MultiprocessIncompatibleExtensions");
-  while (keys.hasMore()) {
-    let id = parser.getString("MultiprocessIncompatibleExtensions", keys.getNext());
-    gAddonsList.mpIncompatible.add(id);
-  }
-}
-
-function isItemInAddonsList(aType, aDir, aId) {
-  var path = aDir.clone();
-  path.append(aId);
-  var xpiPath = aDir.clone();
-  xpiPath.append(aId + ".xpi");
-  for (var i = 0; i < gAddonsList[aType].length; i++) {
-    let file = gAddonsList[aType][i];
-    if (!file.exists())
-      do_throw("Non-existant path found in extensions.ini: " + file.path)
-    if (file.isDirectory() && file.equals(path))
-      return true;
-    if (file.isFile() && file.equals(xpiPath))
-      return true;
-  }
-  return false;
+  awaitPromise(promiseShutdownManager());
 }
 
 function isItemMarkedMPIncompatible(aId) {
-  return gAddonsList.mpIncompatible.has(aId);
+  return AddonTestUtils.addonsList.isMultiprocessIncompatible(aId);
 }
 
 function isThemeInAddonsList(aDir, aId) {
-  return isItemInAddonsList("themes", aDir, aId);
+  return AddonTestUtils.addonsList.hasTheme(aDir, aId);
 }
 
 function isExtensionInAddonsList(aDir, aId) {
-  return isItemInAddonsList("extensions", aDir, aId);
+  return AddonTestUtils.addonsList.hasExtension(aDir, aId);
 }
 
 function check_startup_changes(aType, aIds) {
   var ids = aIds.slice(0);
   ids.sort();
   var changes = AddonManager.getStartupChanges(aType);
   changes = changes.filter(aEl => /@tests.mozilla.org$/.test(aEl));
   changes.sort();
 
   do_check_eq(JSON.stringify(ids), JSON.stringify(changes));
 }
 
-/**
- * Escapes any occurances of &, ", < or > with XML entities.
- *
- * @param   str
- *          The string to escape
- * @return  The escaped string
- */
-function escapeXML(aStr) {
-  return aStr.toString()
-             .replace(/&/g, "&amp;")
-             .replace(/"/g, "&quot;")
-             .replace(/</g, "&lt;")
-             .replace(/>/g, "&gt;");
-}
-
-function writeLocaleStrings(aData) {
-  let rdf = "";
-  ["name", "description", "creator", "homepageURL"].forEach(function(aProp) {
-    if (aProp in aData)
-      rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n";
-  });
-
-  ["developer", "translator", "contributor"].forEach(function(aProp) {
-    if (aProp in aData) {
-      aData[aProp].forEach(function(aValue) {
-        rdf += "<em:" + aProp + ">" + escapeXML(aValue) + "</em:" + aProp + ">\n";
-      });
-    }
-  });
-  return rdf;
-}
-
-/**
- * Creates an update.rdf structure as a string using for the update data passed.
- *
- * @param   aData
- *          The update data as a JS object. Each property name is an add-on ID,
- *          the property value is an array of each version of the add-on. Each
- *          array value is a JS object containing the data for the version, at
- *          minimum a "version" and "targetApplications" property should be
- *          included to create a functional update manifest.
- * @return  the update.rdf structure as a string.
- */
-function createUpdateRDF(aData) {
-  var rdf = '<?xml version="1.0"?>\n';
-  rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
-         '     xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
-
-  for (let addon in aData) {
-    rdf += '  <Description about="urn:mozilla:extension:' + escapeXML(addon) + '"><em:updates><Seq>\n';
-
-    for (let versionData of aData[addon]) {
-      rdf += '    <li><Description>\n';
-
-      for (let prop of ["version", "multiprocessCompatible"]) {
-        if (prop in versionData)
-          rdf += "      <em:" + prop + ">" + escapeXML(versionData[prop]) + "</em:" + prop + ">\n";
-      }
-
-      if ("targetApplications" in versionData) {
-        for (let app of versionData.targetApplications) {
-          rdf += "      <em:targetApplication><Description>\n";
-          for (let prop of ["id", "minVersion", "maxVersion", "updateLink", "updateHash"]) {
-            if (prop in app)
-              rdf += "        <em:" + prop + ">" + escapeXML(app[prop]) + "</em:" + prop + ">\n";
-          }
-          rdf += "      </Description></em:targetApplication>\n";
-        }
-      }
-
-      rdf += '    </Description></li>\n';
-    }
-
-    rdf += '  </Seq></em:updates></Description>\n'
-  }
-  rdf += "</RDF>\n";
-
-  return rdf;
-}
-
-function createInstallRDF(aData) {
-  var rdf = '<?xml version="1.0"?>\n';
-  rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
-         '     xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
-  rdf += '<Description about="urn:mozilla:install-manifest">\n';
-
-  ["id", "version", "type", "internalName", "updateURL", "updateKey",
-   "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
-   "skinnable", "bootstrap", "unpack", "strictCompatibility", "multiprocessCompatible"].forEach(function(aProp) {
-    if (aProp in aData)
-      rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n";
-  });
-
-  rdf += writeLocaleStrings(aData);
-
-  if ("targetPlatforms" in aData) {
-    aData.targetPlatforms.forEach(function(aPlatform) {
-      rdf += "<em:targetPlatform>" + escapeXML(aPlatform) + "</em:targetPlatform>\n";
-    });
-  }
-
-  if ("targetApplications" in aData) {
-    aData.targetApplications.forEach(function(aApp) {
-      rdf += "<em:targetApplication><Description>\n";
-      ["id", "minVersion", "maxVersion"].forEach(function(aProp) {
-        if (aProp in aApp)
-          rdf += "<em:" + aProp + ">" + escapeXML(aApp[aProp]) + "</em:" + aProp + ">\n";
-      });
-      rdf += "</Description></em:targetApplication>\n";
-    });
-  }
-
-  if ("localized" in aData) {
-    aData.localized.forEach(function(aLocalized) {
-      rdf += "<em:localized><Description>\n";
-      if ("locale" in aLocalized) {
-        aLocalized.locale.forEach(function(aLocaleName) {
-          rdf += "<em:locale>" + escapeXML(aLocaleName) + "</em:locale>\n";
-        });
-      }
-      rdf += writeLocaleStrings(aLocalized);
-      rdf += "</Description></em:localized>\n";
-    });
-  }
-
-  if ("dependencies" in aData) {
-    aData.dependencies.forEach(function(aDependency) {
-      rdf += `<em:dependency><Description em:id="${escapeXML(aDependency)}"/></em:dependency>\n`;
-    });
-  }
-
-  rdf += "</Description>\n</RDF>\n";
-  return rdf;
-}
+var {createUpdateRDF} = AddonTestUtils;
+var {createInstallRDF} = AddonTestUtils;
 
 /**
  * Writes an install.rdf manifest into a directory using the properties passed
  * in a JS object. The objects should contain a property for each property to
  * appear in the RDF. The object may contain an array of objects with id,
  * minVersion and maxVersion in the targetApplications property to give target
  * application compatibility.
  *
@@ -1051,44 +620,63 @@ function createInstallRDF(aData) {
  * @param   aDir
  *          The directory to add the install.rdf to
  * @param   aId
  *          An optional string to override the default installation aId
  * @param   aExtraFile
  *          An optional dummy file to create in the directory
  * @return  An nsIFile for the directory in which the add-on is installed.
  */
-function writeInstallRDFToDir(aData, aDir, aId, aExtraFile) {
-  var id = aId ? aId : aData.id
+function writeInstallRDFToDir(aData, aDir, aId = aData.id, aExtraFile = null) {
+  let files = {
+    "install.rdf": AddonTestUtils.createInstallRDF(aData),
+  };
+  if (aExtraFile)
+    files[aExtraFile] = "";
 
-  var dir = aDir.clone();
-  dir.append(id);
+  let dir = aDir.clone();
+  dir.append(aId);
+
+  awaitPromise(AddonTestUtils.promiseWriteFilesToDir(dir.path, files))
+  return dir;
+}
 
-  var rdf = createInstallRDF(aData);
-  if (!dir.exists())
-    dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-  var file = dir.clone();
-  file.append("install.rdf");
-  if (file.exists())
-    file.remove(true);
-  var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
-            createInstance(AM_Ci.nsIFileOutputStream);
-  fos.init(file,
-           FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
-           FileUtils.PERMS_FILE, 0);
-  fos.write(rdf, rdf.length);
-  fos.close();
+/**
+ * Writes an install.rdf manifest into a packed extension using the properties passed
+ * in a JS object. The objects should contain a property for each property to
+ * appear in the RDF. The object may contain an array of objects with id,
+ * minVersion and maxVersion in the targetApplications property to give target
+ * application compatibility.
+ *
+ * @param   aData
+ *          The object holding data about the add-on
+ * @param   aDir
+ *          The install directory to add the extension to
+ * @param   aId
+ *          An optional string to override the default installation aId
+ * @param   aExtraFile
+ *          An optional dummy file to create in the extension
+ * @return  A file pointing to where the extension was installed
+ */
+function writeInstallRDFToXPI(aData, aDir, aId = aData.id, aExtraFile = null) {
+  let files = {
+    "install.rdf": AddonTestUtils.createInstallRDF(aData),
+  };
+  if (aExtraFile)
+    files[aExtraFile] = "";
 
-  if (!aExtraFile)
-    return dir;
+  if (!aDir.exists())
+    aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
-  file = dir.clone();
-  file.append(aExtraFile);
-  file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-  return dir;
+  var file = aDir.clone();
+  file.append(`${aId}.xpi`);
+
+  AddonTestUtils.writeFilesToZip(file.path, files);
+
+  return file;
 }
 
 /**
  * Writes an install.rdf manifest into an extension using the properties passed
  * in a JS object. The objects should contain a property for each property to
  * appear in the RDF. The object may contain an array of objects with id,
  * minVersion and maxVersion in the targetApplications property to give target
  * application compatibility.
@@ -1117,315 +705,58 @@ function writeInstallRDFForExtension(aDa
  * @param   aManifest
  *          The data to write
  * @param   aDir
  *          The install directory to add the extension to
  * @param   aId
  *          An optional string to override the default installation aId
  * @return  A file pointing to where the extension was installed
  */
-function writeWebManifestForExtension(aData, aDir, aId = undefined) {
-  if (!aId)
-    aId = aData.applications.gecko.id;
-
-  if (TEST_UNPACKED) {
-    let dir = aDir.clone();
-    dir.append(aId);
-    if (!dir.exists())
-      dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-
-    let file = dir.clone();
-    file.append("manifest.json");
-    if (file.exists())
-      file.remove(true);
+function writeWebManifestForExtension(aData, aDir, aId = aData.applications.gecko.id) {
+  let files = {
+    "manifest.json": JSON.stringify(aData),
+  }
+  let promise = AddonTestUtils.promiseWriteFilesToExtension(aDir.path, aId, files);
 
-    let data = JSON.stringify(aData);
-    let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
-              createInstance(AM_Ci.nsIFileOutputStream);
-    fos.init(file,
-             FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
-             FileUtils.PERMS_FILE, 0);
-    fos.write(data, data.length);
-    fos.close();
-
-    return dir;
-  }
-  let file = aDir.clone();
-  file.append(aId + ".xpi");
-
-  let stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
-               createInstance(AM_Ci.nsIStringInputStream);
-  stream.setData(JSON.stringify(aData), -1);
-  let zipW = AM_Cc["@mozilla.org/zipwriter;1"].
-             createInstance(AM_Ci.nsIZipWriter);
-  zipW.open(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
-  zipW.addEntryStream("manifest.json", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
-                      stream, false);
-  zipW.close();
-
-  return file;
+  return awaitPromise(promise);
 }
 
-/**
- * Writes an install.rdf manifest into a packed extension using the properties passed
- * in a JS object. The objects should contain a property for each property to
- * appear in the RDF. The object may contain an array of objects with id,
- * minVersion and maxVersion in the targetApplications property to give target
- * application compatibility.
- *
- * @param   aData
- *          The object holding data about the add-on
- * @param   aDir
- *          The install directory to add the extension to
- * @param   aId
- *          An optional string to override the default installation aId
- * @param   aExtraFile
- *          An optional dummy file to create in the extension
- * @return  A file pointing to where the extension was installed
- */
-function writeInstallRDFToXPI(aData, aDir, aId, aExtraFile) {
-  var id = aId ? aId : aData.id
-
-  if (!aDir.exists())
-    aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-
-  var file = aDir.clone();
-  file.append(id + ".xpi");
-  writeInstallRDFToXPIFile(aData, file, aExtraFile);
-
-  return file;
-}
-
-/**
- * Writes the given data to a file in the given zip file.
- *
- * @param   aFile
- *          The zip file to write to.
- * @param   aFiles
- *          An object containing filenames and the data to write to the
- *          corresponding paths in the zip file.
- * @param   aFlags
- *          Additional flags to open the file with.
- */
-function writeFilesToZip(aFile, aFiles, aFlags = 0) {
-  var zipW = AM_Cc["@mozilla.org/zipwriter;1"].createInstance(AM_Ci.nsIZipWriter);
-  zipW.open(aFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | aFlags);
+var {writeFilesToZip} = AddonTestUtils;
 
-  for (let path of Object.keys(aFiles)) {
-    let data = aFiles[path];
-    if (!(data instanceof ArrayBuffer)) {
-      data = new TextEncoder("utf-8").encode(data).buffer;
-    }
-
-    let stream = AM_Cc["@mozilla.org/io/arraybuffer-input-stream;1"]
-      .createInstance(AM_Ci.nsIArrayBufferInputStream);
-    stream.setData(data, 0, data.byteLength);
-
-    // Note these files are being created in the XPI archive with date "0" which is 1970-01-01.
-    zipW.addEntryStream(path, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
-                        stream, false);
-  }
-
-  zipW.close();
-}
-
-/**
- * Writes an install.rdf manifest into an XPI file using the properties passed
- * in a JS object. The objects should contain a property for each property to
- * appear in the RDF. The object may contain an array of objects with id,
- * minVersion and maxVersion in the targetApplications property to give target
- * application compatibility.
- *
- * @param   aData
- *          The object holding data about the add-on
- * @param   aFile
- *          The XPI file to write to. Any existing file will be overwritten
- * @param   aExtraFile
- *          An optional dummy file to create in the extension
- */
-function writeInstallRDFToXPIFile(aData, aFile, aExtraFile) {
-  let files = {
-    "install.rdf": createInstallRDF(aData),
-  };
-
-  if (typeof aExtraFile == "object")
-    Object.assign(files, aExtraFile);
-  else if (aExtraFile)
-    files[aExtraFile] = "";
-
-  writeFilesToZip(aFile, files, FileUtils.MODE_TRUNCATE);
-}
-
-var temp_xpis = [];
 /**
  * Creates an XPI file for some manifest data in the temporary directory and
  * returns the nsIFile for it. The file will be deleted when the test completes.
  *
  * @param   aData
  *          The object holding data about the add-on
  * @return  A file pointing to the created XPI file
  */
 function createTempXPIFile(aData, aExtraFile) {
-  var file = gTmpD.clone();
-  file.append("foo.xpi");
-  do {
-    file.leafName = Math.floor(Math.random() * 1000000) + ".xpi";
-  } while (file.exists());
-
-  temp_xpis.push(file);
-  writeInstallRDFToXPIFile(aData, file, aExtraFile);
-  return file;
-}
-
-/**
- * Creates an XPI file for some WebExtension data in the temporary directory and
- * returns the nsIFile for it. The file will be deleted when the test completes.
- *
- * @param   aData
- *          The object holding data about the add-on, as expected by
- *          |Extension.generateXPI|.
- * @return  A file pointing to the created XPI file
- */
-function createTempWebExtensionFile(aData) {
-  let file = Extension.generateXPI(aData);
-  temp_xpis.push(file);
-  return file;
-}
+  let files = {
+    "install.rdf": aData,
+  };
+  if (typeof aExtraFile == "object")
+    Object.assign(files, aExtraFile);
+  else if (aExtraFile)
+    files[aExtraFile] = "";
 
-/**
- * Sets the last modified time of the extension, usually to trigger an update
- * of its metadata. If the extension is unpacked, this function assumes that
- * the extension contains only the install.rdf file.
- *
- * @param aExt   a file pointing to either the packed extension or its unpacked directory.
- * @param aTime  the time to which we set the lastModifiedTime of the extension
- *
- * @deprecated Please use promiseSetExtensionModifiedTime instead
- */
-function setExtensionModifiedTime(aExt, aTime) {
-  aExt.lastModifiedTime = aTime;
-  if (aExt.isDirectory()) {
-    let entries = aExt.directoryEntries
-                      .QueryInterface(AM_Ci.nsIDirectoryEnumerator);
-    while (entries.hasMoreElements())
-      setExtensionModifiedTime(entries.nextFile, aTime);
-    entries.close();
-  }
-}
-function promiseSetExtensionModifiedTime(aPath, aTime) {
-  return Task.spawn(function* () {
-    yield OS.File.setDates(aPath, aTime, aTime);
-    let entries, iterator;
-    try {
-      let iterator = new OS.File.DirectoryIterator(aPath);
-      entries = yield iterator.nextBatch();
-    } catch (ex) {
-      if (!(ex instanceof OS.File.Error))
-        throw ex;
-      return;
-    } finally {
-      if (iterator) {
-        iterator.close();
-      }
-    }
-    for (let entry of entries) {
-      yield promiseSetExtensionModifiedTime(entry.path, aTime);
-    }
-  });
+  return AddonTestUtils.createTempXPIFile(files);
 }
 
-/**
- * Manually installs an XPI file into an install location by either copying the
- * XPI there or extracting it depending on whether unpacking is being tested
- * or not.
- *
- * @param aXPIFile
- *        The XPI file to install.
- * @param aInstallLocation
- *        The install location (an nsIFile) to install into.
- * @param aID
- *        The ID to install as.
- */
-function manuallyInstall(aXPIFile, aInstallLocation, aID) {
-  if (TEST_UNPACKED) {
-    let dir = aInstallLocation.clone();
-    dir.append(aID);
-    dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-    let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-              createInstance(AM_Ci.nsIZipReader);
-    zip.open(aXPIFile);
-    let entries = zip.findEntries(null);
-    while (entries.hasMore()) {
-      let entry = entries.getNext();
-      let target = dir.clone();
-      entry.split("/").forEach(function(aPart) {
-        target.append(aPart);
-      });
-      zip.extract(entry, target);
-    }
-    zip.close();
+var {createTempWebExtensionFile} = AddonTestUtils;
 
-    return dir;
-  }
-  let target = aInstallLocation.clone();
-  target.append(aID + ".xpi");
-  aXPIFile.copyTo(target.parent, target.leafName);
-  return target;
-}
+var {setExtensionModifiedTime} = AddonTestUtils;
+var {promiseSetExtensionModifiedTime} = AddonTestUtils;
 
-/**
- * Manually uninstalls an add-on by removing its files from the install
- * location.
- *
- * @param aInstallLocation
- *        The nsIFile of the install location to remove from.
- * @param aID
- *        The ID of the add-on to remove.
- */
-function manuallyUninstall(aInstallLocation, aID) {
-  let file = getFileForAddon(aInstallLocation, aID);
-
-  // In reality because the app is restarted a flush isn't necessary for XPIs
-  // removed outside the app, but for testing we must flush manually.
-  if (file.isFile())
-    Services.obs.notifyObservers(file, "flush-cache-entry", null);
-
-  file.remove(true);
-}
+var {manuallyInstall} = AddonTestUtils;
+var {manuallyUninstall} = AddonTestUtils;
 
-/**
- * Gets the nsIFile for where an add-on is installed. It may point to a file or
- * a directory depending on whether add-ons are being installed unpacked or not.
- *
- * @param  aDir
- *         The nsIFile for the install location
- * @param  aId
- *         The ID of the add-on
- * @return an nsIFile
- */
-function getFileForAddon(aDir, aId) {
-  var dir = aDir.clone();
-  dir.append(do_get_expected_addon_name(aId));
-  return dir;
-}
+var {getFileForAddon} = AddonTestUtils;
 
-function registerDirectory(aKey, aDir) {
-  var dirProvider = {
-    getFile: function(aProp, aPersistent) {
-      aPersistent.value = false;
-      if (aProp == aKey)
-        return aDir.clone();
-      return null;
-    },
-
-    QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIDirectoryServiceProvider,
-                                           AM_Ci.nsISupports])
-  };
-  Services.dirsvc.registerProvider(dirProvider);
-}
+var {registerDirectory} = AddonTestUtils;
 
 var gExpectedEvents = {};
 var gExpectedInstalls = [];
 var gNext = null;
 
 function getExpectedEvent(aId) {
   if (!(aId in gExpectedEvents))
     do_throw("Wasn't expecting events for " + aId);
@@ -1667,193 +998,66 @@ function ensure_test_completed() {
     if (gExpectedEvents[i].length > 0)
       do_throw("Didn't see all the expected events for " + i);
   }
   gExpectedEvents = {};
   if (gExpectedInstalls)
     do_check_eq(gExpectedInstalls.length, 0);
 }
 
-/**
- * Returns a promise that resolves when the given add-on event is fired. The
- * resolved value is an array of arguments passed for the event.
- */
-function promiseAddonEvent(event) {
-  return new Promise(resolve => {
-    let listener = {
-      [event]: function(...args) {
-        AddonManager.removeAddonListener(listener);
-        resolve(args);
-      }
-    }
+var {promiseAddonEvent} = AddonTestUtils;
 
-    AddonManager.addAddonListener(listener);
-  });
-}
+var {promiseCompleteAllInstalls} = AddonTestUtils;
 
 /**
  * A helper method to install an array of AddonInstall to completion and then
  * call a provided callback.
  *
  * @param   aInstalls
  *          The array of AddonInstalls to install
  * @param   aCallback
  *          The callback to call when all installs have finished
  */
 function completeAllInstalls(aInstalls, aCallback) {
-  let count = aInstalls.length;
-
-  if (count == 0) {
-    aCallback();
-    return;
-  }
-
-  function installCompleted(aInstall) {
-    aInstall.removeListener(listener);
-
-    if (--count == 0)
-      do_execute_soon(aCallback);
-  }
-
-  let listener = {
-    onDownloadFailed: installCompleted,
-    onDownloadCancelled: installCompleted,
-    onInstallFailed: installCompleted,
-    onInstallCancelled: installCompleted,
-    onInstallEnded: installCompleted,
-    onInstallPostponed: installCompleted,
-  };
-
-  aInstalls.forEach(function(aInstall) {
-    aInstall.addListener(listener);
-    aInstall.install();
-  });
+  promiseCompleteAllInstalls(aInstalls).then(aCallback);
 }
 
-function promiseCompleteAllInstalls(aInstalls) {
-  return new Promise(resolve => {
-    completeAllInstalls(aInstalls, resolve);
-  });
-}
+var {promiseInstallAllFiles} = AddonTestUtils;
 
 /**
  * A helper method to install an array of files and call a callback after the
  * installs are completed.
  *
  * @param   aFiles
  *          The array of files to install
  * @param   aCallback
  *          The callback to call when all installs have finished
  * @param   aIgnoreIncompatible
  *          Optional parameter to ignore add-ons that are incompatible in
  *          aome way with the application
  */
 function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
-  let count = aFiles.length;
-  let installs = [];
-  function callback() {
-    if (aCallback) {
-      aCallback();
-    }
-  }
-  aFiles.forEach(function(aFile) {
-    AddonManager.getInstallForFile(aFile, function(aInstall) {
-      if (!aInstall)
-        do_throw("No AddonInstall created for " + aFile.path);
-      do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED);
-
-      if (!aIgnoreIncompatible || !aInstall.addon.appDisabled)
-        installs.push(aInstall);
-
-      if (--count == 0)
-        completeAllInstalls(installs, callback);
-    });
-  });
+  promiseInstallAllFiles(aFiles, aIgnoreIncompatible).then(aCallback);
 }
 
-function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) {
-  let deferred = Promise.defer();
-  installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible);
-  return deferred.promise;
-}
-
-// Get the profile directory for tests to use.
-const gProfD = do_get_profile();
-
 const EXTENSIONS_DB = "extensions.json";
 var gExtensionsJSON = gProfD.clone();
 gExtensionsJSON.append(EXTENSIONS_DB);
 
-const EXTENSIONS_INI = "extensions.ini";
-var gExtensionsINI = gProfD.clone();
-gExtensionsINI.append(EXTENSIONS_INI);
-
-// Enable more extensive EM logging
-Services.prefs.setBoolPref("extensions.logging.enabled", true);
-
-// By default only load extensions from the profile install location
-Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE);
-
-// By default don't disable add-ons from any scope
-Services.prefs.setIntPref("extensions.autoDisableScopes", 0);
-
-// By default, don't cache add-ons in AddonRepository.jsm
-Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
-
-// Disable the compatibility updates window by default
-Services.prefs.setBoolPref("extensions.showMismatchUI", false);
-
-// Point update checks to the local machine for fast failures
-Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL");
-Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL");
-Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
-Services.prefs.setCharPref("services.settings.server", "http://localhost/dummy-kinto/v1");
-
-// By default ignore bundled add-ons
-Services.prefs.setBoolPref("extensions.installDistroAddons", false);
 
 // By default use strict compatibility
 Services.prefs.setBoolPref("extensions.strictCompatibility", true);
 
-// By default don't check for hotfixes
-Services.prefs.setCharPref("extensions.hotfix.id", "");
-
 // By default, set min compatible versions to 0
 Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0");
 Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0");
 
 // Ensure signature checks are enabled by default
 Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
 
-// Register a temporary directory for the tests.
-const gTmpD = gProfD.clone();
-gTmpD.append("temp");
-gTmpD.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-registerDirectory("TmpD", gTmpD);
-
-// Create a replacement app directory for the tests.
-const gAppDirForAddons = gProfD.clone();
-gAppDirForAddons.append("appdir-addons");
-gAppDirForAddons.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-registerDirectory("XREAddonAppDir", gAppDirForAddons);
-
-// Write out an empty blocklist.xml file to the profile to ensure nothing
-// is blocklisted by default
-var blockFile = gProfD.clone();
-blockFile.append("blocklist.xml");
-var stream = AM_Cc["@mozilla.org/network/file-output-stream;1"].
-             createInstance(AM_Ci.nsIFileOutputStream);
-stream.init(blockFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
-            FileUtils.PERMS_FILE, 0);
-
-var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
-           "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">\n" +
-           "</blocklist>\n";
-stream.write(data, data.length);
-stream.close();
 
 // Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml.
 function copyBlocklistToProfile(blocklistFile) {
   var dest = gProfD.clone();
   dest.append("blocklist.xml");
   if (dest.exists())
     dest.remove(false);
   blocklistFile.copyTo(gProfD, "blocklist.xml");
@@ -1869,61 +1073,25 @@ function timeout() {
   // Attempt to bail out of the test
   do_test_finished();
 }
 
 var timer = AM_Cc["@mozilla.org/timer;1"].createInstance(AM_Ci.nsITimer);
 timer.init(timeout, TIMEOUT_MS, AM_Ci.nsITimer.TYPE_ONE_SHOT);
 
 // Make sure that a given path does not exist
-function pathShouldntExist(aPath) {
-  if (aPath.exists()) {
-    do_throw("Test cleanup: path " + aPath.path + " exists when it should not");
+function pathShouldntExist(file) {
+  if (file.exists()) {
+    do_throw(`Test cleanup: path ${file.path} exists when it should not`);
   }
 }
 
 do_register_cleanup(function addon_cleanup() {
   if (timer)
     timer.cancel();
-
-  for (let file of temp_xpis) {
-    if (file.exists())
-      file.remove(false);
-  }
-
-  // Check that the temporary directory is empty
-  var dirEntries = gTmpD.directoryEntries
-                        .QueryInterface(AM_Ci.nsIDirectoryEnumerator);
-  var entry;
-  while ((entry = dirEntries.nextFile)) {
-    do_throw("Found unexpected file in temporary directory: " + entry.leafName);
-  }
-  dirEntries.close();
-
-  try {
-    gAppDirForAddons.remove(true);
-  } catch (ex) { do_print("Got exception removing addon app dir, " + ex); }
-
-  var testDir = gProfD.clone();
-  testDir.append("extensions");
-  testDir.append("trash");
-  pathShouldntExist(testDir);
-
-  testDir.leafName = "staged";
-  pathShouldntExist(testDir);
-
-  shutdownManager();
-
-  // Clear commonly set prefs.
-  try {
-    Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
-  } catch (e) {}
-  try {
-    Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY);
-  } catch (e) {}
 });
 
 /**
  * Creates a new HttpServer for testing, and begins listening on the
  * specified port. Automatically shuts down the server when the test
  * unit ends.
  *
  * @param port
@@ -2085,51 +1253,21 @@ function saveJSON(aData, aFile) {
 function callback_soon(aFunction) {
   return function(...args) {
     do_execute_soon(function() {
       aFunction.apply(null, args);
     }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback");
   }
 }
 
-/**
- * A promise-based variant of AddonManager.getAddonsByIDs.
- *
- * @param {array} list As the first argument of AddonManager.getAddonsByIDs
- * @return {promise}
- * @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to
- * its callback.
- */
-function promiseAddonsByIDs(list) {
-  return new Promise(resolve => AddonManager.getAddonsByIDs(list, resolve));
-}
+var {promiseAddonsByIDs} = AddonTestUtils;
 
-/**
- * A promise-based variant of AddonManager.getAddonByID.
- *
- * @param {string} aId The ID of the add-on.
- * @return {promise}
- * @resolve {AddonWrapper} The corresponding add-on, or null.
- */
-function promiseAddonByID(aId) {
-  return new Promise(resolve => AddonManager.getAddonByID(aId, resolve));
-}
+var {promiseAddonByID} = AddonTestUtils;
 
-/**
- * A promise-based variant of AddonManager.getAddonsWithOperationsByTypes
- *
- * @param {array} aTypes The first argument to
- *                       AddonManager.getAddonsWithOperationsByTypes
- * @return {promise}
- * @resolve {array} The list of add-ons sent by
- *                  AddonManaget.getAddonsWithOperationsByTypes to its callback.
- */
-function promiseAddonsWithOperationsByTypes(aTypes) {
-  return new Promise(resolve => AddonManager.getAddonsWithOperationsByTypes(aTypes, resolve));
-}
+var {promiseAddonsWithOperationsByTypes} = AddonTestUtils;
 
 /**
  * Returns a promise that will be resolved when an add-on update check is
  * complete. The value resolved will be an AddonInstall if a new version was
  * found.
  */
 function promiseFindAddonUpdates(addon, reason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
   return new Promise((resolve, reject) => {
@@ -2175,80 +1313,19 @@ function promiseFindAddonUpdates(addon, 
           result.error = error;
           reject(result);
         }
       }
     }, reason);
   });
 }
 
-/**
- * Monitors console output for the duration of a task, and returns a promise
- * which resolves to a tuple containing a list of all console messages
- * generated during the task's execution, and the result of the task itself.
- *
- * @param {function} aTask
- *                   The task to run while monitoring console output. May be
- *                   either a generator function, per Task.jsm, or an ordinary
- *                   function which returns promose.
- * @return {Promise<[Array<nsIConsoleMessage>, *]>}
- */
-var promiseConsoleOutput = Task.async(function*(aTask) {
-  const DONE = "=== xpcshell test console listener done ===";
+var {promiseConsoleOutput} = AddonTestUtils;
 
-  let listener, messages = [];
-  let awaitListener = new Promise(resolve => {
-    listener = msg => {
-      if (msg == DONE) {
-        resolve();
-      } else {
-        msg instanceof Components.interfaces.nsIScriptError;
-        messages.push(msg);
-      }
-    }
-  });
-
-  Services.console.registerListener(listener);
-  try {
-    let result = yield aTask();
-
-    Services.console.logStringMessage(DONE);
-    yield awaitListener;
+var {promiseWriteProxyFileToDir} = AddonTestUtils;
 
-    return { messages, result };
-  }
-  finally {
-    Services.console.unregisterListener(listener);
-  }
-});
+function writeProxyFileToDir(aDir, aAddon, aId) {
+  awaitPromise(promiseWriteProxyFileToDir(aDir, aAddon, aId));
 
-/**
- * Creates an extension proxy file.
- * See: https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment#Firefox_extension_proxy_file
- * @param   aDir
- *          The directory to add the proxy file to.
- * @param   aAddon
- *          An nsIFile for the add-on file that this is a proxy file for.
- * @param   aId
- *          A string to use for the add-on ID.
- * @return  An nsIFile for the proxy file.
- */
-function writeProxyFileToDir(aDir, aAddon, aId) {
-  let dir = aDir.clone();
-
-  if (!dir.exists())
-    dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-
-  let file = dir.clone();
+  let file = aDir.clone();
   file.append(aId);
-
-  let addonPath = aAddon.path;
-
-  let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
-            createInstance(AM_Ci.nsIFileOutputStream);
-  fos.init(file,
-           FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
-           FileUtils.PERMS_FILE, 0);
-  fos.write(addonPath, addonPath.length);
-  fos.close();
-
-  return file;
+  return file
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
@@ -55,32 +55,32 @@ writeInstallRDFToDir({
   bootstrap: true,
   unpack: true,
   targetApplications: [{
     id: "xpcshell@tests.mozilla.org",
     minVersion: "1",
     maxVersion: "1"
   }],
   name: "Unpacked, Enabled",
-}, profileDir, null, "extraFile.js");
+}, profileDir, undefined, "extraFile.js");
 
 
 // Unpacked, disabled
 writeInstallRDFToDir({
   id: "unpacked-disabled@tests.mozilla.org",
   version: "1.0",
   bootstrap: true,
   unpack: true,
   targetApplications: [{
     id: "xpcshell@tests.mozilla.org",
     minVersion: "1",
     maxVersion: "1"
   }],
   name: "Unpacked, disabled",
-}, profileDir, null, "extraFile.js");
+}, profileDir, undefined, "extraFile.js");
 
 // Keep track of the last time stamp we've used, so that we can keep moving
 // it forward (if we touch two different files in the same add-on with the same
 // timestamp we may not consider the change significant)
 var lastTimestamp = Date.now();
 
 /*
  * Helper function to touch a file and then test whether we detect the change.
--- a/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js
@@ -325,17 +325,18 @@ function run_test_1() {
 
       // Should be correctly recovered
       do_check_neq(t2, null);
       do_check_true(t2.isActive);
       do_check_false(t2.userDisabled);
       do_check_false(t2.appDisabled);
       do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
 
-      restartManager();
+      Assert.throws(shutdownManager);
+      startupManager(false);
 
       AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                    "addon2@tests.mozilla.org",
                                    "addon3@tests.mozilla.org",
                                    "addon4@tests.mozilla.org",
                                    "addon5@tests.mozilla.org",
                                    "addon6@tests.mozilla.org",
                                    "addon7@tests.mozilla.org",
@@ -391,13 +392,15 @@ function run_test_1() {
         do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
 
         do_check_neq(t2, null);
         do_check_true(t2.isActive);
         do_check_false(t2.userDisabled);
         do_check_false(t2.appDisabled);
         do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
 
+        Assert.throws(shutdownManager);
+
         end_test();
       }));
     }));
   }));
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js
@@ -324,17 +324,18 @@ function run_test_1() {
 
       // Should be correctly recovered
       do_check_neq(t2, null);
       do_check_true(t2.isActive);
       do_check_false(t2.userDisabled);
       do_check_false(t2.appDisabled);
       do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
 
-      restartManager();
+      Assert.throws(shutdownManager);
+      startupManager(false);
 
       AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                    "addon2@tests.mozilla.org",
                                    "addon3@tests.mozilla.org",
                                    "addon4@tests.mozilla.org",
                                    "addon5@tests.mozilla.org",
                                    "addon6@tests.mozilla.org",
                                    "addon7@tests.mozilla.org",
@@ -390,13 +391,15 @@ function run_test_1() {
         do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
 
         do_check_neq(t2, null);
         do_check_true(t2.isActive);
         do_check_false(t2.userDisabled);
         do_check_false(t2.appDisabled);
         do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
 
+        Assert.throws(shutdownManager);
+
         end_test();
       }));
     }));
   }));
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
@@ -212,26 +212,31 @@ add_task(function*() {
   do_check_true(a6.isActive);
   do_check_false(a6.userDisabled);
   do_check_false(a6.appDisabled);
   do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
   do_check_true(isExtensionInAddonsList(profileDir, a6.id));
 
   // After allowing access to the original DB things should still be
   // back how they were before the lock
-  shutdownManager();
+  let shutdownError;
+  try {
+    shutdownManager();
+  } catch (e) {
+    shutdownError = e;
+  }
   yield file.close();
   gExtensionsJSON.permissions = filePermissions;
   startupManager();
 
   // On Unix, we can save the DB even when the original file wasn't
   // readable, so our changes were saved. On Windows,
   // these things happened when we had no access to the database so
   // they are seen as external changes when we get the database back
-  if (gXPISaveError) {
+  if (shutdownError) {
     do_print("Previous XPI save failed");
     check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
         ["addon6@tests.mozilla.org"]);
     check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED,
         ["addon4@tests.mozilla.org"]);
   }
   else {
     do_print("Previous XPI save succeeded");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
@@ -436,17 +436,22 @@ add_task(function* run_test_1() {
   do_check_true(t2.isActive);
   do_check_false(t2.userDisabled);
   do_check_false(t2.appDisabled);
   do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
   do_check_true(isThemeInAddonsList(profileDir, t2.id));
 
   // After allowing access to the original DB things should go back to as
   // back how they were before the lock
-  shutdownManager();
+  let shutdownError;
+  try {
+    shutdownManager();
+  } catch (e) {
+    shutdownError = e;
+  }
   do_print("Unlocking " + gExtensionsJSON.path);
   yield file.close();
   gExtensionsJSON.permissions = filePermissions;
   startupManager(false);
 
   // Shouldn't have seen any startup changes
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
@@ -476,17 +481,17 @@ add_task(function* run_test_1() {
   do_check_false(isExtensionInAddonsList(profileDir, a2.id));
 
   do_check_neq(a3, null);
   do_check_false(a3.userDisabled);
   // On Unix, we may be able to save our changes over the locked DB so we
   // remember that this extension was changed to disabled. On Windows we
   // couldn't replace the old DB so we read the older version of the DB
   // where the extension is enabled
-  if (gXPISaveError) {
+  if (shutdownError) {
     do_print("XPI save failed");
     do_check_true(a3.isActive);
     do_check_false(a3.appDisabled);
     do_check_true(isExtensionInAddonsList(profileDir, a3.id));
   }
   else {
     do_print("XPI save succeeded");
     do_check_false(a3.isActive);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
@@ -238,17 +238,17 @@ function run_test() {
     id: "addon18@tests.mozilla.org",
     version: "1.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 18"
-  }, profileDir, null, "options.xul");
+  }, profileDir, undefined, "options.xul");
 
   writeInstallRDFForExtension({
     id: "addon19@tests.mozilla.org",
     version: "1.0",
     optionsType: "99",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
@@ -300,28 +300,28 @@ function run_test() {
     version: "1.0",
     optionsType: "2",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 23"
-  }, profileDir, null, "options.xul");
+  }, profileDir, undefined, "options.xul");
 
   writeInstallRDFForExtension({
     id: "addon24@tests.mozilla.org",
     version: "1.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 24"
-  }, profileDir, null, "options.xul");
+  }, profileDir, undefined, "options.xul");
 
   writeInstallRDFForExtension({
     id: "addon25@tests.mozilla.org",
     version: "1.0",
     optionsType: "3",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
@@ -335,17 +335,17 @@ function run_test() {
     version: "1.0",
     optionsType: "4",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 26"
-  }, profileDir, null, "options.xul");
+  }, profileDir, undefined, "options.xul");
 
   do_test_pending();
   startupManager();
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",