tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_sync_auth_manager.js (26320B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { AuthenticationError, SyncAuthManager } = ChromeUtils.importESModule(
      5  "resource://services-sync/sync_auth.sys.mjs"
      6 );
      7 const { Resource } = ChromeUtils.importESModule(
      8  "resource://services-sync/resource.sys.mjs"
      9 );
     10 const { initializeIdentityWithTokenServerResponse } =
     11  ChromeUtils.importESModule(
     12    "resource://testing-common/services/sync/fxa_utils.sys.mjs"
     13  );
     14 const { HawkClient } = ChromeUtils.importESModule(
     15  "resource://services-common/hawkclient.sys.mjs"
     16 );
     17 const { FxAccounts } = ChromeUtils.importESModule(
     18  "resource://gre/modules/FxAccounts.sys.mjs"
     19 );
     20 const { FxAccountsClient } = ChromeUtils.importESModule(
     21  "resource://gre/modules/FxAccountsClient.sys.mjs"
     22 );
     23 const { ERRNO_INVALID_AUTH_TOKEN, SCOPE_APP_SYNC } = ChromeUtils.importESModule(
     24  "resource://gre/modules/FxAccountsCommon.sys.mjs"
     25 );
     26 const { Service } = ChromeUtils.importESModule(
     27  "resource://services-sync/service.sys.mjs"
     28 );
     29 const { Status } = ChromeUtils.importESModule(
     30  "resource://services-sync/status.sys.mjs"
     31 );
     32 const { TokenServerClient, TokenServerClientServerError } =
     33  ChromeUtils.importESModule(
     34    "resource://services-common/tokenserverclient.sys.mjs"
     35  );
     36 const { AccountState, ERROR_INVALID_ACCOUNT_STATE } =
     37  ChromeUtils.importESModule("resource://gre/modules/FxAccounts.sys.mjs");
     38 
     39 const SECOND_MS = 1000;
     40 const MINUTE_MS = SECOND_MS * 60;
     41 const HOUR_MS = MINUTE_MS * 60;
     42 
     43 const MOCK_ACCESS_TOKEN =
     44  "e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba";
     45 
     46 var globalIdentityConfig = makeIdentityConfig();
     47 var globalSyncAuthManager = new SyncAuthManager();
     48 configureFxAccountIdentity(globalSyncAuthManager, globalIdentityConfig);
     49 
     50 /**
     51 * Mock client clock and skew vs server in FxAccounts signed-in user module and
     52 * API client.  sync_auth.js queries these values to construct HAWK
     53 * headers.  We will use this to test clock skew compensation in these headers
     54 * below.
     55 */
     56 var MockFxAccountsClient = function () {
     57  FxAccountsClient.apply(this);
     58 };
     59 MockFxAccountsClient.prototype = {
     60  accountStatus() {
     61    return Promise.resolve(true);
     62  },
     63  getScopedKeyData() {
     64    return Promise.resolve({
     65      [SCOPE_APP_SYNC]: {
     66        identifier: SCOPE_APP_SYNC,
     67        keyRotationSecret:
     68          "0000000000000000000000000000000000000000000000000000000000000000",
     69        keyRotationTimestamp: 1234567890123,
     70      },
     71    });
     72  },
     73 };
     74 Object.setPrototypeOf(
     75  MockFxAccountsClient.prototype,
     76  FxAccountsClient.prototype
     77 );
     78 
     79 add_test(function test_initial_state() {
     80  _("Verify initial state");
     81  Assert.ok(!globalSyncAuthManager._token);
     82  Assert.ok(!globalSyncAuthManager._hasValidToken());
     83  run_next_test();
     84 });
     85 
     86 add_task(async function test_initialialize() {
     87  _("Verify start after fetching token");
     88  await globalSyncAuthManager._ensureValidToken();
     89  Assert.ok(!!globalSyncAuthManager._token);
     90  Assert.ok(globalSyncAuthManager._hasValidToken());
     91 });
     92 
     93 add_task(async function test_refreshOAuthTokenOn401() {
     94  _("Refreshes the FXA OAuth token after a 401.");
     95  let getTokenCount = 0;
     96  let syncAuthManager = new SyncAuthManager();
     97  let identityConfig = makeIdentityConfig();
     98  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
     99  configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
    100  syncAuthManager._fxaService._internal.initialize();
    101  syncAuthManager._fxaService.getOAuthToken = () => {
    102    ++getTokenCount;
    103    return Promise.resolve(MOCK_ACCESS_TOKEN);
    104  };
    105 
    106  let didReturn401 = false;
    107  let didReturn200 = false;
    108  let mockTSC = mockTokenServer(() => {
    109    if (getTokenCount <= 1) {
    110      didReturn401 = true;
    111      return {
    112        status: 401,
    113        headers: { "content-type": "application/json" },
    114        body: JSON.stringify({}),
    115      };
    116    }
    117    didReturn200 = true;
    118    return {
    119      status: 200,
    120      headers: { "content-type": "application/json" },
    121      body: JSON.stringify({
    122        id: "id",
    123        key: "key",
    124        api_endpoint: "http://example.com/",
    125        uid: "uid",
    126        duration: 300,
    127      }),
    128    };
    129  });
    130 
    131  syncAuthManager._tokenServerClient = mockTSC;
    132 
    133  await syncAuthManager._ensureValidToken();
    134 
    135  Assert.equal(getTokenCount, 2);
    136  Assert.ok(didReturn401);
    137  Assert.ok(didReturn200);
    138  Assert.ok(syncAuthManager._token);
    139  Assert.ok(syncAuthManager._hasValidToken());
    140 });
    141 
    142 add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
    143  _("Verify sync state with auth error + account deleted");
    144 
    145  var identityConfig = makeIdentityConfig();
    146  var syncAuthManager = new SyncAuthManager();
    147 
    148  // Use the real `getOAuthToken` method that calls
    149  // `mockFxAClient.accessTokenWithSessionToken`.
    150  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
    151  delete fxaInternal.getOAuthToken;
    152 
    153  configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
    154  syncAuthManager._fxaService._internal.initialize();
    155 
    156  let accessTokenWithSessionTokenCalled = false;
    157  let accountStatusCalled = false;
    158  let sessionStatusCalled = false;
    159 
    160  let AuthErrorMockFxAClient = function () {
    161    FxAccountsClient.apply(this);
    162  };
    163  AuthErrorMockFxAClient.prototype = {
    164    accessTokenWithSessionToken() {
    165      accessTokenWithSessionTokenCalled = true;
    166      return Promise.reject({
    167        code: 401,
    168        errno: ERRNO_INVALID_AUTH_TOKEN,
    169      });
    170    },
    171    accountStatus() {
    172      accountStatusCalled = true;
    173      return Promise.resolve(false);
    174    },
    175    sessionStatus() {
    176      sessionStatusCalled = true;
    177      return Promise.resolve(false);
    178    },
    179  };
    180  Object.setPrototypeOf(
    181    AuthErrorMockFxAClient.prototype,
    182    FxAccountsClient.prototype
    183  );
    184 
    185  let mockFxAClient = new AuthErrorMockFxAClient();
    186  syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient;
    187 
    188  await Assert.rejects(
    189    syncAuthManager._ensureValidToken(),
    190    err => {
    191      Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
    192      return true; // expected error
    193    },
    194    "should reject because the account was deleted"
    195  );
    196 
    197  Assert.ok(accessTokenWithSessionTokenCalled);
    198  Assert.ok(sessionStatusCalled);
    199  Assert.ok(accountStatusCalled);
    200  Assert.ok(!syncAuthManager._token);
    201  Assert.ok(!syncAuthManager._hasValidToken());
    202 });
    203 
    204 add_task(async function test_getResourceAuthenticator() {
    205  _(
    206    "SyncAuthManager supplies a Resource Authenticator callback which returns a Hawk header."
    207  );
    208  configureFxAccountIdentity(globalSyncAuthManager);
    209  let authenticator = globalSyncAuthManager.getResourceAuthenticator();
    210  Assert.ok(!!authenticator);
    211  let req = {
    212    uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"),
    213    method: "GET",
    214  };
    215  let output = await authenticator(req, "GET");
    216  Assert.ok("headers" in output);
    217  Assert.ok("authorization" in output.headers);
    218  Assert.ok(output.headers.authorization.startsWith("Hawk"));
    219  _("Expected internal state after successful call.");
    220  Assert.equal(
    221    globalSyncAuthManager._token.uid,
    222    globalIdentityConfig.fxaccount.token.uid
    223  );
    224 });
    225 
    226 add_task(async function test_resourceAuthenticatorSkew() {
    227  _(
    228    "SyncAuthManager Resource Authenticator compensates for clock skew in Hawk header."
    229  );
    230 
    231  // Clock is skewed 12 hours into the future
    232  // We pick a date in the past so we don't risk concealing bugs in code that
    233  // uses new Date() instead of our given date.
    234  let now =
    235    new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
    236  let syncAuthManager = new SyncAuthManager();
    237  let hawkClient = new HawkClient("https://example.net/v1", "/foo");
    238 
    239  // mock fxa hawk client skew
    240  hawkClient.now = function () {
    241    dump("mocked client now: " + now + "\n");
    242    return now;
    243  };
    244  // Imagine there's already been one fxa request and the hawk client has
    245  // already detected skew vs the fxa auth server.
    246  let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
    247  hawkClient._localtimeOffsetMsec = localtimeOffsetMsec;
    248 
    249  let fxaClient = new MockFxAccountsClient();
    250  fxaClient.hawk = hawkClient;
    251 
    252  // Sanity check
    253  Assert.equal(hawkClient.now(), now);
    254  Assert.equal(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
    255 
    256  // Properly picked up by the client
    257  Assert.equal(fxaClient.now(), now);
    258  Assert.equal(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
    259 
    260  let identityConfig = makeIdentityConfig();
    261  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
    262  fxaInternal._now_is = now;
    263  fxaInternal.fxAccountsClient = fxaClient;
    264 
    265  // Mocks within mocks...
    266  configureFxAccountIdentity(
    267    syncAuthManager,
    268    globalIdentityConfig,
    269    fxaInternal
    270  );
    271 
    272  Assert.equal(syncAuthManager._fxaService._internal.now(), now);
    273  Assert.equal(
    274    syncAuthManager._fxaService._internal.localtimeOffsetMsec,
    275    localtimeOffsetMsec
    276  );
    277 
    278  Assert.equal(syncAuthManager._fxaService._internal.now(), now);
    279  Assert.equal(
    280    syncAuthManager._fxaService._internal.localtimeOffsetMsec,
    281    localtimeOffsetMsec
    282  );
    283 
    284  let request = new Resource("https://example.net/i/like/pie/");
    285  let authenticator = syncAuthManager.getResourceAuthenticator();
    286  let output = await authenticator(request, "GET");
    287  dump("output" + JSON.stringify(output));
    288  let authHeader = output.headers.authorization;
    289  Assert.ok(authHeader.startsWith("Hawk"));
    290 
    291  // Skew correction is applied in the header and we're within the two-minute
    292  // window.
    293  Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
    294  Assert.less(getTimestampDelta(authHeader, now) - 12 * HOUR_MS, 2 * MINUTE_MS);
    295 });
    296 
    297 add_task(async function test_RESTResourceAuthenticatorSkew() {
    298  _(
    299    "SyncAuthManager REST Resource Authenticator compensates for clock skew in Hawk header."
    300  );
    301 
    302  // Clock is skewed 12 hours into the future from our arbitary date
    303  let now =
    304    new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
    305  let syncAuthManager = new SyncAuthManager();
    306  let hawkClient = new HawkClient("https://example.net/v1", "/foo");
    307 
    308  // mock fxa hawk client skew
    309  hawkClient.now = function () {
    310    return now;
    311  };
    312  // Imagine there's already been one fxa request and the hawk client has
    313  // already detected skew vs the fxa auth server.
    314  hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;
    315 
    316  let fxaClient = new MockFxAccountsClient();
    317  fxaClient.hawk = hawkClient;
    318 
    319  let identityConfig = makeIdentityConfig();
    320  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
    321  fxaInternal._now_is = now;
    322  fxaInternal.fxAccountsClient = fxaClient;
    323 
    324  configureFxAccountIdentity(
    325    syncAuthManager,
    326    globalIdentityConfig,
    327    fxaInternal
    328  );
    329 
    330  Assert.equal(syncAuthManager._fxaService._internal.now(), now);
    331 
    332  let request = new Resource("https://example.net/i/like/pie/");
    333  let authenticator = syncAuthManager.getResourceAuthenticator();
    334  let output = await authenticator(request, "GET");
    335  dump("output" + JSON.stringify(output));
    336  let authHeader = output.headers.authorization;
    337  Assert.ok(authHeader.startsWith("Hawk"));
    338 
    339  // Skew correction is applied in the header and we're within the two-minute
    340  // window.
    341  Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
    342  Assert.less(getTimestampDelta(authHeader, now) - 12 * HOUR_MS, 2 * MINUTE_MS);
    343 });
    344 
    345 add_task(async function test_ensureLoggedIn() {
    346  configureFxAccountIdentity(globalSyncAuthManager);
    347  await globalSyncAuthManager._ensureValidToken();
    348  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
    349  Assert.ok(globalSyncAuthManager._token);
    350 
    351  // arrange for no logged in user.
    352  let fxa = globalSyncAuthManager._fxaService;
    353  let signedInUser =
    354    fxa._internal.currentAccountState.storageManager.accountData;
    355  fxa._internal.currentAccountState.storageManager.accountData = null;
    356  await Assert.rejects(
    357    globalSyncAuthManager._ensureValidToken(true),
    358    /no user is logged in/,
    359    "expecting rejection due to no user"
    360  );
    361  // Restore the logged in user to what it was.
    362  fxa._internal.currentAccountState.storageManager.accountData = signedInUser;
    363  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
    364  await globalSyncAuthManager._ensureValidToken(true);
    365  Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
    366 });
    367 
    368 add_task(async function test_tokenExpiration() {
    369  _("SyncAuthManager notices token expiration:");
    370  let bimExp = new SyncAuthManager();
    371  configureFxAccountIdentity(bimExp, globalIdentityConfig);
    372 
    373  let authenticator = bimExp.getResourceAuthenticator();
    374  Assert.ok(!!authenticator);
    375  let req = {
    376    uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"),
    377    method: "GET",
    378  };
    379  await authenticator(req, "GET");
    380 
    381  // Mock the clock.
    382  _("Forcing the token to expire ...");
    383  Object.defineProperty(bimExp, "_now", {
    384    value: function customNow() {
    385      return Date.now() + 3000001;
    386    },
    387    writable: true,
    388  });
    389  Assert.less(bimExp._token.expiration, bimExp._now());
    390  _("... means SyncAuthManager knows to re-fetch it on the next call.");
    391  Assert.ok(!bimExp._hasValidToken());
    392 });
    393 
    394 add_task(async function test_getTokenErrors() {
    395  _("SyncAuthManager correctly handles various failures to get a token.");
    396 
    397  _("Arrange for a 401 - Sync should reflect an auth error.");
    398  initializeIdentityWithTokenServerResponse({
    399    status: 401,
    400    headers: { "content-type": "application/json" },
    401    body: JSON.stringify({}),
    402  });
    403  let syncAuthManager = Service.identity;
    404 
    405  await Assert.rejects(
    406    syncAuthManager._ensureValidToken(),
    407    AuthenticationError,
    408    "should reject due to 401"
    409  );
    410  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
    411 
    412  // XXX - other interesting responses to return?
    413 
    414  // And for good measure, some totally "unexpected" errors - we generally
    415  // assume these problems are going to magically go away at some point.
    416  _(
    417    "Arrange for an empty body with a 200 response - should reflect a network error."
    418  );
    419  initializeIdentityWithTokenServerResponse({
    420    status: 200,
    421    headers: [],
    422    body: "",
    423  });
    424  syncAuthManager = Service.identity;
    425  await Assert.rejects(
    426    syncAuthManager._ensureValidToken(),
    427    TokenServerClientServerError,
    428    "should reject due to non-JSON response"
    429  );
    430  Assert.equal(
    431    Status.login,
    432    LOGIN_FAILED_NETWORK_ERROR,
    433    "login state is LOGIN_FAILED_NETWORK_ERROR"
    434  );
    435 });
    436 
    437 add_task(async function test_refreshAccessTokenOn401() {
    438  _("SyncAuthManager refreshes the FXA OAuth access token after a 401.");
    439  var identityConfig = makeIdentityConfig();
    440  var syncAuthManager = new SyncAuthManager();
    441  // Use the real `getOAuthToken` method that calls
    442  // `mockFxAClient.accessTokenWithSessionToken`.
    443  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
    444  delete fxaInternal.getOAuthToken;
    445  configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal);
    446  syncAuthManager._fxaService._internal.initialize();
    447 
    448  let getTokenCount = 0;
    449 
    450  let CheckSignMockFxAClient = function () {
    451    FxAccountsClient.apply(this);
    452  };
    453  CheckSignMockFxAClient.prototype = {
    454    accessTokenWithSessionToken() {
    455      ++getTokenCount;
    456      return Promise.resolve({ access_token: "token" });
    457    },
    458  };
    459  Object.setPrototypeOf(
    460    CheckSignMockFxAClient.prototype,
    461    FxAccountsClient.prototype
    462  );
    463 
    464  let mockFxAClient = new CheckSignMockFxAClient();
    465  syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient;
    466 
    467  let didReturn401 = false;
    468  let didReturn200 = false;
    469  let mockTSC = mockTokenServer(() => {
    470    if (getTokenCount <= 1) {
    471      didReturn401 = true;
    472      return {
    473        status: 401,
    474        headers: { "content-type": "application/json" },
    475        body: JSON.stringify({}),
    476      };
    477    }
    478    didReturn200 = true;
    479    return {
    480      status: 200,
    481      headers: { "content-type": "application/json" },
    482      body: JSON.stringify({
    483        id: "id",
    484        key: "key",
    485        api_endpoint: "http://example.com/",
    486        uid: "uid",
    487        duration: 300,
    488      }),
    489    };
    490  });
    491 
    492  syncAuthManager._tokenServerClient = mockTSC;
    493 
    494  await syncAuthManager._ensureValidToken();
    495 
    496  Assert.equal(getTokenCount, 2);
    497  Assert.ok(didReturn401);
    498  Assert.ok(didReturn200);
    499  Assert.ok(syncAuthManager._token);
    500  Assert.ok(syncAuthManager._hasValidToken());
    501 });
    502 
    503 add_task(async function test_getTokenErrorWithRetry() {
    504  _("tokenserver sends an observer notification on various backoff headers.");
    505 
    506  // Set Sync's backoffInterval to zero - after we simulated the backoff header
    507  // it should reflect the value we sent.
    508  Status.backoffInterval = 0;
    509  _("Arrange for a 503 with a Retry-After header.");
    510  initializeIdentityWithTokenServerResponse({
    511    status: 503,
    512    headers: { "content-type": "application/json", "retry-after": "100" },
    513    body: JSON.stringify({}),
    514  });
    515  let syncAuthManager = Service.identity;
    516 
    517  await Assert.rejects(
    518    syncAuthManager._ensureValidToken(),
    519    TokenServerClientServerError,
    520    "should reject due to 503"
    521  );
    522 
    523  // The observer should have fired - check it got the value in the response.
    524  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
    525  // Sync will have the value in ms with some slop - so check it is at least that.
    526  Assert.greaterOrEqual(Status.backoffInterval, 100000);
    527 
    528  _("Arrange for a 200 with an X-Backoff header.");
    529  Status.backoffInterval = 0;
    530  initializeIdentityWithTokenServerResponse({
    531    status: 503,
    532    headers: { "content-type": "application/json", "x-backoff": "200" },
    533    body: JSON.stringify({}),
    534  });
    535  syncAuthManager = Service.identity;
    536 
    537  await Assert.rejects(
    538    syncAuthManager._ensureValidToken(),
    539    TokenServerClientServerError,
    540    "should reject due to no token in response"
    541  );
    542 
    543  // The observer should have fired - check it got the value in the response.
    544  Assert.greaterOrEqual(Status.backoffInterval, 200000);
    545 });
    546 
    547 add_task(async function test_getKeysErrorWithBackoff() {
    548  _(
    549    "Auth server (via hawk) sends an observer notification on backoff headers."
    550  );
    551 
    552  // Set Sync's backoffInterval to zero - after we simulated the backoff header
    553  // it should reflect the value we sent.
    554  Status.backoffInterval = 0;
    555  _("Arrange for a 503 with a X-Backoff header.");
    556 
    557  let config = makeIdentityConfig();
    558  // We want no scopedKeys so we attempt to fetch them.
    559  delete config.fxaccount.user.scopedKeys;
    560  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
    561  await initializeIdentityWithHAWKResponseFactory(
    562    config,
    563    function (method, data, uri) {
    564      Assert.equal(method, "get");
    565      Assert.equal(uri, "http://mockedserver:9999/account/keys");
    566      return {
    567        status: 503,
    568        headers: { "content-type": "application/json", "x-backoff": "100" },
    569        body: "{}",
    570      };
    571    }
    572  );
    573 
    574  let syncAuthManager = Service.identity;
    575  await Assert.rejects(
    576    syncAuthManager._ensureValidToken(),
    577    TokenServerClientServerError,
    578    "should reject due to 503"
    579  );
    580 
    581  // The observer should have fired - check it got the value in the response.
    582  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
    583  // Sync will have the value in ms with some slop - so check it is at least that.
    584  Assert.greaterOrEqual(Status.backoffInterval, 100000);
    585 });
    586 
    587 add_task(async function test_getKeysErrorWithRetry() {
    588  _("Auth server (via hawk) sends an observer notification on retry headers.");
    589 
    590  // Set Sync's backoffInterval to zero - after we simulated the backoff header
    591  // it should reflect the value we sent.
    592  Status.backoffInterval = 0;
    593  _("Arrange for a 503 with a Retry-After header.");
    594 
    595  let config = makeIdentityConfig();
    596  // We want no scopedKeys so we attempt to fetch them.
    597  delete config.fxaccount.user.scopedKeys;
    598  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
    599  await initializeIdentityWithHAWKResponseFactory(
    600    config,
    601    function (method, data, uri) {
    602      Assert.equal(method, "get");
    603      Assert.equal(uri, "http://mockedserver:9999/account/keys");
    604      return {
    605        status: 503,
    606        headers: { "content-type": "application/json", "retry-after": "100" },
    607        body: "{}",
    608      };
    609    }
    610  );
    611 
    612  let syncAuthManager = Service.identity;
    613  await Assert.rejects(
    614    syncAuthManager._ensureValidToken(),
    615    TokenServerClientServerError,
    616    "should reject due to 503"
    617  );
    618 
    619  // The observer should have fired - check it got the value in the response.
    620  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
    621  // Sync will have the value in ms with some slop - so check it is at least that.
    622  Assert.greaterOrEqual(Status.backoffInterval, 100000);
    623 });
    624 
    625 add_task(async function test_getHAWKErrors() {
    626  _("SyncAuthManager correctly handles various HAWK failures.");
    627 
    628  _("Arrange for a 401 - Sync should reflect an auth error.");
    629  let config = makeIdentityConfig();
    630  await initializeIdentityWithHAWKResponseFactory(
    631    config,
    632    function (method, data, uri) {
    633      if (uri == "http://mockedserver:9999/oauth/token") {
    634        Assert.equal(method, "post");
    635        return {
    636          status: 401,
    637          headers: { "content-type": "application/json" },
    638          body: JSON.stringify({
    639            code: 401,
    640            errno: 110,
    641            error: "invalid token",
    642          }),
    643        };
    644      }
    645      // For any follow-up requests that check account status.
    646      return {
    647        status: 200,
    648        headers: { "content-type": "application/json" },
    649        body: JSON.stringify({}),
    650      };
    651    }
    652  );
    653  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
    654 
    655  // XXX - other interesting responses to return?
    656 
    657  // And for good measure, some totally "unexpected" errors - we generally
    658  // assume these problems are going to magically go away at some point.
    659  _(
    660    "Arrange for an empty body with a 200 response - should reflect a network error."
    661  );
    662  await initializeIdentityWithHAWKResponseFactory(
    663    config,
    664    function (method, data, uri) {
    665      Assert.equal(method, "post");
    666      Assert.equal(uri, "http://mockedserver:9999/oauth/token");
    667      return {
    668        status: 200,
    669        headers: [],
    670        body: "",
    671      };
    672    }
    673  );
    674  Assert.equal(
    675    Status.login,
    676    LOGIN_FAILED_NETWORK_ERROR,
    677    "login state is LOGIN_FAILED_NETWORK_ERROR"
    678  );
    679 });
    680 
    681 // End of tests
    682 // Utility functions follow
    683 
    684 // Create a new sync_auth object and initialize it with a
    685 // hawk mock that simulates HTTP responses.
    686 // The callback function will be called each time the mocked hawk server wants
    687 // to make a request.  The result of the callback should be the mock response
    688 // object that will be returned to hawk.
    689 // A token server mock will be used that doesn't hit a server, so we move
    690 // directly to a hawk request.
    691 async function initializeIdentityWithHAWKResponseFactory(
    692  config,
    693  cbGetResponse
    694 ) {
    695  // A mock request object.
    696  function MockRESTRequest(uri, credentials, extra) {
    697    this._uri = uri;
    698    this._credentials = credentials;
    699    this._extra = extra;
    700  }
    701  MockRESTRequest.prototype = {
    702    setHeader() {},
    703    async post(data) {
    704      this.response = cbGetResponse(
    705        "post",
    706        data,
    707        this._uri,
    708        this._credentials,
    709        this._extra
    710      );
    711      return this.response;
    712    },
    713    async get() {
    714      // Skip /status requests (sync_auth checks if the account still
    715      // exists after an auth error)
    716      if (this._uri.startsWith("http://mockedserver:9999/account/status")) {
    717        this.response = {
    718          status: 200,
    719          headers: { "content-type": "application/json" },
    720          body: JSON.stringify({ exists: true }),
    721        };
    722      } else {
    723        this.response = cbGetResponse(
    724          "get",
    725          null,
    726          this._uri,
    727          this._credentials,
    728          this._extra
    729        );
    730      }
    731      return this.response;
    732    },
    733  };
    734 
    735  // The hawk client.
    736  function MockedHawkClient() {}
    737  MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999");
    738  MockedHawkClient.prototype.constructor = MockedHawkClient;
    739  MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function (
    740    uri,
    741    credentials,
    742    extra
    743  ) {
    744    return new MockRESTRequest(uri, credentials, extra);
    745  };
    746  // Arrange for the same observerPrefix as FxAccountsClient uses
    747  MockedHawkClient.prototype.observerPrefix = "FxA:hawk";
    748 
    749  // tie it all together - configureFxAccountIdentity isn't useful here :(
    750  let fxaClient = new MockFxAccountsClient();
    751  fxaClient.hawk = new MockedHawkClient();
    752  let internal = {
    753    fxAccountsClient: fxaClient,
    754    newAccountState(credentials) {
    755      // We only expect this to be called with null indicating the (mock)
    756      // storage should be read.
    757      if (credentials) {
    758        throw new Error("Not expecting to have credentials passed");
    759      }
    760      let storageManager = new MockFxaStorageManager();
    761      storageManager.initialize(config.fxaccount.user);
    762      return new AccountState(storageManager);
    763    },
    764  };
    765  let fxa = new FxAccounts(internal);
    766 
    767  globalSyncAuthManager._fxaService = fxa;
    768  await Assert.rejects(
    769    globalSyncAuthManager._ensureValidToken(true),
    770    // TODO: Ideally this should have a specific check for an error.
    771    () => true,
    772    "expecting rejection due to hawk error"
    773  );
    774 }
    775 
    776 function getTimestamp(hawkAuthHeader) {
    777  return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
    778 }
    779 
    780 function getTimestampDelta(hawkAuthHeader, now = Date.now()) {
    781  return Math.abs(getTimestamp(hawkAuthHeader) - now);
    782 }
    783 
    784 function mockTokenServer(func) {
    785  let requestLog = Log.repository.getLogger("testing.mock-rest");
    786  if (!requestLog.appenders.length) {
    787    // might as well see what it says :)
    788    requestLog.addAppender(new Log.DumpAppender());
    789    requestLog.level = Log.Level.Trace;
    790  }
    791  function MockRESTRequest() {}
    792  MockRESTRequest.prototype = {
    793    _log: requestLog,
    794    setHeader() {},
    795    async get() {
    796      this.response = func();
    797      return this.response;
    798    },
    799  };
    800  // The mocked TokenServer client which will get the response.
    801  function MockTSC() {}
    802  MockTSC.prototype = new TokenServerClient();
    803  MockTSC.prototype.constructor = MockTSC;
    804  MockTSC.prototype.newRESTRequest = function (url) {
    805    return new MockRESTRequest(url);
    806  };
    807  // Arrange for the same observerPrefix as sync_auth uses.
    808  MockTSC.prototype.observerPrefix = "weave:service";
    809  return new MockTSC();
    810 }