/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */ if (!_TEST_NAME.includes("toolkit/mozapps/extensions/test/xpcshell/")) { Assert.ok( false, "head_addons.js may not be loaded by tests outside of " + "the add-on manager component." ); } const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility"; const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url"; const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required"; // 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; const { AddonManager, AddonManagerPrivate } = ChromeUtils.importESModule( "resource://gre/modules/AddonManager.sys.mjs" ); var { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); var { FileUtils } = ChromeUtils.importESModule( "resource://gre/modules/FileUtils.sys.mjs" ); var { NetUtil } = ChromeUtils.importESModule( "resource://gre/modules/NetUtil.sys.mjs" ); var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); var { AddonRepository } = ChromeUtils.importESModule( "resource://gre/modules/addons/AddonRepository.sys.mjs" ); var { AddonTestUtils, MockAsyncShutdown } = ChromeUtils.importESModule( "resource://testing-common/AddonTestUtils.sys.mjs" ); ChromeUtils.defineESModuleGetters(this, { Blocklist: "resource://gre/modules/Blocklist.sys.mjs", Extension: "resource://gre/modules/Extension.sys.mjs", ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs", ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", HttpServer: "resource://testing-common/httpd.sys.mjs", MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs", MockRegistry: "resource://testing-common/MockRegistry.sys.mjs", PromiseTestUtils: "resource://testing-common/PromiseTestUtils.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", TestUtils: "resource://testing-common/TestUtils.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", }); XPCOMUtils.defineLazyServiceGetter( this, "aomStartup", "@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup" ); const { createAppInfo, createHttpServer, createTempWebExtensionFile, getFileForAddon, manuallyInstall, manuallyUninstall, overrideBuiltIns, promiseAddonEvent, promiseCompleteAllInstalls, promiseCompleteInstall, promiseConsoleOutput, promiseFindAddonUpdates, promiseInstallAllFiles, promiseInstallFile, promiseRestartManager, promiseSetExtensionModifiedTime, promiseShutdownManager, promiseStartupManager, promiseWebExtensionStartup, promiseWriteProxyFileToDir, registerDirectory, setExtensionModifiedTime, writeFilesToZip, } = AddonTestUtils; // WebExtension wrapper for ease of testing ExtensionTestUtils.init(this); AddonTestUtils.init(this); AddonTestUtils.overrideCertDB(); ChromeUtils.defineLazyGetter( this, "BOOTSTRAP_REASONS", () => AddonManagerPrivate.BOOTSTRAP_REASONS ); function getReasonName(reason) { for (let key of Object.keys(BOOTSTRAP_REASONS)) { if (BOOTSTRAP_REASONS[key] == reason) { return key; } } throw new Error("This shouldn't happen."); } Object.defineProperty(this, "gAppInfo", { get() { return AddonTestUtils.appInfo; }, }); Object.defineProperty(this, "gAddonStartup", { get() { return AddonTestUtils.addonStartup.clone(); }, }); Object.defineProperty(this, "gInternalManager", { get() { return AddonTestUtils.addonIntegrationService.QueryInterface( 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) { AddonTestUtils.useRealCertChecks = val; }, }); Object.defineProperty(this, "TEST_UNPACKED", { get() { return AddonTestUtils.testUnpacked; }, set(val) { AddonTestUtils.testUnpacked = val; }, }); const promiseAddonByID = AddonManager.getAddonByID; const promiseAddonsByIDs = AddonManager.getAddonsByIDs; const promiseAddonsByTypes = AddonManager.getAddonsByTypes; var gPort = null; var BootstrapMonitor = { started: new Map(), stopped: new Map(), installed: new Map(), uninstalled: new Map(), init() { this.onEvent = this.onEvent.bind(this); AddonTestUtils.on("addon-manager-shutdown", this.onEvent); AddonTestUtils.on("bootstrap-method", this.onEvent); }, shutdownCheck() { equal( this.started.size, 0, "Should have no add-ons that were started but not shutdown" ); }, onEvent(msg, data) { switch (msg) { case "addon-manager-shutdown": this.shutdownCheck(); break; case "bootstrap-method": this.onBootstrapMethod(data.method, data.params, data.reason); break; } }, onBootstrapMethod(method, params, reason) { let { id } = params; info( `Bootstrap method ${method} ${reason} for ${params.id} version ${params.version}` ); if (method !== "install") { this.checkInstalled(id); } switch (method) { case "install": this.checkNotInstalled(id); this.installed.set(id, { reason, params }); this.uninstalled.delete(id); break; case "startup": this.checkNotStarted(id); this.started.set(id, { reason, params }); this.stopped.delete(id); break; case "shutdown": this.checkMatches("shutdown", "startup", params, this.started.get(id)); this.checkStarted(id); this.stopped.set(id, { reason, params }); this.started.delete(id); break; case "uninstall": this.checkMatches( "uninstall", "install", params, this.installed.get(id) ); this.uninstalled.set(id, { reason, params }); this.installed.delete(id); break; case "update": this.checkMatches("update", "install", params, this.installed.get(id)); this.installed.set(id, { reason, params, method }); break; } }, clear(id) { this.installed.delete(id); this.started.delete(id); this.stopped.delete(id); this.uninstalled.delete(id); }, checkMatches(method, lastMethod, params, { params: lastParams } = {}) { ok( lastParams, `Expecting matching ${lastMethod} call for add-on ${params.id} ${method} call` ); if (method == "update") { equal( params.oldVersion, lastParams.version, "params.oldVersion should match last call" ); } else { equal( params.version, lastParams.version, "params.version should match last call" ); } if (method !== "update" && method !== "uninstall") { equal( params.resourceURI.spec, lastParams.resourceURI.spec, `params.resourceURI should match last call` ); ok( params.resourceURI.equals(lastParams.resourceURI), `params.resourceURI should match: "${params.resourceURI.spec}" == "${lastParams.resourceURI.spec}"` ); } }, checkStarted(id, version = undefined) { let started = this.started.get(id); ok(started, `Should have seen startup method call for ${id}`); if (version !== undefined) { equal(started.params.version, version, "Expected version number"); } return started; }, checkNotStarted(id) { ok( !this.started.has(id), `Should not have seen startup method call for ${id}` ); }, checkInstalled(id, version = undefined) { const installed = this.installed.get(id); ok(installed, `Should have seen install call for ${id}`); if (version !== undefined) { equal(installed.params.version, version, "Expected version number"); } return installed; }, checkUpdated(id, version = undefined) { const installed = this.installed.get(id); equal(installed.method, "update", `Should have seen update call for ${id}`); if (version !== undefined) { equal(installed.params.version, version, "Expected version number"); } return installed; }, checkNotInstalled(id) { ok( !this.installed.has(id), `Should not have seen install method call for ${id}` ); }, }; function isNightlyChannel() { var channel = Services.prefs.getCharPref("app.update.channel", "default"); return ( channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr" ); } async function restartWithLocales(locales) { Services.locale.requestedLocales = locales; await promiseRestartManager(); } function delay(msec) { return new Promise(resolve => { setTimeout(resolve, msec); }); } /** * Returns a map of Addon objects for installed add-ons with the given * IDs. The returned map contains a key for the ID of each add-on that * is found. IDs for add-ons which do not exist are not present in the * map. * * @param {sequence} ids * The list of add-on IDs to get. * @returns {Promise} * Map of add-ons that were found. */ async function getAddons(ids) { let addons = new Map(); for (let addon of await AddonManager.getAddonsByIDs(ids)) { if (addon) { addons.set(addon.id, addon); } } return addons; } /** * Checks that the given add-on has the given expected properties. * * @param {string} id * The id of the add-on. * @param {Addon?} addon * The add-on object, or null if the add-on does not exist. * @param {object?} expected * An object containing the expected values for properties of the * add-on, or null if the add-on is expected not to exist. */ function checkAddon(id, addon, expected) { info(`Checking state of addon ${id}`); if (expected === null) { ok(!addon, `Addon ${id} should not exist`); } else { ok(addon, `Addon ${id} should exist`); for (let [key, value] of Object.entries(expected)) { if (value instanceof Ci.nsIURI) { equal( addon[key] && addon[key].spec, value.spec, `Expected value of addon.${key}` ); } else { deepEqual(addon[key], value, `Expected value of addon.${key}`); } } } } /** * 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 (!AppConstants.MOZ_CRASHREPORTER) { return; } if (!("Add-ons" in gAppInfo.annotations)) { Assert.ok(false, "Cannot find Add-ons entry in crash annotations"); return; } let addons = gAppInfo.annotations["Add-ons"].split(","); Assert.ok( addons.includes( `${encodeURIComponent(aId)}:${encodeURIComponent(aVersion)}` ) ); } /** * 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 (!AppConstants.MOZ_CRASHREPORTER) { return; } if (!("Add-ons" in gAppInfo.annotations)) { Assert.ok(true); return; } let addons = gAppInfo.annotations["Add-ons"].split(","); Assert.ok( !addons.includes( `${encodeURIComponent(aId)}:${encodeURIComponent(aVersion)}` ) ); } function do_get_file_hash(aFile, aAlgorithm) { if (!aAlgorithm) { aAlgorithm = "sha256"; } let crypto = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); crypto.initWithString(aAlgorithm); let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( 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"; } /** * Returns the file containing the add-on. For packed add-ons, this is * an XPI file. For unpacked add-ons, it is the add-on's root directory. * * @param {Addon} addon * @returns {nsIFile} */ function getAddonFile(addon) { let uri = addon.getResourceURI(""); if (uri instanceof Ci.nsIJARURI) { uri = uri.JARFile; } return uri.QueryInterface(Ci.nsIFileURL).file; } /** * 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) { Assert.notEqual(aActualAddons, null); Assert.equal(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) { Assert.notEqual(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": Assert.equal(actualValue.length, expectedValue.length); for (let i = 0; i < actualValue.length; i++) { do_check_author(actualValue[i], expectedValue[i]); } break; case "screenshots": Assert.equal(actualValue.length, expectedValue.length); for (let i = 0; i < actualValue.length; i++) { do_check_screenshot(actualValue[i], expectedValue[i]); } break; case "sourceURI": Assert.equal(actualValue.spec, expectedValue); break; case "updateDate": Assert.equal(actualValue.getTime(), expectedValue.getTime()); break; case "compatibilityOverrides": Assert.equal(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 (actualValue !== 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) { Assert.equal(aActual.toString(), aExpected.name); Assert.equal(aActual.name, aExpected.name); Assert.equal(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) { Assert.equal(aActual.toString(), aExpected.url); Assert.equal(aActual.url, aExpected.url); Assert.equal(aActual.width, aExpected.width); Assert.equal(aActual.height, aExpected.height); Assert.equal(aActual.thumbnailURL, aExpected.thumbnailURL); Assert.equal(aActual.thumbnailWidth, aExpected.thumbnailWidth); Assert.equal(aActual.thumbnailHeight, aExpected.thumbnailHeight); Assert.equal(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) { Assert.equal(aActual.type, aExpected.type); Assert.equal(aActual.minVersion, aExpected.minVersion); Assert.equal(aActual.maxVersion, aExpected.maxVersion); Assert.equal(aActual.appID, aExpected.appID); Assert.equal(aActual.appMinVersion, aExpected.appMinVersion); Assert.equal(aActual.appMaxVersion, aExpected.appMaxVersion); } function do_check_icons(aActual, aExpected) { for (var size in aExpected) { Assert.equal(aActual[size], aExpected[size]); } } function isThemeInAddonsList(aDir, aId) { return AddonTestUtils.addonsList.hasTheme(aDir, aId); } function isExtensionInBootstrappedList(aDir, aId) { return AddonTestUtils.addonsList.hasExtension(aDir, aId); } /** * 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) { let files = { "manifest.json": JSON.stringify(aData), }; if (!aId) { aId = aData?.browser_specific_settings?.gecko?.id || aData?.applications?.gecko?.id; } return AddonTestUtils.promiseWriteFilesToExtension(aDir.path, aId, files); } function hasFlag(aBits, aFlag) { return (aBits & aFlag) != 0; } class EventChecker { constructor(options) { this.expectedEvents = options.addonEvents || {}; this.expectedInstalls = options.installEvents || null; this.ignorePlugins = options.ignorePlugins || false; this.finished = new Promise(resolve => { this.resolveFinished = resolve; }); AddonManager.addAddonListener(this); if (this.expectedInstalls) { AddonManager.addInstallListener(this); } } cleanup() { AddonManager.removeAddonListener(this); if (this.expectedInstalls) { AddonManager.removeInstallListener(this); } } checkValue(prop, value, flagName) { if (Array.isArray(flagName)) { let names = flagName.map(name => `AddonManager.${name}`); Assert.ok( flagName.map(name => AddonManager[name]).includes(value), `${prop} value \`${value}\` should be one of [${names.join(", ")}` ); } else { Assert.equal( value, AddonManager[flagName], `${prop} should have value AddonManager.${flagName}` ); } } checkFlag(prop, value, flagName) { Assert.equal( value & AddonManager[flagName], AddonManager[flagName], `${prop} should have flag AddonManager.${flagName}` ); } checkNoFlag(prop, value, flagName) { Assert.ok( !(value & AddonManager[flagName]), `${prop} should not have flag AddonManager.${flagName}` ); } checkComplete() { if (this.expectedInstalls && this.expectedInstalls.length) { return; } if (Object.values(this.expectedEvents).some(events => events.length)) { return; } info("Test complete"); this.cleanup(); this.resolveFinished(); } ensureComplete() { this.cleanup(); for (let [id, events] of Object.entries(this.expectedEvents)) { Assert.equal( events.length, 0, `Should have no remaining events for ${id}` ); } if (this.expectedInstalls) { Assert.deepEqual( this.expectedInstalls, [], "Should have no remaining install events" ); } } // Add-on listener events getExpectedEvent(aId) { if (!(aId in this.expectedEvents)) { return null; } let events = this.expectedEvents[aId]; Assert.ok(!!events.length, `Should be expecting events for ${aId}`); return events.shift(); } checkAddonEvent(event, addon, details = {}) { info(`Got event "${event}" for add-on ${addon.id}`); if ("requiresRestart" in details) { Assert.equal( details.requiresRestart, false, "requiresRestart should always be false" ); } let expected = this.getExpectedEvent(addon.id); if (!expected) { return undefined; } Assert.equal( expected.event, event, `Expecting event "${expected.event}" got "${event}"` ); for (let prop of ["properties"]) { if (prop in expected) { Assert.deepEqual( expected[prop], details[prop], `Expected value for ${prop}` ); } } this.checkComplete(); if ("returnValue" in expected) { return expected.returnValue; } return undefined; } onPropertyChanged(addon, properties) { return this.checkAddonEvent("onPropertyChanged", addon, { properties }); } onEnabling(addon, requiresRestart) { let result = this.checkAddonEvent("onEnabling", addon, { requiresRestart }); this.checkNoFlag("addon.permissions", addon.permissions, "PERM_CAN_ENABLE"); return result; } onEnabled(addon) { let result = this.checkAddonEvent("onEnabled", addon); this.checkNoFlag("addon.permissions", addon.permissions, "PERM_CAN_ENABLE"); return result; } onDisabling(addon, requiresRestart) { let result = this.checkAddonEvent("onDisabling", addon, { requiresRestart, }); this.checkNoFlag( "addon.permissions", addon.permissions, "PERM_CAN_DISABLE" ); return result; } onDisabled(addon) { let result = this.checkAddonEvent("onDisabled", addon); this.checkNoFlag( "addon.permissions", addon.permissions, "PERM_CAN_DISABLE" ); return result; } onInstalling(addon, requiresRestart) { return this.checkAddonEvent("onInstalling", addon, { requiresRestart }); } onInstalled(addon) { return this.checkAddonEvent("onInstalled", addon); } onUninstalling(addon) { return this.checkAddonEvent("onUninstalling", addon); } onUninstalled(addon) { return this.checkAddonEvent("onUninstalled", addon); } onOperationCancelled(addon) { return this.checkAddonEvent("onOperationCancelled", addon); } // Install listener events. checkInstall(event, install, details = {}) { // Lazy initialization of the plugin host means we can get spurious // install events for plugins. If we're not looking for plugin // installs, ignore them completely. If we *are* looking for plugin // installs, the onus is on the individual test to ensure it waits // for the plugin host to have done its initial work. if (this.ignorePlugins && install.type == "plugin") { info(`Ignoring install event for plugin ${install.id}`); return undefined; } info(`Got install event "${event}"`); let expected = this.expectedInstalls.shift(); Assert.ok(expected, "Should be expecting install event"); Assert.equal( expected.event, event, "Should be expecting onExternalInstall event" ); if ("state" in details) { this.checkValue("install.state", install.state, details.state); } this.checkComplete(); if ("callback" in expected) { expected.callback(install); } if ("returnValue" in expected) { return expected.returnValue; } return undefined; } onNewInstall(install) { let result = this.checkInstall("onNewInstall", install, { state: ["STATE_DOWNLOADED", "STATE_DOWNLOAD_FAILED", "STATE_AVAILABLE"], }); if (install.state != AddonManager.STATE_DOWNLOAD_FAILED) { Assert.equal(install.error, 0, "Should have no error"); } else { Assert.notEqual(install.error, 0, "Should have error"); } return result; } onDownloadStarted(install) { return this.checkInstall("onDownloadStarted", install, { state: "STATE_DOWNLOADING", error: 0, }); } onDownloadEnded(install) { return this.checkInstall("onDownloadEnded", install, { state: "STATE_DOWNLOADED", error: 0, }); } onDownloadFailed(install) { return this.checkInstall("onDownloadFailed", install, { state: "STATE_FAILED", }); } onDownloadCancelled(install) { return this.checkInstall("onDownloadCancelled", install, { state: "STATE_CANCELLED", error: 0, }); } onInstallStarted(install) { return this.checkInstall("onInstallStarted", install, { state: "STATE_INSTALLING", error: 0, }); } onInstallEnded(install) { return this.checkInstall("onInstallEnded", install, { state: "STATE_INSTALLED", error: 0, }); } onInstallFailed(install) { return this.checkInstall("onInstallFailed", install, { state: "STATE_FAILED", }); } onInstallCancelled(install) { // If the install was cancelled by a listener returning false from // onInstallStarted, then the state will revert to STATE_DOWNLOADED. return this.checkInstall("onInstallCancelled", install, { state: ["STATE_CANCELED", "STATE_DOWNLOADED"], error: 0, }); } onExternalInstall(addon, existingAddon, requiresRestart) { if (this.ignorePlugins && addon.type == "plugin") { info(`Ignoring install event for plugin ${addon.id}`); return undefined; } let expected = this.expectedInstalls.shift(); Assert.ok(expected, "Should be expecting install event"); Assert.equal( expected.event, "onExternalInstall", "Should be expecting onExternalInstall event" ); Assert.ok(!requiresRestart, "Should never require restart"); this.checkComplete(); if ("returnValue" in expected) { return expected.returnValue; } return undefined; } } /** * Run the giving callback function, and expect the given set of add-on * and install listener events to be emitted, and returns a promise * which resolves when they have all been observed. * * If `callback` returns a promise, all events are expected to be * observed by the time the promise resolves. If not, simply waits for * all events to be observed before resolving the returned promise. * * @param {object} details * @param {function} callback * @returns {Promise} */ /* exported expectEvents */ async function expectEvents(details, callback) { let checker = new EventChecker(details); try { let result = callback(); if ( result && typeof result === "object" && typeof result.then === "function" ) { result = await result; checker.ensureComplete(); } else { await checker.finished; } return result; } catch (e) { do_throw(e); return undefined; } } const EXTENSIONS_DB = "extensions.json"; var gExtensionsJSON = gProfD.clone(); gExtensionsJSON.append(EXTENSIONS_DB); async function promiseInstallWebExtension(aData) { let addonFile = createTempWebExtensionFile(aData); let { addon } = await promiseInstallFile(addonFile); return addon; } // By default use strict compatibility Services.prefs.setBoolPref("extensions.strictCompatibility", true); // Ensure signature checks are enabled by default Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true); Services.prefs.setBoolPref("extensions.experiments.enabled", 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(); } async function mockGfxBlocklistItemsFromDisk(path) { let response = await fetch(Services.io.newFileURI(do_get_file(path)).spec); let json = await response.json(); return mockGfxBlocklistItems(json); } async function mockGfxBlocklistItems(items) { const { generateUUID } = Services.uuid; const { BlocklistPrivate } = ChromeUtils.importESModule( "resource://gre/modules/Blocklist.sys.mjs" ); const client = RemoteSettings("gfx", { bucketName: "blocklists", }); const records = items.map(item => { if (item.id && item.last_modified) { return item; } return { id: generateUUID().toString().replace(/[{}]/g, ""), last_modified: Date.now(), ...item, }; }); const collectionTimestamp = Math.max(...records.map(r => r.last_modified)); await client.db.importChanges({}, collectionTimestamp, records, { clear: true, }); let rv = await BlocklistPrivate.GfxBlocklistRS.checkForEntries(); return rv; } /** * Change the schema version of the JSON extensions database */ async function changeXPIDBVersion(aNewVersion) { let json = await IOUtils.readJSON(gExtensionsJSON.path); json.schemaVersion = aNewVersion; await IOUtils.writeJSON(gExtensionsJSON.path, json); } async function setInitialState(addon, initialState) { if (initialState.userDisabled) { await addon.disable(); } else if (initialState.userDisabled === false) { await addon.enable(); } } async function setupBuiltinExtension(extensionData, location = "ext-test") { let xpi = await AddonTestUtils.createTempWebExtensionFile(extensionData); // The built-in location requires a resource: URL that maps to a // jar: or file: URL. This would typically be something bundled // into omni.ja but for testing we just use a temp file. let base = Services.io.newURI(`jar:file:${xpi.path}!/`); let resProto = Services.io .getProtocolHandler("resource") .QueryInterface(Ci.nsIResProtocolHandler); resProto.setSubstitution(location, base); } async function installBuiltinExtension(extensionData, waitForStartup = true) { await setupBuiltinExtension(extensionData); let id = extensionData.manifest?.browser_specific_settings?.gecko?.id || extensionData.manifest?.applications?.gecko?.id; let wrapper = ExtensionTestUtils.expectExtension(id); await AddonManager.installBuiltinAddon("resource://ext-test/"); if (waitForStartup) { await wrapper.awaitStartup(); } return wrapper; } function useAMOStageCert() { // NOTE: add_task internally calls add_test which mutate the add_task properties object, // and so we should not reuse the same object as add_task options passed to multiple // add_task calls. return { pref_set: [["xpinstall.signatures.dev-root", true]] }; }