|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 * http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 Cu.import("resource://gre/modules/Log.jsm"); |
|
5 Cu.import("resource://services-common/observers.js"); |
|
6 Cu.import("resource://services-sync/identity.js"); |
|
7 Cu.import("resource://services-sync/resource.js"); |
|
8 Cu.import("resource://services-sync/util.js"); |
|
9 |
|
10 let logger; |
|
11 |
|
12 let fetched = false; |
|
13 function server_open(metadata, response) { |
|
14 let body; |
|
15 if (metadata.method == "GET") { |
|
16 fetched = true; |
|
17 body = "This path exists"; |
|
18 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
19 } else { |
|
20 body = "Wrong request method"; |
|
21 response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); |
|
22 } |
|
23 response.bodyOutputStream.write(body, body.length); |
|
24 } |
|
25 |
|
26 function server_protected(metadata, response) { |
|
27 let body; |
|
28 |
|
29 if (basic_auth_matches(metadata, "guest", "guest")) { |
|
30 body = "This path exists and is protected"; |
|
31 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); |
|
32 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); |
|
33 } else { |
|
34 body = "This path exists and is protected - failed"; |
|
35 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); |
|
36 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); |
|
37 } |
|
38 |
|
39 response.bodyOutputStream.write(body, body.length); |
|
40 } |
|
41 |
|
42 function server_404(metadata, response) { |
|
43 let body = "File not found"; |
|
44 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); |
|
45 response.bodyOutputStream.write(body, body.length); |
|
46 } |
|
47 |
|
48 let pacFetched = false; |
|
49 function server_pac(metadata, response) { |
|
50 pacFetched = true; |
|
51 let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }'; |
|
52 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
53 response.setHeader("Content-Type", "application/x-ns-proxy-autoconfig", false); |
|
54 response.bodyOutputStream.write(body, body.length); |
|
55 } |
|
56 |
|
57 |
|
58 let sample_data = { |
|
59 some: "sample_data", |
|
60 injson: "format", |
|
61 number: 42 |
|
62 }; |
|
63 |
|
64 function server_upload(metadata, response) { |
|
65 let body; |
|
66 |
|
67 let input = readBytesFromInputStream(metadata.bodyInputStream); |
|
68 if (input == JSON.stringify(sample_data)) { |
|
69 body = "Valid data upload via " + metadata.method; |
|
70 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
71 } else { |
|
72 body = "Invalid data upload via " + metadata.method + ': ' + input; |
|
73 response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error"); |
|
74 } |
|
75 |
|
76 response.bodyOutputStream.write(body, body.length); |
|
77 } |
|
78 |
|
79 function server_delete(metadata, response) { |
|
80 let body; |
|
81 if (metadata.method == "DELETE") { |
|
82 body = "This resource has been deleted"; |
|
83 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
84 } else { |
|
85 body = "Wrong request method"; |
|
86 response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); |
|
87 } |
|
88 response.bodyOutputStream.write(body, body.length); |
|
89 } |
|
90 |
|
91 function server_json(metadata, response) { |
|
92 let body = JSON.stringify(sample_data); |
|
93 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
94 response.bodyOutputStream.write(body, body.length); |
|
95 } |
|
96 |
|
97 const TIMESTAMP = 1274380461; |
|
98 |
|
99 function server_timestamp(metadata, response) { |
|
100 let body = "Thank you for your request"; |
|
101 response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false); |
|
102 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
103 response.bodyOutputStream.write(body, body.length); |
|
104 } |
|
105 |
|
106 function server_backoff(metadata, response) { |
|
107 let body = "Hey, back off!"; |
|
108 response.setHeader("X-Weave-Backoff", '600', false); |
|
109 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
110 response.bodyOutputStream.write(body, body.length); |
|
111 } |
|
112 |
|
113 function server_quota_notice(request, response) { |
|
114 let body = "You're approaching quota."; |
|
115 response.setHeader("X-Weave-Quota-Remaining", '1048576', false); |
|
116 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
117 response.bodyOutputStream.write(body, body.length); |
|
118 } |
|
119 |
|
120 function server_quota_error(request, response) { |
|
121 let body = "14"; |
|
122 response.setHeader("X-Weave-Quota-Remaining", '-1024', false); |
|
123 response.setStatusLine(request.httpVersion, 400, "OK"); |
|
124 response.bodyOutputStream.write(body, body.length); |
|
125 } |
|
126 |
|
127 function server_headers(metadata, response) { |
|
128 let ignore_headers = ["host", "user-agent", "accept", "accept-language", |
|
129 "accept-encoding", "accept-charset", "keep-alive", |
|
130 "connection", "pragma", "cache-control", |
|
131 "content-length"]; |
|
132 let headers = metadata.headers; |
|
133 let header_names = []; |
|
134 while (headers.hasMoreElements()) { |
|
135 let header = headers.getNext().toString(); |
|
136 if (ignore_headers.indexOf(header) == -1) { |
|
137 header_names.push(header); |
|
138 } |
|
139 } |
|
140 header_names = header_names.sort(); |
|
141 |
|
142 headers = {}; |
|
143 for each (let header in header_names) { |
|
144 headers[header] = metadata.getHeader(header); |
|
145 } |
|
146 let body = JSON.stringify(headers); |
|
147 response.setStatusLine(metadata.httpVersion, 200, "OK"); |
|
148 response.bodyOutputStream.write(body, body.length); |
|
149 } |
|
150 |
|
151 function run_test() { |
|
152 initTestLogging("Trace"); |
|
153 |
|
154 do_test_pending(); |
|
155 |
|
156 logger = Log.repository.getLogger('Test'); |
|
157 Log.repository.rootLogger.addAppender(new Log.DumpAppender()); |
|
158 |
|
159 let server = httpd_setup({ |
|
160 "/open": server_open, |
|
161 "/protected": server_protected, |
|
162 "/404": server_404, |
|
163 "/upload": server_upload, |
|
164 "/delete": server_delete, |
|
165 "/json": server_json, |
|
166 "/timestamp": server_timestamp, |
|
167 "/headers": server_headers, |
|
168 "/backoff": server_backoff, |
|
169 "/pac1": server_pac, |
|
170 "/quota-notice": server_quota_notice, |
|
171 "/quota-error": server_quota_error |
|
172 }); |
|
173 |
|
174 Svc.Prefs.set("network.numRetries", 1); // speed up test |
|
175 |
|
176 // This apparently has to come first in order for our PAC URL to be hit. |
|
177 // Don't put any other HTTP requests earlier in the file! |
|
178 _("Testing handling of proxy auth redirection."); |
|
179 PACSystemSettings.PACURI = server.baseURI + "/pac1"; |
|
180 installFakePAC(); |
|
181 let proxiedRes = new Resource(server.baseURI + "/open"); |
|
182 let content = proxiedRes.get(); |
|
183 do_check_true(pacFetched); |
|
184 do_check_true(fetched); |
|
185 do_check_eq(content, "This path exists"); |
|
186 pacFetched = fetched = false; |
|
187 uninstallFakePAC(); |
|
188 |
|
189 _("Resource object members"); |
|
190 let res = new Resource(server.baseURI + "/open"); |
|
191 do_check_true(res.uri instanceof Ci.nsIURI); |
|
192 do_check_eq(res.uri.spec, server.baseURI + "/open"); |
|
193 do_check_eq(res.spec, server.baseURI + "/open"); |
|
194 do_check_eq(typeof res.headers, "object"); |
|
195 do_check_eq(typeof res.authenticator, "object"); |
|
196 // Initially res.data is null since we haven't performed a GET or |
|
197 // PUT/POST request yet. |
|
198 do_check_eq(res.data, null); |
|
199 |
|
200 _("GET a non-password-protected resource"); |
|
201 content = res.get(); |
|
202 do_check_eq(content, "This path exists"); |
|
203 do_check_eq(content.status, 200); |
|
204 do_check_true(content.success); |
|
205 // res.data has been updated with the result from the request |
|
206 do_check_eq(res.data, content); |
|
207 |
|
208 // Observe logging messages. |
|
209 let logger = res._log; |
|
210 let dbg = logger.debug; |
|
211 let debugMessages = []; |
|
212 logger.debug = function (msg) { |
|
213 debugMessages.push(msg); |
|
214 dbg.call(this, msg); |
|
215 } |
|
216 |
|
217 // Since we didn't receive proper JSON data, accessing content.obj |
|
218 // will result in a SyntaxError from JSON.parse. |
|
219 // Furthermore, we'll have logged. |
|
220 let didThrow = false; |
|
221 try { |
|
222 content.obj; |
|
223 } catch (ex) { |
|
224 didThrow = true; |
|
225 } |
|
226 do_check_true(didThrow); |
|
227 do_check_eq(debugMessages.length, 1); |
|
228 do_check_eq(debugMessages[0], |
|
229 "Parse fail: Response body starts: \"\"This path exists\"\"."); |
|
230 logger.debug = dbg; |
|
231 |
|
232 _("Test that the BasicAuthenticator doesn't screw up header case."); |
|
233 let res1 = new Resource(server.baseURI + "/foo"); |
|
234 res1.setHeader("Authorization", "Basic foobar"); |
|
235 do_check_eq(res1.headers["authorization"], "Basic foobar"); |
|
236 |
|
237 _("GET a password protected resource (test that it'll fail w/o pass, no throw)"); |
|
238 let res2 = new Resource(server.baseURI + "/protected"); |
|
239 content = res2.get(); |
|
240 do_check_eq(content, "This path exists and is protected - failed"); |
|
241 do_check_eq(content.status, 401); |
|
242 do_check_false(content.success); |
|
243 |
|
244 _("GET a password protected resource"); |
|
245 let res3 = new Resource(server.baseURI + "/protected"); |
|
246 let identity = new IdentityManager(); |
|
247 let auth = identity.getBasicResourceAuthenticator("guest", "guest"); |
|
248 res3.authenticator = auth; |
|
249 do_check_eq(res3.authenticator, auth); |
|
250 content = res3.get(); |
|
251 do_check_eq(content, "This path exists and is protected"); |
|
252 do_check_eq(content.status, 200); |
|
253 do_check_true(content.success); |
|
254 |
|
255 _("GET a non-existent resource (test that it'll fail, but not throw)"); |
|
256 let res4 = new Resource(server.baseURI + "/404"); |
|
257 content = res4.get(); |
|
258 do_check_eq(content, "File not found"); |
|
259 do_check_eq(content.status, 404); |
|
260 do_check_false(content.success); |
|
261 |
|
262 // Check some headers of the 404 response |
|
263 do_check_eq(content.headers.connection, "close"); |
|
264 do_check_eq(content.headers.server, "httpd.js"); |
|
265 do_check_eq(content.headers["content-length"], 14); |
|
266 |
|
267 _("PUT to a resource (string)"); |
|
268 let res5 = new Resource(server.baseURI + "/upload"); |
|
269 content = res5.put(JSON.stringify(sample_data)); |
|
270 do_check_eq(content, "Valid data upload via PUT"); |
|
271 do_check_eq(content.status, 200); |
|
272 do_check_eq(res5.data, content); |
|
273 |
|
274 _("PUT to a resource (object)"); |
|
275 content = res5.put(sample_data); |
|
276 do_check_eq(content, "Valid data upload via PUT"); |
|
277 do_check_eq(content.status, 200); |
|
278 do_check_eq(res5.data, content); |
|
279 |
|
280 _("PUT without data arg (uses resource.data) (string)"); |
|
281 res5.data = JSON.stringify(sample_data); |
|
282 content = res5.put(); |
|
283 do_check_eq(content, "Valid data upload via PUT"); |
|
284 do_check_eq(content.status, 200); |
|
285 do_check_eq(res5.data, content); |
|
286 |
|
287 _("PUT without data arg (uses resource.data) (object)"); |
|
288 res5.data = sample_data; |
|
289 content = res5.put(); |
|
290 do_check_eq(content, "Valid data upload via PUT"); |
|
291 do_check_eq(content.status, 200); |
|
292 do_check_eq(res5.data, content); |
|
293 |
|
294 _("POST to a resource (string)"); |
|
295 content = res5.post(JSON.stringify(sample_data)); |
|
296 do_check_eq(content, "Valid data upload via POST"); |
|
297 do_check_eq(content.status, 200); |
|
298 do_check_eq(res5.data, content); |
|
299 |
|
300 _("POST to a resource (object)"); |
|
301 content = res5.post(sample_data); |
|
302 do_check_eq(content, "Valid data upload via POST"); |
|
303 do_check_eq(content.status, 200); |
|
304 do_check_eq(res5.data, content); |
|
305 |
|
306 _("POST without data arg (uses resource.data) (string)"); |
|
307 res5.data = JSON.stringify(sample_data); |
|
308 content = res5.post(); |
|
309 do_check_eq(content, "Valid data upload via POST"); |
|
310 do_check_eq(content.status, 200); |
|
311 do_check_eq(res5.data, content); |
|
312 |
|
313 _("POST without data arg (uses resource.data) (object)"); |
|
314 res5.data = sample_data; |
|
315 content = res5.post(); |
|
316 do_check_eq(content, "Valid data upload via POST"); |
|
317 do_check_eq(content.status, 200); |
|
318 do_check_eq(res5.data, content); |
|
319 |
|
320 _("DELETE a resource"); |
|
321 let res6 = new Resource(server.baseURI + "/delete"); |
|
322 content = res6.delete(); |
|
323 do_check_eq(content, "This resource has been deleted") |
|
324 do_check_eq(content.status, 200); |
|
325 |
|
326 _("JSON conversion of response body"); |
|
327 let res7 = new Resource(server.baseURI + "/json"); |
|
328 content = res7.get(); |
|
329 do_check_eq(content, JSON.stringify(sample_data)); |
|
330 do_check_eq(content.status, 200); |
|
331 do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data)); |
|
332 |
|
333 _("X-Weave-Timestamp header updates AsyncResource.serverTime"); |
|
334 // Before having received any response containing the |
|
335 // X-Weave-Timestamp header, AsyncResource.serverTime is null. |
|
336 do_check_eq(AsyncResource.serverTime, null); |
|
337 let res8 = new Resource(server.baseURI + "/timestamp"); |
|
338 content = res8.get(); |
|
339 do_check_eq(AsyncResource.serverTime, TIMESTAMP); |
|
340 |
|
341 _("GET: no special request headers"); |
|
342 let res9 = new Resource(server.baseURI + "/headers"); |
|
343 content = res9.get(); |
|
344 do_check_eq(content, '{}'); |
|
345 |
|
346 _("PUT: Content-Type defaults to text/plain"); |
|
347 content = res9.put('data'); |
|
348 do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); |
|
349 |
|
350 _("POST: Content-Type defaults to text/plain"); |
|
351 content = res9.post('data'); |
|
352 do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); |
|
353 |
|
354 _("setHeader(): setting simple header"); |
|
355 res9.setHeader('X-What-Is-Weave', 'awesome'); |
|
356 do_check_eq(res9.headers['x-what-is-weave'], 'awesome'); |
|
357 content = res9.get(); |
|
358 do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"})); |
|
359 |
|
360 _("setHeader(): setting multiple headers, overwriting existing header"); |
|
361 res9.setHeader('X-WHAT-is-Weave', 'more awesomer'); |
|
362 res9.setHeader('X-Another-Header', 'hello world'); |
|
363 do_check_eq(res9.headers['x-what-is-weave'], 'more awesomer'); |
|
364 do_check_eq(res9.headers['x-another-header'], 'hello world'); |
|
365 content = res9.get(); |
|
366 do_check_eq(content, JSON.stringify({"x-another-header": "hello world", |
|
367 "x-what-is-weave": "more awesomer"})); |
|
368 |
|
369 _("Setting headers object"); |
|
370 res9.headers = {}; |
|
371 content = res9.get(); |
|
372 do_check_eq(content, "{}"); |
|
373 |
|
374 _("PUT/POST: override default Content-Type"); |
|
375 res9.setHeader('Content-Type', 'application/foobar'); |
|
376 do_check_eq(res9.headers['content-type'], 'application/foobar'); |
|
377 content = res9.put('data'); |
|
378 do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); |
|
379 content = res9.post('data'); |
|
380 do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); |
|
381 |
|
382 |
|
383 _("X-Weave-Backoff header notifies observer"); |
|
384 let backoffInterval; |
|
385 function onBackoff(subject, data) { |
|
386 backoffInterval = subject; |
|
387 } |
|
388 Observers.add("weave:service:backoff:interval", onBackoff); |
|
389 |
|
390 let res10 = new Resource(server.baseURI + "/backoff"); |
|
391 content = res10.get(); |
|
392 do_check_eq(backoffInterval, 600); |
|
393 |
|
394 |
|
395 _("X-Weave-Quota-Remaining header notifies observer on successful requests."); |
|
396 let quotaValue; |
|
397 function onQuota(subject, data) { |
|
398 quotaValue = subject; |
|
399 } |
|
400 Observers.add("weave:service:quota:remaining", onQuota); |
|
401 |
|
402 res10 = new Resource(server.baseURI + "/quota-error"); |
|
403 content = res10.get(); |
|
404 do_check_eq(content.status, 400); |
|
405 do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification. |
|
406 |
|
407 res10 = new Resource(server.baseURI + "/quota-notice"); |
|
408 content = res10.get(); |
|
409 do_check_eq(content.status, 200); |
|
410 do_check_eq(quotaValue, 1048576); |
|
411 |
|
412 |
|
413 _("Error handling in _request() preserves exception information"); |
|
414 let error; |
|
415 let res11 = new Resource("http://localhost:12345/does/not/exist"); |
|
416 try { |
|
417 content = res11.get(); |
|
418 } catch(ex) { |
|
419 error = ex; |
|
420 } |
|
421 do_check_eq(error.result, Cr.NS_ERROR_CONNECTION_REFUSED); |
|
422 do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); |
|
423 do_check_eq(typeof error.stack, "string"); |
|
424 |
|
425 _("Checking handling of errors in onProgress."); |
|
426 let res18 = new Resource(server.baseURI + "/json"); |
|
427 let onProgress = function(rec) { |
|
428 // Provoke an XPC exception without a Javascript wrapper. |
|
429 Services.io.newURI("::::::::", null, null); |
|
430 }; |
|
431 res18._onProgress = onProgress; |
|
432 let oldWarn = res18._log.warn; |
|
433 let warnings = []; |
|
434 res18._log.warn = function(msg) { warnings.push(msg) }; |
|
435 error = undefined; |
|
436 try { |
|
437 content = res18.get(); |
|
438 } catch (ex) { |
|
439 error = ex; |
|
440 } |
|
441 |
|
442 // It throws and logs. |
|
443 do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI); |
|
444 do_check_eq(error, "Error: NS_ERROR_MALFORMED_URI"); |
|
445 do_check_eq(warnings.pop(), |
|
446 "Got exception calling onProgress handler during fetch of " + |
|
447 server.baseURI + "/json"); |
|
448 |
|
449 // And this is what happens if JS throws an exception. |
|
450 res18 = new Resource(server.baseURI + "/json"); |
|
451 onProgress = function(rec) { |
|
452 throw "BOO!"; |
|
453 }; |
|
454 res18._onProgress = onProgress; |
|
455 oldWarn = res18._log.warn; |
|
456 warnings = []; |
|
457 res18._log.warn = function(msg) { warnings.push(msg) }; |
|
458 error = undefined; |
|
459 try { |
|
460 content = res18.get(); |
|
461 } catch (ex) { |
|
462 error = ex; |
|
463 } |
|
464 |
|
465 // It throws and logs. |
|
466 do_check_eq(error.result, Cr.NS_ERROR_XPC_JS_THREW_STRING); |
|
467 do_check_eq(error, "Error: NS_ERROR_XPC_JS_THREW_STRING"); |
|
468 do_check_eq(warnings.pop(), |
|
469 "Got exception calling onProgress handler during fetch of " + |
|
470 server.baseURI + "/json"); |
|
471 |
|
472 |
|
473 _("Ensure channel timeouts are thrown appropriately."); |
|
474 let res19 = new Resource(server.baseURI + "/json"); |
|
475 res19.ABORT_TIMEOUT = 0; |
|
476 error = undefined; |
|
477 try { |
|
478 content = res19.get(); |
|
479 } catch (ex) { |
|
480 error = ex; |
|
481 } |
|
482 do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT); |
|
483 |
|
484 _("Testing URI construction."); |
|
485 let args = []; |
|
486 args.push("newer=" + 1234); |
|
487 args.push("limit=" + 1234); |
|
488 args.push("sort=" + 1234); |
|
489 |
|
490 let query = "?" + args.join("&"); |
|
491 |
|
492 let uri1 = Utils.makeURI("http://foo/" + query) |
|
493 .QueryInterface(Ci.nsIURL); |
|
494 let uri2 = Utils.makeURI("http://foo/") |
|
495 .QueryInterface(Ci.nsIURL); |
|
496 uri2.query = query; |
|
497 do_check_eq(uri1.query, uri2.query); |
|
498 server.stop(do_test_finished); |
|
499 } |