summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js685
1 files changed, 685 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
new file mode 100644
index 0000000000..e2867d1f03
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
@@ -0,0 +1,685 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+
+const gServer = createHttpServer();
+gServer.registerDirectory("/data/", do_get_file("data"));
+
+gServer.registerPathHandler("/dir/", (_, res) => res.write("length=8"));
+
+const WINDOWS = AppConstants.platform == "win";
+
+const BASE = `http://localhost:${gServer.identity.primaryPort}/`;
+const FILE_NAME = "file_download.txt";
+const FILE_NAME_W_SPACES = "file download.txt";
+const FILE_URL = BASE + "data/" + FILE_NAME;
+const FILE_NAME_UNIQUE = "file_download(1).txt";
+const FILE_LEN = 46;
+
+let downloadDir;
+
+function joinPath(...components) {
+ const separator = WINDOWS ? "\\" : "/";
+
+ return components.join(separator);
+}
+
+function setup() {
+ downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(
+ Ci.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY
+ );
+ info(`Using download directory ${downloadDir.path}`);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue(
+ "browser.download.dir",
+ Ci.nsIFile,
+ downloadDir
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+
+ let entries = downloadDir.directoryEntries;
+ while (entries.hasMoreElements()) {
+ let entry = entries.nextFile;
+ ok(false, `Leftover file ${entry.path} in download directory`);
+ entry.remove(false);
+ }
+
+ downloadDir.remove(false);
+ });
+}
+
+function backgroundScript() {
+ let blobUrl;
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == "download.request") {
+ let options = args[0];
+
+ if (options.blobme) {
+ let blob = new Blob(options.blobme);
+ delete options.blobme;
+ blobUrl = options.url = window.URL.createObjectURL(blob);
+ }
+
+ try {
+ let id = await browser.downloads.download(options);
+ browser.test.sendMessage("download.done", { status: "success", id });
+ } catch (error) {
+ browser.test.sendMessage("download.done", {
+ status: "error",
+ errmsg: error.message,
+ });
+ }
+ } else if (msg == "killTheBlob") {
+ window.URL.revokeObjectURL(blobUrl);
+ blobUrl = null;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+// This function is a bit of a sledgehammer, it looks at every download
+// the browser knows about and waits for all active downloads to complete.
+// But we only start one at a time and only do a handful in total, so
+// this lets us test download() without depending on anything else.
+async function waitForDownloads() {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ let inprogress = downloads.filter(dl => !dl.stopped);
+ return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
+}
+
+// Create a file in the downloads directory.
+function touch(filename) {
+ let file = downloadDir.clone();
+ file.append(filename);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+}
+
+// Remove a file in the downloads directory.
+function remove(filename, recursive = false) {
+ let file = downloadDir.clone();
+ file.append(filename);
+ file.remove(recursive);
+}
+
+add_task(async function test_downloads() {
+ setup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ incognitoOverride: "spanning",
+ });
+
+ function download(options) {
+ extension.sendMessage("download.request", options);
+ return extension.awaitMessage("download.done");
+ }
+
+ async function testDownload(options, localFile, expectedSize, description) {
+ let msg = await download(options);
+ equal(
+ msg.status,
+ "success",
+ `downloads.download() works with ${description}`
+ );
+
+ await waitForDownloads();
+
+ let localPath = downloadDir.clone();
+ let parts = Array.isArray(localFile) ? localFile : [localFile];
+
+ parts.map(p => localPath.append(p));
+ equal(
+ localPath.fileSize,
+ expectedSize,
+ "Downloaded file has expected size"
+ );
+ localPath.remove(false);
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ info("extension started");
+
+ // Call download() with just the url property.
+ await testDownload({ url: FILE_URL }, FILE_NAME, FILE_LEN, "just source");
+
+ // Call download() with a filename property.
+ await testDownload(
+ {
+ url: FILE_URL,
+ filename: "newpath.txt",
+ },
+ "newpath.txt",
+ FILE_LEN,
+ "source and filename"
+ );
+
+ // Call download() with a filename with subdirs.
+ await testDownload(
+ {
+ url: FILE_URL,
+ filename: "sub/dir/file",
+ },
+ ["sub", "dir", "file"],
+ FILE_LEN,
+ "source and filename with subdirs"
+ );
+
+ // Call download() with a filename with existing subdirs.
+ await testDownload(
+ {
+ url: FILE_URL,
+ filename: "sub/dir/file2",
+ },
+ ["sub", "dir", "file2"],
+ FILE_LEN,
+ "source and filename with existing subdirs"
+ );
+
+ // Only run Windows path separator test on Windows.
+ if (WINDOWS) {
+ // Call download() with a filename with Windows path separator.
+ await testDownload(
+ {
+ url: FILE_URL,
+ filename: "sub\\dir\\file3",
+ },
+ ["sub", "dir", "file3"],
+ FILE_LEN,
+ "filename with Windows path separator"
+ );
+ }
+ remove("sub", true);
+
+ // Call download(), filename with subdir, skipping parts.
+ await testDownload(
+ {
+ url: FILE_URL,
+ filename: "skip//part",
+ },
+ ["skip", "part"],
+ FILE_LEN,
+ "source, filename, with subdir, skipping parts"
+ );
+ remove("skip", true);
+
+ // Check conflictAction of "uniquify".
+ touch(FILE_NAME);
+ await testDownload(
+ {
+ url: FILE_URL,
+ conflictAction: "uniquify",
+ },
+ FILE_NAME_UNIQUE,
+ FILE_LEN,
+ "conflictAction=uniquify"
+ );
+ // todo check that preexisting file was not modified?
+ remove(FILE_NAME);
+
+ // Check conflictAction of "overwrite".
+ touch(FILE_NAME);
+ await testDownload(
+ {
+ url: FILE_URL,
+ conflictAction: "overwrite",
+ },
+ FILE_NAME,
+ FILE_LEN,
+ "conflictAction=overwrite"
+ );
+
+ // Try to download in invalid url
+ await download({ url: "this is not a valid URL" }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with invalid url");
+ ok(
+ /not a valid URL/.test(msg.errmsg),
+ "error message for invalid url is correct"
+ );
+ });
+
+ // Try to download to an empty path.
+ await download({
+ url: FILE_URL,
+ filename: "",
+ }).then(msg => {
+ equal(
+ msg.status,
+ "error",
+ "downloads.download() fails with empty filename"
+ );
+ equal(
+ msg.errmsg,
+ "filename must not be empty",
+ "error message for empty filename is correct"
+ );
+ });
+
+ // Try to download to an absolute path.
+ const absolutePath = PathUtils.join(
+ WINDOWS ? "C:\\tmp" : "/tmp",
+ "file_download.txt"
+ );
+ await download({
+ url: FILE_URL,
+ filename: absolutePath,
+ }).then(msg => {
+ equal(
+ msg.status,
+ "error",
+ "downloads.download() fails with absolute filename"
+ );
+ equal(
+ msg.errmsg,
+ "filename must not be an absolute path",
+ `error message for absolute path (${absolutePath}) is correct`
+ );
+ });
+
+ if (WINDOWS) {
+ await download({
+ url: FILE_URL,
+ filename: "C:\\file_download.txt",
+ }).then(msg => {
+ equal(
+ msg.status,
+ "error",
+ "downloads.download() fails with absolute filename"
+ );
+ equal(
+ msg.errmsg,
+ "filename must not be an absolute path",
+ "error message for absolute path with drive letter is correct"
+ );
+ });
+ }
+
+ // Try to download to a relative path containing ..
+ await download({
+ url: FILE_URL,
+ filename: joinPath("..", "file_download.txt"),
+ }).then(msg => {
+ equal(
+ msg.status,
+ "error",
+ "downloads.download() fails with back-references"
+ );
+ equal(
+ msg.errmsg,
+ "filename must not contain back-references (..)",
+ "error message for back-references is correct"
+ );
+ });
+
+ // Try to download to a long relative path containing ..
+ await download({
+ url: FILE_URL,
+ filename: joinPath("foo", "..", "..", "file_download.txt"),
+ }).then(msg => {
+ equal(
+ msg.status,
+ "error",
+ "downloads.download() fails with back-references"
+ );
+ equal(
+ msg.errmsg,
+ "filename must not contain back-references (..)",
+ "error message for back-references is correct"
+ );
+ });
+
+ // Test illegal characters.
+ await download({
+ url: FILE_URL,
+ filename: "like:this",
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with illegal chars");
+ equal(
+ msg.errmsg,
+ "filename must not contain illegal characters",
+ "error message correct"
+ );
+ });
+
+ // Try to download a blob url
+ const BLOB_STRING = "Hello, world";
+ await testDownload(
+ {
+ blobme: [BLOB_STRING],
+ filename: FILE_NAME,
+ },
+ FILE_NAME,
+ BLOB_STRING.length,
+ "blob url"
+ );
+ extension.sendMessage("killTheBlob");
+
+ // Try to download a blob url without a given filename
+ await testDownload(
+ {
+ blobme: [BLOB_STRING],
+ },
+ "download",
+ BLOB_STRING.length,
+ "blob url with no filename"
+ );
+ extension.sendMessage("killTheBlob");
+
+ // Download a normal URL with an empty filename part.
+ await testDownload(
+ {
+ url: BASE + "dir/",
+ },
+ "download",
+ 8,
+ "normal url with empty filename"
+ );
+
+ // Download a filename with multiple spaces, url is ignored for this test.
+ await testDownload(
+ {
+ url: FILE_URL,
+ filename: "a file.txt",
+ },
+ "a file.txt",
+ FILE_LEN,
+ "filename with multiple spaces"
+ );
+
+ // Download a normal URL with a leafname containing multiple spaces.
+ // Note: spaces are compressed by file name normalization.
+ await testDownload(
+ {
+ url: BASE + "data/" + FILE_NAME_W_SPACES,
+ },
+ FILE_NAME_W_SPACES.replace(/\s+/, " "),
+ FILE_LEN,
+ "leafname with multiple spaces"
+ );
+
+ // Check that the "incognito" property is supported.
+ await testDownload(
+ {
+ url: FILE_URL,
+ incognito: false,
+ },
+ FILE_NAME,
+ FILE_LEN,
+ "incognito=false"
+ );
+
+ await testDownload(
+ {
+ url: FILE_URL,
+ incognito: true,
+ },
+ FILE_NAME,
+ FILE_LEN,
+ "incognito=true"
+ );
+
+ await extension.unload();
+});
+
+async function testHttpErrors(allowHttpErrors) {
+ const server = createHttpServer();
+ const url = `http://localhost:${server.identity.primaryPort}/error`;
+ const content = "HTTP Error test";
+
+ server.registerPathHandler("/error", (request, response) => {
+ response.setStatusLine(
+ "1.1",
+ parseInt(request.queryString, 10),
+ "Some Error"
+ );
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Content-Length", content.length.toString());
+ response.write(content);
+ });
+
+ function background(code) {
+ let dlid = 0;
+ let expectedState;
+ browser.test.onMessage.addListener(async options => {
+ try {
+ expectedState = options.allowHttpErrors ? "complete" : "interrupted";
+ dlid = await browser.downloads.download(options);
+ } catch (err) {
+ browser.test.fail(`Unexpected error in downloads.download(): ${err}`);
+ }
+ });
+ function onChanged({ id, state }) {
+ if (dlid !== id || !state || state.current === "in_progress") {
+ return;
+ }
+ browser.test.assertEq(state.current, expectedState, "correct state");
+ browser.downloads.search({ id }).then(([download]) => {
+ browser.test.sendMessage("done", download.error);
+ });
+ }
+ browser.downloads.onChanged.addListener(onChanged);
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["downloads"],
+ },
+ background,
+ });
+ await extension.startup();
+
+ async function download(code, expected_when_disallowed) {
+ const options = {
+ url: url + "?" + code,
+ filename: `test-${code}`,
+ conflictAction: "overwrite",
+ allowHttpErrors,
+ };
+ extension.sendMessage(options);
+ const rv = await extension.awaitMessage("done");
+
+ if (allowHttpErrors) {
+ const localPath = downloadDir.clone();
+ localPath.append(options.filename);
+ equal(
+ localPath.fileSize,
+ // The 20x No content errors will not produce any response body,
+ // only "true" errors do.
+ code >= 400 ? content.length : 0,
+ "Downloaded file has expected size" + code
+ );
+ localPath.remove(false);
+
+ ok(!rv, "error must be ignored and hence false-y");
+ return;
+ }
+
+ equal(
+ rv,
+ expected_when_disallowed,
+ "error must have the correct InterruptReason"
+ );
+ }
+
+ await download(204, "SERVER_BAD_CONTENT"); // No Content
+ await download(205, "SERVER_BAD_CONTENT"); // Reset Content
+ await download(404, "SERVER_BAD_CONTENT"); // Not Found
+ await download(403, "SERVER_FORBIDDEN"); // Forbidden
+ await download(402, "SERVER_UNAUTHORIZED"); // Unauthorized
+ await download(407, "SERVER_UNAUTHORIZED"); // Proxy auth required
+ await download(504, "SERVER_FAILED"); //General errors, here Gateway Timeout
+
+ await extension.unload();
+}
+
+add_task(function test_download_disallowed_http_errors() {
+ return testHttpErrors(false);
+});
+
+add_task(function test_download_allowed_http_errors() {
+ return testHttpErrors(true);
+});
+
+add_task(async function test_download_http_details() {
+ const server = createHttpServer();
+ const url = `http://localhost:${server.identity.primaryPort}/post-log`;
+
+ let received;
+ server.registerPathHandler("/post-log", (request, response) => {
+ received = request;
+ response.setHeader("Set-Cookie", "monster=", false);
+ });
+
+ // Confirm received vs. expected values.
+ function confirm(method, headers = {}, body) {
+ equal(received.method, method, "method is correct");
+
+ for (let name in headers) {
+ ok(received.hasHeader(name), `header ${name} received`);
+ equal(
+ received.getHeader(name),
+ headers[name],
+ `header ${name} is correct`
+ );
+ }
+
+ if (body) {
+ const str = NetUtil.readInputStreamToString(
+ received.bodyInputStream,
+ received.bodyInputStream.available()
+ );
+ equal(str, body, "body is correct");
+ }
+ }
+
+ function background() {
+ browser.test.onMessage.addListener(async options => {
+ try {
+ await browser.downloads.download(options);
+ } catch (err) {
+ browser.test.sendMessage("done", { err: err.message });
+ }
+ });
+ browser.downloads.onChanged.addListener(({ state }) => {
+ if (state && state.current === "complete") {
+ browser.test.sendMessage("done", { ok: true });
+ }
+ });
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["downloads"],
+ },
+ background,
+ incognitoOverride: "spanning",
+ });
+ await extension.startup();
+
+ function download(options) {
+ options.url = url;
+ options.conflictAction = "overwrite";
+
+ extension.sendMessage(options);
+ return extension.awaitMessage("done");
+ }
+
+ // Test that site cookies are sent with download requests,
+ // and "incognito" downloads use a separate cookie jar.
+ let testDownloadCookie = async function (incognito) {
+ let result = await download({ incognito });
+ ok(result.ok, `preflight to set cookies with incognito=${incognito}`);
+ ok(!received.hasHeader("cookie"), "first request has no cookies");
+
+ result = await download({ incognito });
+ ok(result.ok, `download with cookie with incognito=${incognito}`);
+ equal(
+ received.getHeader("cookie"),
+ "monster=",
+ "correct cookie header sent for second download"
+ );
+ };
+
+ await testDownloadCookie(false);
+ await testDownloadCookie(true);
+
+ // Test method option.
+ let result = await download({});
+ ok(result.ok, "download works without the method option, defaults to GET");
+ confirm("GET");
+
+ result = await download({ method: "PUT" });
+ ok(!result.ok, "download rejected with PUT method");
+ ok(
+ /method: Invalid enumeration/.test(result.err),
+ "descriptive error message"
+ );
+
+ result = await download({ method: "POST" });
+ ok(result.ok, "download works with POST method");
+ confirm("POST");
+
+ // Test body option values.
+ result = await download({ body: [] });
+ ok(!result.ok, "download rejected because of non-string body");
+ ok(/body: Expected string/.test(result.err), "descriptive error message");
+
+ result = await download({ method: "POST", body: "of work" });
+ ok(result.ok, "download works with POST method and body");
+ confirm("POST", { "Content-Length": 7 }, "of work");
+
+ // Test custom headers.
+ result = await download({ headers: [{ name: "X-Custom" }] });
+ ok(!result.ok, "download rejected because of missing header value");
+ ok(/"value" is required/.test(result.err), "descriptive error message");
+
+ result = await download({ headers: [{ name: "X-Custom", value: "13" }] });
+ ok(result.ok, "download works with a custom header");
+ confirm("GET", { "X-Custom": "13" });
+
+ // Test Referer header.
+ const referer = "http://example.org/test";
+ result = await download({ headers: [{ name: "Referer", value: referer }] });
+ ok(result.ok, "download works with Referer header");
+ confirm("GET", { Referer: referer });
+
+ // Test forbidden headers.
+ result = await download({ headers: [{ name: "DNT", value: "1" }] });
+ ok(!result.ok, "download rejected because of forbidden header name DNT");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ result = await download({
+ headers: [{ name: "Proxy-Connection", value: "keep" }],
+ });
+ ok(
+ !result.ok,
+ "download rejected because of forbidden header name prefix Proxy-"
+ );
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ result = await download({ headers: [{ name: "Sec-ret", value: "13" }] });
+ ok(
+ !result.ok,
+ "download rejected because of forbidden header name prefix Sec-"
+ );
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ remove("post-log");
+ await extension.unload();
+});