diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
commit | 37d5300335d81cecbecc99812747a657588c63eb (patch) | |
tree | 765efa3b6a56bb715d9813a8697473e120436278 /toolkit/mozapps/webextensions/test/xpcshell/head_addons.js | |
parent | b2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff) | |
parent | 4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff) | |
download | UXP-37d5300335d81cecbecc99812747a657588c63eb.tar UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz UXP-37d5300335d81cecbecc99812747a657588c63eb.zip |
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/mozapps/webextensions/test/xpcshell/head_addons.js')
-rw-r--r-- | toolkit/mozapps/webextensions/test/xpcshell/head_addons.js | 1345 |
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; +} |