toolkit/components/jsdownloads/test/unit/head.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/jsdownloads/test/unit/head.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,889 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80: */
     1.6 +/* Any copyright is dedicated to the Public Domain.
     1.7 + * http://creativecommons.org/publicdomain/zero/1.0/ */
     1.8 +
     1.9 +/**
    1.10 + * Provides infrastructure for automated download components tests.
    1.11 + */
    1.12 +
    1.13 +"use strict";
    1.14 +
    1.15 +////////////////////////////////////////////////////////////////////////////////
    1.16 +//// Globals
    1.17 +
    1.18 +const Cc = Components.classes;
    1.19 +const Ci = Components.interfaces;
    1.20 +const Cu = Components.utils;
    1.21 +const Cr = Components.results;
    1.22 +
    1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.24 +
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
    1.26 +                                  "resource://gre/modules/DownloadPaths.jsm");
    1.27 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
    1.28 +                                  "resource://gre/modules/DownloadIntegration.jsm");
    1.29 +XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
    1.30 +                                  "resource://gre/modules/Downloads.jsm");
    1.31 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    1.32 +                                  "resource://gre/modules/FileUtils.jsm");
    1.33 +XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
    1.34 +                                  "resource://testing-common/httpd.js");
    1.35 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.36 +                                  "resource://gre/modules/NetUtil.jsm");
    1.37 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.38 +                                  "resource://gre/modules/PlacesUtils.jsm");
    1.39 +XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    1.40 +                                  "resource://gre/modules/Promise.jsm");
    1.41 +XPCOMUtils.defineLazyModuleGetter(this, "Services",
    1.42 +                                  "resource://gre/modules/Services.jsm");
    1.43 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.44 +                                  "resource://gre/modules/Task.jsm");
    1.45 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.46 +                                  "resource://gre/modules/osfile.jsm");
    1.47 +
    1.48 +XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
    1.49 +           "@mozilla.org/uriloader/external-helper-app-service;1",
    1.50 +           Ci.nsIExternalHelperAppService);
    1.51 +
    1.52 +const ServerSocket = Components.Constructor(
    1.53 +                                "@mozilla.org/network/server-socket;1",
    1.54 +                                "nsIServerSocket",
    1.55 +                                "init");
    1.56 +const BinaryOutputStream = Components.Constructor(
    1.57 +                                      "@mozilla.org/binaryoutputstream;1",
    1.58 +                                      "nsIBinaryOutputStream",
    1.59 +                                      "setOutputStream")
    1.60 +
    1.61 +XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
    1.62 +                                   "@mozilla.org/mime;1",
    1.63 +                                   "nsIMIMEService");
    1.64 +
    1.65 +const TEST_TARGET_FILE_NAME = "test-download.txt";
    1.66 +const TEST_STORE_FILE_NAME = "test-downloads.json";
    1.67 +
    1.68 +const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
    1.69 +
    1.70 +const TEST_DATA_SHORT = "This test string is downloaded.";
    1.71 +// Generate using gzipCompressString in TelemetryPing.jsm.
    1.72 +const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
    1.73 + 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
    1.74 +];
    1.75 +const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
    1.76 +  75,87,0,114,83,242,203,243,114,242,19,83,82,83,244,0,151,222,109,43,31,0,0,0
    1.77 +];
    1.78 +const TEST_DATA_SHORT_GZIP_ENCODED =
    1.79 +  TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
    1.80 +
    1.81 +/**
    1.82 + * All the tests are implemented with add_task, this starts them automatically.
    1.83 + */
    1.84 +function run_test()
    1.85 +{
    1.86 +  do_get_profile();
    1.87 +  run_next_test();
    1.88 +}
    1.89 +
    1.90 +////////////////////////////////////////////////////////////////////////////////
    1.91 +//// Support functions
    1.92 +
    1.93 +/**
    1.94 + * HttpServer object initialized before tests start.
    1.95 + */
    1.96 +let gHttpServer;
    1.97 +
    1.98 +/**
    1.99 + * Given a file name, returns a string containing an URI that points to the file
   1.100 + * on the currently running instance of the test HTTP server.
   1.101 + */
   1.102 +function httpUrl(aFileName) {
   1.103 +  return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
   1.104 +         aFileName;
   1.105 +}
   1.106 +
   1.107 +// While the previous test file should have deleted all the temporary files it
   1.108 +// used, on Windows these might still be pending deletion on the physical file
   1.109 +// system.  Thus, start from a new base number every time, to make a collision
   1.110 +// with a file that is still pending deletion highly unlikely.
   1.111 +let gFileCounter = Math.floor(Math.random() * 1000000);
   1.112 +
   1.113 +/**
   1.114 + * Returns a reference to a temporary file, that is guaranteed not to exist, and
   1.115 + * to have never been created before.
   1.116 + *
   1.117 + * @param aLeafName
   1.118 + *        Suggested leaf name for the file to be created.
   1.119 + *
   1.120 + * @return nsIFile pointing to a non-existent file in a temporary directory.
   1.121 + *
   1.122 + * @note It is not enough to delete the file if it exists, or to delete the file
   1.123 + *       after calling nsIFile.createUnique, because on Windows the delete
   1.124 + *       operation in the file system may still be pending, preventing a new
   1.125 + *       file with the same name to be created.
   1.126 + */
   1.127 +function getTempFile(aLeafName)
   1.128 +{
   1.129 +  // Prepend a serial number to the extension in the suggested leaf name.
   1.130 +  let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
   1.131 +  let leafName = base + "-" + gFileCounter + ext;
   1.132 +  gFileCounter++;
   1.133 +
   1.134 +  // Get a file reference under the temporary directory for this test file.
   1.135 +  let file = FileUtils.getFile("TmpD", [leafName]);
   1.136 +  do_check_false(file.exists());
   1.137 +
   1.138 +  do_register_cleanup(function () {
   1.139 +    if (file.exists()) {
   1.140 +      file.remove(false);
   1.141 +    }
   1.142 +  });
   1.143 +
   1.144 +  return file;
   1.145 +}
   1.146 +
   1.147 +/**
   1.148 + * Waits for pending events to be processed.
   1.149 + *
   1.150 + * @return {Promise}
   1.151 + * @resolves When pending events have been processed.
   1.152 + * @rejects Never.
   1.153 + */
   1.154 +function promiseExecuteSoon()
   1.155 +{
   1.156 +  let deferred = Promise.defer();
   1.157 +  do_execute_soon(deferred.resolve);
   1.158 +  return deferred.promise;
   1.159 +}
   1.160 +
   1.161 +/**
   1.162 + * Waits for a pending events to be processed after a timeout.
   1.163 + *
   1.164 + * @return {Promise}
   1.165 + * @resolves When pending events have been processed.
   1.166 + * @rejects Never.
   1.167 + */
   1.168 +function promiseTimeout(aTime)
   1.169 +{
   1.170 +  let deferred = Promise.defer();
   1.171 +  do_timeout(aTime, deferred.resolve);
   1.172 +  return deferred.promise;
   1.173 +}
   1.174 +
   1.175 +/**
   1.176 + * Allows waiting for an observer notification once.
   1.177 + *
   1.178 + * @param aTopic
   1.179 + *        Notification topic to observe.
   1.180 + *
   1.181 + * @return {Promise}
   1.182 + * @resolves The array [aSubject, aData] from the observed notification.
   1.183 + * @rejects Never.
   1.184 + */
   1.185 +function promiseTopicObserved(aTopic)
   1.186 +{
   1.187 +  let deferred = Promise.defer();
   1.188 +
   1.189 +  Services.obs.addObserver(
   1.190 +    function PTO_observe(aSubject, aTopic, aData) {
   1.191 +      Services.obs.removeObserver(PTO_observe, aTopic);
   1.192 +      deferred.resolve([aSubject, aData]);
   1.193 +    }, aTopic, false);
   1.194 +
   1.195 +  return deferred.promise;
   1.196 +}
   1.197 +
   1.198 +/**
   1.199 + * Clears history asynchronously.
   1.200 + *
   1.201 + * @return {Promise}
   1.202 + * @resolves When history has been cleared.
   1.203 + * @rejects Never.
   1.204 + */
   1.205 +function promiseClearHistory()
   1.206 +{
   1.207 +  let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
   1.208 +  do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
   1.209 +  return promise;
   1.210 +}
   1.211 +
   1.212 +/**
   1.213 + * Waits for a new history visit to be notified for the specified URI.
   1.214 + *
   1.215 + * @param aUrl
   1.216 + *        String containing the URI that will be visited.
   1.217 + *
   1.218 + * @return {Promise}
   1.219 + * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
   1.220 + * @rejects Never.
   1.221 + */
   1.222 +function promiseWaitForVisit(aUrl)
   1.223 +{
   1.224 +  let deferred = Promise.defer();
   1.225 +
   1.226 +  let uri = NetUtil.newURI(aUrl);
   1.227 +
   1.228 +  PlacesUtils.history.addObserver({
   1.229 +    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
   1.230 +    onBeginUpdateBatch: function () {},
   1.231 +    onEndUpdateBatch: function () {},
   1.232 +    onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
   1.233 +                       aTransitionType, aGUID, aHidden) {
   1.234 +      if (aURI.equals(uri)) {
   1.235 +        PlacesUtils.history.removeObserver(this);
   1.236 +        deferred.resolve([aTime, aTransitionType]);
   1.237 +      }
   1.238 +    },
   1.239 +    onTitleChanged: function () {},
   1.240 +    onDeleteURI: function () {},
   1.241 +    onClearHistory: function () {},
   1.242 +    onPageChanged: function () {},
   1.243 +    onDeleteVisits: function () {},
   1.244 +  }, false);
   1.245 +
   1.246 +  return deferred.promise;
   1.247 +}
   1.248 +
   1.249 +/**
   1.250 + * Check browsing history to see whether the given URI has been visited.
   1.251 + *
   1.252 + * @param aUrl
   1.253 + *        String containing the URI that will be visited.
   1.254 + *
   1.255 + * @return {Promise}
   1.256 + * @resolves Boolean indicating whether the URI has been visited.
   1.257 + * @rejects JavaScript exception.
   1.258 + */
   1.259 +function promiseIsURIVisited(aUrl) {
   1.260 +  let deferred = Promise.defer();
   1.261 +
   1.262 +  PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
   1.263 +    function (aURI, aIsVisited) {
   1.264 +      deferred.resolve(aIsVisited);
   1.265 +    });
   1.266 +
   1.267 +  return deferred.promise;
   1.268 +}
   1.269 +
   1.270 +/**
   1.271 + * Creates a new Download object, setting a temporary file as the target.
   1.272 + *
   1.273 + * @param aSourceUrl
   1.274 + *        String containing the URI for the download source, or null to use
   1.275 + *        httpUrl("source.txt").
   1.276 + *
   1.277 + * @return {Promise}
   1.278 + * @resolves The newly created Download object.
   1.279 + * @rejects JavaScript exception.
   1.280 + */
   1.281 +function promiseNewDownload(aSourceUrl) {
   1.282 +  return Downloads.createDownload({
   1.283 +    source: aSourceUrl || httpUrl("source.txt"),
   1.284 +    target: getTempFile(TEST_TARGET_FILE_NAME),
   1.285 +  });
   1.286 +}
   1.287 +
   1.288 +/**
   1.289 + * Starts a new download using the nsIWebBrowserPersist interface, and controls
   1.290 + * it using the legacy nsITransfer interface.
   1.291 + *
   1.292 + * @param aSourceUrl
   1.293 + *        String containing the URI for the download source, or null to use
   1.294 + *        httpUrl("source.txt").
   1.295 + * @param aOptions
   1.296 + *        An optional object used to control the behavior of this function.
   1.297 + *        You may pass an object with a subset of the following fields:
   1.298 + *        {
   1.299 + *          isPrivate: Boolean indicating whether the download originated from a
   1.300 + *                     private window.
   1.301 + *          targetFile: nsIFile for the target, or null to use a temporary file.
   1.302 + *          outPersist: Receives a reference to the created nsIWebBrowserPersist
   1.303 + *                      instance.
   1.304 + *          launchWhenSucceeded: Boolean indicating whether the target should
   1.305 + *                               be launched when it has completed successfully.
   1.306 + *          launcherPath: String containing the path of the custom executable to
   1.307 + *                        use to launch the target of the download.
   1.308 + *        }
   1.309 + *
   1.310 + * @return {Promise}
   1.311 + * @resolves The Download object created as a consequence of controlling the
   1.312 + *           download through the legacy nsITransfer interface.
   1.313 + * @rejects Never.  The current test fails in case of exceptions.
   1.314 + */
   1.315 +function promiseStartLegacyDownload(aSourceUrl, aOptions) {
   1.316 +  let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
   1.317 +  let targetFile = (aOptions && aOptions.targetFile)
   1.318 +                   || getTempFile(TEST_TARGET_FILE_NAME);
   1.319 +
   1.320 +  let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
   1.321 +                  .createInstance(Ci.nsIWebBrowserPersist);
   1.322 +  if (aOptions) {
   1.323 +    aOptions.outPersist = persist;
   1.324 +  }
   1.325 +
   1.326 +  let fileExtension = null, mimeInfo = null;
   1.327 +  let match = sourceURI.path.match(/\.([^.\/]+)$/);
   1.328 +  if (match) {
   1.329 +    fileExtension = match[1];
   1.330 +  }
   1.331 +
   1.332 +  if (fileExtension) {
   1.333 +    try {
   1.334 +      mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension);
   1.335 +      mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
   1.336 +    } catch (ex) { }
   1.337 +  }
   1.338 +
   1.339 +  if (aOptions && aOptions.launcherPath) {
   1.340 +    do_check_true(mimeInfo != null);
   1.341 +
   1.342 +    let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
   1.343 +                            .createInstance(Ci.nsILocalHandlerApp);
   1.344 +    localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath);
   1.345 +
   1.346 +    mimeInfo.preferredApplicationHandler = localHandlerApp;
   1.347 +    mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
   1.348 +  }
   1.349 +
   1.350 +  if (aOptions && aOptions.launchWhenSucceeded) {
   1.351 +    do_check_true(mimeInfo != null);
   1.352 +
   1.353 +    mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
   1.354 +  }
   1.355 +
   1.356 +  // Apply decoding if required by the "Content-Encoding" header.
   1.357 +  persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
   1.358 +  persist.persistFlags |=
   1.359 +    Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
   1.360 +
   1.361 +  // We must create the nsITransfer implementation using its class ID because
   1.362 +  // the "@mozilla.org/transfer;1" contract is currently implemented in
   1.363 +  // "toolkit/components/downloads".  When the other folder is not included in
   1.364 +  // builds anymore (bug 851471), we'll be able to use the contract ID.
   1.365 +  let transfer =
   1.366 +      Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"]
   1.367 +                .createInstance(Ci.nsITransfer);
   1.368 +
   1.369 +  let deferred = Promise.defer();
   1.370 +
   1.371 +  Downloads.getList(Downloads.ALL).then(function (aList) {
   1.372 +    // Temporarily register a view that will get notified when the download we
   1.373 +    // are controlling becomes visible in the list of downloads.
   1.374 +    aList.addView({
   1.375 +      onDownloadAdded: function (aDownload) {
   1.376 +        aList.removeView(this).then(null, do_report_unexpected_exception);
   1.377 +
   1.378 +        // Remove the download to keep the list empty for the next test.  This
   1.379 +        // also allows the caller to register the "onchange" event directly.
   1.380 +        let promise = aList.remove(aDownload);
   1.381 +
   1.382 +        // When the download object is ready, make it available to the caller.
   1.383 +        promise.then(() => deferred.resolve(aDownload),
   1.384 +                     do_report_unexpected_exception);
   1.385 +      },
   1.386 +    }).then(null, do_report_unexpected_exception);
   1.387 +
   1.388 +    let isPrivate = aOptions && aOptions.isPrivate;
   1.389 +
   1.390 +    // Initialize the components so they reference each other.  This will cause
   1.391 +    // the Download object to be created and added to the public downloads.
   1.392 +    transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
   1.393 +                  null, persist, isPrivate);
   1.394 +    persist.progressListener = transfer;
   1.395 +
   1.396 +    // Start the actual download process.
   1.397 +    persist.savePrivacyAwareURI(sourceURI, null, null, null, null, targetFile,
   1.398 +                                isPrivate);
   1.399 +  }.bind(this)).then(null, do_report_unexpected_exception);
   1.400 +
   1.401 +  return deferred.promise;
   1.402 +}
   1.403 +
   1.404 +/**
   1.405 + * Starts a new download using the nsIHelperAppService interface, and controls
   1.406 + * it using the legacy nsITransfer interface.  The source of the download will
   1.407 + * be "interruptible_resumable.txt" and partially downloaded data will be kept.
   1.408 + *
   1.409 + * @param aSourceUrl
   1.410 + *        String containing the URI for the download source, or null to use
   1.411 + *        httpUrl("interruptible_resumable.txt").
   1.412 + *
   1.413 + * @return {Promise}
   1.414 + * @resolves The Download object created as a consequence of controlling the
   1.415 + *           download through the legacy nsITransfer interface.
   1.416 + * @rejects Never.  The current test fails in case of exceptions.
   1.417 + */
   1.418 +function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
   1.419 +  let sourceURI = NetUtil.newURI(aSourceUrl ||
   1.420 +                                 httpUrl("interruptible_resumable.txt"));
   1.421 +
   1.422 +  let deferred = Promise.defer();
   1.423 +
   1.424 +  Downloads.getList(Downloads.PUBLIC).then(function (aList) {
   1.425 +    // Temporarily register a view that will get notified when the download we
   1.426 +    // are controlling becomes visible in the list of downloads.
   1.427 +    aList.addView({
   1.428 +      onDownloadAdded: function (aDownload) {
   1.429 +        aList.removeView(this).then(null, do_report_unexpected_exception);
   1.430 +
   1.431 +        // Remove the download to keep the list empty for the next test.  This
   1.432 +        // also allows the caller to register the "onchange" event directly.
   1.433 +        let promise = aList.remove(aDownload);
   1.434 +
   1.435 +        // When the download object is ready, make it available to the caller.
   1.436 +        promise.then(() => deferred.resolve(aDownload),
   1.437 +                     do_report_unexpected_exception);
   1.438 +      },
   1.439 +    }).then(null, do_report_unexpected_exception);
   1.440 +
   1.441 +    let channel = NetUtil.newChannel(sourceURI);
   1.442 +
   1.443 +    // Start the actual download process.
   1.444 +    channel.asyncOpen({
   1.445 +      contentListener: null,
   1.446 +
   1.447 +      onStartRequest: function (aRequest, aContext)
   1.448 +      {
   1.449 +        let channel = aRequest.QueryInterface(Ci.nsIChannel);
   1.450 +        this.contentListener = gExternalHelperAppService.doContent(
   1.451 +                                     channel.contentType, aRequest, null, true);
   1.452 +        this.contentListener.onStartRequest(aRequest, aContext);
   1.453 +      },
   1.454 +
   1.455 +      onStopRequest: function (aRequest, aContext, aStatusCode)
   1.456 +      {
   1.457 +        this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
   1.458 +      },
   1.459 +
   1.460 +      onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
   1.461 +                                 aCount)
   1.462 +      {
   1.463 +        this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
   1.464 +                                             aOffset, aCount);
   1.465 +      },
   1.466 +    }, null);
   1.467 +  }.bind(this)).then(null, do_report_unexpected_exception);
   1.468 +
   1.469 +  return deferred.promise;
   1.470 +}
   1.471 +
   1.472 +/**
   1.473 + * Waits for a download to finish, in case it has not finished already.
   1.474 + *
   1.475 + * @param aDownload
   1.476 + *        The Download object to wait upon.
   1.477 + *
   1.478 + * @return {Promise}
   1.479 + * @resolves When the download has finished successfully.
   1.480 + * @rejects JavaScript exception if the download failed.
   1.481 + */
   1.482 +function promiseDownloadStopped(aDownload) {
   1.483 +  if (!aDownload.stopped) {
   1.484 +    // The download is in progress, wait for the current attempt to finish and
   1.485 +    // report any errors that may occur.
   1.486 +    return aDownload.start();
   1.487 +  }
   1.488 +
   1.489 +  if (aDownload.succeeded) {
   1.490 +    return Promise.resolve();
   1.491 +  }
   1.492 +
   1.493 +  // The download failed or was canceled.
   1.494 +  return Promise.reject(aDownload.error || new Error("Download canceled."));
   1.495 +}
   1.496 +
   1.497 +/**
   1.498 + * Waits for a download to reach half of its progress, in case it has not
   1.499 + * reached the expected progress already.
   1.500 + *
   1.501 + * @param aDownload
   1.502 + *        The Download object to wait upon.
   1.503 + *
   1.504 + * @return {Promise}
   1.505 + * @resolves When the download has reached half of its progress.
   1.506 + * @rejects Never.
   1.507 + */
   1.508 +function promiseDownloadMidway(aDownload) {
   1.509 +  let deferred = Promise.defer();
   1.510 +
   1.511 +  // Wait for the download to reach half of its progress.
   1.512 +  let onchange = function () {
   1.513 +    if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
   1.514 +      aDownload.onchange = null;
   1.515 +      deferred.resolve();
   1.516 +    }
   1.517 +  };
   1.518 +
   1.519 +  // Register for the notification, but also call the function directly in
   1.520 +  // case the download already reached the expected progress.
   1.521 +  aDownload.onchange = onchange;
   1.522 +  onchange();
   1.523 +
   1.524 +  return deferred.promise;
   1.525 +}
   1.526 +
   1.527 +/**
   1.528 + * Waits for a download to finish, in case it has not finished already.
   1.529 + *
   1.530 + * @param aDownload
   1.531 + *        The Download object to wait upon.
   1.532 + *
   1.533 + * @return {Promise}
   1.534 + * @resolves When the download has finished successfully.
   1.535 + * @rejects JavaScript exception if the download failed.
   1.536 + */
   1.537 +function promiseDownloadStopped(aDownload) {
   1.538 +  if (!aDownload.stopped) {
   1.539 +    // The download is in progress, wait for the current attempt to finish and
   1.540 +    // report any errors that may occur.
   1.541 +    return aDownload.start();
   1.542 +  }
   1.543 +
   1.544 +  if (aDownload.succeeded) {
   1.545 +    return Promise.resolve();
   1.546 +  }
   1.547 +
   1.548 +  // The download failed or was canceled.
   1.549 +  return Promise.reject(aDownload.error || new Error("Download canceled."));
   1.550 +}
   1.551 +
   1.552 +/**
   1.553 + * Returns a new public or private DownloadList object.
   1.554 + *
   1.555 + * @param aIsPrivate
   1.556 + *        True for the private list, false or undefined for the public list.
   1.557 + *
   1.558 + * @return {Promise}
   1.559 + * @resolves The newly created DownloadList object.
   1.560 + * @rejects JavaScript exception.
   1.561 + */
   1.562 +function promiseNewList(aIsPrivate)
   1.563 +{
   1.564 +  // We need to clear all the internal state for the list and summary objects,
   1.565 +  // since all the objects are interdependent internally.
   1.566 +  Downloads._promiseListsInitialized = null;
   1.567 +  Downloads._lists = {};
   1.568 +  Downloads._summaries = {};
   1.569 +
   1.570 +  return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
   1.571 +}
   1.572 +
   1.573 +/**
   1.574 + * Ensures that the given file contents are equal to the given string.
   1.575 + *
   1.576 + * @param aPath
   1.577 + *        String containing the path of the file whose contents should be
   1.578 + *        verified.
   1.579 + * @param aExpectedContents
   1.580 + *        String containing the octets that are expected in the file.
   1.581 + *
   1.582 + * @return {Promise}
   1.583 + * @resolves When the operation completes.
   1.584 + * @rejects Never.
   1.585 + */
   1.586 +function promiseVerifyContents(aPath, aExpectedContents)
   1.587 +{
   1.588 +  return Task.spawn(function() {
   1.589 +    let file = new FileUtils.File(aPath);
   1.590 +
   1.591 +    if (!(yield OS.File.exists(aPath))) {
   1.592 +      do_throw("File does not exist: " + aPath);
   1.593 +    }
   1.594 +
   1.595 +    if ((yield OS.File.stat(aPath)).size == 0) {
   1.596 +      do_throw("File is empty: " + aPath);
   1.597 +    }
   1.598 +
   1.599 +    let deferred = Promise.defer();
   1.600 +    NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
   1.601 +      do_check_true(Components.isSuccessCode(aStatus));
   1.602 +      let contents = NetUtil.readInputStreamToString(aInputStream,
   1.603 +                                                     aInputStream.available());
   1.604 +      if (contents.length > TEST_DATA_SHORT.length * 2 ||
   1.605 +          /[^\x20-\x7E]/.test(contents)) {
   1.606 +        // Do not print the entire content string to the test log.
   1.607 +        do_check_eq(contents.length, aExpectedContents.length);
   1.608 +        do_check_true(contents == aExpectedContents);
   1.609 +      } else {
   1.610 +        // Print the string if it is short and made of printable characters.
   1.611 +        do_check_eq(contents, aExpectedContents);
   1.612 +      }
   1.613 +      deferred.resolve();
   1.614 +    });
   1.615 +    yield deferred.promise;
   1.616 +  });
   1.617 +}
   1.618 +
   1.619 +/**
   1.620 + * Starts a socket listener that closes each incoming connection.
   1.621 + *
   1.622 + * @returns nsIServerSocket that listens for connections.  Call its "close"
   1.623 + *          method to stop listening and free the server port.
   1.624 + */
   1.625 +function startFakeServer()
   1.626 +{
   1.627 +  let serverSocket = new ServerSocket(-1, true, -1);
   1.628 +  serverSocket.asyncListen({
   1.629 +    onSocketAccepted: function (aServ, aTransport) {
   1.630 +      aTransport.close(Cr.NS_BINDING_ABORTED);
   1.631 +    },
   1.632 +    onStopListening: function () { },
   1.633 +  });
   1.634 +  return serverSocket;
   1.635 +}
   1.636 +
   1.637 +/**
   1.638 + * This is an internal reference that should not be used directly by tests.
   1.639 + */
   1.640 +let _gDeferResponses = Promise.defer();
   1.641 +
   1.642 +/**
   1.643 + * Ensures that all the interruptible requests started after this function is
   1.644 + * called won't complete until the continueResponses function is called.
   1.645 + *
   1.646 + * Normally, the internal HTTP server returns all the available data as soon as
   1.647 + * a request is received.  In order for some requests to be served one part at a
   1.648 + * time, special interruptible handlers are registered on the HTTP server.  This
   1.649 + * allows testing events or actions that need to happen in the middle of a
   1.650 + * download.
   1.651 + *
   1.652 + * For example, the handler accessible at the httpUri("interruptible.txt")
   1.653 + * address returns the TEST_DATA_SHORT text, then it may block until the
   1.654 + * continueResponses method is called.  At this point, the handler sends the
   1.655 + * TEST_DATA_SHORT text again to complete the response.
   1.656 + *
   1.657 + * If an interruptible request is started before the function is called, it may
   1.658 + * or may not be blocked depending on the actual sequence of events.
   1.659 + */
   1.660 +function mustInterruptResponses()
   1.661 +{
   1.662 +  // If there are pending blocked requests, allow them to complete.  This is
   1.663 +  // done to prevent requests from being blocked forever, but should not affect
   1.664 +  // the test logic, since previously started requests should not be monitored
   1.665 +  // on the client side anymore.
   1.666 +  _gDeferResponses.resolve();
   1.667 +
   1.668 +  do_print("Interruptible responses will be blocked midway.");
   1.669 +  _gDeferResponses = Promise.defer();
   1.670 +}
   1.671 +
   1.672 +/**
   1.673 + * Allows all the current and future interruptible requests to complete.
   1.674 + */
   1.675 +function continueResponses()
   1.676 +{
   1.677 +  do_print("Interruptible responses are now allowed to continue.");
   1.678 +  _gDeferResponses.resolve();
   1.679 +}
   1.680 +
   1.681 +/**
   1.682 + * Registers an interruptible response handler.
   1.683 + *
   1.684 + * @param aPath
   1.685 + *        Path passed to nsIHttpServer.registerPathHandler.
   1.686 + * @param aFirstPartFn
   1.687 + *        This function is called when the response is received, with the
   1.688 + *        aRequest and aResponse arguments of the server.
   1.689 + * @param aSecondPartFn
   1.690 + *        This function is called with the aRequest and aResponse arguments of
   1.691 + *        the server, when the continueResponses function is called.
   1.692 + */
   1.693 +function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
   1.694 +{
   1.695 +  gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
   1.696 +    do_print("Interruptible request started.");
   1.697 +
   1.698 +    // Process the first part of the response.
   1.699 +    aResponse.processAsync();
   1.700 +    aFirstPartFn(aRequest, aResponse);
   1.701 +
   1.702 +    // Wait on the current deferred object, then finish the request.
   1.703 +    _gDeferResponses.promise.then(function RIH_onSuccess() {
   1.704 +      aSecondPartFn(aRequest, aResponse);
   1.705 +      aResponse.finish();
   1.706 +      do_print("Interruptible request finished.");
   1.707 +    }).then(null, Cu.reportError);
   1.708 +  });
   1.709 +}
   1.710 +
   1.711 +/**
   1.712 + * Ensure the given date object is valid.
   1.713 + *
   1.714 + * @param aDate
   1.715 + *        The date object to be checked. This value can be null.
   1.716 + */
   1.717 +function isValidDate(aDate) {
   1.718 +  return aDate && aDate.getTime && !isNaN(aDate.getTime());
   1.719 +}
   1.720 +
   1.721 +/**
   1.722 + * Position of the first byte served by the "interruptible_resumable.txt"
   1.723 + * handler during the most recent response.
   1.724 + */
   1.725 +let gMostRecentFirstBytePos;
   1.726 +
   1.727 +////////////////////////////////////////////////////////////////////////////////
   1.728 +//// Initialization functions common to all tests
   1.729 +
   1.730 +add_task(function test_common_initialize()
   1.731 +{
   1.732 +  // Start the HTTP server.
   1.733 +  gHttpServer = new HttpServer();
   1.734 +  gHttpServer.registerDirectory("/", do_get_file("../data"));
   1.735 +  gHttpServer.start(-1);
   1.736 +
   1.737 +  // Cache locks might prevent concurrent requests to the same resource, and
   1.738 +  // this may block tests that use the interruptible handlers.
   1.739 +  Services.prefs.setBoolPref("browser.cache.disk.enable", false);
   1.740 +  Services.prefs.setBoolPref("browser.cache.memory.enable", false);
   1.741 +  do_register_cleanup(function () {
   1.742 +    Services.prefs.clearUserPref("browser.cache.disk.enable");
   1.743 +    Services.prefs.clearUserPref("browser.cache.memory.enable");
   1.744 +  });
   1.745 +
   1.746 +  registerInterruptibleHandler("/interruptible.txt",
   1.747 +    function firstPart(aRequest, aResponse) {
   1.748 +      aResponse.setHeader("Content-Type", "text/plain", false);
   1.749 +      aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
   1.750 +                          false);
   1.751 +      aResponse.write(TEST_DATA_SHORT);
   1.752 +    }, function secondPart(aRequest, aResponse) {
   1.753 +      aResponse.write(TEST_DATA_SHORT);
   1.754 +    });
   1.755 +
   1.756 +  registerInterruptibleHandler("/interruptible_resumable.txt",
   1.757 +    function firstPart(aRequest, aResponse) {
   1.758 +      aResponse.setHeader("Content-Type", "text/plain", false);
   1.759 +
   1.760 +      // Determine if only part of the data should be sent.
   1.761 +      let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
   1.762 +      if (aRequest.hasHeader("Range")) {
   1.763 +        var matches = aRequest.getHeader("Range")
   1.764 +                              .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
   1.765 +        var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
   1.766 +        var lastBytePos = (matches[2] === undefined) ? data.length - 1
   1.767 +                                            : matches[2];
   1.768 +        if (firstBytePos >= data.length) {
   1.769 +          aResponse.setStatusLine(aRequest.httpVersion, 416,
   1.770 +                             "Requested Range Not Satisfiable");
   1.771 +          aResponse.setHeader("Content-Range", "*/" + data.length, false);
   1.772 +          aResponse.finish();
   1.773 +          return;
   1.774 +        }
   1.775 +
   1.776 +        aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
   1.777 +        aResponse.setHeader("Content-Range", firstBytePos + "-" +
   1.778 +                                             lastBytePos + "/" +
   1.779 +                                             data.length, false);
   1.780 +
   1.781 +        data = data.substring(firstBytePos, lastBytePos + 1);
   1.782 +
   1.783 +        gMostRecentFirstBytePos = firstBytePos;
   1.784 +      } else {
   1.785 +        gMostRecentFirstBytePos = 0;
   1.786 +      }
   1.787 +
   1.788 +      aResponse.setHeader("Content-Length", "" + data.length, false);
   1.789 +
   1.790 +      aResponse.write(data.substring(0, data.length / 2));
   1.791 +
   1.792 +      // Store the second part of the data on the response object, so that it
   1.793 +      // can be used by the secondPart function.
   1.794 +      aResponse.secondPartData = data.substring(data.length / 2);
   1.795 +    }, function secondPart(aRequest, aResponse) {
   1.796 +      aResponse.write(aResponse.secondPartData);
   1.797 +    });
   1.798 +
   1.799 +  registerInterruptibleHandler("/interruptible_gzip.txt",
   1.800 +    function firstPart(aRequest, aResponse) {
   1.801 +      aResponse.setHeader("Content-Type", "text/plain", false);
   1.802 +      aResponse.setHeader("Content-Encoding", "gzip", false);
   1.803 +      aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
   1.804 +
   1.805 +      let bos =  new BinaryOutputStream(aResponse.bodyOutputStream);
   1.806 +      bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
   1.807 +                         TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
   1.808 +    }, function secondPart(aRequest, aResponse) {
   1.809 +      let bos =  new BinaryOutputStream(aResponse.bodyOutputStream);
   1.810 +      bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
   1.811 +                         TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
   1.812 +    });
   1.813 +
   1.814 +  // This URL will emulate being blocked by Windows Parental controls
   1.815 +  gHttpServer.registerPathHandler("/parentalblocked.zip",
   1.816 +    function (aRequest, aResponse) {
   1.817 +      aResponse.setStatusLine(aRequest.httpVersion, 450,
   1.818 +                              "Blocked by Windows Parental Controls");
   1.819 +    });
   1.820 +
   1.821 +  // Disable integration with the host application requiring profile access.
   1.822 +  DownloadIntegration.dontLoadList = true;
   1.823 +  DownloadIntegration.dontLoadObservers = true;
   1.824 +  // Disable the parental controls checking.
   1.825 +  DownloadIntegration.dontCheckParentalControls = true;
   1.826 +  // Disable application reputation checks.
   1.827 +  DownloadIntegration.dontCheckApplicationReputation = true;
   1.828 +  // Disable the calls to the OS to launch files and open containing folders
   1.829 +  DownloadIntegration.dontOpenFileAndFolder = true;
   1.830 +  DownloadIntegration._deferTestOpenFile = Promise.defer();
   1.831 +  DownloadIntegration._deferTestShowDir = Promise.defer();
   1.832 +
   1.833 +  // Get a reference to nsIComponentRegistrar, and ensure that is is freed
   1.834 +  // before the XPCOM shutdown.
   1.835 +  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
   1.836 +  do_register_cleanup(() => registrar = null);
   1.837 +
   1.838 +  // Make sure that downloads started using nsIExternalHelperAppService are
   1.839 +  // saved to disk without asking for a destination interactively.
   1.840 +  let mockFactory = {
   1.841 +    createInstance: function (aOuter, aIid) {
   1.842 +      return {
   1.843 +        QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
   1.844 +        promptForSaveToFile: function (aLauncher, aWindowContext,
   1.845 +                                       aDefaultFileName,
   1.846 +                                       aSuggestedFileExtension,
   1.847 +                                       aForcePrompt)
   1.848 +        {
   1.849 +          throw new Components.Exception(
   1.850 +                             "Synchronous promptForSaveToFile not implemented.",
   1.851 +                             Cr.NS_ERROR_NOT_AVAILABLE);
   1.852 +        },
   1.853 +        promptForSaveToFileAsync: function (aLauncher, aWindowContext,
   1.854 +                                            aDefaultFileName,
   1.855 +                                            aSuggestedFileExtension,
   1.856 +                                            aForcePrompt)
   1.857 +        {
   1.858 +          // The dialog should create the empty placeholder file.
   1.859 +          let file = getTempFile(TEST_TARGET_FILE_NAME);
   1.860 +          file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
   1.861 +          aLauncher.saveDestinationAvailable(file);
   1.862 +        },
   1.863 +      }.QueryInterface(aIid);
   1.864 +    }
   1.865 +  };
   1.866 +
   1.867 +  let contractID = "@mozilla.org/helperapplauncherdialog;1";
   1.868 +  let cid = registrar.contractIDToCID(contractID);
   1.869 +  let oldFactory = Components.manager.getClassObject(Cc[contractID],
   1.870 +                                                     Ci.nsIFactory);
   1.871 +
   1.872 +  registrar.unregisterFactory(cid, oldFactory);
   1.873 +  registrar.registerFactory(cid, "", contractID, mockFactory);
   1.874 +  do_register_cleanup(function () {
   1.875 +    registrar.unregisterFactory(cid, mockFactory);
   1.876 +    registrar.registerFactory(cid, "", contractID, oldFactory);
   1.877 +  });
   1.878 +
   1.879 +  // We must also make sure that nsIExternalHelperAppService uses the
   1.880 +  // JavaScript implementation of nsITransfer, because the
   1.881 +  // "@mozilla.org/transfer;1" contract is currently implemented in
   1.882 +  // "toolkit/components/downloads".  When the other folder is not included in
   1.883 +  // builds anymore (bug 851471), we'll not need to do this anymore.
   1.884 +  let transferContractID = "@mozilla.org/transfer;1";
   1.885 +  let transferNewCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
   1.886 +  let transferCid = registrar.contractIDToCID(transferContractID);
   1.887 +
   1.888 +  registrar.registerFactory(transferNewCid, "", transferContractID, null);
   1.889 +  do_register_cleanup(function () {
   1.890 +    registrar.registerFactory(transferCid, "", transferContractID, null);
   1.891 +  });
   1.892 +});

mercurial