/* 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 = `\n\n\n`; if (addons) { xml += ` \n`; for (let addon of addons) { xml += ` \n`; } xml += ` \n`; } xml += `\n`; return xml; }