tor-browser

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

test_client.js (29959B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { FxAccountsClient } = ChromeUtils.importESModule(
      7  "resource://gre/modules/FxAccountsClient.sys.mjs"
      8 );
      9 
     10 const FAKE_SESSION_TOKEN =
     11  "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
     12 
     13 // https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys
     14 var ACCOUNT_KEYS = {
     15  keyFetch: h(
     16    // eslint-disable-next-line no-useless-concat
     17    "8081828384858687 88898a8b8c8d8e8f" + "9091929394959697 98999a9b9c9d9e9f"
     18  ),
     19 
     20  response: h(
     21    "ee5c58845c7c9412 b11bbd20920c2fdd" +
     22      "d83c33c9cd2c2de2 d66b222613364636" +
     23      "c2c0f8cfbb7c6304 72c0bd88451342c6" +
     24      "c05b14ce342c5ad4 6ad89e84464c993c" +
     25      "3927d30230157d08 17a077eef4b20d97" +
     26      "6f7a97363faf3f06 4c003ada7d01aa70"
     27  ),
     28 
     29  kA: h(
     30    // eslint-disable-next-line no-useless-concat
     31    "2021222324252627 28292a2b2c2d2e2f" + "3031323334353637 38393a3b3c3d3e3f"
     32  ),
     33 
     34  wrapKB: h(
     35    // eslint-disable-next-line no-useless-concat
     36    "4041424344454647 48494a4b4c4d4e4f" + "5051525354555657 58595a5b5c5d5e5f"
     37  ),
     38 };
     39 
     40 add_task(async function test_authenticated_get_request() {
     41  let message = '{"msg": "Great Success!"}';
     42  let credentials = {
     43    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     44    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     45    algorithm: "sha256",
     46  };
     47  let method = "GET";
     48 
     49  let server = httpd_setup({
     50    "/foo": function (request, response) {
     51      Assert.ok(request.hasHeader("Authorization"));
     52 
     53      response.setStatusLine(request.httpVersion, 200, "OK");
     54      response.bodyOutputStream.write(message, message.length);
     55    },
     56  });
     57 
     58  let client = new FxAccountsClient(server.baseURI);
     59 
     60  let result = await client._request("/foo", method, credentials);
     61  Assert.equal("Great Success!", result.msg);
     62 
     63  await promiseStopServer(server);
     64 });
     65 
     66 add_task(async function test_authenticated_post_request() {
     67  let credentials = {
     68    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     69    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     70    algorithm: "sha256",
     71  };
     72  let method = "POST";
     73 
     74  let server = httpd_setup({
     75    "/foo": function (request, response) {
     76      Assert.ok(request.hasHeader("Authorization"));
     77 
     78      response.setStatusLine(request.httpVersion, 200, "OK");
     79      response.setHeader("Content-Type", "application/json");
     80      response.bodyOutputStream.writeFrom(
     81        request.bodyInputStream,
     82        request.bodyInputStream.available()
     83      );
     84    },
     85  });
     86 
     87  let client = new FxAccountsClient(server.baseURI);
     88 
     89  let result = await client._request("/foo", method, credentials, {
     90    foo: "bar",
     91  });
     92  Assert.equal("bar", result.foo);
     93 
     94  await promiseStopServer(server);
     95 });
     96 
     97 add_task(async function test_500_error() {
     98  let message = "<h1>Ooops!</h1>";
     99  let method = "GET";
    100 
    101  let server = httpd_setup({
    102    "/foo": function (request, response) {
    103      response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
    104      response.bodyOutputStream.write(message, message.length);
    105    },
    106  });
    107 
    108  let client = new FxAccountsClient(server.baseURI);
    109 
    110  try {
    111    await client._request("/foo", method);
    112    do_throw("Expected to catch an exception");
    113  } catch (e) {
    114    Assert.equal(500, e.code);
    115    Assert.equal("Internal Server Error", e.message);
    116  }
    117 
    118  await promiseStopServer(server);
    119 });
    120 
    121 add_task(async function test_backoffError() {
    122  let method = "GET";
    123  let server = httpd_setup({
    124    "/retryDelay": function (request, response) {
    125      response.setHeader("Retry-After", "30");
    126      response.setStatusLine(
    127        request.httpVersion,
    128        429,
    129        "Client has sent too many requests"
    130      );
    131      let message = "<h1>Ooops!</h1>";
    132      response.bodyOutputStream.write(message, message.length);
    133    },
    134    "/duringDelayIShouldNotBeCalled": function (request, response) {
    135      response.setStatusLine(request.httpVersion, 200, "OK");
    136      let jsonMessage = '{"working": "yes"}';
    137      response.bodyOutputStream.write(jsonMessage, jsonMessage.length);
    138    },
    139  });
    140 
    141  let client = new FxAccountsClient(server.baseURI);
    142 
    143  // Retry-After header sets client.backoffError
    144  Assert.equal(client.backoffError, null);
    145  try {
    146    await client._request("/retryDelay", method);
    147  } catch (e) {
    148    Assert.equal(429, e.code);
    149    Assert.equal(30, e.retryAfter);
    150    Assert.notEqual(typeof client.fxaBackoffTimer, "undefined");
    151    Assert.notEqual(client.backoffError, null);
    152  }
    153  // While delay is in effect, client short-circuits any requests
    154  // and re-rejects with previous error.
    155  try {
    156    await client._request("/duringDelayIShouldNotBeCalled", method);
    157    throw new Error("I should not be reached");
    158  } catch (e) {
    159    Assert.equal(e.retryAfter, 30);
    160    Assert.equal(e.message, "Client has sent too many requests");
    161    Assert.notEqual(client.backoffError, null);
    162  }
    163  // Once timer fires, client nulls error out and HTTP calls work again.
    164  client._clearBackoff();
    165  let result = await client._request("/duringDelayIShouldNotBeCalled", method);
    166  Assert.equal(client.backoffError, null);
    167  Assert.equal(result.working, "yes");
    168 
    169  await promiseStopServer(server);
    170 });
    171 
    172 add_task(async function test_signUp() {
    173  let creationMessage_noKey = JSON.stringify({
    174    uid: "uid",
    175    sessionToken: "sessionToken",
    176  });
    177  let creationMessage_withKey = JSON.stringify({
    178    uid: "uid",
    179    sessionToken: "sessionToken",
    180    keyFetchToken: "keyFetchToken",
    181  });
    182  let errorMessage = JSON.stringify({
    183    code: 400,
    184    errno: 101,
    185    error: "account exists",
    186  });
    187  let created = false;
    188 
    189  // Note these strings must be unicode and not already utf-8 encoded.
    190  let unicodeUsername = "andr\xe9@example.org"; // 'andré@example.org'
    191  let unicodePassword = "p\xe4ssw\xf6rd"; // 'pässwörd'
    192  let server = httpd_setup({
    193    "/account/create": function (request, response) {
    194      let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
    195      body = CommonUtils.decodeUTF8(body);
    196      let jsonBody = JSON.parse(body);
    197 
    198      // https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
    199 
    200      if (created) {
    201        // Error trying to create same account a second time
    202        response.setStatusLine(request.httpVersion, 400, "Bad request");
    203        response.bodyOutputStream.write(errorMessage, errorMessage.length);
    204        return;
    205      }
    206 
    207      if (jsonBody.email == unicodeUsername) {
    208        Assert.equal("", request._queryString);
    209        Assert.equal(
    210          jsonBody.authPW,
    211          "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375"
    212        );
    213 
    214        response.setStatusLine(request.httpVersion, 200, "OK");
    215        response.bodyOutputStream.write(
    216          creationMessage_noKey,
    217          creationMessage_noKey.length
    218        );
    219        return;
    220      }
    221 
    222      if (jsonBody.email == "you@example.org") {
    223        Assert.equal("keys=true", request._queryString);
    224        Assert.equal(
    225          jsonBody.authPW,
    226          "e5c1cdfdaa5fcee06142db865b212cc8ba8abee2a27d639d42c139f006cdb930"
    227        );
    228        created = true;
    229 
    230        response.setStatusLine(request.httpVersion, 200, "OK");
    231        response.bodyOutputStream.write(
    232          creationMessage_withKey,
    233          creationMessage_withKey.length
    234        );
    235        return;
    236      }
    237      // just throwing here doesn't make any log noise, so have an assertion
    238      // fail instead.
    239      Assert.ok(false, "unexpected email: " + jsonBody.email);
    240    },
    241  });
    242 
    243  // Try to create an account without retrieving optional keys.
    244  let client = new FxAccountsClient(server.baseURI);
    245  let result = await client.signUp(unicodeUsername, unicodePassword);
    246  Assert.equal("uid", result.uid);
    247  Assert.equal("sessionToken", result.sessionToken);
    248  Assert.equal(undefined, result.keyFetchToken);
    249  Assert.equal(
    250    result.unwrapBKey,
    251    "de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28"
    252  );
    253 
    254  // Try to create an account retrieving optional keys.
    255  result = await client.signUp("you@example.org", "pässwörd", true);
    256  Assert.equal("uid", result.uid);
    257  Assert.equal("sessionToken", result.sessionToken);
    258  Assert.equal("keyFetchToken", result.keyFetchToken);
    259  Assert.equal(
    260    result.unwrapBKey,
    261    "f589225b609e56075d76eb74f771ff9ab18a4dc0e901e131ba8f984c7fb0ca8c"
    262  );
    263 
    264  // Try to create an existing account.  Triggers error path.
    265  try {
    266    result = await client.signUp(unicodeUsername, unicodePassword);
    267    do_throw("Expected to catch an exception");
    268  } catch (expectedError) {
    269    Assert.equal(101, expectedError.errno);
    270  }
    271 
    272  await promiseStopServer(server);
    273 });
    274 
    275 add_task(async function test_signIn() {
    276  let sessionMessage_noKey = JSON.stringify({
    277    sessionToken: FAKE_SESSION_TOKEN,
    278  });
    279  let sessionMessage_withKey = JSON.stringify({
    280    sessionToken: FAKE_SESSION_TOKEN,
    281    keyFetchToken: "keyFetchToken",
    282  });
    283  let errorMessage_notExistent = JSON.stringify({
    284    code: 400,
    285    errno: 102,
    286    error: "doesn't exist",
    287  });
    288  let errorMessage_wrongCap = JSON.stringify({
    289    code: 400,
    290    errno: 120,
    291    error: "Incorrect email case",
    292    email: "you@example.com",
    293  });
    294 
    295  // Note this strings must be unicode and not already utf-8 encoded.
    296  let unicodeUsername = "m\xe9@example.com"; // 'mé@example.com'
    297  let server = httpd_setup({
    298    "/account/login": function (request, response) {
    299      let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
    300      body = CommonUtils.decodeUTF8(body);
    301      let jsonBody = JSON.parse(body);
    302 
    303      if (jsonBody.email == unicodeUsername) {
    304        Assert.equal("", request._queryString);
    305        Assert.equal(
    306          jsonBody.authPW,
    307          "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6"
    308        );
    309        response.setStatusLine(request.httpVersion, 200, "OK");
    310        response.bodyOutputStream.write(
    311          sessionMessage_noKey,
    312          sessionMessage_noKey.length
    313        );
    314      } else if (jsonBody.email == "you@example.com") {
    315        Assert.equal("keys=true", request._queryString);
    316        Assert.equal(
    317          jsonBody.authPW,
    318          "93d20ec50304d496d0707ec20d7e8c89459b6396ec5dd5b9e92809c5e42856c7"
    319        );
    320        response.setStatusLine(request.httpVersion, 200, "OK");
    321        response.bodyOutputStream.write(
    322          sessionMessage_withKey,
    323          sessionMessage_withKey.length
    324        );
    325      } else if (jsonBody.email == "You@example.com") {
    326        // Error trying to sign in with a wrong capitalization
    327        response.setStatusLine(request.httpVersion, 400, "Bad request");
    328        response.bodyOutputStream.write(
    329          errorMessage_wrongCap,
    330          errorMessage_wrongCap.length
    331        );
    332      } else {
    333        // Error trying to sign in to nonexistent account
    334        response.setStatusLine(request.httpVersion, 400, "Bad request");
    335        response.bodyOutputStream.write(
    336          errorMessage_notExistent,
    337          errorMessage_notExistent.length
    338        );
    339      }
    340    },
    341  });
    342 
    343  // Login without retrieving optional keys
    344  let client = new FxAccountsClient(server.baseURI);
    345  let result = await client.signIn(unicodeUsername, "bigsecret");
    346  Assert.equal(FAKE_SESSION_TOKEN, result.sessionToken);
    347  Assert.equal(
    348    result.unwrapBKey,
    349    "c076ec3f4af123a615157154c6e1d0d6293e514fd7b0221e32d50517ecf002b8"
    350  );
    351  Assert.equal(undefined, result.keyFetchToken);
    352 
    353  // Login with retrieving optional keys
    354  result = await client.signIn("you@example.com", "bigsecret", true);
    355  Assert.equal(FAKE_SESSION_TOKEN, result.sessionToken);
    356  Assert.equal(
    357    result.unwrapBKey,
    358    "65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624"
    359  );
    360  Assert.equal("keyFetchToken", result.keyFetchToken);
    361 
    362  // Retry due to wrong email capitalization
    363  result = await client.signIn("You@example.com", "bigsecret", true);
    364  Assert.equal(FAKE_SESSION_TOKEN, result.sessionToken);
    365  Assert.equal(
    366    result.unwrapBKey,
    367    "65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624"
    368  );
    369  Assert.equal("keyFetchToken", result.keyFetchToken);
    370 
    371  // Trigger error path
    372  try {
    373    result = await client.signIn("yøü@bad.example.org", "nofear");
    374    do_throw("Expected to catch an exception");
    375  } catch (expectedError) {
    376    Assert.equal(102, expectedError.errno);
    377  }
    378 
    379  await promiseStopServer(server);
    380 });
    381 
    382 add_task(async function test_signOut() {
    383  let signoutMessage = JSON.stringify({});
    384  let errorMessage = JSON.stringify({
    385    code: 400,
    386    errno: 102,
    387    error: "doesn't exist",
    388  });
    389  let signedOut = false;
    390 
    391  let server = httpd_setup({
    392    "/session/destroy": function (request, response) {
    393      if (!signedOut) {
    394        signedOut = true;
    395        Assert.ok(request.hasHeader("Authorization"));
    396        response.setStatusLine(request.httpVersion, 200, "OK");
    397        response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
    398        return;
    399      }
    400 
    401      // Error trying to sign out of nonexistent account
    402      response.setStatusLine(request.httpVersion, 400, "Bad request");
    403      response.bodyOutputStream.write(errorMessage, errorMessage.length);
    404    },
    405  });
    406 
    407  let client = new FxAccountsClient(server.baseURI);
    408  let result = await client.signOut("FakeSession");
    409  Assert.equal(typeof result, "object");
    410 
    411  // Trigger error path
    412  try {
    413    result = await client.signOut("FakeSession");
    414    do_throw("Expected to catch an exception");
    415  } catch (expectedError) {
    416    Assert.equal(102, expectedError.errno);
    417  }
    418 
    419  await promiseStopServer(server);
    420 });
    421 
    422 add_task(async function test_recoveryEmailStatus() {
    423  let emailStatus = JSON.stringify({ verified: true });
    424  let errorMessage = JSON.stringify({
    425    code: 400,
    426    errno: 102,
    427    error: "doesn't exist",
    428  });
    429  let tries = 0;
    430 
    431  let server = httpd_setup({
    432    "/recovery_email/status": function (request, response) {
    433      Assert.ok(request.hasHeader("Authorization"));
    434      Assert.equal("", request._queryString);
    435 
    436      if (tries === 0) {
    437        tries += 1;
    438        response.setStatusLine(request.httpVersion, 200, "OK");
    439        response.bodyOutputStream.write(emailStatus, emailStatus.length);
    440        return;
    441      }
    442 
    443      // Second call gets an error trying to query a nonexistent account
    444      response.setStatusLine(request.httpVersion, 400, "Bad request");
    445      response.bodyOutputStream.write(errorMessage, errorMessage.length);
    446    },
    447  });
    448 
    449  let client = new FxAccountsClient(server.baseURI);
    450  let result = await client.recoveryEmailStatus(FAKE_SESSION_TOKEN);
    451  Assert.equal(result.verified, true);
    452 
    453  // Trigger error path
    454  try {
    455    result = await client.recoveryEmailStatus("some bogus session");
    456    do_throw("Expected to catch an exception");
    457  } catch (expectedError) {
    458    Assert.equal(102, expectedError.errno);
    459  }
    460 
    461  await promiseStopServer(server);
    462 });
    463 
    464 add_task(async function test_recoveryEmailStatusWithReason() {
    465  let emailStatus = JSON.stringify({ verified: true });
    466  let server = httpd_setup({
    467    "/recovery_email/status": function (request, response) {
    468      Assert.ok(request.hasHeader("Authorization"));
    469      // if there is a query string then it will have a reason
    470      Assert.equal("reason=push", request._queryString);
    471 
    472      response.setStatusLine(request.httpVersion, 200, "OK");
    473      response.bodyOutputStream.write(emailStatus, emailStatus.length);
    474    },
    475  });
    476 
    477  let client = new FxAccountsClient(server.baseURI);
    478  let result = await client.recoveryEmailStatus(FAKE_SESSION_TOKEN, {
    479    reason: "push",
    480  });
    481  Assert.equal(result.verified, true);
    482  await promiseStopServer(server);
    483 });
    484 
    485 add_task(async function test_resendVerificationEmail() {
    486  let emptyMessage = "{}";
    487  let errorMessage = JSON.stringify({
    488    code: 400,
    489    errno: 102,
    490    error: "doesn't exist",
    491  });
    492  let tries = 0;
    493 
    494  let server = httpd_setup({
    495    "/recovery_email/resend_code": function (request, response) {
    496      Assert.ok(request.hasHeader("Authorization"));
    497      if (tries === 0) {
    498        tries += 1;
    499        response.setStatusLine(request.httpVersion, 200, "OK");
    500        response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
    501        return;
    502      }
    503 
    504      // Second call gets an error trying to query a nonexistent account
    505      response.setStatusLine(request.httpVersion, 400, "Bad request");
    506      response.bodyOutputStream.write(errorMessage, errorMessage.length);
    507    },
    508  });
    509 
    510  let client = new FxAccountsClient(server.baseURI);
    511  let result = await client.resendVerificationEmail(FAKE_SESSION_TOKEN);
    512  Assert.equal(JSON.stringify(result), emptyMessage);
    513 
    514  // Trigger error path
    515  try {
    516    result = await client.resendVerificationEmail("some bogus session");
    517    do_throw("Expected to catch an exception");
    518  } catch (expectedError) {
    519    Assert.equal(102, expectedError.errno);
    520  }
    521 
    522  await promiseStopServer(server);
    523 });
    524 
    525 add_task(async function test_accountKeys() {
    526  // Four calls to accountKeys().  The first one should work correctly, and we
    527  // should get a valid bundle back, in exchange for our keyFetch token, from
    528  // which we correctly derive kA and wrapKB.  The subsequent three calls
    529  // should all trigger separate error paths.
    530  let responseMessage = JSON.stringify({ bundle: ACCOUNT_KEYS.response });
    531  let errorMessage = JSON.stringify({
    532    code: 400,
    533    errno: 102,
    534    error: "doesn't exist",
    535  });
    536  let emptyMessage = "{}";
    537  let attempt = 0;
    538 
    539  let server = httpd_setup({
    540    "/account/keys": function (request, response) {
    541      Assert.ok(request.hasHeader("Authorization"));
    542      attempt += 1;
    543 
    544      switch (attempt) {
    545        case 1:
    546          // First time succeeds
    547          response.setStatusLine(request.httpVersion, 200, "OK");
    548          response.bodyOutputStream.write(
    549            responseMessage,
    550            responseMessage.length
    551          );
    552          break;
    553 
    554        case 2:
    555          // Second time, return no bundle to trigger client error
    556          response.setStatusLine(request.httpVersion, 200, "OK");
    557          response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
    558          break;
    559 
    560        case 3: {
    561          // Return gibberish to trigger client MAC error
    562          // Tweak a byte
    563          let garbageResponse = JSON.stringify({
    564            bundle: ACCOUNT_KEYS.response.slice(0, -1) + "1",
    565          });
    566          response.setStatusLine(request.httpVersion, 200, "OK");
    567          response.bodyOutputStream.write(
    568            garbageResponse,
    569            garbageResponse.length
    570          );
    571          break;
    572        }
    573 
    574        case 4:
    575          // Trigger error for nonexistent account
    576          response.setStatusLine(request.httpVersion, 400, "Bad request");
    577          response.bodyOutputStream.write(errorMessage, errorMessage.length);
    578          break;
    579      }
    580    },
    581  });
    582 
    583  let client = new FxAccountsClient(server.baseURI);
    584 
    585  // First try, all should be good
    586  let result = await client.accountKeys(ACCOUNT_KEYS.keyFetch);
    587  Assert.equal(CommonUtils.hexToBytes(ACCOUNT_KEYS.kA), result.kA);
    588  Assert.equal(CommonUtils.hexToBytes(ACCOUNT_KEYS.wrapKB), result.wrapKB);
    589 
    590  // Second try, empty bundle should trigger error
    591  try {
    592    result = await client.accountKeys(ACCOUNT_KEYS.keyFetch);
    593    do_throw("Expected to catch an exception");
    594  } catch (expectedError) {
    595    Assert.equal(expectedError.message, "failed to retrieve keys");
    596  }
    597 
    598  // Third try, bad bundle results in MAC error
    599  try {
    600    result = await client.accountKeys(ACCOUNT_KEYS.keyFetch);
    601    do_throw("Expected to catch an exception");
    602  } catch (expectedError) {
    603    Assert.equal(expectedError.message, "error unbundling encryption keys");
    604  }
    605 
    606  // Fourth try, pretend account doesn't exist
    607  try {
    608    result = await client.accountKeys(ACCOUNT_KEYS.keyFetch);
    609    do_throw("Expected to catch an exception");
    610  } catch (expectedError) {
    611    Assert.equal(102, expectedError.errno);
    612  }
    613 
    614  await promiseStopServer(server);
    615 });
    616 
    617 add_task(async function test_accessTokenWithSessionToken() {
    618  let server = httpd_setup({
    619    "/oauth/token": function (request, response) {
    620      const responseMessage = JSON.stringify({
    621        access_token:
    622          "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69",
    623        token_type: "bearer",
    624        scope: SCOPE_APP_SYNC,
    625        expires_in: 21600,
    626        auth_at: 1589579900,
    627      });
    628 
    629      response.setStatusLine(request.httpVersion, 200, "OK");
    630      response.bodyOutputStream.write(responseMessage, responseMessage.length);
    631    },
    632  });
    633 
    634  let client = new FxAccountsClient(server.baseURI);
    635  let sessionTokenHex =
    636    "0599c36ebb5cad6feb9285b9547b65342b5434d55c07b33bffd4307ab8f82dc4";
    637  let clientId = "5882386c6d801776";
    638  let scope = SCOPE_APP_SYNC;
    639  let ttl = 100;
    640  let result = await client.accessTokenWithSessionToken(
    641    sessionTokenHex,
    642    clientId,
    643    scope,
    644    ttl
    645  );
    646  Assert.ok(result);
    647 
    648  await promiseStopServer(server);
    649 });
    650 
    651 add_task(async function test_accountExists() {
    652  let existsMessage = JSON.stringify({
    653    error: "wrong password",
    654    code: 400,
    655    errno: 103,
    656  });
    657  let doesntExistMessage = JSON.stringify({
    658    error: "no such account",
    659    code: 400,
    660    errno: 102,
    661  });
    662  let emptyMessage = "{}";
    663 
    664  let server = httpd_setup({
    665    "/account/login": function (request, response) {
    666      let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
    667      let jsonBody = JSON.parse(body);
    668 
    669      switch (jsonBody.email) {
    670        // We'll test that these users' accounts exist
    671        case "i.exist@example.com":
    672        case "i.also.exist@example.com":
    673          response.setStatusLine(request.httpVersion, 400, "Bad request");
    674          response.bodyOutputStream.write(existsMessage, existsMessage.length);
    675          break;
    676 
    677        // This user's account doesn't exist
    678        case "i.dont.exist@example.com":
    679          response.setStatusLine(request.httpVersion, 400, "Bad request");
    680          response.bodyOutputStream.write(
    681            doesntExistMessage,
    682            doesntExistMessage.length
    683          );
    684          break;
    685 
    686        // This user throws an unexpected response
    687        // This will reject the client signIn promise
    688        case "i.break.things@example.com":
    689          response.setStatusLine(request.httpVersion, 500, "Alas");
    690          response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
    691          break;
    692 
    693        default:
    694          throw new Error("Unexpected login from " + jsonBody.email);
    695      }
    696    },
    697  });
    698 
    699  let client = new FxAccountsClient(server.baseURI);
    700  let result;
    701 
    702  result = await client.accountExists("i.exist@example.com");
    703  Assert.ok(result);
    704 
    705  result = await client.accountExists("i.also.exist@example.com");
    706  Assert.ok(result);
    707 
    708  result = await client.accountExists("i.dont.exist@example.com");
    709  Assert.ok(!result);
    710 
    711  try {
    712    result = await client.accountExists("i.break.things@example.com");
    713    do_throw("Expected to catch an exception");
    714  } catch (unexpectedError) {
    715    Assert.equal(unexpectedError.code, 500);
    716  }
    717 
    718  await promiseStopServer(server);
    719 });
    720 
    721 add_task(async function test_registerDevice() {
    722  const DEVICE_ID = "device id";
    723  const DEVICE_NAME = "device name";
    724  const DEVICE_TYPE = "device type";
    725  const ERROR_NAME = "test that the client promise rejects";
    726 
    727  const server = httpd_setup({
    728    "/account/device": function (request, response) {
    729      const body = JSON.parse(
    730        CommonUtils.readBytesFromInputStream(request.bodyInputStream)
    731      );
    732 
    733      if (
    734        body.id ||
    735        !body.name ||
    736        !body.type ||
    737        Object.keys(body).length !== 2
    738      ) {
    739        response.setStatusLine(request.httpVersion, 400, "Invalid request");
    740        response.bodyOutputStream.write("{}", 2);
    741        return;
    742      }
    743 
    744      if (body.name === ERROR_NAME) {
    745        response.setStatusLine(request.httpVersion, 500, "Alas");
    746        response.bodyOutputStream.write("{}", 2);
    747        return;
    748      }
    749 
    750      body.id = DEVICE_ID;
    751      body.createdAt = Date.now();
    752 
    753      const responseMessage = JSON.stringify(body);
    754 
    755      response.setStatusLine(request.httpVersion, 200, "OK");
    756      response.bodyOutputStream.write(responseMessage, responseMessage.length);
    757    },
    758  });
    759 
    760  const client = new FxAccountsClient(server.baseURI);
    761  const result = await client.registerDevice(
    762    FAKE_SESSION_TOKEN,
    763    DEVICE_NAME,
    764    DEVICE_TYPE
    765  );
    766 
    767  Assert.ok(result);
    768  Assert.equal(Object.keys(result).length, 4);
    769  Assert.equal(result.id, DEVICE_ID);
    770  Assert.equal(typeof result.createdAt, "number");
    771  Assert.greater(result.createdAt, 0);
    772  Assert.equal(result.name, DEVICE_NAME);
    773  Assert.equal(result.type, DEVICE_TYPE);
    774 
    775  try {
    776    await client.registerDevice(FAKE_SESSION_TOKEN, ERROR_NAME, DEVICE_TYPE);
    777    do_throw("Expected to catch an exception");
    778  } catch (unexpectedError) {
    779    Assert.equal(unexpectedError.code, 500);
    780  }
    781 
    782  await promiseStopServer(server);
    783 });
    784 
    785 add_task(async function test_updateDevice() {
    786  const DEVICE_ID = "some other id";
    787  const DEVICE_NAME = "some other name";
    788  const ERROR_ID = "test that the client promise rejects";
    789 
    790  const server = httpd_setup({
    791    "/account/device": function (request, response) {
    792      const body = JSON.parse(
    793        CommonUtils.readBytesFromInputStream(request.bodyInputStream)
    794      );
    795 
    796      if (
    797        !body.id ||
    798        !body.name ||
    799        body.type ||
    800        Object.keys(body).length !== 2
    801      ) {
    802        response.setStatusLine(request.httpVersion, 400, "Invalid request");
    803        response.bodyOutputStream.write("{}", 2);
    804        return;
    805      }
    806 
    807      if (body.id === ERROR_ID) {
    808        response.setStatusLine(request.httpVersion, 500, "Alas");
    809        response.bodyOutputStream.write("{}", 2);
    810        return;
    811      }
    812 
    813      const responseMessage = JSON.stringify(body);
    814 
    815      response.setStatusLine(request.httpVersion, 200, "OK");
    816      response.bodyOutputStream.write(responseMessage, responseMessage.length);
    817    },
    818  });
    819 
    820  const client = new FxAccountsClient(server.baseURI);
    821  const result = await client.updateDevice(
    822    FAKE_SESSION_TOKEN,
    823    DEVICE_ID,
    824    DEVICE_NAME
    825  );
    826 
    827  Assert.ok(result);
    828  Assert.equal(Object.keys(result).length, 2);
    829  Assert.equal(result.id, DEVICE_ID);
    830  Assert.equal(result.name, DEVICE_NAME);
    831 
    832  try {
    833    await client.updateDevice(FAKE_SESSION_TOKEN, ERROR_ID, DEVICE_NAME);
    834    do_throw("Expected to catch an exception");
    835  } catch (unexpectedError) {
    836    Assert.equal(unexpectedError.code, 500);
    837  }
    838 
    839  await promiseStopServer(server);
    840 });
    841 
    842 add_task(async function test_getDeviceList() {
    843  let canReturnDevices;
    844 
    845  const server = httpd_setup({
    846    "/account/devices": function (request, response) {
    847      if (canReturnDevices) {
    848        response.setStatusLine(request.httpVersion, 200, "OK");
    849        response.bodyOutputStream.write("[]", 2);
    850      } else {
    851        response.setStatusLine(request.httpVersion, 500, "Alas");
    852        response.bodyOutputStream.write("{}", 2);
    853      }
    854    },
    855  });
    856 
    857  const client = new FxAccountsClient(server.baseURI);
    858 
    859  canReturnDevices = true;
    860  const result = await client.getDeviceList(FAKE_SESSION_TOKEN);
    861  Assert.ok(Array.isArray(result));
    862  Assert.equal(result.length, 0);
    863 
    864  try {
    865    canReturnDevices = false;
    866    await client.getDeviceList(FAKE_SESSION_TOKEN);
    867    do_throw("Expected to catch an exception");
    868  } catch (unexpectedError) {
    869    Assert.equal(unexpectedError.code, 500);
    870  }
    871 
    872  await promiseStopServer(server);
    873 });
    874 
    875 add_task(async function test_client_metrics() {
    876  function writeResp(response, msg) {
    877    if (typeof msg === "object") {
    878      msg = JSON.stringify(msg);
    879    }
    880    response.bodyOutputStream.write(msg, msg.length);
    881  }
    882 
    883  let server = httpd_setup({
    884    "/session/destroy": function (request, response) {
    885      response.setHeader("Content-Type", "application/json; charset=utf-8");
    886      response.setStatusLine(request.httpVersion, 401, "Unauthorized");
    887      writeResp(response, {
    888        error: "invalid authentication timestamp",
    889        code: 401,
    890        errno: 111,
    891      });
    892    },
    893  });
    894 
    895  let client = new FxAccountsClient(server.baseURI);
    896 
    897  await Assert.rejects(
    898    client.signOut(FAKE_SESSION_TOKEN, {
    899      service: "sync",
    900    }),
    901    function (err) {
    902      return err.errno == 111;
    903    }
    904  );
    905 
    906  await promiseStopServer(server);
    907 });
    908 
    909 add_task(async function test_email_case() {
    910  let canonicalEmail = "greta.garbo@gmail.com";
    911  let clientEmail = "Greta.Garbo@gmail.COM";
    912  let attempts = 0;
    913 
    914  function writeResp(response, msg) {
    915    if (typeof msg === "object") {
    916      msg = JSON.stringify(msg);
    917    }
    918    response.bodyOutputStream.write(msg, msg.length);
    919  }
    920 
    921  let server = httpd_setup({
    922    "/account/login": function (request, response) {
    923      response.setHeader("Content-Type", "application/json; charset=utf-8");
    924      attempts += 1;
    925      if (attempts > 2) {
    926        response.setStatusLine(
    927          request.httpVersion,
    928          429,
    929          "Sorry, you had your chance"
    930        );
    931        return writeResp(response, "");
    932      }
    933 
    934      let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
    935      let jsonBody = JSON.parse(body);
    936      let email = jsonBody.email;
    937 
    938      // If the client has the wrong case on the email, we return a 400, with
    939      // the capitalization of the email as saved in the accounts database.
    940      if (email == canonicalEmail) {
    941        response.setStatusLine(request.httpVersion, 200, "Yay");
    942        return writeResp(response, { areWeHappy: "yes" });
    943      }
    944 
    945      response.setStatusLine(request.httpVersion, 400, "Incorrect email case");
    946      return writeResp(response, {
    947        code: 400,
    948        errno: 120,
    949        error: "Incorrect email case",
    950        email: canonicalEmail,
    951      });
    952    },
    953  });
    954 
    955  let client = new FxAccountsClient(server.baseURI);
    956 
    957  let result = await client.signIn(clientEmail, "123456");
    958  Assert.equal(result.areWeHappy, "yes");
    959  Assert.equal(attempts, 2);
    960 
    961  await promiseStopServer(server);
    962 });
    963 
    964 // turn formatted test vectors into normal hex strings
    965 function h(hexStr) {
    966  return hexStr.replace(/\s+/g, "");
    967 }