services/sync/tests/unit/test_browserid_identity.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/tests/unit/test_browserid_identity.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,666 @@
     1.4 +/* Any copyright is dedicated to the Public Domain.
     1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */
     1.6 +
     1.7 +Cu.import("resource://gre/modules/FxAccounts.jsm");
     1.8 +Cu.import("resource://services-sync/browserid_identity.js");
     1.9 +Cu.import("resource://services-sync/rest.js");
    1.10 +Cu.import("resource://services-sync/util.js");
    1.11 +Cu.import("resource://services-common/utils.js");
    1.12 +Cu.import("resource://services-crypto/utils.js");
    1.13 +Cu.import("resource://testing-common/services/sync/utils.js");
    1.14 +Cu.import("resource://testing-common/services/sync/fxa_utils.js");
    1.15 +Cu.import("resource://services-common/hawkclient.js");
    1.16 +Cu.import("resource://gre/modules/FxAccounts.jsm");
    1.17 +Cu.import("resource://gre/modules/FxAccountsClient.jsm");
    1.18 +Cu.import("resource://gre/modules/FxAccountsCommon.js");
    1.19 +Cu.import("resource://services-sync/service.js");
    1.20 +Cu.import("resource://services-sync/status.js");
    1.21 +Cu.import("resource://services-sync/constants.js");
    1.22 +
    1.23 +const SECOND_MS = 1000;
    1.24 +const MINUTE_MS = SECOND_MS * 60;
    1.25 +const HOUR_MS = MINUTE_MS * 60;
    1.26 +
    1.27 +let identityConfig = makeIdentityConfig();
    1.28 +let browseridManager = new BrowserIDManager();
    1.29 +configureFxAccountIdentity(browseridManager, identityConfig);
    1.30 +
    1.31 +/**
    1.32 + * Mock client clock and skew vs server in FxAccounts signed-in user module and
    1.33 + * API client.  browserid_identity.js queries these values to construct HAWK
    1.34 + * headers.  We will use this to test clock skew compensation in these headers
    1.35 + * below.
    1.36 + */
    1.37 +let MockFxAccountsClient = function() {
    1.38 +  FxAccountsClient.apply(this);
    1.39 +};
    1.40 +MockFxAccountsClient.prototype = {
    1.41 +  __proto__: FxAccountsClient.prototype
    1.42 +};
    1.43 +
    1.44 +function MockFxAccounts() {
    1.45 +  let fxa = new FxAccounts({
    1.46 +    _now_is: Date.now(),
    1.47 +
    1.48 +    now: function () {
    1.49 +      return this._now_is;
    1.50 +    },
    1.51 +
    1.52 +    fxAccountsClient: new MockFxAccountsClient()
    1.53 +  });
    1.54 +  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
    1.55 +    this.cert = {
    1.56 +      validUntil: fxa.internal.now() + CERT_LIFETIME,
    1.57 +      cert: "certificate",
    1.58 +    };
    1.59 +    return Promise.resolve(this.cert.cert);
    1.60 +  };
    1.61 +  return fxa;
    1.62 +}
    1.63 +
    1.64 +function run_test() {
    1.65 +  initTestLogging("Trace");
    1.66 +  Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace;
    1.67 +  Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace;
    1.68 +  run_next_test();
    1.69 +};
    1.70 +
    1.71 +add_test(function test_initial_state() {
    1.72 +    _("Verify initial state");
    1.73 +    do_check_false(!!browseridManager._token);
    1.74 +    do_check_false(browseridManager.hasValidToken());
    1.75 +    run_next_test();
    1.76 +  }
    1.77 +);
    1.78 +
    1.79 +add_task(function test_initialializeWithCurrentIdentity() {
    1.80 +    _("Verify start after initializeWithCurrentIdentity");
    1.81 +    browseridManager.initializeWithCurrentIdentity();
    1.82 +    yield browseridManager.whenReadyToAuthenticate.promise;
    1.83 +    do_check_true(!!browseridManager._token);
    1.84 +    do_check_true(browseridManager.hasValidToken());
    1.85 +    do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email);
    1.86 +  }
    1.87 +);
    1.88 +
    1.89 +
    1.90 +add_test(function test_getResourceAuthenticator() {
    1.91 +    _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
    1.92 +    let authenticator = browseridManager.getResourceAuthenticator();
    1.93 +    do_check_true(!!authenticator);
    1.94 +    let req = {uri: CommonUtils.makeURI(
    1.95 +      "https://example.net/somewhere/over/the/rainbow"),
    1.96 +               method: 'GET'};
    1.97 +    let output = authenticator(req, 'GET');
    1.98 +    do_check_true('headers' in output);
    1.99 +    do_check_true('authorization' in output.headers);
   1.100 +    do_check_true(output.headers.authorization.startsWith('Hawk'));
   1.101 +    _("Expected internal state after successful call.");
   1.102 +    do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid);
   1.103 +    run_next_test();
   1.104 +  }
   1.105 +);
   1.106 +
   1.107 +add_test(function test_getRESTRequestAuthenticator() {
   1.108 +    _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object.");
   1.109 +    let request = new SyncStorageRequest(
   1.110 +      "https://example.net/somewhere/over/the/rainbow");
   1.111 +    let authenticator = browseridManager.getRESTRequestAuthenticator();
   1.112 +    do_check_true(!!authenticator);
   1.113 +    let output = authenticator(request, 'GET');
   1.114 +    do_check_eq(request.uri, output.uri);
   1.115 +    do_check_true(output._headers.authorization.startsWith('Hawk'));
   1.116 +    do_check_true(output._headers.authorization.contains('nonce'));
   1.117 +    do_check_true(browseridManager.hasValidToken());
   1.118 +    run_next_test();
   1.119 +  }
   1.120 +);
   1.121 +
   1.122 +add_test(function test_resourceAuthenticatorSkew() {
   1.123 +  _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header.");
   1.124 +
   1.125 +  // Clock is skewed 12 hours into the future
   1.126 +  // We pick a date in the past so we don't risk concealing bugs in code that
   1.127 +  // uses new Date() instead of our given date.
   1.128 +  let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
   1.129 +  let browseridManager = new BrowserIDManager();
   1.130 +  let hawkClient = new HawkClient("https://example.net/v1", "/foo");
   1.131 +
   1.132 +  // mock fxa hawk client skew
   1.133 +  hawkClient.now = function() {
   1.134 +    dump("mocked client now: " + now + '\n');
   1.135 +    return now;
   1.136 +  }
   1.137 +  // Imagine there's already been one fxa request and the hawk client has
   1.138 +  // already detected skew vs the fxa auth server.
   1.139 +  let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
   1.140 +  hawkClient._localtimeOffsetMsec = localtimeOffsetMsec;
   1.141 +
   1.142 +  let fxaClient = new MockFxAccountsClient();
   1.143 +  fxaClient.hawk = hawkClient;
   1.144 +
   1.145 +  // Sanity check
   1.146 +  do_check_eq(hawkClient.now(), now);
   1.147 +  do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
   1.148 +
   1.149 +  // Properly picked up by the client
   1.150 +  do_check_eq(fxaClient.now(), now);
   1.151 +  do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
   1.152 +
   1.153 +  let fxa = new MockFxAccounts();
   1.154 +  fxa.internal._now_is = now;
   1.155 +  fxa.internal.fxAccountsClient = fxaClient;
   1.156 +
   1.157 +  // Picked up by the signed-in user module
   1.158 +  do_check_eq(fxa.internal.now(), now);
   1.159 +  do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec);
   1.160 +
   1.161 +  do_check_eq(fxa.now(), now);
   1.162 +  do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec);
   1.163 +
   1.164 +  // Mocks within mocks...
   1.165 +  configureFxAccountIdentity(browseridManager, identityConfig);
   1.166 +
   1.167 +  // Ensure the new FxAccounts mock has a signed-in user.
   1.168 +  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
   1.169 +
   1.170 +  browseridManager._fxaService = fxa;
   1.171 +
   1.172 +  do_check_eq(browseridManager._fxaService.internal.now(), now);
   1.173 +  do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec,
   1.174 +      localtimeOffsetMsec);
   1.175 +
   1.176 +  do_check_eq(browseridManager._fxaService.now(), now);
   1.177 +  do_check_eq(browseridManager._fxaService.localtimeOffsetMsec,
   1.178 +      localtimeOffsetMsec);
   1.179 +
   1.180 +  let request = new SyncStorageRequest("https://example.net/i/like/pie/");
   1.181 +  let authenticator = browseridManager.getResourceAuthenticator();
   1.182 +  let output = authenticator(request, 'GET');
   1.183 +  dump("output" + JSON.stringify(output));
   1.184 +  let authHeader = output.headers.authorization;
   1.185 +  do_check_true(authHeader.startsWith('Hawk'));
   1.186 +
   1.187 +  // Skew correction is applied in the header and we're within the two-minute
   1.188 +  // window.
   1.189 +  do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
   1.190 +  do_check_true(
   1.191 +      (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
   1.192 +
   1.193 +  run_next_test();
   1.194 +});
   1.195 +
   1.196 +add_test(function test_RESTResourceAuthenticatorSkew() {
   1.197 +  _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header.");
   1.198 +
   1.199 +  // Clock is skewed 12 hours into the future from our arbitary date
   1.200 +  let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
   1.201 +  let browseridManager = new BrowserIDManager();
   1.202 +  let hawkClient = new HawkClient("https://example.net/v1", "/foo");
   1.203 +
   1.204 +  // mock fxa hawk client skew
   1.205 +  hawkClient.now = function() {
   1.206 +    return now;
   1.207 +  }
   1.208 +  // Imagine there's already been one fxa request and the hawk client has
   1.209 +  // already detected skew vs the fxa auth server.
   1.210 +  hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;
   1.211 +
   1.212 +  let fxaClient = new MockFxAccountsClient();
   1.213 +  fxaClient.hawk = hawkClient;
   1.214 +  let fxa = new MockFxAccounts();
   1.215 +  fxa.internal._now_is = now;
   1.216 +  fxa.internal.fxAccountsClient = fxaClient;
   1.217 +
   1.218 +  configureFxAccountIdentity(browseridManager, identityConfig);
   1.219 +
   1.220 +  // Ensure the new FxAccounts mock has a signed-in user.
   1.221 +  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
   1.222 +
   1.223 +  browseridManager._fxaService = fxa;
   1.224 +
   1.225 +  do_check_eq(browseridManager._fxaService.internal.now(), now);
   1.226 +
   1.227 +  let request = new SyncStorageRequest("https://example.net/i/like/pie/");
   1.228 +  let authenticator = browseridManager.getResourceAuthenticator();
   1.229 +  let output = authenticator(request, 'GET');
   1.230 +  dump("output" + JSON.stringify(output));
   1.231 +  let authHeader = output.headers.authorization;
   1.232 +  do_check_true(authHeader.startsWith('Hawk'));
   1.233 +
   1.234 +  // Skew correction is applied in the header and we're within the two-minute
   1.235 +  // window.
   1.236 +  do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS);
   1.237 +  do_check_true(
   1.238 +      (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
   1.239 +
   1.240 +  run_next_test();
   1.241 +});
   1.242 +
   1.243 +add_task(function test_ensureLoggedIn() {
   1.244 +  configureFxAccountIdentity(browseridManager);
   1.245 +  yield browseridManager.initializeWithCurrentIdentity();
   1.246 +  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
   1.247 +  yield browseridManager.ensureLoggedIn();
   1.248 +  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
   1.249 +  Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
   1.250 +            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
   1.251 +
   1.252 +  // arrange for no logged in user.
   1.253 +  let fxa = browseridManager._fxaService
   1.254 +  let signedInUser = fxa.internal.currentAccountState.signedInUser;
   1.255 +  fxa.internal.currentAccountState.signedInUser = null;
   1.256 +  browseridManager.initializeWithCurrentIdentity();
   1.257 +  Assert.ok(!browseridManager._shouldHaveSyncKeyBundle,
   1.258 +            "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
   1.259 +  Status.login = LOGIN_FAILED_NO_USERNAME;
   1.260 +  yield Assert_rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user");
   1.261 +  Assert.ok(browseridManager._shouldHaveSyncKeyBundle,
   1.262 +            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
   1.263 +  fxa.internal.currentAccountState.signedInUser = signedInUser;
   1.264 +  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
   1.265 +  yield Assert_rejects(browseridManager.ensureLoggedIn(),
   1.266 +                       "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
   1.267 +  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
   1.268 +               "status should remain LOGIN_FAILED_LOGIN_REJECTED");
   1.269 +  Status.login = LOGIN_FAILED_NETWORK_ERROR;
   1.270 +  yield browseridManager.ensureLoggedIn();
   1.271 +  Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
   1.272 +});
   1.273 +
   1.274 +add_test(function test_tokenExpiration() {
   1.275 +    _("BrowserIDManager notices token expiration:");
   1.276 +    let bimExp = new BrowserIDManager();
   1.277 +    configureFxAccountIdentity(bimExp, identityConfig);
   1.278 +
   1.279 +    let authenticator = bimExp.getResourceAuthenticator();
   1.280 +    do_check_true(!!authenticator);
   1.281 +    let req = {uri: CommonUtils.makeURI(
   1.282 +      "https://example.net/somewhere/over/the/rainbow"),
   1.283 +               method: 'GET'};
   1.284 +    authenticator(req, 'GET');
   1.285 +
   1.286 +    // Mock the clock.
   1.287 +    _("Forcing the token to expire ...");
   1.288 +    Object.defineProperty(bimExp, "_now", {
   1.289 +      value: function customNow() {
   1.290 +        return (Date.now() + 3000001);
   1.291 +      },
   1.292 +      writable: true,
   1.293 +    });
   1.294 +    do_check_true(bimExp._token.expiration < bimExp._now());
   1.295 +    _("... means BrowserIDManager knows to re-fetch it on the next call.");
   1.296 +    do_check_false(bimExp.hasValidToken());
   1.297 +    run_next_test();
   1.298 +  }
   1.299 +);
   1.300 +
   1.301 +add_test(function test_sha256() {
   1.302 +  // Test vectors from http://www.bichlmeier.info/sha256test.html
   1.303 +  let vectors = [
   1.304 +    ["",
   1.305 +     "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
   1.306 +    ["abc",
   1.307 +     "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
   1.308 +    ["message digest",
   1.309 +     "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"],
   1.310 +    ["secure hash algorithm",
   1.311 +     "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"],
   1.312 +    ["SHA256 is considered to be safe",
   1.313 +     "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"],
   1.314 +    ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
   1.315 +     "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"],
   1.316 +    ["For this sample, this 63-byte string will be used as input data",
   1.317 +     "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"],
   1.318 +    ["This is exactly 64 bytes long, not counting the terminating byte",
   1.319 +     "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"]
   1.320 +  ];
   1.321 +  let bidUser = new BrowserIDManager();
   1.322 +  for (let [input,output] of vectors) {
   1.323 +    do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output);
   1.324 +  }
   1.325 +  run_next_test();
   1.326 +});
   1.327 +
   1.328 +add_test(function test_computeXClientStateHeader() {
   1.329 +  let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
   1.330 +  let kB = CommonUtils.hexToBytes(kBhex);
   1.331 +
   1.332 +  let bidUser = new BrowserIDManager();
   1.333 +  let header = bidUser._computeXClientState(kB);
   1.334 +
   1.335 +  do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f");
   1.336 +  run_next_test();
   1.337 +});
   1.338 +
   1.339 +add_task(function test_getTokenErrors() {
   1.340 +  _("BrowserIDManager correctly handles various failures to get a token.");
   1.341 +
   1.342 +  _("Arrange for a 401 - Sync should reflect an auth error.");
   1.343 +  initializeIdentityWithTokenServerResponse({
   1.344 +    status: 401,
   1.345 +    headers: {"content-type": "application/json"},
   1.346 +    body: JSON.stringify({}),
   1.347 +  });
   1.348 +  let browseridManager = Service.identity;
   1.349 +
   1.350 +  yield browseridManager.initializeWithCurrentIdentity();
   1.351 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.352 +                       "should reject due to 401");
   1.353 +  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
   1.354 +
   1.355 +  // XXX - other interesting responses to return?
   1.356 +
   1.357 +  // And for good measure, some totally "unexpected" errors - we generally
   1.358 +  // assume these problems are going to magically go away at some point.
   1.359 +  _("Arrange for an empty body with a 200 response - should reflect a network error.");
   1.360 +  initializeIdentityWithTokenServerResponse({
   1.361 +    status: 200,
   1.362 +    headers: [],
   1.363 +    body: "",
   1.364 +  });
   1.365 +  browseridManager = Service.identity;
   1.366 +  yield browseridManager.initializeWithCurrentIdentity();
   1.367 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.368 +                       "should reject due to non-JSON response");
   1.369 +  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
   1.370 +});
   1.371 +
   1.372 +add_task(function test_getTokenErrorWithRetry() {
   1.373 +  _("tokenserver sends an observer notification on various backoff headers.");
   1.374 +
   1.375 +  // Set Sync's backoffInterval to zero - after we simulated the backoff header
   1.376 +  // it should reflect the value we sent.
   1.377 +  Status.backoffInterval = 0;
   1.378 +  _("Arrange for a 503 with a Retry-After header.");
   1.379 +  initializeIdentityWithTokenServerResponse({
   1.380 +    status: 503,
   1.381 +    headers: {"content-type": "application/json",
   1.382 +              "retry-after": "100"},
   1.383 +    body: JSON.stringify({}),
   1.384 +  });
   1.385 +  let browseridManager = Service.identity;
   1.386 +
   1.387 +  yield browseridManager.initializeWithCurrentIdentity();
   1.388 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.389 +                       "should reject due to 503");
   1.390 +
   1.391 +  // The observer should have fired - check it got the value in the response.
   1.392 +  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
   1.393 +  // Sync will have the value in ms with some slop - so check it is at least that.
   1.394 +  Assert.ok(Status.backoffInterval >= 100000);
   1.395 +
   1.396 +  _("Arrange for a 200 with an X-Backoff header.");
   1.397 +  Status.backoffInterval = 0;
   1.398 +  initializeIdentityWithTokenServerResponse({
   1.399 +    status: 503,
   1.400 +    headers: {"content-type": "application/json",
   1.401 +              "x-backoff": "200"},
   1.402 +    body: JSON.stringify({}),
   1.403 +  });
   1.404 +  browseridManager = Service.identity;
   1.405 +
   1.406 +  yield browseridManager.initializeWithCurrentIdentity();
   1.407 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.408 +                       "should reject due to no token in response");
   1.409 +
   1.410 +  // The observer should have fired - check it got the value in the response.
   1.411 +  Assert.ok(Status.backoffInterval >= 200000);
   1.412 +});
   1.413 +
   1.414 +add_task(function test_getKeysErrorWithBackoff() {
   1.415 +  _("Auth server (via hawk) sends an observer notification on backoff headers.");
   1.416 +
   1.417 +  // Set Sync's backoffInterval to zero - after we simulated the backoff header
   1.418 +  // it should reflect the value we sent.
   1.419 +  Status.backoffInterval = 0;
   1.420 +  _("Arrange for a 503 with a X-Backoff header.");
   1.421 +
   1.422 +  let config = makeIdentityConfig();
   1.423 +  // We want no kA or kB so we attempt to fetch them.
   1.424 +  delete config.fxaccount.user.kA;
   1.425 +  delete config.fxaccount.user.kB;
   1.426 +  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   1.427 +  yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
   1.428 +    Assert.equal(method, "get");
   1.429 +    Assert.equal(uri, "http://mockedserver:9999/account/keys")
   1.430 +    return {
   1.431 +      status: 503,
   1.432 +      headers: {"content-type": "application/json",
   1.433 +                "x-backoff": "100"},
   1.434 +      body: "{}",
   1.435 +    }
   1.436 +  });
   1.437 +
   1.438 +  let browseridManager = Service.identity;
   1.439 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.440 +                       "should reject due to 503");
   1.441 +
   1.442 +  // The observer should have fired - check it got the value in the response.
   1.443 +  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
   1.444 +  // Sync will have the value in ms with some slop - so check it is at least that.
   1.445 +  Assert.ok(Status.backoffInterval >= 100000);
   1.446 +});
   1.447 +
   1.448 +add_task(function test_getKeysErrorWithRetry() {
   1.449 +  _("Auth server (via hawk) sends an observer notification on retry headers.");
   1.450 +
   1.451 +  // Set Sync's backoffInterval to zero - after we simulated the backoff header
   1.452 +  // it should reflect the value we sent.
   1.453 +  Status.backoffInterval = 0;
   1.454 +  _("Arrange for a 503 with a Retry-After header.");
   1.455 +
   1.456 +  let config = makeIdentityConfig();
   1.457 +  // We want no kA or kB so we attempt to fetch them.
   1.458 +  delete config.fxaccount.user.kA;
   1.459 +  delete config.fxaccount.user.kB;
   1.460 +  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   1.461 +  yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
   1.462 +    Assert.equal(method, "get");
   1.463 +    Assert.equal(uri, "http://mockedserver:9999/account/keys")
   1.464 +    return {
   1.465 +      status: 503,
   1.466 +      headers: {"content-type": "application/json",
   1.467 +                "retry-after": "100"},
   1.468 +      body: "{}",
   1.469 +    }
   1.470 +  });
   1.471 +
   1.472 +  let browseridManager = Service.identity;
   1.473 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.474 +                       "should reject due to 503");
   1.475 +
   1.476 +  // The observer should have fired - check it got the value in the response.
   1.477 +  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
   1.478 +  // Sync will have the value in ms with some slop - so check it is at least that.
   1.479 +  Assert.ok(Status.backoffInterval >= 100000);
   1.480 +});
   1.481 +
   1.482 +add_task(function test_getHAWKErrors() {
   1.483 +  _("BrowserIDManager correctly handles various HAWK failures.");
   1.484 +
   1.485 +  _("Arrange for a 401 - Sync should reflect an auth error.");
   1.486 +  let config = makeIdentityConfig();
   1.487 +  yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
   1.488 +    Assert.equal(method, "post");
   1.489 +    Assert.equal(uri, "http://mockedserver:9999/certificate/sign")
   1.490 +    return {
   1.491 +      status: 401,
   1.492 +      headers: {"content-type": "application/json"},
   1.493 +      body: JSON.stringify({}),
   1.494 +    }
   1.495 +  });
   1.496 +  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
   1.497 +
   1.498 +  // XXX - other interesting responses to return?
   1.499 +
   1.500 +  // And for good measure, some totally "unexpected" errors - we generally
   1.501 +  // assume these problems are going to magically go away at some point.
   1.502 +  _("Arrange for an empty body with a 200 response - should reflect a network error.");
   1.503 +  yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
   1.504 +    Assert.equal(method, "post");
   1.505 +    Assert.equal(uri, "http://mockedserver:9999/certificate/sign")
   1.506 +    return {
   1.507 +      status: 200,
   1.508 +      headers: [],
   1.509 +      body: "",
   1.510 +    }
   1.511 +  });
   1.512 +  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
   1.513 +});
   1.514 +
   1.515 +add_task(function test_getGetKeysFailing401() {
   1.516 +  _("BrowserIDManager correctly handles 401 responses fetching keys.");
   1.517 +
   1.518 +  _("Arrange for a 401 - Sync should reflect an auth error.");
   1.519 +  let config = makeIdentityConfig();
   1.520 +  // We want no kA or kB so we attempt to fetch them.
   1.521 +  delete config.fxaccount.user.kA;
   1.522 +  delete config.fxaccount.user.kB;
   1.523 +  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   1.524 +  yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
   1.525 +    Assert.equal(method, "get");
   1.526 +    Assert.equal(uri, "http://mockedserver:9999/account/keys")
   1.527 +    return {
   1.528 +      status: 401,
   1.529 +      headers: {"content-type": "application/json"},
   1.530 +      body: "{}",
   1.531 +    }
   1.532 +  });
   1.533 +  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
   1.534 +});
   1.535 +
   1.536 +add_task(function test_getGetKeysFailing503() {
   1.537 +  _("BrowserIDManager correctly handles 5XX responses fetching keys.");
   1.538 +
   1.539 +  _("Arrange for a 503 - Sync should reflect a network error.");
   1.540 +  let config = makeIdentityConfig();
   1.541 +  // We want no kA or kB so we attempt to fetch them.
   1.542 +  delete config.fxaccount.user.kA;
   1.543 +  delete config.fxaccount.user.kB;
   1.544 +  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
   1.545 +  yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
   1.546 +    Assert.equal(method, "get");
   1.547 +    Assert.equal(uri, "http://mockedserver:9999/account/keys")
   1.548 +    return {
   1.549 +      status: 503,
   1.550 +      headers: {"content-type": "application/json"},
   1.551 +      body: "{}",
   1.552 +    }
   1.553 +  });
   1.554 +  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error");
   1.555 +});
   1.556 +
   1.557 +add_task(function test_getKeysMissing() {
   1.558 +  _("BrowserIDManager correctly handles getKeys succeeding but not returning keys.");
   1.559 +
   1.560 +  let browseridManager = new BrowserIDManager();
   1.561 +  let identityConfig = makeIdentityConfig();
   1.562 +  // our mock identity config already has kA and kB - remove them or we never
   1.563 +  // try and fetch them.
   1.564 +  delete identityConfig.fxaccount.user.kA;
   1.565 +  delete identityConfig.fxaccount.user.kB;
   1.566 +  identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken';
   1.567 +
   1.568 +  configureFxAccountIdentity(browseridManager, identityConfig);
   1.569 +
   1.570 +  // Mock a fxAccounts object that returns no keys
   1.571 +  let fxa = new FxAccounts({
   1.572 +    fetchAndUnwrapKeys: function () {
   1.573 +      return Promise.resolve({});
   1.574 +    },
   1.575 +    fxAccountsClient: new MockFxAccountsClient()
   1.576 +  });
   1.577 +
   1.578 +  // Add a mock to the currentAccountState object.
   1.579 +  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
   1.580 +    this.cert = {
   1.581 +      validUntil: fxa.internal.now() + CERT_LIFETIME,
   1.582 +      cert: "certificate",
   1.583 +    };
   1.584 +    return Promise.resolve(this.cert.cert);
   1.585 +  };
   1.586 +
   1.587 +  // Ensure the new FxAccounts mock has a signed-in user.
   1.588 +  fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
   1.589 +
   1.590 +  browseridManager._fxaService = fxa;
   1.591 +
   1.592 +  yield browseridManager.initializeWithCurrentIdentity();
   1.593 +
   1.594 +  let ex;
   1.595 +  try {
   1.596 +    yield browseridManager.whenReadyToAuthenticate.promise;
   1.597 +  } catch (e) {
   1.598 +    ex = e;
   1.599 +  }
   1.600 +
   1.601 +  Assert.ok(ex.message.indexOf("missing kA or kB") >= 0);
   1.602 +});
   1.603 +
   1.604 +// End of tests
   1.605 +// Utility functions follow
   1.606 +
   1.607 +// Create a new browserid_identity object and initialize it with a
   1.608 +// hawk mock that simulates HTTP responses.
   1.609 +// The callback function will be called each time the mocked hawk server wants
   1.610 +// to make a request.  The result of the callback should be the mock response
   1.611 +// object that will be returned to hawk.
   1.612 +// A token server mock will be used that doesn't hit a server, so we move
   1.613 +// directly to a hawk request.
   1.614 +function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
   1.615 +  // A mock request object.
   1.616 +  function MockRESTRequest(uri, credentials, extra) {
   1.617 +    this._uri = uri;
   1.618 +    this._credentials = credentials;
   1.619 +    this._extra = extra;
   1.620 +  };
   1.621 +  MockRESTRequest.prototype = {
   1.622 +    setHeader: function() {},
   1.623 +    post: function(data, callback) {
   1.624 +      this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra);
   1.625 +      callback.call(this);
   1.626 +    },
   1.627 +    get: function(callback) {
   1.628 +      this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra);
   1.629 +      callback.call(this);
   1.630 +    }
   1.631 +  }
   1.632 +
   1.633 +  // The hawk client.
   1.634 +  function MockedHawkClient() {}
   1.635 +  MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999");
   1.636 +  MockedHawkClient.prototype.constructor = MockedHawkClient;
   1.637 +  MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
   1.638 +    return new MockRESTRequest(uri, credentials, extra);
   1.639 +  }
   1.640 +  // Arrange for the same observerPrefix as FxAccountsClient uses
   1.641 +  MockedHawkClient.prototype.observerPrefix = "FxA:hawk";
   1.642 +
   1.643 +  // tie it all together - configureFxAccountIdentity isn't useful here :(
   1.644 +  let fxaClient = new MockFxAccountsClient();
   1.645 +  fxaClient.hawk = new MockedHawkClient();
   1.646 +  let internal = {
   1.647 +    fxAccountsClient: fxaClient,
   1.648 +  }
   1.649 +  let fxa = new FxAccounts(internal);
   1.650 +  fxa.internal.currentAccountState.signedInUser = {
   1.651 +      accountData: config.fxaccount.user,
   1.652 +  };
   1.653 +
   1.654 +  browseridManager._fxaService = fxa;
   1.655 +  browseridManager._signedInUser = null;
   1.656 +  yield browseridManager.initializeWithCurrentIdentity();
   1.657 +  yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise,
   1.658 +                       "expecting rejection due to hawk error");
   1.659 +}
   1.660 +
   1.661 +
   1.662 +function getTimestamp(hawkAuthHeader) {
   1.663 +  return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
   1.664 +}
   1.665 +
   1.666 +function getTimestampDelta(hawkAuthHeader, now=Date.now()) {
   1.667 +  return Math.abs(getTimestamp(hawkAuthHeader) - now);
   1.668 +}
   1.669 +

mercurial