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