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

531 lines
19 KiB
JavaScript

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Provides infrastructure for automated download components tests.
*/
"use strict";
////////////////////////////////////////////////////////////////////////////////
//// Globals
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
"resource://gre/modules/DownloadIntegration.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/commonjs/sdk/core/promise.js");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const ServerSocket = Components.Constructor(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"init");
const BinaryOutputStream = Components.Constructor(
"@mozilla.org/binaryoutputstream;1",
"nsIBinaryOutputStream",
"setOutputStream")
const TEST_TARGET_FILE_NAME = "test-download.txt";
const TEST_STORE_FILE_NAME = "test-downloads.json";
const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
const TEST_DATA_SHORT = "This test string is downloaded.";
// Generate using gzipCompressString in TelemetryPing.js.
const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
31,139,8,0,0,0,0,0,0,3,11,201,200,44,86,40,73,45,46,81,40,46,41,202,204
];
const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
75,87,0,114,83,242,203,243,114,242,19,83,82,83,244,0,151,222,109,43,31,0,0,0
];
const TEST_DATA_SHORT_GZIP_ENCODED =
TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
/**
* All the tests are implemented with add_task, this starts them automatically.
*/
function run_test()
{
do_get_profile();
run_next_test();
}
////////////////////////////////////////////////////////////////////////////////
//// Support functions
/**
* HttpServer object initialized before tests start.
*/
let gHttpServer;
/**
* Given a file name, returns a string containing an URI that points to the file
* on the currently running instance of the test HTTP server.
*/
function httpUrl(aFileName) {
return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
aFileName;
}
// While the previous test file should have deleted all the temporary files it
// used, on Windows these might still be pending deletion on the physical file
// system. Thus, start from a new base number every time, to make a collision
// with a file that is still pending deletion highly unlikely.
let gFileCounter = Math.floor(Math.random() * 1000000);
/**
* Returns a reference to a temporary file, that is guaranteed not to exist, and
* to have never been created before.
*
* @param aLeafName
* Suggested leaf name for the file to be created.
*
* @return nsIFile pointing to a non-existent file in a temporary directory.
*
* @note It is not enough to delete the file if it exists, or to delete the file
* after calling nsIFile.createUnique, because on Windows the delete
* operation in the file system may still be pending, preventing a new
* file with the same name to be created.
*/
function getTempFile(aLeafName)
{
// Prepend a serial number to the extension in the suggested leaf name.
let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
let leafName = base + "-" + gFileCounter + ext;
gFileCounter++;
// Get a file reference under the temporary directory for this test file.
let file = FileUtils.getFile("TmpD", [leafName]);
do_check_false(file.exists());
do_register_cleanup(function () {
if (file.exists()) {
file.remove(false);
}
});
return file;
}
/**
* Waits for pending events to be processed.
*
* @return {Promise}
* @resolves When pending events have been processed.
* @rejects Never.
*/
function promiseExecuteSoon()
{
let deferred = Promise.defer();
do_execute_soon(deferred.resolve);
return deferred.promise;
}
/**
* Waits for a pending events to be processed after a timeout.
*
* @return {Promise}
* @resolves When pending events have been processed.
* @rejects Never.
*/
function promiseTimeout(aTime)
{
let deferred = Promise.defer();
do_timeout(aTime, deferred.resolve);
return deferred.promise;
}
/**
* Creates a new Download object, setting a temporary file as the target.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("source.txt").
*
* @return {Promise}
* @resolves The newly created Download object.
* @rejects JavaScript exception.
*/
function promiseNewDownload(aSourceUrl) {
return Downloads.createDownload({
source: aSourceUrl || httpUrl("source.txt"),
target: getTempFile(TEST_TARGET_FILE_NAME),
});
}
/**
* Starts a new download using the nsIWebBrowserPersist interface, and controls
* it using the legacy nsITransfer interface.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("source.txt").
* @param aOptions
* An optional object used to control the behavior of this function.
* You may pass an object with a subset of the following fields:
* {
* isPrivate: Boolean indicating whether the download originated from a
* private window.
* targetFile: nsIFile for the target, or null to use a temporary file.
* outPersist: Receives a reference to the created nsIWebBrowserPersist
* instance.
* }
*
* @return {Promise}
* @resolves The Download object created as a consequence of controlling the
* download through the legacy nsITransfer interface.
* @rejects Never. The current test fails in case of exceptions.
*/
function promiseStartLegacyDownload(aSourceUrl, aOptions) {
let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
let targetFile = (aOptions && aOptions.targetFile)
|| getTempFile(TEST_TARGET_FILE_NAME);
let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(Ci.nsIWebBrowserPersist);
if (aOptions) {
aOptions.outPersist = persist;
}
// Apply decoding if required by the "Content-Encoding" header.
persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
// We must create the nsITransfer implementation using its class ID because
// the "@mozilla.org/transfer;1" contract is currently implemented in
// "toolkit/components/downloads". When the other folder is not included in
// builds anymore (bug 851471), we'll be able to use the contract ID.
let transfer =
Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"]
.createInstance(Ci.nsITransfer);
let deferred = Promise.defer();
let isPrivate = aOptions && aOptions.isPrivate;
let promise = isPrivate ? Downloads.getPrivateDownloadList()
: Downloads.getPublicDownloadList();
promise.then(function (aList) {
// Temporarily register a view that will get notified when the download we
// are controlling becomes visible in the list of downloads.
aList.addView({
onDownloadAdded: function (aDownload) {
aList.removeView(this);
// Remove the download to keep the list empty for the next test. This
// also allows the caller to register the "onchange" event directly.
aList.remove(aDownload);
// When the download object is ready, make it available to the caller.
deferred.resolve(aDownload);
},
});
// Initialize the components so they reference each other. This will cause
// the Download object to be created and added to the public downloads.
transfer.init(sourceURI, NetUtil.newURI(targetFile), null, null, null, null,
persist, isPrivate);
persist.progressListener = transfer;
// Start the actual download process.
persist.savePrivacyAwareURI(sourceURI, null, null, null, null, targetFile,
isPrivate);
}.bind(this)).then(null, do_report_unexpected_exception);
return deferred.promise;
}
/**
* Returns a new public DownloadList object.
*
* @return {Promise}
* @resolves The newly created DownloadList object.
* @rejects JavaScript exception.
*/
function promiseNewDownloadList() {
// Force the creation of a new public download list.
Downloads._promisePublicDownloadList = null;
return Downloads.getPublicDownloadList();
}
/**
* Returns a new private DownloadList object.
*
* @return {Promise}
* @resolves The newly created DownloadList object.
* @rejects JavaScript exception.
*/
function promiseNewPrivateDownloadList() {
// Force the creation of a new public download list.
Downloads._privateDownloadList = null;
return Downloads.getPrivateDownloadList();
}
/**
* Ensures that the given file contents are equal to the given string.
*
* @param aPath
* String containing the path of the file whose contents should be
* verified.
* @param aExpectedContents
* String containing the octets that are expected in the file.
*
* @return {Promise}
* @resolves When the operation completes.
* @rejects Never.
*/
function promiseVerifyContents(aPath, aExpectedContents)
{
let deferred = Promise.defer();
let file = new FileUtils.File(aPath);
NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
do_check_true(Components.isSuccessCode(aStatus));
let contents = NetUtil.readInputStreamToString(aInputStream,
aInputStream.available());
if (contents.length <= TEST_DATA_SHORT.length * 2) {
do_check_eq(contents, aExpectedContents);
} else {
// Do not print the entire content string to the test log.
do_check_eq(contents.length, aExpectedContents.length);
do_check_true(contents == aExpectedContents);
}
deferred.resolve();
});
return deferred.promise;
}
/**
* Adds entry for download.
*
* @param aSourceUrl
* String containing the URI for the download source, or null to use
* httpUrl("source.txt").
*
* @return {Promise}
* @rejects JavaScript exception.
*/
function promiseAddDownloadToHistory(aSourceUrl) {
let deferred = Promise.defer();
PlacesUtils.asyncHistory.updatePlaces(
{
uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
visits: [{
transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
visitDate: Date.now()
}]
},
{
handleError: function handleError(aResultCode, aPlaceInfo) {
let ex = new Components.Exception("Unexpected error in adding visits.",
aResultCode);
deferred.reject(ex);
},
handleResult: function () {},
handleCompletion: function handleCompletion() {
deferred.resolve();
}
});
return deferred.promise;
}
/**
* Starts a socket listener that closes each incoming connection.
*
* @returns nsIServerSocket that listens for connections. Call its "close"
* method to stop listening and free the server port.
*/
function startFakeServer()
{
let serverSocket = new ServerSocket(-1, true, -1);
serverSocket.asyncListen({
onSocketAccepted: function (aServ, aTransport) {
aTransport.close(Cr.NS_BINDING_ABORTED);
},
onStopListening: function () { },
});
return serverSocket;
}
/**
* This function allows testing events or actions that need to happen in the
* middle of a download.
*
* Normally, the internal HTTP server returns all the available data as soon as
* a request is received. In order for some requests to be served one part at a
* time, special interruptible handlers are registered on the HTTP server.
*
* Before making a request to one of the addresses served by the interruptible
* handlers, you may call "deferNextResponse" to get a reference to an object
* that allows you to control the next request.
*
* For example, the handler accessible at the httpUri("interruptible.txt")
* address returns the TEST_DATA_SHORT text, then waits until the "resolve"
* method is called on the object returned by the function. At this point, the
* handler sends the TEST_DATA_SHORT text again to complete the response.
*
* You can also call the "reject" method on the returned object to interrupt the
* response midway. Because of how the network layer is implemented, this does
* not cause the socket to return an error.
*
* @returns Deferred object used to control the response.
*/
function deferNextResponse()
{
do_print("Interruptible request will be controlled.");
// Store an internal reference that should not be used directly by tests.
if (!deferNextResponse._deferred) {
deferNextResponse._deferred = Promise.defer();
}
return deferNextResponse._deferred;
}
/**
* Returns a promise that is resolved when the next interruptible response
* handler has received the request, and has started sending the first part of
* the response. The response might not have been received by the client yet.
*
* @return {Promise}
* @resolves When the next request has been received.
* @rejects Never.
*/
function promiseNextRequestReceived()
{
do_print("Requested notification when interruptible request is received.");
// Store an internal reference that should not be used directly by tests.
promiseNextRequestReceived._deferred = Promise.defer();
return promiseNextRequestReceived._deferred.promise;
}
/**
* Registers an interruptible response handler.
*
* @param aPath
* Path passed to nsIHttpServer.registerPathHandler.
* @param aFirstPartFn
* This function is called when the response is received, with the
* aRequest and aResponse arguments of the server.
* @param aSecondPartFn
* This function is called after the "resolve" method of the object
* returned by deferNextResponse is called. This function is called with
* the aRequest and aResponse arguments of the server.
*/
function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
{
gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
// Get a reference to the controlling object for this request. If the
// deferNextResponse function was not called, interrupt the test.
let deferResponse = deferNextResponse._deferred;
deferNextResponse._deferred = null;
if (deferResponse) {
do_print("Interruptible request started under control.");
} else {
do_print("Interruptible request started without being controlled.");
deferResponse = Promise.defer();
deferResponse.resolve();
}
// Process the first part of the response.
aResponse.processAsync();
aFirstPartFn(aRequest, aResponse);
if (promiseNextRequestReceived._deferred) {
do_print("Notifying that interruptible request has been received.");
promiseNextRequestReceived._deferred.resolve();
promiseNextRequestReceived._deferred = null;
}
// Wait on the deferred object, then finish or abort the request.
deferResponse.promise.then(function RIH_onSuccess() {
aSecondPartFn(aRequest, aResponse);
aResponse.finish();
do_print("Interruptible request finished.");
}, function RIH_onFailure() {
aResponse.abort();
do_print("Interruptible request aborted.");
});
});
}
/**
* Ensure the given date object is valid.
*
* @param aDate
* The date object to be checked. This value can be null.
*/
function isValidDate(aDate) {
return aDate && aDate.getTime && !isNaN(aDate.getTime());
}
////////////////////////////////////////////////////////////////////////////////
//// Initialization functions common to all tests
add_task(function test_common_initialize()
{
// Start the HTTP server.
gHttpServer = new HttpServer();
gHttpServer.registerDirectory("/", do_get_file("../data"));
gHttpServer.start(-1);
registerInterruptibleHandler("/interruptible.txt",
function firstPart(aRequest, aResponse) {
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
false);
aResponse.write(TEST_DATA_SHORT);
}, function secondPart(aRequest, aResponse) {
aResponse.write(TEST_DATA_SHORT);
});
registerInterruptibleHandler("/empty-noprogress.txt",
function firstPart(aRequest, aResponse) {
aResponse.setHeader("Content-Type", "text/plain", false);
}, function secondPart(aRequest, aResponse) { });
registerInterruptibleHandler("/interruptible_gzip.txt",
function firstPart(aRequest, aResponse) {
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Encoding", "gzip", false);
aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
}, function secondPart(aRequest, aResponse) {
let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
});
// Disable integration with the host application requiring profile access.
DownloadIntegration.dontLoad = true;
// Disable the parental controls checking.
DownloadIntegration.dontCheckParentalControls = true;
});