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.js515
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();
+}