1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_resource.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,499 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +Cu.import("resource://gre/modules/Log.jsm"); 1.8 +Cu.import("resource://services-common/observers.js"); 1.9 +Cu.import("resource://services-sync/identity.js"); 1.10 +Cu.import("resource://services-sync/resource.js"); 1.11 +Cu.import("resource://services-sync/util.js"); 1.12 + 1.13 +let logger; 1.14 + 1.15 +let fetched = false; 1.16 +function server_open(metadata, response) { 1.17 + let body; 1.18 + if (metadata.method == "GET") { 1.19 + fetched = true; 1.20 + body = "This path exists"; 1.21 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.22 + } else { 1.23 + body = "Wrong request method"; 1.24 + response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); 1.25 + } 1.26 + response.bodyOutputStream.write(body, body.length); 1.27 +} 1.28 + 1.29 +function server_protected(metadata, response) { 1.30 + let body; 1.31 + 1.32 + if (basic_auth_matches(metadata, "guest", "guest")) { 1.33 + body = "This path exists and is protected"; 1.34 + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); 1.35 + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); 1.36 + } else { 1.37 + body = "This path exists and is protected - failed"; 1.38 + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); 1.39 + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); 1.40 + } 1.41 + 1.42 + response.bodyOutputStream.write(body, body.length); 1.43 +} 1.44 + 1.45 +function server_404(metadata, response) { 1.46 + let body = "File not found"; 1.47 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.48 + response.bodyOutputStream.write(body, body.length); 1.49 +} 1.50 + 1.51 +let pacFetched = false; 1.52 +function server_pac(metadata, response) { 1.53 + pacFetched = true; 1.54 + let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }'; 1.55 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.56 + response.setHeader("Content-Type", "application/x-ns-proxy-autoconfig", false); 1.57 + response.bodyOutputStream.write(body, body.length); 1.58 +} 1.59 + 1.60 + 1.61 +let sample_data = { 1.62 + some: "sample_data", 1.63 + injson: "format", 1.64 + number: 42 1.65 +}; 1.66 + 1.67 +function server_upload(metadata, response) { 1.68 + let body; 1.69 + 1.70 + let input = readBytesFromInputStream(metadata.bodyInputStream); 1.71 + if (input == JSON.stringify(sample_data)) { 1.72 + body = "Valid data upload via " + metadata.method; 1.73 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.74 + } else { 1.75 + body = "Invalid data upload via " + metadata.method + ': ' + input; 1.76 + response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error"); 1.77 + } 1.78 + 1.79 + response.bodyOutputStream.write(body, body.length); 1.80 +} 1.81 + 1.82 +function server_delete(metadata, response) { 1.83 + let body; 1.84 + if (metadata.method == "DELETE") { 1.85 + body = "This resource has been deleted"; 1.86 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.87 + } else { 1.88 + body = "Wrong request method"; 1.89 + response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); 1.90 + } 1.91 + response.bodyOutputStream.write(body, body.length); 1.92 +} 1.93 + 1.94 +function server_json(metadata, response) { 1.95 + let body = JSON.stringify(sample_data); 1.96 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.97 + response.bodyOutputStream.write(body, body.length); 1.98 +} 1.99 + 1.100 +const TIMESTAMP = 1274380461; 1.101 + 1.102 +function server_timestamp(metadata, response) { 1.103 + let body = "Thank you for your request"; 1.104 + response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false); 1.105 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.106 + response.bodyOutputStream.write(body, body.length); 1.107 +} 1.108 + 1.109 +function server_backoff(metadata, response) { 1.110 + let body = "Hey, back off!"; 1.111 + response.setHeader("X-Weave-Backoff", '600', false); 1.112 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.113 + response.bodyOutputStream.write(body, body.length); 1.114 +} 1.115 + 1.116 +function server_quota_notice(request, response) { 1.117 + let body = "You're approaching quota."; 1.118 + response.setHeader("X-Weave-Quota-Remaining", '1048576', false); 1.119 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.120 + response.bodyOutputStream.write(body, body.length); 1.121 +} 1.122 + 1.123 +function server_quota_error(request, response) { 1.124 + let body = "14"; 1.125 + response.setHeader("X-Weave-Quota-Remaining", '-1024', false); 1.126 + response.setStatusLine(request.httpVersion, 400, "OK"); 1.127 + response.bodyOutputStream.write(body, body.length); 1.128 +} 1.129 + 1.130 +function server_headers(metadata, response) { 1.131 + let ignore_headers = ["host", "user-agent", "accept", "accept-language", 1.132 + "accept-encoding", "accept-charset", "keep-alive", 1.133 + "connection", "pragma", "cache-control", 1.134 + "content-length"]; 1.135 + let headers = metadata.headers; 1.136 + let header_names = []; 1.137 + while (headers.hasMoreElements()) { 1.138 + let header = headers.getNext().toString(); 1.139 + if (ignore_headers.indexOf(header) == -1) { 1.140 + header_names.push(header); 1.141 + } 1.142 + } 1.143 + header_names = header_names.sort(); 1.144 + 1.145 + headers = {}; 1.146 + for each (let header in header_names) { 1.147 + headers[header] = metadata.getHeader(header); 1.148 + } 1.149 + let body = JSON.stringify(headers); 1.150 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.151 + response.bodyOutputStream.write(body, body.length); 1.152 +} 1.153 + 1.154 +function run_test() { 1.155 + initTestLogging("Trace"); 1.156 + 1.157 + do_test_pending(); 1.158 + 1.159 + logger = Log.repository.getLogger('Test'); 1.160 + Log.repository.rootLogger.addAppender(new Log.DumpAppender()); 1.161 + 1.162 + let server = httpd_setup({ 1.163 + "/open": server_open, 1.164 + "/protected": server_protected, 1.165 + "/404": server_404, 1.166 + "/upload": server_upload, 1.167 + "/delete": server_delete, 1.168 + "/json": server_json, 1.169 + "/timestamp": server_timestamp, 1.170 + "/headers": server_headers, 1.171 + "/backoff": server_backoff, 1.172 + "/pac1": server_pac, 1.173 + "/quota-notice": server_quota_notice, 1.174 + "/quota-error": server_quota_error 1.175 + }); 1.176 + 1.177 + Svc.Prefs.set("network.numRetries", 1); // speed up test 1.178 + 1.179 + // This apparently has to come first in order for our PAC URL to be hit. 1.180 + // Don't put any other HTTP requests earlier in the file! 1.181 + _("Testing handling of proxy auth redirection."); 1.182 + PACSystemSettings.PACURI = server.baseURI + "/pac1"; 1.183 + installFakePAC(); 1.184 + let proxiedRes = new Resource(server.baseURI + "/open"); 1.185 + let content = proxiedRes.get(); 1.186 + do_check_true(pacFetched); 1.187 + do_check_true(fetched); 1.188 + do_check_eq(content, "This path exists"); 1.189 + pacFetched = fetched = false; 1.190 + uninstallFakePAC(); 1.191 + 1.192 + _("Resource object members"); 1.193 + let res = new Resource(server.baseURI + "/open"); 1.194 + do_check_true(res.uri instanceof Ci.nsIURI); 1.195 + do_check_eq(res.uri.spec, server.baseURI + "/open"); 1.196 + do_check_eq(res.spec, server.baseURI + "/open"); 1.197 + do_check_eq(typeof res.headers, "object"); 1.198 + do_check_eq(typeof res.authenticator, "object"); 1.199 + // Initially res.data is null since we haven't performed a GET or 1.200 + // PUT/POST request yet. 1.201 + do_check_eq(res.data, null); 1.202 + 1.203 + _("GET a non-password-protected resource"); 1.204 + content = res.get(); 1.205 + do_check_eq(content, "This path exists"); 1.206 + do_check_eq(content.status, 200); 1.207 + do_check_true(content.success); 1.208 + // res.data has been updated with the result from the request 1.209 + do_check_eq(res.data, content); 1.210 + 1.211 + // Observe logging messages. 1.212 + let logger = res._log; 1.213 + let dbg = logger.debug; 1.214 + let debugMessages = []; 1.215 + logger.debug = function (msg) { 1.216 + debugMessages.push(msg); 1.217 + dbg.call(this, msg); 1.218 + } 1.219 + 1.220 + // Since we didn't receive proper JSON data, accessing content.obj 1.221 + // will result in a SyntaxError from JSON.parse. 1.222 + // Furthermore, we'll have logged. 1.223 + let didThrow = false; 1.224 + try { 1.225 + content.obj; 1.226 + } catch (ex) { 1.227 + didThrow = true; 1.228 + } 1.229 + do_check_true(didThrow); 1.230 + do_check_eq(debugMessages.length, 1); 1.231 + do_check_eq(debugMessages[0], 1.232 + "Parse fail: Response body starts: \"\"This path exists\"\"."); 1.233 + logger.debug = dbg; 1.234 + 1.235 + _("Test that the BasicAuthenticator doesn't screw up header case."); 1.236 + let res1 = new Resource(server.baseURI + "/foo"); 1.237 + res1.setHeader("Authorization", "Basic foobar"); 1.238 + do_check_eq(res1.headers["authorization"], "Basic foobar"); 1.239 + 1.240 + _("GET a password protected resource (test that it'll fail w/o pass, no throw)"); 1.241 + let res2 = new Resource(server.baseURI + "/protected"); 1.242 + content = res2.get(); 1.243 + do_check_eq(content, "This path exists and is protected - failed"); 1.244 + do_check_eq(content.status, 401); 1.245 + do_check_false(content.success); 1.246 + 1.247 + _("GET a password protected resource"); 1.248 + let res3 = new Resource(server.baseURI + "/protected"); 1.249 + let identity = new IdentityManager(); 1.250 + let auth = identity.getBasicResourceAuthenticator("guest", "guest"); 1.251 + res3.authenticator = auth; 1.252 + do_check_eq(res3.authenticator, auth); 1.253 + content = res3.get(); 1.254 + do_check_eq(content, "This path exists and is protected"); 1.255 + do_check_eq(content.status, 200); 1.256 + do_check_true(content.success); 1.257 + 1.258 + _("GET a non-existent resource (test that it'll fail, but not throw)"); 1.259 + let res4 = new Resource(server.baseURI + "/404"); 1.260 + content = res4.get(); 1.261 + do_check_eq(content, "File not found"); 1.262 + do_check_eq(content.status, 404); 1.263 + do_check_false(content.success); 1.264 + 1.265 + // Check some headers of the 404 response 1.266 + do_check_eq(content.headers.connection, "close"); 1.267 + do_check_eq(content.headers.server, "httpd.js"); 1.268 + do_check_eq(content.headers["content-length"], 14); 1.269 + 1.270 + _("PUT to a resource (string)"); 1.271 + let res5 = new Resource(server.baseURI + "/upload"); 1.272 + content = res5.put(JSON.stringify(sample_data)); 1.273 + do_check_eq(content, "Valid data upload via PUT"); 1.274 + do_check_eq(content.status, 200); 1.275 + do_check_eq(res5.data, content); 1.276 + 1.277 + _("PUT to a resource (object)"); 1.278 + content = res5.put(sample_data); 1.279 + do_check_eq(content, "Valid data upload via PUT"); 1.280 + do_check_eq(content.status, 200); 1.281 + do_check_eq(res5.data, content); 1.282 + 1.283 + _("PUT without data arg (uses resource.data) (string)"); 1.284 + res5.data = JSON.stringify(sample_data); 1.285 + content = res5.put(); 1.286 + do_check_eq(content, "Valid data upload via PUT"); 1.287 + do_check_eq(content.status, 200); 1.288 + do_check_eq(res5.data, content); 1.289 + 1.290 + _("PUT without data arg (uses resource.data) (object)"); 1.291 + res5.data = sample_data; 1.292 + content = res5.put(); 1.293 + do_check_eq(content, "Valid data upload via PUT"); 1.294 + do_check_eq(content.status, 200); 1.295 + do_check_eq(res5.data, content); 1.296 + 1.297 + _("POST to a resource (string)"); 1.298 + content = res5.post(JSON.stringify(sample_data)); 1.299 + do_check_eq(content, "Valid data upload via POST"); 1.300 + do_check_eq(content.status, 200); 1.301 + do_check_eq(res5.data, content); 1.302 + 1.303 + _("POST to a resource (object)"); 1.304 + content = res5.post(sample_data); 1.305 + do_check_eq(content, "Valid data upload via POST"); 1.306 + do_check_eq(content.status, 200); 1.307 + do_check_eq(res5.data, content); 1.308 + 1.309 + _("POST without data arg (uses resource.data) (string)"); 1.310 + res5.data = JSON.stringify(sample_data); 1.311 + content = res5.post(); 1.312 + do_check_eq(content, "Valid data upload via POST"); 1.313 + do_check_eq(content.status, 200); 1.314 + do_check_eq(res5.data, content); 1.315 + 1.316 + _("POST without data arg (uses resource.data) (object)"); 1.317 + res5.data = sample_data; 1.318 + content = res5.post(); 1.319 + do_check_eq(content, "Valid data upload via POST"); 1.320 + do_check_eq(content.status, 200); 1.321 + do_check_eq(res5.data, content); 1.322 + 1.323 + _("DELETE a resource"); 1.324 + let res6 = new Resource(server.baseURI + "/delete"); 1.325 + content = res6.delete(); 1.326 + do_check_eq(content, "This resource has been deleted") 1.327 + do_check_eq(content.status, 200); 1.328 + 1.329 + _("JSON conversion of response body"); 1.330 + let res7 = new Resource(server.baseURI + "/json"); 1.331 + content = res7.get(); 1.332 + do_check_eq(content, JSON.stringify(sample_data)); 1.333 + do_check_eq(content.status, 200); 1.334 + do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data)); 1.335 + 1.336 + _("X-Weave-Timestamp header updates AsyncResource.serverTime"); 1.337 + // Before having received any response containing the 1.338 + // X-Weave-Timestamp header, AsyncResource.serverTime is null. 1.339 + do_check_eq(AsyncResource.serverTime, null); 1.340 + let res8 = new Resource(server.baseURI + "/timestamp"); 1.341 + content = res8.get(); 1.342 + do_check_eq(AsyncResource.serverTime, TIMESTAMP); 1.343 + 1.344 + _("GET: no special request headers"); 1.345 + let res9 = new Resource(server.baseURI + "/headers"); 1.346 + content = res9.get(); 1.347 + do_check_eq(content, '{}'); 1.348 + 1.349 + _("PUT: Content-Type defaults to text/plain"); 1.350 + content = res9.put('data'); 1.351 + do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); 1.352 + 1.353 + _("POST: Content-Type defaults to text/plain"); 1.354 + content = res9.post('data'); 1.355 + do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); 1.356 + 1.357 + _("setHeader(): setting simple header"); 1.358 + res9.setHeader('X-What-Is-Weave', 'awesome'); 1.359 + do_check_eq(res9.headers['x-what-is-weave'], 'awesome'); 1.360 + content = res9.get(); 1.361 + do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"})); 1.362 + 1.363 + _("setHeader(): setting multiple headers, overwriting existing header"); 1.364 + res9.setHeader('X-WHAT-is-Weave', 'more awesomer'); 1.365 + res9.setHeader('X-Another-Header', 'hello world'); 1.366 + do_check_eq(res9.headers['x-what-is-weave'], 'more awesomer'); 1.367 + do_check_eq(res9.headers['x-another-header'], 'hello world'); 1.368 + content = res9.get(); 1.369 + do_check_eq(content, JSON.stringify({"x-another-header": "hello world", 1.370 + "x-what-is-weave": "more awesomer"})); 1.371 + 1.372 + _("Setting headers object"); 1.373 + res9.headers = {}; 1.374 + content = res9.get(); 1.375 + do_check_eq(content, "{}"); 1.376 + 1.377 + _("PUT/POST: override default Content-Type"); 1.378 + res9.setHeader('Content-Type', 'application/foobar'); 1.379 + do_check_eq(res9.headers['content-type'], 'application/foobar'); 1.380 + content = res9.put('data'); 1.381 + do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); 1.382 + content = res9.post('data'); 1.383 + do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); 1.384 + 1.385 + 1.386 + _("X-Weave-Backoff header notifies observer"); 1.387 + let backoffInterval; 1.388 + function onBackoff(subject, data) { 1.389 + backoffInterval = subject; 1.390 + } 1.391 + Observers.add("weave:service:backoff:interval", onBackoff); 1.392 + 1.393 + let res10 = new Resource(server.baseURI + "/backoff"); 1.394 + content = res10.get(); 1.395 + do_check_eq(backoffInterval, 600); 1.396 + 1.397 + 1.398 + _("X-Weave-Quota-Remaining header notifies observer on successful requests."); 1.399 + let quotaValue; 1.400 + function onQuota(subject, data) { 1.401 + quotaValue = subject; 1.402 + } 1.403 + Observers.add("weave:service:quota:remaining", onQuota); 1.404 + 1.405 + res10 = new Resource(server.baseURI + "/quota-error"); 1.406 + content = res10.get(); 1.407 + do_check_eq(content.status, 400); 1.408 + do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification. 1.409 + 1.410 + res10 = new Resource(server.baseURI + "/quota-notice"); 1.411 + content = res10.get(); 1.412 + do_check_eq(content.status, 200); 1.413 + do_check_eq(quotaValue, 1048576); 1.414 + 1.415 + 1.416 + _("Error handling in _request() preserves exception information"); 1.417 + let error; 1.418 + let res11 = new Resource("http://localhost:12345/does/not/exist"); 1.419 + try { 1.420 + content = res11.get(); 1.421 + } catch(ex) { 1.422 + error = ex; 1.423 + } 1.424 + do_check_eq(error.result, Cr.NS_ERROR_CONNECTION_REFUSED); 1.425 + do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); 1.426 + do_check_eq(typeof error.stack, "string"); 1.427 + 1.428 + _("Checking handling of errors in onProgress."); 1.429 + let res18 = new Resource(server.baseURI + "/json"); 1.430 + let onProgress = function(rec) { 1.431 + // Provoke an XPC exception without a Javascript wrapper. 1.432 + Services.io.newURI("::::::::", null, null); 1.433 + }; 1.434 + res18._onProgress = onProgress; 1.435 + let oldWarn = res18._log.warn; 1.436 + let warnings = []; 1.437 + res18._log.warn = function(msg) { warnings.push(msg) }; 1.438 + error = undefined; 1.439 + try { 1.440 + content = res18.get(); 1.441 + } catch (ex) { 1.442 + error = ex; 1.443 + } 1.444 + 1.445 + // It throws and logs. 1.446 + do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI); 1.447 + do_check_eq(error, "Error: NS_ERROR_MALFORMED_URI"); 1.448 + do_check_eq(warnings.pop(), 1.449 + "Got exception calling onProgress handler during fetch of " + 1.450 + server.baseURI + "/json"); 1.451 + 1.452 + // And this is what happens if JS throws an exception. 1.453 + res18 = new Resource(server.baseURI + "/json"); 1.454 + onProgress = function(rec) { 1.455 + throw "BOO!"; 1.456 + }; 1.457 + res18._onProgress = onProgress; 1.458 + oldWarn = res18._log.warn; 1.459 + warnings = []; 1.460 + res18._log.warn = function(msg) { warnings.push(msg) }; 1.461 + error = undefined; 1.462 + try { 1.463 + content = res18.get(); 1.464 + } catch (ex) { 1.465 + error = ex; 1.466 + } 1.467 + 1.468 + // It throws and logs. 1.469 + do_check_eq(error.result, Cr.NS_ERROR_XPC_JS_THREW_STRING); 1.470 + do_check_eq(error, "Error: NS_ERROR_XPC_JS_THREW_STRING"); 1.471 + do_check_eq(warnings.pop(), 1.472 + "Got exception calling onProgress handler during fetch of " + 1.473 + server.baseURI + "/json"); 1.474 + 1.475 + 1.476 + _("Ensure channel timeouts are thrown appropriately."); 1.477 + let res19 = new Resource(server.baseURI + "/json"); 1.478 + res19.ABORT_TIMEOUT = 0; 1.479 + error = undefined; 1.480 + try { 1.481 + content = res19.get(); 1.482 + } catch (ex) { 1.483 + error = ex; 1.484 + } 1.485 + do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT); 1.486 + 1.487 + _("Testing URI construction."); 1.488 + let args = []; 1.489 + args.push("newer=" + 1234); 1.490 + args.push("limit=" + 1234); 1.491 + args.push("sort=" + 1234); 1.492 + 1.493 + let query = "?" + args.join("&"); 1.494 + 1.495 + let uri1 = Utils.makeURI("http://foo/" + query) 1.496 + .QueryInterface(Ci.nsIURL); 1.497 + let uri2 = Utils.makeURI("http://foo/") 1.498 + .QueryInterface(Ci.nsIURL); 1.499 + uri2.query = query; 1.500 + do_check_eq(uri1.query, uri2.query); 1.501 + server.stop(do_test_finished); 1.502 +}