diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/downloads/test/unit/common_test_Download.js | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/downloads/test/unit/common_test_Download.js')
-rw-r--r-- | toolkit/components/downloads/test/unit/common_test_Download.js | 2751 |
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"); +}); |