const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; const XPI_URL = `${SECURE_TESTROOT}addons/browser_webapi_install.xpi`; const XPI_SHA = "sha256:d4bab17ff9ba5f635e97c84021f4c527c502250d62ab7f6e6c9e8ee28822f772"; const ID = "webapi_install@tests.mozilla.org"; // eh, would be good to just stat the real file instead of this... const XPI_LEN = 4782; function waitForClear() { const MSG = "WebAPICleanup"; return new Promise(resolve => { let listener = { receiveMessage: function(msg) { if (msg.name == MSG) { Services.mm.removeMessageListener(MSG, listener); resolve(); } } }; Services.mm.addMessageListener(MSG, listener, true); }); } add_task(function* setup() { yield SpecialPowers.pushPrefEnv({ set: [["extensions.webapi.testing", true], ["extensions.install.requireBuiltInCerts", false]], }); info("added preferences"); }); // Wrapper around a common task to run in the content process to test // the mozAddonManager API. Takes a URL for the XPI to install and an // array of steps, each of which can either be an action to take // (i.e., start or cancel the install) or an install event to wait for. // Steps that look for a specific event may also include a "props" property // with properties that the AddonInstall object is expected to have when // that event is triggered. function* testInstall(browser, args, steps, description) { let success = yield ContentTask.spawn(browser, {args, steps}, function* (opts) { let { args, steps } = opts; let install = yield content.navigator.mozAddonManager.createInstall(args); if (!install) { yield Promise.reject("createInstall() did not return an install object"); } // Check that the initial state of the AddonInstall is sane. if (install.state != "STATE_AVAILABLE") { yield Promise.reject("new install should be in STATE_AVAILABLE"); } if (install.error != null) { yield Promise.reject("new install should have null error"); } const events = [ "onDownloadStarted", "onDownloadProgress", "onDownloadEnded", "onDownloadCancelled", "onDownloadFailed", "onInstallStarted", "onInstallEnded", "onInstallCancelled", "onInstallFailed", ]; let eventWaiter = null; let receivedEvents = []; let prevEvent = null; events.forEach(event => { install.addEventListener(event, e => { receivedEvents.push({ event, state: install.state, error: install.error, progress: install.progress, maxProgress: install.maxProgress, }); if (eventWaiter) { eventWaiter(); } }); }); // Returns a promise that is resolved when the given event occurs // or rejects if a different event comes first or if props is supplied // and properties on the AddonInstall don't match those in props. function expectEvent(event, props) { return new Promise((resolve, reject) => { function check() { let received = receivedEvents.shift(); // Skip any repeated onDownloadProgress events. while (received && received.event == prevEvent && prevEvent == "onDownloadProgress") { received = receivedEvents.shift(); } // Wait for more events if we skipped all there were. if (!received) { eventWaiter = () => { eventWaiter = null; check(); } return; } prevEvent = received.event; if (received.event != event) { let err = new Error(`expected ${event} but got ${received.event}`); reject(err); } if (props) { for (let key of Object.keys(props)) { if (received[key] != props[key]) { throw new Error(`AddonInstall property ${key} was ${received[key]} but expected ${props[key]}`); } } } resolve(); } check(); }); } while (steps.length > 0) { let nextStep = steps.shift(); if (nextStep.action) { if (nextStep.action == "install") { yield install.install(); } else if (nextStep.action == "cancel") { yield install.cancel(); } else { throw new Error(`unknown action ${nextStep.action}`); } } else { yield expectEvent(nextStep.event, nextStep.props); } } return true; }); is(success, true, description); } function makeInstallTest(task) { return function*() { // withNewTab() will close the test tab before returning, at which point // the cleanup event will come from the content process. We need to see // that event but don't want to race to install a listener for it after // the tab is closed. So set up the listener now but don't yield the // listening promise until below. let clearPromise = waitForClear(); yield BrowserTestUtils.withNewTab(TESTPAGE, task); yield clearPromise; is(AddonManager.webAPI.installs.size, 0, "AddonInstall was cleaned up"); }; } function makeRegularTest(options, what) { return makeInstallTest(function* (browser) { let steps = [ {action: "install"}, { event: "onDownloadStarted", props: {state: "STATE_DOWNLOADING"}, }, { event: "onDownloadProgress", props: {maxProgress: XPI_LEN}, }, { event: "onDownloadEnded", props: { state: "STATE_DOWNLOADED", progress: XPI_LEN, maxProgress: XPI_LEN, }, }, { event: "onInstallStarted", props: {state: "STATE_INSTALLING"}, }, { event: "onInstallEnded", props: {state: "STATE_INSTALLED"}, }, ]; yield testInstall(browser, options, steps, what); let version = Services.prefs.getIntPref("webapitest.active_version"); is(version, 1, "the install really did work"); // Sanity check to ensure that the test in makeInstallTest() that // installs.size == 0 means we actually did clean up. ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall"); let addons = yield promiseAddonsByIDs([ID]); isnot(addons[0], null, "Found the addon"); yield addons[0].uninstall(); addons = yield promiseAddonsByIDs([ID]); is(addons[0], null, "Addon was uninstalled"); }); } add_task(makeRegularTest({url: XPI_URL}, "a basic install works")); add_task(makeRegularTest({url: XPI_URL, hash: null}, "install with hash=null works")); add_task(makeRegularTest({url: XPI_URL, hash: ""}, "install with empty string for hash works")); add_task(makeRegularTest({url: XPI_URL, hash: XPI_SHA}, "install with hash works")); add_task(makeInstallTest(function* (browser) { let steps = [ {action: "cancel"}, { event: "onDownloadCancelled", props: { state: "STATE_CANCELLED", error: null, }, } ]; yield testInstall(browser, {url: XPI_URL}, steps, "canceling an install works"); let addons = yield promiseAddonsByIDs([ID]); is(addons[0], null, "The addon was not installed"); ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall"); })); add_task(makeInstallTest(function* (browser) { let steps = [ {action: "install"}, { event: "onDownloadStarted", props: {state: "STATE_DOWNLOADING"}, }, {event: "onDownloadProgress"}, { event: "onDownloadFailed", props: { state: "STATE_DOWNLOAD_FAILED", error: "ERROR_NETWORK_FAILURE", }, } ]; yield testInstall(browser, {url: XPI_URL + "bogus"}, steps, "install of a bad url fails"); let addons = yield promiseAddonsByIDs([ID]); is(addons[0], null, "The addon was not installed"); ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall"); })); add_task(makeInstallTest(function* (browser) { let steps = [ {action: "install"}, { event: "onDownloadStarted", props: {state: "STATE_DOWNLOADING"}, }, {event: "onDownloadProgress"}, { event: "onDownloadFailed", props: { state: "STATE_DOWNLOAD_FAILED", error: "ERROR_INCORRECT_HASH", }, } ]; yield testInstall(browser, {url: XPI_URL, hash: "sha256:bogus"}, steps, "install with bad hash fails"); let addons = yield promiseAddonsByIDs([ID]); is(addons[0], null, "The addon was not installed"); ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall"); })); add_task(function* test_permissions() { function testBadUrl(url, pattern, successMessage) { return BrowserTestUtils.withNewTab(TESTPAGE, function* (browser) { let result = yield ContentTask.spawn(browser, {url, pattern}, function (opts) { return new Promise(resolve => { content.navigator.mozAddonManager.createInstall({url: opts.url}) .then(() => { resolve({success: false, message: "createInstall should not have succeeded"}); }, err => { if (err.message.match(new RegExp(opts.pattern))) { resolve({success: true}); } resolve({success: false, message: `Wrong error message: ${err.message}`}); }); }); }); is(result.success, true, result.message || successMessage); }); } yield testBadUrl("i am not a url", "NS_ERROR_MALFORMED_URI", "Installing from an unparseable URL fails"); yield testBadUrl("https://addons.not-really-mozilla.org/impostor.xpi", "not permitted", "Installing from non-approved URL fails"); });