summaryrefslogtreecommitdiffstats
path: root/toolkit/components/jsdownloads/test
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/jsdownloads/test')
-rw-r--r--toolkit/components/jsdownloads/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/jsdownloads/test/browser/browser.ini7
-rw-r--r--toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js97
-rw-r--r--toolkit/components/jsdownloads/test/browser/head.js87
-rw-r--r--toolkit/components/jsdownloads/test/browser/testFile.html9
-rw-r--r--toolkit/components/jsdownloads/test/data/.eslintrc.js7
-rw-r--r--toolkit/components/jsdownloads/test/data/empty.txt0
-rw-r--r--toolkit/components/jsdownloads/test/data/source.txt1
-rw-r--r--toolkit/components/jsdownloads/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/jsdownloads/test/unit/common_test_Download.js2432
-rw-r--r--toolkit/components/jsdownloads/test/unit/head.js843
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadCore.js87
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadImport.js701
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js432
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js17
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadList.js564
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadStore.js315
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_Downloads.js194
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js24
-rw-r--r--toolkit/components/jsdownloads/test/unit/xpcshell.ini19
20 files changed, 5850 insertions, 0 deletions
diff --git a/toolkit/components/jsdownloads/test/browser/.eslintrc.js b/toolkit/components/jsdownloads/test/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/jsdownloads/test/browser/browser.ini b/toolkit/components/jsdownloads/test/browser/browser.ini
new file mode 100644
index 000000000..131fc4ec8
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+ testFile.html
+
+[browser_DownloadPDFSaver.js]
+skip-if = os != "win"
diff --git a/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
new file mode 100644
index 000000000..80ed9665a
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the PDF download saver, and tests using a window as a
+ * source for the copy download saver.
+ */
+
+"use strict";
+
+/**
+ * Helper function to make sure a window reference exists on the download source.
+ */
+function* test_download_windowRef(aTab, aDownload) {
+ ok(aDownload.source.windowRef, "Download source had a window reference");
+ ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref");
+ is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test");
+}
+
+/**
+ * Helper function to check the state of a completed download.
+ */
+function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) {
+ ok(aDownload.source, "Download has a source");
+ is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url");
+ is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state");
+ ok(aDownload.stopped, "Download is stopped");
+ is(aCanceled, aDownload.canceled, "Download has correct canceled state");
+ is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state");
+ is(aDownload.error, null, "Download error is not defined");
+}
+
+function* test_createDownload_common(aPrivate, aType) {
+ let win = yield BrowserTestUtils.openNewBrowserWindow({ private : aPrivate});
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, getRootDirectory(gTestPath) + "testFile.html");
+ let download = yield Downloads.createDownload({
+ source: tab.linkedBrowser.contentWindow,
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+ saver: { type: aType }
+ });
+
+ yield test_download_windowRef(tab, download);
+ yield download.start();
+
+ yield test_download_state_complete(tab, download, aPrivate, false);
+ if (aType == "pdf") {
+ let signature = yield OS.File.read(download.target.path,
+ { bytes: 4, encoding: "us-ascii" });
+ is(signature, "%PDF", "File exists and signature matches");
+ } else {
+ ok((yield OS.File.exists(download.target.path)), "File exists");
+ }
+
+ win.gBrowser.removeTab(tab);
+ win.close()
+}
+
+add_task(function* test_createDownload_pdf_private() {
+ yield test_createDownload_common(true, "pdf");
+});
+add_task(function* test_createDownload_pdf_not_private() {
+ yield test_createDownload_common(false, "pdf");
+});
+
+// Even for the copy saver, using a window should produce valid results
+add_task(function* test_createDownload_copy_private() {
+ yield test_createDownload_common(true, "copy");
+});
+add_task(function* test_createDownload_copy_not_private() {
+ yield test_createDownload_common(false, "copy");
+});
+
+add_task(function* test_cancel_pdf_download() {
+ let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ let download = yield Downloads.createDownload({
+ source: tab.linkedBrowser.contentWindow,
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+ saver: "pdf",
+ });
+
+ yield test_download_windowRef(tab, download);
+ download.start().catch(() => {});
+
+ // Immediately cancel the download to test that it is erased correctly.
+ yield download.cancel();
+ yield test_download_state_complete(tab, download, false, true);
+
+ let exists = yield OS.File.exists(download.target.path)
+ ok(!exists, "Target file does not exist");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/jsdownloads/test/browser/head.js b/toolkit/components/jsdownloads/test/browser/head.js
new file mode 100644
index 000000000..769aaacb3
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/head.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf";
+
+// Support functions
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+var gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ ok(!file.exists(), "Temp file does not exist");
+
+ registerCleanupFunction(function () {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+function promiseBrowserLoaded(browser) {
+ return new Promise(resolve => {
+ browser.addEventListener("load", function onLoad(event) {
+ if (event.target == browser.contentDocument) {
+ browser.removeEventListener("load", onLoad, true);
+ resolve();
+ }
+ }, true);
+ });
+}
diff --git a/toolkit/components/jsdownloads/test/browser/testFile.html b/toolkit/components/jsdownloads/test/browser/testFile.html
new file mode 100644
index 000000000..ee413514b
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/testFile.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test Save as PDF</title>
+ </head>
+ <body>
+ <p>Save me as a PDF!</p>
+ </body>
+</html>
diff --git a/toolkit/components/jsdownloads/test/data/.eslintrc.js b/toolkit/components/jsdownloads/test/data/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/data/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/jsdownloads/test/data/empty.txt b/toolkit/components/jsdownloads/test/data/empty.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/data/empty.txt
diff --git a/toolkit/components/jsdownloads/test/data/source.txt b/toolkit/components/jsdownloads/test/data/source.txt
new file mode 100644
index 000000000..2156cb8c0
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/data/source.txt
@@ -0,0 +1 @@
+This test string is downloaded. \ No newline at end of file
diff --git a/toolkit/components/jsdownloads/test/unit/.eslintrc.js b/toolkit/components/jsdownloads/test/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/jsdownloads/test/unit/common_test_Download.js b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
new file mode 100644
index 000000000..42d4c5682
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -0,0 +1,2432 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This script is loaded by "test_DownloadCore.js" and "test_DownloadLegacy.js"
+ * with different values of the gUseLegacySaver variable, to apply tests to both
+ * the "copy" and "legacy" saver implementations.
+ */
+
+"use strict";
+
+// Globals
+
+const kDeleteTempFileOnExit = "browser.helperApps.deleteTempFileOnExit";
+
+/**
+ * Creates and starts a new download, using either DownloadCopySaver or
+ * DownloadLegacySaver based on the current test run.
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object. The download may be in progress
+ * or already finished. The promiseDownloadStopped function can be
+ * used to wait for completion.
+ * @rejects JavaScript exception.
+ */
+function promiseStartDownload(aSourceUrl) {
+ if (gUseLegacySaver) {
+ return promiseStartLegacyDownload(aSourceUrl);
+ }
+
+ return promiseNewDownload(aSourceUrl).then(download => {
+ download.start().catch(() => {});
+ return download;
+ });
+}
+
+/**
+ * Creates and starts a new download, configured to keep partial data, and
+ * returns only when the first part of "interruptible_resumable.txt" has been
+ * saved to disk. You must call "continueResponses" to allow the interruptible
+ * request to continue.
+ *
+ * This function uses either DownloadCopySaver or DownloadLegacySaver based on
+ * the current test run.
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object, still in progress.
+ * @rejects JavaScript exception.
+ */
+function promiseStartDownload_tryToKeepPartialData() {
+ return Task.spawn(function* () {
+ mustInterruptResponses();
+
+ // Start a new download and configure it to keep partially downloaded data.
+ let download;
+ if (!gUseLegacySaver) {
+ let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ download = yield Downloads.createDownload({
+ source: httpUrl("interruptible_resumable.txt"),
+ target: { path: targetFilePath,
+ partFilePath: targetFilePath + ".part" },
+ });
+ download.tryToKeepPartialData = true;
+ download.start().catch(() => {});
+ } else {
+ // Start a download using nsIExternalHelperAppService, that is configured
+ // to keep partially downloaded data by default.
+ download = yield promiseStartExternalHelperAppServiceDownload();
+ }
+
+ yield promiseDownloadMidway(download);
+ yield promisePartFileReady(download);
+
+ return download;
+ });
+}
+
+/**
+ * This function should be called after the progress notification for a download
+ * is received, and waits for the worker thread of BackgroundFileSaver to
+ * receive the data to be written to the ".part" file on disk.
+ *
+ * @return {Promise}
+ * @resolves When the ".part" file has been written to disk.
+ * @rejects JavaScript exception.
+ */
+function promisePartFileReady(aDownload) {
+ return Task.spawn(function* () {
+ // We don't have control over the file output code in BackgroundFileSaver.
+ // After we receive the download progress notification, we may only check
+ // that the ".part" file has been created, while its size cannot be
+ // determined because the file is currently open.
+ try {
+ do {
+ yield promiseTimeout(50);
+ } while (!(yield OS.File.exists(aDownload.target.partFilePath)));
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error)) {
+ throw ex;
+ }
+ // This indicates that the file has been created and cannot be accessed.
+ // The specific error might vary with the platform.
+ do_print("Expected exception while checking existence: " + ex.toString());
+ // Wait some more time to allow the write to complete.
+ yield promiseTimeout(100);
+ }
+ });
+}
+
+/**
+ * Checks that the actual data written to disk matches the expected data as well
+ * as the properties of the given DownloadTarget object.
+ *
+ * @param downloadTarget
+ * The DownloadTarget object whose details have to be verified.
+ * @param expectedContents
+ * String containing the octets that are expected in the file.
+ *
+ * @return {Promise}
+ * @resolves When the properties have been verified.
+ * @rejects JavaScript exception.
+ */
+var promiseVerifyTarget = Task.async(function* (downloadTarget,
+ expectedContents) {
+ yield promiseVerifyContents(downloadTarget.path, expectedContents);
+ do_check_true(downloadTarget.exists);
+ do_check_eq(downloadTarget.size, expectedContents.length);
+});
+
+/**
+ * Waits for an attempt to launch a file, and returns the nsIMIMEInfo used for
+ * the launch, or null if the file was launched with the default handler.
+ */
+function waitForFileLaunched() {
+ return new Promise(resolve => {
+ let waitFn = base => ({
+ launchFile(file, mimeInfo) {
+ Integration.downloads.unregister(waitFn);
+ if (!mimeInfo ||
+ mimeInfo.preferredAction == Ci.nsIMIMEInfo.useSystemDefault) {
+ resolve(null);
+ } else {
+ resolve(mimeInfo);
+ }
+ return Promise.resolve();
+ },
+ });
+ Integration.downloads.register(waitFn);
+ });
+}
+
+/**
+ * Waits for an attempt to show the directory where a file is located, and
+ * returns the path of the file.
+ */
+function waitForDirectoryShown() {
+ return new Promise(resolve => {
+ let waitFn = base => ({
+ showContainingDirectory(path) {
+ Integration.downloads.unregister(waitFn);
+ resolve(path);
+ return Promise.resolve();
+ },
+ });
+ Integration.downloads.register(waitFn);
+ });
+}
+
+// Tests
+
+/**
+ * Executes a download and checks its basic properties after construction.
+ * The download is started by constructing the simplest Download object with
+ * the "copy" saver, or using the legacy nsITransfer interface.
+ */
+add_task(function* test_basic()
+{
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can check its basic properties before it starts.
+ download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: { path: targetFile.path },
+ saver: { type: "copy" },
+ });
+
+ do_check_eq(download.source.url, httpUrl("source.txt"));
+ do_check_eq(download.target.path, targetFile.path);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we must check its basic properties while in progress.
+ download = yield promiseStartLegacyDownload(null,
+ { targetFile: targetFile });
+
+ do_check_eq(download.source.url, httpUrl("source.txt"));
+ do_check_eq(download.target.path, targetFile.path);
+
+ yield promiseDownloadStopped(download);
+ }
+
+ // Check additional properties on the finished download.
+ do_check_true(download.source.referrer === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Executes a download with the tryToKeepPartialData property set, and ensures
+ * that the file is saved correctly. When testing DownloadLegacySaver, the
+ * download is executed using the nsIExternalHelperAppService component.
+ */
+add_task(function* test_basic_tryToKeepPartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ continueResponses();
+ yield promiseDownloadStopped(download);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_eq(32, download.saver.getSha256Hash().length);
+});
+
+/**
+ * Tests the permissions of the final target file once the download finished.
+ */
+add_task(function* test_unix_permissions()
+{
+ // This test is only executed on some Desktop systems.
+ if (Services.appinfo.OS != "Darwin" && Services.appinfo.OS != "Linux" &&
+ Services.appinfo.OS != "WINNT") {
+ do_print("Skipping test.");
+ return;
+ }
+
+ let launcherPath = getTempFile("app-launcher").path;
+
+ for (let autoDelete of [false, true]) {
+ for (let isPrivate of [false, true]) {
+ for (let launchWhenSucceeded of [false, true]) {
+ do_print("Checking " + JSON.stringify({ autoDelete,
+ isPrivate,
+ launchWhenSucceeded }));
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, autoDelete);
+
+ let download;
+ if (!gUseLegacySaver) {
+ download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launchWhenSucceeded,
+ launcherPath,
+ });
+ yield download.start();
+ } else {
+ download = yield promiseStartLegacyDownload(httpUrl("source.txt"), {
+ isPrivate,
+ launchWhenSucceeded,
+ launcherPath: launchWhenSucceeded && launcherPath,
+ });
+ yield promiseDownloadStopped(download);
+ }
+
+ let isTemporary = launchWhenSucceeded && (autoDelete || isPrivate);
+ let stat = yield OS.File.stat(download.target.path);
+ if (Services.appinfo.OS == "WINNT") {
+ // On Windows
+ // Temporary downloads should be read-only
+ do_check_eq(stat.winAttributes.readOnly, isTemporary ? true : false);
+ } else {
+ // On Linux, Mac
+ // Temporary downloads should be read-only and not accessible to other
+ // users, while permanently downloaded files should be readable and
+ // writable as specified by the system umask.
+ do_check_eq(stat.unixMode,
+ isTemporary ? 0o400 : (0o666 & ~OS.Constants.Sys.umask));
+ }
+ }
+ }
+ }
+
+ // Clean up the changes to the preference.
+ Services.prefs.clearUserPref(kDeleteTempFileOnExit);
+});
+
+/**
+ * Checks the referrer for downloads.
+ */
+add_task(function* test_referrer()
+{
+ let sourcePath = "/test_referrer.txt";
+ let sourceUrl = httpUrl("test_referrer.txt");
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ do_check_true(aRequest.hasHeader("Referer"));
+ do_check_eq(aRequest.getHeader("Referer"), TEST_REFERRER_URL);
+ });
+ let download = yield Downloads.createDownload({
+ source: { url: sourceUrl, referrer: TEST_REFERRER_URL },
+ target: targetPath,
+ });
+ do_check_eq(download.source.referrer, TEST_REFERRER_URL);
+ yield download.start();
+
+ download = yield Downloads.createDownload({
+ source: { url: sourceUrl, referrer: TEST_REFERRER_URL,
+ isPrivate: true },
+ target: targetPath,
+ });
+ do_check_eq(download.source.referrer, TEST_REFERRER_URL);
+ yield download.start();
+
+ // Test the download still works for non-HTTP channel with referrer.
+ sourceUrl = "data:text/html,<html><body></body></html>";
+ download = yield Downloads.createDownload({
+ source: { url: sourceUrl, referrer: TEST_REFERRER_URL },
+ target: targetPath,
+ });
+ do_check_eq(download.source.referrer, TEST_REFERRER_URL);
+ yield download.start();
+
+ cleanup();
+});
+
+/**
+ * Checks the adjustChannel callback for downloads.
+ */
+add_task(function* test_adjustChannel()
+{
+ const sourcePath = "/test_post.txt";
+ const sourceUrl = httpUrl("test_post.txt");
+ const targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ const customHeader = { name: "X-Answer", value: "42" };
+ const postData = "Don't Panic";
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, aRequest => {
+ do_check_eq(aRequest.method, "POST");
+
+ do_check_true(aRequest.hasHeader(customHeader.name));
+ do_check_eq(aRequest.getHeader(customHeader.name), customHeader.value);
+
+ const stream = aRequest.bodyInputStream;
+ const body = NetUtil.readInputStreamToString(stream, stream.available());
+ do_check_eq(body, postData);
+ });
+
+ function adjustChannel(channel) {
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.setRequestHeader(customHeader.name, customHeader.value, false);
+
+ const stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(postData, postData.length);
+
+ channel.QueryInterface(Ci.nsIUploadChannel2);
+ channel.explicitSetUploadStream(stream, null, -1, "POST", false);
+
+ return Promise.resolve();
+ }
+
+ const download = yield Downloads.createDownload({
+ source: { url: sourceUrl, adjustChannel },
+ target: targetPath,
+ });
+ do_check_eq(download.source.adjustChannel, adjustChannel);
+ do_check_eq(download.toSerializable(), null);
+ yield download.start();
+
+ cleanup();
+});
+
+/**
+ * Checks initial and final state and progress for a successful download.
+ */
+add_task(function* test_initial_final_state()
+{
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can check its state before it starts.
+ download = yield promiseNewDownload();
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+ do_check_eq(download.progress, 0);
+ do_check_true(download.startTime === null);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we cannot check its initial state.
+ download = yield promiseStartLegacyDownload();
+ yield promiseDownloadStopped(download);
+ }
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+ do_check_eq(download.progress, 100);
+ do_check_true(isValidDate(download.startTime));
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, TEST_DATA_SHORT.length);
+});
+
+/**
+ * Checks the notification of the final download state.
+ */
+add_task(function* test_final_state_notified()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let onchangeNotified = false;
+ let lastNotifiedStopped;
+ let lastNotifiedProgress;
+ download.onchange = function () {
+ onchangeNotified = true;
+ lastNotifiedStopped = download.stopped;
+ lastNotifiedProgress = download.progress;
+ };
+
+ // Allow the download to complete.
+ let promiseAttempt = download.start();
+ continueResponses();
+ yield promiseAttempt;
+
+ // The view should have been notified before the download completes.
+ do_check_true(onchangeNotified);
+ do_check_true(lastNotifiedStopped);
+ do_check_eq(lastNotifiedProgress, 100);
+});
+
+/**
+ * Checks intermediate progress for a successful download.
+ */
+add_task(function* test_intermediate_progress()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ yield promiseDownloadMidway(download);
+
+ do_check_true(download.hasProgress);
+ do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
+
+ // The final file size should not be computed for in-progress downloads.
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // Continue after the first chunk of data is fully received.
+ continueResponses();
+ yield promiseDownloadStopped(download);
+
+ do_check_true(download.stopped);
+ do_check_eq(download.progress, 100);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Downloads a file with a "Content-Length" of 0 and checks the progress.
+ */
+add_task(function* test_empty_progress()
+{
+ let download = yield promiseStartDownload(httpUrl("empty.txt"));
+ yield promiseDownloadStopped(download);
+
+ do_check_true(download.stopped);
+ do_check_true(download.hasProgress);
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+
+ // We should have received the content type even for an empty file.
+ do_check_eq(download.contentType, "text/plain");
+
+ do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Downloads a file with a "Content-Length" of 0 with the tryToKeepPartialData
+ * property set, and ensures that the file is saved correctly.
+ */
+add_task(function* test_empty_progress_tryToKeepPartialData()
+{
+ // Start a new download and configure it to keep partially downloaded data.
+ let download;
+ if (!gUseLegacySaver) {
+ let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ download = yield Downloads.createDownload({
+ source: httpUrl("empty.txt"),
+ target: { path: targetFilePath,
+ partFilePath: targetFilePath + ".part" },
+ });
+ download.tryToKeepPartialData = true;
+ download.start().catch(() => {});
+ } else {
+ // Start a download using nsIExternalHelperAppService, that is configured
+ // to keep partially downloaded data by default.
+ download = yield promiseStartExternalHelperAppServiceDownload(
+ httpUrl("empty.txt"));
+ }
+ yield promiseDownloadStopped(download);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_eq(32, download.saver.getSha256Hash().length);
+});
+
+/**
+ * Downloads an empty file with no "Content-Length" and checks the progress.
+ */
+add_task(function* test_empty_noprogress()
+{
+ let sourcePath = "/test_empty_noprogress.txt";
+ let sourceUrl = httpUrl("test_empty_noprogress.txt");
+ let deferRequestReceived = Promise.defer();
+
+ // Register an interruptible handler that notifies us when the request occurs.
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ registerInterruptibleHandler(sourcePath,
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ deferRequestReceived.resolve();
+ }, function secondPart(aRequest, aResponse) { });
+
+ // Start the download, without allowing the request to finish.
+ mustInterruptResponses();
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can hook its onchange callback that will be notified when the
+ // download starts.
+ download = yield promiseNewDownload(sourceUrl);
+
+ download.onchange = function () {
+ if (!download.stopped) {
+ do_check_false(download.hasProgress);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+ }
+ };
+
+ download.start().catch(() => {});
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, and it may have already made all needed property change
+ // notifications, thus there is no point in checking the onchange callback.
+ download = yield promiseStartLegacyDownload(sourceUrl);
+ }
+
+ // Wait for the request to be received by the HTTP server, but don't allow the
+ // request to finish yet. Before checking the download state, wait for the
+ // events to be processed by the client.
+ yield deferRequestReceived.promise;
+ yield promiseExecuteSoon();
+
+ // Check that this download has no progress report.
+ do_check_false(download.stopped);
+ do_check_false(download.hasProgress);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+
+ // Now allow the response to finish.
+ continueResponses();
+ yield promiseDownloadStopped(download);
+
+ // We should have received the content type even if no progress is reported.
+ do_check_eq(download.contentType, "text/plain");
+
+ // Verify the state of the completed download.
+ do_check_true(download.stopped);
+ do_check_false(download.hasProgress);
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
+});
+
+/**
+ * Calls the "start" method two times before the download is finished.
+ */
+add_task(function* test_start_twice()
+{
+ mustInterruptResponses();
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can start the download later during the test.
+ download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created. Effectively, we are starting the download three times.
+ download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"));
+ }
+
+ // Call the start method two times.
+ let promiseAttempt1 = download.start();
+ let promiseAttempt2 = download.start();
+
+ // Allow the download to finish.
+ continueResponses();
+
+ // Both promises should now be resolved.
+ yield promiseAttempt1;
+ yield promiseAttempt2;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download and verifies that its state is reported correctly.
+ */
+add_task(function* test_cancel_midway()
+{
+ mustInterruptResponses();
+
+ // In this test case, we execute different checks that are only possible with
+ // DownloadCopySaver or DownloadLegacySaver respectively.
+ let download;
+ let options = {};
+ if (!gUseLegacySaver) {
+ download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ } else {
+ download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"),
+ options);
+ }
+
+ // Cancel the download after receiving the first part of the response.
+ let deferCancel = Promise.defer();
+ let onchange = function () {
+ if (!download.stopped && !download.canceled && download.progress == 50) {
+ // Cancel the download immediately during the notification.
+ deferCancel.resolve(download.cancel());
+
+ // The state change happens immediately after calling "cancel", but
+ // temporary files or part files may still exist at this point.
+ do_check_true(download.canceled);
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress. This may happen
+ // when using DownloadLegacySaver.
+ download.onchange = onchange;
+ onchange();
+
+ let promiseAttempt;
+ if (!gUseLegacySaver) {
+ promiseAttempt = download.start();
+ }
+
+ // Wait on the promise returned by the "cancel" method to ensure that the
+ // cancellation process finished and temporary files were removed.
+ yield deferCancel.promise;
+
+ if (gUseLegacySaver) {
+ // The nsIWebBrowserPersist instance should have been canceled now.
+ do_check_eq(options.outPersist.result, Cr.NS_ERROR_ABORT);
+ }
+
+ do_check_true(download.stopped);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ // Progress properties are not reset by canceling.
+ do_check_eq(download.progress, 50);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
+
+ if (!gUseLegacySaver) {
+ // The promise returned by "start" should have been rejected meanwhile.
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+ }
+});
+
+/**
+ * Cancels a download while keeping partially downloaded data, and verifies that
+ * both the target file and the ".part" file are deleted.
+ */
+add_task(function* test_cancel_midway_tryToKeepPartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+
+ do_check_true(yield OS.File.exists(download.target.path));
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ yield download.cancel();
+ yield download.removePartialData();
+
+ do_check_true(download.stopped);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download right after starting it.
+ */
+add_task(function* test_cancel_immediately()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseAttempt = download.start();
+ do_check_false(download.stopped);
+
+ let promiseCancel = download.cancel();
+ do_check_true(download.canceled);
+
+ // At this point, we don't know whether the download has already stopped or
+ // is still waiting for cancellation. We can wait on the promise returned
+ // by the "start" method to know for sure.
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ do_check_true(download.stopped);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ // Check that the promise returned by the "cancel" method has been resolved.
+ yield promiseCancel;
+});
+
+/**
+ * Cancels and restarts a download sequentially.
+ */
+add_task(function* test_cancel_midway_restart()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ // The first time, cancel the download midway.
+ yield promiseDownloadMidway(download);
+ yield download.cancel();
+
+ do_check_true(download.stopped);
+
+ // The second time, we'll provide the entire interruptible response.
+ continueResponses();
+ download.onchange = null;
+ let promiseAttempt = download.start();
+
+ // Download state should have already been reset.
+ do_check_false(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ // For the following test, we rely on the network layer reporting its progress
+ // asynchronously. Otherwise, there is nothing stopping the restarted
+ // download from reaching the same progress as the first request already.
+ do_check_eq(download.progress, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_eq(download.currentBytes, 0);
+
+ yield promiseAttempt;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download and restarts it from where it stopped.
+ */
+add_task(function* test_cancel_midway_restart_tryToKeepPartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.cancel();
+
+ do_check_true(download.stopped);
+ do_check_true(download.hasPartialData);
+
+ // The target file should not exist, but we should have kept the partial data.
+ do_check_false(yield OS.File.exists(download.target.path));
+ yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // Verify that the server sent the response from the start.
+ do_check_eq(gMostRecentFirstBytePos, 0);
+
+ // The second time, we'll request and obtain the second part of the response,
+ // but we still stop when half of the remaining progress is reached.
+ let deferMidway = Promise.defer();
+ download.onchange = function () {
+ if (!download.stopped && !download.canceled &&
+ download.currentBytes == Math.floor(TEST_DATA_SHORT.length * 3 / 2)) {
+ download.onchange = null;
+ deferMidway.resolve();
+ }
+ };
+
+ mustInterruptResponses();
+ let promiseAttempt = download.start();
+
+ // Continue when the number of bytes we received is correct, then check that
+ // progress is at about 75 percent. The exact figure may vary because of
+ // rounding issues, since the total number of bytes in the response might not
+ // be a multiple of four.
+ yield deferMidway.promise;
+ do_check_true(download.progress > 72 && download.progress < 78);
+
+ // Now we allow the download to finish.
+ continueResponses();
+ yield promiseAttempt;
+
+ // Check that the server now sent the second part only.
+ do_check_eq(gMostRecentFirstBytePos, TEST_DATA_SHORT.length);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download while keeping partially downloaded data, then removes the
+ * data and restarts the download from the beginning.
+ */
+add_task(function* test_cancel_midway_restart_removePartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.cancel();
+
+ do_check_true(download.hasPartialData);
+ yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ yield download.removePartialData();
+
+ do_check_false(download.hasPartialData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // The second time, we'll request and obtain the entire response again.
+ continueResponses();
+ yield download.start();
+
+ // Verify that the server sent the response from the start.
+ do_check_eq(gMostRecentFirstBytePos, 0);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download while keeping partially downloaded data, then removes the
+ * data and restarts the download from the beginning without keeping the partial
+ * data anymore.
+ */
+add_task(function* test_cancel_midway_restart_tryToKeepPartialData_false()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.cancel();
+
+ download.tryToKeepPartialData = false;
+
+ // The above property change does not affect existing partial data.
+ do_check_true(download.hasPartialData);
+ yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+
+ yield download.removePartialData();
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+
+ // Restart the download from the beginning.
+ mustInterruptResponses();
+ download.start().catch(() => {});
+
+ yield promiseDownloadMidway(download);
+ yield promisePartFileReady(download);
+
+ // While the download is in progress, we should still have a ".part" file.
+ do_check_false(download.hasPartialData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ // On Unix, verify that the file with the partially downloaded data is not
+ // accessible by other users on the system.
+ if (Services.appinfo.OS == "Darwin" || Services.appinfo.OS == "Linux") {
+ do_check_eq((yield OS.File.stat(download.target.partFilePath)).unixMode,
+ 0o600);
+ }
+
+ yield download.cancel();
+
+ // The ".part" file should be deleted now that the download is canceled.
+ do_check_false(download.hasPartialData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+
+ // The third time, we'll request and obtain the entire response again.
+ continueResponses();
+ yield download.start();
+
+ // Verify that the server sent the response from the start.
+ do_check_eq(gMostRecentFirstBytePos, 0);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download right after starting it, then restarts it immediately.
+ */
+add_task(function* test_cancel_immediately_restart_immediately()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt = download.start();
+
+ do_check_false(download.stopped);
+
+ download.cancel();
+ do_check_true(download.canceled);
+
+ let promiseRestarted = download.start();
+ do_check_false(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ // For the following test, we rely on the network layer reporting its progress
+ // asynchronously. Otherwise, there is nothing stopping the restarted
+ // download from reaching the same progress as the first request already.
+ do_check_eq(download.hasProgress, false);
+ do_check_eq(download.progress, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_eq(download.currentBytes, 0);
+
+ // Ensure the next request is now allowed to complete, regardless of whether
+ // the canceled request was received by the server or not.
+ continueResponses();
+ try {
+ yield promiseAttempt;
+ // If we get here, it means that the first attempt actually succeeded. In
+ // fact, this could be a valid outcome, because the cancellation request may
+ // not have been processed in time before the download finished.
+ do_print("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ yield promiseRestarted;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download midway, then restarts it immediately.
+ */
+add_task(function* test_cancel_midway_restart_immediately()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt = download.start();
+
+ // The first time, cancel the download midway.
+ yield promiseDownloadMidway(download);
+ download.cancel();
+ do_check_true(download.canceled);
+
+ let promiseRestarted = download.start();
+ do_check_false(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ // For the following test, we rely on the network layer reporting its progress
+ // asynchronously. Otherwise, there is nothing stopping the restarted
+ // download from reaching the same progress as the first request already.
+ do_check_eq(download.hasProgress, false);
+ do_check_eq(download.progress, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_eq(download.currentBytes, 0);
+
+ // The second request is allowed to complete.
+ continueResponses();
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ yield promiseRestarted;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Calls the "cancel" method on a successful download.
+ */
+add_task(function* test_cancel_successful()
+{
+ let download = yield promiseStartDownload();
+ yield promiseDownloadStopped(download);
+
+ // The cancel method should succeed with no effect.
+ yield download.cancel();
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Calls the "cancel" method two times in a row.
+ */
+add_task(function* test_cancel_twice()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseAttempt = download.start();
+ do_check_false(download.stopped);
+
+ let promiseCancel1 = download.cancel();
+ do_check_true(download.canceled);
+ let promiseCancel2 = download.cancel();
+
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ // Both promises should now be resolved.
+ yield promiseCancel1;
+ yield promiseCancel2;
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Checks the "refresh" method for succeeded downloads.
+ */
+add_task(function* test_refresh_succeeded()
+{
+ let download = yield promiseStartDownload();
+ yield promiseDownloadStopped(download);
+
+ // The DownloadTarget properties should be the same after calling "refresh".
+ yield download.refresh();
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+
+ // If the file is removed, only the "exists" property should change, and the
+ // "size" property should keep its previous value.
+ yield OS.File.move(download.target.path, download.target.path + ".old");
+ yield download.refresh();
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, TEST_DATA_SHORT.length);
+
+ // The DownloadTarget properties should be restored when the file is put back.
+ yield OS.File.move(download.target.path + ".old", download.target.path);
+ yield download.refresh();
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Checks that a download cannot be restarted after the "finalize" method.
+ */
+add_task(function* test_finalize()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseFinalized = download.finalize();
+
+ try {
+ yield download.start();
+ do_throw("It should not be possible to restart after finalization.");
+ } catch (ex) { }
+
+ yield promiseFinalized;
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Checks that the "finalize" method can remove partially downloaded data.
+ */
+add_task(function* test_finalize_tryToKeepPartialData()
+{
+ // Check finalization without removing partial data.
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.finalize();
+
+ do_check_true(download.hasPartialData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ // Clean up.
+ yield download.removePartialData();
+
+ // Check finalization while removing partial data.
+ download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.finalize(true);
+
+ do_check_false(download.hasPartialData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Checks that whenSucceeded returns a promise that is resolved after a restart.
+ */
+add_task(function* test_whenSucceeded_after_restart()
+{
+ mustInterruptResponses();
+
+ let promiseSucceeded;
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can verify getting a reference before the first download attempt.
+ download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ promiseSucceeded = download.whenSucceeded();
+ download.start().catch(() => {});
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we cannot get the reference before the first attempt.
+ download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"));
+ promiseSucceeded = download.whenSucceeded();
+ }
+
+ // Cancel the first download attempt.
+ yield download.cancel();
+
+ // The second request is allowed to complete.
+ continueResponses();
+ download.start().catch(() => {});
+
+ // Wait for the download to finish by waiting on the whenSucceeded promise.
+ yield promiseSucceeded;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Ensures download error details are reported on network failures.
+ */
+add_task(function* test_error_source()
+{
+ let serverSocket = startFakeServer();
+ try {
+ let sourceUrl = "http://localhost:" + serverSocket.port + "/source.txt";
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload(sourceUrl);
+
+ do_check_true(download.error === null);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when reading from the source fails.
+ }
+
+ // Check the properties now that the download stopped.
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+ } finally {
+ serverSocket.close();
+ }
+});
+
+/**
+ * Ensures a download error is reported when receiving less bytes than what was
+ * specified in the Content-Length header.
+ */
+add_task(function* test_error_source_partial()
+{
+ let sourceUrl = httpUrl("shorter-than-content-length-http-1-1.txt");
+
+ let enforcePref = Services.prefs.getBoolPref("network.http.enforce-framing.http1");
+ Services.prefs.setBoolPref("network.http.enforce-framing.http1", true);
+
+ function cleanup() {
+ Services.prefs.setBoolPref("network.http.enforce-framing.http1", enforcePref);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload(sourceUrl);
+
+ do_check_true(download.error === null);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when reading from the source fails.
+ }
+
+ // Check the properties now that the download stopped.
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+ do_check_eq(download.error.result, Cr.NS_ERROR_NET_PARTIAL_TRANSFER);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Ensures download error details are reported on local writing failures.
+ */
+add_task(function* test_error_target()
+{
+ // Create a file without write access permissions before downloading.
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
+ try {
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: targetFile,
+ });
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload(null,
+ { targetFile: targetFile });
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseTargetFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when writing to the target fails.
+ }
+
+ // Check the properties now that the download stopped.
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseTargetFailed);
+ do_check_false(download.error.becauseSourceFailed);
+ } finally {
+ // Restore the default permissions to allow deleting the file on Windows.
+ if (targetFile.exists()) {
+ targetFile.permissions = FileUtils.PERMS_FILE;
+ targetFile.remove(false);
+ }
+ }
+});
+
+/**
+ * Restarts a failed download.
+ */
+add_task(function* test_error_restart()
+{
+ let download;
+
+ // Create a file without write access permissions before downloading.
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
+ try {
+ // Use DownloadCopySaver or DownloadLegacySaver based on the test run,
+ // specifying the target file we created.
+ if (!gUseLegacySaver) {
+ download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: targetFile,
+ });
+ download.start().catch(() => {});
+ } else {
+ download = yield promiseStartLegacyDownload(null,
+ { targetFile: targetFile });
+ }
+ yield promiseDownloadStopped(download);
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseTargetFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when writing to the target fails.
+ } finally {
+ // Restore the default permissions to allow deleting the file on Windows.
+ if (targetFile.exists()) {
+ targetFile.permissions = FileUtils.PERMS_FILE;
+
+ // Also for Windows, rename the file before deleting. This makes the
+ // current file name available immediately for a new file, while deleting
+ // in place prevents creation of a file with the same name for some time.
+ targetFile.moveTo(null, targetFile.leafName + ".delete.tmp");
+ targetFile.remove(false);
+ }
+ }
+
+ // Restart the download and wait for completion.
+ yield download.start();
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+ do_check_eq(download.progress, 100);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Executes download in both public and private modes.
+ */
+add_task(function* test_public_and_private()
+{
+ let sourcePath = "/test_public_and_private.txt";
+ let sourceUrl = httpUrl("test_public_and_private.txt");
+ let testCount = 0;
+
+ // Apply pref to allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ function cleanup() {
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+ Services.cookies.removeAll();
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ if (testCount == 0) {
+ // No cookies should exist for first public download.
+ do_check_false(aRequest.hasHeader("Cookie"));
+ aResponse.setHeader("Set-Cookie", "foobar=1", false);
+ testCount++;
+ } else if (testCount == 1) {
+ // The cookie should exists for second public download.
+ do_check_true(aRequest.hasHeader("Cookie"));
+ do_check_eq(aRequest.getHeader("Cookie"), "foobar=1");
+ testCount++;
+ } else if (testCount == 2) {
+ // No cookies should exist for first private download.
+ do_check_false(aRequest.hasHeader("Cookie"));
+ }
+ });
+
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ yield Downloads.fetch(sourceUrl, targetFile);
+ yield Downloads.fetch(sourceUrl, targetFile);
+
+ if (!gUseLegacySaver) {
+ let download = yield Downloads.createDownload({
+ source: { url: sourceUrl, isPrivate: true },
+ target: targetFile,
+ });
+ yield download.start();
+ } else {
+ let download = yield promiseStartLegacyDownload(sourceUrl,
+ { isPrivate: true });
+ yield promiseDownloadStopped(download);
+ }
+
+ cleanup();
+});
+
+/**
+ * Checks the startTime gets updated even after a restart.
+ */
+add_task(function* test_cancel_immediately_restart_and_check_startTime()
+{
+ let download = yield promiseStartDownload();
+
+ let startTime = download.startTime;
+ do_check_true(isValidDate(download.startTime));
+
+ yield download.cancel();
+ do_check_eq(download.startTime.getTime(), startTime.getTime());
+
+ // Wait for a timeout.
+ yield promiseTimeout(10);
+
+ yield download.start();
+ do_check_true(download.startTime.getTime() > startTime.getTime());
+});
+
+/**
+ * Executes download with content-encoding.
+ */
+add_task(function* test_with_content_encoding()
+{
+ let sourcePath = "/test_with_content_encoding.txt";
+ let sourceUrl = httpUrl("test_with_content_encoding.txt");
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Encoding", "gzip", false);
+ aResponse.setHeader("Content-Length",
+ "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
+ TEST_DATA_SHORT_GZIP_ENCODED.length);
+ });
+
+ let download = yield promiseStartDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ // Ensure the content matches the decoded test data.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+
+ cleanup();
+});
+
+/**
+ * Checks that the file is not decoded if the extension matches the encoding.
+ */
+add_task(function* test_with_content_encoding_ignore_extension()
+{
+ let sourcePath = "/test_with_content_encoding_ignore_extension.gz";
+ let sourceUrl = httpUrl("test_with_content_encoding_ignore_extension.gz");
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Encoding", "gzip", false);
+ aResponse.setHeader("Content-Length",
+ "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
+ TEST_DATA_SHORT_GZIP_ENCODED.length);
+ });
+
+ let download = yield promiseStartDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+ do_check_eq(download.target.size, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ // Ensure the content matches the encoded test data. We convert the data to a
+ // string before executing the content check.
+ yield promiseVerifyTarget(download.target,
+ String.fromCharCode.apply(String, TEST_DATA_SHORT_GZIP_ENCODED));
+
+ cleanup();
+});
+
+/**
+ * Cancels and restarts a download sequentially with content-encoding.
+ */
+add_task(function* test_cancel_midway_restart_with_content_encoding()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible_gzip.txt"));
+
+ // The first time, cancel the download midway.
+ let deferCancel = Promise.defer();
+ let onchange = function () {
+ if (!download.stopped && !download.canceled &&
+ download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length) {
+ deferCancel.resolve(download.cancel());
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ download.onchange = onchange;
+ onchange();
+
+ yield deferCancel.promise;
+
+ do_check_true(download.stopped);
+
+ // The second time, we'll provide the entire interruptible response.
+ continueResponses();
+ download.onchange = null;
+ yield download.start();
+
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Download with parental controls enabled.
+ */
+add_task(function* test_blocked_parental_controls()
+{
+ let blockFn = base => ({
+ shouldBlockForParentalControls: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload();
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload();
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByParentalControls);
+ do_check_true(download.error.becauseBlockedByParentalControls);
+ }
+
+ // Now that the download stopped, the target file should not exist.
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ cleanup();
+});
+
+/**
+ * Test a download that will be blocked by Windows parental controls by
+ * resulting in an HTTP status code of 450.
+ */
+add_task(function* test_blocked_parental_controls_httpstatus450()
+{
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ download = yield promiseNewDownload(httpUrl("parentalblocked.zip"));
+ yield download.start();
+ }
+ else {
+ download = yield promiseStartLegacyDownload(httpUrl("parentalblocked.zip"));
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByParentalControls);
+ do_check_true(download.error.becauseBlockedByParentalControls);
+ do_check_true(download.stopped);
+ }
+
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Download with runtime permissions
+ */
+add_task(function* test_blocked_runtime_permissions()
+{
+ let blockFn = base => ({
+ shouldBlockForRuntimePermissions: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload();
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload();
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByRuntimePermissions);
+ do_check_true(download.error.becauseBlockedByRuntimePermissions);
+ }
+
+ // Now that the download stopped, the target file should not exist.
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ cleanup();
+});
+
+/**
+ * Check that DownloadCopySaver can always retrieve the hash.
+ * DownloadLegacySaver can only retrieve the hash when
+ * nsIExternalHelperAppService is invoked.
+ */
+add_task(function* test_getSha256Hash()
+{
+ if (!gUseLegacySaver) {
+ let download = yield promiseStartDownload(httpUrl("source.txt"));
+ yield promiseDownloadStopped(download);
+ do_check_true(download.stopped);
+ do_check_eq(32, download.saver.getSha256Hash().length);
+ }
+});
+
+/**
+ * Create a download which will be reputation blocked.
+ *
+ * @param options
+ * {
+ * keepPartialData: bool,
+ * keepBlockedData: bool,
+ * }
+ * @return {Promise}
+ * @resolves The reputation blocked download.
+ * @rejects JavaScript exception.
+ */
+var promiseBlockedDownload = Task.async(function* (options) {
+ let blockFn = base => ({
+ shouldBlockForReputationCheck: () => Promise.resolve({
+ shouldBlock: true,
+ verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ }),
+ shouldKeepBlockedData: () => Promise.resolve(options.keepBlockedData),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+
+ try {
+ if (options.keepPartialData) {
+ download = yield promiseStartDownload_tryToKeepPartialData();
+ continueResponses();
+ } else if (gUseLegacySaver) {
+ download = yield promiseStartLegacyDownload();
+ } else {
+ download = yield promiseNewDownload();
+ yield download.start();
+ do_throw("The download should have blocked.");
+ }
+
+ yield promiseDownloadStopped(download);
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByReputationCheck);
+ do_check_eq(ex.reputationCheckVerdict,
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON);
+ do_check_true(download.error.becauseBlockedByReputationCheck);
+ do_check_eq(download.error.reputationCheckVerdict,
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON);
+ }
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ cleanup();
+ return download;
+});
+
+/**
+ * Checks that application reputation blocks the download and the target file
+ * does not exist.
+ */
+add_task(function* test_blocked_applicationReputation()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: false,
+ keepBlockedData: false,
+ });
+
+ // Now that the download is blocked, the target file should not exist.
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // There should also be no blocked data in this case
+ do_check_false(download.hasBlockedData);
+});
+
+/**
+ * Checks that if a download restarts while processing an application reputation
+ * request, the status is handled correctly.
+ */
+add_task(function* test_blocked_applicationReputation_race()
+{
+ let isFirstShouldBlockCall = true;
+
+ let blockFn = base => ({
+ shouldBlockForReputationCheck(download) {
+ if (isFirstShouldBlockCall) {
+ isFirstShouldBlockCall = false;
+
+ // 2. Cancel and restart the download before the first attempt has a
+ // chance to finish.
+ download.cancel();
+ download.removePartialData();
+ download.start();
+
+ // 3. Allow the first attempt to finish with a blocked response.
+ return Promise.resolve({
+ shouldBlock: true,
+ verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ });
+ }
+
+ // 4/5. Don't block the download the second time. The race condition would
+ // occur with the first attempt regardless of whether the second one
+ // is blocked, but not blocking here makes the test simpler.
+ return Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ });
+ },
+ shouldKeepBlockedData: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+
+ try {
+ // 1. Start the download and get a reference to the promise asociated with
+ // the first attempt, before allowing the response to continue.
+ download = yield promiseStartDownload_tryToKeepPartialData();
+ let firstAttempt = promiseDownloadStopped(download);
+ continueResponses();
+
+ // 4/5. Wait for the first attempt to be completed. The result of this
+ // should appear as a cancellation.
+ yield firstAttempt;
+
+ do_throw("The first attempt should have been canceled.");
+ } catch (ex) {
+ // The "becauseBlocked" property should be false.
+ if (!(ex instanceof Downloads.Error) || ex.becauseBlocked) {
+ throw ex;
+ }
+ }
+
+ // 6. Wait for the second attempt to be completed.
+ yield promiseDownloadStopped(download);
+
+ // 7. At this point, "hasBlockedData" should be false.
+ do_check_false(download.hasBlockedData);
+
+ cleanup();
+});
+
+/**
+ * Checks that application reputation blocks the download but maintains the
+ * blocked data, which will be deleted when the block is confirmed.
+ */
+add_task(function* test_blocked_applicationReputation_confirmBlock()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ do_check_true(download.hasBlockedData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ yield download.confirmBlock();
+
+ // After confirming the block the download should be in a failed state and
+ // have no downloaded data left on disk.
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Checks that application reputation blocks the download but maintains the
+ * blocked data, which will be used to complete the download when unblocking.
+ */
+add_task(function* test_blocked_applicationReputation_unblock()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ do_check_true(download.hasBlockedData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ yield download.unblock();
+
+ // After unblocking the download should have succeeded and be
+ // present at the final path.
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+
+ // The only indication the download was previously blocked is the
+ // existence of the error, so we make sure it's still set.
+ do_check_true(download.error instanceof Downloads.Error);
+ do_check_true(download.error.becauseBlocked);
+ do_check_true(download.error.becauseBlockedByReputationCheck);
+});
+
+/**
+ * Check that calling cancel on a blocked download will not cause errors
+ */
+add_task(function* test_blocked_applicationReputation_cancel()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ // This call should succeed on a blocked download.
+ yield download.cancel();
+
+ // Calling cancel should not have changed the current state, the download
+ // should still be blocked.
+ do_check_true(download.error.becauseBlockedByReputationCheck);
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_true(download.hasBlockedData);
+});
+
+/**
+ * Checks that unblock and confirmBlock cannot race on a blocked download
+ */
+add_task(function* test_blocked_applicationReputation_decisionRace()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ let unblockPromise = download.unblock();
+ let confirmBlockPromise = download.confirmBlock();
+
+ yield confirmBlockPromise.then(() => {
+ do_throw("confirmBlock should have failed.");
+ }, () => {});
+
+ yield unblockPromise;
+
+ // After unblocking the download should have succeeded and be
+ // present at the final path.
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_true(yield OS.File.exists(download.target.path));
+
+ download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ confirmBlockPromise = download.confirmBlock();
+ unblockPromise = download.unblock();
+
+ yield unblockPromise.then(() => {
+ do_throw("unblock should have failed.");
+ }, () => {});
+
+ yield confirmBlockPromise;
+
+ // After confirming the block the download should be in a failed state and
+ // have no downloaded data left on disk.
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Checks that unblocking a blocked download fails if the blocked data has been
+ * removed.
+ */
+add_task(function* test_blocked_applicationReputation_unblock()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ do_check_true(download.hasBlockedData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ // Remove the blocked data without telling the download.
+ yield OS.File.remove(download.target.partFilePath);
+
+ let unblockPromise = download.unblock();
+ yield unblockPromise.then(() => {
+ do_throw("unblock should have failed.");
+ }, () => {});
+
+ // Even though unblocking failed the download state should have been updated
+ // to reflect the lack of blocked data.
+ do_check_false(download.hasBlockedData);
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * download.showContainingDirectory() action
+ */
+add_task(function* test_showContainingDirectory() {
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ let download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: ""
+ });
+
+ let promiseDirectoryShown = waitForDirectoryShown();
+ yield download.showContainingDirectory();
+ let path = yield promiseDirectoryShown;
+ try {
+ new FileUtils.File(path);
+ do_throw("Should have failed because of an invalid path.");
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception)) {
+ throw ex;
+ }
+ // Invalid paths on Windows are reported with NS_ERROR_FAILURE,
+ // but with NS_ERROR_FILE_UNRECOGNIZED_PATH on Mac/Linux
+ let validResult = ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH ||
+ ex.result == Cr.NS_ERROR_FAILURE;
+ do_check_true(validResult);
+ }
+
+ download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: targetPath
+ });
+
+ promiseDirectoryShown = waitForDirectoryShown();
+ download.showContainingDirectory();
+ yield promiseDirectoryShown;
+});
+
+/**
+ * download.launch() action
+ */
+add_task(function* test_launch() {
+ let customLauncher = getTempFile("app-launcher");
+
+ // Test both with and without setting a custom application.
+ for (let launcherPath of [null, customLauncher.path]) {
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can test that file is not launched if download.succeeded is not set.
+ download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launcherPath: launcherPath,
+ launchWhenSucceeded: true
+ });
+
+ try {
+ yield download.launch();
+ do_throw("Can't launch download file as it has not completed yet");
+ } catch (ex) {
+ do_check_eq(ex.message,
+ "launch can only be called if the download succeeded");
+ }
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when
+ // it is created, thus we don't test calling "launch" before starting.
+ download = yield promiseStartLegacyDownload(
+ httpUrl("source.txt"),
+ { launcherPath: launcherPath,
+ launchWhenSucceeded: true });
+ yield promiseDownloadStopped(download);
+ }
+
+ do_check_true(download.launchWhenSucceeded);
+
+ let promiseFileLaunched = waitForFileLaunched();
+ download.launch();
+ let result = yield promiseFileLaunched;
+
+ // Verify that the results match the test case.
+ if (!launcherPath) {
+ // This indicates that the default handler has been chosen.
+ do_check_true(result === null);
+ } else {
+ // Check the nsIMIMEInfo instance that would have been used for launching.
+ do_check_eq(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
+ do_check_true(result.preferredApplicationHandler
+ .QueryInterface(Ci.nsILocalHandlerApp)
+ .executable.equals(customLauncher));
+ }
+ }
+});
+
+/**
+ * Test passing an invalid path as the launcherPath property.
+ */
+add_task(function* test_launcherPath_invalid() {
+ let download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ launcherPath: " "
+ });
+
+ let promiseDownloadLaunched = new Promise(resolve => {
+ let waitFn = base => ({
+ __proto__: base,
+ launchDownload() {
+ Integration.downloads.unregister(waitFn);
+ let superPromise = super.launchDownload(...arguments);
+ resolve(superPromise);
+ return superPromise;
+ },
+ });
+ Integration.downloads.register(waitFn);
+ });
+
+ yield download.start();
+ try {
+ download.launch();
+ yield promiseDownloadLaunched;
+ do_throw("Can't launch file with invalid custom launcher")
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception)) {
+ throw ex;
+ }
+ // Invalid paths on Windows are reported with NS_ERROR_FAILURE,
+ // but with NS_ERROR_FILE_UNRECOGNIZED_PATH on Mac/Linux
+ let validResult = ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH ||
+ ex.result == Cr.NS_ERROR_FAILURE;
+ do_check_true(validResult);
+ }
+});
+
+/**
+ * Tests that download.launch() is automatically called after
+ * the download finishes if download.launchWhenSucceeded = true
+ */
+add_task(function* test_launchWhenSucceeded() {
+ let customLauncher = getTempFile("app-launcher");
+
+ // Test both with and without setting a custom application.
+ for (let launcherPath of [null, customLauncher.path]) {
+ let promiseFileLaunched = waitForFileLaunched();
+
+ if (!gUseLegacySaver) {
+ let download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launchWhenSucceeded: true,
+ launcherPath: launcherPath,
+ });
+ yield download.start();
+ } else {
+ let download = yield promiseStartLegacyDownload(
+ httpUrl("source.txt"),
+ { launcherPath: launcherPath,
+ launchWhenSucceeded: true });
+ yield promiseDownloadStopped(download);
+ }
+
+ let result = yield promiseFileLaunched;
+
+ // Verify that the results match the test case.
+ if (!launcherPath) {
+ // This indicates that the default handler has been chosen.
+ do_check_true(result === null);
+ } else {
+ // Check the nsIMIMEInfo instance that would have been used for launching.
+ do_check_eq(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
+ do_check_true(result.preferredApplicationHandler
+ .QueryInterface(Ci.nsILocalHandlerApp)
+ .executable.equals(customLauncher));
+ }
+ }
+});
+
+/**
+ * Tests that the proper content type is set for a normal download.
+ */
+add_task(function* test_contentType() {
+ let download = yield promiseStartDownload(httpUrl("source.txt"));
+ yield promiseDownloadStopped(download);
+
+ do_check_eq("text/plain", download.contentType);
+});
+
+/**
+ * Tests that the serialization/deserialization of the startTime Date
+ * object works correctly.
+ */
+add_task(function* test_toSerializable_startTime()
+{
+ let download1 = yield promiseStartDownload(httpUrl("source.txt"));
+ yield promiseDownloadStopped(download1);
+
+ let serializable = download1.toSerializable();
+ let reserialized = JSON.parse(JSON.stringify(serializable));
+
+ let download2 = yield Downloads.createDownload(reserialized);
+
+ do_check_eq(download1.startTime.constructor.name, "Date");
+ do_check_eq(download2.startTime.constructor.name, "Date");
+ do_check_eq(download1.startTime.toJSON(), download2.startTime.toJSON());
+});
+
+/**
+ * Checks that downloads are added to browsing history when they start.
+ */
+add_task(function* test_history()
+{
+ mustInterruptResponses();
+
+ // We will wait for the visit to be notified during the download.
+ yield PlacesTestUtils.clearHistory();
+ let promiseVisit = promiseWaitForVisit(httpUrl("interruptible.txt"));
+
+ // Start a download that is not allowed to finish yet.
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ // The history notifications should be received before the download completes.
+ let [time, transitionType] = yield promiseVisit;
+ do_check_eq(time, download.startTime.getTime() * 1000);
+ do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+ // Restart and complete the download after clearing history.
+ yield PlacesTestUtils.clearHistory();
+ download.cancel();
+ continueResponses();
+ yield download.start();
+
+ // The restart should not have added a new history visit.
+ do_check_false(yield promiseIsURIVisited(httpUrl("interruptible.txt")));
+});
+
+/**
+ * Checks that downloads started by nsIHelperAppService are added to the
+ * browsing history when they start.
+ */
+add_task(function* test_history_tryToKeepPartialData()
+{
+ // We will wait for the visit to be notified during the download.
+ yield PlacesTestUtils.clearHistory();
+ let promiseVisit =
+ promiseWaitForVisit(httpUrl("interruptible_resumable.txt"));
+
+ // Start a download that is not allowed to finish yet.
+ let beforeStartTimeMs = Date.now();
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+
+ // The history notifications should be received before the download completes.
+ let [time, transitionType] = yield promiseVisit;
+ do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+ // The time set by nsIHelperAppService may be different than the start time in
+ // the download object, thus we only check that it is a meaningful time. Note
+ // that we subtract one second from the earliest time to account for rounding.
+ do_check_true(time >= beforeStartTimeMs * 1000 - 1000000);
+
+ // Complete the download before finishing the test.
+ continueResponses();
+ yield promiseDownloadStopped(download);
+});
+
+/**
+ * Tests that the temp download files are removed on exit and exiting private
+ * mode after they have been launched.
+ */
+add_task(function* test_launchWhenSucceeded_deleteTempFileOnExit() {
+ let customLauncherPath = getTempFile("app-launcher").path;
+ let autoDeleteTargetPathOne = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let autoDeleteTargetPathTwo = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let noAutoDeleteTargetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ let autoDeleteDownloadOne = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: autoDeleteTargetPathOne,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ yield autoDeleteDownloadOne.start();
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, true);
+ let autoDeleteDownloadTwo = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: autoDeleteTargetPathTwo,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ yield autoDeleteDownloadTwo.start();
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, false);
+ let noAutoDeleteDownload = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: noAutoDeleteTargetPath,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ yield noAutoDeleteDownload.start();
+
+ Services.prefs.clearUserPref(kDeleteTempFileOnExit);
+
+ do_check_true(yield OS.File.exists(autoDeleteTargetPathOne));
+ do_check_true(yield OS.File.exists(autoDeleteTargetPathTwo));
+ do_check_true(yield OS.File.exists(noAutoDeleteTargetPath));
+
+ // Simulate leaving private browsing mode
+ Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+ do_check_false(yield OS.File.exists(autoDeleteTargetPathOne));
+
+ // Simulate browser shutdown
+ let expire = Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Ci.nsIObserver);
+ expire.observe(null, "profile-before-change", null);
+ do_check_false(yield OS.File.exists(autoDeleteTargetPathTwo));
+ do_check_true(yield OS.File.exists(noAutoDeleteTargetPath));
+});
diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js
new file mode 100644
index 000000000..f322244c4
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -0,0 +1,843 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
+ "resource://testing-common/MockRegistrar.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsIExternalHelperAppService);
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+const ServerSocket = Components.Constructor(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init");
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream")
+
+XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService");
+
+const TEST_TARGET_FILE_NAME = "test-download.txt";
+const TEST_STORE_FILE_NAME = "test-downloads.json";
+
+const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
+
+const TEST_DATA_SHORT = "This test string is downloaded.";
+// Generate using gzipCompressString in TelemetryController.jsm.
+const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
+ 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 11, 201, 200, 44, 86, 40, 73, 45, 46, 81, 40, 46, 41, 202, 204
+];
+const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
+ 75, 87, 0, 114, 83, 242, 203, 243, 114, 242, 19, 83, 82, 83, 244, 0, 151, 222, 109, 43, 31, 0, 0, 0
+];
+const TEST_DATA_SHORT_GZIP_ENCODED =
+ TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
+
+/**
+ * All the tests are implemented with add_task, this starts them automatically.
+ */
+function run_test()
+{
+ do_get_profile();
+ run_next_test();
+}
+
+// Support functions
+
+/**
+ * HttpServer object initialized before tests start.
+ */
+var gHttpServer;
+
+/**
+ * Given a file name, returns a string containing an URI that points to the file
+ * on the currently running instance of the test HTTP server.
+ */
+function httpUrl(aFileName) {
+ return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
+ aFileName;
+}
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+var gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function () {
+ try {
+ file.remove(false)
+ } catch (e) {
+ if (!(e instanceof Components.Exception &&
+ (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
+ e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST ||
+ e.result == Cr.NS_ERROR_FILE_NOT_FOUND))) {
+ throw e;
+ }
+ // On Windows, we may get an access denied error if the file existed before,
+ // and was recently deleted.
+ // Don't bother checking file.exists() as that may also cause an access
+ // denied error.
+ }
+ });
+
+ return file;
+}
+
+/**
+ * Waits for pending events to be processed.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+function promiseExecuteSoon()
+{
+ let deferred = Promise.defer();
+ do_execute_soon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Waits for a pending events to be processed after a timeout.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+function promiseTimeout(aTime)
+{
+ let deferred = Promise.defer();
+ do_timeout(aTime, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Waits for a new history visit to be notified for the specified URI.
+ *
+ * @param aUrl
+ * String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
+ * @rejects Never.
+ */
+function promiseWaitForVisit(aUrl)
+{
+ let deferred = Promise.defer();
+
+ let uri = NetUtil.newURI(aUrl);
+
+ PlacesUtils.history.addObserver({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType, aGUID, aHidden) {
+ if (aURI.equals(uri)) {
+ PlacesUtils.history.removeObserver(this);
+ deferred.resolve([aTime, aTransitionType]);
+ }
+ },
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ }, false);
+
+ return deferred.promise;
+}
+
+/**
+ * Check browsing history to see whether the given URI has been visited.
+ *
+ * @param aUrl
+ * String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the URI has been visited.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aUrl) {
+ let deferred = Promise.defer();
+
+ PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
+ function (aURI, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Creates a new Download object, setting a temporary file as the target.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object.
+ * @rejects JavaScript exception.
+ */
+function promiseNewDownload(aSourceUrl) {
+ return Downloads.createDownload({
+ source: aSourceUrl || httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ });
+}
+
+/**
+ * Starts a new download using the nsIWebBrowserPersist interface, and controls
+ * it using the legacy nsITransfer interface.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ * @param aOptions
+ * An optional object used to control the behavior of this function.
+ * You may pass an object with a subset of the following fields:
+ * {
+ * isPrivate: Boolean indicating whether the download originated from a
+ * private window.
+ * targetFile: nsIFile for the target, or null to use a temporary file.
+ * outPersist: Receives a reference to the created nsIWebBrowserPersist
+ * instance.
+ * launchWhenSucceeded: Boolean indicating whether the target should
+ * be launched when it has completed successfully.
+ * launcherPath: String containing the path of the custom executable to
+ * use to launch the target of the download.
+ * }
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ * download through the legacy nsITransfer interface.
+ * @rejects Never. The current test fails in case of exceptions.
+ */
+function promiseStartLegacyDownload(aSourceUrl, aOptions) {
+ let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
+ let targetFile = (aOptions && aOptions.targetFile)
+ || getTempFile(TEST_TARGET_FILE_NAME);
+
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ if (aOptions) {
+ aOptions.outPersist = persist;
+ }
+
+ let fileExtension = null, mimeInfo = null;
+ let match = sourceURI.path.match(/\.([^.\/]+)$/);
+ if (match) {
+ fileExtension = match[1];
+ }
+
+ if (fileExtension) {
+ try {
+ mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension);
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+ } catch (ex) { }
+ }
+
+ if (aOptions && aOptions.launcherPath) {
+ do_check_true(mimeInfo != null);
+
+ let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath);
+
+ mimeInfo.preferredApplicationHandler = localHandlerApp;
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ }
+
+ if (aOptions && aOptions.launchWhenSucceeded) {
+ do_check_true(mimeInfo != null);
+
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ }
+
+ // Apply decoding if required by the "Content-Encoding" header.
+ persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
+ persist.persistFlags |=
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ let transfer = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+
+ let deferred = Promise.defer();
+
+ Downloads.getList(Downloads.ALL).then(function (aList) {
+ // Temporarily register a view that will get notified when the download we
+ // are controlling becomes visible in the list of downloads.
+ aList.addView({
+ onDownloadAdded: function (aDownload) {
+ aList.removeView(this).then(null, do_report_unexpected_exception);
+
+ // Remove the download to keep the list empty for the next test. This
+ // also allows the caller to register the "onchange" event directly.
+ let promise = aList.remove(aDownload);
+
+ // When the download object is ready, make it available to the caller.
+ promise.then(() => deferred.resolve(aDownload),
+ do_report_unexpected_exception);
+ },
+ }).then(null, do_report_unexpected_exception);
+
+ let isPrivate = aOptions && aOptions.isPrivate;
+
+ // Initialize the components so they reference each other. This will cause
+ // the Download object to be created and added to the public downloads.
+ transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
+ null, persist, isPrivate);
+ persist.progressListener = transfer;
+
+ // Start the actual download process.
+ persist.savePrivacyAwareURI(sourceURI, null, null, 0, null, null, targetFile,
+ isPrivate);
+ }.bind(this)).then(null, do_report_unexpected_exception);
+
+ return deferred.promise;
+}
+
+/**
+ * Starts a new download using the nsIHelperAppService interface, and controls
+ * it using the legacy nsITransfer interface. The source of the download will
+ * be "interruptible_resumable.txt" and partially downloaded data will be kept.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("interruptible_resumable.txt").
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ * download through the legacy nsITransfer interface.
+ * @rejects Never. The current test fails in case of exceptions.
+ */
+function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
+ let sourceURI = NetUtil.newURI(aSourceUrl ||
+ httpUrl("interruptible_resumable.txt"));
+
+ let deferred = Promise.defer();
+
+ Downloads.getList(Downloads.PUBLIC).then(function (aList) {
+ // Temporarily register a view that will get notified when the download we
+ // are controlling becomes visible in the list of downloads.
+ aList.addView({
+ onDownloadAdded: function (aDownload) {
+ aList.removeView(this).then(null, do_report_unexpected_exception);
+
+ // Remove the download to keep the list empty for the next test. This
+ // also allows the caller to register the "onchange" event directly.
+ let promise = aList.remove(aDownload);
+
+ // When the download object is ready, make it available to the caller.
+ promise.then(() => deferred.resolve(aDownload),
+ do_report_unexpected_exception);
+ },
+ }).then(null, do_report_unexpected_exception);
+
+ let channel = NetUtil.newChannel({
+ uri: sourceURI,
+ loadUsingSystemPrincipal: true
+ });
+
+ // Start the actual download process.
+ channel.asyncOpen2({
+ contentListener: null,
+
+ onStartRequest: function (aRequest, aContext)
+ {
+ let requestChannel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.contentListener = gExternalHelperAppService.doContent(
+ requestChannel.contentType, aRequest, null, true);
+ this.contentListener.onStartRequest(aRequest, aContext);
+ },
+
+ onStopRequest: function (aRequest, aContext, aStatusCode)
+ {
+ this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+
+ onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
+ aCount)
+ {
+ this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount);
+ },
+ });
+ }.bind(this)).then(null, do_report_unexpected_exception);
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for a download to reach half of its progress, in case it has not
+ * reached the expected progress already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has reached half of its progress.
+ * @rejects Never.
+ */
+function promiseDownloadMidway(aDownload) {
+ let deferred = Promise.defer();
+
+ // Wait for the download to reach half of its progress.
+ let onchange = function () {
+ if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
+ aDownload.onchange = null;
+ deferred.resolve();
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ aDownload.onchange = onchange;
+ onchange();
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+ if (!aDownload.stopped) {
+ // The download is in progress, wait for the current attempt to finish and
+ // report any errors that may occur.
+ return aDownload.start();
+ }
+
+ if (aDownload.succeeded) {
+ return Promise.resolve();
+ }
+
+ // The download failed or was canceled.
+ return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+/**
+ * Returns a new public or private DownloadList object.
+ *
+ * @param aIsPrivate
+ * True for the private list, false or undefined for the public list.
+ *
+ * @return {Promise}
+ * @resolves The newly created DownloadList object.
+ * @rejects JavaScript exception.
+ */
+function promiseNewList(aIsPrivate)
+{
+ // We need to clear all the internal state for the list and summary objects,
+ // since all the objects are interdependent internally.
+ Downloads._promiseListsInitialized = null;
+ Downloads._lists = {};
+ Downloads._summaries = {};
+
+ return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
+}
+
+/**
+ * Ensures that the given file contents are equal to the given string.
+ *
+ * @param aPath
+ * String containing the path of the file whose contents should be
+ * verified.
+ * @param aExpectedContents
+ * String containing the octets that are expected in the file.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects Never.
+ */
+function promiseVerifyContents(aPath, aExpectedContents)
+{
+ return Task.spawn(function* () {
+ let file = new FileUtils.File(aPath);
+
+ if (!(yield OS.File.exists(aPath))) {
+ do_throw("File does not exist: " + aPath);
+ }
+
+ if ((yield OS.File.stat(aPath)).size == 0) {
+ do_throw("File is empty: " + aPath);
+ }
+
+ let deferred = Promise.defer();
+ NetUtil.asyncFetch(
+ { uri: NetUtil.newURI(file), loadUsingSystemPrincipal: true },
+ function(aInputStream, aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ let contents = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ if (contents.length > TEST_DATA_SHORT.length * 2 ||
+ /[^\x20-\x7E]/.test(contents)) {
+ // Do not print the entire content string to the test log.
+ do_check_eq(contents.length, aExpectedContents.length);
+ do_check_true(contents == aExpectedContents);
+ } else {
+ // Print the string if it is short and made of printable characters.
+ do_check_eq(contents, aExpectedContents);
+ }
+ deferred.resolve();
+ });
+
+ yield deferred.promise;
+ });
+}
+
+/**
+ * Starts a socket listener that closes each incoming connection.
+ *
+ * @returns nsIServerSocket that listens for connections. Call its "close"
+ * method to stop listening and free the server port.
+ */
+function startFakeServer()
+{
+ let serverSocket = new ServerSocket(-1, true, -1);
+ serverSocket.asyncListen({
+ onSocketAccepted: function (aServ, aTransport) {
+ aTransport.close(Cr.NS_BINDING_ABORTED);
+ },
+ onStopListening: function () { },
+ });
+ return serverSocket;
+}
+
+/**
+ * This is an internal reference that should not be used directly by tests.
+ */
+var _gDeferResponses = Promise.defer();
+
+/**
+ * Ensures that all the interruptible requests started after this function is
+ * called won't complete until the continueResponses function is called.
+ *
+ * Normally, the internal HTTP server returns all the available data as soon as
+ * a request is received. In order for some requests to be served one part at a
+ * time, special interruptible handlers are registered on the HTTP server. This
+ * allows testing events or actions that need to happen in the middle of a
+ * download.
+ *
+ * For example, the handler accessible at the httpUri("interruptible.txt")
+ * address returns the TEST_DATA_SHORT text, then it may block until the
+ * continueResponses method is called. At this point, the handler sends the
+ * TEST_DATA_SHORT text again to complete the response.
+ *
+ * If an interruptible request is started before the function is called, it may
+ * or may not be blocked depending on the actual sequence of events.
+ */
+function mustInterruptResponses()
+{
+ // If there are pending blocked requests, allow them to complete. This is
+ // done to prevent requests from being blocked forever, but should not affect
+ // the test logic, since previously started requests should not be monitored
+ // on the client side anymore.
+ _gDeferResponses.resolve();
+
+ do_print("Interruptible responses will be blocked midway.");
+ _gDeferResponses = Promise.defer();
+}
+
+/**
+ * Allows all the current and future interruptible requests to complete.
+ */
+function continueResponses()
+{
+ do_print("Interruptible responses are now allowed to continue.");
+ _gDeferResponses.resolve();
+}
+
+/**
+ * Registers an interruptible response handler.
+ *
+ * @param aPath
+ * Path passed to nsIHttpServer.registerPathHandler.
+ * @param aFirstPartFn
+ * This function is called when the response is received, with the
+ * aRequest and aResponse arguments of the server.
+ * @param aSecondPartFn
+ * This function is called with the aRequest and aResponse arguments of
+ * the server, when the continueResponses function is called.
+ */
+function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
+{
+ gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
+ do_print("Interruptible request started.");
+
+ // Process the first part of the response.
+ aResponse.processAsync();
+ aFirstPartFn(aRequest, aResponse);
+
+ // Wait on the current deferred object, then finish the request.
+ _gDeferResponses.promise.then(function RIH_onSuccess() {
+ aSecondPartFn(aRequest, aResponse);
+ aResponse.finish();
+ do_print("Interruptible request finished.");
+ }).then(null, Cu.reportError);
+ });
+}
+
+/**
+ * Ensure the given date object is valid.
+ *
+ * @param aDate
+ * The date object to be checked. This value can be null.
+ */
+function isValidDate(aDate) {
+ return aDate && aDate.getTime && !isNaN(aDate.getTime());
+}
+
+/**
+ * Position of the first byte served by the "interruptible_resumable.txt"
+ * handler during the most recent response.
+ */
+var gMostRecentFirstBytePos;
+
+// Initialization functions common to all tests
+
+add_task(function test_common_initialize()
+{
+ // Start the HTTP server.
+ gHttpServer = new HttpServer();
+ gHttpServer.registerDirectory("/", do_get_file("../data"));
+ gHttpServer.start(-1);
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ // Ensure all the pending HTTP requests have a chance to finish.
+ continueResponses();
+ // Stop the HTTP server, calling resolve when it's done.
+ gHttpServer.stop(resolve);
+ });
+ });
+
+ // Cache locks might prevent concurrent requests to the same resource, and
+ // this may block tests that use the interruptible handlers.
+ Services.prefs.setBoolPref("browser.cache.disk.enable", false);
+ Services.prefs.setBoolPref("browser.cache.memory.enable", false);
+ do_register_cleanup(function () {
+ Services.prefs.clearUserPref("browser.cache.disk.enable");
+ Services.prefs.clearUserPref("browser.cache.memory.enable");
+ });
+
+ registerInterruptibleHandler("/interruptible.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
+ false);
+ aResponse.write(TEST_DATA_SHORT);
+ }, function secondPart(aRequest, aResponse) {
+ aResponse.write(TEST_DATA_SHORT);
+ });
+
+ registerInterruptibleHandler("/interruptible_resumable.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ // Determine if only part of the data should be sent.
+ let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
+ if (aRequest.hasHeader("Range")) {
+ var matches = aRequest.getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
+ var lastBytePos = (matches[2] === undefined) ? data.length - 1
+ : matches[2];
+ if (firstBytePos >= data.length) {
+ aResponse.setStatusLine(aRequest.httpVersion, 416,
+ "Requested Range Not Satisfiable");
+ aResponse.setHeader("Content-Range", "*/" + data.length, false);
+ aResponse.finish();
+ return;
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
+ aResponse.setHeader("Content-Range", firstBytePos + "-" +
+ lastBytePos + "/" +
+ data.length, false);
+
+ data = data.substring(firstBytePos, lastBytePos + 1);
+
+ gMostRecentFirstBytePos = firstBytePos;
+ } else {
+ gMostRecentFirstBytePos = 0;
+ }
+
+ aResponse.setHeader("Content-Length", "" + data.length, false);
+
+ aResponse.write(data.substring(0, data.length / 2));
+
+ // Store the second part of the data on the response object, so that it
+ // can be used by the secondPart function.
+ aResponse.secondPartData = data.substring(data.length / 2);
+ }, function secondPart(aRequest, aResponse) {
+ aResponse.write(aResponse.secondPartData);
+ });
+
+ registerInterruptibleHandler("/interruptible_gzip.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Encoding", "gzip", false);
+ aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
+ TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
+ }, function secondPart(aRequest, aResponse) {
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
+ TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
+ });
+
+ gHttpServer.registerPathHandler("/shorter-than-content-length-http-1-1.txt",
+ function (aRequest, aResponse) {
+ aResponse.processAsync();
+ aResponse.setStatusLine("1.1", 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
+ false);
+ aResponse.write(TEST_DATA_SHORT);
+ aResponse.finish();
+ });
+
+ // This URL will emulate being blocked by Windows Parental controls
+ gHttpServer.registerPathHandler("/parentalblocked.zip",
+ function (aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 450,
+ "Blocked by Windows Parental Controls");
+ });
+
+ // During unit tests, most of the functions that require profile access or
+ // operating system features will be disabled. Individual tests may override
+ // them again to check for specific behaviors.
+ Integration.downloads.register(base => ({
+ __proto__: base,
+ loadPublicDownloadListFromStore: () => Promise.resolve(),
+ shouldKeepBlockedData: () => Promise.resolve(false),
+ shouldBlockForParentalControls: () => Promise.resolve(false),
+ shouldBlockForRuntimePermissions: () => Promise.resolve(false),
+ shouldBlockForReputationCheck: () => Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ }),
+ confirmLaunchExecutable: () => Promise.resolve(),
+ launchFile: () => Promise.resolve(),
+ showContainingDirectory: () => Promise.resolve(),
+ // This flag allows re-enabling the default observers during their tests.
+ allowObservers: false,
+ addListObservers() {
+ return this.allowObservers ? super.addListObservers(...arguments)
+ : Promise.resolve();
+ },
+ // This flag allows re-enabling the download directory logic for its tests.
+ _allowDirectories: false,
+ set allowDirectories(value) {
+ this._allowDirectories = value;
+ // We have to invalidate the previously computed directory path.
+ this._downloadsDirectory = null;
+ },
+ _getDirectory(name) {
+ return super._getDirectory(this._allowDirectories ? name : "TmpD");
+ },
+ }));
+
+ // Make sure that downloads started using nsIExternalHelperAppService are
+ // saved to disk without asking for a destination interactively.
+ let mock = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+ promptForSaveToFileAsync(aLauncher,
+ aWindowContext,
+ aDefaultFileName,
+ aSuggestedFileExtension,
+ aForcePrompt) {
+ // The dialog should create the empty placeholder file.
+ let file = getTempFile(TEST_TARGET_FILE_NAME);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ aLauncher.saveDestinationAvailable(file);
+ },
+ };
+
+ let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", mock);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(cid);
+ });
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
new file mode 100644
index 000000000..6e32c63d3
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the main download interfaces using DownloadCopySaver.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadError",
+ "resource://gre/modules/DownloadCore.jsm");
+
+// Execution of common tests
+
+var gUseLegacySaver = false;
+
+var scriptFile = do_get_file("common_test_Download.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
+
+// Tests
+
+/**
+ * Tests the DownloadError object.
+ */
+add_task(function test_DownloadError()
+{
+ let error = new DownloadError({ result: Cr.NS_ERROR_NOT_RESUMABLE,
+ message: "Not resumable."});
+ do_check_eq(error.result, Cr.NS_ERROR_NOT_RESUMABLE);
+ do_check_eq(error.message, "Not resumable.");
+ do_check_false(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_false(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ error = new DownloadError({ message: "Unknown error."});
+ do_check_eq(error.result, Cr.NS_ERROR_FAILURE);
+ do_check_eq(error.message, "Unknown error.");
+
+ error = new DownloadError({ result: Cr.NS_ERROR_NOT_RESUMABLE });
+ do_check_eq(error.result, Cr.NS_ERROR_NOT_RESUMABLE);
+ do_check_true(error.message.indexOf("Exception") > 0);
+
+ // becauseSourceFailed will be set, but not the unknown property.
+ error = new DownloadError({ message: "Unknown error.",
+ becauseSourceFailed: true,
+ becauseUnknown: true });
+ do_check_true(error.becauseSourceFailed);
+ do_check_false("becauseUnknown" in error);
+
+ error = new DownloadError({ result: Cr.NS_ERROR_MALFORMED_URI,
+ inferCause: true });
+ do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
+ do_check_true(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_false(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ // This test does not set inferCause, so becauseSourceFailed will not be set.
+ error = new DownloadError({ result: Cr.NS_ERROR_MALFORMED_URI });
+ do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
+ do_check_false(error.becauseSourceFailed);
+
+ error = new DownloadError({ result: Cr.NS_ERROR_FILE_INVALID_PATH,
+ inferCause: true });
+ do_check_eq(error.result, Cr.NS_ERROR_FILE_INVALID_PATH);
+ do_check_false(error.becauseSourceFailed);
+ do_check_true(error.becauseTargetFailed);
+ do_check_false(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ error = new DownloadError({ becauseBlocked: true });
+ do_check_eq(error.message, "Download blocked.");
+ do_check_false(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_true(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ error = new DownloadError({ becauseBlockedByParentalControls: true });
+ do_check_eq(error.message, "Download blocked.");
+ do_check_false(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_true(error.becauseBlocked);
+ do_check_true(error.becauseBlockedByParentalControls);
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js b/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
new file mode 100644
index 000000000..388870f00
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
@@ -0,0 +1,701 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadImport object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+ "resource://gre/modules/DownloadImport.jsm");
+
+// Importable states
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+// Non importable states
+const DOWNLOAD_FAILED = 2;
+const DOWNLOAD_CANCELED = 3;
+const DOWNLOAD_BLOCKED_PARENTAL = 6;
+const DOWNLOAD_SCANNING = 7;
+const DOWNLOAD_DIRTY = 8;
+const DOWNLOAD_BLOCKED_POLICY = 9;
+
+// The TEST_DATA_TAINTED const is a version of TEST_DATA_SHORT in which the
+// beginning of the data was changed (with the TEST_DATA_REPLACEMENT value).
+// We use this to test that the entityID is properly imported and the download
+// can be resumed from where it was paused.
+// For simplification purposes, the test requires that TEST_DATA_SHORT and
+// TEST_DATA_TAINTED have the same length.
+const TEST_DATA_REPLACEMENT = "-changed- ";
+const TEST_DATA_TAINTED = TEST_DATA_REPLACEMENT +
+ TEST_DATA_SHORT.substr(TEST_DATA_REPLACEMENT.length);
+const TEST_DATA_LENGTH = TEST_DATA_SHORT.length;
+
+// The length of the partial file that we'll write to disk as an existing
+// ongoing download.
+const TEST_DATA_PARTIAL_LENGTH = TEST_DATA_REPLACEMENT.length;
+
+// The value of the "maxBytes" column stored in the DB about the downloads.
+// It's intentionally different than TEST_DATA_LENGTH to test that each value
+// is seen when expected.
+const MAXBYTES_IN_DB = TEST_DATA_LENGTH - 10;
+
+var gDownloadsRowToImport;
+var gDownloadsRowNonImportable;
+
+/**
+ * Creates a database with an empty moz_downloads table and leaves an
+ * open connection to it.
+ *
+ * @param aPath
+ * String containing the path of the database file to be created.
+ * @param aSchemaVersion
+ * Number with the version of the database schema to set.
+ *
+ * @return {Promise}
+ * @resolves The open connection to the database.
+ * @rejects If an error occurred during the database creation.
+ */
+function promiseEmptyDatabaseConnection({aPath, aSchemaVersion}) {
+ return Task.spawn(function* () {
+ let connection = yield Sqlite.openConnection({ path: aPath });
+
+ yield connection.execute("CREATE TABLE moz_downloads ("
+ + "id INTEGER PRIMARY KEY,"
+ + "name TEXT,"
+ + "source TEXT,"
+ + "target TEXT,"
+ + "tempPath TEXT,"
+ + "startTime INTEGER,"
+ + "endTime INTEGER,"
+ + "state INTEGER,"
+ + "referrer TEXT,"
+ + "entityID TEXT,"
+ + "currBytes INTEGER NOT NULL DEFAULT 0,"
+ + "maxBytes INTEGER NOT NULL DEFAULT -1,"
+ + "mimeType TEXT,"
+ + "preferredApplication TEXT,"
+ + "preferredAction INTEGER NOT NULL DEFAULT 0,"
+ + "autoResume INTEGER NOT NULL DEFAULT 0,"
+ + "guid TEXT)");
+
+ yield connection.setSchemaVersion(aSchemaVersion);
+
+ return connection;
+ });
+}
+
+/**
+ * Inserts a new entry in the database with the given columns' values.
+ *
+ * @param aConnection
+ * The database connection.
+ * @param aDownloadRow
+ * An object representing the values for each column of the row
+ * being inserted.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects If there's an error inserting the row.
+ */
+function promiseInsertRow(aConnection, aDownloadRow) {
+ // We can't use the aDownloadRow obj directly in the execute statement
+ // because the obj bind code in Sqlite.jsm doesn't allow objects
+ // with extra properties beyond those being binded. So we might as well
+ // use an array as it is simpler.
+ let values = [
+ aDownloadRow.source, aDownloadRow.target, aDownloadRow.tempPath,
+ aDownloadRow.startTime.getTime() * 1000, aDownloadRow.state,
+ aDownloadRow.referrer, aDownloadRow.entityID, aDownloadRow.maxBytes,
+ aDownloadRow.mimeType, aDownloadRow.preferredApplication,
+ aDownloadRow.preferredAction, aDownloadRow.autoResume
+ ];
+
+ return aConnection.execute("INSERT INTO moz_downloads ("
+ + "name, source, target, tempPath, startTime,"
+ + "endTime, state, referrer, entityID, currBytes,"
+ + "maxBytes, mimeType, preferredApplication,"
+ + "preferredAction, autoResume, guid)"
+ + "VALUES ("
+ + "'', ?, ?, ?, ?, " // name,
+ + "0, ?, ?, ?, 0, " // endTime, currBytes
+ + " ?, ?, ?, " //
+ + " ?, ?, '')", // and guid are not imported
+ values);
+}
+
+/**
+ * Retrieves the number of rows in the moz_downloads table of the
+ * database.
+ *
+ * @param aConnection
+ * The database connection.
+ *
+ * @return {Promise}
+ * @resolves With the number of rows.
+ * @rejects Never.
+ */
+function promiseTableCount(aConnection) {
+ return aConnection.execute("SELECT COUNT(*) FROM moz_downloads")
+ .then(res => res[0].getResultByName("COUNT(*)"))
+ .then(null, Cu.reportError);
+}
+
+/**
+ * Briefly opens a network channel to a given URL to retrieve
+ * the entityID of this url, as generated by the network code.
+ *
+ * @param aUrl
+ * The URL to retrieve the entityID.
+ *
+ * @return {Promise}
+ * @resolves The EntityID of the given URL.
+ * @rejects When there's a problem accessing the URL.
+ */
+function promiseEntityID(aUrl) {
+ let deferred = Promise.defer();
+ let entityID = "";
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(aUrl),
+ loadUsingSystemPrincipal: true
+ });
+
+ channel.asyncOpen2({
+ onStartRequest: function (aRequest) {
+ if (aRequest instanceof Ci.nsIResumableChannel) {
+ entityID = aRequest.entityID;
+ }
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest: function (aRequest, aContext, aStatusCode) {
+ if (aStatusCode == Cr.NS_BINDING_ABORTED) {
+ deferred.resolve(entityID);
+ } else {
+ deferred.reject("Unexpected status code received");
+ }
+ },
+
+ onDataAvailable: function () {}
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Gets a file path to a temporary writeable download target, in the
+ * correct format as expected to be stored in the downloads database,
+ * which is file:///absolute/path/to/file
+ *
+ * @param aLeafName
+ * A hint leaf name for the file.
+ *
+ * @return String The path to the download target.
+ */
+function getDownloadTarget(aLeafName) {
+ return NetUtil.newURI(getTempFile(aLeafName)).spec;
+}
+
+/**
+ * Generates a temporary partial file to use as an in-progress
+ * download. The file is written to disk with a part of the total expected
+ * download content pre-written.
+ *
+ * @param aLeafName
+ * A hint leaf name for the file.
+ * @param aTainted
+ * A boolean value. When true, the partial content of the file
+ * will be different from the expected content of the original source
+ * file. See the declaration of TEST_DATA_TAINTED for more information.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes, and returns a string with the path
+ * to the generated file.
+ * @rejects If there's an error writing the file.
+ */
+function getPartialFile(aLeafName, aTainted = false) {
+ let tempDownload = getTempFile(aLeafName);
+ let partialContent = aTainted
+ ? TEST_DATA_TAINTED.substr(0, TEST_DATA_PARTIAL_LENGTH)
+ : TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH);
+
+ return OS.File.writeAtomic(tempDownload.path, partialContent,
+ { tmpPath: tempDownload.path + ".tmp",
+ flush: true })
+ .then(() => tempDownload.path);
+}
+
+/**
+ * Generates a Date object to be used as the startTime for the download rows
+ * in the DB. A date that is obviously different from the current time is
+ * generated to make sure this stored data and a `new Date()` can't collide.
+ *
+ * @param aOffset
+ * A offset from the base generated date is used to differentiate each
+ * row in the database.
+ *
+ * @return A Date object.
+ */
+function getStartTime(aOffset) {
+ return new Date(1000000 + (aOffset * 10000));
+}
+
+/**
+ * Performs various checks on an imported Download object to make sure
+ * all properties are properly set as expected from the import procedure.
+ *
+ * @param aDownload
+ * The Download object to be checked.
+ * @param aDownloadRow
+ * An object that represents a row from the original database table,
+ * with extra properties describing expected values that are not
+ * explictly part of the database.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes
+ * @rejects Never
+ */
+function checkDownload(aDownload, aDownloadRow) {
+ return Task.spawn(function*() {
+ do_check_eq(aDownload.source.url, aDownloadRow.source);
+ do_check_eq(aDownload.source.referrer, aDownloadRow.referrer);
+
+ do_check_eq(aDownload.target.path,
+ NetUtil.newURI(aDownloadRow.target)
+ .QueryInterface(Ci.nsIFileURL).file.path);
+
+ do_check_eq(aDownload.target.partFilePath, aDownloadRow.tempPath);
+
+ if (aDownloadRow.expectedResume) {
+ do_check_true(!aDownload.stopped || aDownload.succeeded);
+ yield promiseDownloadStopped(aDownload);
+
+ do_check_true(aDownload.succeeded);
+ do_check_eq(aDownload.progress, 100);
+ // If the download has resumed, a new startTime will be set.
+ // By calling toJSON we're also testing that startTime is a Date object.
+ do_check_neq(aDownload.startTime.toJSON(),
+ aDownloadRow.startTime.toJSON());
+ } else {
+ do_check_false(aDownload.succeeded);
+ do_check_eq(aDownload.startTime.toJSON(),
+ aDownloadRow.startTime.toJSON());
+ }
+
+ do_check_eq(aDownload.stopped, true);
+
+ let serializedSaver = aDownload.saver.toSerializable();
+ if (typeof(serializedSaver) == "object") {
+ do_check_eq(serializedSaver.type, "copy");
+ } else {
+ do_check_eq(serializedSaver, "copy");
+ }
+
+ if (aDownloadRow.entityID) {
+ do_check_eq(aDownload.saver.entityID, aDownloadRow.entityID);
+ }
+
+ do_check_eq(aDownload.currentBytes, aDownloadRow.expectedCurrentBytes);
+ do_check_eq(aDownload.totalBytes, aDownloadRow.expectedTotalBytes);
+
+ if (aDownloadRow.expectedContent) {
+ let fileToCheck = aDownloadRow.expectedResume
+ ? aDownload.target.path
+ : aDownload.target.partFilePath;
+ yield promiseVerifyContents(fileToCheck, aDownloadRow.expectedContent);
+ }
+
+ do_check_eq(aDownload.contentType, aDownloadRow.expectedContentType);
+ do_check_eq(aDownload.launcherPath, aDownloadRow.preferredApplication);
+
+ do_check_eq(aDownload.launchWhenSucceeded,
+ aDownloadRow.preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+ });
+}
+
+// Preparation tasks
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * be imported by the import procedure.
+ */
+add_task(function* prepareDownloadsToImport() {
+
+ let sourceUrl = httpUrl("source.txt");
+ let sourceEntityId = yield promiseEntityID(sourceUrl);
+
+ gDownloadsRowToImport = [
+ // Paused download with autoResume and a partial file. By
+ // setting the correct entityID the download can resume from
+ // where it stopped, and to test that this works properly we
+ // intentionally set different data in the beginning of the
+ // partial file to make sure it was not replaced.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress1.txt"),
+ tempPath: yield getPartialFile("inprogress1.txt.part", true),
+ startTime: getStartTime(1),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer1"),
+ entityID: sourceEntityId,
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType1",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication1",
+ autoResume: 1,
+
+ // Even though the information stored in the DB said
+ // maxBytes was MAXBYTES_IN_DB, the download turned out to be
+ // a different length. Here we make sure the totalBytes property
+ // was correctly set with the actual value. The same consideration
+ // applies to the contentType.
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_TAINTED,
+ },
+
+ // Paused download with autoResume and a partial file,
+ // but missing entityID. This means that the download will
+ // start from beginning, and the entire original content of the
+ // source file should replace the different data that was stored
+ // in the partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress2.txt"),
+ tempPath: yield getPartialFile("inprogress2.txt.part", true),
+ startTime: getStartTime(2),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer2"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType2",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication2",
+ autoResume: 1,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Paused download with no autoResume and a partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress3.txt"),
+ tempPath: yield getPartialFile("inprogress3.txt.part"),
+ startTime: getStartTime(3),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer3"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType3",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication3",
+ autoResume: 0,
+
+ // Since this download has not been resumed, the actual data
+ // about its total size and content type is not known.
+ // Therefore, we're going by the information imported from the DB.
+ expectedCurrentBytes: TEST_DATA_PARTIAL_LENGTH,
+ expectedTotalBytes: MAXBYTES_IN_DB,
+ expectedResume: false,
+ expectedContentType: "mimeType3",
+ expectedContent: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH),
+ },
+
+ // Paused download with autoResume and no partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress4.txt"),
+ tempPath: "",
+ startTime: getStartTime(4),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer4"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+ preferredApplication: "prerredApplication4",
+ autoResume: 1,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Paused download with no autoResume and no partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress5.txt"),
+ tempPath: "",
+ startTime: getStartTime(5),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer4"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useSystemDefault,
+ preferredApplication: "prerredApplication5",
+ autoResume: 0,
+
+ expectedCurrentBytes: 0,
+ expectedTotalBytes: MAXBYTES_IN_DB,
+ expectedResume: false,
+ expectedContentType: "text/plain",
+ },
+
+ // Queued download with no autoResume and no partial file.
+ // Even though autoResume=0, queued downloads always autoResume.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress6.txt"),
+ tempPath: "",
+ startTime: getStartTime(6),
+ state: DOWNLOAD_QUEUED,
+ referrer: httpUrl("referrer6"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+ preferredApplication: "prerredApplication6",
+ autoResume: 0,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Notstarted download with no autoResume and no partial file.
+ // Even though autoResume=0, notstarted downloads always autoResume.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress7.txt"),
+ tempPath: "",
+ startTime: getStartTime(7),
+ state: DOWNLOAD_NOTSTARTED,
+ referrer: httpUrl("referrer7"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+ preferredApplication: "prerredApplication7",
+ autoResume: 0,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Downloading download with no autoResume and a partial file.
+ // Even though autoResume=0, downloading downloads always autoResume.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress8.txt"),
+ tempPath: yield getPartialFile("inprogress8.txt.part", true),
+ startTime: getStartTime(8),
+ state: DOWNLOAD_DOWNLOADING,
+ referrer: httpUrl("referrer8"),
+ entityID: sourceEntityId,
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication8",
+ autoResume: 0,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_TAINTED
+ },
+ ];
+});
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * *not* be imported by the import procedure.
+ */
+add_task(function* prepareNonImportableDownloads()
+{
+ gDownloadsRowNonImportable = [
+ // Download with no source (should never happen in normal circumstances).
+ {
+ source: "",
+ target: "nonimportable1.txt",
+ tempPath: "",
+ startTime: getStartTime(1),
+ state: DOWNLOAD_PAUSED,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType1",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication1",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_FAILED
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable2.txt",
+ tempPath: "",
+ startTime: getStartTime(2),
+ state: DOWNLOAD_FAILED,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType2",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication2",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_CANCELED
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable3.txt",
+ tempPath: "",
+ startTime: getStartTime(3),
+ state: DOWNLOAD_CANCELED,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType3",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication3",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_BLOCKED_PARENTAL
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable4.txt",
+ tempPath: "",
+ startTime: getStartTime(4),
+ state: DOWNLOAD_BLOCKED_PARENTAL,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType4",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication4",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_SCANNING
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable5.txt",
+ tempPath: "",
+ startTime: getStartTime(5),
+ state: DOWNLOAD_SCANNING,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType5",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication5",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_DIRTY
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable6.txt",
+ tempPath: "",
+ startTime: getStartTime(6),
+ state: DOWNLOAD_DIRTY,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType6",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication6",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_BLOCKED_POLICY
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable7.txt",
+ tempPath: "",
+ startTime: getStartTime(7),
+ state: DOWNLOAD_BLOCKED_POLICY,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType7",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication7",
+ autoResume: 1
+ },
+ ];
+});
+
+// Test
+
+/**
+ * Creates a temporary Sqlite database with download data and perform an
+ * import of that data to the new Downloads API to verify that the import
+ * worked correctly.
+ */
+add_task(function* test_downloadImport()
+{
+ let connection = null;
+ let downloadsSqlite = getTempFile("downloads.sqlite").path;
+
+ try {
+ // Set up the database.
+ connection = yield promiseEmptyDatabaseConnection({
+ aPath: downloadsSqlite,
+ aSchemaVersion: 9
+ });
+
+ // Insert both the importable and non-importable
+ // downloads together.
+ for (let downloadRow of gDownloadsRowToImport) {
+ yield promiseInsertRow(connection, downloadRow);
+ }
+
+ for (let downloadRow of gDownloadsRowNonImportable) {
+ yield promiseInsertRow(connection, downloadRow);
+ }
+
+ // Check that every item was inserted.
+ do_check_eq((yield promiseTableCount(connection)),
+ gDownloadsRowToImport.length +
+ gDownloadsRowNonImportable.length);
+ } finally {
+ // Close the connection so that DownloadImport can open it.
+ yield connection.close();
+ }
+
+ // Import items.
+ let list = yield promiseNewList(false);
+ yield new DownloadImport(list, downloadsSqlite).import();
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, gDownloadsRowToImport.length);
+
+ for (let i = 0; i < gDownloadsRowToImport.length; i++) {
+ yield checkDownload(items[i], gDownloadsRowToImport[i]);
+ }
+})
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
new file mode 100644
index 000000000..31dd7c7a4
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
@@ -0,0 +1,432 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadIntegration object.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Notifies the prompt observers and verify the expected downloads count.
+ *
+ * @param aIsPrivate
+ * Flag to know is test private observers.
+ * @param aExpectedCount
+ * the expected downloads count for quit and offline observers.
+ * @param aExpectedPBCount
+ * the expected downloads count for private browsing observer.
+ */
+function notifyPromptObservers(aIsPrivate, aExpectedCount, aExpectedPBCount) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+
+ // Notify quit application requested observer.
+ DownloadIntegration._testPromptDownloads = -1;
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+ do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
+
+ // Notify offline requested observer.
+ DownloadIntegration._testPromptDownloads = -1;
+ Services.obs.notifyObservers(cancelQuit, "offline-requested", null);
+ do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
+
+ if (aIsPrivate) {
+ // Notify last private browsing requested observer.
+ DownloadIntegration._testPromptDownloads = -1;
+ Services.obs.notifyObservers(cancelQuit, "last-pb-context-exiting", null);
+ do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedPBCount);
+ }
+
+ delete DownloadIntegration._testPromptDownloads;
+}
+
+// Tests
+
+/**
+ * Allows re-enabling the real download directory logic during one test.
+ */
+function allowDirectoriesInTest() {
+ DownloadIntegration.allowDirectories = true;
+ function cleanup() {
+ DownloadIntegration.allowDirectories = false;
+ }
+ do_register_cleanup(cleanup);
+ return cleanup;
+}
+
+XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://mozapps/locale/downloads/downloads.properties");
+});
+
+/**
+ * Tests that getSystemDownloadsDirectory returns an existing directory or
+ * creates a new directory depending on the platform. Instead of the real
+ * directory, this test is executed in the temporary directory so we can safely
+ * delete the created folder to check whether it is created again.
+ */
+add_task(function* test_getSystemDownloadsDirectory_exists_or_creates()
+{
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let downloadDir;
+
+ // OSX / Linux / Windows but not XP/2k
+ if (Services.appinfo.OS == "Darwin" ||
+ Services.appinfo.OS == "Linux" ||
+ (Services.appinfo.OS == "WINNT" &&
+ parseFloat(Services.sysinfo.getProperty("version")) >= 6)) {
+ downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ do_check_eq(downloadDir, tempDir.path);
+ do_check_true(yield OS.File.exists(downloadDir));
+
+ let info = yield OS.File.stat(downloadDir);
+ do_check_true(info.isDir);
+ } else {
+ let targetPath = OS.Path.join(tempDir.path,
+ gStringBundle.GetStringFromName("downloadsFolder"));
+ try {
+ yield OS.File.removeEmptyDir(targetPath);
+ } catch (e) {}
+ downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ do_check_eq(downloadDir, targetPath);
+ do_check_true(yield OS.File.exists(downloadDir));
+
+ let info = yield OS.File.stat(downloadDir);
+ do_check_true(info.isDir);
+ yield OS.File.removeEmptyDir(targetPath);
+ }
+});
+
+/**
+ * Tests that the real directory returned by getSystemDownloadsDirectory is not
+ * the one that is used during unit tests. Since this is the actual downloads
+ * directory of the operating system, we don't try to delete it afterwards.
+ */
+add_task(function* test_getSystemDownloadsDirectory_real()
+{
+ let fakeDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+
+ let cleanup = allowDirectoriesInTest();
+ let realDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ cleanup();
+
+ do_check_neq(fakeDownloadDir, realDownloadDir);
+});
+
+/**
+ * Tests that the getPreferredDownloadsDirectory returns a valid download
+ * directory string path.
+ */
+add_task(function* test_getPreferredDownloadsDirectory()
+{
+ let cleanupDirectories = allowDirectoriesInTest();
+
+ let folderListPrefName = "browser.download.folderList";
+ let dirPrefName = "browser.download.dir";
+ function cleanupPrefs() {
+ Services.prefs.clearUserPref(folderListPrefName);
+ Services.prefs.clearUserPref(dirPrefName);
+ }
+ do_register_cleanup(cleanupPrefs);
+
+ // Should return the system downloads directory.
+ Services.prefs.setIntPref(folderListPrefName, 1);
+ let systemDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, systemDir);
+
+ // Should return the desktop directory.
+ Services.prefs.setIntPref(folderListPrefName, 0);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, Services.dirsvc.get("Desk", Ci.nsIFile).path);
+
+ // Should return the system downloads directory because the dir preference
+ // is not set.
+ Services.prefs.setIntPref(folderListPrefName, 2);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, systemDir);
+
+ // Should return the directory which is listed in the dir preference.
+ let time = (new Date()).getTime();
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append(time);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, tempDir.path);
+ do_check_true(yield OS.File.exists(downloadDir));
+ yield OS.File.removeEmptyDir(tempDir.path);
+
+ // Should return the system downloads directory beacause the path is invalid
+ // in the dir preference.
+ tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append("dir_not_exist");
+ tempDir.append(time);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_eq(downloadDir, systemDir);
+
+ // Should return the system downloads directory because the folderList
+ // preference is invalid
+ Services.prefs.setIntPref(folderListPrefName, 999);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_eq(downloadDir, systemDir);
+
+ cleanupPrefs();
+ cleanupDirectories();
+});
+
+/**
+ * Tests that the getTemporaryDownloadsDirectory returns a valid download
+ * directory string path.
+ */
+add_task(function* test_getTemporaryDownloadsDirectory()
+{
+ let cleanup = allowDirectoriesInTest();
+
+ let downloadDir = yield DownloadIntegration.getTemporaryDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+
+ if ("nsILocalFileMac" in Ci) {
+ let preferredDownloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_eq(downloadDir, preferredDownloadDir);
+ } else {
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ do_check_eq(downloadDir, tempDir.path);
+ }
+
+ cleanup();
+});
+
+// Tests DownloadObserver
+
+/**
+ * Re-enables the default observers for the following tests.
+ *
+ * This takes effect the first time a DownloadList object is created, and lasts
+ * until this test file has completed.
+ */
+add_task(function* test_observers_setup()
+{
+ DownloadIntegration.allowObservers = true;
+ do_register_cleanup(function () {
+ DownloadIntegration.allowObservers = false;
+ });
+});
+
+/**
+ * Tests notifications prompts when observers are notified if there are public
+ * and private active downloads.
+ */
+add_task(function* test_notifications()
+{
+ for (let isPrivate of [false, true]) {
+ mustInterruptResponses();
+
+ let list = yield promiseNewList(isPrivate);
+ let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt1 = download1.start();
+ let promiseAttempt2 = download2.start();
+ download3.start().catch(() => {});
+
+ // Add downloads to list.
+ yield list.add(download1);
+ yield list.add(download2);
+ yield list.add(download3);
+ // Cancel third download
+ yield download3.cancel();
+
+ notifyPromptObservers(isPrivate, 2, 2);
+
+ // Allow the downloads to complete.
+ continueResponses();
+ yield promiseAttempt1;
+ yield promiseAttempt2;
+
+ // Clean up.
+ yield list.remove(download1);
+ yield list.remove(download2);
+ yield list.remove(download3);
+ }
+});
+
+/**
+ * Tests that notifications prompts observers are not notified if there are no
+ * public or private active downloads.
+ */
+add_task(function* test_no_notifications()
+{
+ for (let isPrivate of [false, true]) {
+ let list = yield promiseNewList(isPrivate);
+ let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ download1.start().catch(() => {});
+ download2.start().catch(() => {});
+
+ // Add downloads to list.
+ yield list.add(download1);
+ yield list.add(download2);
+
+ yield download1.cancel();
+ yield download2.cancel();
+
+ notifyPromptObservers(isPrivate, 0, 0);
+
+ // Clean up.
+ yield list.remove(download1);
+ yield list.remove(download2);
+ }
+});
+
+/**
+ * Tests notifications prompts when observers are notified if there are public
+ * and private active downloads at the same time.
+ */
+add_task(function* test_mix_notifications()
+{
+ mustInterruptResponses();
+
+ let publicList = yield promiseNewList();
+ let privateList = yield Downloads.getList(Downloads.PRIVATE);
+ let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt1 = download1.start();
+ let promiseAttempt2 = download2.start();
+
+ // Add downloads to lists.
+ yield publicList.add(download1);
+ yield privateList.add(download2);
+
+ notifyPromptObservers(true, 2, 1);
+
+ // Allow the downloads to complete.
+ continueResponses();
+ yield promiseAttempt1;
+ yield promiseAttempt2;
+
+ // Clean up.
+ yield publicList.remove(download1);
+ yield privateList.remove(download2);
+});
+
+/**
+ * Tests suspending and resuming as well as going offline and then online again.
+ * The downloads should stop when suspending and start again when resuming.
+ */
+add_task(function* test_suspend_resume()
+{
+ // The default wake delay is 10 seconds, so set the wake delay to be much
+ // faster for these tests.
+ Services.prefs.setIntPref("browser.download.manager.resumeOnWakeDelay", 5);
+
+ let addDownload = function(list)
+ {
+ return Task.spawn(function* () {
+ let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ download.start().catch(() => {});
+ list.add(download);
+ return download;
+ });
+ }
+
+ let publicList = yield promiseNewList();
+ let privateList = yield promiseNewList(true);
+
+ let download1 = yield addDownload(publicList);
+ let download2 = yield addDownload(publicList);
+ let download3 = yield addDownload(privateList);
+ let download4 = yield addDownload(privateList);
+ let download5 = yield addDownload(publicList);
+
+ // First, check that the downloads are all canceled when going to sleep.
+ Services.obs.notifyObservers(null, "sleep_notification", null);
+ do_check_true(download1.canceled);
+ do_check_true(download2.canceled);
+ do_check_true(download3.canceled);
+ do_check_true(download4.canceled);
+ do_check_true(download5.canceled);
+
+ // Remove a download. It should not be started again.
+ publicList.remove(download5);
+ do_check_true(download5.canceled);
+
+ // When waking up again, the downloads start again after the wake delay. To be
+ // more robust, don't check after a delay but instead just wait for the
+ // downloads to finish.
+ Services.obs.notifyObservers(null, "wake_notification", null);
+ yield download1.whenSucceeded();
+ yield download2.whenSucceeded();
+ yield download3.whenSucceeded();
+ yield download4.whenSucceeded();
+
+ // Downloads should no longer be canceled. However, as download5 was removed
+ // from the public list, it will not be restarted.
+ do_check_false(download1.canceled);
+ do_check_true(download5.canceled);
+
+ // Create four new downloads and check for going offline and then online again.
+
+ download1 = yield addDownload(publicList);
+ download2 = yield addDownload(publicList);
+ download3 = yield addDownload(privateList);
+ download4 = yield addDownload(privateList);
+
+ // Going offline should cancel the downloads.
+ Services.obs.notifyObservers(null, "network:offline-about-to-go-offline", null);
+ do_check_true(download1.canceled);
+ do_check_true(download2.canceled);
+ do_check_true(download3.canceled);
+ do_check_true(download4.canceled);
+
+ // Going back online should start the downloads again.
+ Services.obs.notifyObservers(null, "network:offline-status-changed", "online");
+ yield download1.whenSucceeded();
+ yield download2.whenSucceeded();
+ yield download3.whenSucceeded();
+ yield download4.whenSucceeded();
+
+ Services.prefs.clearUserPref("browser.download.manager.resumeOnWakeDelay");
+});
+
+/**
+ * Tests both the downloads list and the in-progress downloads are clear when
+ * private browsing observer is notified.
+ */
+add_task(function* test_exit_private_browsing()
+{
+ mustInterruptResponses();
+
+ let privateList = yield promiseNewList(true);
+ let download1 = yield promiseNewDownload(httpUrl("source.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt1 = download1.start();
+ download2.start();
+
+ // Add downloads to list.
+ yield privateList.add(download1);
+ yield privateList.add(download2);
+
+ // Complete the download.
+ yield promiseAttempt1;
+
+ do_check_eq((yield privateList.getAll()).length, 2);
+
+ // Simulate exiting the private browsing.
+ yield new Promise(resolve => {
+ DownloadIntegration._testResolveClearPrivateList = resolve;
+ Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+ });
+ delete DownloadIntegration._testResolveClearPrivateList;
+
+ do_check_eq((yield privateList.getAll()).length, 0);
+
+ continueResponses();
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
new file mode 100644
index 000000000..dc6c18623
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
@@ -0,0 +1,17 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the integration with legacy interfaces for downloads.
+ */
+
+"use strict";
+
+// Execution of common tests
+
+var gUseLegacySaver = true;
+
+var scriptFile = do_get_file("common_test_Download.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
new file mode 100644
index 000000000..71e880741
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
@@ -0,0 +1,564 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadList object.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * @note Expiration ignores any visit added in the last 7 days, but it's
+ * better be safe against DST issues, by going back one day more.
+ */
+function getExpirablePRTime()
+{
+ let dateObj = new Date();
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ dateObj = new Date(dateObj.getTime() - 8 * 86400000);
+ return dateObj.getTime() * 1000;
+}
+
+/**
+ * Adds an expirable history visit for a download.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @rejects JavaScript exception.
+ */
+function promiseExpirableDownloadVisit(aSourceUrl)
+{
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.updatePlaces(
+ {
+ uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
+ visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ visitDate: getExpirablePRTime(),
+ }]
+ },
+ {
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ let ex = new Components.Exception("Unexpected error in adding visits.",
+ aResultCode);
+ deferred.reject(ex);
+ },
+ handleResult: function () {},
+ handleCompletion: function handleCompletion() {
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
+
+// Tests
+
+/**
+ * Checks the testing mechanism used to build different download lists.
+ */
+add_task(function* test_construction()
+{
+ let downloadListOne = yield promiseNewList();
+ let downloadListTwo = yield promiseNewList();
+ let privateDownloadListOne = yield promiseNewList(true);
+ let privateDownloadListTwo = yield promiseNewList(true);
+
+ do_check_neq(downloadListOne, downloadListTwo);
+ do_check_neq(privateDownloadListOne, privateDownloadListTwo);
+ do_check_neq(downloadListOne, privateDownloadListOne);
+});
+
+/**
+ * Checks the methods to add and retrieve items from the list.
+ */
+add_task(function* test_add_getAll()
+{
+ let list = yield promiseNewList();
+
+ let downloadOne = yield promiseNewDownload();
+ yield list.add(downloadOne);
+
+ let itemsOne = yield list.getAll();
+ do_check_eq(itemsOne.length, 1);
+ do_check_eq(itemsOne[0], downloadOne);
+
+ let downloadTwo = yield promiseNewDownload();
+ yield list.add(downloadTwo);
+
+ let itemsTwo = yield list.getAll();
+ do_check_eq(itemsTwo.length, 2);
+ do_check_eq(itemsTwo[0], downloadOne);
+ do_check_eq(itemsTwo[1], downloadTwo);
+
+ // The first snapshot should not have been modified.
+ do_check_eq(itemsOne.length, 1);
+});
+
+/**
+ * Checks the method to remove items from the list.
+ */
+add_task(function* test_remove()
+{
+ let list = yield promiseNewList();
+
+ yield list.add(yield promiseNewDownload());
+ yield list.add(yield promiseNewDownload());
+
+ let items = yield list.getAll();
+ yield list.remove(items[0]);
+
+ // Removing an item that was never added should not raise an error.
+ yield list.remove(yield promiseNewDownload());
+
+ items = yield list.getAll();
+ do_check_eq(items.length, 1);
+});
+
+/**
+ * Tests that the "add", "remove", and "getAll" methods on the global
+ * DownloadCombinedList object combine the contents of the global DownloadList
+ * objects for public and private downloads.
+ */
+add_task(function* test_DownloadCombinedList_add_remove_getAll()
+{
+ let publicList = yield promiseNewList();
+ let privateList = yield Downloads.getList(Downloads.PRIVATE);
+ let combinedList = yield Downloads.getList(Downloads.ALL);
+
+ let publicDownload = yield promiseNewDownload();
+ let privateDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+
+ yield publicList.add(publicDownload);
+ yield privateList.add(privateDownload);
+
+ do_check_eq((yield combinedList.getAll()).length, 2);
+
+ yield combinedList.remove(publicDownload);
+ yield combinedList.remove(privateDownload);
+
+ do_check_eq((yield combinedList.getAll()).length, 0);
+
+ yield combinedList.add(publicDownload);
+ yield combinedList.add(privateDownload);
+
+ do_check_eq((yield publicList.getAll()).length, 1);
+ do_check_eq((yield privateList.getAll()).length, 1);
+ do_check_eq((yield combinedList.getAll()).length, 2);
+
+ yield publicList.remove(publicDownload);
+ yield privateList.remove(privateDownload);
+
+ do_check_eq((yield combinedList.getAll()).length, 0);
+});
+
+/**
+ * Checks that views receive the download add and remove notifications, and that
+ * adding and removing views works as expected, both for a normal and a combined
+ * list.
+ */
+add_task(function* test_notifications_add_remove()
+{
+ for (let isCombined of [false, true]) {
+ // Force creating a new list for both the public and combined cases.
+ let list = yield promiseNewList();
+ if (isCombined) {
+ list = yield Downloads.getList(Downloads.ALL);
+ }
+
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ // Check that we receive add notifications for existing elements.
+ let addNotifications = 0;
+ let viewOne = {
+ onDownloadAdded: function (aDownload) {
+ // The first download to be notified should be the first that was added.
+ if (addNotifications == 0) {
+ do_check_eq(aDownload, downloadOne);
+ } else if (addNotifications == 1) {
+ do_check_eq(aDownload, downloadTwo);
+ }
+ addNotifications++;
+ },
+ };
+ yield list.addView(viewOne);
+ do_check_eq(addNotifications, 2);
+
+ // Check that we receive add notifications for new elements.
+ yield list.add(yield promiseNewDownload());
+ do_check_eq(addNotifications, 3);
+
+ // Check that we receive remove notifications.
+ let removeNotifications = 0;
+ let viewTwo = {
+ onDownloadRemoved: function (aDownload) {
+ do_check_eq(aDownload, downloadOne);
+ removeNotifications++;
+ },
+ };
+ yield list.addView(viewTwo);
+ yield list.remove(downloadOne);
+ do_check_eq(removeNotifications, 1);
+
+ // We should not receive remove notifications after the view is removed.
+ yield list.removeView(viewTwo);
+ yield list.remove(downloadTwo);
+ do_check_eq(removeNotifications, 1);
+
+ // We should not receive add notifications after the view is removed.
+ yield list.removeView(viewOne);
+ yield list.add(yield promiseNewDownload());
+ do_check_eq(addNotifications, 3);
+ }
+});
+
+/**
+ * Checks that views receive the download change notifications, both for a
+ * normal and a combined list.
+ */
+add_task(function* test_notifications_change()
+{
+ for (let isCombined of [false, true]) {
+ // Force creating a new list for both the public and combined cases.
+ let list = yield promiseNewList();
+ if (isCombined) {
+ list = yield Downloads.getList(Downloads.ALL);
+ }
+
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ // Check that we receive change notifications.
+ let receivedOnDownloadChanged = false;
+ yield list.addView({
+ onDownloadChanged: function (aDownload) {
+ do_check_eq(aDownload, downloadOne);
+ receivedOnDownloadChanged = true;
+ },
+ });
+ yield downloadOne.start();
+ do_check_true(receivedOnDownloadChanged);
+
+ // We should not receive change notifications after a download is removed.
+ receivedOnDownloadChanged = false;
+ yield list.remove(downloadTwo);
+ yield downloadTwo.start();
+ do_check_false(receivedOnDownloadChanged);
+ }
+});
+
+/**
+ * Checks that the reference to "this" is correct in the view callbacks.
+ */
+add_task(function* test_notifications_this()
+{
+ let list = yield promiseNewList();
+
+ // Check that we receive change notifications.
+ let receivedOnDownloadAdded = false;
+ let receivedOnDownloadChanged = false;
+ let receivedOnDownloadRemoved = false;
+ let view = {
+ onDownloadAdded: function () {
+ do_check_eq(this, view);
+ receivedOnDownloadAdded = true;
+ },
+ onDownloadChanged: function () {
+ // Only do this check once.
+ if (!receivedOnDownloadChanged) {
+ do_check_eq(this, view);
+ receivedOnDownloadChanged = true;
+ }
+ },
+ onDownloadRemoved: function () {
+ do_check_eq(this, view);
+ receivedOnDownloadRemoved = true;
+ },
+ };
+ yield list.addView(view);
+
+ let download = yield promiseNewDownload();
+ yield list.add(download);
+ yield download.start();
+ yield list.remove(download);
+
+ // Verify that we executed the checks.
+ do_check_true(receivedOnDownloadAdded);
+ do_check_true(receivedOnDownloadChanged);
+ do_check_true(receivedOnDownloadRemoved);
+});
+
+/**
+ * Checks that download is removed on history expiration.
+ */
+add_task(function* test_history_expiration()
+{
+ mustInterruptResponses();
+
+ function cleanup() {
+ Services.prefs.clearUserPref("places.history.expiration.max_pages");
+ }
+ do_register_cleanup(cleanup);
+
+ // Set max pages to 0 to make the download expire.
+ Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
+
+ let list = yield promiseNewList();
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));
+
+ let deferred = Promise.defer();
+ let removeNotifications = 0;
+ let downloadView = {
+ onDownloadRemoved: function (aDownload) {
+ if (++removeNotifications == 2) {
+ deferred.resolve();
+ }
+ },
+ };
+ yield list.addView(downloadView);
+
+ // Work with one finished download and one canceled download.
+ yield downloadOne.start();
+ downloadTwo.start().catch(() => {});
+ yield downloadTwo.cancel();
+
+ // We must replace the visits added while executing the downloads with visits
+ // that are older than 7 days, otherwise they will not be expired.
+ yield PlacesTestUtils.clearHistory();
+ yield promiseExpirableDownloadVisit();
+ yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));
+
+ // After clearing history, we can add the downloads to be removed to the list.
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ // Force a history expiration.
+ Cc["@mozilla.org/places/expiration;1"]
+ .getService(Ci.nsIObserver).observe(null, "places-debug-start-expiration", -1);
+
+ // Wait for both downloads to be removed.
+ yield deferred.promise;
+
+ cleanup();
+});
+
+/**
+ * Checks all downloads are removed after clearing history.
+ */
+add_task(function* test_history_clear()
+{
+ let list = yield promiseNewList();
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield promiseNewDownload();
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ let deferred = Promise.defer();
+ let removeNotifications = 0;
+ let downloadView = {
+ onDownloadRemoved: function (aDownload) {
+ if (++removeNotifications == 2) {
+ deferred.resolve();
+ }
+ },
+ };
+ yield list.addView(downloadView);
+
+ yield downloadOne.start();
+ yield downloadTwo.start();
+
+ yield PlacesTestUtils.clearHistory();
+
+ // Wait for the removal notifications that may still be pending.
+ yield deferred.promise;
+});
+
+/**
+ * Tests the removeFinished method to ensure that it only removes
+ * finished downloads.
+ */
+add_task(function* test_removeFinished()
+{
+ let list = yield promiseNewList();
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield promiseNewDownload();
+ let downloadThree = yield promiseNewDownload();
+ let downloadFour = yield promiseNewDownload();
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+ yield list.add(downloadThree);
+ yield list.add(downloadFour);
+
+ let deferred = Promise.defer();
+ let removeNotifications = 0;
+ let downloadView = {
+ onDownloadRemoved: function (aDownload) {
+ do_check_true(aDownload == downloadOne ||
+ aDownload == downloadTwo ||
+ aDownload == downloadThree);
+ do_check_true(removeNotifications < 3);
+ if (++removeNotifications == 3) {
+ deferred.resolve();
+ }
+ },
+ };
+ yield list.addView(downloadView);
+
+ // Start three of the downloads, but don't start downloadTwo, then set
+ // downloadFour to have partial data. All downloads except downloadFour
+ // should be removed.
+ yield downloadOne.start();
+ yield downloadThree.start();
+ yield downloadFour.start();
+ downloadFour.hasPartialData = true;
+
+ list.removeFinished();
+ yield deferred.promise;
+
+ let downloads = yield list.getAll()
+ do_check_eq(downloads.length, 1);
+});
+
+/**
+ * Tests the global DownloadSummary objects for the public, private, and
+ * combined download lists.
+ */
+add_task(function* test_DownloadSummary()
+{
+ mustInterruptResponses();
+
+ let publicList = yield promiseNewList();
+ let privateList = yield Downloads.getList(Downloads.PRIVATE);
+
+ let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC);
+ let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE);
+ let combinedSummary = yield Downloads.getSummary(Downloads.ALL);
+
+ // Add a public download that has succeeded.
+ let succeededPublicDownload = yield promiseNewDownload();
+ yield succeededPublicDownload.start();
+ yield publicList.add(succeededPublicDownload);
+
+ // Add a public download that has been canceled midway.
+ let canceledPublicDownload =
+ yield promiseNewDownload(httpUrl("interruptible.txt"));
+ canceledPublicDownload.start().catch(() => {});
+ yield promiseDownloadMidway(canceledPublicDownload);
+ yield canceledPublicDownload.cancel();
+ yield publicList.add(canceledPublicDownload);
+
+ // Add a public download that is in progress.
+ let inProgressPublicDownload =
+ yield promiseNewDownload(httpUrl("interruptible.txt"));
+ inProgressPublicDownload.start().catch(() => {});
+ yield promiseDownloadMidway(inProgressPublicDownload);
+ yield publicList.add(inProgressPublicDownload);
+
+ // Add a private download that is in progress.
+ let inProgressPrivateDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("interruptible.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ inProgressPrivateDownload.start().catch(() => {});
+ yield promiseDownloadMidway(inProgressPrivateDownload);
+ yield privateList.add(inProgressPrivateDownload);
+
+ // Verify that the summary includes the total number of bytes and the
+ // currently transferred bytes only for the downloads that are not stopped.
+ // For simplicity, we assume that after a download is added to the list, its
+ // current state is immediately propagated to the summary object, which is
+ // true in the current implementation, though it is not guaranteed as all the
+ // download operations may happen asynchronously.
+ do_check_false(publicSummary.allHaveStopped);
+ do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ do_check_false(privateSummary.allHaveStopped);
+ do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ do_check_false(combinedSummary.allHaveStopped);
+ do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4);
+ do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2);
+
+ yield inProgressPublicDownload.cancel();
+
+ // Stopping the download should have excluded it from the summary.
+ do_check_true(publicSummary.allHaveStopped);
+ do_check_eq(publicSummary.progressTotalBytes, 0);
+ do_check_eq(publicSummary.progressCurrentBytes, 0);
+
+ do_check_false(privateSummary.allHaveStopped);
+ do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ do_check_false(combinedSummary.allHaveStopped);
+ do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ yield inProgressPrivateDownload.cancel();
+
+ // All the downloads should be stopped now.
+ do_check_true(publicSummary.allHaveStopped);
+ do_check_eq(publicSummary.progressTotalBytes, 0);
+ do_check_eq(publicSummary.progressCurrentBytes, 0);
+
+ do_check_true(privateSummary.allHaveStopped);
+ do_check_eq(privateSummary.progressTotalBytes, 0);
+ do_check_eq(privateSummary.progressCurrentBytes, 0);
+
+ do_check_true(combinedSummary.allHaveStopped);
+ do_check_eq(combinedSummary.progressTotalBytes, 0);
+ do_check_eq(combinedSummary.progressCurrentBytes, 0);
+});
+
+/**
+ * Checks that views receive the summary change notification. This is tested on
+ * the combined summary when adding a public download, as we assume that if we
+ * pass the test in this case we will also pass it in the others.
+ */
+add_task(function* test_DownloadSummary_notifications()
+{
+ let list = yield promiseNewList();
+ let summary = yield Downloads.getSummary(Downloads.ALL);
+
+ let download = yield promiseNewDownload();
+ yield list.add(download);
+
+ // Check that we receive change notifications.
+ let receivedOnSummaryChanged = false;
+ yield summary.addView({
+ onSummaryChanged: function () {
+ receivedOnSummaryChanged = true;
+ },
+ });
+ yield download.start();
+ do_check_true(receivedOnSummaryChanged);
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
new file mode 100644
index 000000000..3a23dfbe3
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -0,0 +1,315 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadStore object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
+ "resource://gre/modules/DownloadStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+
+/**
+ * Returns a new DownloadList object with an associated DownloadStore.
+ *
+ * @param aStorePath
+ * String pointing to the file to be associated with the DownloadStore,
+ * or undefined to use a non-existing temporary file. In this case, the
+ * temporary file is deleted when the test file execution finishes.
+ *
+ * @return {Promise}
+ * @resolves Array [ Newly created DownloadList , associated DownloadStore ].
+ * @rejects JavaScript exception.
+ */
+function promiseNewListAndStore(aStorePath)
+{
+ return promiseNewList().then(function (aList) {
+ let path = aStorePath || getTempFile(TEST_STORE_FILE_NAME).path;
+ let store = new DownloadStore(aList, path);
+ return [aList, store];
+ });
+}
+
+// Tests
+
+/**
+ * Saves downloads to a file, then reloads them.
+ */
+add_task(function* test_save_reload()
+{
+ let [listForSave, storeForSave] = yield promiseNewListAndStore();
+ let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
+ storeForSave.path);
+
+ listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
+ listForSave.add(yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ }));
+
+ // This PDF download should not be serialized because it never succeeds.
+ let pdfDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ saver: "pdf",
+ });
+ listForSave.add(pdfDownload);
+
+ // If we used a callback to adjust the channel, the download should
+ // not be serialized because we can't recreate it across sessions.
+ let adjustedDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ adjustChannel: () => Promise.resolve() },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ });
+ listForSave.add(adjustedDownload);
+
+ let legacyDownload = yield promiseStartLegacyDownload();
+ yield legacyDownload.cancel();
+ listForSave.add(legacyDownload);
+
+ yield storeForSave.save();
+ yield storeForLoad.load();
+
+ // Remove the PDF and adjusted downloads because they should not appear here.
+ listForSave.remove(adjustedDownload);
+ listForSave.remove(pdfDownload);
+
+ let itemsForSave = yield listForSave.getAll();
+ let itemsForLoad = yield listForLoad.getAll();
+
+ do_check_eq(itemsForSave.length, itemsForLoad.length);
+
+ // Downloads should be reloaded in the same order.
+ for (let i = 0; i < itemsForSave.length; i++) {
+ // The reloaded downloads are different objects.
+ do_check_neq(itemsForSave[i], itemsForLoad[i]);
+
+ // The reloaded downloads have the same properties.
+ do_check_eq(itemsForSave[i].source.url,
+ itemsForLoad[i].source.url);
+ do_check_eq(itemsForSave[i].source.referrer,
+ itemsForLoad[i].source.referrer);
+ do_check_eq(itemsForSave[i].target.path,
+ itemsForLoad[i].target.path);
+ do_check_eq(itemsForSave[i].saver.toSerializable(),
+ itemsForLoad[i].saver.toSerializable());
+ }
+});
+
+/**
+ * Checks that saving an empty list deletes any existing file.
+ */
+add_task(function* test_save_empty()
+{
+ let [, store] = yield promiseNewListAndStore();
+
+ let createdFile = yield OS.File.open(store.path, { create: true });
+ yield createdFile.close();
+
+ yield store.save();
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ // If the file does not exist, saving should not generate exceptions.
+ yield store.save();
+});
+
+/**
+ * Checks that loading from a missing file results in an empty list.
+ */
+add_task(function* test_load_empty()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ yield store.load();
+
+ let items = yield list.getAll();
+ do_check_eq(items.length, 0);
+});
+
+/**
+ * Loads downloads from a string in a predefined format. The purpose of this
+ * test is to verify that the JSON format used in previous versions can be
+ * loaded, assuming the file is reloaded on the same platform.
+ */
+add_task(function* test_load_string_predefined()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ // The platform-dependent file name should be generated dynamically.
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let filePathLiteral = JSON.stringify(targetPath);
+ let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
+ let emptyUriLiteral = JSON.stringify(httpUrl("empty.txt"));
+ let referrerUriLiteral = JSON.stringify(TEST_REFERRER_URL);
+
+ let string = "{\"list\":[{\"source\":" + sourceUriLiteral + "," +
+ "\"target\":" + filePathLiteral + "}," +
+ "{\"source\":{\"url\":" + emptyUriLiteral + "," +
+ "\"referrer\":" + referrerUriLiteral + "}," +
+ "\"target\":" + filePathLiteral + "}]}";
+
+ yield OS.File.writeAtomic(store.path,
+ new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, 2);
+
+ do_check_eq(items[0].source.url, httpUrl("source.txt"));
+ do_check_eq(items[0].target.path, targetPath);
+
+ do_check_eq(items[1].source.url, httpUrl("empty.txt"));
+ do_check_eq(items[1].source.referrer, TEST_REFERRER_URL);
+ do_check_eq(items[1].target.path, targetPath);
+});
+
+/**
+ * Loads downloads from a well-formed JSON string containing unrecognized data.
+ */
+add_task(function* test_load_string_unrecognized()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ // The platform-dependent file name should be generated dynamically.
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let filePathLiteral = JSON.stringify(targetPath);
+ let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
+
+ let string = "{\"list\":[{\"source\":null," +
+ "\"target\":null}," +
+ "{\"source\":{\"url\":" + sourceUriLiteral + "}," +
+ "\"target\":{\"path\":" + filePathLiteral + "}," +
+ "\"saver\":{\"type\":\"copy\"}}]}";
+
+ yield OS.File.writeAtomic(store.path,
+ new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, 1);
+
+ do_check_eq(items[0].source.url, httpUrl("source.txt"));
+ do_check_eq(items[0].target.path, targetPath);
+});
+
+/**
+ * Loads downloads from a malformed JSON string.
+ */
+add_task(function* test_load_string_malformed()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ let string = "{\"list\":[{\"source\":null,\"target\":null}," +
+ "{\"source\":{\"url\":\"about:blank\"}}}";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ try {
+ yield store.load();
+ do_throw("Exception expected when JSON data is malformed.");
+ } catch (ex) {
+ if (ex.name != "SyntaxError") {
+ throw ex;
+ }
+ do_print("The expected SyntaxError exception was thrown.");
+ }
+
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, 0);
+});
+
+/**
+ * Saves downloads with unknown properties to a file and then reloads
+ * them to ensure that these properties are preserved.
+ */
+add_task(function* test_save_reload_unknownProperties()
+{
+ let [listForSave, storeForSave] = yield promiseNewListAndStore();
+ let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
+ storeForSave.path);
+
+ let download1 = yield promiseNewDownload(httpUrl("source.txt"));
+ // startTime should be ignored as it is a known property, and error
+ // is ignored by serialization
+ download1._unknownProperties = { peanut: "butter",
+ orange: "marmalade",
+ startTime: 77,
+ error: { message: "Passed" } };
+ listForSave.add(download1);
+
+ let download2 = yield promiseStartLegacyDownload();
+ yield download2.cancel();
+ download2._unknownProperties = { number: 5, object: { test: "string" } };
+ listForSave.add(download2);
+
+ let download3 = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL,
+ source1: "download3source1",
+ source2: "download3source2" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path,
+ target1: "download3target1",
+ target2: "download3target2" },
+ saver : { type: "copy",
+ saver1: "download3saver1",
+ saver2: "download3saver2" },
+ });
+ listForSave.add(download3);
+
+ yield storeForSave.save();
+ yield storeForLoad.load();
+
+ let itemsForSave = yield listForSave.getAll();
+ let itemsForLoad = yield listForLoad.getAll();
+
+ do_check_eq(itemsForSave.length, itemsForLoad.length);
+
+ do_check_eq(Object.keys(itemsForLoad[0]._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[0]._unknownProperties.peanut, "butter");
+ do_check_eq(itemsForLoad[0]._unknownProperties.orange, "marmalade");
+ do_check_false("startTime" in itemsForLoad[0]._unknownProperties);
+ do_check_false("error" in itemsForLoad[0]._unknownProperties);
+
+ do_check_eq(Object.keys(itemsForLoad[1]._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[1]._unknownProperties.number, 5);
+ do_check_eq(itemsForLoad[1]._unknownProperties.object.test, "string");
+
+ do_check_eq(Object.keys(itemsForLoad[2].source._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[2].source._unknownProperties.source1,
+ "download3source1");
+ do_check_eq(itemsForLoad[2].source._unknownProperties.source2,
+ "download3source2");
+
+ do_check_eq(Object.keys(itemsForLoad[2].target._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[2].target._unknownProperties.target1,
+ "download3target1");
+ do_check_eq(itemsForLoad[2].target._unknownProperties.target2,
+ "download3target2");
+
+ do_check_eq(Object.keys(itemsForLoad[2].saver._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[2].saver._unknownProperties.saver1,
+ "download3saver1");
+ do_check_eq(itemsForLoad[2].saver._unknownProperties.saver2,
+ "download3saver2");
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_Downloads.js b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
new file mode 100644
index 000000000..2027beee1
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -0,0 +1,194 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the functions located directly in the "Downloads" object.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Tests that the createDownload function exists and can be called. More
+ * detailed tests are implemented separately for the DownloadCore module.
+ */
+add_task(function* test_createDownload()
+{
+ // Creates a simple Download object without starting the download.
+ yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "copy" },
+ });
+});
+
+/**
+ * Tests createDownload for private download.
+ */
+add_task(function* test_createDownload_private()
+{
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank", isPrivate: true },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "copy" }
+ });
+ do_check_true(download.source.isPrivate);
+});
+
+/**
+ * Tests createDownload for normal (public) download.
+ */
+add_task(function* test_createDownload_public()
+{
+ let tempPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank", isPrivate: false },
+ target: { path: tempPath },
+ saver: { type: "copy" }
+ });
+ do_check_false(download.source.isPrivate);
+
+ download = yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: tempPath },
+ saver: { type: "copy" }
+ });
+ do_check_false(download.source.isPrivate);
+});
+
+/**
+ * Tests createDownload for a pdf saver throws if only given a url.
+ */
+add_task(function* test_createDownload_pdf()
+{
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "pdf" },
+ });
+
+ try {
+ yield download.start();
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
+ throw ex;
+ }
+ }
+
+ do_check_false(download.succeeded);
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Tests "fetch" with nsIURI and nsIFile as arguments.
+ */
+add_task(function* test_fetch_uri_file_arguments()
+{
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
+ yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
+});
+
+/**
+ * Tests "fetch" with DownloadSource and DownloadTarget as arguments.
+ */
+add_task(function* test_fetch_object_arguments()
+{
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ yield Downloads.fetch({ url: httpUrl("source.txt") }, { path: targetPath });
+ yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
+});
+
+/**
+ * Tests "fetch" with string arguments.
+ */
+add_task(function* test_fetch_string_arguments()
+{
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ yield Downloads.fetch(httpUrl("source.txt"), targetPath);
+ yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
+
+ targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ yield Downloads.fetch(new String(httpUrl("source.txt")),
+ new String(targetPath));
+ yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
+});
+
+/**
+ * Tests that the getList function returns the same list when called multiple
+ * times with the same argument, but returns different lists when called with
+ * different arguments. More detailed tests are implemented separately for the
+ * DownloadList module.
+ */
+add_task(function* test_getList()
+{
+ let publicListOne = yield Downloads.getList(Downloads.PUBLIC);
+ let privateListOne = yield Downloads.getList(Downloads.PRIVATE);
+
+ let publicListTwo = yield Downloads.getList(Downloads.PUBLIC);
+ let privateListTwo = yield Downloads.getList(Downloads.PRIVATE);
+
+ do_check_eq(publicListOne, publicListTwo);
+ do_check_eq(privateListOne, privateListTwo);
+
+ do_check_neq(publicListOne, privateListOne);
+});
+
+/**
+ * Tests that the getSummary function returns the same summary when called
+ * multiple times with the same argument, but returns different summaries when
+ * called with different arguments. More detailed tests are implemented
+ * separately for the DownloadSummary object in the DownloadList module.
+ */
+add_task(function* test_getSummary()
+{
+ let publicSummaryOne = yield Downloads.getSummary(Downloads.PUBLIC);
+ let privateSummaryOne = yield Downloads.getSummary(Downloads.PRIVATE);
+
+ let publicSummaryTwo = yield Downloads.getSummary(Downloads.PUBLIC);
+ let privateSummaryTwo = yield Downloads.getSummary(Downloads.PRIVATE);
+
+ do_check_eq(publicSummaryOne, publicSummaryTwo);
+ do_check_eq(privateSummaryOne, privateSummaryTwo);
+
+ do_check_neq(publicSummaryOne, privateSummaryOne);
+});
+
+/**
+ * Tests that the getSystemDownloadsDirectory returns a non-empty download
+ * directory string.
+ */
+add_task(function* test_getSystemDownloadsDirectory()
+{
+ let downloadDir = yield Downloads.getSystemDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+});
+
+/**
+ * Tests that the getPreferredDownloadsDirectory returns a non-empty download
+ * directory string.
+ */
+add_task(function* test_getPreferredDownloadsDirectory()
+{
+ let downloadDir = yield Downloads.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+});
+
+/**
+ * Tests that the getTemporaryDownloadsDirectory returns a non-empty download
+ * directory string.
+ */
+add_task(function* test_getTemporaryDownloadsDirectory()
+{
+ let downloadDir = yield Downloads.getTemporaryDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js b/toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js
new file mode 100644
index 000000000..1308e9782
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * The temporary directory downloads saves to, should be only readable
+ * for the current user.
+ */
+add_task(function* test_private_temp() {
+
+ let download = yield promiseStartExternalHelperAppServiceDownload(
+ httpUrl("empty.txt"));
+
+ yield promiseDownloadStopped(download);
+
+ var targetFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);
+ targetFile.initWithPath(download.target.path);
+
+ // 488 is the decimal value of 0o700.
+ equal(targetFile.parent.permissions, 448);
+});
diff --git a/toolkit/components/jsdownloads/test/unit/xpcshell.ini b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
new file mode 100644
index 000000000..8de554540
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+
+# Note: The "tail.js" file is not defined in the "tail" key because it calls
+# the "add_test_task" function, that does not work properly in tail files.
+support-files =
+ common_test_Download.js
+
+[test_DownloadCore.js]
+[test_DownloadImport.js]
+[test_DownloadIntegration.js]
+[test_DownloadLegacy.js]
+[test_DownloadList.js]
+[test_Downloads.js]
+[test_DownloadStore.js]
+[test_PrivateTemp.js]
+skip-if = os != 'linux'