3057 lines
99 KiB
JavaScript
3057 lines
99 KiB
JavaScript
/* -*- 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,<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.setByteStringData(postData);
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The `toolkit/components/downloads` tests use a custom partial override of the
|
|
* directory service via `Integration.downloads`, defined in `./head.js`. The
|
|
* `nsIExternalHelperAppService` has no way of knowing about that, though, so
|
|
* we'll need to disable the override for the comparison test.
|
|
*
|
|
* This wrapper does that -- and more importantly, ensures that the override is
|
|
* reenabled afterwards, regardless of how the test exited.
|
|
*
|
|
* (Ideally we'd do this inside the test by registering a test-specific cleanup
|
|
* function, as with SimpleTest's `registerCurrentTaskCleanupFunction() -- but,
|
|
* at time of writing, the xpcshell test setup doesn't support those.)
|
|
*
|
|
* @param {() => any} innerTask - The task to be run with directory-service
|
|
* overrides deactivated.
|
|
*/
|
|
function allowDirectoriesDuring(innerTask) {
|
|
return async () => {
|
|
DownloadIntegration.allowDirectories = true;
|
|
try {
|
|
return await innerTask();
|
|
} finally {
|
|
DownloadIntegration.allowDirectories = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check that `DownloadIntegration` and the `nsIExternalHelperAppService` agree
|
|
* concerning the value of `getPreferredDownloadsDirectory()`.
|
|
*/
|
|
add_task(
|
|
{
|
|
// nsIExternalHelperAppService (q.v.) doesn't implement this on Android
|
|
skip_if: AppConstants.platform === "android",
|
|
},
|
|
allowDirectoriesDuring(
|
|
async function test_preferredDownloadsDirectoryMatches() {
|
|
const integrationDirectory =
|
|
await DownloadIntegration.getPreferredDownloadsDirectory();
|
|
Assert.ok(
|
|
!integrationDirectory || typeof integrationDirectory === "string"
|
|
);
|
|
|
|
/** @type nsIExternalHelperAppService */
|
|
const extHelperAppSvc = Cc[
|
|
"@mozilla.org/uriloader/external-helper-app-service;1"
|
|
].getService(Ci.nsIExternalHelperAppService);
|
|
|
|
const externalHelperDirectory =
|
|
extHelperAppSvc.getPreferredDownloadsDirectory();
|
|
Assert.ok(
|
|
!externalHelperDirectory ||
|
|
externalHelperDirectory.QueryInterface(Ci.nsIFile)
|
|
);
|
|
|
|
Assert.equal(!externalHelperDirectory, !integrationDirectory);
|
|
if (externalHelperDirectory) {
|
|
Assert.equal(externalHelperDirectory.path, integrationDirectory);
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Confirm that the mocking layer has been reenabled after the previous test.
|
|
*/
|
|
add_task(async function test_allowDirectoriesIsOnlyLocallySet() {
|
|
Assert.ok(!DownloadIntegration.allowDirectories);
|
|
Assert.equal(
|
|
DownloadIntegration._getDirectory("TmpD"),
|
|
DownloadIntegration._getDirectory("Home")
|
|
);
|
|
});
|
|
|
|
/**
|
|
* 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 with launcherPath
|
|
*/
|
|
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)
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* download.launch() action with launcherId
|
|
*/
|
|
add_task(
|
|
{
|
|
skip_if: () => mozinfo.os != "linux", // This test is relevant only for Linux.
|
|
},
|
|
async function test_launch_id() {
|
|
let customLauncherId = "org.gnome.gedit.desktop";
|
|
|
|
// Test both with and without setting a custom application.
|
|
for (let launcherId of [null, customLauncherId]) {
|
|
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,
|
|
launcherId,
|
|
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"), {
|
|
launcherId,
|
|
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 (!launcherId) {
|
|
// 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.
|
|
let launcher = Cc["@mozilla.org/gio-service;1"]
|
|
.getService(Ci.nsIGIOService)
|
|
.createHandlerAppFromAppId(launcherId);
|
|
|
|
Assert.equal(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
|
|
Assert.ok(
|
|
result.preferredApplicationHandler
|
|
.QueryInterface(Ci.nsIGIOHandlerApp)
|
|
.equals(launcher)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Test passing an invalid id as the launcherId property.
|
|
*/
|
|
add_task(
|
|
{
|
|
skip_if: () => mozinfo.os != "linux", // This test is relevant only for Linux.
|
|
},
|
|
async function test_launcherId_invalid() {
|
|
let download = await Downloads.createDownload({
|
|
source: { url: httpUrl("source.txt") },
|
|
target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
|
|
launcherId: " ",
|
|
});
|
|
todo_check_true(false, "Size should not be zero.");
|
|
|
|
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;
|
|
}
|
|
let validResult = 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 download.launch() is automatically called after
|
|
* the download finishes if download.launchWhenSucceeded = true
|
|
*
|
|
* This is version when using launcherId property instead of launcherPath.
|
|
*/
|
|
add_task(
|
|
{
|
|
skip_if: () => mozinfo.os != "linux", // This test is relevant only for Linux.
|
|
},
|
|
async function test_launchWhenSucceeded() {
|
|
let customLauncherId = "org.gnome.gedit.desktop";
|
|
|
|
// Test both with and without setting a custom application.
|
|
for (let launcherId of [null, customLauncherId]) {
|
|
let promiseFileLaunched = waitForFileLaunched();
|
|
|
|
if (!gUseLegacySaver) {
|
|
let download = await Downloads.createDownload({
|
|
source: httpUrl("source.txt"),
|
|
target: getTempFile(TEST_TARGET_FILE_NAME).path,
|
|
launchWhenSucceeded: true,
|
|
launcherId,
|
|
});
|
|
await download.start();
|
|
} else {
|
|
let download = await promiseStartLegacyDownload(httpUrl("source.txt"), {
|
|
launcherId,
|
|
launchWhenSucceeded: true,
|
|
});
|
|
await promiseDownloadStopped(download);
|
|
}
|
|
|
|
let result = await promiseFileLaunched;
|
|
|
|
// Verify that the results match the test case.
|
|
if (!launcherId) {
|
|
// This indicates that the default handler has been chosen.
|
|
Assert.strictEqual(result, null, "Result expected to be null");
|
|
} else {
|
|
// Check the nsIMIMEInfo instance that would have been used for launching.
|
|
Assert.equal(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
|
|
let launcher = Cc["@mozilla.org/gio-service;1"]
|
|
.getService(Ci.nsIGIOService)
|
|
.createHandlerAppFromAppId(launcherId);
|
|
Assert.ok(
|
|
result.preferredApplicationHandler
|
|
.QueryInterface(Ci.nsIGIOHandlerApp)
|
|
.equals(launcher)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
/**
|
|
* 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));
|
|
});
|
|
|
|
/**
|
|
* Tests that the temp download files are removed on exit and exiting private
|
|
* mode after they have been launched.
|
|
*
|
|
* This is version when using launcherId property instead of launcherPath.
|
|
*/
|
|
add_task(
|
|
async function test_launchWhenSucceeded_deleteTempFileOnExit_with_launcherId() {
|
|
let customLauncherId = "org.gnome.gedit.desktop";
|
|
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,
|
|
launcherId: customLauncherId,
|
|
});
|
|
await autoDeleteDownloadOne.start();
|
|
|
|
Services.prefs.setBoolPref(kDeleteTempFileOnExit, true);
|
|
let autoDeleteDownloadTwo = await Downloads.createDownload({
|
|
source: httpUrl("source.txt"),
|
|
target: autoDeleteTargetPathTwo,
|
|
launchWhenSucceeded: true,
|
|
launcherId: customLauncherId,
|
|
});
|
|
await autoDeleteDownloadTwo.start();
|
|
|
|
Services.prefs.setBoolPref(kDeleteTempFileOnExit, false);
|
|
let noAutoDeleteDownload = await Downloads.createDownload({
|
|
source: httpUrl("source.txt"),
|
|
target: noAutoDeleteTargetPath,
|
|
launchWhenSucceeded: true,
|
|
launcherId: customLauncherId,
|
|
});
|
|
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");
|
|
});
|