/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components; const URL_HOST = "http://localhost"; var GMPScope = Cu.import("resource://gre/modules/GMPInstallManager.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://testing-common/httpd.js"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Preferences.jsm") Cu.import("resource://gre/modules/UpdateUtils.jsm"); var { computeHash } = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); var ProductAddonCheckerScope = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); do_get_profile(); function run_test() { Cu.import("resource://gre/modules/Preferences.jsm") Preferences.set("media.gmp.log.dump", true); Preferences.set("media.gmp.log.level", 0); run_next_test(); } /** * Tests that the helper used for preferences works correctly */ add_task(function* test_prefs() { let addon1 = "addon1", addon2 = "addon2"; GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL, "http://not-really-used"); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2"); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "1", addon1); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "2", addon1); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "3", addon2); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "4", addon2); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, addon2); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS, true); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL), "http://not-really-used"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE), "http://not-really-used-2"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon1), "1"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon1), "2"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon2), "3"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon2), "4"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, undefined, addon2), false); do_check_true(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS)); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, addon2); }); /** * Tests that an uninit without a check works fine */ add_task(function* test_checkForAddons_uninitWithoutCheck() { let installManager = new GMPInstallManager(); installManager.uninit(); }); /** * Tests that an uninit without an install works fine */ add_test(function test_checkForAddons_uninitWithoutInstall() { overrideXHR(200, ""); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that no response returned rejects */ add_test(function test_checkForAddons_noResponse() { overrideXHR(200, ""); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that no addons element returned resolves with no addons */ add_task(function* test_checkForAddons_noAddonsElement() { overrideXHR(200, ""); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 0); installManager.uninit(); }); /** * Tests that empty addons element returned resolves with no addons */ add_task(function* test_checkForAddons_emptyAddonsElement() { overrideXHR(200, ""); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 0); installManager.uninit(); }); /** * Tests that a response with the wrong root element rejects */ add_test(function test_checkForAddons_wrongResponseXML() { overrideXHR(200, "3.141592653589793...."); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that a 404 error works as expected */ add_test(function test_checkForAddons_404Error() { overrideXHR(404, ""); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that a xhr abort() works as expected */ add_test(function test_checkForAddons_abort() { let overriddenXhr = overrideXHR(200, "", { dropRequest: true} ); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); overriddenXhr.abort(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that a defensive timeout works as expected */ add_test(function test_checkForAddons_timeout() { overrideXHR(200, "", { dropRequest: true, timeout: true }); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that we throw correctly in case of ssl certification error. */ add_test(function test_checkForAddons_bad_ssl() { // // Add random stuff that cause CertUtil to require https. // let PREF_KEY_URL_OVERRIDE_BACKUP = Preferences.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, undefined); Preferences.reset(GMPScope.GMPPrefs.KEY_URL_OVERRIDE); let CERTS_BRANCH_DOT_ONE = GMPScope.GMPPrefs.KEY_CERTS_BRANCH + ".1"; let PREF_CERTS_BRANCH_DOT_ONE_BACKUP = Preferences.get(CERTS_BRANCH_DOT_ONE, undefined); Services.prefs.setCharPref(CERTS_BRANCH_DOT_ONE, "funky value"); overrideXHR(200, ""); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); if (PREF_KEY_URL_OVERRIDE_BACKUP) { Preferences.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, PREF_KEY_URL_OVERRIDE_BACKUP); } if (PREF_CERTS_BRANCH_DOT_ONE_BACKUP) { Preferences.set(CERTS_BRANCH_DOT_ONE, PREF_CERTS_BRANCH_DOT_ONE_BACKUP); } run_next_test(); }); }); /** * Tests that gettinga a funky non XML response works as expected */ add_test(function test_checkForAddons_notXML() { overrideXHR(200, "3.141592653589793...."); let installManager = new GMPInstallManager(); let promise = installManager.checkForAddons(); promise.then(res => { do_check_true(res.usedFallback); installManager.uninit(); run_next_test(); }); }); /** * Tests that getting a response with a single addon works as expected */ add_task(function* test_checkForAddons_singleAddon() { let responseXML = "" + "" + " " + " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 1); let gmpAddon = res.gmpAddons[0]; do_check_eq(gmpAddon.id, "gmp-gmpopenh264"); do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); do_check_eq(gmpAddon.hashFunction, "sha256"); do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); do_check_eq(gmpAddon.version, "1.1"); do_check_eq(gmpAddon.size, undefined); do_check_true(gmpAddon.isValid); do_check_false(gmpAddon.isInstalled); installManager.uninit(); }); /** * Tests that getting a response with a single addon with the optional size * attribute parses as expected. */ add_task(function* test_checkForAddons_singleAddonWithSize() { let responseXML = "" + "" + " " + " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 1); let gmpAddon = res.gmpAddons[0]; do_check_eq(gmpAddon.id, "openh264-plugin-no-at-symbol"); do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); do_check_eq(gmpAddon.hashFunction, "sha256"); do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); do_check_eq(gmpAddon.size, 42); do_check_eq(gmpAddon.version, "1.1"); do_check_true(gmpAddon.isValid); do_check_false(gmpAddon.isInstalled); installManager.uninit(); }); /** * Tests that checking for multiple addons work correctly. * Also tests that invalid addons work correctly. */ add_task(function* test_checkForAddons_multipleAddonNoUpdatesSomeInvalid() { let responseXML = "" + "" + " " + // valid openh264 " " + // valid not openh264 " " + // noid " " + // no URL " " + // no hash function " " + // no hash function " " + // not version " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 7); let gmpAddon = res.gmpAddons[0]; do_check_eq(gmpAddon.id, "gmp-gmpopenh264"); do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); do_check_eq(gmpAddon.hashFunction, "sha256"); do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); do_check_eq(gmpAddon.version, "1.1"); do_check_true(gmpAddon.isValid); do_check_false(gmpAddon.isInstalled); gmpAddon = res.gmpAddons[1]; do_check_eq(gmpAddon.id, "NOT-gmp-gmpopenh264"); do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip"); do_check_eq(gmpAddon.hashFunction, "sha512"); do_check_eq(gmpAddon.hashValue, "141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); do_check_eq(gmpAddon.version, "9.1"); do_check_true(gmpAddon.isValid); do_check_false(gmpAddon.isInstalled); for (let i = 2; i < res.gmpAddons.length; i++) { do_check_false(res.gmpAddons[i].isValid); do_check_false(res.gmpAddons[i].isInstalled); } installManager.uninit(); }); /** * Tests that checking for addons when there are also updates available * works as expected. */ add_task(function* test_checkForAddons_updatesWithAddons() { let responseXML = "" + " " + " " + " " + " " + " " + " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 1); let gmpAddon = res.gmpAddons[0]; do_check_eq(gmpAddon.id, "gmp-gmpopenh264"); do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); do_check_eq(gmpAddon.hashFunction, "sha256"); do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); do_check_eq(gmpAddon.version, "1.1"); do_check_true(gmpAddon.isValid); do_check_false(gmpAddon.isInstalled); installManager.uninit(); }); /** * Tests that installing found addons works as expected */ function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) { do_print("Running installAddon for id: " + id + ", includeSize: " + includeSize + " and wantInstallReject: " + wantInstallReject); let httpServer = new HttpServer(); let dir = FileUtils.getDir("TmpD", [], true); httpServer.registerDirectory("/", dir); httpServer.start(-1); let testserverPort = httpServer.identity.primaryPort; let zipFileName = "test_" + id + "_GMP.zip"; let zipURL = URL_HOST + ":" + testserverPort + "/" + zipFileName; do_print("zipURL: " + zipURL); let data = "e~=0.5772156649"; let zipFile = createNewZipFile(zipFileName, data); let hashFunc = "sha256"; let expectedDigest = yield computeHash(hashFunc, zipFile.path); let fileSize = zipFile.fileSize; if (wantInstallReject) { fileSize = 1; } let responseXML = "" + "" + " " + " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let res = yield installManager.checkForAddons(); do_check_eq(res.gmpAddons.length, 1); let gmpAddon = res.gmpAddons[0]; do_check_false(gmpAddon.isInstalled); try { let extractedPaths = yield installManager.installAddon(gmpAddon); if (wantInstallReject) { do_check_true(false); // installAddon() should have thrown. } do_check_eq(extractedPaths.length, 1); let extractedPath = extractedPaths[0]; do_print("Extracted path: " + extractedPath); let extractedFile = Cc["@mozilla.org/file/local;1"]. createInstance(Ci.nsIFile); extractedFile.initWithPath(extractedPath); do_check_true(extractedFile.exists()); let readData = readStringFromFile(extractedFile); do_check_eq(readData, data); // Make sure the prefs are set correctly do_check_true(!!GMPScope.GMPPrefs.get( GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id)); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", gmpAddon.id), "1.1"); do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_ABI, "", gmpAddon.id), UpdateUtils.ABI); // Make sure it reports as being installed do_check_true(gmpAddon.isInstalled); // Cleanup extractedFile.parent.remove(true); zipFile.remove(false); httpServer.stop(function() {}); installManager.uninit(); } catch (ex) { zipFile.remove(false); if (!wantInstallReject) { do_throw("install update should not reject " + ex.message); } } } add_task(test_checkForAddons_installAddon.bind(null, "1", true, false)); add_task(test_checkForAddons_installAddon.bind(null, "2", false, false)); add_task(test_checkForAddons_installAddon.bind(null, "3", true, true)); /** * Tests simpleCheckAndInstall when autoupdate is disabled for a GMP */ add_task(function* test_simpleCheckAndInstall_autoUpdateDisabled() { GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, GMPScope.OPEN_H264_ID); let responseXML = "" + "" + " " + // valid openh264 " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let result = yield installManager.simpleCheckAndInstall(); do_check_eq(result.status, "nothing-new-to-install"); Preferences.reset(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK); GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, GMPScope.OPEN_H264_ID); }); /** * Tests simpleCheckAndInstall nothing to install */ add_task(function* test_simpleCheckAndInstall_nothingToInstall() { let responseXML = "" + "" + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let result = yield installManager.simpleCheckAndInstall(); do_check_eq(result.status, "nothing-new-to-install"); }); /** * Tests simpleCheckAndInstall too frequent */ add_task(function* test_simpleCheckAndInstall_tooFrequent() { let responseXML = "" + "" + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let result = yield installManager.simpleCheckAndInstall(); do_check_eq(result.status, "too-frequent-no-check"); }); /** * Tests that installing addons when there is no server works as expected */ add_test(function test_installAddon_noServer() { let dir = FileUtils.getDir("TmpD", [], true); let zipFileName = "test_GMP.zip"; let zipURL = URL_HOST + ":0/" + zipFileName; let data = "e~=0.5772156649"; let zipFile = createNewZipFile(zipFileName, data); let responseXML = "" + "" + " " + " " + " " + "" overrideXHR(200, responseXML); let installManager = new GMPInstallManager(); let checkPromise = installManager.checkForAddons(); checkPromise.then(res => { do_check_eq(res.gmpAddons.length, 1); let gmpAddon = res.gmpAddons[0]; GMPInstallManager.overrideLeaveDownloadedZip = true; let installPromise = installManager.installAddon(gmpAddon); installPromise.then(extractedPaths => { do_throw("No server for install should reject"); }, err => { do_check_true(!!err); installManager.uninit(); run_next_test(); }); }, () => { do_throw("check should not reject for install no server"); }); }); /** * Returns the read stream into a string */ function readStringFromInputStream(inputStream) { let sis = Cc["@mozilla.org/scriptableinputstream;1"]. createInstance(Ci.nsIScriptableInputStream); sis.init(inputStream); let text = sis.read(sis.available()); sis.close(); return text; } /** * Reads a string of text from a file. * This function only works with ASCII text. */ function readStringFromFile(file) { if (!file.exists()) { do_print("readStringFromFile - file doesn't exist: " + file.path); return null; } let fis = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); return readStringFromInputStream(fis); } /** * Bare bones XMLHttpRequest implementation for testing onprogress, onerror, * and onload nsIDomEventListener handleEvent. */ function makeHandler(aVal) { if (typeof aVal == "function") return { handleEvent: aVal }; return aVal; } /** * Constructs a mock xhr which is used for testing different aspects * of responses. */ function xhr(inputStatus, inputResponse, options) { this.inputStatus = inputStatus; this.inputResponse = inputResponse; this.status = 0; this.responseXML = null; this._aborted = false; this._onabort = null; this._onprogress = null; this._onerror = null; this._onload = null; this._onloadend = null; this._ontimeout = null; this._url = null; this._method = null; this._timeout = 0; this._notified = false; this._options = options || {}; } xhr.prototype = { overrideMimeType: function(aMimetype) { }, setRequestHeader: function(aHeader, aValue) { }, status: null, channel: { set notificationCallbacks(aVal) { } }, open: function(aMethod, aUrl) { this.channel.originalURI = Services.io.newURI(aUrl, null, null); this._method = aMethod; this._url = aUrl; }, abort: function() { this._dropRequest = true; this._notify(["abort", "loadend"]); }, responseXML: null, responseText: null, send: function(aBody) { do_execute_soon(function() { try { if (this._options.dropRequest) { if (this._timeout > 0 && this._options.timeout) { this._notify(["timeout", "loadend"]); } return; } this.status = this.inputStatus; this.responseText = this.inputResponse; try { let parser = Cc["@mozilla.org/xmlextras/domparser;1"]. createInstance(Ci.nsIDOMParser); this.responseXML = parser.parseFromString(this.inputResponse, "application/xml"); } catch (e) { this.responseXML = null; } if (this.inputStatus === 200) { this._notify(["load", "loadend"]); } else { this._notify(["error", "loadend"]); } } catch (ex) { do_throw(ex); } }.bind(this)); }, set onabort(aValue) { this._onabort = makeHandler(aValue); }, get onabort() { return this._onabort; }, set onprogress(aValue) { this._onprogress = makeHandler(aValue); }, get onprogress() { return this._onprogress; }, set onerror(aValue) { this._onerror = makeHandler(aValue); }, get onerror() { return this._onerror; }, set onload(aValue) { this._onload = makeHandler(aValue); }, get onload() { return this._onload; }, set onloadend(aValue) { this._onloadend = makeHandler(aValue); }, get onloadend() { return this._onloadend; }, set ontimeout(aValue) { this._ontimeout = makeHandler(aValue); }, get ontimeout() { return this._ontimeout; }, set timeout(aValue) { this._timeout = aValue; }, _notify: function(events) { if (this._notified) { return; } this._notified = true; for (let item of events) { let k = "on" + item; if (this[k]) { do_print("Notifying " + item); let e = { target: this, type: item, }; this[k](e); } else { do_print("Notifying " + item + ", but there are no listeners"); } } }, addEventListener: function(aEvent, aValue, aCapturing) { eval("this._on" + aEvent + " = aValue"); }, flags: Ci.nsIClassInfo.SINGLETON, getScriptableHelper: () => null, getInterfaces: function(aCount) { let interfaces = [Ci.nsISupports]; aCount.value = interfaces.length; return interfaces; }, classDescription: "XMLHttpRequest", contractID: "@mozilla.org/xmlextras/xmlhttprequest;1", classID: Components.ID("{c9b37f43-4278-4304-a5e0-600991ab08cb}"), createInstance: function(aOuter, aIID) { if (aOuter == null) return this.QueryInterface(aIID); throw Cr.NS_ERROR_NO_AGGREGATION; }, QueryInterface: function(aIID) { if (aIID.equals(Ci.nsIClassInfo) || aIID.equals(Ci.nsISupports)) return this; throw Cr.NS_ERROR_NO_INTERFACE; }, get wrappedJSObject() { return this; } }; /** * Helper used to overrideXHR requests (no matter to what URL) with the * specified status and response. * @param status The status you want to get back when an XHR request is made * @param response The response you want to get back when an XHR request is made */ function overrideXHR(status, response, options) { overrideXHR.myxhr = new xhr(status, response, options); ProductAddonCheckerScope.CreateXHR = function() { return overrideXHR.myxhr; }; return overrideXHR.myxhr; } /** * Creates a new zip file containing a file with the specified data * @param zipName The name of the zip file * @param data The data to go inside the zip for the filename entry1.info */ function createNewZipFile(zipName, data) { // Create a zip file which will be used for extracting let stream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); stream.setData(data, data.length); let zipWriter = Cc["@mozilla.org/zipwriter;1"]. createInstance(Components.interfaces.nsIZipWriter); let zipFile = FileUtils.getFile("TmpD", [zipName]); if (zipFile.exists()) { zipFile.remove(false); } // From prio.h const PR_RDWR = 0x04; const PR_CREATE_FILE = 0x08; const PR_TRUNCATE = 0x20; zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); zipWriter.addEntryStream("entry1.info", Date.now(), Ci.nsIZipWriter.COMPRESSION_BEST, stream, false); zipWriter.close(); stream.close(); do_print("zip file created on disk at: " + zipFile.path); return zipFile; }