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 +});