summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test/xpcshell/head_addons.js')
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_addons.js1372
1 files changed, 893 insertions, 479 deletions
diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
index 960caceeb..60259944e 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -2,14 +2,11 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
-var AM_Cc = Components.classes;
-var AM_Ci = Components.interfaces;
-var AM_Cu = Components.utils;
+const AM_Cc = Components.classes;
+const AM_Ci = Components.interfaces;
-AM_Cu.importGlobalProperties(["TextEncoder"]);
-
-const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
-const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
+const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
+const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
@@ -17,20 +14,10 @@ const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"
const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
-const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required";
// Forcibly end the test if it runs longer than 15 minutes
const TIMEOUT_MS = 900000;
-// Maximum error in file modification times. Some file systems don't store
-// modification times exactly. As long as we are closer than this then it
-// still passes.
-const MAX_TIME_DIFFERENCE = 3000;
-
-// Time to reset file modified time relative to Date.now() so we can test that
-// times are modified (10 hours old).
-const MAKE_FILE_OLD_DIFFERENCE = 10 * 3600 * 1000;
-
Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");
@@ -38,300 +25,41 @@ 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/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, "ExtensionTestUtils",
- "resource://testing-common/ExtensionXPCShellUtils.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");
-
-const {
- awaitPromise,
- createAppInfo,
- createInstallRDF,
- createTempWebExtensionFile,
- createUpdateRDF,
- getFileForAddon,
- manuallyInstall,
- manuallyUninstall,
- promiseAddonByID,
- promiseAddonEvent,
- promiseAddonsByIDs,
- promiseAddonsWithOperationsByTypes,
- promiseCompleteAllInstalls,
- promiseConsoleOutput,
- promiseFindAddonUpdates,
- promiseInstallAllFiles,
- promiseInstallFile,
- promiseRestartManager,
- promiseSetExtensionModifiedTime,
- promiseShutdownManager,
- promiseStartupManager,
- promiseWriteProxyFileToDir,
- registerDirectory,
- setExtensionModifiedTime,
- writeFilesToZip
-} = AddonTestUtils;
-
-// WebExtension wrapper for ease of testing
-ExtensionTestUtils.init(this);
-
-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;
- },
-});
+Services.prefs.setBoolPref("toolkit.osfile.log", true);
// We need some internal bits of AddonManager
-var AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
-var { AddonManager, AddonManagerInternal, AddonManagerPrivate } = AMscope;
-
-var gPort = null;
-var gUrlToFileMap = {};
-
-// 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) {
- let manifests = Components.manager.getManifestLocations();
- for (let i = 0; i < manifests.length; i++) {
- let manifest = manifests.queryElementAt(i, AM_Ci.nsIURI);
-
- // manifest is the url to the manifest file either in an XPI or a directory.
- // We want the location of the XPI or directory itself.
- if (manifest instanceof AM_Ci.nsIJARURI) {
- manifest = manifest.JARFile.QueryInterface(AM_Ci.nsIFileURL).file;
- }
- else if (manifest instanceof AM_Ci.nsIFileURL) {
- manifest = manifest.file.parent;
- }
- else {
- continue;
- }
-
- if (manifest.equals(file))
- return true;
- }
- return false;
-}
-
-// Listens to messages from bootstrap.js telling us what add-ons were started
-// and stopped etc. and performs some sanity checks that only installed add-ons
-// are started etc.
-this.BootstrapMonitor = {
- inited: false,
-
- // Contain the current state of add-ons in the system
- installed: new Map(),
- started: new Map(),
-
- // Contain the last state of shutdown and uninstall calls for an add-on
- stopped: new Map(),
- uninstalled: new Map(),
-
- startupPromises: [],
- installPromises: [],
-
- init() {
- this.inited = true;
- Services.obs.addObserver(this, "bootstrapmonitor-event", false);
- },
-
- shutdownCheck() {
- if (!this.inited)
- return;
-
- do_check_eq(this.started.size, 0);
- },
-
- clear(id) {
- this.installed.delete(id);
- this.started.delete(id);
- this.stopped.delete(id);
- this.uninstalled.delete(id);
- },
-
- promiseAddonStartup(id) {
- return new Promise(resolve => {
- this.startupPromises.push(resolve);
- });
- },
-
- promiseAddonInstall(id) {
- return new Promise(resolve => {
- this.installPromises.push(resolve);
- });
- },
-
- checkMatches(cached, current) {
- do_check_neq(cached, undefined);
- do_check_eq(current.data.version, cached.data.version);
- do_check_eq(current.data.installPath, cached.data.installPath);
- do_check_eq(current.data.resourceURI, cached.data.resourceURI);
- },
-
- checkAddonStarted(id, version = undefined) {
- let started = this.started.get(id);
- do_check_neq(started, undefined);
- if (version != undefined)
- do_check_eq(started.data.version, version);
-
- // Chrome should be registered by now
- let installPath = new FileUtils.File(started.data.installPath);
- let isRegistered = isManifestRegistered(installPath);
- do_check_true(isRegistered);
- },
-
- checkAddonNotStarted(id) {
- do_check_false(this.started.has(id));
- },
-
- checkAddonInstalled(id, version = undefined) {
- const installed = this.installed.get(id);
- notEqual(installed, undefined);
- if (version !== undefined) {
- equal(installed.data.version, version);
+let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
+let AddonManager = AMscope.AddonManager;
+let AddonManagerInternal = AMscope.AddonManagerInternal;
+// Mock out AddonManager's reference to the AsyncShutdown module so we can shut
+// down AddonManager from the test
+let 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;
}
- return installed;
},
+ // We can use the real Barrier
+ Barrier: AsyncShutdown.Barrier
+};
- checkAddonNotInstalled(id) {
- do_check_false(this.installed.has(id));
- },
-
- observe(subject, topic, data) {
- let info = JSON.parse(data);
- let id = info.data.id;
- let installPath = new FileUtils.File(info.data.installPath);
-
- if (subject && subject.wrappedJSObject) {
- // NOTE: in some of the new tests, we need to received the real objects instead of
- // their JSON representations, but most of the current tests expect intallPath
- // and resourceURI to have been converted to strings.
- info.data = Object.assign({}, subject.wrappedJSObject.data, {
- installPath: info.data.installPath,
- resourceURI: info.data.resourceURI,
- });
- }
-
- // If this is the install event the add-ons shouldn't already be installed
- if (info.event == "install") {
- this.checkAddonNotInstalled(id);
-
- this.installed.set(id, info);
-
- for (let resolve of this.installPromises)
- resolve();
- this.installPromises = [];
- }
- else {
- this.checkMatches(this.installed.get(id), info);
- }
-
- // If this is the shutdown event than the add-on should already be started
- if (info.event == "shutdown") {
- this.checkMatches(this.started.get(id), info);
-
- this.started.delete(id);
- this.stopped.set(id, info);
-
- // Chrome should still be registered at this point
- let isRegistered = isManifestRegistered(installPath);
- do_check_true(isRegistered);
-
- // XPIProvider doesn't bother unregistering chrome on app shutdown but
- // since we simulate restarts we must do so manually to keep the registry
- // consistent.
- if (info.reason == 2 /* APP_SHUTDOWN */)
- Components.manager.removeBootstrappedManifestLocation(installPath);
- }
- else {
- this.checkAddonNotStarted(id);
- }
-
- if (info.event == "uninstall") {
- // Chrome should be unregistered at this point
- let isRegistered = isManifestRegistered(installPath);
- do_check_false(isRegistered);
-
- this.installed.delete(id);
- this.uninstalled.set(id, info)
- }
- else if (info.event == "startup") {
- this.started.set(id, info);
+AMscope.AsyncShutdown = MockAsyncShutdown;
- // Chrome should be registered at this point
- let isRegistered = isManifestRegistered(installPath);
- do_check_true(isRegistered);
+var gInternalManager = null;
+var gAppInfo = null;
+var gAddonsList;
- for (let resolve of this.startupPromises)
- resolve();
- this.startupPromises = [];
- }
- }
-}
+var gPort = null;
+var gUrlToFileMap = {};
-AddonTestUtils.on("addon-manager-shutdown", () => BootstrapMonitor.shutdownCheck());
+var TEST_UNPACKED = false;
function isNightlyChannel() {
var channel = "default";
@@ -343,6 +71,51 @@ function isNightlyChannel() {
return channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr";
}
+function createAppInfo(id, name, version, platformVersion) {
+ gAppInfo = {
+ // nsIXULAppInfo
+ vendor: "Mozilla",
+ name: name,
+ ID: id,
+ version: version,
+ appBuildID: "2007010101",
+ platformVersion: platformVersion ? platformVersion : "1.0",
+ platformBuildID: "2007010101",
+
+ // nsIXULRuntime
+ inSafeMode: false,
+ logConsoleErrors: true,
+ OS: "XPCShell",
+ XPCOMABI: "noarch-spidermonkey",
+ invalidateCachesOnRestart: function invalidateCachesOnRestart() {
+ // Do nothing
+ },
+
+ // nsICrashReporter
+ annotations: {},
+
+ annotateCrashReport: function(key, data) {
+ this.annotations[key] = data;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIXULAppInfo,
+ AM_Ci.nsIXULRuntime,
+ AM_Ci.nsICrashReporter,
+ AM_Ci.nsISupports])
+ };
+
+ var XULAppInfoFactory = {
+ createInstance: function (outer, iid) {
+ if (outer != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ return gAppInfo.QueryInterface(iid);
+ }
+ };
+ var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
+ registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
+ XULAPPINFO_CONTRACTID, XULAppInfoFactory);
+}
+
/**
* 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
@@ -418,11 +191,11 @@ function do_get_file_hash(aFile, aAlgorithm) {
crypto.updateFromStream(fis, aFile.fileSize);
// return the two-digit hexadecimal code for a byte
- let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
+ function toHexString(charCode)
+ ("0" + charCode.toString(16)).slice(-2);
let binary = crypto.finish(false);
- let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)));
- return aAlgorithm + ":" + hash.join("");
+ return aAlgorithm + ":" + [toHexString(binary.charCodeAt(i)) for (i in binary)].join("")
}
/**
@@ -439,7 +212,9 @@ function do_get_addon_root_uri(aProfileDir, aId) {
path.leafName += ".xpi";
return "jar:" + Services.io.newFileURI(path).spec + "!/";
}
- return Services.io.newFileURI(path).spec;
+ else {
+ return Services.io.newFileURI(path).spec;
+ }
}
function do_get_expected_addon_name(aId) {
@@ -601,8 +376,54 @@ function do_check_icons(aActual, aExpected) {
}
}
+// Record the error (if any) from trying to save the XPI
+// database at shutdown time
+let 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) {
- promiseStartupManager(aAppChanged);
+ if (gInternalManager)
+ do_throw("Test attempt to startup manager that was already started.");
+
+ if (aAppChanged || aAppChanged === undefined) {
+ if (gExtensionsINI.exists())
+ gExtensionsINI.remove(true);
+ }
+
+ 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);
+ }
}
/**
@@ -614,36 +435,270 @@ function startupManager(aAppChanged) {
* the application version has changed.
*/
function restartManager(aNewVersion) {
- awaitPromise(promiseRestartManager(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);
+ }
+ });
}
function shutdownManager() {
- awaitPromise(promiseShutdownManager());
+ 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( () => {
+ 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;
}
function isItemMarkedMPIncompatible(aId) {
- return AddonTestUtils.addonsList.isMultiprocessIncompatible(aId);
+ return gAddonsList.mpIncompatible.has(aId);
}
function isThemeInAddonsList(aDir, aId) {
- return AddonTestUtils.addonsList.hasTheme(aDir, aId);
+ return isItemInAddonsList("themes", aDir, aId);
}
function isExtensionInAddonsList(aDir, aId) {
- return AddonTestUtils.addonsList.hasExtension(aDir, aId);
+ return isItemInAddonsList("extensions", 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 = changes.filter(function(aEl) /@tests.mozilla.org$/.test(aEl));
changes.sort();
do_check_eq(JSON.stringify(ids), JSON.stringify(changes));
}
/**
+ * Escapes any occurances of &, ", < or > with XML entities.
+ *
+ * @param str
+ * The string to escape
+ * @return The escaped string
+ */
+function escapeXML(aStr) {
+ return aStr.toString()
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+}
+
+function writeLocaleStrings(aData) {
+ let rdf = "";
+ ["name", "description", "creator", "homepageURL"].forEach(function(aProp) {
+ if (aProp in aData)
+ rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n";
+ });
+
+ ["developer", "translator", "contributor"].forEach(function(aProp) {
+ if (aProp in aData) {
+ aData[aProp].forEach(function(aValue) {
+ rdf += "<em:" + aProp + ">" + escapeXML(aValue) + "</em:" + aProp + ">\n";
+ });
+ }
+ });
+ return rdf;
+}
+
+/**
+ * Creates an update.rdf structure as a string using for the update data passed.
+ *
+ * @param aData
+ * The update data as a JS object. Each property name is an add-on ID,
+ * the property value is an array of each version of the add-on. Each
+ * array value is a JS object containing the data for the version, at
+ * minimum a "version" and "targetApplications" property should be
+ * included to create a functional update manifest.
+ * @return the update.rdf structure as a string.
+ */
+function createUpdateRDF(aData) {
+ var rdf = '<?xml version="1.0"?>\n';
+ rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
+ ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
+
+ for (let addon in aData) {
+ rdf += ' <Description about="urn:mozilla:extension:' + escapeXML(addon) + '"><em:updates><Seq>\n';
+
+ for (let versionData of aData[addon]) {
+ rdf += ' <li><Description>\n';
+
+ for (let prop of ["version", "multiprocessCompatible"]) {
+ if (prop in versionData)
+ rdf += " <em:" + prop + ">" + escapeXML(versionData[prop]) + "</em:" + prop + ">\n";
+ }
+
+ if ("targetApplications" in versionData) {
+ for (let app of versionData.targetApplications) {
+ rdf += " <em:targetApplication><Description>\n";
+ for (let prop of ["id", "minVersion", "maxVersion", "updateLink", "updateHash"]) {
+ if (prop in app)
+ rdf += " <em:" + prop + ">" + escapeXML(app[prop]) + "</em:" + prop + ">\n";
+ }
+ rdf += " </Description></em:targetApplication>\n";
+ }
+ }
+
+ rdf += ' </Description></li>\n';
+ }
+
+ rdf += ' </Seq></em:updates></Description>\n'
+ }
+ rdf += "</RDF>\n";
+
+ return rdf;
+}
+
+function createInstallRDF(aData) {
+ var rdf = '<?xml version="1.0"?>\n';
+ rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
+ ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
+ rdf += '<Description about="urn:mozilla:install-manifest">\n';
+
+ ["id", "version", "type", "internalName", "updateURL", "updateKey",
+ "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
+ "skinnable", "bootstrap", "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";
+ });
+ }
+
+ rdf += "</Description>\n</RDF>\n";
+ return rdf;
+}
+
+/**
* 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,
@@ -660,22 +715,38 @@ function check_startup_changes(aType, aIds) {
* 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 = aData.id, aExtraFile = null) {
- let files = {
- "install.rdf": AddonTestUtils.createInstallRDF(aData),
- };
- if (aExtraFile)
- files[aExtraFile] = "";
-
- let dir = aDir.clone();
- dir.append(aId);
-
- awaitPromise(AddonTestUtils.promiseWriteFilesToDir(dir.path, files));
+function writeInstallRDFToDir(aData, aDir, aId, aExtraFile) {
+ var id = aId ? aId : aData.id
+
+ var dir = aDir.clone();
+ dir.append(id);
+
+ 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();
+
+ if (!aExtraFile)
+ return dir;
+
+ file = dir.clone();
+ file.append(aExtraFile);
+ file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
return dir;
}
/**
- * Writes an install.rdf manifest into a packed extension using the properties passed
+ * 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
@@ -691,26 +762,15 @@ function writeInstallRDFToDir(aData, aDir, aId = aData.id, aExtraFile = null) {
* 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 (!aDir.exists())
- aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-
- var file = aDir.clone();
- file.append(`${aId}.xpi`);
-
- AddonTestUtils.writeFilesToZip(file.path, files);
-
- return file;
+function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
+ if (TEST_UNPACKED) {
+ return writeInstallRDFToDir(aData, aDir, aId, aExtraFile);
+ }
+ return writeInstallRDFToXPI(aData, aDir, aId, aExtraFile);
}
/**
- * Writes an install.rdf manifest into an extension using the properties passed
+ * 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
@@ -726,32 +786,50 @@ function writeInstallRDFToXPI(aData, aDir, aId = aData.id, aExtraFile = null) {
* An optional dummy file to create in the extension
* @return A file pointing to where the extension was installed
*/
-function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
- if (TEST_UNPACKED) {
- return writeInstallRDFToDir(aData, aDir, aId, aExtraFile);
- }
- return writeInstallRDFToXPI(aData, aDir, aId, aExtraFile);
+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 a manifest.json manifest into an extension using the properties passed
- * in a JS object.
+ * 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 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
+ * @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 promiseWriteWebManifestForExtension(aData, aDir, aId = aData.applications.gecko.id) {
- let files = {
- "manifest.json": JSON.stringify(aData),
- }
- return AddonTestUtils.promiseWriteFilesToExtension(aDir.path, aId, files);
+function writeInstallRDFToXPIFile(aData, aFile, aExtraFile) {
+ var rdf = createInstallRDF(aData);
+ var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(AM_Ci.nsIStringInputStream);
+ stream.setData(rdf, -1);
+ var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+ createInstance(AM_Ci.nsIZipWriter);
+ zipW.open(aFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
+ zipW.addEntryStream("install.rdf", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+ stream, false);
+ if (aExtraFile)
+ zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+ stream, false);
+ zipW.close();
}
+let 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.
@@ -760,16 +838,148 @@ function promiseWriteWebManifestForExtension(aData, aDir, aId = aData.applicatio
* The object holding data about the add-on
* @return A file pointing to the created XPI file
*/
-function createTempXPIFile(aData, aExtraFile) {
- let files = {
- "install.rdf": aData,
- };
- if (typeof aExtraFile == "object")
- Object.assign(files, aExtraFile);
- else if (aExtraFile)
- files[aExtraFile] = "";
+function createTempXPIFile(aData) {
+ 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);
+ return file;
+}
+
+/**
+ * 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) {
+ return;
+ } finally {
+ if (iterator) {
+ iterator.close();
+ }
+ }
+ for (let entry of entries) {
+ yield promiseSetExtensionModifiedTime(entry.path, aTime);
+ }
+ });
+}
+
+/**
+ * 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();
+
+ return dir;
+ }
+ else {
+ let target = aInstallLocation.clone();
+ target.append(aID + ".xpi");
+ aXPIFile.copyTo(target.parent, target.leafName);
+ return target;
+ }
+}
+
+/**
+ * Manually uninstalls an add-on by removing its files from the install
+ * location.
+ *
+ * @param 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);
+}
+
+/**
+ * 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;
+}
- return AddonTestUtils.createTempXPIFile(files);
+function registerDirectory(aKey, aDir) {
+ var dirProvider = {
+ getFile: function(aProp, aPersistent) {
+ aPersistent.value = true;
+ if (aProp == aKey)
+ return aDir.clone();
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIDirectoryServiceProvider,
+ AM_Ci.nsISupports])
+ };
+ Services.dirsvc.registerProvider(dirProvider);
}
var gExpectedEvents = {};
@@ -898,13 +1108,9 @@ const AddonListener = {
const InstallListener = {
onNewInstall: function(install) {
if (install.state != AddonManager.STATE_DOWNLOADED &&
- install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
install.state != AddonManager.STATE_AVAILABLE)
do_throw("Bad install state " + install.state);
- if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
- do_check_eq(install.error, 0);
- else
- do_check_neq(install.error, 0);
+ do_check_eq(install.error, 0);
do_check_eq("onNewInstall", getExpectedInstall());
return check_test_completed(arguments);
},
@@ -996,9 +1202,7 @@ function check_test_completed(aArgs) {
if (gExpectedInstalls instanceof Array &&
gExpectedInstalls.length > 0)
return undefined;
-
- for (let id in gExpectedInstalls) {
- let installList = gExpectedInstalls[id];
+ else for each (let installList in gExpectedInstalls) {
if (installList.length > 0)
return undefined;
}
@@ -1032,7 +1236,38 @@ function ensure_test_completed() {
* The callback to call when all installs have finished
*/
function completeAllInstalls(aInstalls, aCallback) {
- promiseCompleteAllInstalls(aInstalls).then(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
+ };
+
+ aInstalls.forEach(function(aInstall) {
+ aInstall.addListener(listener);
+ aInstall.install();
+ });
+}
+
+function promiseCompleteAllInstalls(aInstalls) {
+ return new Promise(resolve => {
+ completeAllInstalls(aInstalls, resolve);
+ });
}
/**
@@ -1048,13 +1283,179 @@ function completeAllInstalls(aInstalls, aCallback) {
* aome way with the application
*/
function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
- promiseInstallAllFiles(aFiles, aIgnoreIncompatible).then(aCallback);
+ 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);
+ });
+ });
+}
+
+function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) {
+ let deferred = Promise.defer();
+ installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible);
+ return deferred.promise;
+
+}
+
+if ("nsIWindowsRegKey" in AM_Ci) {
+ var MockRegistry = {
+ LOCAL_MACHINE: {},
+ CURRENT_USER: {},
+ CLASSES_ROOT: {},
+
+ getRoot: function(aRoot) {
+ switch (aRoot) {
+ case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE:
+ return MockRegistry.LOCAL_MACHINE;
+ case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER:
+ return MockRegistry.CURRENT_USER;
+ case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT:
+ return MockRegistry.CLASSES_ROOT;
+ default:
+ do_throw("Unknown root " + aRootKey);
+ return null;
+ }
+ },
+
+ setValue: function(aRoot, aPath, aName, aValue) {
+ let rootKey = MockRegistry.getRoot(aRoot);
+
+ if (!(aPath in rootKey)) {
+ rootKey[aPath] = [];
+ }
+ else {
+ for (let i = 0; i < rootKey[aPath].length; i++) {
+ if (rootKey[aPath][i].name == aName) {
+ if (aValue === null)
+ rootKey[aPath].splice(i, 1);
+ else
+ rootKey[aPath][i].value = aValue;
+ return;
+ }
+ }
+ }
+
+ if (aValue === null)
+ return;
+
+ rootKey[aPath].push({
+ name: aName,
+ value: aValue
+ });
+ }
+ };
+
+ /**
+ * This is a mock nsIWindowsRegistry implementation. It only implements the
+ * methods that the extension manager requires.
+ */
+ function MockWindowsRegKey() {
+ }
+
+ MockWindowsRegKey.prototype = {
+ values: null,
+
+ // --- Overridden nsISupports interface functions ---
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIWindowsRegKey]),
+
+ // --- Overridden nsIWindowsRegKey interface functions ---
+ open: function(aRootKey, aRelPath, aMode) {
+ let rootKey = MockRegistry.getRoot(aRootKey);
+
+ if (!(aRelPath in rootKey))
+ rootKey[aRelPath] = [];
+ this.values = rootKey[aRelPath];
+ },
+
+ close: function() {
+ this.values = null;
+ },
+
+ get valueCount() {
+ if (!this.values)
+ throw Components.results.NS_ERROR_FAILURE;
+ return this.values.length;
+ },
+
+ getValueName: function(aIndex) {
+ if (!this.values || aIndex >= this.values.length)
+ throw Components.results.NS_ERROR_FAILURE;
+ return this.values[aIndex].name;
+ },
+
+ readStringValue: function(aName) {
+ for (let value of this.values) {
+ if (value.name == aName)
+ return value.value;
+ }
+ return null;
+ }
+ };
+
+ var WinRegFactory = {
+ createInstance: function(aOuter, aIid) {
+ if (aOuter != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+
+ var key = new MockWindowsRegKey();
+ return key.QueryInterface(aIid);
+ }
+ };
+
+ var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
+ registrar.registerFactory(Components.ID("{0478de5b-0f38-4edb-851d-4c99f1ed8eba}"),
+ "Mock Windows Registry Implementation",
+ "@mozilla.org/windows-registry-key;1", WinRegFactory);
}
+// Get the profile directory for tests to use.
+const gProfD = do_get_profile();
+
const EXTENSIONS_DB = "extensions.json";
-var gExtensionsJSON = gProfD.clone();
+let gExtensionsJSON = gProfD.clone();
gExtensionsJSON.append(EXTENSIONS_DB);
+const EXTENSIONS_INI = "extensions.ini";
+let 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");
+
+// By default ignore bundled add-ons
+Services.prefs.setBoolPref("extensions.installDistroAddons", false);
// By default use strict compatibility
Services.prefs.setBoolPref("extensions.strictCompatibility", true);
@@ -1063,9 +1464,26 @@ Services.prefs.setBoolPref("extensions.strictCompatibility", true);
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);
+
+// 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) {
@@ -1091,40 +1509,51 @@ 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(file) {
- if (file.exists()) {
- do_throw(`Test cleanup: path ${file.path} exists when it should not`);
+function pathShouldntExist(aPath) {
+ if (aPath.exists()) {
+ do_throw("Test cleanup: path " + aPath.path + " exists when it should not");
}
}
do_register_cleanup(function addon_cleanup() {
if (timer)
timer.cancel();
-});
-/**
- * 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
- * The port to listen on. If omitted, listen on a random
- * port. The latter is the preferred behavior.
- *
- * @return HttpServer
- */
-function createHttpServer(port = -1) {
- let server = new HttpServer();
- server.start(port);
+ for (let file of temp_xpis) {
+ if (file.exists())
+ file.remove(false);
+ }
- do_register_cleanup(() => {
- return new Promise(resolve => {
- server.stop(resolve);
- });
- });
+ // 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();
- return server;
-}
+ var testDir = gProfD.clone();
+ testDir.append("extensions");
+ testDir.append("trash");
+ pathShouldntExist(testDir);
+
+ testDir.leafName = "staged";
+ pathShouldntExist(testDir);
+
+ testDir.leafName = "staged-xpis";
+ 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) {}
+});
/**
* Handler function that responds with the interpolated
@@ -1154,7 +1583,7 @@ function interpolateAndServeFile(request, response) {
response.write(data);
} catch (e) {
- do_throw(`Exception while serving interpolated file: ${e}\n${e.stack}`);
+ do_throw("Exception while serving interpolated file.");
} finally {
cstream.close(); // this closes fstream as well
}
@@ -1195,7 +1624,7 @@ function do_exception_wrap(func) {
try {
func.apply(null, arguments);
}
- catch (e) {
+ catch(e) {
do_report_unexpected_exception(e);
}
};
@@ -1204,11 +1633,9 @@ function do_exception_wrap(func) {
/**
* Change the schema version of the JSON extensions database
*/
-function changeXPIDBVersion(aNewVersion, aMutator = undefined) {
+function changeXPIDBVersion(aNewVersion) {
let jData = loadJSON(gExtensionsJSON);
jData.schemaVersion = aNewVersion;
- if (aMutator)
- aMutator(jData);
saveJSON(jData, gExtensionsJSON);
}
@@ -1239,7 +1666,7 @@ function loadFile(aFile) {
function loadJSON(aFile) {
let data = loadFile(aFile);
do_print("Loaded JSON file " + aFile.path);
- return (JSON.parse(data));
+ return(JSON.parse(data));
}
/**
@@ -1271,75 +1698,62 @@ function callback_soon(aFunction) {
}
}
-function writeProxyFileToDir(aDir, aAddon, aId) {
- awaitPromise(promiseWriteProxyFileToDir(aDir, aAddon, aId));
-
- let file = aDir.clone();
- file.append(aId);
- return file
+/**
+ * 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));
}
-function* serveSystemUpdate(xml, perform_update, testserver) {
- testserver.registerPathHandler("/data/update.xml", (request, response) => {
- response.write(xml);
- });
-
- try {
- yield perform_update();
- }
- finally {
- testserver.registerPathHandler("/data/update.xml", null);
- }
+/**
+ * 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));
}
-// Runs an update check making it use the passed in xml string. Uses the direct
-// call to the update function so we get rejections on failure.
-function* installSystemAddons(xml, testserver) {
- do_print("Triggering system add-on update check.");
-
- yield serveSystemUpdate(xml, function*() {
- let { XPIProvider } = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
- yield XPIProvider.updateSystemAddons();
- }, testserver);
+/**
+ * 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));
}
-// Runs a full add-on update check which will in some cases do a system add-on
-// update check. Always succeeds.
-function* updateAllSystemAddons(xml, testserver) {
- do_print("Triggering full add-on update check.");
-
- yield serveSystemUpdate(xml, function() {
- return new Promise(resolve => {
- Services.obs.addObserver(function() {
- Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
-
- resolve();
- }, "addons-background-update-complete", false);
-
- // Trigger the background update timer handler
- gInternalManager.notify(null);
- });
- }, testserver);
-}
-
-// Builds an update.xml file for an update check based on the data passed.
-function* buildSystemAddonUpdates(addons, root) {
- let xml = `<?xml version="1.0" encoding="UTF-8"?>\n\n<updates>\n`;
- if (addons) {
- xml += ` <addons>\n`;
- for (let addon of addons) {
- xml += ` <addon id="${addon.id}" URL="${root + addon.path}" version="${addon.version}"`;
- if (addon.size)
- xml += ` size="${addon.size}"`;
- if (addon.hashFunction)
- xml += ` hashFunction="${addon.hashFunction}"`;
- if (addon.hashValue)
- xml += ` hashValue="${addon.hashValue}"`;
- xml += `/>\n`;
- }
- xml += ` </addons>\n`;
- }
- xml += `</updates>\n`;
-
- return xml;
+/**
+ * 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) => {
+ addon.findUpdates({
+ install: null,
+
+ onUpdateAvailable: function(addon, install) {
+ this.install = install;
+ },
+
+ onUpdateFinished: function(addon, error) {
+ if (error == AddonManager.UPDATE_STATUS_NO_ERROR)
+ resolve(this.install);
+ else
+ reject(error);
+ }
+ }, reason);
+ });
}