Cu.import("resource://services-sync/ext/Observers.js"); Cu.import("resource://services-sync/identity.js"); Cu.import("resource://services-sync/log4moz.js"); Cu.import("resource://services-sync/resource.js"); Cu.import("resource://services-sync/util.js"); let logger; function server_open(metadata, response) { let body; if (metadata.method == "GET") { body = "This path exists"; response.setStatusLine(metadata.httpVersion, 200, "OK"); } else { body = "Wrong request method"; response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); } response.bodyOutputStream.write(body, body.length); } function server_protected(metadata, response) { let body; // no btoa() in xpcshell. it's guest:guest if (metadata.hasHeader("Authorization") && metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") { body = "This path exists and is protected"; response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); } else { body = "This path exists and is protected - failed"; response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); } response.bodyOutputStream.write(body, body.length); } function server_404(metadata, response) { let body = "File not found"; response.setStatusLine(metadata.httpVersion, 404, "Not Found"); response.bodyOutputStream.write(body, body.length); } let sample_data = { some: "sample_data", injson: "format", number: 42 }; function server_upload(metadata, response) { let body; let input = readBytesFromInputStream(metadata.bodyInputStream); if (input == JSON.stringify(sample_data)) { body = "Valid data upload via " + metadata.method; response.setStatusLine(metadata.httpVersion, 200, "OK"); } else { body = "Invalid data upload via " + metadata.method + ': ' + input; response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error"); } response.bodyOutputStream.write(body, body.length); } function server_delete(metadata, response) { let body; if (metadata.method == "DELETE") { body = "This resource has been deleted"; response.setStatusLine(metadata.httpVersion, 200, "OK"); } else { body = "Wrong request method"; response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); } response.bodyOutputStream.write(body, body.length); } function server_json(metadata, response) { let body = JSON.stringify(sample_data); response.setStatusLine(metadata.httpVersion, 200, "OK"); response.bodyOutputStream.write(body, body.length); } const TIMESTAMP = 1274380461; function server_timestamp(metadata, response) { let body = "Thank you for your request"; response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false); response.setStatusLine(metadata.httpVersion, 200, "OK"); response.bodyOutputStream.write(body, body.length); } function server_backoff(metadata, response) { let body = "Hey, back off!"; response.setHeader("X-Weave-Backoff", '600', false); response.setStatusLine(metadata.httpVersion, 200, "OK"); response.bodyOutputStream.write(body, body.length); } function server_quota_notice(request, response) { let body = "You're approaching quota."; response.setHeader("X-Weave-Quota-Remaining", '1048576', false); response.setStatusLine(request.httpVersion, 200, "OK"); response.bodyOutputStream.write(body, body.length); } function server_quota_error(request, response) { let body = "14"; response.setHeader("X-Weave-Quota-Remaining", '-1024', false); response.setStatusLine(request.httpVersion, 400, "OK"); response.bodyOutputStream.write(body, body.length); } function server_headers(metadata, response) { let ignore_headers = ["host", "user-agent", "accept", "accept-language", "accept-encoding", "accept-charset", "keep-alive", "connection", "pragma", "cache-control", "content-length"]; let headers = metadata.headers; let header_names = []; while (headers.hasMoreElements()) { let header = headers.getNext().toString(); if (ignore_headers.indexOf(header) == -1) { header_names.push(header); } } header_names = header_names.sort(); headers = {}; for each (let header in header_names) { headers[header] = metadata.getHeader(header); } let body = JSON.stringify(headers); response.setStatusLine(metadata.httpVersion, 200, "OK"); response.bodyOutputStream.write(body, body.length); } /* * Utility to allow us to fake a bad cached response within AsyncResource. * Swap out the _onComplete handler, pretending to throw before setting * status to non-zero. Return an empty response. * * This should prompt Res_get to retry once. * * Set FAKE_ZERO_COUNTER accordingly. */ let FAKE_ZERO_COUNTER = 0; function fake_status_failure() { _("Switching in status-0 _onComplete handler."); let c = AsyncResource.prototype._onComplete; AsyncResource.prototype._onComplete = function(error, data) { if (FAKE_ZERO_COUNTER > 0) { _("Faking status 0 return..."); FAKE_ZERO_COUNTER--; let ret = new String(data); ret.headers = {}; ret.status = 0; ret.success = false; Utils.lazy2(ret, "obj", function() JSON.parse(ret)); this._callback(null, ret); } else { c.apply(this, arguments); } }; } function run_test() { do_test_pending(); logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); let server = httpd_setup({ "/open": server_open, "/protected": server_protected, "/404": server_404, "/upload": server_upload, "/delete": server_delete, "/json": server_json, "/timestamp": server_timestamp, "/headers": server_headers, "/backoff": server_backoff, "/quota-notice": server_quota_notice, "/quota-error": server_quota_error }); Utils.prefs.setIntPref("network.numRetries", 1); // speed up test _("Resource object memebers"); let res = new Resource("http://localhost:8080/open"); do_check_true(res.uri instanceof Ci.nsIURI); do_check_eq(res.uri.spec, "http://localhost:8080/open"); do_check_eq(res.spec, "http://localhost:8080/open"); do_check_eq(typeof res.headers, "object"); do_check_eq(typeof res.authenticator, "object"); // Initially res.data is null since we haven't performed a GET or // PUT/POST request yet. do_check_eq(res.data, null); _("GET a non-password-protected resource"); let content = res.get(); do_check_eq(content, "This path exists"); do_check_eq(content.status, 200); do_check_true(content.success); // res.data has been updated with the result from the request do_check_eq(res.data, content); // Since we didn't receive proper JSON data, accessing content.obj // will result in a SyntaxError from JSON.parse let didThrow = false; try { content.obj; } catch (ex) { didThrow = true; } do_check_true(didThrow); let did401 = false; Observers.add("weave:resource:status:401", function() did401 = true); _("GET a password protected resource (test that it'll fail w/o pass, no throw)"); let res2 = new Resource("http://localhost:8080/protected"); content = res2.get(); do_check_true(did401); do_check_eq(content, "This path exists and is protected - failed"); do_check_eq(content.status, 401); do_check_false(content.success); _("GET a password protected resource"); let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); let res3 = new Resource("http://localhost:8080/protected"); res3.authenticator = auth; do_check_eq(res3.authenticator, auth); content = res3.get(); do_check_eq(content, "This path exists and is protected"); do_check_eq(content.status, 200); do_check_true(content.success); _("GET a non-existent resource (test that it'll fail, but not throw)"); let res4 = new Resource("http://localhost:8080/404"); content = res4.get(); do_check_eq(content, "File not found"); do_check_eq(content.status, 404); do_check_false(content.success); // Check some headers of the 404 response do_check_eq(content.headers.connection, "close"); do_check_eq(content.headers.server, "httpd.js"); do_check_eq(content.headers["content-length"], 14); _("PUT to a resource (string)"); let res5 = new Resource("http://localhost:8080/upload"); content = res5.put(JSON.stringify(sample_data)); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("PUT to a resource (object)"); content = res5.put(sample_data); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("PUT without data arg (uses resource.data) (string)"); res5.data = JSON.stringify(sample_data); content = res5.put(); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("PUT without data arg (uses resource.data) (object)"); res5.data = sample_data; content = res5.put(); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("POST to a resource (string)"); content = res5.post(JSON.stringify(sample_data)); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("POST to a resource (object)"); content = res5.post(sample_data); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("POST without data arg (uses resource.data) (string)"); res5.data = JSON.stringify(sample_data); content = res5.post(); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("POST without data arg (uses resource.data) (object)"); res5.data = sample_data; content = res5.post(); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res5.data, content); _("DELETE a resource"); let res6 = new Resource("http://localhost:8080/delete"); content = res6.delete(); do_check_eq(content, "This resource has been deleted") do_check_eq(content.status, 200); _("JSON conversion of response body"); let res7 = new Resource("http://localhost:8080/json"); content = res7.get(); do_check_eq(content, JSON.stringify(sample_data)); do_check_eq(content.status, 200); do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data)); _("X-Weave-Timestamp header updates Resource.serverTime"); // Before having received any response containing the // X-Weave-Timestamp header, Resource.serverTime is null. do_check_eq(Resource.serverTime, null); let res8 = new Resource("http://localhost:8080/timestamp"); content = res8.get(); do_check_eq(Resource.serverTime, TIMESTAMP); _("GET: no special request headers"); let res9 = new Resource("http://localhost:8080/headers"); content = res9.get(); do_check_eq(content, '{}'); _("PUT: Content-Type defaults to text/plain"); content = res9.put('data'); do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); _("POST: Content-Type defaults to text/plain"); content = res9.post('data'); do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); _("setHeader(): setting simple header"); res9.setHeader('X-What-Is-Weave', 'awesome'); do_check_eq(res9.headers['x-what-is-weave'], 'awesome'); content = res9.get(); do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"})); _("setHeader(): setting multiple headers, overwriting existing header"); res9.setHeader('X-WHAT-is-Weave', 'more awesomer', 'X-Another-Header', 'hello world'); do_check_eq(res9.headers['x-what-is-weave'], 'more awesomer'); do_check_eq(res9.headers['x-another-header'], 'hello world'); content = res9.get(); do_check_eq(content, JSON.stringify({"x-another-header": "hello world", "x-what-is-weave": "more awesomer"})); _("Setting headers object"); res9.headers = {}; content = res9.get(); do_check_eq(content, "{}"); _("PUT/POST: override default Content-Type"); res9.setHeader('Content-Type', 'application/foobar'); do_check_eq(res9.headers['content-type'], 'application/foobar'); content = res9.put('data'); do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); content = res9.post('data'); do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); _("X-Weave-Backoff header notifies observer"); let backoffInterval; function onBackoff(subject, data) { backoffInterval = subject; } Observers.add("weave:service:backoff:interval", onBackoff); let res10 = new Resource("http://localhost:8080/backoff"); content = res10.get(); do_check_eq(backoffInterval, 600); _("X-Weave-Quota-Remaining header notifies observer on successful requests."); let quotaValue; function onQuota(subject, data) { quotaValue = subject; } Observers.add("weave:service:quota:remaining", onQuota); res10 = new Resource("http://localhost:8080/quota-error"); content = res10.get(); do_check_eq(content.status, 400); do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification. res10 = new Resource("http://localhost:8080/quota-notice"); content = res10.get(); do_check_eq(content.status, 200); do_check_eq(quotaValue, 1048576); _("Error handling in _request() preserves exception information"); let error; let res11 = new Resource("http://localhost:12345/does/not/exist"); try { content = res11.get(); } catch(ex) { error = ex; } do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); do_check_eq(typeof error.stack, "string"); let redirRequest; let redirToOpen = function(subject) { subject.newUri = "http://localhost:8080/open"; redirRequest = subject; }; Observers.add("weave:resource:status:401", redirToOpen); _("Notification of 401 can redirect to another uri"); did401 = false; let res12 = new Resource("http://localhost:8080/protected"); content = res12.get(); do_check_eq(res12.spec, "http://localhost:8080/open"); do_check_eq(content, "This path exists"); do_check_eq(content.status, 200); do_check_true(content.success); do_check_eq(res.data, content); do_check_true(did401); do_check_eq(redirRequest.response, "This path exists and is protected - failed"); do_check_eq(redirRequest.response.status, 401); do_check_false(redirRequest.response.success); Observers.remove("weave:resource:status:401", redirToOpen); _("Removing the observer should result in the original 401"); did401 = false; let res13 = new Resource("http://localhost:8080/protected"); content = res13.get(); do_check_true(did401); do_check_eq(content, "This path exists and is protected - failed"); do_check_eq(content.status, 401); do_check_false(content.success); // Faking problems. fake_status_failure(); // POST doesn't do our inner retry, so we get a status 0. FAKE_ZERO_COUNTER = 1; let res14 = new Resource("http://localhost:8080/open"); content = res14.post("hello"); do_check_eq(content.status, 0); do_check_false(content.success); // And now we succeed... let res15 = new Resource("http://localhost:8080/open"); content = res15.post("hello"); do_check_eq(content.status, 405); do_check_false(content.success); // Now check that GET silent failures get retried. FAKE_ZERO_COUNTER = 1; let res16 = new Resource("http://localhost:8080/open"); content = res16.get(); do_check_eq(content.status, 200); do_check_true(content.success); // ... but only once. FAKE_ZERO_COUNTER = 2; let res17 = new Resource("http://localhost:8080/open"); content = res17.get(); do_check_eq(content.status, 0); do_check_false(content.success); _("Checking handling of errors in onProgress."); let res18 = new Resource("http://localhost:8080/json"); let onProgress = function(rec) { // Provoke an XPC exception without a Javascript wrapper. Svc.IO.newURI("::::::::", null, null); }; res18._onProgress = onProgress; let oldWarn = res18._log.warn; let warnings = []; res18._log.warn = function(msg) { warnings.push(msg) }; error = undefined; try { content = res18.get(); } catch (ex) { error = ex; } // It throws and logs. do_check_eq(error, "Error: NS_ERROR_MALFORMED_URI"); do_check_eq(warnings.pop(), "Got exception calling onProgress handler during fetch of " + "http://localhost:8080/json"); // And this is what happens if JS throws an exception. res18 = new Resource("http://localhost:8080/json"); onProgress = function(rec) { throw "BOO!"; }; res18._onProgress = onProgress; oldWarn = res18._log.warn; warnings = []; res18._log.warn = function(msg) { warnings.push(msg) }; error = undefined; try { content = res18.get(); } catch (ex) { error = ex; } // It throws and logs. do_check_eq(error, "Error: NS_ERROR_XPC_JS_THREW_STRING"); do_check_eq(warnings.pop(), "Got exception calling onProgress handler during fetch of " + "http://localhost:8080/json"); server.stop(do_test_finished); }