summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js1169
1 files changed, 1169 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
new file mode 100644
index 0000000000..d315d37aaa
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -0,0 +1,1169 @@
+/* -*- 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 { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const ROOT = `http://localhost:${server.identity.primaryPort}`;
+const BASE = `${ROOT}/data`;
+const TXT_FILE = "file_download.txt";
+const TXT_URL = BASE + "/" + TXT_FILE;
+
+// Keep these in sync with code in interruptible.sjs
+const INT_PARTIAL_LEN = 15;
+const INT_TOTAL_LEN = 31;
+
+const TEST_DATA = "This is 31 bytes of sample data";
+const TOTAL_LEN = TEST_DATA.length;
+const PARTIAL_LEN = 15;
+
+// A handler to let us systematically test pausing/resuming/canceling
+// of downloads. This target represents a small text file but a simple
+// GET will stall after sending part of the data, to give the test code
+// a chance to pause or do other operations on an in-progress download.
+// A resumed download (ie, a GET with a Range: header) will allow the
+// download to complete.
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ if (request.hasHeader("Range")) {
+ let start, end;
+ let matches = request
+ .getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ if (matches != null) {
+ start = matches[1] ? parseInt(matches[1], 10) : 0;
+ end = matches[2] ? parseInt(matches[2], 10) : TOTAL_LEN - 1;
+ }
+
+ if (end == undefined || end >= TOTAL_LEN) {
+ response.setStatusLine(
+ request.httpVersion,
+ 416,
+ "Requested Range Not Satisfiable"
+ );
+ response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
+ response.finish();
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
+ response.write(TEST_DATA.slice(start, end + 1));
+ } else if (request.queryString.includes("stream")) {
+ response.processAsync();
+ response.setHeader("Content-Length", "10000", false);
+ response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ setInterval(() => {
+ response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ }, 50);
+ } else {
+ response.processAsync();
+ response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
+ response.write(TEST_DATA.slice(0, PARTIAL_LEN));
+ }
+
+ registerCleanupFunction(() => {
+ try {
+ response.finish();
+ } catch (e) {
+ // This will throw, but we don't care at this point.
+ }
+ });
+}
+
+server.registerPrefixHandler("/interruptible/", handleRequest);
+
+let interruptibleCount = 0;
+function getInterruptibleUrl(filename = "interruptible.html") {
+ let n = interruptibleCount++;
+ return `${ROOT}/interruptible/${filename}?count=${n}`;
+}
+
+function backgroundScript() {
+ let events = new Set();
+ let eventWaiter = null;
+
+ browser.downloads.onCreated.addListener(data => {
+ events.add({ type: "onCreated", data });
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ browser.downloads.onChanged.addListener(data => {
+ events.add({ type: "onChanged", data });
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ browser.downloads.onErased.addListener(data => {
+ events.add({ type: "onErased", data });
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ // Returns a promise that will resolve when the given list of expected
+ // events have all been seen. By default, succeeds only if the exact list
+ // of expected events is seen in the given order. options.exact can be
+ // set to false to allow other events and options.inorder can be set to
+ // false to allow the events to arrive in any order.
+ function waitForEvents(expected, options = {}) {
+ function compare(a, b) {
+ if (typeof b == "object" && b != null) {
+ if (typeof a != "object") {
+ return false;
+ }
+ return Object.keys(b).every(fld => compare(a[fld], b[fld]));
+ }
+ return a == b;
+ }
+
+ const exact = "exact" in options ? options.exact : true;
+ const inorder = "inorder" in options ? options.inorder : true;
+ return new Promise((resolve, reject) => {
+ function check() {
+ function fail(msg) {
+ browser.test.fail(msg);
+ reject(new Error(msg));
+ }
+ if (events.size < expected.length) {
+ return;
+ }
+ if (exact && expected.length < events.size) {
+ fail(
+ `Got ${events.size} events but only expected ${expected.length}`
+ );
+ return;
+ }
+
+ let remaining = new Set(events);
+ if (inorder) {
+ for (let event of events) {
+ if (compare(event, expected[0])) {
+ expected.shift();
+ remaining.delete(event);
+ }
+ }
+ } else {
+ expected = expected.filter(val => {
+ for (let remainingEvent of remaining) {
+ if (compare(remainingEvent, val)) {
+ remaining.delete(remainingEvent);
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+
+ // Events that did occur have been removed from expected so if
+ // expected is empty, we're done. If we didn't see all the
+ // expected events and we're not looking for an exact match,
+ // then we just may not have seen the event yet, so return without
+ // failing and check() will be called again when a new event arrives.
+ if (!expected.length) {
+ events = remaining;
+ eventWaiter = null;
+ resolve();
+ } else if (exact) {
+ fail(
+ `Mismatched event: expecting ${JSON.stringify(
+ expected[0]
+ )} but got ${JSON.stringify(Array.from(remaining)[0])}`
+ );
+ }
+ }
+ eventWaiter = check;
+ check();
+ });
+ }
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ let match = msg.match(/(\w+).request$/);
+ if (!match) {
+ return;
+ }
+
+ let what = match[1];
+ if (what == "waitForEvents") {
+ try {
+ await waitForEvents(...args);
+ browser.test.sendMessage("waitForEvents.done", { status: "success" });
+ } catch (error) {
+ browser.test.sendMessage("waitForEvents.done", {
+ status: "error",
+ errmsg: error.message,
+ });
+ }
+ } else if (what == "clearEvents") {
+ events = new Set();
+ browser.test.sendMessage("clearEvents.done", { status: "success" });
+ } else {
+ try {
+ let result = await browser.downloads[what](...args);
+ browser.test.sendMessage(`${what}.done`, { status: "success", result });
+ } catch (error) {
+ browser.test.sendMessage(`${what}.done`, {
+ status: "error",
+ errmsg: error.message,
+ });
+ }
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+let downloadDir;
+let extension;
+
+async function waitForCreatedPartFile(baseFilename = "interruptible.html") {
+ const partFilePath = PathUtils.join(downloadDir.path, `${baseFilename}.part`);
+
+ info(`Wait for ${partFilePath} to be created`);
+ let lastError;
+ await TestUtils.waitForCondition(
+ () =>
+ IOUtils.exists(partFilePath).catch(err => {
+ lastError = err;
+ return false;
+ }),
+ `Wait for the ${partFilePath} to exists before pausing the download`
+ ).catch(err => {
+ if (lastError) {
+ throw lastError;
+ }
+ throw err;
+ });
+}
+
+async function clearDownloads() {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ await Promise.all(
+ downloads.map(async download => {
+ await download.finalize(true);
+ list.remove(download);
+ })
+ );
+
+ return downloads;
+}
+
+function runInExtension(what, ...args) {
+ extension.sendMessage(`${what}.request`, ...args);
+ return extension.awaitMessage(`${what}.done`);
+}
+
+// This is pretty simplistic, it looks for a progress update for a
+// download of the given url in which the total bytes are exactly equal
+// to the given value. Unless you know exactly how data will arrive from
+// the server (eg see interruptible.sjs), it probably isn't very useful.
+async function waitForProgress(url, testFn) {
+ let list = await Downloads.getList(Downloads.ALL);
+
+ return new Promise(resolve => {
+ const view = {
+ onDownloadChanged(download) {
+ if (download.source.url == url && testFn(download.currentBytes)) {
+ list.removeView(view);
+ resolve(download.currentBytes);
+ }
+ },
+ };
+ list.addView(view);
+ });
+}
+
+add_setup(async () => {
+ const nsIFile = Ci.nsIFile;
+ downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ info(`downloadDir ${downloadDir.path}`);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ await clearDownloads();
+ downloadDir.remove(true);
+ });
+
+ await clearDownloads().then(downloads => {
+ info(`removed ${downloads.length} pre-existing downloads from history`);
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ registerCleanupFunction(async () => {
+ await extension.unload();
+ });
+});
+
+add_task(async function test_events() {
+ let msg = await runInExtension("download", { url: TXT_URL });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id, url: TXT_URL } },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onCreated and onChanged events");
+});
+
+add_task(async function test_cancel() {
+ let url = getInterruptibleUrl();
+ info(url);
+ let msg = await runInExtension("download", { url });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id } },
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ await progressPromise;
+ info(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = await runInExtension("cancel", id);
+ equal(msg.status, "success", "cancel() succeeded");
+
+ // TODO bug 1256243: This sequence of events is bogus
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ paused: {
+ previous: true,
+ current: false,
+ },
+ },
+ },
+ ]);
+ equal(
+ msg.status,
+ "success",
+ "got onChanged events corresponding to cancel()"
+ );
+
+ msg = await runInExtension("search", { error: "USER_CANCELED" });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(
+ msg.result[0].estimatedEndTime,
+ null,
+ "download.estimatedEndTime is correct"
+ );
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(
+ msg.result[0].totalBytes,
+ INT_TOTAL_LEN,
+ "download.totalBytes is correct"
+ );
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = await runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause a canceled download");
+
+ msg = await runInExtension("resume", id);
+ equal(msg.status, "error", "cannot resume a canceled download");
+});
+
+add_task(async function test_pauseresume() {
+ const filename = "pauseresume.html";
+ let url = getInterruptibleUrl(filename);
+ let msg = await runInExtension("download", { url });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id } },
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ await progressPromise;
+ info(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ // Prevent intermittent timeouts due to the part file not yet created
+ // (e.g. see Bug 1573360).
+ await waitForCreatedPartFile(filename);
+
+ info("Pause the download item");
+ msg = await runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = await runInExtension("search", { paused: true });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, true, "download.paused is correct");
+ equal(
+ msg.result[0].estimatedEndTime,
+ null,
+ "download.estimatedEndTime is correct"
+ );
+ equal(msg.result[0].canResume, true, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(
+ msg.result[0].bytesReceived,
+ INT_PARTIAL_LEN,
+ "download.bytesReceived is correct"
+ );
+ equal(
+ msg.result[0].totalBytes,
+ INT_TOTAL_LEN,
+ "download.totalBytes is correct"
+ );
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = await runInExtension("search", { error: "USER_CANCELED" });
+ equal(msg.status, "success", "search() succeeded");
+ let found = msg.result.filter(item => item.id == id);
+ equal(found.length, 1, "search() by error found the paused download");
+
+ msg = await runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause an already paused download");
+
+ msg = await runInExtension("resume", id);
+ equal(msg.status, "success", "resume() succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "interrupted",
+ current: "in_progress",
+ },
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ error: {
+ previous: "USER_CANCELED",
+ current: null,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events for resume and complete");
+
+ msg = await runInExtension("search", { id });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].state, "complete", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(
+ msg.result[0].estimatedEndTime,
+ null,
+ "download.estimatedEndTime is correct"
+ );
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, null, "download.error is correct");
+ equal(
+ msg.result[0].bytesReceived,
+ INT_TOTAL_LEN,
+ "download.bytesReceived is correct"
+ );
+ equal(
+ msg.result[0].totalBytes,
+ INT_TOTAL_LEN,
+ "download.totalBytes is correct"
+ );
+ equal(msg.result[0].exists, true, "download.exists is correct");
+
+ msg = await runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause a completed download");
+
+ msg = await runInExtension("resume", id);
+ equal(msg.status, "error", "cannot resume a completed download");
+});
+
+add_task(async function test_pausecancel() {
+ let url = getInterruptibleUrl();
+ let msg = await runInExtension("download", { url });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id } },
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ await progressPromise;
+ info(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = await runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = await runInExtension("search", { paused: true });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, true, "download.paused is correct");
+ equal(
+ msg.result[0].estimatedEndTime,
+ null,
+ "download.estimatedEndTime is correct"
+ );
+ equal(msg.result[0].canResume, true, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(
+ msg.result[0].bytesReceived,
+ INT_PARTIAL_LEN,
+ "download.bytesReceived is correct"
+ );
+ equal(
+ msg.result[0].totalBytes,
+ INT_TOTAL_LEN,
+ "download.totalBytes is correct"
+ );
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = await runInExtension("search", { error: "USER_CANCELED" });
+ equal(msg.status, "success", "search() succeeded");
+ let found = msg.result.filter(item => item.id == id);
+ equal(found.length, 1, "search() by error found the paused download");
+
+ msg = await runInExtension("cancel", id);
+ equal(msg.status, "success", "cancel() succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged event for cancel");
+
+ msg = await runInExtension("search", { id });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(
+ msg.result[0].estimatedEndTime,
+ null,
+ "download.estimatedEndTime is correct"
+ );
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(
+ msg.result[0].totalBytes,
+ INT_TOTAL_LEN,
+ "download.totalBytes is correct"
+ );
+ equal(msg.result[0].exists, false, "download.exists is correct");
+});
+
+add_task(async function test_pause_resume_cancel_badargs() {
+ let BAD_ID = 1000;
+
+ let msg = await runInExtension("pause", BAD_ID);
+ equal(msg.status, "error", "pause() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+
+ msg = await runInExtension("resume", BAD_ID);
+ equal(msg.status, "error", "resume() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+
+ msg = await runInExtension("cancel", BAD_ID);
+ equal(msg.status, "error", "cancel() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+});
+
+add_task(async function test_file_removal() {
+ let msg = await runInExtension("download", { url: TXT_URL });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id, url: TXT_URL } },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+
+ equal(msg.status, "success", "got onCreated and onChanged events");
+
+ msg = await runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded");
+
+ msg = await runInExtension("removeFile", id);
+ equal(
+ msg.status,
+ "error",
+ "removeFile() fails since the file was already removed."
+ );
+ equal(
+ msg.errmsg,
+ `Could not remove download id ${id} because the file doesn't exist`,
+ "removeFile() failed on removed file."
+ );
+
+ msg = await runInExtension("removeFile", 1000);
+ equal(
+ msg.errmsg,
+ "Invalid download id 1000",
+ "removeFile() failed due to non-existent id"
+ );
+});
+
+add_task(async function test_file_removeFile_permission_failure() {
+ const inputDirname = "subdir_for_download";
+ const inputFilename = "downloaded_filename.txt";
+ const expectedDir = PathUtils.join(downloadDir.path, inputDirname);
+ const expectedPath = PathUtils.join(expectedDir, inputFilename);
+
+ let msg = await runInExtension("download", {
+ url: TXT_URL,
+ filename: `${inputDirname}/${inputFilename}`,
+ });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id, url: TXT_URL } },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+
+ equal(msg.status, "success", "got onCreated and onChanged events");
+
+ msg = await runInExtension("search", { id });
+ equal(msg.status, "success", "search() succeeded");
+ equal(expectedPath, msg.result[0]?.filename, "Got expected filename");
+
+ async function withUndeletableFileUnix(testRemoveFile) {
+ try {
+ // Temporarily make directory unreadable/inaccessible.
+ await IOUtils.setPermissions(expectedDir, 0);
+ // Remove should fail with Unix error 13 (EACCES).
+ await testRemoveFile();
+ } finally {
+ await IOUtils.setPermissions(expectedDir, 0o777);
+ }
+ }
+ async function withUndeletableFileWin(testRemoveFile) {
+ // On Windows, a directory marked as read-only does not prevent the deletion
+ // of its content. So we need an alternative approach here.
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ try {
+ // Open a file handle. The file cannot be deleted until it is closed.
+ stream.init(await IOUtils.getFile(expectedPath), -1, 0, 0);
+ // Remove should fail with Win error 32 (ERROR_SHARING_VIOLATION).
+ await testRemoveFile();
+ } finally {
+ stream.close();
+ }
+ }
+
+ const withUndeletableFile =
+ AppConstants.platform === "win"
+ ? withUndeletableFileWin
+ : withUndeletableFileUnix;
+
+ let consoleOutput;
+ await withUndeletableFile(async () => {
+ consoleOutput = await promiseConsoleOutput(async () => {
+ msg = await runInExtension("removeFile", id);
+ });
+ });
+
+ equal(msg.status, "error", "removeFile() fails due to missing dir perms");
+ // Verify that an unexpected error is redacted, with a useful error message
+ // logged to the console.
+ // Note: if we ever decide to make the error for permission failures more
+ // useful, try to add a new test case for unexpected errors, even if
+ // completely artificial such as mocking + breaking an internal API.
+ equal(msg.errmsg, "An unexpected error occurred", "Error message redacted");
+
+ AddonTestUtils.checkMessages(consoleOutput.messages, {
+ expected: [{ message: /NotAllowedError/ }],
+ });
+
+ ok(await IOUtils.exists(expectedPath), "File exists before removeFile()");
+
+ msg = await runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded");
+
+ equal(await IOUtils.exists(expectedPath), false, "File was really removed");
+
+ // As a bonus: check that the re-created file can be deleted without issues.
+ await IOUtils.writeUTF8(expectedPath, "content here");
+
+ msg = await runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded after recreation");
+
+ equal(await IOUtils.exists(expectedPath), false, "File was removed again");
+});
+
+add_task(async function test_removal_of_incomplete_download() {
+ const filename = "remove-incomplete.html";
+ let url = getInterruptibleUrl(filename);
+ let msg = await runInExtension("download", { url });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id } },
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ await progressPromise;
+ info(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ // Prevent intermittent timeouts due to the part file not yet created
+ // (e.g. see Bug 1573360).
+ await waitForCreatedPartFile(filename);
+
+ msg = await runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = await runInExtension("removeFile", id);
+ equal(msg.status, "error", "removeFile() on paused download failed");
+
+ ok(
+ /Cannot remove incomplete download/.test(msg.errmsg),
+ "removeFile() failed due to download being incomplete"
+ );
+
+ msg = await runInExtension("resume", id);
+ equal(msg.status, "success", "resume() succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "interrupted",
+ current: "in_progress",
+ },
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ error: {
+ previous: "USER_CANCELED",
+ current: null,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events for resume and complete");
+
+ msg = await runInExtension("removeFile", id);
+ equal(
+ msg.status,
+ "success",
+ "removeFile() succeeded following completion of resumed download."
+ );
+});
+
+// Test erase(). We don't do elaborate testing of the query handling
+// since it uses the exact same engine as search() which is tested
+// more thoroughly in test_chrome_ext_downloads_search.html
+add_task(async function test_erase() {
+ await clearDownloads();
+
+ await runInExtension("clearEvents");
+
+ async function download() {
+ let msg = await runInExtension("download", { url: TXT_URL });
+ equal(msg.status, "success", "download succeeded");
+ let id = msg.result;
+
+ msg = await runInExtension(
+ "waitForEvents",
+ [
+ {
+ type: "onChanged",
+ data: { id, state: { current: "complete" } },
+ },
+ ],
+ { exact: false }
+ );
+ equal(msg.status, "success", "download finished");
+
+ return id;
+ }
+
+ let ids = {};
+ ids.dl1 = await download();
+ ids.dl2 = await download();
+ ids.dl3 = await download();
+
+ let msg = await runInExtension("search", {});
+ equal(msg.status, "success", "search succeeded");
+ equal(msg.result.length, 3, "search found 3 downloads");
+
+ msg = await runInExtension("clearEvents");
+
+ msg = await runInExtension("erase", { id: ids.dl1 });
+ equal(msg.status, "success", "erase by id succeeded");
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onErased", data: ids.dl1 },
+ ]);
+ equal(msg.status, "success", "received onErased event");
+
+ msg = await runInExtension("search", {});
+ equal(msg.status, "success", "search succeeded");
+ equal(msg.result.length, 2, "search found 2 downloads");
+
+ msg = await runInExtension("erase", {});
+ equal(msg.status, "success", "erase everything succeeded");
+
+ msg = await runInExtension(
+ "waitForEvents",
+ [
+ { type: "onErased", data: ids.dl2 },
+ { type: "onErased", data: ids.dl3 },
+ ],
+ { inorder: false }
+ );
+ equal(msg.status, "success", "received 2 onErased events");
+
+ msg = await runInExtension("search", {});
+ equal(msg.status, "success", "search succeeded");
+ equal(msg.result.length, 0, "search found 0 downloads");
+});
+
+function loadImage(img, data) {
+ return new Promise(resolve => {
+ img.src = data;
+ img.onload = resolve;
+ });
+}
+
+add_task(async function test_getFileIcon() {
+ let webNav = Services.appShell.createWindowlessBrowser(false);
+ let docShell = webNav.docShell;
+
+ let system = Services.scriptSecurityManager.getSystemPrincipal();
+ docShell.createAboutBlankContentViewer(system, system);
+
+ let img = webNav.document.createElement("img");
+
+ let msg = await runInExtension("download", { url: TXT_URL });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = await runInExtension("getFileIcon", id);
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ await loadImage(img, msg.result);
+ equal(img.height, 32, "returns an icon with the right height");
+ equal(img.width, 32, "returns an icon with the right width");
+
+ msg = await runInExtension("waitForEvents", [
+ { type: "onCreated", data: { id, url: TXT_URL } },
+ { type: "onChanged" },
+ ]);
+ equal(msg.status, "success", "got events");
+
+ msg = await runInExtension("getFileIcon", id);
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ await loadImage(img, msg.result);
+ equal(img.height, 32, "returns an icon with the right height after download");
+ equal(img.width, 32, "returns an icon with the right width after download");
+
+ msg = await runInExtension("getFileIcon", id + 100);
+ equal(msg.status, "error", "getFileIcon() failed");
+ ok(msg.errmsg.includes("Invalid download id"), "download id is invalid");
+
+ msg = await runInExtension("getFileIcon", id, { size: 127 });
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ await loadImage(img, msg.result);
+ equal(img.height, 127, "returns an icon with the right custom height");
+ equal(img.width, 127, "returns an icon with the right custom width");
+
+ msg = await runInExtension("getFileIcon", id, { size: 1 });
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ await loadImage(img, msg.result);
+ equal(img.height, 1, "returns an icon with the right custom height");
+ equal(img.width, 1, "returns an icon with the right custom width");
+
+ msg = await runInExtension("getFileIcon", id, { size: "foo" });
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is not a number");
+
+ msg = await runInExtension("getFileIcon", id, { size: 0 });
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is too small");
+
+ msg = await runInExtension("getFileIcon", id, { size: 128 });
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is too big");
+
+ webNav.close();
+});
+
+add_task(async function test_estimatedendtime() {
+ // Note we are not testing the actual value calculation of estimatedEndTime,
+ // only whether it is null/non-null at the appropriate times.
+
+ let url = `${getInterruptibleUrl()}&stream=1`;
+ let msg = await runInExtension("download", { url });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let previousBytes = await waitForProgress(url, bytes => bytes > 0);
+ await waitForProgress(url, bytes => bytes > previousBytes);
+
+ msg = await runInExtension("search", { id });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ ok(msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+ ok(msg.result[0].bytesReceived > 0, "download.bytesReceived is correct");
+
+ msg = await runInExtension("cancel", id);
+
+ msg = await runInExtension("search", { id });
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ ok(!msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+});
+
+add_task(async function test_byExtension() {
+ let msg = await runInExtension("download", { url: TXT_URL });
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+ msg = await runInExtension("search", { id });
+
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(
+ msg.result[0].byExtensionName,
+ "Generated extension",
+ "download.byExtensionName is correct"
+ );
+ equal(
+ msg.result[0].byExtensionId,
+ extension.id,
+ "download.byExtensionId is correct"
+ );
+});