From 0ca0a616cf9a7cf1641d1a730b7f90f865ff3878 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Tue, 3 Nov 2015 14:49:46 -0800 Subject: [PATCH] Bug 1214058: Part 1 - Add a simplified JSON-based add-on update protocol. r=Mossop --- modules/libpref/init/all.js | 1 + toolkit/mozapps/extensions/AddonManager.jsm | 16 + .../internal/AddonUpdateChecker.jsm | 248 ++++++++-- .../extensions/internal/XPIProvider.jsm | 2 +- .../test/xpcshell/data/test_updatecheck.json | 327 +++++++++++++ .../test/xpcshell/data/test_updatecheck.rdf | 2 +- .../extensions/test/xpcshell/head_addons.js | 66 ++- .../test/xpcshell/test_json_updatecheck.js | 373 ++++++++++++++ .../test/xpcshell/test_updatecheck.js | 462 ++++++++---------- .../test/xpcshell/xpcshell-shared.ini | 1 + 10 files changed, 1190 insertions(+), 308 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 65b9b7ebbd2..001a6d9570c 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4402,6 +4402,7 @@ pref("xpinstall.whitelist.required", true); pref("xpinstall.signatures.required", false); pref("extensions.alwaysUnpack", false); pref("extensions.minCompatiblePlatformVersion", "2.0"); +pref("extensions.webExtensionsMinPlatformVersion", "42.0a1"); pref("network.buffer.cache.count", 24); pref("network.buffer.cache.size", 32768); diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index 0864cfbec3d..184bf3b1f3c 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -42,6 +42,8 @@ const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; const PREF_SELECTED_LOCALE = "general.useragent.locale"; const UNKNOWN_XPCOM_ABI = "unknownABI"; +const PREF_MIN_WEBEXT_PLATFORM_VERSION = "extensions.webExtensionsMinPlatformVersion"; + const UPDATE_REQUEST_VERSION = 2; const CATEGORY_UPDATE_PARAMS = "extension-update-params"; @@ -663,6 +665,7 @@ var gCheckUpdateSecurity = gCheckUpdateSecurityDefault; var gUpdateEnabled = true; var gAutoUpdateDefault = true; var gHotfixID = null; +var gWebExtensionsMinPlatformVersion = null; var gShutdownBarrier = null; var gRepoShutdownState = ""; var gShutdownInProgress = false; @@ -947,6 +950,11 @@ var AddonManagerInternal = { } catch (e) {} Services.prefs.addObserver(PREF_EM_HOTFIX_ID, this, false); + try { + gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(PREF_MIN_WEBEXT_PLATFORM_VERSION); + } catch (e) {} + Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this, false); + let defaultProvidersEnabled = true; try { defaultProvidersEnabled = Services.prefs.getBoolPref(PREF_DEFAULT_PROVIDERS_ENABLED); @@ -1377,6 +1385,10 @@ var AddonManagerInternal = { } break; } + case PREF_MIN_WEBEXT_PLATFORM_VERSION: { + gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(PREF_MIN_WEBEXT_PLATFORM_VERSION); + break; + } } }, @@ -2894,6 +2906,10 @@ this.AddonManagerPrivate = { safeCall(listener.onUpdateFinished.bind(listener), addon); } }, + + get webExtensionsMinPlatformVersion() { + return gWebExtensionsMinPlatformVersion; + }, }; /** diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm index f6becf98fb4..a567eb678de 100644 --- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm @@ -31,6 +31,8 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", + "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm"); @@ -217,6 +219,48 @@ RDFSerializer.prototype = { } } +/** + * Sanitizes the update URL in an update item, as returned by + * parseRDFManifest and parseJSONManifest. Ensures that: + * + * - The URL is secure, or secured by a strong enough hash. + * - The security principal of the update manifest has permission to + * load the URL. + * + * @param aUpdate + * The update item to sanitize. + * @param aRequest + * The XMLHttpRequest used to load the manifest. + * @param aHashPattern + * The regular expression used to validate the update hash. + * @param aHashString + * The human-readable string specifying which hash functions + * are accepted. + */ +function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) { + if (aUpdate.updateURL) { + let scriptSecurity = Services.scriptSecurityManager; + let principal = scriptSecurity.getChannelURIPrincipal(aRequest.channel); + try { + // This logs an error on failure, so no need to log it a second time + scriptSecurity.checkLoadURIStrWithPrincipal(principal, aUpdate.updateURL, + scriptSecurity.DISALLOW_SCRIPT); + } catch (e) { + delete aUpdate.updateURL; + return; + } + + if (AddonManager.checkUpdateSecurity && + !aUpdate.updateURL.startsWith("https:") && + !aHashPattern.test(aUpdate.updateHash)) { + logger.warn(`Update link ${aUpdate.updateURL} is not secure and is not verified ` + + `by a strong enough hash (needs to be ${aHashString}).`); + delete aUpdate.updateURL; + delete aUpdate.updateHash; + } + } +} + /** * Parses an RDF style update manifest into an array of update objects. * @@ -226,10 +270,17 @@ RDFSerializer.prototype = { * An optional update key for the add-on * @param aRequest * The XMLHttpRequest that has retrieved the update manifest + * @param aManifestData + * The pre-parsed manifest, as a bare XML DOM document * @return an array of update objects * @throws if the update manifest is invalid in any way */ -function parseRDFManifest(aId, aUpdateKey, aRequest) { +function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) { + if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) { + throw Components.Exception("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI); + return; + } + function EM_R(aProp) { return gRDF.GetResource(PREFIX_NS_EM + aProp); } @@ -366,20 +417,136 @@ function parseRDFManifest(aId, aUpdateKey, aRequest) { targetApplications: [appEntry] }; - if (result.updateURL && AddonManager.checkUpdateSecurity && - result.updateURL.substring(0, 6) != "https:" && - (!result.updateHash || result.updateHash.substring(0, 3) != "sha")) { - logger.warn("updateLink " + result.updateURL + " is not secure and is not verified" + - " by a strong enough hash (needs to be sha1 or stronger)."); - delete result.updateURL; - delete result.updateHash; - } + // The JSON update protocol requires an SHA-2 hash. RDF still + // supports SHA-1, for compatibility reasons. + sanitizeUpdateURL(result, aRequest, /^sha/, "sha1 or stronger"); + results.push(result); } } return results; } +/** + * Parses an JSON update manifest into an array of update objects. + * + * @param aId + * The ID of the add-on being checked for updates + * @param aUpdateKey + * An optional update key for the add-on + * @param aRequest + * The XMLHttpRequest that has retrieved the update manifest + * @param aManifestData + * The pre-parsed manifest, as a JSON object tree + * @return an array of update objects + * @throws if the update manifest is invalid in any way + */ +function parseJSONManifest(aId, aUpdateKey, aRequest, aManifestData) { + if (aUpdateKey) + throw Components.Exception("Update keys are not supported for JSON update manifests"); + + let TYPE_CHECK = { + "array": val => Array.isArray(val), + "object": val => val && typeof val == "object" && !Array.isArray(val), + }; + + function getProperty(aObj, aProperty, aType, aDefault = undefined) { + if (!(aProperty in aObj)) + return aDefault; + + let value = aObj[aProperty]; + + let matchesType = aType in TYPE_CHECK ? TYPE_CHECK[aType](value) : typeof value == aType; + if (!matchesType) + throw Components.Exception(`Update manifest property '${aProperty}' has incorrect type (expected ${aType})`); + + return value; + } + + function getRequiredProperty(aObj, aProperty, aType) { + let value = getProperty(aObj, aProperty, aType); + if (value === undefined) + throw Components.Exception(`Update manifest is missing a required ${aProperty} property.`); + return value; + } + + let manifest = aManifestData; + + if (!TYPE_CHECK["object"](manifest)) + throw Components.Exception("Root element of update manifest must be a JSON object literal"); + + // The set of add-ons this manifest has updates for + let addons = getRequiredProperty(manifest, "addons", "object"); + + // The entry for this particular add-on + let addon = getProperty(addons, aId, "object"); + + // A missing entry doesn't count as a failure, just as no avialable update + // information + if (!addon) { + logger.warn("Update manifest did not contain an entry for " + aId); + return []; + } + + // The list of available updates + let updates = getProperty(addon, "updates", "array", []); + + let results = []; + + for (let update of updates) { + let version = getRequiredProperty(update, "version", "string"); + + logger.debug(`Found an update entry for ${aId} version ${version}`); + + let applications = getProperty(update, "applications", "object", + { gecko: {} }); + + // "gecko" is currently the only supported application entry. If + // it's missing, skip this update. + if (!("gecko" in applications)) + continue; + + let app = getProperty(applications, "gecko", "object"); + + let appEntry = { + id: TOOLKIT_ID, + minVersion: getProperty(app, "strict_min_version", "string", + AddonManagerPrivate.webExtensionsMinPlatformVersion), + maxVersion: "*", + }; + + let result = { + id: aId, + version: version, + multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", true), + updateURL: getProperty(update, "update_link", "string"), + updateHash: getProperty(update, "update_hash", "string"), + updateInfoURL: getProperty(update, "update_info_url", "string"), + strictCompatibility: false, + targetApplications: [appEntry], + }; + + if ("strict_max_version" in app) { + if ("advisory_max_version" in app) { + logger.warn("Ignoring 'advisory_max_version' update manifest property for " + + aId + " property since 'strict_max_version' also present"); + } + + appEntry.maxVersion = getProperty(app, "strict_max_version", "string"); + result.strictCompatibility = appEntry.maxVersion != "*"; + } else if ("advisory_max_version" in app) { + appEntry.maxVersion = getProperty(app, "advisory_max_version", "string"); + } + + // The JSON update protocol requires an SHA-2 hash. RDF still + // supports SHA-1, for compatibility reasons. + sanitizeUpdateURL(result, aRequest, /^sha(256|512):/, "sha256 or sha512"); + + results.push(result); + } + return results; +} + /** * Starts downloading an update manifest and then passes it to an appropriate * parser to convert to an array of update objects @@ -415,7 +582,7 @@ function UpdateParser(aId, aUpdateKey, aUrl, aObserver) { this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // Prevent the request from writing to cache. this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - this.request.overrideMimeType("text/xml"); + this.request.overrideMimeType("text/plain"); this.request.setRequestHeader("Moz-XPI-Update", "1", true); this.request.timeout = TIMEOUT; var self = this; @@ -474,41 +641,50 @@ UpdateParser.prototype = { return; } - let xml = request.responseXML; - if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) { - logger.warn("Update manifest was not valid XML"); + // Detect the manifest type by first attempting to parse it as + // JSON, and falling back to parsing it as XML if that fails. + let parser; + try { + try { + let json = JSON.parse(request.responseText); + + parser = () => parseJSONManifest(this.id, this.updateKey, request, json); + } catch (e if e instanceof SyntaxError) { + let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); + let xml = domParser.parseFromString(request.responseText, "text/xml"); + + if (xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) + throw new Error("Update manifest was not valid XML or JSON"); + + parser = () => parseRDFManifest(this.id, this.updateKey, request, xml); + } + } catch (e) { + logger.warn("onUpdateCheckComplete failed to determine manifest type"); + this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT); + return; + } + + let results; + try { + results = parser(); + } + catch (e) { + logger.warn("onUpdateCheckComplete failed to parse update manifest", e); this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); return; } - // We currently only know about RDF update manifests - if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) { - let results = null; - + if ("onUpdateCheckComplete" in this.observer) { try { - results = parseRDFManifest(this.id, this.updateKey, request); + this.observer.onUpdateCheckComplete(results); } catch (e) { - logger.warn("onUpdateCheckComplete failed to parse RDF manifest", e); - this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR); - return; + logger.warn("onUpdateCheckComplete notification failed", e); } - if ("onUpdateCheckComplete" in this.observer) { - try { - this.observer.onUpdateCheckComplete(results); - } - catch (e) { - logger.warn("onUpdateCheckComplete notification failed", e); - } - } - else { - logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker")); - } - return; } - - logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI); - this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT); + else { + logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker")); + } }, /** diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index 0d5ed412f3e..6a8a8a9d064 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -906,7 +906,7 @@ function loadManifestFromWebManifest(aStream) { addon.targetApplications = [{ id: TOOLKIT_ID, - minVersion: "42a1", + minVersion: AddonManagerPrivate.webExtensionsMinPlatformVersion, maxVersion: "*", }]; diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json new file mode 100644 index 00000000000..811e50158ea --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json @@ -0,0 +1,327 @@ +{ + "addons": { + "updatecheck1@tests.mozilla.org": { + "updates": [ + { + "version": "1.0", + "update_link": "https://localhost:4444/addons/test1.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + }, + { + "_comment_": "This update is incompatible and so should not be considered a valid update", + "version": "2.0", + "update_link": "https://localhost:4444/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "strict_max_version": "2" + } + } + }, + { + "version": "3.0", + "update_link": "https://localhost:4444/addons/test3.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + }, + { + "version": "2.0", + "update_link": "https://localhost:4444/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "2" + } + } + }, + { + "_comment_": "This update is incompatible and so should not be considered a valid update", + "version": "4.0", + "update_link": "https://localhost:4444/addons/test4.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "strict_max_version": "2" + } + } + } + ] + }, + + "test_bug378216_5@tests.mozilla.org": { + "_comment_": "An update which expects a signature. It will fail since signatures are ", + "_comment_": "supported in this format.", + "_comment_": "The updateLink will also be ignored since it is not secure and there ", + "_comment_": "is no updateHash.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:4444/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_5@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:4444/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_7@tests.mozilla.org": { + "_comment_": "An update which expects a signature. It will fail since signatures are ", + "_comment_": "supported in this format.", + "_comment_": "The updateLink will also be ignored since it is not secure ", + "_comment_": "and there is no updateHash.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:4444/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "2" + } + } + } + ] + }, + + "test_bug378216_8@tests.mozilla.org": { + "_comment_": "The updateLink will be ignored since it is not secure and ", + "_comment_": "there is no updateHash.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:4444/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_9@tests.mozilla.org": { + "_comment_": "The updateLink will used since there is an updateHash to verify it.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:4444/broken.xpi", + "update_hash": "sha256:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_10@tests.mozilla.org": { + "_comment_": "The updateLink will used since it is a secure URL.", + + "updates": [ + { + "version": "2.0", + "update_link": "https://localhost:4444/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_11@tests.mozilla.org": { + "_comment_": "The updateLink will used since it is a secure URL.", + + "updates": [ + { + "version": "2.0", + "update_link": "https://localhost:4444/broken.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_12@tests.mozilla.org": { + "_comment_": "The updateLink will not be used since the updateHash ", + "_comment_": "verifying it is not strong enough.", + + "updates": [ + { + "version": "2.0", + "update_link": "http://localhost:4444/broken.xpi", + "update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "test_bug378216_13@tests.mozilla.org": { + "_comment_": "An update with a weak hash. The updateLink will used since it is ", + "_comment_": "a secure URL.", + + "updates": [ + { + "version": "2.0", + "update_link": "https://localhost:4444/broken.xpi", + "update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6", + "applications": { + "gecko": { + "strict_min_version": "1", + "strict_max_version": "1" + } + } + } + ] + }, + + "_comment_": "There should be no information present for test_bug378216_14", + + "test_bug378216_15@tests.mozilla.org": { + "_comment_": "Invalid update JSON", + + "updates": "foo" + }, + + "ignore-compat@tests.mozilla.org": { + "_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled", + + "updates": [ + { + "version": "1.0", + "update_link": "https://localhost:4444/addons/test1.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.1", + "advisory_max_version": "0.2" + } + } + }, + { + "version": "2.0", + "update_link": "https://localhost:4444/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.5", + "advisory_max_version": "0.6" + } + } + }, + { + "_comment_": "Update for future app versions - should never be compatible", + "version": "3.0", + "update_link": "https://localhost:4444/addons/test3.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "advisory_max_version": "3" + } + } + } + ] + }, + + "compat-override@tests.mozilla.org": { + "_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled", + + "updates": [ + { + "_comment_": "Has compatibility override, but it doesn't match this app version", + "version": "1.0", + "update_link": "https://localhost:4444/addons/test1.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.1", + "advisory_max_version": "0.2" + } + } + }, + { + "_comment_": "Has compatibility override, so is incompaible", + "version": "2.0", + "update_link": "https://localhost:4444/addons/test2.xpi", + "applications": { + "gecko": { + "strict_min_version": "0.5", + "advisory_max_version": "0.6" + } + } + }, + { + "_comment_": "Update for future app versions - should never be compatible", + "version": "3.0", + "update_link": "https://localhost:4444/addons/test3.xpi", + "applications": { + "gecko": { + "strict_min_version": "2", + "advisory_max_version": "3" + } + } + } + ] + }, + + "compat-strict-optin@tests.mozilla.org": { + "_comment_": "Opt-in to strict compatibility checking", + + "updates": [ + { + "version": "1.0", + "update_link": "https://localhost:4444/addons/test1.xpi", + "_comment_": "strictCompatibility: true", + "applications": { + "gecko": { + "strict_min_version": "0.1", + "strict_max_version": "0.2" + } + } + } + ] + } + } +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf index 93c82886a65..c5d97ada0d5 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf @@ -236,7 +236,7 @@ A90eF5zy - diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js index a5e6ba3d1a3..d8f3b795909 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -1542,7 +1542,7 @@ if ("nsIWindowsRegKey" in AM_Ci) { * This is a mock nsIWindowsRegistry implementation. It only implements the * methods that the extension manager requires. */ - function MockWindowsRegKey() { + var MockWindowsRegKey = function MockWindowsRegKey() { } MockWindowsRegKey.prototype = { @@ -1723,6 +1723,30 @@ do_register_cleanup(function addon_cleanup() { } catch (e) {} }); +/** + * 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. @@ -1912,3 +1936,43 @@ function promiseFindAddonUpdates(addon, reason = AddonManager.UPDATE_WHEN_PERIOD }, reason); }); } + +/** + * Monitors console output for the duration of a task, and returns a promise + * which resolves to a tuple containing a list of all console messages + * generated during the task's execution, and the result of the task itself. + * + * @param {function} aTask + * The task to run while monitoring console output. May be + * either a generator function, per Task.jsm, or an ordinary + * function which returns promose. + * @return {Promise<[Array, *]>} + */ +var promiseConsoleOutput = Task.async(function*(aTask) { + const DONE = "=== xpcshell test console listener done ==="; + + let listener, messages = []; + let awaitListener = new Promise(resolve => { + listener = msg => { + if (msg == DONE) { + resolve(); + } else { + msg instanceof Components.interfaces.nsIScriptError; + messages.push(msg); + } + } + }); + + Services.console.registerListener(listener); + try { + let result = yield aTask(); + + Services.console.logStringMessage(DONE); + yield awaitListener; + + return { messages, result }; + } + finally { + Services.console.unregisterListener(listener); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js b/toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js new file mode 100644 index 00000000000..2e8cc6a972c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js @@ -0,0 +1,373 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +// This verifies that AddonUpdateChecker works correctly for JSON +// update manifests, particularly for behavior which does not +// cleanly overlap with RDF manifests. + +const TOOLKIT_ID = "toolkit@mozilla.org"; +const TOOLKIT_MINVERSION = "42.0a1"; + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0a2", "42.0a2"); + +Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); +Components.utils.import("resource://testing-common/httpd.js"); + +let testserver = createHttpServer(); +gPort = testserver.identity.primaryPort; + +let gUpdateManifests = {}; + +function mapManifest(aPath, aManifestData) { + gUpdateManifests[aPath] = aManifestData; + testserver.registerPathHandler(aPath, serveManifest); +} + +function serveManifest(request, response) { + let manifest = gUpdateManifests[request.path]; + + response.setHeader("Content-Type", manifest.contentType, false); + response.write(manifest.data); +} + +const extensionsDir = gProfD.clone(); +extensionsDir.append("extensions"); + + +function checkUpdates(aData) { + // Registers JSON update manifest for it with the testing server, + // checks for updates, and yields the list of updates on + // success. + + let extension = aData.manifestExtension || "json"; + + let path = `/updates/${aData.id}.${extension}`; + let updateUrl = `http://localhost:${gPort}${path}` + + let addonData = {}; + if ("updates" in aData) + addonData.updates = aData.updates; + + let manifestJSON = { + "addons": { + [aData.id]: addonData + } + }; + + mapManifest(path.replace(/\?.*/, ""), + { data: JSON.stringify(manifestJSON), + contentType: aData.contentType || "application/json" }); + + + return new Promise((resolve, reject) => { + AddonUpdateChecker.checkForUpdates(aData.id, aData.updateKey, updateUrl, { + onUpdateCheckComplete: resolve, + + onUpdateCheckError: function(status) { + reject(new Error("Update check failed with status " + status)); + } + }); + }); +} + + +add_task(function* test_default_values() { + // Checks that the appropriate defaults are used for omitted values. + + startupManager(); + + let updates = yield checkUpdates({ + id: "updatecheck-defaults@tests.mozilla.org", + version: "0.1", + updates: [{ + version: "0.2" + }] + }); + + equal(updates.length, 1); + let update = updates[0]; + + equal(update.targetApplications.length, 1); + let targetApp = update.targetApplications[0]; + + equal(targetApp.id, TOOLKIT_ID); + equal(targetApp.minVersion, TOOLKIT_MINVERSION); + equal(targetApp.maxVersion, "*"); + + equal(update.version, "0.2"); + equal(update.multiprocessCompatible, true, "multiprocess_compatible flag"); + equal(update.strictCompatibility, false, "inferred strictConpatibility flag"); + equal(update.updateURL, null, "updateURL"); + equal(update.updateHash, null, "updateHash"); + equal(update.updateInfoURL, null, "updateInfoURL"); + + // If there's no applications property, we default to using one + // containing "gecko". If there is an applications property, but + // it doesn't contain "gecko", the update is skipped. + updates = yield checkUpdates({ + id: "updatecheck-defaults@tests.mozilla.org", + version: "0.1", + updates: [{ + version: "0.2", + applications: { "foo": {} } + }] + }); + + equal(updates.length, 0); + + // Updates property is also optional. No updates, but also no error. + updates = yield checkUpdates({ + id: "updatecheck-defaults@tests.mozilla.org", + version: "0.1", + }); + + equal(updates.length, 0); +}); + + +add_task(function* test_explicit_values() { + // Checks that the appropriate explicit values are used when + // provided. + + let updates = yield checkUpdates({ + id: "updatecheck-explicit@tests.mozilla.org", + version: "0.1", + updates: [{ + version: "0.2", + update_link: "https://example.com/foo.xpi", + update_hash: "sha256:0", + update_info_url: "https://example.com/update_info.html", + multiprocess_compatible: false, + applications: { + gecko: { + strict_min_version: "42.0a2.xpcshell", + strict_max_version: "43.xpcshell" + } + } + }] + }); + + equal(updates.length, 1); + let update = updates[0]; + + equal(update.targetApplications.length, 1); + let targetApp = update.targetApplications[0]; + + equal(targetApp.id, TOOLKIT_ID); + equal(targetApp.minVersion, "42.0a2.xpcshell"); + equal(targetApp.maxVersion, "43.xpcshell"); + + equal(update.version, "0.2"); + equal(update.multiprocessCompatible, false, "multiprocess_compatible flag"); + equal(update.strictCompatibility, true, "inferred strictCompatibility flag"); + equal(update.updateURL, "https://example.com/foo.xpi", "updateURL"); + equal(update.updateHash, "sha256:0", "updateHash"); + equal(update.updateInfoURL, "https://example.com/update_info.html", "updateInfoURL"); +}); + + +add_task(function* test_secure_hashes() { + // Checks that only secure hash functions are accepted for + // non-secure update URLs. + + let hashFunctions = ["sha512", + "sha256", + "sha1", + "md5", + "md4", + "xxx"]; + + let updateItems = hashFunctions.map((hash, idx) => ({ + version: `0.${idx}`, + update_link: `http://localhost:${gPort}/updates/${idx}-${hash}.xpi`, + update_hash: `${hash}:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a`, + })); + + let { messages, result: updates } = yield promiseConsoleOutput(() => { + return checkUpdates({ + id: "updatecheck-hashes@tests.mozilla.org", + version: "0.1", + updates: updateItems + }); + }); + + equal(updates.length, hashFunctions.length); + + updates = updates.filter(update => update.updateHash || update.updateURL); + equal(updates.length, 2, "expected number of update hashes were accepted"); + + ok(updates[0].updateHash.startsWith("sha512:"), "sha512 hash is present"); + ok(updates[0].updateURL); + + ok(updates[1].updateHash.startsWith("sha256:"), "sha256 hash is present"); + ok(updates[1].updateURL); + + messages = messages.filter(msg => /Update link.*not secure.*strong enough hash \(needs to be sha256 or sha512\)/.test(msg.message)); + equal(messages.length, hashFunctions.length - 2, "insecure hashes generated the expected warning"); +}); + + +add_task(function* test_strict_compat() { + // Checks that strict compatibility is enabled for strict max + // versions other than "*", but not for advisory max versions. + // Also, ensure that strict max versions take precedence over + // advisory versions. + + let { messages, result: updates } = yield promiseConsoleOutput(() => { + return checkUpdates({ + id: "updatecheck-strict@tests.mozilla.org", + version: "0.1", + updates: [ + { version: "0.2", + applications: { gecko: { strict_max_version: "*" } } }, + { version: "0.3", + applications: { gecko: { strict_max_version: "43" } } }, + { version: "0.4", + applications: { gecko: { advisory_max_version: "43" } } }, + { version: "0.5", + applications: { gecko: { advisory_max_version: "43", + strict_max_version: "44" } } }, + ] + }); + }); + + equal(updates.length, 4, "all update items accepted"); + + equal(updates[0].targetApplications[0].maxVersion, "*"); + equal(updates[0].strictCompatibility, false); + + equal(updates[1].targetApplications[0].maxVersion, "43"); + equal(updates[1].strictCompatibility, true); + + equal(updates[2].targetApplications[0].maxVersion, "43"); + equal(updates[2].strictCompatibility, false); + + equal(updates[3].targetApplications[0].maxVersion, "44"); + equal(updates[3].strictCompatibility, true); + + messages = messages.filter(msg => /Ignoring 'advisory_max_version'.*'strict_max_version' also present/.test(msg.message)); + equal(messages.length, 1, "mix of advisory_max_version and strict_max_version generated the expected warning"); +}); + + +add_task(function* test_update_url_security() { + // Checks that update links to privileged URLs are not accepted. + + let { messages, result: updates } = yield promiseConsoleOutput(() => { + return checkUpdates({ + id: "updatecheck-security@tests.mozilla.org", + version: "0.1", + updates: [ + { version: "0.2", + update_link: "chrome://browser/content/browser.xul", + update_hash: "sha256:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a" }, + { version: "0.3", + update_link: "http://example.com/update.xpi", + update_hash: "sha256:18ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a" }, + ] + }); + }); + + equal(updates.length, 2, "both updates were processed"); + equal(updates[0].updateURL, null, "privileged update URL was removed"); + equal(updates[1].updateURL, "http://example.com/update.xpi", "safe update URL was accepted"); + + messages = messages.filter(msg => /http:\/\/localhost.*\/updates\/.*may not load or link to chrome:/.test(msg.message)); + equal(messages.length, 1, "privileged upate URL generated the expected console message"); +}); + + +add_task(function* test_no_update_key() { + // Checks that updates fail when an update key has been specified. + + let { messages } = yield promiseConsoleOutput(function* () { + yield Assert.rejects( + checkUpdates({ + id: "updatecheck-updatekey@tests.mozilla.org", + version: "0.1", + updateKey: "ayzzx=", + updates: [ + { version: "0.2" }, + { version: "0.3" }, + ] + }), + null, "updated expected to fail"); + }); + + messages = messages.filter(msg => /Update keys are not supported for JSON update manifests/.test(msg.message)); + equal(messages.length, 1, "got expected update-key-unsupported error"); +}); + + +add_task(function* test_type_detection() { + // Checks that JSON update manifests are detected correctly + // regardless of extension or MIME type. + + let tests = [ + { contentType: "application/json", + extension: "json", + valid: true }, + { contentType: "application/json", + extension: "php", + valid: true }, + { contentType: "text/plain", + extension: "json", + valid: true }, + { contentType: "application/octet-stream", + extension: "json", + valid: true }, + { contentType: "text/plain", + extension: "json?foo=bar", + valid: true }, + { contentType: "text/plain", + extension: "php", + valid: true }, + { contentType: "text/plain", + extension: "rdf", + valid: true }, + { contentType: "application/json", + extension: "rdf", + valid: true }, + { contentType: "text/xml", + extension: "json", + valid: true }, + { contentType: "application/rdf+xml", + extension: "json", + valid: true }, + ]; + + for (let [i, test] of tests.entries()) { + let { messages } = yield promiseConsoleOutput(function *() { + let id = `updatecheck-typedetection-${i}@tests.mozilla.org`; + let updates; + try { + updates = yield checkUpdates({ + id: id, + version: "0.1", + contentType: test.contentType, + manifestExtension: test.extension, + updates: [{ version: "0.2" }] + }); + } catch (e) { + ok(!test.valid, "update manifest correctly detected as RDF"); + return; + } + + ok(test.valid, "update manifest correctly detected as JSON"); + equal(updates.length, 1, "correct number of updates"); + equal(updates[0].id, id, "update is for correct extension"); + }); + + if (test.valid) { + // Make sure we don't get any XML parsing errors from the + // XMLHttpRequest machinery. + ok(!messages.some(msg => /not well-formed/.test(msg.message)), + "expect XMLHttpRequest not to attempt XML parsing"); + } + + messages = messages.filter(msg => /Update manifest was not valid XML/.test(msg.message)); + equal(messages.length, !test.valid, "expected number of XML parsing errors"); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js index 9d251933aab..9a2f6a01dbf 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js @@ -7,52 +7,47 @@ Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); Components.utils.import("resource://testing-common/httpd.js"); -var testserver; -function run_test() { - createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); +var testserver = createHttpServer(4444); +testserver.registerDirectory("/data/", do_get_file("data")); - // Create and configure the HTTP server. - testserver = new HttpServer(); - testserver.registerDirectory("/data/", do_get_file("data")); - testserver.start(4444); +function checkUpdates(aId, aUpdateKey, aUpdateFile) { + return new Promise((resolve, reject) => { + AddonUpdateChecker.checkForUpdates(aId, aUpdateKey, `http://localhost:4444/data/${aUpdateFile}`, { + onUpdateCheckComplete: resolve, - do_test_pending(); - run_test_1(); -} - -function end_test() { - testserver.stop(do_test_finished); -} - -// Test that a basic update check returns the expected available updates -function run_test_1() { - AddonUpdateChecker.checkForUpdates("updatecheck1@tests.mozilla.org", null, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - check_test_1(updates); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } + onUpdateCheckError: function(status) { + let error = new Error("Update check failed with status " + status); + error.status = status; + reject(error); + } + }); }); } -function check_test_1(updates) { - do_check_eq(updates.length, 5); - let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates); - do_check_neq(update, null); - do_check_eq(update.version, 3); - update = AddonUpdateChecker.getCompatibilityUpdate(updates, "2"); - do_check_neq(update, null); - do_check_eq(update.version, 2); - do_check_eq(update.targetApplications[0].minVersion, 1); - do_check_eq(update.targetApplications[0].maxVersion, 2); +function run_test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); - run_test_2(); + run_next_test(); } +// Test that a basic update check returns the expected available updates +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + let updates = yield checkUpdates("updatecheck1@tests.mozilla.org", null, file); + + equal(updates.length, 5); + let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates); + notEqual(update, null); + equal(update.version, "3.0"); + update = AddonUpdateChecker.getCompatibilityUpdate(updates, "2"); + notEqual(update, null); + equal(update.version, "2.0"); + equal(update.targetApplications[0].minVersion, "1"); + equal(update.targetApplications[0].maxVersion, "2"); + } +}); + /* * Tests that the security checks are applied correctly * @@ -73,240 +68,169 @@ var updateKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK426erD/H3XtsjvaB5+PJqbh "NyeP6i4LuUYjTURnn7Yw/IgzyIJ2oKsYa32RuxAyteqAWqPT/J63wBixIeCxmysf" + "awB/zH4KaPiY3vnrzQIDAQAB"; -function run_test_2() { - AddonUpdateChecker.checkForUpdates("test_bug378216_5@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_throw("Expected the update check to fail"); - }, +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + try { + yield checkUpdates("test_bug378216_5@tests.mozilla.org", + updateKey, file); + throw "Expected the update check to fail"; + } catch (e) {} + } +}); - onUpdateCheckError: function(status) { - run_test_3(); +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + try { + yield checkUpdates("test_bug378216_7@tests.mozilla.org", + updateKey, file); + + throw "Expected the update check to fail"; + } catch (e) {} + } +}); + +add_task(function* () { + // Make sure that the JSON manifest is rejected when an update key is + // required, but perform the remaining tests which aren't expected to fail + // because of the update key, without requiring one for the JSON variant. + + try { + let updates = yield checkUpdates("test_bug378216_8@tests.mozilla.org", + updateKey, "test_updatecheck.json"); + + throw "Expected the update check to fail"; + } catch(e) {} + + for (let [file, key] of [["test_updatecheck.rdf", updateKey], + ["test_updatecheck.json", null]]) { + let updates = yield checkUpdates("test_bug378216_8@tests.mozilla.org", + key, file); + equal(updates.length, 1); + ok(!("updateURL" in updates[0])); + } +}); + +add_task(function* () { + for (let [file, key] of [["test_updatecheck.rdf", updateKey], + ["test_updatecheck.json", null]]) { + let updates = yield checkUpdates("test_bug378216_9@tests.mozilla.org", + key, file); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); + } +}); + +add_task(function* () { + for (let [file, key] of [["test_updatecheck.rdf", updateKey], + ["test_updatecheck.json", null]]) { + let updates = yield checkUpdates("test_bug378216_10@tests.mozilla.org", + key, file); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); + } +}); + +add_task(function* () { + for (let [file, key] of [["test_updatecheck.rdf", updateKey], + ["test_updatecheck.json", null]]) { + let updates = yield checkUpdates("test_bug378216_11@tests.mozilla.org", + key, file); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); + } +}); + +add_task(function* () { + for (let [file, key] of [["test_updatecheck.rdf", updateKey], + ["test_updatecheck.json", null]]) { + let updates = yield checkUpdates("test_bug378216_12@tests.mozilla.org", + key, file); + equal(updates.length, 1); + do_check_false("updateURL" in updates[0]); + } +}); + +add_task(function* () { + for (let [file, key] of [["test_updatecheck.rdf", updateKey], + ["test_updatecheck.json", null]]) { + let updates = yield checkUpdates("test_bug378216_13@tests.mozilla.org", + key, file); + equal(updates.length, 1); + equal(updates[0].version, "2.0"); + ok("updateURL" in updates[0]); + } +}); + +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + let updates = yield checkUpdates("test_bug378216_14@tests.mozilla.org", + null, file); + equal(updates.length, 0); + } +}); + +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + try { + yield checkUpdates("test_bug378216_15@tests.mozilla.org", + null, file); + + throw "Update check should have failed"; + } catch (e) { + equal(e.status, AddonUpdateChecker.ERROR_PARSE_ERROR); } - }); -} + } +}); -function run_test_3() { - AddonUpdateChecker.checkForUpdates("test_bug378216_7@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_throw("Expected the update check to fail"); - }, +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + let updates = yield checkUpdates("ignore-compat@tests.mozilla.org", + null, file); + equal(updates.length, 3); + let update = AddonUpdateChecker.getNewestCompatibleUpdate( + updates, null, null, true); + notEqual(update, null); + equal(update.version, 2); + } +}); - onUpdateCheckError: function(status) { - run_test_4(); - } - }); -} +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + let updates = yield checkUpdates("compat-override@tests.mozilla.org", + null, file); + equal(updates.length, 3); + let overrides = [{ + type: "incompatible", + minVersion: 1, + maxVersion: 2, + appID: "xpcshell@tests.mozilla.org", + appMinVersion: 0.1, + appMaxVersion: 0.2 + }, { + type: "incompatible", + minVersion: 2, + maxVersion: 2, + appID: "xpcshell@tests.mozilla.org", + appMinVersion: 1, + appMaxVersion: 2 + }]; + let update = AddonUpdateChecker.getNewestCompatibleUpdate( + updates, null, null, true, false, overrides); + notEqual(update, null); + equal(update.version, 1); + } +}); -function run_test_4() { - AddonUpdateChecker.checkForUpdates("test_bug378216_8@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - do_check_false("updateURL" in updates[0]); - run_test_5(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_5() { - AddonUpdateChecker.checkForUpdates("test_bug378216_9@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - do_check_eq(updates[0].version, "2.0"); - do_check_true("updateURL" in updates[0]); - run_test_6(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_6() { - AddonUpdateChecker.checkForUpdates("test_bug378216_10@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - do_check_eq(updates[0].version, "2.0"); - do_check_true("updateURL" in updates[0]); - run_test_7(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_7() { - AddonUpdateChecker.checkForUpdates("test_bug378216_11@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - do_check_eq(updates[0].version, "2.0"); - do_check_true("updateURL" in updates[0]); - run_test_8(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_8() { - AddonUpdateChecker.checkForUpdates("test_bug378216_12@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - do_check_false("updateURL" in updates[0]); - run_test_9(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_9() { - AddonUpdateChecker.checkForUpdates("test_bug378216_13@tests.mozilla.org", - updateKey, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - do_check_eq(updates[0].version, "2.0"); - do_check_true("updateURL" in updates[0]); - run_test_10(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_10() { - AddonUpdateChecker.checkForUpdates("test_bug378216_14@tests.mozilla.org", - null, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 0); - run_test_11(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_11() { - AddonUpdateChecker.checkForUpdates("test_bug378216_15@tests.mozilla.org", - null, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_throw("Update check should have failed"); - }, - - onUpdateCheckError: function(status) { - do_check_eq(status, AddonUpdateChecker.ERROR_PARSE_ERROR); - run_test_12(); - } - }); -} - -function run_test_12() { - AddonUpdateChecker.checkForUpdates("ignore-compat@tests.mozilla.org", - null, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 3); - let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates, - null, - null, - true); - do_check_neq(update, null); - do_check_eq(update.version, 2); - run_test_13(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_13() { - AddonUpdateChecker.checkForUpdates("compat-override@tests.mozilla.org", - null, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 3); - let overrides = [{ - type: "incompatible", - minVersion: 1, - maxVersion: 2, - appID: "xpcshell@tests.mozilla.org", - appMinVersion: 0.1, - appMaxVersion: 0.2 - }, { - type: "incompatible", - minVersion: 2, - maxVersion: 2, - appID: "xpcshell@tests.mozilla.org", - appMinVersion: 1, - appMaxVersion: 2 - }]; - let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates, - null, - null, - true, - false, - overrides); - do_check_neq(update, null); - do_check_eq(update.version, 1); - run_test_14(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} - -function run_test_14() { - AddonUpdateChecker.checkForUpdates("compat-strict-optin@tests.mozilla.org", - null, - "http://localhost:4444/data/test_updatecheck.rdf", { - onUpdateCheckComplete: function(updates) { - do_check_eq(updates.length, 1); - let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates, - null, - null, - true, - false); - do_check_eq(update, null); - end_test(); - }, - - onUpdateCheckError: function(status) { - do_throw("Update check failed with status " + status); - } - }); -} +add_task(function* () { + for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) { + let updates = yield checkUpdates("compat-strict-optin@tests.mozilla.org", + null, file); + equal(updates.length, 1); + let update = AddonUpdateChecker.getNewestCompatibleUpdate( + updates, null, null, true, false); + equal(update, null); + } +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini index f8b9972cf28..fdd2dd9ac35 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini @@ -279,6 +279,7 @@ skip-if = os == "android" # Bug 676992: test consistently hangs on Android skip-if = os == "android" run-sequentially = Uses hardcoded ports in xpi files. +[test_json_updatecheck.js] [test_updateid.js] # Bug 676992: test consistently hangs on Android skip-if = os == "android"