summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/webextensions/test/xpcshell/head_addons.js
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:49:12 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:49:12 -0500
commit4fb11cd5966461bccc3ed1599b808237be6b0de9 (patch)
treed7f0ccd49cebb3544d52635ff1bd6ed4d763823f /toolkit/mozapps/webextensions/test/xpcshell/head_addons.js
parentf164d9124708b50789dbb6959e1de96cc5697c48 (diff)
downloadUXP-4fb11cd5966461bccc3ed1599b808237be6b0de9.tar
UXP-4fb11cd5966461bccc3ed1599b808237be6b0de9.tar.gz
UXP-4fb11cd5966461bccc3ed1599b808237be6b0de9.tar.lz
UXP-4fb11cd5966461bccc3ed1599b808237be6b0de9.tar.xz
UXP-4fb11cd5966461bccc3ed1599b808237be6b0de9.zip
Move WebExtensions enabled Add-ons Manager
Diffstat (limited to 'toolkit/mozapps/webextensions/test/xpcshell/head_addons.js')
-rw-r--r--toolkit/mozapps/webextensions/test/xpcshell/head_addons.js1345
1 files changed, 1345 insertions, 0 deletions
diff --git a/toolkit/mozapps/webextensions/test/xpcshell/head_addons.js b/toolkit/mozapps/webextensions/test/xpcshell/head_addons.js
new file mode 100644
index 000000000..960caceeb
--- /dev/null
+++ b/toolkit/mozapps/webextensions/test/xpcshell/head_addons.js
@@ -0,0 +1,1345 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var AM_Cc = Components.classes;
+var AM_Ci = Components.interfaces;
+var AM_Cu = Components.utils;
+
+AM_Cu.importGlobalProperties(["TextEncoder"]);
+
+const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
+const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
+
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
+const PREF_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");
+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, "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;
+ },
+});
+
+// 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);
+ }
+ return installed;
+ },
+
+ 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);
+
+ // Chrome should be registered at this point
+ let isRegistered = isManifestRegistered(installPath);
+ do_check_true(isRegistered);
+
+ 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";
+}
+
+/**
+ * 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
+ * The version of the add-on
+ */
+function do_check_in_crash_annotation(aId, aVersion) {
+ if (!("nsICrashReporter" in AM_Ci))
+ return;
+
+ if (!("Add-ons" in gAppInfo.annotations)) {
+ do_check_false(true);
+ return;
+ }
+
+ let addons = gAppInfo.annotations["Add-ons"].split(",");
+ do_check_false(addons.indexOf(encodeURIComponent(aId) + ":" +
+ encodeURIComponent(aVersion)) < 0);
+}
+
+/**
+ * Tests that an add-on does not appear in the crash report annotations, if
+ * crash reporting is enabled. The test will fail if the add-on is in the
+ * annotation.
+ * @param aId
+ * The ID of the add-on
+ * @param aVersion
+ * The version of the add-on
+ */
+function do_check_not_in_crash_annotation(aId, aVersion) {
+ if (!("nsICrashReporter" in AM_Ci))
+ return;
+
+ if (!("Add-ons" in gAppInfo.annotations)) {
+ do_check_true(true);
+ return;
+ }
+
+ let addons = gAppInfo.annotations["Add-ons"].split(",");
+ do_check_true(addons.indexOf(encodeURIComponent(aId) + ":" +
+ encodeURIComponent(aVersion)) < 0);
+}
+
+/**
+ * Returns a testcase xpi
+ *
+ * @param aName
+ * The name of the testcase (without extension)
+ * @return an nsIFile pointing to the testcase xpi
+ */
+function do_get_addon(aName) {
+ return do_get_file("addons/" + aName + ".xpi");
+}
+
+function do_get_addon_hash(aName, aAlgorithm) {
+ let file = do_get_addon(aName);
+ return do_get_file_hash(file);
+}
+
+function do_get_file_hash(aFile, aAlgorithm) {
+ if (!aAlgorithm)
+ aAlgorithm = "sha1";
+
+ let crypto = AM_Cc["@mozilla.org/security/hash;1"].
+ createInstance(AM_Ci.nsICryptoHash);
+ crypto.initWithString(aAlgorithm);
+ let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ fis.init(aFile, -1, -1, false);
+ crypto.updateFromStream(fis, aFile.fileSize);
+
+ // return the two-digit hexadecimal code for a byte
+ let 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("");
+}
+
+/**
+ * Returns an extension uri spec
+ *
+ * @param aProfileDir
+ * The extension install directory
+ * @return a uri spec pointing to the root of the extension
+ */
+function do_get_addon_root_uri(aProfileDir, aId) {
+ let path = aProfileDir.clone();
+ path.append(aId);
+ if (!path.exists()) {
+ path.leafName += ".xpi";
+ return "jar:" + Services.io.newFileURI(path).spec + "!/";
+ }
+ return Services.io.newFileURI(path).spec;
+}
+
+function do_get_expected_addon_name(aId) {
+ if (TEST_UNPACKED)
+ return aId;
+ return aId + ".xpi";
+}
+
+/**
+ * Check that an array of actual add-ons is the same as an array of
+ * expected add-ons.
+ *
+ * @param aActualAddons
+ * The array of actual add-ons to check.
+ * @param aExpectedAddons
+ * The array of expected add-ons to check against.
+ * @param aProperties
+ * An array of properties to check.
+ */
+function do_check_addons(aActualAddons, aExpectedAddons, aProperties) {
+ do_check_neq(aActualAddons, null);
+ do_check_eq(aActualAddons.length, aExpectedAddons.length);
+ for (let i = 0; i < aActualAddons.length; i++)
+ do_check_addon(aActualAddons[i], aExpectedAddons[i], aProperties);
+}
+
+/**
+ * Check that the actual add-on is the same as the expected add-on.
+ *
+ * @param aActualAddon
+ * The actual add-on to check.
+ * @param aExpectedAddon
+ * The expected add-on to check against.
+ * @param aProperties
+ * An array of properties to check.
+ */
+function do_check_addon(aActualAddon, aExpectedAddon, aProperties) {
+ do_check_neq(aActualAddon, null);
+
+ aProperties.forEach(function(aProperty) {
+ let actualValue = aActualAddon[aProperty];
+ let expectedValue = aExpectedAddon[aProperty];
+
+ // Check that all undefined expected properties are null on actual add-on
+ if (!(aProperty in aExpectedAddon)) {
+ if (actualValue !== undefined && actualValue !== null) {
+ do_throw("Unexpected defined/non-null property for add-on " +
+ aExpectedAddon.id + " (addon[" + aProperty + "] = " +
+ actualValue.toSource() + ")");
+ }
+
+ return;
+ }
+ else if (expectedValue && !actualValue) {
+ do_throw("Missing property for add-on " + aExpectedAddon.id +
+ ": expected addon[" + aProperty + "] = " + expectedValue);
+ return;
+ }
+
+ switch (aProperty) {
+ case "creator":
+ do_check_author(actualValue, expectedValue);
+ break;
+
+ case "developers":
+ case "translators":
+ case "contributors":
+ do_check_eq(actualValue.length, expectedValue.length);
+ for (let i = 0; i < actualValue.length; i++)
+ do_check_author(actualValue[i], expectedValue[i]);
+ break;
+
+ case "screenshots":
+ do_check_eq(actualValue.length, expectedValue.length);
+ for (let i = 0; i < actualValue.length; i++)
+ do_check_screenshot(actualValue[i], expectedValue[i]);
+ break;
+
+ case "sourceURI":
+ do_check_eq(actualValue.spec, expectedValue);
+ break;
+
+ case "updateDate":
+ do_check_eq(actualValue.getTime(), expectedValue.getTime());
+ break;
+
+ case "compatibilityOverrides":
+ do_check_eq(actualValue.length, expectedValue.length);
+ for (let i = 0; i < actualValue.length; i++)
+ do_check_compatibilityoverride(actualValue[i], expectedValue[i]);
+ break;
+
+ case "icons":
+ do_check_icons(actualValue, expectedValue);
+ break;
+
+ default:
+ if (remove_port(actualValue) !== remove_port(expectedValue))
+ do_throw("Failed for " + aProperty + " for add-on " + aExpectedAddon.id +
+ " (" + actualValue + " === " + expectedValue + ")");
+ }
+ });
+}
+
+/**
+ * Check that the actual author is the same as the expected author.
+ *
+ * @param aActual
+ * The actual author to check.
+ * @param aExpected
+ * The expected author to check against.
+ */
+function do_check_author(aActual, aExpected) {
+ do_check_eq(aActual.toString(), aExpected.name);
+ do_check_eq(aActual.name, aExpected.name);
+ do_check_eq(aActual.url, aExpected.url);
+}
+
+/**
+ * Check that the actual screenshot is the same as the expected screenshot.
+ *
+ * @param aActual
+ * The actual screenshot to check.
+ * @param aExpected
+ * The expected screenshot to check against.
+ */
+function do_check_screenshot(aActual, aExpected) {
+ do_check_eq(aActual.toString(), aExpected.url);
+ do_check_eq(aActual.url, aExpected.url);
+ do_check_eq(aActual.width, aExpected.width);
+ do_check_eq(aActual.height, aExpected.height);
+ do_check_eq(aActual.thumbnailURL, aExpected.thumbnailURL);
+ do_check_eq(aActual.thumbnailWidth, aExpected.thumbnailWidth);
+ do_check_eq(aActual.thumbnailHeight, aExpected.thumbnailHeight);
+ do_check_eq(aActual.caption, aExpected.caption);
+}
+
+/**
+ * Check that the actual compatibility override is the same as the expected
+ * compatibility override.
+ *
+ * @param aAction
+ * The actual compatibility override to check.
+ * @param aExpected
+ * The expected compatibility override to check against.
+ */
+function do_check_compatibilityoverride(aActual, aExpected) {
+ do_check_eq(aActual.type, aExpected.type);
+ do_check_eq(aActual.minVersion, aExpected.minVersion);
+ do_check_eq(aActual.maxVersion, aExpected.maxVersion);
+ do_check_eq(aActual.appID, aExpected.appID);
+ do_check_eq(aActual.appMinVersion, aExpected.appMinVersion);
+ do_check_eq(aActual.appMaxVersion, aExpected.appMaxVersion);
+}
+
+function do_check_icons(aActual, aExpected) {
+ for (var size in aExpected) {
+ do_check_eq(remove_port(aActual[size]), remove_port(aExpected[size]));
+ }
+}
+
+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) {
+ awaitPromise(promiseRestartManager(aNewVersion));
+}
+
+function shutdownManager() {
+ awaitPromise(promiseShutdownManager());
+}
+
+function isItemMarkedMPIncompatible(aId) {
+ return AddonTestUtils.addonsList.isMultiprocessIncompatible(aId);
+}
+
+function isThemeInAddonsList(aDir, aId) {
+ return AddonTestUtils.addonsList.hasTheme(aDir, aId);
+}
+
+function isExtensionInAddonsList(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));
+}
+
+/**
+ * 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.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @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 = 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));
+ return dir;
+}
+
+/**
+ * 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 (!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;
+}
+
+/**
+ * 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.
+ *
+ * @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 writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
+ if (TEST_UNPACKED) {
+ return writeInstallRDFToDir(aData, aDir, aId, aExtraFile);
+ }
+ return writeInstallRDFToXPI(aData, aDir, aId, aExtraFile);
+}
+
+/**
+ * Writes a manifest.json manifest into an extension using the properties passed
+ * in a JS object.
+ *
+ * @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 promiseWriteWebManifestForExtension(aData, aDir, aId = aData.applications.gecko.id) {
+ let files = {
+ "manifest.json": JSON.stringify(aData),
+ }
+ return AddonTestUtils.promiseWriteFilesToExtension(aDir.path, aId, files);
+}
+
+/**
+ * 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) {
+ let files = {
+ "install.rdf": aData,
+ };
+ if (typeof aExtraFile == "object")
+ Object.assign(files, aExtraFile);
+ else if (aExtraFile)
+ files[aExtraFile] = "";
+
+ return AddonTestUtils.createTempXPIFile(files);
+}
+
+var gExpectedEvents = {};
+var gExpectedInstalls = [];
+var gNext = null;
+
+function getExpectedEvent(aId) {
+ if (!(aId in gExpectedEvents))
+ do_throw("Wasn't expecting events for " + aId);
+ if (gExpectedEvents[aId].length == 0)
+ do_throw("Too many events for " + aId);
+ let event = gExpectedEvents[aId].shift();
+ if (event instanceof Array)
+ return event;
+ return [event, true];
+}
+
+function getExpectedInstall(aAddon) {
+ if (gExpectedInstalls instanceof Array)
+ return gExpectedInstalls.shift();
+ if (!aAddon || !aAddon.id)
+ return gExpectedInstalls["NO_ID"].shift();
+ let id = aAddon.id;
+ if (!(id in gExpectedInstalls) || !(gExpectedInstalls[id] instanceof Array))
+ do_throw("Wasn't expecting events for " + id);
+ if (gExpectedInstalls[id].length == 0)
+ do_throw("Too many events for " + id);
+ return gExpectedInstalls[id].shift();
+}
+
+const AddonListener = {
+ onPropertyChanged: function(aAddon, aProperties) {
+ do_print(`Got onPropertyChanged event for ${aAddon.id}`);
+ let [event, properties] = getExpectedEvent(aAddon.id);
+ do_check_eq("onPropertyChanged", event);
+ do_check_eq(aProperties.length, properties.length);
+ properties.forEach(function(aProperty) {
+ // Only test that the expected properties are listed, having additional
+ // properties listed is not necessary a problem
+ if (aProperties.indexOf(aProperty) == -1)
+ do_throw("Did not see property change for " + aProperty);
+ });
+ return check_test_completed(arguments);
+ },
+
+ onEnabling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onEnabling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onEnabling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE));
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ return check_test_completed(arguments);
+ },
+
+ onEnabled: function(aAddon) {
+ do_print(`Got onEnabled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onEnabled", event);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ return check_test_completed(arguments);
+ },
+
+ onDisabling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onDisabling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onDisabling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE));
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ return check_test_completed(arguments);
+ },
+
+ onDisabled: function(aAddon) {
+ do_print(`Got onDisabled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onDisabled", event);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ return check_test_completed(arguments);
+ },
+
+ onInstalling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onInstalling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onInstalling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_INSTALL));
+ return check_test_completed(arguments);
+ },
+
+ onInstalled: function(aAddon) {
+ do_print(`Got onInstalled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onInstalled", event);
+ return check_test_completed(arguments);
+ },
+
+ onUninstalling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onUninstalling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onUninstalling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL));
+ return check_test_completed(arguments);
+ },
+
+ onUninstalled: function(aAddon) {
+ do_print(`Got onUninstalled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onUninstalled", event);
+ return check_test_completed(arguments);
+ },
+
+ onOperationCancelled: function(aAddon) {
+ do_print(`Got onOperationCancelled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onOperationCancelled", event);
+ return check_test_completed(arguments);
+ }
+};
+
+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("onNewInstall", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadStarted: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADING);
+ do_check_eq(install.error, 0);
+ do_check_eq("onDownloadStarted", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadEnded: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.error, 0);
+ do_check_eq("onDownloadEnded", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadFailed: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq("onDownloadFailed", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadCancelled: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_CANCELLED);
+ do_check_eq(install.error, 0);
+ do_check_eq("onDownloadCancelled", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onInstallStarted: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_INSTALLING);
+ do_check_eq(install.error, 0);
+ do_check_eq("onInstallStarted", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onInstallEnded: function(install, newAddon) {
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_eq(install.error, 0);
+ do_check_eq("onInstallEnded", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onInstallFailed: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_INSTALL_FAILED);
+ do_check_eq("onInstallFailed", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onInstallCancelled: function(install) {
+ // If the install was cancelled by a listener returning false from
+ // onInstallStarted, then the state will revert to STATE_DOWNLOADED.
+ let possibleStates = [AddonManager.STATE_CANCELLED,
+ AddonManager.STATE_DOWNLOADED];
+ do_check_true(possibleStates.indexOf(install.state) != -1);
+ do_check_eq(install.error, 0);
+ do_check_eq("onInstallCancelled", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onExternalInstall: function(aAddon, existingAddon, aRequiresRestart) {
+ do_check_eq("onExternalInstall", getExpectedInstall(aAddon));
+ do_check_false(aRequiresRestart);
+ return check_test_completed(arguments);
+ }
+};
+
+function hasFlag(aBits, aFlag) {
+ return (aBits & aFlag) != 0;
+}
+
+// Just a wrapper around setting the expected events
+function prepare_test(aExpectedEvents, aExpectedInstalls, aNext) {
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+
+ gExpectedInstalls = aExpectedInstalls;
+ gExpectedEvents = aExpectedEvents;
+ gNext = aNext;
+}
+
+// Checks if all expected events have been seen and if so calls the callback
+function check_test_completed(aArgs) {
+ if (!gNext)
+ return undefined;
+
+ if (gExpectedInstalls instanceof Array &&
+ gExpectedInstalls.length > 0)
+ return undefined;
+
+ for (let id in gExpectedInstalls) {
+ let installList = gExpectedInstalls[id];
+ if (installList.length > 0)
+ return undefined;
+ }
+
+ for (let id in gExpectedEvents) {
+ if (gExpectedEvents[id].length > 0)
+ return undefined;
+ }
+
+ return gNext.apply(null, aArgs);
+}
+
+// Verifies that all the expected events for all add-ons were seen
+function ensure_test_completed() {
+ for (let i in gExpectedEvents) {
+ 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);
+}
+
+/**
+ * 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) {
+ promiseCompleteAllInstalls(aInstalls).then(aCallback);
+}
+
+/**
+ * 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) {
+ promiseInstallAllFiles(aFiles, aIgnoreIncompatible).then(aCallback);
+}
+
+const EXTENSIONS_DB = "extensions.json";
+var gExtensionsJSON = gProfD.clone();
+gExtensionsJSON.append(EXTENSIONS_DB);
+
+
+// By default use strict compatibility
+Services.prefs.setBoolPref("extensions.strictCompatibility", true);
+
+// 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);
+
+
+// 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");
+ dest.lastModifiedTime = Date.now();
+}
+
+// Throw a failure and attempt to abandon the test if it looks like it is going
+// to timeout
+function timeout() {
+ timer = null;
+ do_throw("Test ran longer than " + TIMEOUT_MS + "ms");
+
+ // 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(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();
+});
+
+/**
+ * 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);
+
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+
+ return server;
+}
+
+/**
+ * Handler function that responds with the interpolated
+ * static file associated to the URL specified by request.path.
+ * This replaces the %PORT% entries in the file with the actual
+ * value of the running server's port (stored in gPort).
+ */
+function interpolateAndServeFile(request, response) {
+ try {
+ let file = gUrlToFileMap[request.path];
+ var data = "";
+ var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ fstream.init(file, -1, 0, 0);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+ do {
+ // read as much as we can and put it in str.value
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+ data = data.replace(/%PORT%/g, gPort);
+
+ response.write(data);
+ } catch (e) {
+ do_throw(`Exception while serving interpolated file: ${e}\n${e.stack}`);
+ } finally {
+ cstream.close(); // this closes fstream as well
+ }
+}
+
+/**
+ * Sets up a path handler for the given URL and saves the
+ * corresponding file in the global url -> file map.
+ *
+ * @param url
+ * the actual URL
+ * @param file
+ * nsILocalFile representing a static file
+ */
+function mapUrlToFile(url, file, server) {
+ server.registerPathHandler(url, interpolateAndServeFile);
+ gUrlToFileMap[url] = file;
+}
+
+function mapFile(path, server) {
+ mapUrlToFile(path, do_get_file(path), server);
+}
+
+/**
+ * Take out the port number in an URL
+ *
+ * @param url
+ * String that represents an URL with a port number in it
+ */
+function remove_port(url) {
+ if (typeof url === "string")
+ return url.replace(/:\d+/, "");
+ return url;
+}
+// Wrap a function (typically a callback) to catch and report exceptions
+function do_exception_wrap(func) {
+ return function() {
+ try {
+ func.apply(null, arguments);
+ }
+ catch (e) {
+ do_report_unexpected_exception(e);
+ }
+ };
+}
+
+/**
+ * Change the schema version of the JSON extensions database
+ */
+function changeXPIDBVersion(aNewVersion, aMutator = undefined) {
+ let jData = loadJSON(gExtensionsJSON);
+ jData.schemaVersion = aNewVersion;
+ if (aMutator)
+ aMutator(jData);
+ saveJSON(jData, gExtensionsJSON);
+}
+
+/**
+ * Load a file into a string
+ */
+function loadFile(aFile) {
+ let data = "";
+ let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ fstream.init(aFile, -1, 0, 0);
+ cstream.init(fstream, "UTF-8", 0, 0);
+ let str = {};
+ let read = 0;
+ do {
+ read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
+ data += str.value;
+ } while (read != 0);
+ cstream.close();
+ return data;
+}
+
+/**
+ * Raw load of a JSON file
+ */
+function loadJSON(aFile) {
+ let data = loadFile(aFile);
+ do_print("Loaded JSON file " + aFile.path);
+ return (JSON.parse(data));
+}
+
+/**
+ * Raw save of a JSON blob to file
+ */
+function saveJSON(aData, aFile) {
+ do_print("Starting to save JSON file " + aFile.path);
+ let stream = FileUtils.openSafeFileOutputStream(aFile);
+ let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(AM_Ci.nsIConverterOutputStream);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ // XXX pretty print the JSON while debugging
+ converter.writeString(JSON.stringify(aData, null, 2));
+ converter.flush();
+ // nsConverterOutputStream doesn't finish() safe output streams on close()
+ FileUtils.closeSafeFileOutputStream(stream);
+ converter.close();
+ do_print("Done saving JSON file " + aFile.path);
+}
+
+/**
+ * Create a callback function that calls do_execute_soon on an actual callback and arguments
+ */
+function callback_soon(aFunction) {
+ return function(...args) {
+ do_execute_soon(function() {
+ aFunction.apply(null, args);
+ }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback");
+ }
+}
+
+function writeProxyFileToDir(aDir, aAddon, aId) {
+ awaitPromise(promiseWriteProxyFileToDir(aDir, aAddon, aId));
+
+ let file = aDir.clone();
+ file.append(aId);
+ return file
+}
+
+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);
+ }
+}
+
+// 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);
+}
+
+// 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;
+}