new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -0,0 +1,1151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* eslint "mozilla/no-aArgs": 1 */
+/* eslint "no-unused-vars": [2, {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}] */
+/* eslint "semi": [2, "always"] */
+/* eslint "valid-jsdoc": [2, {requireReturn: false}] */
+
+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}");
+
+
+Cu.importGlobalProperties(["fetch", "TextEncoder"]);
+
+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.defineLazyServiceGetter(this, "rdfService",
+ "@mozilla.org/rdf/rdf-service;1", "nsIRDFService");
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
+ "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+
+
+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, 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(name, blocker, options) {
+ MockAsyncShutdown.hook = blocker;
+ MockAsyncShutdown.status = options.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 = {"&": "&", '"': """, "'": "'", "<": "<", ">": ">"};
+ return String(str).replace(/[&"''<>]/g, m => replacements[m]);
+}
+
+/**
+ * A tagged template function which escapes any XML metacharacters in
+ * interpolated values.
+ *
+ * @param {Array<string>} strings
+ * An array of literal strings extracted from the templates.
+ * @param {Array} values
+ * An array of interpolated values extracted from the template.
+ * @returns {string}
+ * The result of the escaped values interpolated with the literal
+ * strings.
+ */
+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(type, dir, id) {
+ var path = dir.clone();
+ path.append(id);
+
+ var xpiPath = dir.clone();
+ xpiPath.append(`${id}.xpi`);
+
+ return this[type].some(file => {
+ if (!file.exists())
+ throw new Error(`Non-existent path found in extensions.ini: ${file.path}`);
+
+ if (file.isDirectory())
+ return file.equals(path);
+ if (file.isFile())
+ return file.equals(xpiPath);
+ return false;
+ });
+ }
+
+ isMultiprocessIncompatible(id) {
+ return this.multiprocessIncompatibleIDs.has(id);
+ }
+
+ hasTheme(dir, id) {
+ return this.hasItem("themes", dir, id);
+ }
+
+ hasExtension(dir, id) {
+ return this.hasItem("extensions", dir, id);
+ }
+}
+
+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("xpinstall.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 entries = [];
+ while (dirEntries.hasMoreElements())
+ entries.push(dirEntries.nextFile.leafName);
+ if (entries.length)
+ throw new Error(`Found unexpected files in temporary directory: ${entries.join(", ")}`);
+
+ 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);
+
+ return this.promiseShutdownManager();
+ });
+ },
+
+ /**
+ * Helper to spin the event loop until a promise resolves or rejects
+ *
+ * @param {Promise} promise
+ * The promise to wait on.
+ * @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();
+ }
+ },
+
+ 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();
+ 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();
+ try {
+ return manifest.applications.gecko.id;
+ } catch (e) {
+ // IDs for WebExtensions are extracted from the certificate when
+ // not present in the manifest, so just generate a random one.
+ return uuidGen.generateUUID().number;
+ }
+ }),
+
+ 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
+ } finally {
+ // 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");
+ }
+ }
+
+ 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
+ * @returns {Promise}
+ * Resolves when the add-on manager's startup has completed.
+ */
+ promiseStartupManager(appChanged = true) {
+ if (this.addonIntegrationService)
+ throw new Error("Attempting to startup manager that was already started.");
+
+ if (appChanged && 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} data
+ * 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(data) {
+ 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 data) {
+ rdf += escaped` <Description about="urn:mozilla:extension:${addon}"><em:updates><Seq>\n`;
+
+ for (let versionData of data[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(data) {
+ let items = [];
+
+ items.push(this._writeProps(data, ["name", "description", "creator", "homepageURL"]));
+ items.push(this._writeArrayProps(data, ["developer", "translator", "contributor"]));
+
+ return items.join("");
+ },
+
+ createInstallRDF(data) {
+ 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(data, props);
+
+ rdf += this._writeLocaleStrings(data);
+
+ for (let platform of data.targetPlatforms || [])
+ rdf += escaped`<em:targetPlatform>${platform}</em:targetPlatform>\n`;
+
+ for (let app of data.targetApplications || []) {
+ rdf += "<em:targetApplication><Description>\n";
+ rdf += this._writeProps(app, ["id", "minVersion", "maxVersion"]);
+ rdf += "</Description></em:targetApplication>\n";
+ }
+
+ for (let localized of data.localized || []) {
+ rdf += "<em:localized><Description>\n";
+ rdf += this._writeArrayProps(localized, ["locale"]);
+ rdf += this._writeLocaleStrings(localized);
+ rdf += "</Description></em:localized>\n";
+ }
+
+ for (let dep of data.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} Resolves when all directories have been created.
+ */
+ recursiveMakeDir(path) {
+ let paths = [];
+ for (let lastPath; path != lastPath; lastPath = 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 leafName = path.pop();
+
+ // Create parent directories, if necessary.
+ let dirPath = dir;
+ 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 (typeof files["install.rdf"] === "object")
+ 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();
+ let uuid = uuidGen.generateUUID().number.slice(1, -1);
+ file.append(`${uuid}.xpi`);
+
+ this.tempXPIs.push(file);
+
+ if (typeof files["install.rdf"] === "object")
+ 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} dir
+ * The directory to add the proxy file to.
+ * @param {nsIFile} addon
+ * An nsIFile for the add-on file that this is a proxy file for.
+ * @param {string} id
+ * A string to use for the add-on ID.
+ * @returns {Promise} Resolves when the file has been created.
+ */
+ promiseWriteProxyFileToDir(dir, addon, id) {
+ let files = {
+ [id]: addon.path,
+ };
+
+ return this.promiseWriteFilesToDir(dir.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} xpiFile
+ * The XPI file to install.
+ * @param {nsIFile} installLocation
+ * The install location (an nsIFile) to install into.
+ * @param {string} id
+ * The ID to install as.
+ * @param {boolean} [unpacked = this.testUnpacked]
+ * If true, install as an unpacked directory, rather than a
+ * packed XPI.
+ * @returns {nsIFile}
+ * A file pointing to the installed location of the XPI file or
+ * unpacked directory.
+ */
+ manuallyInstall(xpiFile, installLocation, id, unpacked = this.testUnpacked) {
+ if (unpacked) {
+ let dir = installLocation.clone();
+ dir.append(id);
+ dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let zip = ZipReader(xpiFile);
+ let entries = zip.findEntries(null);
+ while (entries.hasMore()) {
+ let entry = entries.getNext();
+ let target = dir.clone();
+ for (let part of entry.split("/"))
+ target.append(part);
+ zip.extract(entry, target);
+ }
+ zip.close();
+
+ return dir;
+ }
+
+ let target = installLocation.clone();
+ target.append(`${id}.xpi`);
+ xpiFile.copyTo(target.parent, target.leafName);
+ return target;
+ },
+
+ /**
+ * Manually uninstalls an add-on by removing its files from the install
+ * location.
+ *
+ * @param {nsIFile} installLocation
+ * The nsIFile of the install location to remove from.
+ * @param {string} id
+ * 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(installLocation, id, unpacked = this.testUnpacked) {
+ let file = this.getFileForAddon(installLocation, id, 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} dir
+ * The nsIFile for the install location
+ * @param {string} id
+ * 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.
+ * @returns {nsIFile}
+ * A file pointing to the XPI file or unpacked directory where
+ * the add-on should be installed.
+ */
+ getFileForAddon(dir, id, unpacked = this.testUnpacked) {
+ dir = dir.clone();
+ if (unpacked)
+ dir.append(id);
+ else
+ dir.append(`${id}.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} ext A file pointing to either the packed extension or its unpacked directory.
+ * @param {number} time The time to which we set the lastModifiedTime of the extension
+ *
+ * @deprecated Please use promiseSetExtensionModifiedTime instead
+ */
+ setExtensionModifiedTime(ext, time) {
+ ext.lastModifiedTime = time;
+ if (ext.isDirectory()) {
+ let entries = ext.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (entries.hasMoreElements())
+ this.setExtensionModifiedTime(entries.nextFile, time);
+ entries.close();
+ }
+ },
+
+ promiseSetExtensionModifiedTime: Task.async(function*(path, time) {
+ yield OS.File.setDates(path, time, time);
+
+ let iterator = new OS.File.DirectoryIterator(path);
+ try {
+ yield iterator.forEach(entry => {
+ return this.promiseSetExtensionModifiedTime(entry.path, time);
+ });
+ } catch (ex) {
+ if (ex instanceof OS.File.Error)
+ return;
+ throw ex;
+ } finally {
+ iterator.close().catch(() => {});
+ }
+ }),
+
+ registerDirectory(key, dir) {
+ var dirProvider = {
+ getFile(prop, persistent) {
+ persistent.value = false;
+ if (prop == key)
+ return dir.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
+ * The name of the AddonListener event handler method for which
+ * an event is expected.
+ * @returns {Promise<Array>}
+ * Resolves to an array containing the event handler's
+ * arguments the first time it is called.
+ */
+ 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} install
+ * The add-on to install.
+ * @returns {Promise}
+ * Resolves when the install completes, either successfully or
+ * in failure.
+ */
+ promiseCompleteInstall(install) {
+ let listener;
+ return new Promise(resolve => {
+ listener = {
+ onDownloadFailed: resolve,
+ onDownloadCancelled: resolve,
+ onInstallFailed: resolve,
+ onInstallCancelled: resolve,
+ onInstallEnded: resolve,
+ onInstallPostponed: resolve,
+ };
+
+ install.addListener(listener);
+ install.install();
+ }).then(() => {
+ install.removeListener(listener);
+ });
+ },
+
+ /**
+ * A helper method to install a file.
+ *
+ * @param {nsIFile} file
+ * The file to install
+ * @param {boolean} [ignoreIncompatible = false]
+ * Optional parameter to ignore add-ons that are incompatible
+ * with the application
+ * @returns {Promise}
+ * Resolves when the install has completed.
+ */
+ promiseInstallFile(file, ignoreIncompatible = false) {
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(file, install => {
+ if (!install)
+ reject(new Error(`No AddonInstall created for ${file.path}`));
+ else if (install.state != AddonManager.STATE_DOWNLOADED)
+ reject(new Error(`Expected file to be downloaded for install of ${file.path}`));
+ else if (ignoreIncompatible && install.addon.appDisabled)
+ resolve();
+ else
+ resolve(this.promiseCompleteInstall(install));
+ });
+ });
+ },
+
+ /**
+ * A helper method to install an array of files.
+ *
+ * @param {Iterable<nsIFile>} files
+ * The files to install
+ * @param {boolean} [ignoreIncompatible = false]
+ * Optional parameter to ignore add-ons that are incompatible
+ * with the application
+ * @returns {Promise}
+ * Resolves when the installs have completed.
+ */
+ promiseInstallAllFiles(files, ignoreIncompatible = false) {
+ return Promise.all(Array.from(
+ files,
+ file => this.promiseInstallFile(file, ignoreIncompatible)));
+ },
+
+ promiseCompleteAllInstalls(installs) {
+ return Promise.all(Array.from(installs, this.promiseCompleteInstall));
+ },
+
+ /**
+ * A promise-based variant of AddonManager.getAddonsByIDs.
+ *
+ * @param {Array<string>} list
+ * As the first argument of AddonManager.getAddonsByIDs
+ * @return {Promise<Array<Addon>>}
+ * Resolves to the array of add-ons for the given IDs.
+ */
+ promiseAddonsByIDs(list) {
+ return new Promise(resolve => AddonManager.getAddonsByIDs(list, resolve));
+ },
+
+ /**
+ * A promise-based variant of AddonManager.getAddonByID.
+ *
+ * @param {string} id
+ * The ID of the add-on.
+ * @return {Promise<Addon>}
+ * Resolves to the add-on with the given ID.
+ */
+ promiseAddonByID(id) {
+ return new Promise(resolve => AddonManager.getAddonByID(id, resolve));
+ },
+
+ /**
+ * A promise-based variant of AddonManager.getAddonsWithOperationsByTypes
+ *
+ * @param {Array<string>} types
+ * The first argument to AddonManager.getAddonsWithOperationsByTypes
+ * @return {Promise<Array<Addon>>}
+ * Resolves to an array of add-ons with the given operations
+ * pending.
+ */
+ promiseAddonsWithOperationsByTypes(types) {
+ return new Promise(resolve => AddonManager.getAddonsWithOperationsByTypes(types, 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} task
+ * 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>, *]>}
+ * Resolves to an object containing a `messages` property, with
+ * the array of console messages emitted during the execution
+ * of the task, and a `result` property, containing the task's
+ * return value.
+ */
+ promiseConsoleOutput: Task.async(function*(task) {
+ 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 task();
+
+ 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,87 @@ 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, "MockAsyncShutdown",
+ "resource://testing-common/AddonTestUtils.jsm");
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.clone();
+ },
+});
+
+Object.defineProperty(this, "gInternalManager", {
+ get() {
+ return AddonTestUtils.addonIntegrationService.QueryInterface(AM_Ci.nsITimerCallback);
+ },
+});
+
+Object.defineProperty(this, "gProfD", {
+ get() {
+ return AddonTestUtils.profileDir.clone();
+ },
+});
+
+Object.defineProperty(this, "gTmpD", {
+ get() {
+ return AddonTestUtils.tempDir.clone();
+ },
+});
+
+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 +283,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 +555,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, "&")
- .replace(/"/g, """)
- .replace(/</g, "<")
- .replace(/>/g, ">");
-}
-
-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 +622,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 +707,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 +1000,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 +1075,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 +1255,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 +1315,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
@@ -357,17 +357,22 @@ add_task(function* run_test_1() {
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));
// Restarting will actually apply changes to extensions.ini which will
// then be put into the in-memory database when we next fail to load the
// real thing
- restartManager();
+ try {
+ shutdownManager();
+ } catch (e) {
+ // We're expecting an error here.
+ }
+ startupManager(false);
// Shouldn't have seen any startup changes
check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
[a1, a2, a3, a4, a5, a6, a7, t1, t2] =
yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
"addon2@tests.mozilla.org",
"addon3@tests.mozilla.org",
@@ -436,17 +441,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 +486,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);
@@ -538,14 +548,20 @@ add_task(function* run_test_1() {
do_check_false(isThemeInAddonsList(profileDir, t1.id));
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);
do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ try {
+ shutdownManager();
+ } catch (e) {
+ // An error is expected here.
+ }
});
function run_test() {
run_next_test();
}
--- 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",
--- a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
@@ -13,16 +13,29 @@ const sampleRDFManifest = {
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
};
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
startupManager();
+const {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function promiseAddonStartup() {
+ return new Promise(resolve => {
+ let listener = (extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
BootstrapMonitor.init();
// Partial list of bootstrap reasons from XPIProvider.jsm
const BOOTSTRAP_REASONS = {
ADDON_INSTALL: 5,
ADDON_UPGRADE: 7,
ADDON_DOWNGRADE: 8,
};
@@ -130,20 +143,20 @@ add_task(function*() {
let tempdir = gTmpD.clone();
// test that an unpacked add-on works too
writeInstallRDFToDir({
id: ID,
version: "3.0",
bootstrap: true,
targetApplications: [{
- id: "xpcshell@tests.mozilla.org",
+ id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
- }],
+ }],
name: "Test Bootstrap 1 (temporary)",
}, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
let unpacked_addon = tempdir.clone();
unpacked_addon.append(ID);
do_get_file("data/test_temporary/bootstrap.js")
.copyTo(unpacked_addon, "bootstrap.js");
@@ -242,17 +255,20 @@ add_task(function*() {
applications: {
gecko: {
id: ID
}
}
}
});
- yield AddonManager.installTemporaryAddon(webext);
+ yield Promise.all([
+ AddonManager.installTemporaryAddon(webext),
+ promiseAddonStartup(),
+ ]);
addon = yield promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "4.0");
do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
@@ -269,17 +285,20 @@ add_task(function*() {
applications: {
gecko: {
id: ID
}
}
}
});
- yield AddonManager.installTemporaryAddon(webext);
+ yield Promise.all([
+ AddonManager.installTemporaryAddon(webext),
+ promiseAddonStartup(),
+ ]);
addon = yield promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "5.0");
do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);