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