michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: "use strict"; michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://services-common/hawkclient.js"); michael@0: michael@0: const SECOND_MS = 1000; michael@0: const MINUTE_MS = SECOND_MS * 60; michael@0: const HOUR_MS = MINUTE_MS * 60; michael@0: michael@0: const TEST_CREDS = { michael@0: id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", michael@0: key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", michael@0: algorithm: "sha256" michael@0: }; michael@0: michael@0: initTestLogging("Trace"); michael@0: michael@0: add_task(function test_now() { michael@0: let client = new HawkClient("https://example.com"); michael@0: michael@0: do_check_true(client.now() - Date.now() < SECOND_MS); michael@0: }); michael@0: michael@0: add_task(function test_updateClockOffset() { michael@0: let client = new HawkClient("https://example.com"); michael@0: michael@0: let now = new Date(); michael@0: let serverDate = now.toUTCString(); michael@0: michael@0: // Client's clock is off michael@0: client.now = () => { return now.valueOf() + HOUR_MS; } michael@0: michael@0: client._updateClockOffset(serverDate); michael@0: michael@0: // Check that they're close; there will likely be a one-second rounding michael@0: // error, so checking strict equality will likely fail. michael@0: // michael@0: // localtimeOffsetMsec is how many milliseconds to add to the local clock so michael@0: // that it agrees with the server. We are one hour ahead of the server, so michael@0: // our offset should be -1 hour. michael@0: do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS); michael@0: }); michael@0: michael@0: add_task(function test_authenticated_get_request() { michael@0: let message = "{\"msg\": \"Great Success!\"}"; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({"/foo": (request, response) => { michael@0: do_check_true(request.hasHeader("Authorization")); michael@0: michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: michael@0: let response = yield client.request("/foo", method, TEST_CREDS); michael@0: let result = JSON.parse(response); michael@0: michael@0: do_check_eq("Great Success!", result.msg); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_authenticated_post_request() { michael@0: let method = "POST"; michael@0: michael@0: let server = httpd_setup({"/foo": (request, response) => { michael@0: do_check_true(request.hasHeader("Authorization")); michael@0: michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.setHeader("Content-Type", "application/json"); michael@0: response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available()); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: michael@0: let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"}); michael@0: let result = JSON.parse(response); michael@0: michael@0: do_check_eq("bar", result.foo); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_credentials_optional() { michael@0: let method = "GET"; michael@0: let server = httpd_setup({ michael@0: "/foo": (request, response) => { michael@0: do_check_false(request.hasHeader("Authorization")); michael@0: michael@0: let message = JSON.stringify({msg: "you're in the friend zone"}); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.setHeader("Content-Type", "application/json"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: let result = yield client.request("/foo", method); // credentials undefined michael@0: do_check_eq(JSON.parse(result).msg, "you're in the friend zone"); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_server_error() { michael@0: let message = "Ohai!"; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({"/foo": (request, response) => { michael@0: response.setStatusLine(request.httpVersion, 418, "I am a Teapot"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: michael@0: try { michael@0: yield client.request("/foo", method, TEST_CREDS); michael@0: do_throw("Expected an error"); michael@0: } catch(err) { michael@0: do_check_eq(418, err.code); michael@0: do_check_eq("I am a Teapot", err.message); michael@0: } michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_server_error_json() { michael@0: let message = JSON.stringify({error: "Cannot get ye flask."}); michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({"/foo": (request, response) => { michael@0: response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: michael@0: try { michael@0: yield client.request("/foo", method, TEST_CREDS); michael@0: do_throw("Expected an error"); michael@0: } catch(err) { michael@0: do_check_eq("Cannot get ye flask.", err.error); michael@0: } michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_offset_after_request() { michael@0: let message = "Ohai!"; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({"/foo": (request, response) => { michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: let now = Date.now(); michael@0: client.now = () => { return now + HOUR_MS; }; michael@0: michael@0: do_check_eq(client.localtimeOffsetMsec, 0); michael@0: michael@0: let response = yield client.request("/foo", method, TEST_CREDS); michael@0: // Should be about an hour off michael@0: do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_offset_in_hawk_header() { michael@0: let message = "Ohai!"; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({ michael@0: "/first": function(request, response) { michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: }, michael@0: michael@0: "/second": function(request, response) { michael@0: // We see a better date now in the ts component of the header michael@0: let delta = getTimestampDelta(request.getHeader("Authorization")); michael@0: let message = "Delta: " + delta; michael@0: michael@0: // We're now within HAWK's one-minute window. michael@0: // I hope this isn't a recipe for intermittent oranges ... michael@0: if (delta < MINUTE_MS) { michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: } else { michael@0: response.setStatusLine(request.httpVersion, 400, "Delta: " + delta); michael@0: } michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: function getOffset() { michael@0: return client.localtimeOffsetMsec; michael@0: } michael@0: michael@0: client.now = () => { michael@0: return Date.now() + 12 * HOUR_MS; michael@0: }; michael@0: michael@0: // We begin with no offset michael@0: do_check_eq(client.localtimeOffsetMsec, 0); michael@0: yield client.request("/first", method, TEST_CREDS); michael@0: michael@0: // After the first server response, our offset is updated to -12 hours. michael@0: // We should be safely in the window, now. michael@0: do_check_true(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS); michael@0: yield client.request("/second", method, TEST_CREDS); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_2xx_success() { michael@0: // Just to ensure that we're not biased toward 200 OK for success michael@0: let credentials = { michael@0: id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", michael@0: key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", michael@0: algorithm: "sha256" michael@0: }; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({"/foo": (request, response) => { michael@0: response.setStatusLine(request.httpVersion, 202, "Accepted"); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: michael@0: let response = yield client.request("/foo", method, credentials); michael@0: michael@0: // Shouldn't be any content in a 202 michael@0: do_check_eq(response, ""); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_retry_request_on_fail() { michael@0: let attempts = 0; michael@0: let credentials = { michael@0: id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", michael@0: key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", michael@0: algorithm: "sha256" michael@0: }; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({ michael@0: "/maybe": function(request, response) { michael@0: // This path should be hit exactly twice; once with a bad timestamp, and michael@0: // again when the client retries the request with a corrected timestamp. michael@0: attempts += 1; michael@0: do_check_true(attempts <= 2); michael@0: michael@0: let delta = getTimestampDelta(request.getHeader("Authorization")); michael@0: michael@0: // First time through, we should have a bad timestamp michael@0: if (attempts === 1) { michael@0: do_check_true(delta > MINUTE_MS); michael@0: let message = "never!!!"; michael@0: response.setStatusLine(request.httpVersion, 401, "Unauthorized"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: return; michael@0: } michael@0: michael@0: // Second time through, timestamp should be corrected by client michael@0: do_check_true(delta < MINUTE_MS); michael@0: let message = "i love you!!!"; michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: return; michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: function getOffset() { michael@0: return client.localtimeOffsetMsec; michael@0: } michael@0: michael@0: client.now = () => { michael@0: return Date.now() + 12 * HOUR_MS; michael@0: }; michael@0: michael@0: // We begin with no offset michael@0: do_check_eq(client.localtimeOffsetMsec, 0); michael@0: michael@0: // Request will have bad timestamp; client will retry once michael@0: let response = yield client.request("/maybe", method, credentials); michael@0: do_check_eq(response, "i love you!!!"); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_multiple_401_retry_once() { michael@0: // Like test_retry_request_on_fail, but always return a 401 michael@0: // and ensure that the client only retries once. michael@0: let attempts = 0; michael@0: let credentials = { michael@0: id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", michael@0: key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", michael@0: algorithm: "sha256" michael@0: }; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({ michael@0: "/maybe": function(request, response) { michael@0: // This path should be hit exactly twice; once with a bad timestamp, and michael@0: // again when the client retries the request with a corrected timestamp. michael@0: attempts += 1; michael@0: michael@0: do_check_true(attempts <= 2); michael@0: michael@0: let message = "never!!!"; michael@0: response.setStatusLine(request.httpVersion, 401, "Unauthorized"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: function getOffset() { michael@0: return client.localtimeOffsetMsec; michael@0: } michael@0: michael@0: client.now = () => { michael@0: return Date.now() - 12 * HOUR_MS; michael@0: }; michael@0: michael@0: // We begin with no offset michael@0: do_check_eq(client.localtimeOffsetMsec, 0); michael@0: michael@0: // Request will have bad timestamp; client will retry once michael@0: try { michael@0: yield client.request("/maybe", method, credentials); michael@0: do_throw("Expected an error"); michael@0: } catch (err) { michael@0: do_check_eq(err.code, 401); michael@0: } michael@0: do_check_eq(attempts, 2); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_500_no_retry() { michael@0: // If we get a 500 error, the client should not retry (as it would with a michael@0: // 401) michael@0: let credentials = { michael@0: id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", michael@0: key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", michael@0: algorithm: "sha256" michael@0: }; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({ michael@0: "/no-shutup": function() { michael@0: let message = "Cannot get ye flask."; michael@0: response.setStatusLine(request.httpVersion, 500, "Internal server error"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: function getOffset() { michael@0: return client.localtimeOffsetMsec; michael@0: } michael@0: michael@0: // Throw off the clock so the HawkClient would want to retry the request if michael@0: // it could michael@0: client.now = () => { michael@0: return Date.now() - 12 * HOUR_MS; michael@0: }; michael@0: michael@0: // Request will 500; no retries michael@0: try { michael@0: yield client.request("/no-shutup", method, credentials); michael@0: do_throw("Expected an error"); michael@0: } catch(err) { michael@0: do_check_eq(err.code, 500); michael@0: } michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function test_401_then_500() { michael@0: // Like test_multiple_401_retry_once, but return a 500 to the michael@0: // second request, ensuring that the promise is properly rejected michael@0: // in client.request. michael@0: let attempts = 0; michael@0: let credentials = { michael@0: id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", michael@0: key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", michael@0: algorithm: "sha256" michael@0: }; michael@0: let method = "GET"; michael@0: michael@0: let server = httpd_setup({ michael@0: "/maybe": function(request, response) { michael@0: // This path should be hit exactly twice; once with a bad timestamp, and michael@0: // again when the client retries the request with a corrected timestamp. michael@0: attempts += 1; michael@0: do_check_true(attempts <= 2); michael@0: michael@0: let delta = getTimestampDelta(request.getHeader("Authorization")); michael@0: michael@0: // First time through, we should have a bad timestamp michael@0: // Client will retry michael@0: if (attempts === 1) { michael@0: do_check_true(delta > MINUTE_MS); michael@0: let message = "never!!!"; michael@0: response.setStatusLine(request.httpVersion, 401, "Unauthorized"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: return; michael@0: } michael@0: michael@0: // Second time through, timestamp should be corrected by client michael@0: // And fail on the client michael@0: do_check_true(delta < MINUTE_MS); michael@0: let message = "Cannot get ye flask."; michael@0: response.setStatusLine(request.httpVersion, 500, "Internal server error"); michael@0: response.bodyOutputStream.write(message, message.length); michael@0: return; michael@0: } michael@0: }); michael@0: michael@0: let client = new HawkClient(server.baseURI); michael@0: function getOffset() { michael@0: return client.localtimeOffsetMsec; michael@0: } michael@0: michael@0: client.now = () => { michael@0: return Date.now() - 12 * HOUR_MS; michael@0: }; michael@0: michael@0: // We begin with no offset michael@0: do_check_eq(client.localtimeOffsetMsec, 0); michael@0: michael@0: // Request will have bad timestamp; client will retry once michael@0: try { michael@0: yield client.request("/maybe", method, credentials); michael@0: } catch(err) { michael@0: do_check_eq(err.code, 500); michael@0: } michael@0: do_check_eq(attempts, 2); michael@0: michael@0: yield deferredStop(server); michael@0: }); michael@0: michael@0: add_task(function throw_if_not_json_body() { michael@0: let client = new HawkClient("https://example.com"); michael@0: try { michael@0: yield client.request("/bogus", "GET", {}, "I am not json"); michael@0: do_throw("Expected an error"); michael@0: } catch(err) { michael@0: do_check_true(!!err.message); michael@0: } michael@0: }); michael@0: michael@0: // End of tests. michael@0: // Utility functions follow michael@0: michael@0: function getTimestampDelta(authHeader, now=Date.now()) { michael@0: let tsMS = new Date( michael@0: parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS); michael@0: return Math.abs(tsMS - now); michael@0: } michael@0: michael@0: function deferredStop(server) { michael@0: let deferred = Promise.defer(); michael@0: server.stop(deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function run_test() { michael@0: initTestLogging("Trace"); michael@0: run_next_test(); michael@0: } michael@0: