summaryrefslogtreecommitdiffstats
path: root/toolkit/components/downloads/test/unit/common_test_Download.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/downloads/test/unit/common_test_Download.js')
-rw-r--r--toolkit/components/downloads/test/unit/common_test_Download.js2751
1 files changed, 2751 insertions, 0 deletions
diff --git a/toolkit/components/downloads/test/unit/common_test_Download.js b/toolkit/components/downloads/test/unit/common_test_Download.js
new file mode 100644
index 0000000000..5e65cd7cbd
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/common_test_Download.js
@@ -0,0 +1,2751 @@
+/* -*- 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.
+ */
+
+/* import-globals-from head.js */
+/* global gUseLegacySaver */
+
+"use strict";
+
+// Globals
+
+const kDeleteTempFileOnExit = "browser.helperApps.deleteTempFileOnExit";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+/**
+ * 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;
+ });
+}
+
+/**
+ * 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 = async function (downloadTarget, expectedContents) {
+ Assert.ok(downloadTarget.exists);
+ Assert.equal(
+ await expectNonZeroDownloadTargetSize(downloadTarget),
+ expectedContents.length
+ );
+ await promiseVerifyContents(downloadTarget.path, expectedContents);
+};
+
+/**
+ * This is a temporary workaround for frequent intermittent Bug 1760112.
+ * For some reason the download target size is not updated, even if the code
+ * is "apparently" already executing and awaiting for refresh().
+ * TODO(Bug 1814364): Figure out a proper fix for this.
+ */
+async function expectNonZeroDownloadTargetSize(downloadTarget) {
+ todo_check_true(downloadTarget.size, "Size should not be zero.");
+ if (!downloadTarget.size) {
+ await downloadTarget.refresh();
+ }
+ return downloadTarget.size;
+}
+
+/**
+ * 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(async 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 = await Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: { path: targetFile.path },
+ saver: { type: "copy" },
+ });
+
+ Assert.equal(download.source.url, httpUrl("source.txt"));
+ Assert.equal(download.target.path, targetFile.path);
+
+ await 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 = await promiseStartLegacyDownload(null, { targetFile });
+
+ Assert.equal(download.source.url, httpUrl("source.txt"));
+ Assert.equal(download.target.path, targetFile.path);
+
+ await promiseDownloadStopped(download);
+ }
+
+ // Check additional properties on the finished download.
+ Assert.equal(download.source.referrerInfo, null);
+
+ await 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(async function test_basic_tryToKeepPartialData() {
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ continueResponses();
+ await promiseDownloadStopped(download);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ Assert.equal(32, download.saver.getSha256Hash().length);
+});
+
+/**
+ * Tests that the channelIsForDownload property is set for the network request,
+ * and that the request is marked as throttleable.
+ */
+add_task(async function test_channelIsForDownload_classFlags() {
+ let downloadChannel = null;
+
+ // We use a different method based on whether we are testing legacy downloads.
+ if (!gUseLegacySaver) {
+ let download = await Downloads.createDownload({
+ source: {
+ url: httpUrl("interruptible_resumable.txt"),
+ async adjustChannel(channel) {
+ downloadChannel = channel;
+ },
+ },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ await download.start();
+ } else {
+ // Start a download using nsIExternalHelperAppService, but ensure it cannot
+ // finish before we retrieve the "request" property.
+ mustInterruptResponses();
+ let download = await promiseStartExternalHelperAppServiceDownload();
+ downloadChannel = download.saver.request;
+ continueResponses();
+ await promiseDownloadStopped(download);
+ }
+
+ Assert.ok(
+ downloadChannel.QueryInterface(Ci.nsIHttpChannelInternal)
+ .channelIsForDownload
+ );
+
+ // Throttleable is the only class flag assigned to downloads.
+ Assert.equal(
+ downloadChannel.QueryInterface(Ci.nsIClassOfService).classFlags,
+ Ci.nsIClassOfService.Throttleable
+ );
+});
+
+/**
+ * Tests the permissions of the final target file once the download finished.
+ */
+add_task(async 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"
+ ) {
+ info("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]) {
+ info(
+ "Checking " +
+ JSON.stringify({ autoDelete, isPrivate, launchWhenSucceeded })
+ );
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, autoDelete);
+
+ let download;
+ if (!gUseLegacySaver) {
+ download = await Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launchWhenSucceeded,
+ launcherPath,
+ });
+ await download.start();
+ } else {
+ download = await promiseStartLegacyDownload(httpUrl("source.txt"), {
+ isPrivate,
+ launchWhenSucceeded,
+ launcherPath: launchWhenSucceeded && launcherPath,
+ });
+ await promiseDownloadStopped(download);
+ }
+
+ let isTemporary = launchWhenSucceeded && isPrivate;
+ let stat = await IOUtils.stat(download.target.path);
+ if (Services.appinfo.OS == "WINNT") {
+ // On Windows
+ // Temporary downloads should be read-only
+ Assert.equal(stat.permissions, isTemporary ? 0o444 : 0o666);
+ } 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.
+ Assert.equal(
+ stat.permissions,
+ isTemporary ? 0o400 : 0o666 & ~Services.sysinfo.getProperty("umask")
+ );
+ }
+ }
+ }
+ }
+
+ // Clean up the changes to the preference.
+ Services.prefs.clearUserPref(kDeleteTempFileOnExit);
+});
+
+/**
+ * Tests the zone information of the final target once the download finished.
+ */
+add_task(async function test_windows_zoneInformation() {
+ // This test is only executed on Windows, and in order to work correctly it
+ // requires the local user applicaton data directory to be on an NTFS file
+ // system. We use this directory because it is more likely to be on the local
+ // system installation drive, while the temporary directory used by the test
+ // environment is on the same drive as the test sources.
+ if (Services.appinfo.OS != "WINNT") {
+ info("Skipping test.");
+ return;
+ }
+
+ let normalTargetFile = FileUtils.getFile("LocalAppData", [
+ "xpcshell-download-test.txt",
+ ]);
+
+ // The template file name lenght is more than MAX_PATH characters. The final
+ // full path will be shortened to MAX_PATH length by the createUnique call.
+ let longTargetFile = FileUtils.getFile("LocalAppData", [
+ "T".repeat(256) + ".txt",
+ ]);
+ longTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ const httpSourceUrl = httpUrl("source.txt");
+ const dataSourceUrl = "data:text/html," + TEST_DATA_SHORT;
+
+ function createReferrerInfo(
+ aReferrer,
+ aRefererPolicy = Ci.nsIReferrerInfo.EMPTY
+ ) {
+ return new ReferrerInfo(aRefererPolicy, true, NetUtil.newURI(aReferrer));
+ }
+
+ const tests = [
+ {
+ expectedZoneId:
+ "[ZoneTransfer]\r\nZoneId=3\r\nHostUrl=" + httpSourceUrl + "\r\n",
+ },
+ {
+ targetFile: longTargetFile,
+ expectedZoneId:
+ "[ZoneTransfer]\r\nZoneId=3\r\nHostUrl=" + httpSourceUrl + "\r\n",
+ },
+ {
+ sourceUrl: dataSourceUrl,
+ expectedZoneId:
+ "[ZoneTransfer]\r\nZoneId=3\r\nHostUrl=about:internet\r\n",
+ },
+ {
+ options: {
+ referrerInfo: createReferrerInfo(
+ TEST_REFERRER_URL,
+ Ci.nsIReferrerInfo.UNSAFE_URL
+ ),
+ },
+ expectedZoneId:
+ "[ZoneTransfer]\r\nZoneId=3\r\n" +
+ "ReferrerUrl=" +
+ TEST_REFERRER_URL +
+ "\r\n" +
+ "HostUrl=" +
+ httpSourceUrl +
+ "\r\n",
+ },
+ {
+ options: { referrerInfo: createReferrerInfo(dataSourceUrl) },
+ expectedZoneId:
+ "[ZoneTransfer]\r\nZoneId=3\r\nHostUrl=" + httpSourceUrl + "\r\n",
+ },
+ {
+ options: {
+ referrerInfo: createReferrerInfo("http://example.com/a\rb\nc"),
+ },
+ expectedZoneId:
+ "[ZoneTransfer]\r\nZoneId=3\r\n" +
+ "ReferrerUrl=http://example.com/\r\n" +
+ "HostUrl=" +
+ httpSourceUrl +
+ "\r\n",
+ },
+ {
+ options: { isPrivate: true },
+ expectedZoneId: "[ZoneTransfer]\r\nZoneId=3\r\n",
+ },
+ {
+ options: {
+ referrerInfo: createReferrerInfo(TEST_REFERRER_URL),
+ isPrivate: true,
+ },
+ expectedZoneId: "[ZoneTransfer]\r\nZoneId=3\r\n",
+ },
+ ];
+ for (const test of tests) {
+ const sourceUrl = test.sourceUrl || httpSourceUrl;
+ const targetFile = test.targetFile || normalTargetFile;
+ info(targetFile.path);
+ try {
+ if (!gUseLegacySaver) {
+ let download = await Downloads.createDownload({
+ source: test.options
+ ? Object.assign({ url: sourceUrl }, test.options)
+ : sourceUrl,
+ target: targetFile.path,
+ });
+ await download.start();
+ } else {
+ let download = await promiseStartLegacyDownload(
+ sourceUrl,
+ Object.assign({ targetFile }, test.options || {})
+ );
+ await promiseDownloadStopped(download);
+ }
+ await promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
+
+ let path = targetFile.path + ":Zone.Identifier";
+ if (Services.appinfo.OS === "WINNT") {
+ path = PathUtils.toExtendedWindowsPath(path);
+ }
+
+ Assert.equal(await IOUtils.readUTF8(path), test.expectedZoneId);
+ } finally {
+ await IOUtils.remove(targetFile.path);
+ }
+ }
+});
+
+/**
+ * Checks the referrer for downloads.
+ */
+add_task(async function test_referrer() {
+ let sourcePath = "/test_referrer.txt";
+ let sourceUrl = httpUrl("test_referrer.txt");
+ let dataSourceUrl = "data:text/html,<html><body></body></html>";
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ registerCleanupFunction(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ Assert.ok(aRequest.hasHeader("Referer"));
+ Assert.equal(aRequest.getHeader("Referer"), TEST_REFERRER_URL);
+ });
+ let download;
+ let referrerInfo = new ReferrerInfo(
+ Ci.nsIReferrerInfo.UNSAFE_URL,
+ true,
+ NetUtil.newURI(TEST_REFERRER_URL)
+ );
+
+ if (!gUseLegacySaver) {
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ let targetPath = targetFile.path;
+
+ download = await Downloads.createDownload({
+ source: { url: sourceUrl, referrerInfo },
+ target: targetPath,
+ });
+
+ Assert.ok(download.source.referrerInfo.equals(referrerInfo));
+ await download.start();
+
+ download = await Downloads.createDownload({
+ source: { url: sourceUrl, referrerInfo, isPrivate: true },
+ target: targetPath,
+ });
+ Assert.ok(download.source.referrerInfo.equals(referrerInfo));
+ await download.start();
+
+ // Test the download still works for non-HTTP channel with referrer.
+ download = await Downloads.createDownload({
+ source: { url: dataSourceUrl, referrerInfo },
+ target: targetPath,
+ });
+ Assert.ok(download.source.referrerInfo.equals(referrerInfo));
+ await download.start();
+ } else {
+ download = await promiseStartLegacyDownload(sourceUrl, {
+ referrerInfo,
+ });
+ await promiseDownloadStopped(download);
+ checkEqualReferrerInfos(download.source.referrerInfo, referrerInfo);
+
+ download = await promiseStartLegacyDownload(sourceUrl, {
+ referrerInfo,
+ isPrivate: true,
+ });
+ await promiseDownloadStopped(download);
+ checkEqualReferrerInfos(download.source.referrerInfo, referrerInfo);
+
+ download = await promiseStartLegacyDownload(dataSourceUrl, {
+ referrerInfo,
+ });
+ await promiseDownloadStopped(download);
+ Assert.equal(download.source.referrerInfo, null);
+ }
+
+ cleanup();
+});
+
+/**
+ * Checks the adjustChannel callback for downloads.
+ */
+add_task(async 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);
+ }
+ registerCleanupFunction(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, aRequest => {
+ Assert.equal(aRequest.method, "POST");
+
+ Assert.ok(aRequest.hasHeader(customHeader.name));
+ Assert.equal(aRequest.getHeader(customHeader.name), customHeader.value);
+
+ const stream = aRequest.bodyInputStream;
+ const body = NetUtil.readInputStreamToString(stream, stream.available());
+ Assert.equal(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 = await Downloads.createDownload({
+ source: { url: sourceUrl, adjustChannel },
+ target: targetPath,
+ });
+ Assert.equal(download.source.adjustChannel, adjustChannel);
+ Assert.equal(download.toSerializable(), null);
+ await download.start();
+
+ cleanup();
+});
+
+/**
+ * Checks initial and final state and progress for a successful download.
+ */
+add_task(async 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 = await promiseNewDownload();
+
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+ Assert.equal(download.progress, 0);
+ Assert.ok(download.startTime === null);
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ await download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we cannot check its initial state.
+ download = await promiseStartLegacyDownload();
+ await promiseDownloadStopped(download);
+ }
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+ Assert.equal(download.progress, 100);
+ Assert.ok(isValidDate(download.startTime));
+ Assert.ok(download.target.exists);
+ Assert.equal(
+ await expectNonZeroDownloadTargetSize(download.target),
+ TEST_DATA_SHORT.length
+ );
+});
+
+/**
+ * Checks the notification of the final download state.
+ */
+add_task(async function test_final_state_notified() {
+ mustInterruptResponses();
+
+ let download = await 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();
+ await promiseAttempt;
+
+ // The view should have been notified before the download completes.
+ Assert.ok(onchangeNotified);
+ Assert.ok(lastNotifiedStopped);
+ Assert.equal(lastNotifiedProgress, 100);
+});
+
+/**
+ * Checks intermediate progress for a successful download.
+ */
+add_task(async function test_intermediate_progress() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+
+ await promiseDownloadMidway(download);
+
+ Assert.ok(download.hasProgress);
+ Assert.equal(download.currentBytes, TEST_DATA_SHORT.length);
+ Assert.equal(download.totalBytes, TEST_DATA_SHORT.length * 2);
+
+ // The final file size should not be computed for in-progress downloads.
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ // Continue after the first chunk of data is fully received.
+ continueResponses();
+ await promiseDownloadStopped(download);
+
+ Assert.ok(download.stopped);
+ Assert.equal(download.progress, 100);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Downloads a file with a "Content-Length" of 0 and checks the progress.
+ */
+add_task(async function test_empty_progress() {
+ let download = await promiseStartDownload(httpUrl("empty.txt"));
+ await promiseDownloadStopped(download);
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.hasProgress);
+ Assert.equal(download.progress, 100);
+ Assert.equal(download.currentBytes, 0);
+ Assert.equal(download.totalBytes, 0);
+
+ // We should have received the content type even for an empty file.
+ Assert.equal(download.contentType, "text/plain");
+
+ Assert.equal((await IOUtils.stat(download.target.path)).size, 0);
+ Assert.ok(download.target.exists);
+ Assert.equal(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(async 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 = await 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 = await promiseStartExternalHelperAppServiceDownload(
+ httpUrl("empty.txt")
+ );
+ }
+ await promiseDownloadStopped(download);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ Assert.equal((await IOUtils.stat(download.target.path)).size, 0);
+ Assert.ok(download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ Assert.equal(32, download.saver.getSha256Hash().length);
+});
+
+/**
+ * Downloads an empty file with no "Content-Length" and checks the progress.
+ */
+add_task(async function test_empty_noprogress() {
+ let sourcePath = "/test_empty_noprogress.txt";
+ let sourceUrl = httpUrl("test_empty_noprogress.txt");
+ let deferRequestReceived = PromiseUtils.defer();
+
+ // Register an interruptible handler that notifies us when the request occurs.
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ registerCleanupFunction(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 = await promiseNewDownload(sourceUrl);
+
+ download.onchange = function () {
+ if (!download.stopped) {
+ Assert.ok(!download.hasProgress);
+ Assert.equal(download.currentBytes, 0);
+ Assert.equal(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 = await 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.
+ await deferRequestReceived.promise;
+ await promiseExecuteSoon();
+
+ // Check that this download has no progress report.
+ Assert.ok(!download.stopped);
+ Assert.ok(!download.hasProgress);
+ Assert.equal(download.currentBytes, 0);
+ Assert.equal(download.totalBytes, 0);
+
+ // Now allow the response to finish.
+ continueResponses();
+ await promiseDownloadStopped(download);
+
+ // We should have received the content type even if no progress is reported.
+ Assert.equal(download.contentType, "text/plain");
+
+ // Verify the state of the completed download.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.hasProgress);
+ Assert.equal(download.progress, 100);
+ Assert.equal(download.currentBytes, 0);
+ Assert.equal(download.totalBytes, 0);
+ Assert.ok(download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ Assert.equal((await IOUtils.stat(download.target.path)).size, 0);
+});
+
+/**
+ * Calls the "start" method two times before the download is finished.
+ */
+add_task(async 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 = await 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 = await 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.
+ await promiseAttempt1;
+ await promiseAttempt2;
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download and verifies that its state is reported correctly.
+ */
+add_task(async 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 = await promiseNewDownload(httpUrl("interruptible.txt"));
+ } else {
+ download = await promiseStartLegacyDownload(
+ httpUrl("interruptible.txt"),
+ options
+ );
+ }
+
+ // Cancel the download after receiving the first part of the response.
+ let deferCancel = PromiseUtils.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.
+ Assert.ok(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.
+ await deferCancel.promise;
+
+ if (gUseLegacySaver) {
+ // The nsIWebBrowserPersist instance should have been canceled now.
+ Assert.equal(options.outPersist.result, Cr.NS_ERROR_ABORT);
+ }
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.canceled);
+ Assert.ok(download.error === null);
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+
+ // Progress properties are not reset by canceling.
+ Assert.equal(download.progress, 50);
+ Assert.equal(download.totalBytes, TEST_DATA_SHORT.length * 2);
+ Assert.equal(download.currentBytes, TEST_DATA_SHORT.length);
+
+ if (!gUseLegacySaver) {
+ // The promise returned by "start" should have been rejected meanwhile.
+ try {
+ await promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ Assert.ok(!ex.becauseSourceFailed);
+ Assert.ok(!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(async function test_cancel_midway_tryToKeepPartialData() {
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+
+ Assert.ok(await IOUtils.exists(download.target.path));
+ Assert.ok(await IOUtils.exists(download.target.partFilePath));
+
+ await download.cancel();
+ await download.removePartialData();
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.canceled);
+ Assert.ok(download.error === null);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download right after starting it.
+ */
+add_task(async function test_cancel_immediately() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseAttempt = download.start();
+ Assert.ok(!download.stopped);
+
+ let promiseCancel = download.cancel();
+ Assert.ok(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 {
+ await promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ Assert.ok(!ex.becauseSourceFailed);
+ Assert.ok(!ex.becauseTargetFailed);
+ }
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.canceled);
+ Assert.ok(download.error === null);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+
+ // Check that the promise returned by the "cancel" method has been resolved.
+ await promiseCancel;
+});
+
+/**
+ * Cancels and restarts a download sequentially.
+ */
+add_task(async function test_cancel_midway_restart() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+
+ // The first time, cancel the download midway.
+ await promiseDownloadMidway(download);
+ await download.cancel();
+
+ Assert.ok(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.
+ Assert.ok(!download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(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.
+ Assert.equal(download.progress, 0);
+ Assert.equal(download.totalBytes, 0);
+ Assert.equal(download.currentBytes, 0);
+
+ await promiseAttempt;
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download and restarts it from where it stopped.
+ */
+add_task(async function test_cancel_midway_restart_tryToKeepPartialData() {
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ await download.cancel();
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.hasPartialData);
+
+ // We should have kept the partial data and an empty target file placeholder.
+ Assert.ok(await IOUtils.exists(download.target.path));
+ await promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ // Verify that the server sent the response from the start.
+ Assert.equal(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 = PromiseUtils.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.
+ await deferMidway.promise;
+ Assert.ok(download.progress > 72 && download.progress < 78);
+
+ // Now we allow the download to finish.
+ continueResponses();
+ await promiseAttempt;
+
+ // Check that the server now sent the second part only.
+ Assert.equal(gMostRecentFirstBytePos, TEST_DATA_SHORT.length);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ Assert.equal(false, await IOUtils.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(async function test_cancel_midway_restart_removePartialData() {
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ await download.cancel();
+ await download.removePartialData();
+
+ Assert.ok(!download.hasPartialData);
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ // The second time, we'll request and obtain the entire response again.
+ continueResponses();
+ await download.start();
+
+ // Verify that the server sent the response from the start.
+ Assert.equal(gMostRecentFirstBytePos, 0);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ Assert.equal(false, await IOUtils.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(
+ async function test_cancel_midway_restart_tryToKeepPartialData_false() {
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ await download.cancel();
+
+ download.tryToKeepPartialData = false;
+
+ // The above property change does not affect existing partial data.
+ Assert.ok(download.hasPartialData);
+ await promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+
+ await download.removePartialData();
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+
+ // Restart the download from the beginning.
+ mustInterruptResponses();
+ download.start().catch(() => {});
+
+ await promiseDownloadMidway(download);
+ await promisePartFileReady(download);
+
+ // While the download is in progress, we should still have a ".part" file.
+ Assert.ok(!download.hasPartialData);
+ Assert.ok(await IOUtils.exists(download.target.path));
+ Assert.ok(await IOUtils.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") {
+ Assert.equal(
+ (await IOUtils.stat(download.target.partFilePath)).permissions,
+ 0o600
+ );
+ }
+
+ await download.cancel();
+
+ // The ".part" file should be deleted now that the download is canceled.
+ Assert.ok(!download.hasPartialData);
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+
+ // The third time, we'll request and obtain the entire response again.
+ continueResponses();
+ await download.start();
+
+ // Verify that the server sent the response from the start.
+ Assert.equal(gMostRecentFirstBytePos, 0);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ await promiseVerifyTarget(
+ download.target,
+ TEST_DATA_SHORT + TEST_DATA_SHORT
+ );
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ }
+);
+
+/**
+ * Cancels a download right after starting it, then restarts it immediately.
+ */
+add_task(async function test_cancel_immediately_restart_immediately() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt = download.start();
+
+ Assert.ok(!download.stopped);
+
+ download.cancel();
+ Assert.ok(download.canceled);
+
+ let promiseRestarted = download.start();
+ Assert.ok(!download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(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.
+ Assert.equal(download.hasProgress, false);
+ Assert.equal(download.progress, 0);
+ Assert.equal(download.totalBytes, 0);
+ Assert.equal(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 {
+ await 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.
+ info("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ Assert.ok(!ex.becauseSourceFailed);
+ Assert.ok(!ex.becauseTargetFailed);
+ }
+
+ await promiseRestarted;
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download midway, then restarts it immediately.
+ */
+add_task(async function test_cancel_midway_restart_immediately() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt = download.start();
+
+ // The first time, cancel the download midway.
+ await promiseDownloadMidway(download);
+ download.cancel();
+ Assert.ok(download.canceled);
+
+ let promiseRestarted = download.start();
+ Assert.ok(!download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(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.
+ Assert.equal(download.hasProgress, false);
+ Assert.equal(download.progress, 0);
+ Assert.equal(download.totalBytes, 0);
+ Assert.equal(download.currentBytes, 0);
+
+ // The second request is allowed to complete.
+ continueResponses();
+ try {
+ await promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ Assert.ok(!ex.becauseSourceFailed);
+ Assert.ok(!ex.becauseTargetFailed);
+ }
+
+ await promiseRestarted;
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Calls the "cancel" method on a successful download.
+ */
+add_task(async function test_cancel_successful() {
+ let download = await promiseStartDownload();
+ await promiseDownloadStopped(download);
+
+ // The cancel method should succeed with no effect.
+ await download.cancel();
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Calls the "cancel" method two times in a row.
+ */
+add_task(async function test_cancel_twice() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseAttempt = download.start();
+ Assert.ok(!download.stopped);
+
+ let promiseCancel1 = download.cancel();
+ Assert.ok(download.canceled);
+ let promiseCancel2 = download.cancel();
+
+ try {
+ await promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ Assert.ok(!ex.becauseSourceFailed);
+ Assert.ok(!ex.becauseTargetFailed);
+ }
+
+ // Both promises should now be resolved.
+ await promiseCancel1;
+ await promiseCancel2;
+
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(download.canceled);
+ Assert.ok(download.error === null);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+});
+
+/**
+ * Checks the "refresh" method for succeeded downloads.
+ */
+add_task(async function test_refresh_succeeded() {
+ let download = await promiseStartDownload();
+ await promiseDownloadStopped(download);
+
+ // The DownloadTarget properties should be the same after calling "refresh".
+ await download.refresh();
+ await 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.
+ await IOUtils.move(download.target.path, `${download.target.path}.old`);
+ await download.refresh();
+ Assert.ok(!download.target.exists);
+ Assert.equal(
+ await expectNonZeroDownloadTargetSize(download.target),
+ TEST_DATA_SHORT.length
+ );
+
+ // The DownloadTarget properties should be restored when the file is put back.
+ await IOUtils.move(`${download.target.path}.old`, download.target.path);
+ await download.refresh();
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Checks that a download cannot be restarted after the "finalize" method.
+ */
+add_task(async function test_finalize() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseFinalized = download.finalize();
+
+ try {
+ await download.start();
+ do_throw("It should not be possible to restart after finalization.");
+ } catch (ex) {}
+
+ await promiseFinalized;
+
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(download.canceled);
+ Assert.ok(download.error === null);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+});
+
+/**
+ * Checks that the "finalize" method can remove partially downloaded data.
+ */
+add_task(async function test_finalize_tryToKeepPartialData() {
+ // Check finalization without removing partial data.
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ await download.finalize();
+
+ Assert.ok(download.hasPartialData);
+ Assert.ok(await IOUtils.exists(download.target.path));
+ Assert.ok(await IOUtils.exists(download.target.partFilePath));
+
+ // Clean up.
+ await download.removePartialData();
+
+ // Check finalization while removing partial data.
+ download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ await download.finalize(true);
+
+ Assert.ok(!download.hasPartialData);
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+});
+
+/**
+ * Checks that whenSucceeded returns a promise that is resolved after a restart.
+ */
+add_task(async 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 = await 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 = await promiseStartLegacyDownload(httpUrl("interruptible.txt"));
+ promiseSucceeded = download.whenSucceeded();
+ }
+
+ // Cancel the first download attempt.
+ await 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.
+ await promiseSucceeded;
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Ensures download error details are reported on network failures.
+ */
+add_task(async 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 = await promiseNewDownload(sourceUrl);
+
+ Assert.ok(download.error === null);
+
+ await 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 = await promiseStartLegacyDownload(sourceUrl);
+ await 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.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error !== null);
+ Assert.ok(download.error.becauseSourceFailed);
+ Assert.ok(!download.error.becauseTargetFailed);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.ok(!download.target.exists);
+ Assert.equal(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(async 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
+ );
+ }
+ registerCleanupFunction(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = await promiseNewDownload(sourceUrl);
+
+ Assert.ok(download.error === null);
+
+ await 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 = await promiseStartLegacyDownload(sourceUrl);
+ await 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.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error !== null);
+ Assert.ok(download.error.becauseSourceFailed);
+ Assert.ok(!download.error.becauseTargetFailed);
+ Assert.equal(download.error.result, Cr.NS_ERROR_NET_PARTIAL_TRANSFER);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+});
+
+/**
+ * Ensures a download error is reported when an RST packet is received.
+ */
+add_task(async function test_error_source_netreset() {
+ if (AppConstants.platform == "win") {
+ return;
+ }
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = await promiseNewDownload(httpUrl("netreset.txt"));
+
+ Assert.ok(download.error === null);
+
+ await 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 = await promiseStartLegacyDownload(httpUrl("netreset.txt"));
+ await 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.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error !== null);
+ Assert.ok(download.error.becauseSourceFailed);
+ Assert.ok(!download.error.becauseTargetFailed);
+ Assert.equal(download.error.result, Cr.NS_ERROR_NET_RESET);
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+});
+
+/**
+ * Ensures download error details are reported on local writing failures.
+ */
+add_task(async 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 = await Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: targetFile,
+ });
+ await 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 = await promiseStartLegacyDownload(null, { targetFile });
+ await 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.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error !== null);
+ Assert.ok(download.error.becauseTargetFailed);
+ Assert.ok(!download.error.becauseSourceFailed);
+
+ // Check unserializing a download with an errorObj and restarting it will
+ // clear the errorObj initially.
+ let serializable = download.toSerializable();
+ Assert.ok(serializable.errorObj, "Ensure we have an errorObj initially");
+ let reserialized = JSON.parse(JSON.stringify(serializable));
+ download = await Downloads.createDownload(reserialized);
+ let promise = download.start().catch(() => {});
+ serializable = download.toSerializable();
+ Assert.ok(!serializable.errorObj, "Ensure we didn't persist the errorObj");
+ await promise;
+ } 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(async 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 = await Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: targetFile,
+ });
+ download.start().catch(() => {});
+ } else {
+ download = await promiseStartLegacyDownload(null, { targetFile });
+ }
+ await 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.
+ await download.start();
+
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.canceled);
+ Assert.ok(download.error === null);
+ Assert.equal(download.progress, 100);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Executes download in both public and private modes.
+ */
+add_task(async 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);
+ }
+ registerCleanupFunction(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.
+ Assert.ok(!aRequest.hasHeader("Cookie"));
+ aResponse.setHeader("Set-Cookie", "foobar=1", false);
+ testCount++;
+ } else if (testCount == 1) {
+ // The cookie should exists for second public download.
+ Assert.ok(aRequest.hasHeader("Cookie"));
+ Assert.equal(aRequest.getHeader("Cookie"), "foobar=1");
+ testCount++;
+ } else if (testCount == 2) {
+ // No cookies should exist for first private download.
+ Assert.ok(!aRequest.hasHeader("Cookie"));
+ }
+ });
+
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ await Downloads.fetch(sourceUrl, targetFile);
+ await Downloads.fetch(sourceUrl, targetFile);
+
+ if (!gUseLegacySaver) {
+ let download = await Downloads.createDownload({
+ source: { url: sourceUrl, isPrivate: true },
+ target: targetFile,
+ });
+ await download.start();
+ } else {
+ let download = await promiseStartLegacyDownload(sourceUrl, {
+ isPrivate: true,
+ });
+ await promiseDownloadStopped(download);
+ }
+
+ cleanup();
+});
+
+/**
+ * Checks the startTime gets updated even after a restart.
+ */
+add_task(async function test_cancel_immediately_restart_and_check_startTime() {
+ let download = await promiseStartDownload();
+
+ let startTime = download.startTime;
+ Assert.ok(isValidDate(download.startTime));
+
+ await download.cancel();
+ Assert.equal(download.startTime.getTime(), startTime.getTime());
+
+ // Wait for a timeout.
+ await promiseTimeout(10);
+
+ await download.start();
+ Assert.ok(download.startTime.getTime() > startTime.getTime());
+});
+
+/**
+ * Executes download with content-encoding.
+ */
+add_task(async 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);
+ }
+ registerCleanupFunction(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);
+ });
+
+ let download = await promiseStartDownload(sourceUrl);
+ await promiseDownloadStopped(download);
+
+ Assert.equal(download.progress, 100);
+ Assert.equal(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ // Ensure the content matches the decoded test data.
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+
+ cleanup();
+});
+
+/**
+ * Checks that the file is not decoded if the extension matches the encoding.
+ */
+add_task(async 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);
+ }
+ registerCleanupFunction(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);
+ });
+
+ let download = await promiseStartDownload(sourceUrl);
+ await promiseDownloadStopped(download);
+
+ Assert.equal(download.progress, 100);
+ Assert.equal(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+ Assert.equal(
+ await expectNonZeroDownloadTargetSize(download.target),
+ 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.
+ await promiseVerifyTarget(
+ download.target,
+ String.fromCharCode.apply(String, TEST_DATA_SHORT_GZIP_ENCODED)
+ );
+
+ cleanup();
+});
+
+/**
+ * Cancels and restarts a download sequentially with content-encoding.
+ */
+add_task(async function test_cancel_midway_restart_with_content_encoding() {
+ mustInterruptResponses();
+
+ let download = await promiseStartDownload(httpUrl("interruptible_gzip.txt"));
+
+ // The first time, cancel the download midway.
+ await new Promise(resolve => {
+ let onchange = function () {
+ if (
+ !download.stopped &&
+ !download.canceled &&
+ download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length
+ ) {
+ 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();
+ });
+
+ Assert.ok(download.stopped);
+
+ // The second time, we'll provide the entire interruptible response.
+ continueResponses();
+ download.onchange = null;
+ await download.start();
+
+ Assert.equal(download.progress, 100);
+ Assert.equal(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ await promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Download with parental controls enabled.
+ */
+add_task(async function test_blocked_parental_controls() {
+ let blockFn = base => ({
+ shouldBlockForParentalControls: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ registerCleanupFunction(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = await promiseNewDownload();
+ await 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 = await promiseStartLegacyDownload();
+ await promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ Assert.ok(ex.becauseBlockedByParentalControls);
+ Assert.ok(download.error.becauseBlockedByParentalControls);
+ }
+
+ // Now that the download stopped, the target file should not exist.
+ Assert.equal(false, await IOUtils.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(async function test_blocked_parental_controls_httpstatus450() {
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ download = await promiseNewDownload(httpUrl("parentalblocked.zip"));
+ await download.start();
+ } else {
+ download = await promiseStartLegacyDownload(
+ httpUrl("parentalblocked.zip")
+ );
+ await promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ Assert.ok(ex.becauseBlockedByParentalControls);
+ Assert.ok(download.error.becauseBlockedByParentalControls);
+ Assert.ok(download.stopped);
+ }
+
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+});
+
+/**
+ * Check that DownloadCopySaver can always retrieve the hash.
+ * DownloadLegacySaver can only retrieve the hash when
+ * nsIExternalHelperAppService is invoked.
+ */
+add_task(async function test_getSha256Hash() {
+ if (!gUseLegacySaver) {
+ let download = await promiseStartDownload(httpUrl("source.txt"));
+ await promiseDownloadStopped(download);
+ Assert.ok(download.stopped);
+ Assert.equal(32, download.saver.getSha256Hash().length);
+ }
+});
+
+/**
+ * Checks that application reputation blocks the download and the target file
+ * does not exist.
+ */
+add_task(async function test_blocked_applicationReputation() {
+ let download = await promiseBlockedDownload({
+ keepPartialData: false,
+ keepBlockedData: false,
+ });
+
+ // Now that the download is blocked, the target file should not exist.
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+
+ // There should also be no blocked data in this case
+ Assert.ok(!download.hasBlockedData);
+});
+
+/**
+ * Checks that if a download restarts while processing an application reputation
+ * request, the status is handled correctly.
+ */
+add_task(async 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);
+ }
+ registerCleanupFunction(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 = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+ let firstAttempt = promiseDownloadStopped(download);
+ continueResponses();
+
+ // 4/5. Wait for the first attempt to be completed. The result of this
+ // should appear as a cancellation.
+ await 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.
+ await promiseDownloadStopped(download);
+
+ // 7. At this point, "hasBlockedData" should be false.
+ Assert.ok(!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(async function test_blocked_applicationReputation_confirmBlock() {
+ let download = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ Assert.ok(download.hasBlockedData);
+ Assert.equal((await IOUtils.stat(download.target.path)).size, 0);
+ Assert.ok(await IOUtils.exists(download.target.partFilePath));
+
+ await download.confirmBlock();
+
+ // After confirming the block the download should be in a failed state and
+ // have no downloaded data left on disk.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(!download.hasBlockedData);
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.ok(!download.target.exists);
+ Assert.equal(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(async function test_blocked_applicationReputation_unblock() {
+ let download = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ useLegacySaver: gUseLegacySaver,
+ });
+
+ Assert.ok(download.hasBlockedData);
+ Assert.equal((await IOUtils.stat(download.target.path)).size, 0);
+ Assert.ok(await IOUtils.exists(download.target.partFilePath));
+
+ await download.unblock();
+
+ // After unblocking the download should have succeeded and be
+ // present at the final path.
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.hasBlockedData);
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ await 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.
+ Assert.ok(download.error instanceof Downloads.Error);
+ Assert.ok(download.error.becauseBlocked);
+ Assert.ok(download.error.becauseBlockedByReputationCheck);
+});
+
+/**
+ * Check that calling cancel on a blocked download will not cause errors
+ */
+add_task(async function test_blocked_applicationReputation_cancel() {
+ let download = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ // This call should succeed on a blocked download.
+ await download.cancel();
+
+ // Calling cancel should not have changed the current state, the download
+ // should still be blocked.
+ Assert.ok(download.error.becauseBlockedByReputationCheck);
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(download.hasBlockedData);
+});
+
+/**
+ * Checks that unblock and confirmBlock cannot race on a blocked download
+ */
+add_task(async function test_blocked_applicationReputation_decisionRace() {
+ let download = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ let unblockPromise = download.unblock();
+ let confirmBlockPromise = download.confirmBlock();
+
+ await confirmBlockPromise.then(
+ () => {
+ do_throw("confirmBlock should have failed.");
+ },
+ () => {}
+ );
+
+ await unblockPromise;
+
+ // After unblocking the download should have succeeded and be
+ // present at the final path.
+ Assert.ok(download.stopped);
+ Assert.ok(download.succeeded);
+ Assert.ok(!download.hasBlockedData);
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ Assert.ok(await IOUtils.exists(download.target.path));
+
+ download = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ confirmBlockPromise = download.confirmBlock();
+ unblockPromise = download.unblock();
+
+ await unblockPromise.then(
+ () => {
+ do_throw("unblock should have failed.");
+ },
+ () => {}
+ );
+
+ await confirmBlockPromise;
+
+ // After confirming the block the download should be in a failed state and
+ // have no downloaded data left on disk.
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(!download.hasBlockedData);
+ Assert.equal(false, await IOUtils.exists(download.target.partFilePath));
+ Assert.equal(false, await IOUtils.exists(download.target.path));
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+});
+
+/**
+ * Checks that unblocking a blocked download fails if the blocked data has been
+ * removed.
+ */
+add_task(async function test_blocked_applicationReputation_unblock() {
+ let download = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ Assert.ok(download.hasBlockedData);
+ Assert.ok(await IOUtils.exists(download.target.partFilePath));
+
+ // Remove the blocked data without telling the download.
+ await IOUtils.remove(download.target.partFilePath);
+
+ let unblockPromise = download.unblock();
+ await 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.
+ Assert.ok(!download.hasBlockedData);
+ Assert.ok(download.stopped);
+ Assert.ok(!download.succeeded);
+ Assert.ok(!download.target.exists);
+ Assert.equal(download.target.size, 0);
+});
+
+/**
+ * download.showContainingDirectory() action
+ */
+add_task(async function test_showContainingDirectory() {
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ let download = await Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: "",
+ });
+
+ let promiseDirectoryShown = waitForDirectoryShown();
+ await download.showContainingDirectory();
+ let path = await 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;
+ Assert.ok(validResult);
+ }
+
+ download = await Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: targetPath,
+ });
+
+ promiseDirectoryShown = waitForDirectoryShown();
+ download.showContainingDirectory();
+ await promiseDirectoryShown;
+});
+
+/**
+ * download.launch() action
+ */
+add_task(async 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 = await Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launcherPath,
+ launchWhenSucceeded: true,
+ });
+
+ try {
+ await download.launch();
+ do_throw("Can't launch download file as it has not completed yet");
+ } catch (ex) {
+ Assert.equal(
+ ex.message,
+ "launch can only be called if the download succeeded"
+ );
+ }
+
+ Assert.ok(download.launchWhenSucceeded);
+ await 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 = await promiseStartLegacyDownload(httpUrl("source.txt"), {
+ launcherPath,
+ launchWhenSucceeded: true,
+ });
+ Assert.ok(download.launchWhenSucceeded);
+ await promiseDownloadStopped(download);
+ }
+
+ let promiseFileLaunched = waitForFileLaunched();
+ download.launch();
+ let result = await promiseFileLaunched;
+
+ // Verify that the results match the test case.
+ if (!launcherPath) {
+ // This indicates that the default handler has been chosen.
+ Assert.ok(result === null);
+ } else {
+ // Check the nsIMIMEInfo instance that would have been used for launching.
+ Assert.equal(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
+ Assert.ok(
+ result.preferredApplicationHandler
+ .QueryInterface(Ci.nsILocalHandlerApp)
+ .executable.equals(customLauncher)
+ );
+ }
+ }
+});
+
+/**
+ * Test passing an invalid path as the launcherPath property.
+ */
+add_task(async function test_launcherPath_invalid() {
+ let download = await Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ launcherPath: " ",
+ });
+
+ let promiseDownloadLaunched = new Promise(resolve => {
+ let waitFn = base => {
+ let launchOverride = {
+ launchDownload() {
+ Integration.downloads.unregister(waitFn);
+ let superPromise = super.launchDownload(...arguments);
+ resolve(superPromise);
+ return superPromise;
+ },
+ };
+ Object.setPrototypeOf(launchOverride, base);
+ return launchOverride;
+ };
+ Integration.downloads.register(waitFn);
+ });
+
+ await download.start();
+ try {
+ download.launch();
+ await 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;
+ Assert.ok(validResult);
+ }
+});
+
+/**
+ * Tests that download.launch() is automatically called after
+ * the download finishes if download.launchWhenSucceeded = true
+ */
+add_task(async 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 = await Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launchWhenSucceeded: true,
+ launcherPath,
+ });
+ await download.start();
+ } else {
+ let download = await promiseStartLegacyDownload(httpUrl("source.txt"), {
+ launcherPath,
+ launchWhenSucceeded: true,
+ });
+ await promiseDownloadStopped(download);
+ }
+
+ let result = await promiseFileLaunched;
+
+ // Verify that the results match the test case.
+ if (!launcherPath) {
+ // This indicates that the default handler has been chosen.
+ Assert.ok(result === null);
+ } else {
+ // Check the nsIMIMEInfo instance that would have been used for launching.
+ Assert.equal(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
+ Assert.ok(
+ result.preferredApplicationHandler
+ .QueryInterface(Ci.nsILocalHandlerApp)
+ .executable.equals(customLauncher)
+ );
+ }
+ }
+});
+
+/**
+ * Tests that the proper content type is set for a normal download.
+ */
+add_task(async function test_contentType() {
+ let download = await promiseStartDownload(httpUrl("source.txt"));
+ await promiseDownloadStopped(download);
+
+ Assert.equal("text/plain", download.contentType);
+});
+
+/**
+ * Tests that the serialization/deserialization of the startTime Date
+ * object works correctly.
+ */
+add_task(async function test_toSerializable_startTime() {
+ let download1 = await promiseStartDownload(httpUrl("source.txt"));
+ await promiseDownloadStopped(download1);
+
+ let serializable = download1.toSerializable();
+ let reserialized = JSON.parse(JSON.stringify(serializable));
+
+ let download2 = await Downloads.createDownload(reserialized);
+
+ Assert.equal(download1.startTime.constructor.name, "Date");
+ Assert.equal(download2.startTime.constructor.name, "Date");
+ Assert.equal(download1.startTime.toJSON(), download2.startTime.toJSON());
+});
+
+/**
+ * Checks that downloads are added to browsing history when they start.
+ */
+add_task(async function test_history() {
+ mustInterruptResponses();
+
+ let sourceUrl = httpUrl("interruptible.txt");
+
+ // We will wait for the visit to be notified during the download.
+ await PlacesUtils.history.clear();
+ let promiseVisit = promiseWaitForVisit(sourceUrl);
+
+ // Start a download that is not allowed to finish yet.
+ let download = await promiseStartDownload(sourceUrl);
+ let expectedFile = new FileUtils.File(download.target.path);
+ let expectedFileURI = Services.io.newFileURI(expectedFile);
+ let promiseAnnotation = waitForAnnotation(
+ sourceUrl,
+ "downloads/destinationFileURI",
+ expectedFileURI.spec
+ );
+
+ // The history and annotation notifications should be received before the download completes.
+ let [time, transitionType, lastKnownTitle] = await promiseVisit;
+ await promiseAnnotation;
+
+ Assert.equal(time, download.startTime.getTime());
+ Assert.equal(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+ Assert.equal(lastKnownTitle, expectedFile.leafName);
+
+ let pageInfo = await PlacesUtils.history.fetch(sourceUrl, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get("downloads/destinationFileURI"),
+ expectedFileURI.spec,
+ "Should have saved the correct download target annotation."
+ );
+
+ // Restart and complete the download after clearing history.
+ await PlacesUtils.history.clear();
+ download.cancel();
+ continueResponses();
+ await download.start();
+
+ // The restart should not have added a new history visit.
+ Assert.equal(false, await PlacesUtils.history.hasVisits(sourceUrl));
+});
+
+/**
+ * Checks that downloads started by nsIHelperAppService are added to the
+ * browsing history when they start.
+ */
+add_task(async function test_history_tryToKeepPartialData() {
+ // We will wait for the visit to be notified during the download.
+ await PlacesUtils.history.clear();
+ let promiseVisit = promiseWaitForVisit(
+ httpUrl("interruptible_resumable.txt")
+ );
+
+ // Start a download that is not allowed to finish yet.
+ let beforeStartTimeMs = Date.now();
+ let download = await promiseStartDownload_tryToKeepPartialData({
+ useLegacySaver: gUseLegacySaver,
+ });
+
+ // The history notifications should be received before the download completes.
+ let [time, transitionType] = await promiseVisit;
+ Assert.equal(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.
+ Assert.ok(time >= beforeStartTimeMs - 1000);
+
+ // Complete the download before finishing the test.
+ continueResponses();
+ await promiseDownloadStopped(download);
+});
+
+/**
+ * Checks that finished downloads are not removed.
+ */
+add_task(async function test_download_cancel_retry_finalize() {
+ // Start a download that is not allowed to finish yet.
+ let sourceUrl = httpUrl("interruptible.txt");
+ let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ mustInterruptResponses();
+ let download1 = await Downloads.createDownload({
+ source: sourceUrl,
+ target: { path: targetFilePath, partFilePath: targetFilePath + ".part" },
+ });
+ download1.start().catch(() => {});
+ await promiseDownloadMidway(download1);
+ await promisePartFileReady(download1);
+
+ // Cancel the download and make sure that the partial data do not exist.
+ await download1.cancel();
+ Assert.equal(targetFilePath, download1.target.path);
+ Assert.equal(false, await IOUtils.exists(download1.target.path));
+ Assert.equal(false, await IOUtils.exists(download1.target.partFilePath));
+ continueResponses();
+
+ // Download the same file again with a different download session.
+ let download2 = await Downloads.createDownload({
+ source: sourceUrl,
+ target: { path: targetFilePath, partFilePath: targetFilePath + ".part" },
+ });
+ download2.start().catch(() => {});
+
+ // Wait for download to be completed.
+ await promiseDownloadStopped(download2);
+ Assert.equal(targetFilePath, download2.target.path);
+ Assert.ok(await IOUtils.exists(download2.target.path));
+ Assert.equal(false, await IOUtils.exists(download2.target.partFilePath));
+
+ // Finalize the first download session.
+ await download1.finalize(true);
+
+ // The complete download should not have been removed.
+ Assert.ok(await IOUtils.exists(download2.target.path));
+ Assert.equal(false, await IOUtils.exists(download2.target.partFilePath));
+});
+
+/**
+ * Checks that confirmBlock does not clobber unrelated safe files.
+ */
+add_task(async function test_blocked_removeByHand_confirmBlock() {
+ let download1 = await promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ Assert.ok(download1.hasBlockedData);
+ Assert.equal((await IOUtils.stat(download1.target.path)).size, 0);
+ Assert.ok(await IOUtils.exists(download1.target.partFilePath));
+
+ // Remove the placeholder without telling the download.
+ await IOUtils.remove(download1.target.path);
+ Assert.equal(false, await IOUtils.exists(download1.target.path));
+
+ // Download a file with the same name as the blocked download.
+ let download2 = await Downloads.createDownload({
+ source: httpUrl("interruptible_resumable.txt"),
+ target: {
+ path: download1.target.path,
+ partFilePath: download1.target.path + ".part",
+ },
+ });
+ download2.start().catch(() => {});
+
+ // Wait for download to be completed.
+ await promiseDownloadStopped(download2);
+ Assert.equal(download1.target.path, download2.target.path);
+ Assert.ok(await IOUtils.exists(download2.target.path));
+
+ // Remove the blocked download.
+ await download1.confirmBlock();
+
+ // After confirming the complete download should not have been removed.
+ Assert.ok(await IOUtils.exists(download2.target.path));
+});
+
+/**
+ * Tests that the temp download files are removed on exit and exiting private
+ * mode after they have been launched.
+ */
+add_task(async 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 = await Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: autoDeleteTargetPathOne,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ await autoDeleteDownloadOne.start();
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, true);
+ let autoDeleteDownloadTwo = await Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: autoDeleteTargetPathTwo,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ await autoDeleteDownloadTwo.start();
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, false);
+ let noAutoDeleteDownload = await Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: noAutoDeleteTargetPath,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ await noAutoDeleteDownload.start();
+
+ Services.prefs.clearUserPref(kDeleteTempFileOnExit);
+
+ Assert.ok(await IOUtils.exists(autoDeleteTargetPathOne));
+ Assert.ok(await IOUtils.exists(autoDeleteTargetPathTwo));
+ Assert.ok(await IOUtils.exists(noAutoDeleteTargetPath));
+
+ // Simulate leaving private browsing mode
+ Services.obs.notifyObservers(null, "last-pb-context-exited");
+ Assert.equal(false, await IOUtils.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);
+
+ // The file should still exist following the simulated shutdown.
+ Assert.ok(await IOUtils.exists(autoDeleteTargetPathTwo));
+ Assert.ok(await IOUtils.exists(noAutoDeleteTargetPath));
+});
+
+add_task(async function test_partitionKey() {
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ Services.prefs.setBoolPref("privacy.partition.network_state", true);
+
+ function promiseVerifyDownloadChannel(url, partitionKey) {
+ return TestUtils.topicObserved(
+ "http-on-modify-request",
+ (subject, data) => {
+ let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
+ if (httpChannel.URI.spec != url) {
+ return false;
+ }
+
+ let reqLoadInfo = httpChannel.loadInfo;
+ let cookieJarSettings = reqLoadInfo.cookieJarSettings;
+
+ // Check the partitionKey of the cookieJarSettings.
+ Assert.equal(cookieJarSettings.partitionKey, partitionKey);
+
+ return true;
+ }
+ );
+ }
+
+ let test_url = httpUrl("source.txt");
+ let uri = Services.io.newURI(test_url);
+ let cookieJarSettings = Cc["@mozilla.org/cookieJarSettings;1"].createInstance(
+ Ci.nsICookieJarSettings
+ );
+ cookieJarSettings.initWithURI(uri, false);
+ let expectedPartitionKey = cookieJarSettings.partitionKey;
+
+ let verifyPromise;
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can check its basic properties before it starts.
+ download = await Downloads.createDownload({
+ source: { url: test_url, cookieJarSettings },
+ target: { path: targetFile.path },
+ saver: { type: "copy" },
+ });
+
+ Assert.equal(download.source.url, test_url);
+ Assert.equal(download.target.path, targetFile.path);
+
+ verifyPromise = promiseVerifyDownloadChannel(
+ test_url,
+ expectedPartitionKey
+ );
+
+ await download.start();
+ } else {
+ verifyPromise = promiseVerifyDownloadChannel(
+ test_url,
+ expectedPartitionKey
+ );
+
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we must check its basic properties while in progress.
+ download = await promiseStartLegacyDownload(null, {
+ targetFile,
+ cookieJarSettings,
+ });
+
+ Assert.equal(download.source.url, test_url);
+ Assert.equal(download.target.path, targetFile.path);
+
+ await promiseDownloadStopped(download);
+ }
+
+ await verifyPromise;
+
+ Services.prefs.clearUserPref("privacy.partition.network_state");
+});