/* -*- 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 = () => ({ 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 = () => ({ 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 = await IOUtils.getFile( Services.dirsvc.get("LocalAppData", Ci.nsIFile).path, "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 = await IOUtils.getFile( Services.dirsvc.get("LocalAppData", Ci.nsIFile).path, "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,"; 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 = Promise.withResolvers(); // 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() {} ); // 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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 = () => ({ 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 = () => ({ 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 => { 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"); });