diff options
Diffstat (limited to 'services/common/tests/unit/test_hawkclient.js')
-rw-r--r-- | services/common/tests/unit/test_hawkclient.js | 515 |
1 files changed, 515 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 0000000000..33d125f42e --- /dev/null +++ b/services/common/tests/unit/test_hawkclient.js @@ -0,0 +1,515 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HawkClient } = ChromeUtils.importESModule( + "resource://services-common/hawkclient.sys.mjs" +); + +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"); + + Assert.ok(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. + Assert.ok(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS); +}); + +add_task(async function test_authenticated_get_request() { + let message = '{"msg": "Great Success!"}'; + let method = "GET"; + + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(request.hasHeader("Authorization")); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + let response = await client.request("/foo", method, TEST_CREDS); + let result = JSON.parse(response.body); + + Assert.equal("Great Success!", result.msg); + + await promiseStopServer(server); +}); + +async function check_authenticated_request(method) { + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(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 = await client.request("/foo", method, TEST_CREDS, { + foo: "bar", + }); + let result = JSON.parse(response.body); + + Assert.equal("bar", result.foo); + + await promiseStopServer(server); +} + +add_task(async function test_authenticated_post_request() { + await check_authenticated_request("POST"); +}); + +add_task(async function test_authenticated_put_request() { + await check_authenticated_request("PUT"); +}); + +add_task(async function test_authenticated_patch_request() { + await check_authenticated_request("PATCH"); +}); + +add_task(async function test_extra_headers() { + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(request.hasHeader("Authorization")); + Assert.ok(request.hasHeader("myHeader")); + Assert.equal(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 = await client.request( + "/foo", + "POST", + TEST_CREDS, + { foo: "bar" }, + { myHeader: "fake" } + ); + let result = JSON.parse(response.body); + + Assert.equal("bar", result.foo); + + await promiseStopServer(server); +}); + +add_task(async function test_credentials_optional() { + let method = "GET"; + let server = httpd_setup({ + "/foo": (request, response) => { + Assert.ok(!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 = await client.request("/foo", method); // credentials undefined + Assert.equal(JSON.parse(result.body).msg, "you're in the friend zone"); + + await promiseStopServer(server); +}); + +add_task(async 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 { + await client.request("/foo", method, TEST_CREDS); + do_throw("Expected an error"); + } catch (err) { + Assert.equal(418, err.code); + Assert.equal("I am a Teapot", err.message); + } + + await promiseStopServer(server); +}); + +add_task(async 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 { + await client.request("/foo", method, TEST_CREDS); + do_throw("Expected an error"); + } catch (err) { + Assert.equal("Cannot get ye flask.", err.error); + } + + await promiseStopServer(server); +}); + +add_task(async 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; + }; + + Assert.equal(client.localtimeOffsetMsec, 0); + + await client.request("/foo", method, TEST_CREDS); + // Should be about an hour off + Assert.ok(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS); + + await promiseStopServer(server); +}); + +add_task(async 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")); + + // 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); + + client.now = () => { + return Date.now() + 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + await 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. + Assert.ok(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS); + await client.request("/second", method, TEST_CREDS); + + await promiseStopServer(server); +}); + +add_task(async 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 = await client.request("/foo", method, credentials); + + // Shouldn't be any content in a 202 + Assert.equal(response.body, ""); + + await promiseStopServer(server); +}); + +add_task(async 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; + Assert.ok(attempts <= 2); + + let delta = getTimestampDelta(request.getHeader("Authorization")); + + // First time through, we should have a bad timestamp + if (attempts === 1) { + Assert.ok(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 + Assert.ok(delta < MINUTE_MS); + let message = "i love you!!!"; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + client.now = () => { + return Date.now() + 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + + // Request will have bad timestamp; client will retry once + let response = await client.request("/maybe", method, credentials); + Assert.equal(response.body, "i love you!!!"); + + await promiseStopServer(server); +}); + +add_task(async 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; + + Assert.ok(attempts <= 2); + + let message = "never!!!"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.bodyOutputStream.write(message, message.length); + }, + }); + + let client = new HawkClient(server.baseURI); + + client.now = () => { + return Date.now() - 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + + // Request will have bad timestamp; client will retry once + try { + await client.request("/maybe", method, credentials); + do_throw("Expected an error"); + } catch (err) { + Assert.equal(err.code, 401); + } + Assert.equal(attempts, 2); + + await promiseStopServer(server); +}); + +add_task(async 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 (request, response) { + 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); + + // 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 { + await client.request("/no-shutup", method, credentials); + do_throw("Expected an error"); + } catch (err) { + Assert.equal(err.code, 500); + } + + await promiseStopServer(server); +}); + +add_task(async 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; + Assert.ok(attempts <= 2); + + let delta = getTimestampDelta(request.getHeader("Authorization")); + + // First time through, we should have a bad timestamp + // Client will retry + if (attempts === 1) { + Assert.ok(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 + Assert.ok(delta < MINUTE_MS); + 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); + + client.now = () => { + return Date.now() - 12 * HOUR_MS; + }; + + // We begin with no offset + Assert.equal(client.localtimeOffsetMsec, 0); + + // Request will have bad timestamp; client will retry once + try { + await client.request("/maybe", method, credentials); + } catch (err) { + Assert.equal(err.code, 500); + } + Assert.equal(attempts, 2); + + await promiseStopServer(server); +}); + +// 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 run_test() { + initTestLogging("Trace"); + run_next_test(); +} |