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

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80: */
michael@0 3 /* Any copyright is dedicated to the Public Domain.
michael@0 4 * http://creativecommons.org/publicdomain/zero/1.0/ */
michael@0 5
michael@0 6 /**
michael@0 7 * Provides infrastructure for automated download components tests.
michael@0 8 */
michael@0 9
michael@0 10 "use strict";
michael@0 11
michael@0 12 ////////////////////////////////////////////////////////////////////////////////
michael@0 13 //// Globals
michael@0 14
michael@0 15 const Cc = Components.classes;
michael@0 16 const Ci = Components.interfaces;
michael@0 17 const Cu = Components.utils;
michael@0 18 const Cr = Components.results;
michael@0 19
michael@0 20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 21
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
michael@0 23 "resource://gre/modules/DownloadPaths.jsm");
michael@0 24 XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
michael@0 25 "resource://gre/modules/DownloadIntegration.jsm");
michael@0 26 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
michael@0 27 "resource://gre/modules/Downloads.jsm");
michael@0 28 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0 29 "resource://gre/modules/FileUtils.jsm");
michael@0 30 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
michael@0 31 "resource://testing-common/httpd.js");
michael@0 32 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 33 "resource://gre/modules/NetUtil.jsm");
michael@0 34 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
michael@0 35 "resource://gre/modules/PlacesUtils.jsm");
michael@0 36 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 37 "resource://gre/modules/Promise.jsm");
michael@0 38 XPCOMUtils.defineLazyModuleGetter(this, "Services",
michael@0 39 "resource://gre/modules/Services.jsm");
michael@0 40 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 41 "resource://gre/modules/Task.jsm");
michael@0 42 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 43 "resource://gre/modules/osfile.jsm");
michael@0 44
michael@0 45 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
michael@0 46 "@mozilla.org/uriloader/external-helper-app-service;1",
michael@0 47 Ci.nsIExternalHelperAppService);
michael@0 48
michael@0 49 const ServerSocket = Components.Constructor(
michael@0 50 "@mozilla.org/network/server-socket;1",
michael@0 51 "nsIServerSocket",
michael@0 52 "init");
michael@0 53 const BinaryOutputStream = Components.Constructor(
michael@0 54 "@mozilla.org/binaryoutputstream;1",
michael@0 55 "nsIBinaryOutputStream",
michael@0 56 "setOutputStream")
michael@0 57
michael@0 58 XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
michael@0 59 "@mozilla.org/mime;1",
michael@0 60 "nsIMIMEService");
michael@0 61
michael@0 62 const TEST_TARGET_FILE_NAME = "test-download.txt";
michael@0 63 const TEST_STORE_FILE_NAME = "test-downloads.json";
michael@0 64
michael@0 65 const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
michael@0 66
michael@0 67 const TEST_DATA_SHORT = "This test string is downloaded.";
michael@0 68 // Generate using gzipCompressString in TelemetryPing.jsm.
michael@0 69 const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
michael@0 70 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
michael@0 71 ];
michael@0 72 const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
michael@0 73 75,87,0,114,83,242,203,243,114,242,19,83,82,83,244,0,151,222,109,43,31,0,0,0
michael@0 74 ];
michael@0 75 const TEST_DATA_SHORT_GZIP_ENCODED =
michael@0 76 TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
michael@0 77
michael@0 78 /**
michael@0 79 * All the tests are implemented with add_task, this starts them automatically.
michael@0 80 */
michael@0 81 function run_test()
michael@0 82 {
michael@0 83 do_get_profile();
michael@0 84 run_next_test();
michael@0 85 }
michael@0 86
michael@0 87 ////////////////////////////////////////////////////////////////////////////////
michael@0 88 //// Support functions
michael@0 89
michael@0 90 /**
michael@0 91 * HttpServer object initialized before tests start.
michael@0 92 */
michael@0 93 let gHttpServer;
michael@0 94
michael@0 95 /**
michael@0 96 * Given a file name, returns a string containing an URI that points to the file
michael@0 97 * on the currently running instance of the test HTTP server.
michael@0 98 */
michael@0 99 function httpUrl(aFileName) {
michael@0 100 return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
michael@0 101 aFileName;
michael@0 102 }
michael@0 103
michael@0 104 // While the previous test file should have deleted all the temporary files it
michael@0 105 // used, on Windows these might still be pending deletion on the physical file
michael@0 106 // system. Thus, start from a new base number every time, to make a collision
michael@0 107 // with a file that is still pending deletion highly unlikely.
michael@0 108 let gFileCounter = Math.floor(Math.random() * 1000000);
michael@0 109
michael@0 110 /**
michael@0 111 * Returns a reference to a temporary file, that is guaranteed not to exist, and
michael@0 112 * to have never been created before.
michael@0 113 *
michael@0 114 * @param aLeafName
michael@0 115 * Suggested leaf name for the file to be created.
michael@0 116 *
michael@0 117 * @return nsIFile pointing to a non-existent file in a temporary directory.
michael@0 118 *
michael@0 119 * @note It is not enough to delete the file if it exists, or to delete the file
michael@0 120 * after calling nsIFile.createUnique, because on Windows the delete
michael@0 121 * operation in the file system may still be pending, preventing a new
michael@0 122 * file with the same name to be created.
michael@0 123 */
michael@0 124 function getTempFile(aLeafName)
michael@0 125 {
michael@0 126 // Prepend a serial number to the extension in the suggested leaf name.
michael@0 127 let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
michael@0 128 let leafName = base + "-" + gFileCounter + ext;
michael@0 129 gFileCounter++;
michael@0 130
michael@0 131 // Get a file reference under the temporary directory for this test file.
michael@0 132 let file = FileUtils.getFile("TmpD", [leafName]);
michael@0 133 do_check_false(file.exists());
michael@0 134
michael@0 135 do_register_cleanup(function () {
michael@0 136 if (file.exists()) {
michael@0 137 file.remove(false);
michael@0 138 }
michael@0 139 });
michael@0 140
michael@0 141 return file;
michael@0 142 }
michael@0 143
michael@0 144 /**
michael@0 145 * Waits for pending events to be processed.
michael@0 146 *
michael@0 147 * @return {Promise}
michael@0 148 * @resolves When pending events have been processed.
michael@0 149 * @rejects Never.
michael@0 150 */
michael@0 151 function promiseExecuteSoon()
michael@0 152 {
michael@0 153 let deferred = Promise.defer();
michael@0 154 do_execute_soon(deferred.resolve);
michael@0 155 return deferred.promise;
michael@0 156 }
michael@0 157
michael@0 158 /**
michael@0 159 * Waits for a pending events to be processed after a timeout.
michael@0 160 *
michael@0 161 * @return {Promise}
michael@0 162 * @resolves When pending events have been processed.
michael@0 163 * @rejects Never.
michael@0 164 */
michael@0 165 function promiseTimeout(aTime)
michael@0 166 {
michael@0 167 let deferred = Promise.defer();
michael@0 168 do_timeout(aTime, deferred.resolve);
michael@0 169 return deferred.promise;
michael@0 170 }
michael@0 171
michael@0 172 /**
michael@0 173 * Allows waiting for an observer notification once.
michael@0 174 *
michael@0 175 * @param aTopic
michael@0 176 * Notification topic to observe.
michael@0 177 *
michael@0 178 * @return {Promise}
michael@0 179 * @resolves The array [aSubject, aData] from the observed notification.
michael@0 180 * @rejects Never.
michael@0 181 */
michael@0 182 function promiseTopicObserved(aTopic)
michael@0 183 {
michael@0 184 let deferred = Promise.defer();
michael@0 185
michael@0 186 Services.obs.addObserver(
michael@0 187 function PTO_observe(aSubject, aTopic, aData) {
michael@0 188 Services.obs.removeObserver(PTO_observe, aTopic);
michael@0 189 deferred.resolve([aSubject, aData]);
michael@0 190 }, aTopic, false);
michael@0 191
michael@0 192 return deferred.promise;
michael@0 193 }
michael@0 194
michael@0 195 /**
michael@0 196 * Clears history asynchronously.
michael@0 197 *
michael@0 198 * @return {Promise}
michael@0 199 * @resolves When history has been cleared.
michael@0 200 * @rejects Never.
michael@0 201 */
michael@0 202 function promiseClearHistory()
michael@0 203 {
michael@0 204 let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
michael@0 205 do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
michael@0 206 return promise;
michael@0 207 }
michael@0 208
michael@0 209 /**
michael@0 210 * Waits for a new history visit to be notified for the specified URI.
michael@0 211 *
michael@0 212 * @param aUrl
michael@0 213 * String containing the URI that will be visited.
michael@0 214 *
michael@0 215 * @return {Promise}
michael@0 216 * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
michael@0 217 * @rejects Never.
michael@0 218 */
michael@0 219 function promiseWaitForVisit(aUrl)
michael@0 220 {
michael@0 221 let deferred = Promise.defer();
michael@0 222
michael@0 223 let uri = NetUtil.newURI(aUrl);
michael@0 224
michael@0 225 PlacesUtils.history.addObserver({
michael@0 226 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
michael@0 227 onBeginUpdateBatch: function () {},
michael@0 228 onEndUpdateBatch: function () {},
michael@0 229 onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
michael@0 230 aTransitionType, aGUID, aHidden) {
michael@0 231 if (aURI.equals(uri)) {
michael@0 232 PlacesUtils.history.removeObserver(this);
michael@0 233 deferred.resolve([aTime, aTransitionType]);
michael@0 234 }
michael@0 235 },
michael@0 236 onTitleChanged: function () {},
michael@0 237 onDeleteURI: function () {},
michael@0 238 onClearHistory: function () {},
michael@0 239 onPageChanged: function () {},
michael@0 240 onDeleteVisits: function () {},
michael@0 241 }, false);
michael@0 242
michael@0 243 return deferred.promise;
michael@0 244 }
michael@0 245
michael@0 246 /**
michael@0 247 * Check browsing history to see whether the given URI has been visited.
michael@0 248 *
michael@0 249 * @param aUrl
michael@0 250 * String containing the URI that will be visited.
michael@0 251 *
michael@0 252 * @return {Promise}
michael@0 253 * @resolves Boolean indicating whether the URI has been visited.
michael@0 254 * @rejects JavaScript exception.
michael@0 255 */
michael@0 256 function promiseIsURIVisited(aUrl) {
michael@0 257 let deferred = Promise.defer();
michael@0 258
michael@0 259 PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
michael@0 260 function (aURI, aIsVisited) {
michael@0 261 deferred.resolve(aIsVisited);
michael@0 262 });
michael@0 263
michael@0 264 return deferred.promise;
michael@0 265 }
michael@0 266
michael@0 267 /**
michael@0 268 * Creates a new Download object, setting a temporary file as the target.
michael@0 269 *
michael@0 270 * @param aSourceUrl
michael@0 271 * String containing the URI for the download source, or null to use
michael@0 272 * httpUrl("source.txt").
michael@0 273 *
michael@0 274 * @return {Promise}
michael@0 275 * @resolves The newly created Download object.
michael@0 276 * @rejects JavaScript exception.
michael@0 277 */
michael@0 278 function promiseNewDownload(aSourceUrl) {
michael@0 279 return Downloads.createDownload({
michael@0 280 source: aSourceUrl || httpUrl("source.txt"),
michael@0 281 target: getTempFile(TEST_TARGET_FILE_NAME),
michael@0 282 });
michael@0 283 }
michael@0 284
michael@0 285 /**
michael@0 286 * Starts a new download using the nsIWebBrowserPersist interface, and controls
michael@0 287 * it using the legacy nsITransfer interface.
michael@0 288 *
michael@0 289 * @param aSourceUrl
michael@0 290 * String containing the URI for the download source, or null to use
michael@0 291 * httpUrl("source.txt").
michael@0 292 * @param aOptions
michael@0 293 * An optional object used to control the behavior of this function.
michael@0 294 * You may pass an object with a subset of the following fields:
michael@0 295 * {
michael@0 296 * isPrivate: Boolean indicating whether the download originated from a
michael@0 297 * private window.
michael@0 298 * targetFile: nsIFile for the target, or null to use a temporary file.
michael@0 299 * outPersist: Receives a reference to the created nsIWebBrowserPersist
michael@0 300 * instance.
michael@0 301 * launchWhenSucceeded: Boolean indicating whether the target should
michael@0 302 * be launched when it has completed successfully.
michael@0 303 * launcherPath: String containing the path of the custom executable to
michael@0 304 * use to launch the target of the download.
michael@0 305 * }
michael@0 306 *
michael@0 307 * @return {Promise}
michael@0 308 * @resolves The Download object created as a consequence of controlling the
michael@0 309 * download through the legacy nsITransfer interface.
michael@0 310 * @rejects Never. The current test fails in case of exceptions.
michael@0 311 */
michael@0 312 function promiseStartLegacyDownload(aSourceUrl, aOptions) {
michael@0 313 let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
michael@0 314 let targetFile = (aOptions && aOptions.targetFile)
michael@0 315 || getTempFile(TEST_TARGET_FILE_NAME);
michael@0 316
michael@0 317 let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
michael@0 318 .createInstance(Ci.nsIWebBrowserPersist);
michael@0 319 if (aOptions) {
michael@0 320 aOptions.outPersist = persist;
michael@0 321 }
michael@0 322
michael@0 323 let fileExtension = null, mimeInfo = null;
michael@0 324 let match = sourceURI.path.match(/\.([^.\/]+)$/);
michael@0 325 if (match) {
michael@0 326 fileExtension = match[1];
michael@0 327 }
michael@0 328
michael@0 329 if (fileExtension) {
michael@0 330 try {
michael@0 331 mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension);
michael@0 332 mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
michael@0 333 } catch (ex) { }
michael@0 334 }
michael@0 335
michael@0 336 if (aOptions && aOptions.launcherPath) {
michael@0 337 do_check_true(mimeInfo != null);
michael@0 338
michael@0 339 let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
michael@0 340 .createInstance(Ci.nsILocalHandlerApp);
michael@0 341 localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath);
michael@0 342
michael@0 343 mimeInfo.preferredApplicationHandler = localHandlerApp;
michael@0 344 mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
michael@0 345 }
michael@0 346
michael@0 347 if (aOptions && aOptions.launchWhenSucceeded) {
michael@0 348 do_check_true(mimeInfo != null);
michael@0 349
michael@0 350 mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
michael@0 351 }
michael@0 352
michael@0 353 // Apply decoding if required by the "Content-Encoding" header.
michael@0 354 persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
michael@0 355 persist.persistFlags |=
michael@0 356 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
michael@0 357
michael@0 358 // We must create the nsITransfer implementation using its class ID because
michael@0 359 // the "@mozilla.org/transfer;1" contract is currently implemented in
michael@0 360 // "toolkit/components/downloads". When the other folder is not included in
michael@0 361 // builds anymore (bug 851471), we'll be able to use the contract ID.
michael@0 362 let transfer =
michael@0 363 Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"]
michael@0 364 .createInstance(Ci.nsITransfer);
michael@0 365
michael@0 366 let deferred = Promise.defer();
michael@0 367
michael@0 368 Downloads.getList(Downloads.ALL).then(function (aList) {
michael@0 369 // Temporarily register a view that will get notified when the download we
michael@0 370 // are controlling becomes visible in the list of downloads.
michael@0 371 aList.addView({
michael@0 372 onDownloadAdded: function (aDownload) {
michael@0 373 aList.removeView(this).then(null, do_report_unexpected_exception);
michael@0 374
michael@0 375 // Remove the download to keep the list empty for the next test. This
michael@0 376 // also allows the caller to register the "onchange" event directly.
michael@0 377 let promise = aList.remove(aDownload);
michael@0 378
michael@0 379 // When the download object is ready, make it available to the caller.
michael@0 380 promise.then(() => deferred.resolve(aDownload),
michael@0 381 do_report_unexpected_exception);
michael@0 382 },
michael@0 383 }).then(null, do_report_unexpected_exception);
michael@0 384
michael@0 385 let isPrivate = aOptions && aOptions.isPrivate;
michael@0 386
michael@0 387 // Initialize the components so they reference each other. This will cause
michael@0 388 // the Download object to be created and added to the public downloads.
michael@0 389 transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
michael@0 390 null, persist, isPrivate);
michael@0 391 persist.progressListener = transfer;
michael@0 392
michael@0 393 // Start the actual download process.
michael@0 394 persist.savePrivacyAwareURI(sourceURI, null, null, null, null, targetFile,
michael@0 395 isPrivate);
michael@0 396 }.bind(this)).then(null, do_report_unexpected_exception);
michael@0 397
michael@0 398 return deferred.promise;
michael@0 399 }
michael@0 400
michael@0 401 /**
michael@0 402 * Starts a new download using the nsIHelperAppService interface, and controls
michael@0 403 * it using the legacy nsITransfer interface. The source of the download will
michael@0 404 * be "interruptible_resumable.txt" and partially downloaded data will be kept.
michael@0 405 *
michael@0 406 * @param aSourceUrl
michael@0 407 * String containing the URI for the download source, or null to use
michael@0 408 * httpUrl("interruptible_resumable.txt").
michael@0 409 *
michael@0 410 * @return {Promise}
michael@0 411 * @resolves The Download object created as a consequence of controlling the
michael@0 412 * download through the legacy nsITransfer interface.
michael@0 413 * @rejects Never. The current test fails in case of exceptions.
michael@0 414 */
michael@0 415 function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
michael@0 416 let sourceURI = NetUtil.newURI(aSourceUrl ||
michael@0 417 httpUrl("interruptible_resumable.txt"));
michael@0 418
michael@0 419 let deferred = Promise.defer();
michael@0 420
michael@0 421 Downloads.getList(Downloads.PUBLIC).then(function (aList) {
michael@0 422 // Temporarily register a view that will get notified when the download we
michael@0 423 // are controlling becomes visible in the list of downloads.
michael@0 424 aList.addView({
michael@0 425 onDownloadAdded: function (aDownload) {
michael@0 426 aList.removeView(this).then(null, do_report_unexpected_exception);
michael@0 427
michael@0 428 // Remove the download to keep the list empty for the next test. This
michael@0 429 // also allows the caller to register the "onchange" event directly.
michael@0 430 let promise = aList.remove(aDownload);
michael@0 431
michael@0 432 // When the download object is ready, make it available to the caller.
michael@0 433 promise.then(() => deferred.resolve(aDownload),
michael@0 434 do_report_unexpected_exception);
michael@0 435 },
michael@0 436 }).then(null, do_report_unexpected_exception);
michael@0 437
michael@0 438 let channel = NetUtil.newChannel(sourceURI);
michael@0 439
michael@0 440 // Start the actual download process.
michael@0 441 channel.asyncOpen({
michael@0 442 contentListener: null,
michael@0 443
michael@0 444 onStartRequest: function (aRequest, aContext)
michael@0 445 {
michael@0 446 let channel = aRequest.QueryInterface(Ci.nsIChannel);
michael@0 447 this.contentListener = gExternalHelperAppService.doContent(
michael@0 448 channel.contentType, aRequest, null, true);
michael@0 449 this.contentListener.onStartRequest(aRequest, aContext);
michael@0 450 },
michael@0 451
michael@0 452 onStopRequest: function (aRequest, aContext, aStatusCode)
michael@0 453 {
michael@0 454 this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
michael@0 455 },
michael@0 456
michael@0 457 onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
michael@0 458 aCount)
michael@0 459 {
michael@0 460 this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
michael@0 461 aOffset, aCount);
michael@0 462 },
michael@0 463 }, null);
michael@0 464 }.bind(this)).then(null, do_report_unexpected_exception);
michael@0 465
michael@0 466 return deferred.promise;
michael@0 467 }
michael@0 468
michael@0 469 /**
michael@0 470 * Waits for a download to finish, in case it has not finished already.
michael@0 471 *
michael@0 472 * @param aDownload
michael@0 473 * The Download object to wait upon.
michael@0 474 *
michael@0 475 * @return {Promise}
michael@0 476 * @resolves When the download has finished successfully.
michael@0 477 * @rejects JavaScript exception if the download failed.
michael@0 478 */
michael@0 479 function promiseDownloadStopped(aDownload) {
michael@0 480 if (!aDownload.stopped) {
michael@0 481 // The download is in progress, wait for the current attempt to finish and
michael@0 482 // report any errors that may occur.
michael@0 483 return aDownload.start();
michael@0 484 }
michael@0 485
michael@0 486 if (aDownload.succeeded) {
michael@0 487 return Promise.resolve();
michael@0 488 }
michael@0 489
michael@0 490 // The download failed or was canceled.
michael@0 491 return Promise.reject(aDownload.error || new Error("Download canceled."));
michael@0 492 }
michael@0 493
michael@0 494 /**
michael@0 495 * Waits for a download to reach half of its progress, in case it has not
michael@0 496 * reached the expected progress already.
michael@0 497 *
michael@0 498 * @param aDownload
michael@0 499 * The Download object to wait upon.
michael@0 500 *
michael@0 501 * @return {Promise}
michael@0 502 * @resolves When the download has reached half of its progress.
michael@0 503 * @rejects Never.
michael@0 504 */
michael@0 505 function promiseDownloadMidway(aDownload) {
michael@0 506 let deferred = Promise.defer();
michael@0 507
michael@0 508 // Wait for the download to reach half of its progress.
michael@0 509 let onchange = function () {
michael@0 510 if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
michael@0 511 aDownload.onchange = null;
michael@0 512 deferred.resolve();
michael@0 513 }
michael@0 514 };
michael@0 515
michael@0 516 // Register for the notification, but also call the function directly in
michael@0 517 // case the download already reached the expected progress.
michael@0 518 aDownload.onchange = onchange;
michael@0 519 onchange();
michael@0 520
michael@0 521 return deferred.promise;
michael@0 522 }
michael@0 523
michael@0 524 /**
michael@0 525 * Waits for a download to finish, in case it has not finished already.
michael@0 526 *
michael@0 527 * @param aDownload
michael@0 528 * The Download object to wait upon.
michael@0 529 *
michael@0 530 * @return {Promise}
michael@0 531 * @resolves When the download has finished successfully.
michael@0 532 * @rejects JavaScript exception if the download failed.
michael@0 533 */
michael@0 534 function promiseDownloadStopped(aDownload) {
michael@0 535 if (!aDownload.stopped) {
michael@0 536 // The download is in progress, wait for the current attempt to finish and
michael@0 537 // report any errors that may occur.
michael@0 538 return aDownload.start();
michael@0 539 }
michael@0 540
michael@0 541 if (aDownload.succeeded) {
michael@0 542 return Promise.resolve();
michael@0 543 }
michael@0 544
michael@0 545 // The download failed or was canceled.
michael@0 546 return Promise.reject(aDownload.error || new Error("Download canceled."));
michael@0 547 }
michael@0 548
michael@0 549 /**
michael@0 550 * Returns a new public or private DownloadList object.
michael@0 551 *
michael@0 552 * @param aIsPrivate
michael@0 553 * True for the private list, false or undefined for the public list.
michael@0 554 *
michael@0 555 * @return {Promise}
michael@0 556 * @resolves The newly created DownloadList object.
michael@0 557 * @rejects JavaScript exception.
michael@0 558 */
michael@0 559 function promiseNewList(aIsPrivate)
michael@0 560 {
michael@0 561 // We need to clear all the internal state for the list and summary objects,
michael@0 562 // since all the objects are interdependent internally.
michael@0 563 Downloads._promiseListsInitialized = null;
michael@0 564 Downloads._lists = {};
michael@0 565 Downloads._summaries = {};
michael@0 566
michael@0 567 return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
michael@0 568 }
michael@0 569
michael@0 570 /**
michael@0 571 * Ensures that the given file contents are equal to the given string.
michael@0 572 *
michael@0 573 * @param aPath
michael@0 574 * String containing the path of the file whose contents should be
michael@0 575 * verified.
michael@0 576 * @param aExpectedContents
michael@0 577 * String containing the octets that are expected in the file.
michael@0 578 *
michael@0 579 * @return {Promise}
michael@0 580 * @resolves When the operation completes.
michael@0 581 * @rejects Never.
michael@0 582 */
michael@0 583 function promiseVerifyContents(aPath, aExpectedContents)
michael@0 584 {
michael@0 585 return Task.spawn(function() {
michael@0 586 let file = new FileUtils.File(aPath);
michael@0 587
michael@0 588 if (!(yield OS.File.exists(aPath))) {
michael@0 589 do_throw("File does not exist: " + aPath);
michael@0 590 }
michael@0 591
michael@0 592 if ((yield OS.File.stat(aPath)).size == 0) {
michael@0 593 do_throw("File is empty: " + aPath);
michael@0 594 }
michael@0 595
michael@0 596 let deferred = Promise.defer();
michael@0 597 NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
michael@0 598 do_check_true(Components.isSuccessCode(aStatus));
michael@0 599 let contents = NetUtil.readInputStreamToString(aInputStream,
michael@0 600 aInputStream.available());
michael@0 601 if (contents.length > TEST_DATA_SHORT.length * 2 ||
michael@0 602 /[^\x20-\x7E]/.test(contents)) {
michael@0 603 // Do not print the entire content string to the test log.
michael@0 604 do_check_eq(contents.length, aExpectedContents.length);
michael@0 605 do_check_true(contents == aExpectedContents);
michael@0 606 } else {
michael@0 607 // Print the string if it is short and made of printable characters.
michael@0 608 do_check_eq(contents, aExpectedContents);
michael@0 609 }
michael@0 610 deferred.resolve();
michael@0 611 });
michael@0 612 yield deferred.promise;
michael@0 613 });
michael@0 614 }
michael@0 615
michael@0 616 /**
michael@0 617 * Starts a socket listener that closes each incoming connection.
michael@0 618 *
michael@0 619 * @returns nsIServerSocket that listens for connections. Call its "close"
michael@0 620 * method to stop listening and free the server port.
michael@0 621 */
michael@0 622 function startFakeServer()
michael@0 623 {
michael@0 624 let serverSocket = new ServerSocket(-1, true, -1);
michael@0 625 serverSocket.asyncListen({
michael@0 626 onSocketAccepted: function (aServ, aTransport) {
michael@0 627 aTransport.close(Cr.NS_BINDING_ABORTED);
michael@0 628 },
michael@0 629 onStopListening: function () { },
michael@0 630 });
michael@0 631 return serverSocket;
michael@0 632 }
michael@0 633
michael@0 634 /**
michael@0 635 * This is an internal reference that should not be used directly by tests.
michael@0 636 */
michael@0 637 let _gDeferResponses = Promise.defer();
michael@0 638
michael@0 639 /**
michael@0 640 * Ensures that all the interruptible requests started after this function is
michael@0 641 * called won't complete until the continueResponses function is called.
michael@0 642 *
michael@0 643 * Normally, the internal HTTP server returns all the available data as soon as
michael@0 644 * a request is received. In order for some requests to be served one part at a
michael@0 645 * time, special interruptible handlers are registered on the HTTP server. This
michael@0 646 * allows testing events or actions that need to happen in the middle of a
michael@0 647 * download.
michael@0 648 *
michael@0 649 * For example, the handler accessible at the httpUri("interruptible.txt")
michael@0 650 * address returns the TEST_DATA_SHORT text, then it may block until the
michael@0 651 * continueResponses method is called. At this point, the handler sends the
michael@0 652 * TEST_DATA_SHORT text again to complete the response.
michael@0 653 *
michael@0 654 * If an interruptible request is started before the function is called, it may
michael@0 655 * or may not be blocked depending on the actual sequence of events.
michael@0 656 */
michael@0 657 function mustInterruptResponses()
michael@0 658 {
michael@0 659 // If there are pending blocked requests, allow them to complete. This is
michael@0 660 // done to prevent requests from being blocked forever, but should not affect
michael@0 661 // the test logic, since previously started requests should not be monitored
michael@0 662 // on the client side anymore.
michael@0 663 _gDeferResponses.resolve();
michael@0 664
michael@0 665 do_print("Interruptible responses will be blocked midway.");
michael@0 666 _gDeferResponses = Promise.defer();
michael@0 667 }
michael@0 668
michael@0 669 /**
michael@0 670 * Allows all the current and future interruptible requests to complete.
michael@0 671 */
michael@0 672 function continueResponses()
michael@0 673 {
michael@0 674 do_print("Interruptible responses are now allowed to continue.");
michael@0 675 _gDeferResponses.resolve();
michael@0 676 }
michael@0 677
michael@0 678 /**
michael@0 679 * Registers an interruptible response handler.
michael@0 680 *
michael@0 681 * @param aPath
michael@0 682 * Path passed to nsIHttpServer.registerPathHandler.
michael@0 683 * @param aFirstPartFn
michael@0 684 * This function is called when the response is received, with the
michael@0 685 * aRequest and aResponse arguments of the server.
michael@0 686 * @param aSecondPartFn
michael@0 687 * This function is called with the aRequest and aResponse arguments of
michael@0 688 * the server, when the continueResponses function is called.
michael@0 689 */
michael@0 690 function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
michael@0 691 {
michael@0 692 gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
michael@0 693 do_print("Interruptible request started.");
michael@0 694
michael@0 695 // Process the first part of the response.
michael@0 696 aResponse.processAsync();
michael@0 697 aFirstPartFn(aRequest, aResponse);
michael@0 698
michael@0 699 // Wait on the current deferred object, then finish the request.
michael@0 700 _gDeferResponses.promise.then(function RIH_onSuccess() {
michael@0 701 aSecondPartFn(aRequest, aResponse);
michael@0 702 aResponse.finish();
michael@0 703 do_print("Interruptible request finished.");
michael@0 704 }).then(null, Cu.reportError);
michael@0 705 });
michael@0 706 }
michael@0 707
michael@0 708 /**
michael@0 709 * Ensure the given date object is valid.
michael@0 710 *
michael@0 711 * @param aDate
michael@0 712 * The date object to be checked. This value can be null.
michael@0 713 */
michael@0 714 function isValidDate(aDate) {
michael@0 715 return aDate && aDate.getTime && !isNaN(aDate.getTime());
michael@0 716 }
michael@0 717
michael@0 718 /**
michael@0 719 * Position of the first byte served by the "interruptible_resumable.txt"
michael@0 720 * handler during the most recent response.
michael@0 721 */
michael@0 722 let gMostRecentFirstBytePos;
michael@0 723
michael@0 724 ////////////////////////////////////////////////////////////////////////////////
michael@0 725 //// Initialization functions common to all tests
michael@0 726
michael@0 727 add_task(function test_common_initialize()
michael@0 728 {
michael@0 729 // Start the HTTP server.
michael@0 730 gHttpServer = new HttpServer();
michael@0 731 gHttpServer.registerDirectory("/", do_get_file("../data"));
michael@0 732 gHttpServer.start(-1);
michael@0 733
michael@0 734 // Cache locks might prevent concurrent requests to the same resource, and
michael@0 735 // this may block tests that use the interruptible handlers.
michael@0 736 Services.prefs.setBoolPref("browser.cache.disk.enable", false);
michael@0 737 Services.prefs.setBoolPref("browser.cache.memory.enable", false);
michael@0 738 do_register_cleanup(function () {
michael@0 739 Services.prefs.clearUserPref("browser.cache.disk.enable");
michael@0 740 Services.prefs.clearUserPref("browser.cache.memory.enable");
michael@0 741 });
michael@0 742
michael@0 743 registerInterruptibleHandler("/interruptible.txt",
michael@0 744 function firstPart(aRequest, aResponse) {
michael@0 745 aResponse.setHeader("Content-Type", "text/plain", false);
michael@0 746 aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
michael@0 747 false);
michael@0 748 aResponse.write(TEST_DATA_SHORT);
michael@0 749 }, function secondPart(aRequest, aResponse) {
michael@0 750 aResponse.write(TEST_DATA_SHORT);
michael@0 751 });
michael@0 752
michael@0 753 registerInterruptibleHandler("/interruptible_resumable.txt",
michael@0 754 function firstPart(aRequest, aResponse) {
michael@0 755 aResponse.setHeader("Content-Type", "text/plain", false);
michael@0 756
michael@0 757 // Determine if only part of the data should be sent.
michael@0 758 let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
michael@0 759 if (aRequest.hasHeader("Range")) {
michael@0 760 var matches = aRequest.getHeader("Range")
michael@0 761 .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
michael@0 762 var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
michael@0 763 var lastBytePos = (matches[2] === undefined) ? data.length - 1
michael@0 764 : matches[2];
michael@0 765 if (firstBytePos >= data.length) {
michael@0 766 aResponse.setStatusLine(aRequest.httpVersion, 416,
michael@0 767 "Requested Range Not Satisfiable");
michael@0 768 aResponse.setHeader("Content-Range", "*/" + data.length, false);
michael@0 769 aResponse.finish();
michael@0 770 return;
michael@0 771 }
michael@0 772
michael@0 773 aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
michael@0 774 aResponse.setHeader("Content-Range", firstBytePos + "-" +
michael@0 775 lastBytePos + "/" +
michael@0 776 data.length, false);
michael@0 777
michael@0 778 data = data.substring(firstBytePos, lastBytePos + 1);
michael@0 779
michael@0 780 gMostRecentFirstBytePos = firstBytePos;
michael@0 781 } else {
michael@0 782 gMostRecentFirstBytePos = 0;
michael@0 783 }
michael@0 784
michael@0 785 aResponse.setHeader("Content-Length", "" + data.length, false);
michael@0 786
michael@0 787 aResponse.write(data.substring(0, data.length / 2));
michael@0 788
michael@0 789 // Store the second part of the data on the response object, so that it
michael@0 790 // can be used by the secondPart function.
michael@0 791 aResponse.secondPartData = data.substring(data.length / 2);
michael@0 792 }, function secondPart(aRequest, aResponse) {
michael@0 793 aResponse.write(aResponse.secondPartData);
michael@0 794 });
michael@0 795
michael@0 796 registerInterruptibleHandler("/interruptible_gzip.txt",
michael@0 797 function firstPart(aRequest, aResponse) {
michael@0 798 aResponse.setHeader("Content-Type", "text/plain", false);
michael@0 799 aResponse.setHeader("Content-Encoding", "gzip", false);
michael@0 800 aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
michael@0 801
michael@0 802 let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
michael@0 803 bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
michael@0 804 TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
michael@0 805 }, function secondPart(aRequest, aResponse) {
michael@0 806 let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
michael@0 807 bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
michael@0 808 TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
michael@0 809 });
michael@0 810
michael@0 811 // This URL will emulate being blocked by Windows Parental controls
michael@0 812 gHttpServer.registerPathHandler("/parentalblocked.zip",
michael@0 813 function (aRequest, aResponse) {
michael@0 814 aResponse.setStatusLine(aRequest.httpVersion, 450,
michael@0 815 "Blocked by Windows Parental Controls");
michael@0 816 });
michael@0 817
michael@0 818 // Disable integration with the host application requiring profile access.
michael@0 819 DownloadIntegration.dontLoadList = true;
michael@0 820 DownloadIntegration.dontLoadObservers = true;
michael@0 821 // Disable the parental controls checking.
michael@0 822 DownloadIntegration.dontCheckParentalControls = true;
michael@0 823 // Disable application reputation checks.
michael@0 824 DownloadIntegration.dontCheckApplicationReputation = true;
michael@0 825 // Disable the calls to the OS to launch files and open containing folders
michael@0 826 DownloadIntegration.dontOpenFileAndFolder = true;
michael@0 827 DownloadIntegration._deferTestOpenFile = Promise.defer();
michael@0 828 DownloadIntegration._deferTestShowDir = Promise.defer();
michael@0 829
michael@0 830 // Get a reference to nsIComponentRegistrar, and ensure that is is freed
michael@0 831 // before the XPCOM shutdown.
michael@0 832 let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
michael@0 833 do_register_cleanup(() => registrar = null);
michael@0 834
michael@0 835 // Make sure that downloads started using nsIExternalHelperAppService are
michael@0 836 // saved to disk without asking for a destination interactively.
michael@0 837 let mockFactory = {
michael@0 838 createInstance: function (aOuter, aIid) {
michael@0 839 return {
michael@0 840 QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
michael@0 841 promptForSaveToFile: function (aLauncher, aWindowContext,
michael@0 842 aDefaultFileName,
michael@0 843 aSuggestedFileExtension,
michael@0 844 aForcePrompt)
michael@0 845 {
michael@0 846 throw new Components.Exception(
michael@0 847 "Synchronous promptForSaveToFile not implemented.",
michael@0 848 Cr.NS_ERROR_NOT_AVAILABLE);
michael@0 849 },
michael@0 850 promptForSaveToFileAsync: function (aLauncher, aWindowContext,
michael@0 851 aDefaultFileName,
michael@0 852 aSuggestedFileExtension,
michael@0 853 aForcePrompt)
michael@0 854 {
michael@0 855 // The dialog should create the empty placeholder file.
michael@0 856 let file = getTempFile(TEST_TARGET_FILE_NAME);
michael@0 857 file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
michael@0 858 aLauncher.saveDestinationAvailable(file);
michael@0 859 },
michael@0 860 }.QueryInterface(aIid);
michael@0 861 }
michael@0 862 };
michael@0 863
michael@0 864 let contractID = "@mozilla.org/helperapplauncherdialog;1";
michael@0 865 let cid = registrar.contractIDToCID(contractID);
michael@0 866 let oldFactory = Components.manager.getClassObject(Cc[contractID],
michael@0 867 Ci.nsIFactory);
michael@0 868
michael@0 869 registrar.unregisterFactory(cid, oldFactory);
michael@0 870 registrar.registerFactory(cid, "", contractID, mockFactory);
michael@0 871 do_register_cleanup(function () {
michael@0 872 registrar.unregisterFactory(cid, mockFactory);
michael@0 873 registrar.registerFactory(cid, "", contractID, oldFactory);
michael@0 874 });
michael@0 875
michael@0 876 // We must also make sure that nsIExternalHelperAppService uses the
michael@0 877 // JavaScript implementation of nsITransfer, because the
michael@0 878 // "@mozilla.org/transfer;1" contract is currently implemented in
michael@0 879 // "toolkit/components/downloads". When the other folder is not included in
michael@0 880 // builds anymore (bug 851471), we'll not need to do this anymore.
michael@0 881 let transferContractID = "@mozilla.org/transfer;1";
michael@0 882 let transferNewCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
michael@0 883 let transferCid = registrar.contractIDToCID(transferContractID);
michael@0 884
michael@0 885 registrar.registerFactory(transferNewCid, "", transferContractID, null);
michael@0 886 do_register_cleanup(function () {
michael@0 887 registrar.registerFactory(transferCid, "", transferContractID, null);
michael@0 888 });
michael@0 889 });

mercurial