summaryrefslogtreecommitdiffstats
path: root/toolkit/components/jsdownloads/test/unit/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/jsdownloads/test/unit/head.js')
-rw-r--r--toolkit/components/jsdownloads/test/unit/head.js843
1 files changed, 843 insertions, 0 deletions
diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js
new file mode 100644
index 000000000..f322244c4
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -0,0 +1,843 @@
+/* -*- 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/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
+ "resource://testing-common/MockRegistrar.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsIExternalHelperAppService);
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+const ServerSocket = Components.Constructor(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init");
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream")
+
+XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService");
+
+const TEST_TARGET_FILE_NAME = "test-download.txt";
+const TEST_STORE_FILE_NAME = "test-downloads.json";
+
+const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
+
+const TEST_DATA_SHORT = "This test string is downloaded.";
+// Generate using gzipCompressString in TelemetryController.jsm.
+const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
+ 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 11, 201, 200, 44, 86, 40, 73, 45, 46, 81, 40, 46, 41, 202, 204
+];
+const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
+ 75, 87, 0, 114, 83, 242, 203, 243, 114, 242, 19, 83, 82, 83, 244, 0, 151, 222, 109, 43, 31, 0, 0, 0
+];
+const TEST_DATA_SHORT_GZIP_ENCODED =
+ TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
+
+/**
+ * All the tests are implemented with add_task, this starts them automatically.
+ */
+function run_test()
+{
+ do_get_profile();
+ run_next_test();
+}
+
+// Support functions
+
+/**
+ * HttpServer object initialized before tests start.
+ */
+var gHttpServer;
+
+/**
+ * Given a file name, returns a string containing an URI that points to the file
+ * on the currently running instance of the test HTTP server.
+ */
+function httpUrl(aFileName) {
+ return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
+ aFileName;
+}
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+var gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function () {
+ try {
+ file.remove(false)
+ } catch (e) {
+ if (!(e instanceof Components.Exception &&
+ (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
+ e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST ||
+ e.result == Cr.NS_ERROR_FILE_NOT_FOUND))) {
+ throw e;
+ }
+ // On Windows, we may get an access denied error if the file existed before,
+ // and was recently deleted.
+ // Don't bother checking file.exists() as that may also cause an access
+ // denied error.
+ }
+ });
+
+ return file;
+}
+
+/**
+ * Waits for pending events to be processed.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+function promiseExecuteSoon()
+{
+ let deferred = Promise.defer();
+ do_execute_soon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Waits for a pending events to be processed after a timeout.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+function promiseTimeout(aTime)
+{
+ let deferred = Promise.defer();
+ do_timeout(aTime, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Waits for a new history visit to be notified for the specified URI.
+ *
+ * @param aUrl
+ * String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
+ * @rejects Never.
+ */
+function promiseWaitForVisit(aUrl)
+{
+ let deferred = Promise.defer();
+
+ let uri = NetUtil.newURI(aUrl);
+
+ PlacesUtils.history.addObserver({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType, aGUID, aHidden) {
+ if (aURI.equals(uri)) {
+ PlacesUtils.history.removeObserver(this);
+ deferred.resolve([aTime, aTransitionType]);
+ }
+ },
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ }, false);
+
+ return deferred.promise;
+}
+
+/**
+ * Check browsing history to see whether the given URI has been visited.
+ *
+ * @param aUrl
+ * String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the URI has been visited.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aUrl) {
+ let deferred = Promise.defer();
+
+ PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
+ function (aURI, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Creates a new Download object, setting a temporary file as the target.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object.
+ * @rejects JavaScript exception.
+ */
+function promiseNewDownload(aSourceUrl) {
+ return Downloads.createDownload({
+ source: aSourceUrl || httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ });
+}
+
+/**
+ * Starts a new download using the nsIWebBrowserPersist interface, and controls
+ * it using the legacy nsITransfer interface.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ * @param aOptions
+ * An optional object used to control the behavior of this function.
+ * You may pass an object with a subset of the following fields:
+ * {
+ * isPrivate: Boolean indicating whether the download originated from a
+ * private window.
+ * targetFile: nsIFile for the target, or null to use a temporary file.
+ * outPersist: Receives a reference to the created nsIWebBrowserPersist
+ * instance.
+ * launchWhenSucceeded: Boolean indicating whether the target should
+ * be launched when it has completed successfully.
+ * launcherPath: String containing the path of the custom executable to
+ * use to launch the target of the download.
+ * }
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ * download through the legacy nsITransfer interface.
+ * @rejects Never. The current test fails in case of exceptions.
+ */
+function promiseStartLegacyDownload(aSourceUrl, aOptions) {
+ let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
+ let targetFile = (aOptions && aOptions.targetFile)
+ || getTempFile(TEST_TARGET_FILE_NAME);
+
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ if (aOptions) {
+ aOptions.outPersist = persist;
+ }
+
+ let fileExtension = null, mimeInfo = null;
+ let match = sourceURI.path.match(/\.([^.\/]+)$/);
+ if (match) {
+ fileExtension = match[1];
+ }
+
+ if (fileExtension) {
+ try {
+ mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension);
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+ } catch (ex) { }
+ }
+
+ if (aOptions && aOptions.launcherPath) {
+ do_check_true(mimeInfo != null);
+
+ let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath);
+
+ mimeInfo.preferredApplicationHandler = localHandlerApp;
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ }
+
+ if (aOptions && aOptions.launchWhenSucceeded) {
+ do_check_true(mimeInfo != null);
+
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ }
+
+ // Apply decoding if required by the "Content-Encoding" header.
+ persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
+ persist.persistFlags |=
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ let transfer = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+
+ let deferred = Promise.defer();
+
+ Downloads.getList(Downloads.ALL).then(function (aList) {
+ // Temporarily register a view that will get notified when the download we
+ // are controlling becomes visible in the list of downloads.
+ aList.addView({
+ onDownloadAdded: function (aDownload) {
+ aList.removeView(this).then(null, do_report_unexpected_exception);
+
+ // Remove the download to keep the list empty for the next test. This
+ // also allows the caller to register the "onchange" event directly.
+ let promise = aList.remove(aDownload);
+
+ // When the download object is ready, make it available to the caller.
+ promise.then(() => deferred.resolve(aDownload),
+ do_report_unexpected_exception);
+ },
+ }).then(null, do_report_unexpected_exception);
+
+ let isPrivate = aOptions && aOptions.isPrivate;
+
+ // Initialize the components so they reference each other. This will cause
+ // the Download object to be created and added to the public downloads.
+ transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
+ null, persist, isPrivate);
+ persist.progressListener = transfer;
+
+ // Start the actual download process.
+ persist.savePrivacyAwareURI(sourceURI, null, null, 0, null, null, targetFile,
+ isPrivate);
+ }.bind(this)).then(null, do_report_unexpected_exception);
+
+ return deferred.promise;
+}
+
+/**
+ * Starts a new download using the nsIHelperAppService interface, and controls
+ * it using the legacy nsITransfer interface. The source of the download will
+ * be "interruptible_resumable.txt" and partially downloaded data will be kept.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("interruptible_resumable.txt").
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ * download through the legacy nsITransfer interface.
+ * @rejects Never. The current test fails in case of exceptions.
+ */
+function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
+ let sourceURI = NetUtil.newURI(aSourceUrl ||
+ httpUrl("interruptible_resumable.txt"));
+
+ let deferred = Promise.defer();
+
+ Downloads.getList(Downloads.PUBLIC).then(function (aList) {
+ // Temporarily register a view that will get notified when the download we
+ // are controlling becomes visible in the list of downloads.
+ aList.addView({
+ onDownloadAdded: function (aDownload) {
+ aList.removeView(this).then(null, do_report_unexpected_exception);
+
+ // Remove the download to keep the list empty for the next test. This
+ // also allows the caller to register the "onchange" event directly.
+ let promise = aList.remove(aDownload);
+
+ // When the download object is ready, make it available to the caller.
+ promise.then(() => deferred.resolve(aDownload),
+ do_report_unexpected_exception);
+ },
+ }).then(null, do_report_unexpected_exception);
+
+ let channel = NetUtil.newChannel({
+ uri: sourceURI,
+ loadUsingSystemPrincipal: true
+ });
+
+ // Start the actual download process.
+ channel.asyncOpen2({
+ contentListener: null,
+
+ onStartRequest: function (aRequest, aContext)
+ {
+ let requestChannel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.contentListener = gExternalHelperAppService.doContent(
+ requestChannel.contentType, aRequest, null, true);
+ this.contentListener.onStartRequest(aRequest, aContext);
+ },
+
+ onStopRequest: function (aRequest, aContext, aStatusCode)
+ {
+ this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+
+ onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
+ aCount)
+ {
+ this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount);
+ },
+ });
+ }.bind(this)).then(null, do_report_unexpected_exception);
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for a download to reach half of its progress, in case it has not
+ * reached the expected progress already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has reached half of its progress.
+ * @rejects Never.
+ */
+function promiseDownloadMidway(aDownload) {
+ let deferred = Promise.defer();
+
+ // Wait for the download to reach half of its progress.
+ let onchange = function () {
+ if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
+ aDownload.onchange = null;
+ deferred.resolve();
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ aDownload.onchange = onchange;
+ onchange();
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+ if (!aDownload.stopped) {
+ // The download is in progress, wait for the current attempt to finish and
+ // report any errors that may occur.
+ return aDownload.start();
+ }
+
+ if (aDownload.succeeded) {
+ return Promise.resolve();
+ }
+
+ // The download failed or was canceled.
+ return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+/**
+ * Returns a new public or private DownloadList object.
+ *
+ * @param aIsPrivate
+ * True for the private list, false or undefined for the public list.
+ *
+ * @return {Promise}
+ * @resolves The newly created DownloadList object.
+ * @rejects JavaScript exception.
+ */
+function promiseNewList(aIsPrivate)
+{
+ // We need to clear all the internal state for the list and summary objects,
+ // since all the objects are interdependent internally.
+ Downloads._promiseListsInitialized = null;
+ Downloads._lists = {};
+ Downloads._summaries = {};
+
+ return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
+}
+
+/**
+ * Ensures that the given file contents are equal to the given string.
+ *
+ * @param aPath
+ * String containing the path of the file whose contents should be
+ * verified.
+ * @param aExpectedContents
+ * String containing the octets that are expected in the file.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects Never.
+ */
+function promiseVerifyContents(aPath, aExpectedContents)
+{
+ return Task.spawn(function* () {
+ let file = new FileUtils.File(aPath);
+
+ if (!(yield OS.File.exists(aPath))) {
+ do_throw("File does not exist: " + aPath);
+ }
+
+ if ((yield OS.File.stat(aPath)).size == 0) {
+ do_throw("File is empty: " + aPath);
+ }
+
+ let deferred = Promise.defer();
+ NetUtil.asyncFetch(
+ { uri: NetUtil.newURI(file), loadUsingSystemPrincipal: true },
+ function(aInputStream, aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ let contents = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ if (contents.length > TEST_DATA_SHORT.length * 2 ||
+ /[^\x20-\x7E]/.test(contents)) {
+ // Do not print the entire content string to the test log.
+ do_check_eq(contents.length, aExpectedContents.length);
+ do_check_true(contents == aExpectedContents);
+ } else {
+ // Print the string if it is short and made of printable characters.
+ do_check_eq(contents, aExpectedContents);
+ }
+ deferred.resolve();
+ });
+
+ yield deferred.promise;
+ });
+}
+
+/**
+ * Starts a socket listener that closes each incoming connection.
+ *
+ * @returns nsIServerSocket that listens for connections. Call its "close"
+ * method to stop listening and free the server port.
+ */
+function startFakeServer()
+{
+ let serverSocket = new ServerSocket(-1, true, -1);
+ serverSocket.asyncListen({
+ onSocketAccepted: function (aServ, aTransport) {
+ aTransport.close(Cr.NS_BINDING_ABORTED);
+ },
+ onStopListening: function () { },
+ });
+ return serverSocket;
+}
+
+/**
+ * This is an internal reference that should not be used directly by tests.
+ */
+var _gDeferResponses = Promise.defer();
+
+/**
+ * Ensures that all the interruptible requests started after this function is
+ * called won't complete until the continueResponses function is called.
+ *
+ * Normally, the internal HTTP server returns all the available data as soon as
+ * a request is received. In order for some requests to be served one part at a
+ * time, special interruptible handlers are registered on the HTTP server. This
+ * allows testing events or actions that need to happen in the middle of a
+ * download.
+ *
+ * For example, the handler accessible at the httpUri("interruptible.txt")
+ * address returns the TEST_DATA_SHORT text, then it may block until the
+ * continueResponses method is called. At this point, the handler sends the
+ * TEST_DATA_SHORT text again to complete the response.
+ *
+ * If an interruptible request is started before the function is called, it may
+ * or may not be blocked depending on the actual sequence of events.
+ */
+function mustInterruptResponses()
+{
+ // If there are pending blocked requests, allow them to complete. This is
+ // done to prevent requests from being blocked forever, but should not affect
+ // the test logic, since previously started requests should not be monitored
+ // on the client side anymore.
+ _gDeferResponses.resolve();
+
+ do_print("Interruptible responses will be blocked midway.");
+ _gDeferResponses = Promise.defer();
+}
+
+/**
+ * Allows all the current and future interruptible requests to complete.
+ */
+function continueResponses()
+{
+ do_print("Interruptible responses are now allowed to continue.");
+ _gDeferResponses.resolve();
+}
+
+/**
+ * Registers an interruptible response handler.
+ *
+ * @param aPath
+ * Path passed to nsIHttpServer.registerPathHandler.
+ * @param aFirstPartFn
+ * This function is called when the response is received, with the
+ * aRequest and aResponse arguments of the server.
+ * @param aSecondPartFn
+ * This function is called with the aRequest and aResponse arguments of
+ * the server, when the continueResponses function is called.
+ */
+function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
+{
+ gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
+ do_print("Interruptible request started.");
+
+ // Process the first part of the response.
+ aResponse.processAsync();
+ aFirstPartFn(aRequest, aResponse);
+
+ // Wait on the current deferred object, then finish the request.
+ _gDeferResponses.promise.then(function RIH_onSuccess() {
+ aSecondPartFn(aRequest, aResponse);
+ aResponse.finish();
+ do_print("Interruptible request finished.");
+ }).then(null, Cu.reportError);
+ });
+}
+
+/**
+ * Ensure the given date object is valid.
+ *
+ * @param aDate
+ * The date object to be checked. This value can be null.
+ */
+function isValidDate(aDate) {
+ return aDate && aDate.getTime && !isNaN(aDate.getTime());
+}
+
+/**
+ * Position of the first byte served by the "interruptible_resumable.txt"
+ * handler during the most recent response.
+ */
+var gMostRecentFirstBytePos;
+
+// Initialization functions common to all tests
+
+add_task(function test_common_initialize()
+{
+ // Start the HTTP server.
+ gHttpServer = new HttpServer();
+ gHttpServer.registerDirectory("/", do_get_file("../data"));
+ gHttpServer.start(-1);
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ // Ensure all the pending HTTP requests have a chance to finish.
+ continueResponses();
+ // Stop the HTTP server, calling resolve when it's done.
+ gHttpServer.stop(resolve);
+ });
+ });
+
+ // Cache locks might prevent concurrent requests to the same resource, and
+ // this may block tests that use the interruptible handlers.
+ Services.prefs.setBoolPref("browser.cache.disk.enable", false);
+ Services.prefs.setBoolPref("browser.cache.memory.enable", false);
+ do_register_cleanup(function () {
+ Services.prefs.clearUserPref("browser.cache.disk.enable");
+ Services.prefs.clearUserPref("browser.cache.memory.enable");
+ });
+
+ registerInterruptibleHandler("/interruptible.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
+ false);
+ aResponse.write(TEST_DATA_SHORT);
+ }, function secondPart(aRequest, aResponse) {
+ aResponse.write(TEST_DATA_SHORT);
+ });
+
+ registerInterruptibleHandler("/interruptible_resumable.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ // Determine if only part of the data should be sent.
+ let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
+ if (aRequest.hasHeader("Range")) {
+ var matches = aRequest.getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
+ var lastBytePos = (matches[2] === undefined) ? data.length - 1
+ : matches[2];
+ if (firstBytePos >= data.length) {
+ aResponse.setStatusLine(aRequest.httpVersion, 416,
+ "Requested Range Not Satisfiable");
+ aResponse.setHeader("Content-Range", "*/" + data.length, false);
+ aResponse.finish();
+ return;
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
+ aResponse.setHeader("Content-Range", firstBytePos + "-" +
+ lastBytePos + "/" +
+ data.length, false);
+
+ data = data.substring(firstBytePos, lastBytePos + 1);
+
+ gMostRecentFirstBytePos = firstBytePos;
+ } else {
+ gMostRecentFirstBytePos = 0;
+ }
+
+ aResponse.setHeader("Content-Length", "" + data.length, false);
+
+ aResponse.write(data.substring(0, data.length / 2));
+
+ // Store the second part of the data on the response object, so that it
+ // can be used by the secondPart function.
+ aResponse.secondPartData = data.substring(data.length / 2);
+ }, function secondPart(aRequest, aResponse) {
+ aResponse.write(aResponse.secondPartData);
+ });
+
+ registerInterruptibleHandler("/interruptible_gzip.txt",
+ function firstPart(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);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
+ TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
+ }, function secondPart(aRequest, aResponse) {
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
+ TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
+ });
+
+ gHttpServer.registerPathHandler("/shorter-than-content-length-http-1-1.txt",
+ function (aRequest, aResponse) {
+ aResponse.processAsync();
+ aResponse.setStatusLine("1.1", 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
+ false);
+ aResponse.write(TEST_DATA_SHORT);
+ aResponse.finish();
+ });
+
+ // This URL will emulate being blocked by Windows Parental controls
+ gHttpServer.registerPathHandler("/parentalblocked.zip",
+ function (aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 450,
+ "Blocked by Windows Parental Controls");
+ });
+
+ // During unit tests, most of the functions that require profile access or
+ // operating system features will be disabled. Individual tests may override
+ // them again to check for specific behaviors.
+ Integration.downloads.register(base => ({
+ __proto__: base,
+ loadPublicDownloadListFromStore: () => Promise.resolve(),
+ shouldKeepBlockedData: () => Promise.resolve(false),
+ shouldBlockForParentalControls: () => Promise.resolve(false),
+ shouldBlockForRuntimePermissions: () => Promise.resolve(false),
+ shouldBlockForReputationCheck: () => Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ }),
+ confirmLaunchExecutable: () => Promise.resolve(),
+ launchFile: () => Promise.resolve(),
+ showContainingDirectory: () => Promise.resolve(),
+ // This flag allows re-enabling the default observers during their tests.
+ allowObservers: false,
+ addListObservers() {
+ return this.allowObservers ? super.addListObservers(...arguments)
+ : Promise.resolve();
+ },
+ // This flag allows re-enabling the download directory logic for its tests.
+ _allowDirectories: false,
+ set allowDirectories(value) {
+ this._allowDirectories = value;
+ // We have to invalidate the previously computed directory path.
+ this._downloadsDirectory = null;
+ },
+ _getDirectory(name) {
+ return super._getDirectory(this._allowDirectories ? name : "TmpD");
+ },
+ }));
+
+ // Make sure that downloads started using nsIExternalHelperAppService are
+ // saved to disk without asking for a destination interactively.
+ let mock = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+ promptForSaveToFileAsync(aLauncher,
+ aWindowContext,
+ aDefaultFileName,
+ aSuggestedFileExtension,
+ aForcePrompt) {
+ // The dialog should create the empty placeholder file.
+ let file = getTempFile(TEST_TARGET_FILE_NAME);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ aLauncher.saveDestinationAvailable(file);
+ },
+ };
+
+ let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", mock);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(cid);
+ });
+});