1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/common/tests/unit/test_hawkclient.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,487 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +"use strict"; 1.8 + 1.9 +Cu.import("resource://gre/modules/Promise.jsm"); 1.10 +Cu.import("resource://services-common/hawkclient.js"); 1.11 + 1.12 +const SECOND_MS = 1000; 1.13 +const MINUTE_MS = SECOND_MS * 60; 1.14 +const HOUR_MS = MINUTE_MS * 60; 1.15 + 1.16 +const TEST_CREDS = { 1.17 + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", 1.18 + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", 1.19 + algorithm: "sha256" 1.20 +}; 1.21 + 1.22 +initTestLogging("Trace"); 1.23 + 1.24 +add_task(function test_now() { 1.25 + let client = new HawkClient("https://example.com"); 1.26 + 1.27 + do_check_true(client.now() - Date.now() < SECOND_MS); 1.28 +}); 1.29 + 1.30 +add_task(function test_updateClockOffset() { 1.31 + let client = new HawkClient("https://example.com"); 1.32 + 1.33 + let now = new Date(); 1.34 + let serverDate = now.toUTCString(); 1.35 + 1.36 + // Client's clock is off 1.37 + client.now = () => { return now.valueOf() + HOUR_MS; } 1.38 + 1.39 + client._updateClockOffset(serverDate); 1.40 + 1.41 + // Check that they're close; there will likely be a one-second rounding 1.42 + // error, so checking strict equality will likely fail. 1.43 + // 1.44 + // localtimeOffsetMsec is how many milliseconds to add to the local clock so 1.45 + // that it agrees with the server. We are one hour ahead of the server, so 1.46 + // our offset should be -1 hour. 1.47 + do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS); 1.48 +}); 1.49 + 1.50 +add_task(function test_authenticated_get_request() { 1.51 + let message = "{\"msg\": \"Great Success!\"}"; 1.52 + let method = "GET"; 1.53 + 1.54 + let server = httpd_setup({"/foo": (request, response) => { 1.55 + do_check_true(request.hasHeader("Authorization")); 1.56 + 1.57 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.58 + response.bodyOutputStream.write(message, message.length); 1.59 + } 1.60 + }); 1.61 + 1.62 + let client = new HawkClient(server.baseURI); 1.63 + 1.64 + let response = yield client.request("/foo", method, TEST_CREDS); 1.65 + let result = JSON.parse(response); 1.66 + 1.67 + do_check_eq("Great Success!", result.msg); 1.68 + 1.69 + yield deferredStop(server); 1.70 +}); 1.71 + 1.72 +add_task(function test_authenticated_post_request() { 1.73 + let method = "POST"; 1.74 + 1.75 + let server = httpd_setup({"/foo": (request, response) => { 1.76 + do_check_true(request.hasHeader("Authorization")); 1.77 + 1.78 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.79 + response.setHeader("Content-Type", "application/json"); 1.80 + response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available()); 1.81 + } 1.82 + }); 1.83 + 1.84 + let client = new HawkClient(server.baseURI); 1.85 + 1.86 + let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"}); 1.87 + let result = JSON.parse(response); 1.88 + 1.89 + do_check_eq("bar", result.foo); 1.90 + 1.91 + yield deferredStop(server); 1.92 +}); 1.93 + 1.94 +add_task(function test_credentials_optional() { 1.95 + let method = "GET"; 1.96 + let server = httpd_setup({ 1.97 + "/foo": (request, response) => { 1.98 + do_check_false(request.hasHeader("Authorization")); 1.99 + 1.100 + let message = JSON.stringify({msg: "you're in the friend zone"}); 1.101 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.102 + response.setHeader("Content-Type", "application/json"); 1.103 + response.bodyOutputStream.write(message, message.length); 1.104 + } 1.105 + }); 1.106 + 1.107 + let client = new HawkClient(server.baseURI); 1.108 + let result = yield client.request("/foo", method); // credentials undefined 1.109 + do_check_eq(JSON.parse(result).msg, "you're in the friend zone"); 1.110 + 1.111 + yield deferredStop(server); 1.112 +}); 1.113 + 1.114 +add_task(function test_server_error() { 1.115 + let message = "Ohai!"; 1.116 + let method = "GET"; 1.117 + 1.118 + let server = httpd_setup({"/foo": (request, response) => { 1.119 + response.setStatusLine(request.httpVersion, 418, "I am a Teapot"); 1.120 + response.bodyOutputStream.write(message, message.length); 1.121 + } 1.122 + }); 1.123 + 1.124 + let client = new HawkClient(server.baseURI); 1.125 + 1.126 + try { 1.127 + yield client.request("/foo", method, TEST_CREDS); 1.128 + do_throw("Expected an error"); 1.129 + } catch(err) { 1.130 + do_check_eq(418, err.code); 1.131 + do_check_eq("I am a Teapot", err.message); 1.132 + } 1.133 + 1.134 + yield deferredStop(server); 1.135 +}); 1.136 + 1.137 +add_task(function test_server_error_json() { 1.138 + let message = JSON.stringify({error: "Cannot get ye flask."}); 1.139 + let method = "GET"; 1.140 + 1.141 + let server = httpd_setup({"/foo": (request, response) => { 1.142 + response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?"); 1.143 + response.bodyOutputStream.write(message, message.length); 1.144 + } 1.145 + }); 1.146 + 1.147 + let client = new HawkClient(server.baseURI); 1.148 + 1.149 + try { 1.150 + yield client.request("/foo", method, TEST_CREDS); 1.151 + do_throw("Expected an error"); 1.152 + } catch(err) { 1.153 + do_check_eq("Cannot get ye flask.", err.error); 1.154 + } 1.155 + 1.156 + yield deferredStop(server); 1.157 +}); 1.158 + 1.159 +add_task(function test_offset_after_request() { 1.160 + let message = "Ohai!"; 1.161 + let method = "GET"; 1.162 + 1.163 + let server = httpd_setup({"/foo": (request, response) => { 1.164 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.165 + response.bodyOutputStream.write(message, message.length); 1.166 + } 1.167 + }); 1.168 + 1.169 + let client = new HawkClient(server.baseURI); 1.170 + let now = Date.now(); 1.171 + client.now = () => { return now + HOUR_MS; }; 1.172 + 1.173 + do_check_eq(client.localtimeOffsetMsec, 0); 1.174 + 1.175 + let response = yield client.request("/foo", method, TEST_CREDS); 1.176 + // Should be about an hour off 1.177 + do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS); 1.178 + 1.179 + yield deferredStop(server); 1.180 +}); 1.181 + 1.182 +add_task(function test_offset_in_hawk_header() { 1.183 + let message = "Ohai!"; 1.184 + let method = "GET"; 1.185 + 1.186 + let server = httpd_setup({ 1.187 + "/first": function(request, response) { 1.188 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.189 + response.bodyOutputStream.write(message, message.length); 1.190 + }, 1.191 + 1.192 + "/second": function(request, response) { 1.193 + // We see a better date now in the ts component of the header 1.194 + let delta = getTimestampDelta(request.getHeader("Authorization")); 1.195 + let message = "Delta: " + delta; 1.196 + 1.197 + // We're now within HAWK's one-minute window. 1.198 + // I hope this isn't a recipe for intermittent oranges ... 1.199 + if (delta < MINUTE_MS) { 1.200 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.201 + } else { 1.202 + response.setStatusLine(request.httpVersion, 400, "Delta: " + delta); 1.203 + } 1.204 + response.bodyOutputStream.write(message, message.length); 1.205 + } 1.206 + }); 1.207 + 1.208 + let client = new HawkClient(server.baseURI); 1.209 + function getOffset() { 1.210 + return client.localtimeOffsetMsec; 1.211 + } 1.212 + 1.213 + client.now = () => { 1.214 + return Date.now() + 12 * HOUR_MS; 1.215 + }; 1.216 + 1.217 + // We begin with no offset 1.218 + do_check_eq(client.localtimeOffsetMsec, 0); 1.219 + yield client.request("/first", method, TEST_CREDS); 1.220 + 1.221 + // After the first server response, our offset is updated to -12 hours. 1.222 + // We should be safely in the window, now. 1.223 + do_check_true(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS); 1.224 + yield client.request("/second", method, TEST_CREDS); 1.225 + 1.226 + yield deferredStop(server); 1.227 +}); 1.228 + 1.229 +add_task(function test_2xx_success() { 1.230 + // Just to ensure that we're not biased toward 200 OK for success 1.231 + let credentials = { 1.232 + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", 1.233 + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", 1.234 + algorithm: "sha256" 1.235 + }; 1.236 + let method = "GET"; 1.237 + 1.238 + let server = httpd_setup({"/foo": (request, response) => { 1.239 + response.setStatusLine(request.httpVersion, 202, "Accepted"); 1.240 + } 1.241 + }); 1.242 + 1.243 + let client = new HawkClient(server.baseURI); 1.244 + 1.245 + let response = yield client.request("/foo", method, credentials); 1.246 + 1.247 + // Shouldn't be any content in a 202 1.248 + do_check_eq(response, ""); 1.249 + 1.250 + yield deferredStop(server); 1.251 +}); 1.252 + 1.253 +add_task(function test_retry_request_on_fail() { 1.254 + let attempts = 0; 1.255 + let credentials = { 1.256 + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", 1.257 + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", 1.258 + algorithm: "sha256" 1.259 + }; 1.260 + let method = "GET"; 1.261 + 1.262 + let server = httpd_setup({ 1.263 + "/maybe": function(request, response) { 1.264 + // This path should be hit exactly twice; once with a bad timestamp, and 1.265 + // again when the client retries the request with a corrected timestamp. 1.266 + attempts += 1; 1.267 + do_check_true(attempts <= 2); 1.268 + 1.269 + let delta = getTimestampDelta(request.getHeader("Authorization")); 1.270 + 1.271 + // First time through, we should have a bad timestamp 1.272 + if (attempts === 1) { 1.273 + do_check_true(delta > MINUTE_MS); 1.274 + let message = "never!!!"; 1.275 + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); 1.276 + response.bodyOutputStream.write(message, message.length); 1.277 + return; 1.278 + } 1.279 + 1.280 + // Second time through, timestamp should be corrected by client 1.281 + do_check_true(delta < MINUTE_MS); 1.282 + let message = "i love you!!!"; 1.283 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.284 + response.bodyOutputStream.write(message, message.length); 1.285 + return; 1.286 + } 1.287 + }); 1.288 + 1.289 + let client = new HawkClient(server.baseURI); 1.290 + function getOffset() { 1.291 + return client.localtimeOffsetMsec; 1.292 + } 1.293 + 1.294 + client.now = () => { 1.295 + return Date.now() + 12 * HOUR_MS; 1.296 + }; 1.297 + 1.298 + // We begin with no offset 1.299 + do_check_eq(client.localtimeOffsetMsec, 0); 1.300 + 1.301 + // Request will have bad timestamp; client will retry once 1.302 + let response = yield client.request("/maybe", method, credentials); 1.303 + do_check_eq(response, "i love you!!!"); 1.304 + 1.305 + yield deferredStop(server); 1.306 +}); 1.307 + 1.308 +add_task(function test_multiple_401_retry_once() { 1.309 + // Like test_retry_request_on_fail, but always return a 401 1.310 + // and ensure that the client only retries once. 1.311 + let attempts = 0; 1.312 + let credentials = { 1.313 + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", 1.314 + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", 1.315 + algorithm: "sha256" 1.316 + }; 1.317 + let method = "GET"; 1.318 + 1.319 + let server = httpd_setup({ 1.320 + "/maybe": function(request, response) { 1.321 + // This path should be hit exactly twice; once with a bad timestamp, and 1.322 + // again when the client retries the request with a corrected timestamp. 1.323 + attempts += 1; 1.324 + 1.325 + do_check_true(attempts <= 2); 1.326 + 1.327 + let message = "never!!!"; 1.328 + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); 1.329 + response.bodyOutputStream.write(message, message.length); 1.330 + } 1.331 + }); 1.332 + 1.333 + let client = new HawkClient(server.baseURI); 1.334 + function getOffset() { 1.335 + return client.localtimeOffsetMsec; 1.336 + } 1.337 + 1.338 + client.now = () => { 1.339 + return Date.now() - 12 * HOUR_MS; 1.340 + }; 1.341 + 1.342 + // We begin with no offset 1.343 + do_check_eq(client.localtimeOffsetMsec, 0); 1.344 + 1.345 + // Request will have bad timestamp; client will retry once 1.346 + try { 1.347 + yield client.request("/maybe", method, credentials); 1.348 + do_throw("Expected an error"); 1.349 + } catch (err) { 1.350 + do_check_eq(err.code, 401); 1.351 + } 1.352 + do_check_eq(attempts, 2); 1.353 + 1.354 + yield deferredStop(server); 1.355 +}); 1.356 + 1.357 +add_task(function test_500_no_retry() { 1.358 + // If we get a 500 error, the client should not retry (as it would with a 1.359 + // 401) 1.360 + let credentials = { 1.361 + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", 1.362 + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", 1.363 + algorithm: "sha256" 1.364 + }; 1.365 + let method = "GET"; 1.366 + 1.367 + let server = httpd_setup({ 1.368 + "/no-shutup": function() { 1.369 + let message = "Cannot get ye flask."; 1.370 + response.setStatusLine(request.httpVersion, 500, "Internal server error"); 1.371 + response.bodyOutputStream.write(message, message.length); 1.372 + } 1.373 + }); 1.374 + 1.375 + let client = new HawkClient(server.baseURI); 1.376 + function getOffset() { 1.377 + return client.localtimeOffsetMsec; 1.378 + } 1.379 + 1.380 + // Throw off the clock so the HawkClient would want to retry the request if 1.381 + // it could 1.382 + client.now = () => { 1.383 + return Date.now() - 12 * HOUR_MS; 1.384 + }; 1.385 + 1.386 + // Request will 500; no retries 1.387 + try { 1.388 + yield client.request("/no-shutup", method, credentials); 1.389 + do_throw("Expected an error"); 1.390 + } catch(err) { 1.391 + do_check_eq(err.code, 500); 1.392 + } 1.393 + 1.394 + yield deferredStop(server); 1.395 +}); 1.396 + 1.397 +add_task(function test_401_then_500() { 1.398 + // Like test_multiple_401_retry_once, but return a 500 to the 1.399 + // second request, ensuring that the promise is properly rejected 1.400 + // in client.request. 1.401 + let attempts = 0; 1.402 + let credentials = { 1.403 + id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", 1.404 + key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", 1.405 + algorithm: "sha256" 1.406 + }; 1.407 + let method = "GET"; 1.408 + 1.409 + let server = httpd_setup({ 1.410 + "/maybe": function(request, response) { 1.411 + // This path should be hit exactly twice; once with a bad timestamp, and 1.412 + // again when the client retries the request with a corrected timestamp. 1.413 + attempts += 1; 1.414 + do_check_true(attempts <= 2); 1.415 + 1.416 + let delta = getTimestampDelta(request.getHeader("Authorization")); 1.417 + 1.418 + // First time through, we should have a bad timestamp 1.419 + // Client will retry 1.420 + if (attempts === 1) { 1.421 + do_check_true(delta > MINUTE_MS); 1.422 + let message = "never!!!"; 1.423 + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); 1.424 + response.bodyOutputStream.write(message, message.length); 1.425 + return; 1.426 + } 1.427 + 1.428 + // Second time through, timestamp should be corrected by client 1.429 + // And fail on the client 1.430 + do_check_true(delta < MINUTE_MS); 1.431 + let message = "Cannot get ye flask."; 1.432 + response.setStatusLine(request.httpVersion, 500, "Internal server error"); 1.433 + response.bodyOutputStream.write(message, message.length); 1.434 + return; 1.435 + } 1.436 + }); 1.437 + 1.438 + let client = new HawkClient(server.baseURI); 1.439 + function getOffset() { 1.440 + return client.localtimeOffsetMsec; 1.441 + } 1.442 + 1.443 + client.now = () => { 1.444 + return Date.now() - 12 * HOUR_MS; 1.445 + }; 1.446 + 1.447 + // We begin with no offset 1.448 + do_check_eq(client.localtimeOffsetMsec, 0); 1.449 + 1.450 + // Request will have bad timestamp; client will retry once 1.451 + try { 1.452 + yield client.request("/maybe", method, credentials); 1.453 + } catch(err) { 1.454 + do_check_eq(err.code, 500); 1.455 + } 1.456 + do_check_eq(attempts, 2); 1.457 + 1.458 + yield deferredStop(server); 1.459 +}); 1.460 + 1.461 +add_task(function throw_if_not_json_body() { 1.462 + let client = new HawkClient("https://example.com"); 1.463 + try { 1.464 + yield client.request("/bogus", "GET", {}, "I am not json"); 1.465 + do_throw("Expected an error"); 1.466 + } catch(err) { 1.467 + do_check_true(!!err.message); 1.468 + } 1.469 +}); 1.470 + 1.471 +// End of tests. 1.472 +// Utility functions follow 1.473 + 1.474 +function getTimestampDelta(authHeader, now=Date.now()) { 1.475 + let tsMS = new Date( 1.476 + parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS); 1.477 + return Math.abs(tsMS - now); 1.478 +} 1.479 + 1.480 +function deferredStop(server) { 1.481 + let deferred = Promise.defer(); 1.482 + server.stop(deferred.resolve); 1.483 + return deferred.promise; 1.484 +} 1.485 + 1.486 +function run_test() { 1.487 + initTestLogging("Trace"); 1.488 + run_next_test(); 1.489 +} 1.490 +