mirror of
https://github.com/roytam1/basilisk55.git
synced 2026-05-26 15:02:46 +00:00
1613 lines
52 KiB
JavaScript
1613 lines
52 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
|
*/
|
|
|
|
/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */
|
|
|
|
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";
|
|
const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
|
|
const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url";
|
|
const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
|
|
|
|
// 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, "ExtensionTestCommon",
|
|
"resource://testing-common/ExtensionTestCommon.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,
|
|
promiseAddonEvent,
|
|
promiseCompleteAllInstalls,
|
|
promiseCompleteInstall,
|
|
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;
|
|
|
|
const promiseAddonByID = AddonManager.getAddonByID;
|
|
const promiseAddonsByIDs = AddonManager.getAddonsByIDs;
|
|
const promiseAddonsWithOperationsByTypes = AddonManager.getAddonsWithOperationsByTypes;
|
|
|
|
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(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(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(aAddon) {
|
|
do_print(`Got onEnabled event for ${aAddon.id}`);
|
|
let [event] = getExpectedEvent(aAddon.id);
|
|
do_check_eq("onEnabled", event);
|
|
do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
|
|
return check_test_completed(arguments);
|
|
},
|
|
|
|
onDisabling(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(aAddon) {
|
|
do_print(`Got onDisabled event for ${aAddon.id}`);
|
|
let [event] = getExpectedEvent(aAddon.id);
|
|
do_check_eq("onDisabled", event);
|
|
do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
|
|
return check_test_completed(arguments);
|
|
},
|
|
|
|
onInstalling(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(aAddon) {
|
|
do_print(`Got onInstalled event for ${aAddon.id}`);
|
|
let [event] = getExpectedEvent(aAddon.id);
|
|
do_check_eq("onInstalled", event);
|
|
return check_test_completed(arguments);
|
|
},
|
|
|
|
onUninstalling(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(aAddon) {
|
|
do_print(`Got onUninstalled event for ${aAddon.id}`);
|
|
let [event] = getExpectedEvent(aAddon.id);
|
|
do_check_eq("onUninstalled", event);
|
|
return check_test_completed(arguments);
|
|
},
|
|
|
|
onOperationCancelled(aAddon) {
|
|
do_print(`Got onOperationCancelled event for ${aAddon.id}`);
|
|
let [event] = getExpectedEvent(aAddon.id);
|
|
do_check_eq("onOperationCancelled", event);
|
|
return check_test_completed(arguments);
|
|
}
|
|
};
|
|
|
|
const InstallListener = {
|
|
onNewInstall(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(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(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(install) {
|
|
do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
|
|
do_check_eq("onDownloadFailed", getExpectedInstall());
|
|
return check_test_completed(arguments);
|
|
},
|
|
|
|
onDownloadCancelled(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(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(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(install) {
|
|
do_check_eq(install.state, AddonManager.STATE_INSTALL_FAILED);
|
|
do_check_eq("onInstallFailed", getExpectedInstall(install.addon));
|
|
return check_test_completed(arguments);
|
|
},
|
|
|
|
onInstallCancelled(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(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;
|
|
}
|
|
|
|
function initSystemAddonDirs() {
|
|
let hiddenSystemAddonDir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true);
|
|
do_get_file("data/system_addons/system1_1.xpi").copyTo(hiddenSystemAddonDir, "system1@tests.mozilla.org.xpi");
|
|
do_get_file("data/system_addons/system2_1.xpi").copyTo(hiddenSystemAddonDir, "system2@tests.mozilla.org.xpi");
|
|
|
|
let prefilledSystemAddonDir = FileUtils.getDir("ProfD", ["sysfeatures", "prefilled"], true);
|
|
do_get_file("data/system_addons/system2_2.xpi").copyTo(prefilledSystemAddonDir, "system2@tests.mozilla.org.xpi");
|
|
do_get_file("data/system_addons/system3_2.xpi").copyTo(prefilledSystemAddonDir, "system3@tests.mozilla.org.xpi");
|
|
}
|
|
|
|
/**
|
|
* Returns current system add-on update directory (stored in pref).
|
|
*/
|
|
function getCurrentSystemAddonUpdatesDir() {
|
|
const updatesDir = FileUtils.getDir("ProfD", ["features"], false);
|
|
let dir = updatesDir.clone();
|
|
let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET));
|
|
dir.append(set.directory);
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Removes all files from system add-on update directory.
|
|
*/
|
|
function clearSystemAddonUpdatesDir() {
|
|
const updatesDir = FileUtils.getDir("ProfD", ["features"], false);
|
|
// Delete any existing directories
|
|
if (updatesDir.exists())
|
|
updatesDir.remove(true);
|
|
|
|
Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
|
|
}
|
|
|
|
/**
|
|
* Installs a known set of add-ons into the system add-on update directory.
|
|
*/
|
|
function buildPrefilledUpdatesDir() {
|
|
clearSystemAddonUpdatesDir();
|
|
|
|
// Build the test set
|
|
let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true);
|
|
|
|
do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
|
|
do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
|
|
|
|
// Mark these in the past so the startup file scan notices when files have changed properly
|
|
FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
|
|
FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
|
|
|
|
Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify({
|
|
schema: 1,
|
|
directory: dir.leafName,
|
|
addons: {
|
|
"system2@tests.mozilla.org": {
|
|
version: "2.0"
|
|
},
|
|
"system3@tests.mozilla.org": {
|
|
version: "2.0"
|
|
},
|
|
}
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Check currently installed ssystem add-ons against a set of conditions.
|
|
*
|
|
* @param {Array<Object>} conditions - an array of objects of the form { isUpgrade: false, version: null}
|
|
* @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory)
|
|
*/
|
|
function* checkInstalledSystemAddons(conditions, distroDir) {
|
|
for (let i = 0; i < conditions.length; i++) {
|
|
let condition = conditions[i];
|
|
let id = "system" + (i + 1) + "@tests.mozilla.org";
|
|
let addon = yield promiseAddonByID(id);
|
|
|
|
if (!("isUpgrade" in condition) || !("version" in condition)) {
|
|
throw Error("condition must contain isUpgrade and version");
|
|
}
|
|
let isUpgrade = conditions[i].isUpgrade;
|
|
let version = conditions[i].version;
|
|
|
|
let expectedDir = isUpgrade ? getCurrentSystemAddonUpdatesDir() : distroDir;
|
|
|
|
if (version) {
|
|
do_print(`Checking state of add-on ${id}, expecting version ${version}`);
|
|
|
|
// Add-on should be installed
|
|
do_check_neq(addon, null);
|
|
do_check_eq(addon.version, version);
|
|
do_check_true(addon.isActive);
|
|
do_check_false(addon.foreignInstall);
|
|
do_check_true(addon.hidden);
|
|
do_check_true(addon.isSystem);
|
|
|
|
// Verify the add-ons file is in the right place
|
|
let file = expectedDir.clone();
|
|
file.append(id + ".xpi");
|
|
do_check_true(file.exists());
|
|
do_check_true(file.isFile());
|
|
|
|
let uri = addon.getResourceURI(null);
|
|
do_check_true(uri instanceof AM_Ci.nsIFileURL);
|
|
do_check_eq(uri.file.path, file.path);
|
|
|
|
if (isUpgrade) {
|
|
do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM);
|
|
}
|
|
|
|
// Verify the add-on actually started
|
|
BootstrapMonitor.checkAddonStarted(id, version);
|
|
} else {
|
|
do_print(`Checking state of add-on ${id}, expecting it to be missing`);
|
|
|
|
if (isUpgrade) {
|
|
// Add-on should not be installed
|
|
do_check_eq(addon, null);
|
|
}
|
|
|
|
BootstrapMonitor.checkAddonNotStarted(id);
|
|
|
|
if (addon)
|
|
BootstrapMonitor.checkAddonInstalled(id);
|
|
else
|
|
BootstrapMonitor.checkAddonNotInstalled(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns all system add-on updates directories.
|
|
*/
|
|
function* getSystemAddonDirectories() {
|
|
const updatesDir = FileUtils.getDir("ProfD", ["features"], false);
|
|
let subdirs = [];
|
|
|
|
if (yield OS.File.exists(updatesDir.path)) {
|
|
let iterator = new OS.File.DirectoryIterator(updatesDir.path);
|
|
yield iterator.forEach(entry => {
|
|
if (entry.isDir) {
|
|
subdirs.push(entry);
|
|
}
|
|
});
|
|
iterator.close();
|
|
}
|
|
|
|
return subdirs;
|
|
}
|
|
|
|
/**
|
|
* Sets up initial system add-on update conditions.
|
|
*
|
|
* @param {Object<function, Array<Object>} setup - an object containing a setup function and an array of objects
|
|
* of the form {isUpgrade: false, version: null}
|
|
*
|
|
* @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory)
|
|
*/
|
|
function* setupSystemAddonConditions(setup, distroDir) {
|
|
do_print("Clearing existing database.");
|
|
Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
|
|
distroDir.leafName = "empty";
|
|
startupManager(false);
|
|
yield promiseShutdownManager();
|
|
|
|
do_print("Setting up conditions.");
|
|
yield setup.setup();
|
|
|
|
startupManager(false);
|
|
|
|
// Make sure the initial state is correct
|
|
do_print("Checking initial state.");
|
|
yield checkInstalledSystemAddons(setup.initialState, distroDir);
|
|
}
|
|
|
|
/**
|
|
* Verify state of system add-ons after installation.
|
|
*
|
|
* @param {Array<Object>} initialState - an array of objects of the form {isUpgrade: false, version: null}
|
|
* @param {Array<Object>} finalState - an array of objects of the form {isUpgrade: false, version: null}
|
|
* @param {Boolean} alreadyUpgraded - whether a restartless upgrade has already been performed.
|
|
* @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory)
|
|
*/
|
|
function* verifySystemAddonState(initialState, finalState = undefined, alreadyUpgraded = false, distroDir) {
|
|
let expectedDirs = 0;
|
|
|
|
// If the initial state was using the profile set then that directory will
|
|
// still exist.
|
|
|
|
if (initialState.some(a => a.isUpgrade)) {
|
|
expectedDirs++;
|
|
}
|
|
|
|
if (finalState == undefined) {
|
|
finalState = initialState;
|
|
} else if (finalState.some(a => a.isUpgrade)) {
|
|
// If the new state is using the profile then that directory will exist.
|
|
expectedDirs++;
|
|
}
|
|
|
|
// Since upgrades are restartless now, the previous update dir hasn't been removed.
|
|
if (alreadyUpgraded) {
|
|
expectedDirs++;
|
|
}
|
|
|
|
do_print("Checking final state.");
|
|
|
|
let dirs = yield getSystemAddonDirectories();
|
|
do_check_eq(dirs.length, expectedDirs);
|
|
|
|
yield checkInstalledSystemAddons(...finalState, distroDir);
|
|
|
|
// Check that the new state is active after a restart
|
|
yield promiseRestartManager();
|
|
yield checkInstalledSystemAddons(finalState, distroDir);
|
|
}
|
|
|
|
/**
|
|
* Run system add-on tests and compare the results against a set of expected conditions.
|
|
*
|
|
* @param {String} setupName - name of the current setup conditions.
|
|
* @param {Object<function, Array<Object>} setup - Defines the set of initial conditions to run each test against. Each should
|
|
* define the following properties:
|
|
* setup: A task to setup the profile into the initial state.
|
|
* initialState: The initial expected system add-on state after setup has run.
|
|
* @param {Array<Object>} test - The test to run. Each test must define an updateList or test. The following
|
|
* properties are used:
|
|
* updateList: The set of add-ons the server should respond with.
|
|
* test: A function to run to perform the update check (replaces
|
|
* updateList)
|
|
* fails: An optional property, if true the update check is expected to
|
|
* fail.
|
|
* finalState: An optional property, the expected final state of system add-ons,
|
|
* if missing the test condition's initialState is used.
|
|
* @param {nsIFile} distroDir - the system add-on distribution directory (the "features" dir in the app directory)
|
|
* @param {String} root - HTTP URL to the test server
|
|
* @param {HttpServer} testserver - existing HTTP test server to use
|
|
*/
|
|
|
|
function* execSystemAddonTest(setupName, setup, test, distroDir, root, testserver) {
|
|
yield setupSystemAddonConditions(setup, distroDir);
|
|
|
|
try {
|
|
if ("test" in test) {
|
|
yield test.test();
|
|
} else {
|
|
yield installSystemAddons(yield buildSystemAddonUpdates(test.updateList, root), testserver);
|
|
}
|
|
|
|
if (test.fails) {
|
|
do_throw("Expected this test to fail");
|
|
}
|
|
} catch (e) {
|
|
if (!test.fails) {
|
|
do_throw(e);
|
|
}
|
|
}
|
|
|
|
// some tests have a different expected combination of default
|
|
// and updated add-ons.
|
|
if (test.finalState && setupName in test.finalState) {
|
|
yield verifySystemAddonState(setup.initialState, test.finalState[setupName], false, distroDir);
|
|
} else {
|
|
yield verifySystemAddonState(setup.initialState, undefined, false, distroDir);
|
|
}
|
|
|
|
yield promiseShutdownManager();
|
|
}
|