summaryrefslogtreecommitdiffstats
path: root/services/common/tests/unit/test_hawkclient.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/common/tests/unit/test_hawkclient.js')
-rw-r--r--services/common/tests/unit/test_hawkclient.js520
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();
+}
+