Cu.import("resource://services-sync/auth.js"); 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); } function run_test() { logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); do_test_pending(); 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 let did401 = false; Observers.add("weave:resource:status:401", function() did401 = true); let quotaValue; Observers.add("weave:service:quota:remaining", function (subject) quotaValue = subject); // Ensure exceptions from inside callbacks leads to test failures. function ensureThrows(func) { return function() { try { func.apply(this, arguments); } catch (ex) { do_throw(ex); } }; } let res_upload = new AsyncResource("http://localhost:8080/upload"); let res_headers = new AsyncResource("http://localhost:8080/headers"); _("Resource object memebers"); let res = new AsyncResource("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); Utils.asyncChain(function (next) { _("GET a non-password-protected resource"); do_test_pending(); res.get(ensureThrows(function (error, content) { do_check_eq(error, null); 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); do_test_finished(); next(); })); }, function (next) { _("GET a password protected resource (test that it'll fail w/o pass, no throw)"); let res2 = new AsyncResource("http://localhost:8080/protected"); do_test_pending(); res2.get(ensureThrows(function (error, content) { do_check_eq(error, null); 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); do_test_finished(); next(); })); }, function (next) { _("GET a password protected resource"); let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); let res3 = new AsyncResource("http://localhost:8080/protected"); res3.authenticator = auth; do_check_eq(res3.authenticator, auth); do_test_pending(); res3.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "This path exists and is protected"); do_check_eq(content.status, 200); do_check_true(content.success); do_test_finished(); next(); })); }, function (next) { _("GET a non-existent resource (test that it'll fail, but not throw)"); let res4 = new AsyncResource("http://localhost:8080/404"); do_test_pending(); res4.get(ensureThrows(function (error, content) { do_check_eq(error, null); 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); do_test_finished(); next(); })); }, function (next) { _("PUT to a resource (string)"); do_test_pending(); res_upload.put(JSON.stringify(sample_data), ensureThrows(function(error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("PUT to a resource (object)"); do_test_pending(); res_upload.put(sample_data, ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("PUT without data arg (uses resource.data) (string)"); do_test_pending(); res_upload.data = JSON.stringify(sample_data); res_upload.put(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("PUT without data arg (uses resource.data) (object)"); do_test_pending(); res_upload.data = sample_data; res_upload.put(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via PUT"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("POST to a resource (string)"); do_test_pending(); res_upload.post(JSON.stringify(sample_data), ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("POST to a resource (object)"); do_test_pending(); res_upload.post(sample_data, ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("POST without data arg (uses resource.data) (string)"); do_test_pending(); res_upload.data = JSON.stringify(sample_data); res_upload.post(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("POST without data arg (uses resource.data) (object)"); do_test_pending(); res_upload.data = sample_data; res_upload.post(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "Valid data upload via POST"); do_check_eq(content.status, 200); do_check_eq(res_upload.data, content); do_test_finished(); next(); })); }, function (next) { _("DELETE a resource"); do_test_pending(); let res6 = new AsyncResource("http://localhost:8080/delete"); res6.delete(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "This resource has been deleted"); do_check_eq(content.status, 200); do_test_finished(); next(); })); }, function (next) { _("JSON conversion of response body"); do_test_pending(); let res7 = new AsyncResource("http://localhost:8080/json"); res7.get(ensureThrows(function (error, content) { do_check_eq(error, null); 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)); do_test_finished(); next(); })); }, function (next) { _("X-Weave-Timestamp header updates Resource.serverTime"); do_test_pending(); // Before having received any response containing the // X-Weave-Timestamp header, Resource.serverTime is null. do_check_eq(Resource.serverTime, null); let res8 = new AsyncResource("http://localhost:8080/timestamp"); res8.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(Resource.serverTime, TIMESTAMP); do_test_finished(); next(); })); }, function (next) { _("GET: no special request headers"); do_test_pending(); res_headers.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, '{}'); do_test_finished(); next(); })); }, function (next) { _("PUT: Content-Type defaults to text/plain"); do_test_pending(); res_headers.put('data', ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); do_test_finished(); next(); })); }, function (next) { _("POST: Content-Type defaults to text/plain"); do_test_pending(); res_headers.post('data', ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); do_test_finished(); next(); })); }, function (next) { _("setHeader(): setting simple header"); do_test_pending(); res_headers.setHeader('X-What-Is-Weave', 'awesome'); do_check_eq(res_headers.headers['x-what-is-weave'], 'awesome'); res_headers.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"})); do_test_finished(); next(); })); }, function (next) { _("setHeader(): setting multiple headers, overwriting existing header"); do_test_pending(); res_headers.setHeader('X-WHAT-is-Weave', 'more awesomer', 'X-Another-Header', 'hello world'); do_check_eq(res_headers.headers['x-what-is-weave'], 'more awesomer'); do_check_eq(res_headers.headers['x-another-header'], 'hello world'); res_headers.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, JSON.stringify({"x-another-header": "hello world", "x-what-is-weave": "more awesomer"})); do_test_finished(); next(); })); }, function (next) { _("Setting headers object"); do_test_pending(); res_headers.headers = {}; res_headers.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, "{}"); do_test_finished(); next(); })); }, function (next) { _("PUT: override default Content-Type"); do_test_pending(); res_headers.setHeader('Content-Type', 'application/foobar'); do_check_eq(res_headers.headers['content-type'], 'application/foobar'); res_headers.put('data', ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); do_test_finished(); next(); })); }, function (next) { _("POST: override default Content-Type"); do_test_pending(); res_headers.post('data', ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); do_test_finished(); next(); })); }, function (next) { _("X-Weave-Backoff header notifies observer"); let backoffInterval; function onBackoff(subject, data) { backoffInterval = subject; } Observers.add("weave:service:backoff:interval", onBackoff); do_test_pending(); let res10 = new AsyncResource("http://localhost:8080/backoff"); res10.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(backoffInterval, 600); do_test_finished(); next(); })); }, function (next) { _("X-Weave-Quota-Remaining header notifies observer on successful requests."); do_test_pending(); let res10 = new AsyncResource("http://localhost:8080/quota-error"); res10.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content.status, 400); do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification. do_test_finished(); next(); })); }, function (next) { do_test_pending(); let res10 = new AsyncResource("http://localhost:8080/quota-notice"); res10.get(ensureThrows(function (error, content) { do_check_eq(error, null); do_check_eq(content.status, 200); do_check_eq(quotaValue, 1048576); do_test_finished(); next(); })); }, function (next) { _("Error handling in ChannelListener etc. preserves exception information"); do_test_pending(); let res11 = new AsyncResource("http://localhost:12345/does/not/exist"); res11.get(ensureThrows(function (error, content) { do_check_neq(error, null); do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); do_check_eq(typeof error.stack, "string"); do_test_finished(); next(); })); }, function (next) { 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; do_test_pending(); let res12 = new AsyncResource("http://localhost:8080/protected"); res12.get(ensureThrows(function (error, content) { do_check_eq(error, null); 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); do_test_finished(); next(); })); }, function (next) { _("Removing the observer should result in the original 401"); did401 = false; let res13 = new AsyncResource("http://localhost:8080/protected"); res13.get(ensureThrows(function (error, content) { do_check_eq(error, null); 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); do_test_finished(); next(); })); }, function (next) { // Don't quit test harness before server shuts down. server.stop(do_test_finished); })(); }