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

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

mercurial