michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: /** michael@0: * Provides infrastructure for automated download components tests. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths", michael@0: "resource://gre/modules/DownloadPaths.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration", michael@0: "resource://gre/modules/DownloadIntegration.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Downloads", michael@0: "resource://gre/modules/Downloads.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", michael@0: "resource://testing-common/httpd.js"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService", michael@0: "@mozilla.org/uriloader/external-helper-app-service;1", michael@0: Ci.nsIExternalHelperAppService); michael@0: michael@0: const ServerSocket = Components.Constructor( michael@0: "@mozilla.org/network/server-socket;1", michael@0: "nsIServerSocket", michael@0: "init"); michael@0: const BinaryOutputStream = Components.Constructor( michael@0: "@mozilla.org/binaryoutputstream;1", michael@0: "nsIBinaryOutputStream", michael@0: "setOutputStream") michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService", michael@0: "@mozilla.org/mime;1", michael@0: "nsIMIMEService"); michael@0: michael@0: const TEST_TARGET_FILE_NAME = "test-download.txt"; michael@0: const TEST_STORE_FILE_NAME = "test-downloads.json"; michael@0: michael@0: const TEST_REFERRER_URL = "http://www.example.com/referrer.html"; michael@0: michael@0: const TEST_DATA_SHORT = "This test string is downloaded."; michael@0: // Generate using gzipCompressString in TelemetryPing.jsm. michael@0: const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [ michael@0: 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: ]; michael@0: const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [ michael@0: 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: ]; michael@0: const TEST_DATA_SHORT_GZIP_ENCODED = michael@0: TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND); michael@0: michael@0: /** michael@0: * All the tests are implemented with add_task, this starts them automatically. michael@0: */ michael@0: function run_test() michael@0: { michael@0: do_get_profile(); michael@0: run_next_test(); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Support functions michael@0: michael@0: /** michael@0: * HttpServer object initialized before tests start. michael@0: */ michael@0: let gHttpServer; michael@0: michael@0: /** michael@0: * Given a file name, returns a string containing an URI that points to the file michael@0: * on the currently running instance of the test HTTP server. michael@0: */ michael@0: function httpUrl(aFileName) { michael@0: return "http://localhost:" + gHttpServer.identity.primaryPort + "/" + michael@0: aFileName; michael@0: } michael@0: michael@0: // While the previous test file should have deleted all the temporary files it michael@0: // used, on Windows these might still be pending deletion on the physical file michael@0: // system. Thus, start from a new base number every time, to make a collision michael@0: // with a file that is still pending deletion highly unlikely. michael@0: let gFileCounter = Math.floor(Math.random() * 1000000); michael@0: michael@0: /** michael@0: * Returns a reference to a temporary file, that is guaranteed not to exist, and michael@0: * to have never been created before. michael@0: * michael@0: * @param aLeafName michael@0: * Suggested leaf name for the file to be created. michael@0: * michael@0: * @return nsIFile pointing to a non-existent file in a temporary directory. michael@0: * michael@0: * @note It is not enough to delete the file if it exists, or to delete the file michael@0: * after calling nsIFile.createUnique, because on Windows the delete michael@0: * operation in the file system may still be pending, preventing a new michael@0: * file with the same name to be created. michael@0: */ michael@0: function getTempFile(aLeafName) michael@0: { michael@0: // Prepend a serial number to the extension in the suggested leaf name. michael@0: let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName); michael@0: let leafName = base + "-" + gFileCounter + ext; michael@0: gFileCounter++; michael@0: michael@0: // Get a file reference under the temporary directory for this test file. michael@0: let file = FileUtils.getFile("TmpD", [leafName]); michael@0: do_check_false(file.exists()); michael@0: michael@0: do_register_cleanup(function () { michael@0: if (file.exists()) { michael@0: file.remove(false); michael@0: } michael@0: }); michael@0: michael@0: return file; michael@0: } michael@0: michael@0: /** michael@0: * Waits for pending events to be processed. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When pending events have been processed. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseExecuteSoon() michael@0: { michael@0: let deferred = Promise.defer(); michael@0: do_execute_soon(deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Waits for a pending events to be processed after a timeout. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When pending events have been processed. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseTimeout(aTime) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: do_timeout(aTime, deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Allows waiting for an observer notification once. michael@0: * michael@0: * @param aTopic michael@0: * Notification topic to observe. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The array [aSubject, aData] from the observed notification. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseTopicObserved(aTopic) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: michael@0: Services.obs.addObserver( michael@0: function PTO_observe(aSubject, aTopic, aData) { michael@0: Services.obs.removeObserver(PTO_observe, aTopic); michael@0: deferred.resolve([aSubject, aData]); michael@0: }, aTopic, false); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Clears history asynchronously. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When history has been cleared. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseClearHistory() michael@0: { michael@0: let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); michael@0: do_execute_soon(function() PlacesUtils.bhistory.removeAllPages()); michael@0: return promise; michael@0: } michael@0: michael@0: /** michael@0: * Waits for a new history visit to be notified for the specified URI. michael@0: * michael@0: * @param aUrl michael@0: * String containing the URI that will be visited. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseWaitForVisit(aUrl) michael@0: { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let uri = NetUtil.newURI(aUrl); michael@0: michael@0: PlacesUtils.history.addObserver({ michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]), michael@0: onBeginUpdateBatch: function () {}, michael@0: onEndUpdateBatch: function () {}, michael@0: onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, michael@0: aTransitionType, aGUID, aHidden) { michael@0: if (aURI.equals(uri)) { michael@0: PlacesUtils.history.removeObserver(this); michael@0: deferred.resolve([aTime, aTransitionType]); michael@0: } michael@0: }, michael@0: onTitleChanged: function () {}, michael@0: onDeleteURI: function () {}, michael@0: onClearHistory: function () {}, michael@0: onPageChanged: function () {}, michael@0: onDeleteVisits: function () {}, michael@0: }, false); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Check browsing history to see whether the given URI has been visited. michael@0: * michael@0: * @param aUrl michael@0: * String containing the URI that will be visited. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves Boolean indicating whether the URI has been visited. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseIsURIVisited(aUrl) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl), michael@0: function (aURI, aIsVisited) { michael@0: deferred.resolve(aIsVisited); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Creates a new Download object, setting a temporary file as the target. michael@0: * michael@0: * @param aSourceUrl michael@0: * String containing the URI for the download source, or null to use michael@0: * httpUrl("source.txt"). michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The newly created Download object. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseNewDownload(aSourceUrl) { michael@0: return Downloads.createDownload({ michael@0: source: aSourceUrl || httpUrl("source.txt"), michael@0: target: getTempFile(TEST_TARGET_FILE_NAME), michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Starts a new download using the nsIWebBrowserPersist interface, and controls michael@0: * it using the legacy nsITransfer interface. michael@0: * michael@0: * @param aSourceUrl michael@0: * String containing the URI for the download source, or null to use michael@0: * httpUrl("source.txt"). michael@0: * @param aOptions michael@0: * An optional object used to control the behavior of this function. michael@0: * You may pass an object with a subset of the following fields: michael@0: * { michael@0: * isPrivate: Boolean indicating whether the download originated from a michael@0: * private window. michael@0: * targetFile: nsIFile for the target, or null to use a temporary file. michael@0: * outPersist: Receives a reference to the created nsIWebBrowserPersist michael@0: * instance. michael@0: * launchWhenSucceeded: Boolean indicating whether the target should michael@0: * be launched when it has completed successfully. michael@0: * launcherPath: String containing the path of the custom executable to michael@0: * use to launch the target of the download. michael@0: * } michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The Download object created as a consequence of controlling the michael@0: * download through the legacy nsITransfer interface. michael@0: * @rejects Never. The current test fails in case of exceptions. michael@0: */ michael@0: function promiseStartLegacyDownload(aSourceUrl, aOptions) { michael@0: let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt")); michael@0: let targetFile = (aOptions && aOptions.targetFile) michael@0: || getTempFile(TEST_TARGET_FILE_NAME); michael@0: michael@0: let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] michael@0: .createInstance(Ci.nsIWebBrowserPersist); michael@0: if (aOptions) { michael@0: aOptions.outPersist = persist; michael@0: } michael@0: michael@0: let fileExtension = null, mimeInfo = null; michael@0: let match = sourceURI.path.match(/\.([^.\/]+)$/); michael@0: if (match) { michael@0: fileExtension = match[1]; michael@0: } michael@0: michael@0: if (fileExtension) { michael@0: try { michael@0: mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension); michael@0: mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk; michael@0: } catch (ex) { } michael@0: } michael@0: michael@0: if (aOptions && aOptions.launcherPath) { michael@0: do_check_true(mimeInfo != null); michael@0: michael@0: let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"] michael@0: .createInstance(Ci.nsILocalHandlerApp); michael@0: localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath); michael@0: michael@0: mimeInfo.preferredApplicationHandler = localHandlerApp; michael@0: mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp; michael@0: } michael@0: michael@0: if (aOptions && aOptions.launchWhenSucceeded) { michael@0: do_check_true(mimeInfo != null); michael@0: michael@0: mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp; michael@0: } michael@0: michael@0: // Apply decoding if required by the "Content-Encoding" header. michael@0: persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION; michael@0: persist.persistFlags |= michael@0: Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; michael@0: michael@0: // We must create the nsITransfer implementation using its class ID because michael@0: // the "@mozilla.org/transfer;1" contract is currently implemented in michael@0: // "toolkit/components/downloads". When the other folder is not included in michael@0: // builds anymore (bug 851471), we'll be able to use the contract ID. michael@0: let transfer = michael@0: Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"] michael@0: .createInstance(Ci.nsITransfer); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: Downloads.getList(Downloads.ALL).then(function (aList) { michael@0: // Temporarily register a view that will get notified when the download we michael@0: // are controlling becomes visible in the list of downloads. michael@0: aList.addView({ michael@0: onDownloadAdded: function (aDownload) { michael@0: aList.removeView(this).then(null, do_report_unexpected_exception); michael@0: michael@0: // Remove the download to keep the list empty for the next test. This michael@0: // also allows the caller to register the "onchange" event directly. michael@0: let promise = aList.remove(aDownload); michael@0: michael@0: // When the download object is ready, make it available to the caller. michael@0: promise.then(() => deferred.resolve(aDownload), michael@0: do_report_unexpected_exception); michael@0: }, michael@0: }).then(null, do_report_unexpected_exception); michael@0: michael@0: let isPrivate = aOptions && aOptions.isPrivate; michael@0: michael@0: // Initialize the components so they reference each other. This will cause michael@0: // the Download object to be created and added to the public downloads. michael@0: transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null, michael@0: null, persist, isPrivate); michael@0: persist.progressListener = transfer; michael@0: michael@0: // Start the actual download process. michael@0: persist.savePrivacyAwareURI(sourceURI, null, null, null, null, targetFile, michael@0: isPrivate); michael@0: }.bind(this)).then(null, do_report_unexpected_exception); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Starts a new download using the nsIHelperAppService interface, and controls michael@0: * it using the legacy nsITransfer interface. The source of the download will michael@0: * be "interruptible_resumable.txt" and partially downloaded data will be kept. michael@0: * michael@0: * @param aSourceUrl michael@0: * String containing the URI for the download source, or null to use michael@0: * httpUrl("interruptible_resumable.txt"). michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The Download object created as a consequence of controlling the michael@0: * download through the legacy nsITransfer interface. michael@0: * @rejects Never. The current test fails in case of exceptions. michael@0: */ michael@0: function promiseStartExternalHelperAppServiceDownload(aSourceUrl) { michael@0: let sourceURI = NetUtil.newURI(aSourceUrl || michael@0: httpUrl("interruptible_resumable.txt")); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: Downloads.getList(Downloads.PUBLIC).then(function (aList) { michael@0: // Temporarily register a view that will get notified when the download we michael@0: // are controlling becomes visible in the list of downloads. michael@0: aList.addView({ michael@0: onDownloadAdded: function (aDownload) { michael@0: aList.removeView(this).then(null, do_report_unexpected_exception); michael@0: michael@0: // Remove the download to keep the list empty for the next test. This michael@0: // also allows the caller to register the "onchange" event directly. michael@0: let promise = aList.remove(aDownload); michael@0: michael@0: // When the download object is ready, make it available to the caller. michael@0: promise.then(() => deferred.resolve(aDownload), michael@0: do_report_unexpected_exception); michael@0: }, michael@0: }).then(null, do_report_unexpected_exception); michael@0: michael@0: let channel = NetUtil.newChannel(sourceURI); michael@0: michael@0: // Start the actual download process. michael@0: channel.asyncOpen({ michael@0: contentListener: null, michael@0: michael@0: onStartRequest: function (aRequest, aContext) michael@0: { michael@0: let channel = aRequest.QueryInterface(Ci.nsIChannel); michael@0: this.contentListener = gExternalHelperAppService.doContent( michael@0: channel.contentType, aRequest, null, true); michael@0: this.contentListener.onStartRequest(aRequest, aContext); michael@0: }, michael@0: michael@0: onStopRequest: function (aRequest, aContext, aStatusCode) michael@0: { michael@0: this.contentListener.onStopRequest(aRequest, aContext, aStatusCode); michael@0: }, michael@0: michael@0: onDataAvailable: function (aRequest, aContext, aInputStream, aOffset, michael@0: aCount) michael@0: { michael@0: this.contentListener.onDataAvailable(aRequest, aContext, aInputStream, michael@0: aOffset, aCount); michael@0: }, michael@0: }, null); michael@0: }.bind(this)).then(null, do_report_unexpected_exception); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Waits for a download to finish, in case it has not finished already. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to wait upon. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has finished successfully. michael@0: * @rejects JavaScript exception if the download failed. michael@0: */ michael@0: function promiseDownloadStopped(aDownload) { michael@0: if (!aDownload.stopped) { michael@0: // The download is in progress, wait for the current attempt to finish and michael@0: // report any errors that may occur. michael@0: return aDownload.start(); michael@0: } michael@0: michael@0: if (aDownload.succeeded) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: // The download failed or was canceled. michael@0: return Promise.reject(aDownload.error || new Error("Download canceled.")); michael@0: } michael@0: michael@0: /** michael@0: * Waits for a download to reach half of its progress, in case it has not michael@0: * reached the expected progress already. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to wait upon. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has reached half of its progress. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseDownloadMidway(aDownload) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: // Wait for the download to reach half of its progress. michael@0: let onchange = function () { michael@0: if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) { michael@0: aDownload.onchange = null; michael@0: deferred.resolve(); michael@0: } michael@0: }; michael@0: michael@0: // Register for the notification, but also call the function directly in michael@0: // case the download already reached the expected progress. michael@0: aDownload.onchange = onchange; michael@0: onchange(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Waits for a download to finish, in case it has not finished already. michael@0: * michael@0: * @param aDownload michael@0: * The Download object to wait upon. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has finished successfully. michael@0: * @rejects JavaScript exception if the download failed. michael@0: */ michael@0: function promiseDownloadStopped(aDownload) { michael@0: if (!aDownload.stopped) { michael@0: // The download is in progress, wait for the current attempt to finish and michael@0: // report any errors that may occur. michael@0: return aDownload.start(); michael@0: } michael@0: michael@0: if (aDownload.succeeded) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: // The download failed or was canceled. michael@0: return Promise.reject(aDownload.error || new Error("Download canceled.")); michael@0: } michael@0: michael@0: /** michael@0: * Returns a new public or private DownloadList object. michael@0: * michael@0: * @param aIsPrivate michael@0: * True for the private list, false or undefined for the public list. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves The newly created DownloadList object. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: function promiseNewList(aIsPrivate) michael@0: { michael@0: // We need to clear all the internal state for the list and summary objects, michael@0: // since all the objects are interdependent internally. michael@0: Downloads._promiseListsInitialized = null; michael@0: Downloads._lists = {}; michael@0: Downloads._summaries = {}; michael@0: michael@0: return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC); michael@0: } michael@0: michael@0: /** michael@0: * Ensures that the given file contents are equal to the given string. michael@0: * michael@0: * @param aPath michael@0: * String containing the path of the file whose contents should be michael@0: * verified. michael@0: * @param aExpectedContents michael@0: * String containing the octets that are expected in the file. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the operation completes. michael@0: * @rejects Never. michael@0: */ michael@0: function promiseVerifyContents(aPath, aExpectedContents) michael@0: { michael@0: return Task.spawn(function() { michael@0: let file = new FileUtils.File(aPath); michael@0: michael@0: if (!(yield OS.File.exists(aPath))) { michael@0: do_throw("File does not exist: " + aPath); michael@0: } michael@0: michael@0: if ((yield OS.File.stat(aPath)).size == 0) { michael@0: do_throw("File is empty: " + aPath); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: NetUtil.asyncFetch(file, function(aInputStream, aStatus) { michael@0: do_check_true(Components.isSuccessCode(aStatus)); michael@0: let contents = NetUtil.readInputStreamToString(aInputStream, michael@0: aInputStream.available()); michael@0: if (contents.length > TEST_DATA_SHORT.length * 2 || michael@0: /[^\x20-\x7E]/.test(contents)) { michael@0: // Do not print the entire content string to the test log. michael@0: do_check_eq(contents.length, aExpectedContents.length); michael@0: do_check_true(contents == aExpectedContents); michael@0: } else { michael@0: // Print the string if it is short and made of printable characters. michael@0: do_check_eq(contents, aExpectedContents); michael@0: } michael@0: deferred.resolve(); michael@0: }); michael@0: yield deferred.promise; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Starts a socket listener that closes each incoming connection. michael@0: * michael@0: * @returns nsIServerSocket that listens for connections. Call its "close" michael@0: * method to stop listening and free the server port. michael@0: */ michael@0: function startFakeServer() michael@0: { michael@0: let serverSocket = new ServerSocket(-1, true, -1); michael@0: serverSocket.asyncListen({ michael@0: onSocketAccepted: function (aServ, aTransport) { michael@0: aTransport.close(Cr.NS_BINDING_ABORTED); michael@0: }, michael@0: onStopListening: function () { }, michael@0: }); michael@0: return serverSocket; michael@0: } michael@0: michael@0: /** michael@0: * This is an internal reference that should not be used directly by tests. michael@0: */ michael@0: let _gDeferResponses = Promise.defer(); michael@0: michael@0: /** michael@0: * Ensures that all the interruptible requests started after this function is michael@0: * called won't complete until the continueResponses function is called. michael@0: * michael@0: * Normally, the internal HTTP server returns all the available data as soon as michael@0: * a request is received. In order for some requests to be served one part at a michael@0: * time, special interruptible handlers are registered on the HTTP server. This michael@0: * allows testing events or actions that need to happen in the middle of a michael@0: * download. michael@0: * michael@0: * For example, the handler accessible at the httpUri("interruptible.txt") michael@0: * address returns the TEST_DATA_SHORT text, then it may block until the michael@0: * continueResponses method is called. At this point, the handler sends the michael@0: * TEST_DATA_SHORT text again to complete the response. michael@0: * michael@0: * If an interruptible request is started before the function is called, it may michael@0: * or may not be blocked depending on the actual sequence of events. michael@0: */ michael@0: function mustInterruptResponses() michael@0: { michael@0: // If there are pending blocked requests, allow them to complete. This is michael@0: // done to prevent requests from being blocked forever, but should not affect michael@0: // the test logic, since previously started requests should not be monitored michael@0: // on the client side anymore. michael@0: _gDeferResponses.resolve(); michael@0: michael@0: do_print("Interruptible responses will be blocked midway."); michael@0: _gDeferResponses = Promise.defer(); michael@0: } michael@0: michael@0: /** michael@0: * Allows all the current and future interruptible requests to complete. michael@0: */ michael@0: function continueResponses() michael@0: { michael@0: do_print("Interruptible responses are now allowed to continue."); michael@0: _gDeferResponses.resolve(); michael@0: } michael@0: michael@0: /** michael@0: * Registers an interruptible response handler. michael@0: * michael@0: * @param aPath michael@0: * Path passed to nsIHttpServer.registerPathHandler. michael@0: * @param aFirstPartFn michael@0: * This function is called when the response is received, with the michael@0: * aRequest and aResponse arguments of the server. michael@0: * @param aSecondPartFn michael@0: * This function is called with the aRequest and aResponse arguments of michael@0: * the server, when the continueResponses function is called. michael@0: */ michael@0: function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn) michael@0: { michael@0: gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) { michael@0: do_print("Interruptible request started."); michael@0: michael@0: // Process the first part of the response. michael@0: aResponse.processAsync(); michael@0: aFirstPartFn(aRequest, aResponse); michael@0: michael@0: // Wait on the current deferred object, then finish the request. michael@0: _gDeferResponses.promise.then(function RIH_onSuccess() { michael@0: aSecondPartFn(aRequest, aResponse); michael@0: aResponse.finish(); michael@0: do_print("Interruptible request finished."); michael@0: }).then(null, Cu.reportError); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Ensure the given date object is valid. michael@0: * michael@0: * @param aDate michael@0: * The date object to be checked. This value can be null. michael@0: */ michael@0: function isValidDate(aDate) { michael@0: return aDate && aDate.getTime && !isNaN(aDate.getTime()); michael@0: } michael@0: michael@0: /** michael@0: * Position of the first byte served by the "interruptible_resumable.txt" michael@0: * handler during the most recent response. michael@0: */ michael@0: let gMostRecentFirstBytePos; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Initialization functions common to all tests michael@0: michael@0: add_task(function test_common_initialize() michael@0: { michael@0: // Start the HTTP server. michael@0: gHttpServer = new HttpServer(); michael@0: gHttpServer.registerDirectory("/", do_get_file("../data")); michael@0: gHttpServer.start(-1); michael@0: michael@0: // Cache locks might prevent concurrent requests to the same resource, and michael@0: // this may block tests that use the interruptible handlers. michael@0: Services.prefs.setBoolPref("browser.cache.disk.enable", false); michael@0: Services.prefs.setBoolPref("browser.cache.memory.enable", false); michael@0: do_register_cleanup(function () { michael@0: Services.prefs.clearUserPref("browser.cache.disk.enable"); michael@0: Services.prefs.clearUserPref("browser.cache.memory.enable"); michael@0: }); michael@0: michael@0: registerInterruptibleHandler("/interruptible.txt", michael@0: function firstPart(aRequest, aResponse) { michael@0: aResponse.setHeader("Content-Type", "text/plain", false); michael@0: aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2), michael@0: false); michael@0: aResponse.write(TEST_DATA_SHORT); michael@0: }, function secondPart(aRequest, aResponse) { michael@0: aResponse.write(TEST_DATA_SHORT); michael@0: }); michael@0: michael@0: registerInterruptibleHandler("/interruptible_resumable.txt", michael@0: function firstPart(aRequest, aResponse) { michael@0: aResponse.setHeader("Content-Type", "text/plain", false); michael@0: michael@0: // Determine if only part of the data should be sent. michael@0: let data = TEST_DATA_SHORT + TEST_DATA_SHORT; michael@0: if (aRequest.hasHeader("Range")) { michael@0: var matches = aRequest.getHeader("Range") michael@0: .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); michael@0: var firstBytePos = (matches[1] === undefined) ? 0 : matches[1]; michael@0: var lastBytePos = (matches[2] === undefined) ? data.length - 1 michael@0: : matches[2]; michael@0: if (firstBytePos >= data.length) { michael@0: aResponse.setStatusLine(aRequest.httpVersion, 416, michael@0: "Requested Range Not Satisfiable"); michael@0: aResponse.setHeader("Content-Range", "*/" + data.length, false); michael@0: aResponse.finish(); michael@0: return; michael@0: } michael@0: michael@0: aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content"); michael@0: aResponse.setHeader("Content-Range", firstBytePos + "-" + michael@0: lastBytePos + "/" + michael@0: data.length, false); michael@0: michael@0: data = data.substring(firstBytePos, lastBytePos + 1); michael@0: michael@0: gMostRecentFirstBytePos = firstBytePos; michael@0: } else { michael@0: gMostRecentFirstBytePos = 0; michael@0: } michael@0: michael@0: aResponse.setHeader("Content-Length", "" + data.length, false); michael@0: michael@0: aResponse.write(data.substring(0, data.length / 2)); michael@0: michael@0: // Store the second part of the data on the response object, so that it michael@0: // can be used by the secondPart function. michael@0: aResponse.secondPartData = data.substring(data.length / 2); michael@0: }, function secondPart(aRequest, aResponse) { michael@0: aResponse.write(aResponse.secondPartData); michael@0: }); michael@0: michael@0: registerInterruptibleHandler("/interruptible_gzip.txt", michael@0: function firstPart(aRequest, aResponse) { michael@0: aResponse.setHeader("Content-Type", "text/plain", false); michael@0: aResponse.setHeader("Content-Encoding", "gzip", false); michael@0: aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length); michael@0: michael@0: let bos = new BinaryOutputStream(aResponse.bodyOutputStream); michael@0: bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST, michael@0: TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length); michael@0: }, function secondPart(aRequest, aResponse) { michael@0: let bos = new BinaryOutputStream(aResponse.bodyOutputStream); michael@0: bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND, michael@0: TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length); michael@0: }); michael@0: michael@0: // This URL will emulate being blocked by Windows Parental controls michael@0: gHttpServer.registerPathHandler("/parentalblocked.zip", michael@0: function (aRequest, aResponse) { michael@0: aResponse.setStatusLine(aRequest.httpVersion, 450, michael@0: "Blocked by Windows Parental Controls"); michael@0: }); michael@0: michael@0: // Disable integration with the host application requiring profile access. michael@0: DownloadIntegration.dontLoadList = true; michael@0: DownloadIntegration.dontLoadObservers = true; michael@0: // Disable the parental controls checking. michael@0: DownloadIntegration.dontCheckParentalControls = true; michael@0: // Disable application reputation checks. michael@0: DownloadIntegration.dontCheckApplicationReputation = true; michael@0: // Disable the calls to the OS to launch files and open containing folders michael@0: DownloadIntegration.dontOpenFileAndFolder = true; michael@0: DownloadIntegration._deferTestOpenFile = Promise.defer(); michael@0: DownloadIntegration._deferTestShowDir = Promise.defer(); michael@0: michael@0: // Get a reference to nsIComponentRegistrar, and ensure that is is freed michael@0: // before the XPCOM shutdown. michael@0: let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); michael@0: do_register_cleanup(() => registrar = null); michael@0: michael@0: // Make sure that downloads started using nsIExternalHelperAppService are michael@0: // saved to disk without asking for a destination interactively. michael@0: let mockFactory = { michael@0: createInstance: function (aOuter, aIid) { michael@0: return { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), michael@0: promptForSaveToFile: function (aLauncher, aWindowContext, michael@0: aDefaultFileName, michael@0: aSuggestedFileExtension, michael@0: aForcePrompt) michael@0: { michael@0: throw new Components.Exception( michael@0: "Synchronous promptForSaveToFile not implemented.", michael@0: Cr.NS_ERROR_NOT_AVAILABLE); michael@0: }, michael@0: promptForSaveToFileAsync: function (aLauncher, aWindowContext, michael@0: aDefaultFileName, michael@0: aSuggestedFileExtension, michael@0: aForcePrompt) michael@0: { michael@0: // The dialog should create the empty placeholder file. michael@0: let file = getTempFile(TEST_TARGET_FILE_NAME); michael@0: file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); michael@0: aLauncher.saveDestinationAvailable(file); michael@0: }, michael@0: }.QueryInterface(aIid); michael@0: } michael@0: }; michael@0: michael@0: let contractID = "@mozilla.org/helperapplauncherdialog;1"; michael@0: let cid = registrar.contractIDToCID(contractID); michael@0: let oldFactory = Components.manager.getClassObject(Cc[contractID], michael@0: Ci.nsIFactory); michael@0: michael@0: registrar.unregisterFactory(cid, oldFactory); michael@0: registrar.registerFactory(cid, "", contractID, mockFactory); michael@0: do_register_cleanup(function () { michael@0: registrar.unregisterFactory(cid, mockFactory); michael@0: registrar.registerFactory(cid, "", contractID, oldFactory); michael@0: }); michael@0: michael@0: // We must also make sure that nsIExternalHelperAppService uses the michael@0: // JavaScript implementation of nsITransfer, because the michael@0: // "@mozilla.org/transfer;1" contract is currently implemented in michael@0: // "toolkit/components/downloads". When the other folder is not included in michael@0: // builds anymore (bug 851471), we'll not need to do this anymore. michael@0: let transferContractID = "@mozilla.org/transfer;1"; michael@0: let transferNewCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"); michael@0: let transferCid = registrar.contractIDToCID(transferContractID); michael@0: michael@0: registrar.registerFactory(transferNewCid, "", transferContractID, null); michael@0: do_register_cleanup(function () { michael@0: registrar.registerFactory(transferCid, "", transferContractID, null); michael@0: }); michael@0: });