tor-browser

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

test_profile_client.js (12496B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const {
      7  ERRNO_NETWORK,
      8  ERRNO_PARSE,
      9  ERRNO_UNKNOWN_ERROR,
     10  ERROR_CODE_METHOD_NOT_ALLOWED,
     11  ERROR_MSG_METHOD_NOT_ALLOWED,
     12  ERROR_NETWORK,
     13  ERROR_PARSE,
     14  ERROR_UNKNOWN,
     15 } = ChromeUtils.importESModule(
     16  "resource://gre/modules/FxAccountsCommon.sys.mjs"
     17 );
     18 const { FxAccountsProfileClient, FxAccountsProfileClientError } =
     19  ChromeUtils.importESModule(
     20    "resource://gre/modules/FxAccountsProfileClient.sys.mjs"
     21  );
     22 
     23 const STATUS_SUCCESS = 200;
     24 
     25 /**
     26 * Mock request responder
     27 *
     28 * @param {string} response
     29 *        Mocked raw response from the server
     30 * @returns {Function}
     31 */
     32 let mockResponse = function (response) {
     33  let Request = function (requestUri) {
     34    // Store the request uri so tests can inspect it
     35    Request._requestUri = requestUri;
     36    Request.ifNoneMatchSet = false;
     37    return {
     38      setHeader(header, value) {
     39        if (header == "If-None-Match" && value == "bogusETag") {
     40          Request.ifNoneMatchSet = true;
     41        }
     42      },
     43      async dispatch() {
     44        this.response = response;
     45        return this.response;
     46      },
     47    };
     48  };
     49 
     50  return Request;
     51 };
     52 
     53 // A simple mock FxA that hands out tokens without checking them and doesn't
     54 // expect tokens to be revoked. We have specific token tests further down that
     55 // has more checks here.
     56 let mockFxaInternal = {
     57  getOAuthToken(options) {
     58    Assert.equal(options.scope, "profile");
     59    return "token";
     60  },
     61 };
     62 
     63 const PROFILE_OPTIONS = {
     64  serverURL: "http://127.0.0.1:1111/v1",
     65  fxai: mockFxaInternal,
     66 };
     67 
     68 /**
     69 * Mock request error responder
     70 *
     71 * @param {Error} error
     72 *        Error object
     73 * @returns {Function}
     74 */
     75 let mockResponseError = function (error) {
     76  return function () {
     77    return {
     78      setHeader() {},
     79      async dispatch() {
     80        throw error;
     81      },
     82    };
     83  };
     84 };
     85 
     86 add_test(function successfulResponse() {
     87  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
     88  let response = {
     89    success: true,
     90    status: STATUS_SUCCESS,
     91    headers: { etag: "bogusETag" },
     92    body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}',
     93  };
     94 
     95  client._Request = new mockResponse(response);
     96  client.fetchProfile().then(function (result) {
     97    Assert.equal(
     98      client._Request._requestUri,
     99      "http://127.0.0.1:1111/v1/profile"
    100    );
    101    Assert.equal(result.body.email, "someone@restmail.net");
    102    Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a");
    103    Assert.equal(result.etag, "bogusETag");
    104    run_next_test();
    105  });
    106 });
    107 
    108 add_test(function setsIfNoneMatchETagHeader() {
    109  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
    110  let response = {
    111    success: true,
    112    status: STATUS_SUCCESS,
    113    headers: {},
    114    body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}',
    115  };
    116 
    117  let req = new mockResponse(response);
    118  client._Request = req;
    119  client.fetchProfile("bogusETag").then(function (result) {
    120    Assert.equal(
    121      client._Request._requestUri,
    122      "http://127.0.0.1:1111/v1/profile"
    123    );
    124    Assert.equal(result.body.email, "someone@restmail.net");
    125    Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a");
    126    Assert.ok(req.ifNoneMatchSet);
    127    run_next_test();
    128  });
    129 });
    130 
    131 add_test(function successful304Response() {
    132  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
    133  let response = {
    134    success: true,
    135    headers: { etag: "bogusETag" },
    136    status: 304,
    137  };
    138 
    139  client._Request = new mockResponse(response);
    140  client.fetchProfile().then(function (result) {
    141    Assert.equal(result, null);
    142    run_next_test();
    143  });
    144 });
    145 
    146 add_test(function parseErrorResponse() {
    147  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
    148  let response = {
    149    success: true,
    150    status: STATUS_SUCCESS,
    151    body: "unexpected",
    152  };
    153 
    154  client._Request = new mockResponse(response);
    155  client.fetchProfile().catch(function (e) {
    156    Assert.equal(e.name, "FxAccountsProfileClientError");
    157    Assert.equal(e.code, STATUS_SUCCESS);
    158    Assert.equal(e.errno, ERRNO_PARSE);
    159    Assert.equal(e.error, ERROR_PARSE);
    160    Assert.equal(e.message, "unexpected");
    161    run_next_test();
    162  });
    163 });
    164 
    165 add_test(function serverErrorResponse() {
    166  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
    167  let response = {
    168    status: 500,
    169    body: '{ "code": 500, "errno": 100, "error": "Bad Request", "message": "Something went wrong", "reason": "Because the internet" }',
    170  };
    171 
    172  client._Request = new mockResponse(response);
    173  client.fetchProfile().catch(function (e) {
    174    Assert.equal(e.name, "FxAccountsProfileClientError");
    175    Assert.equal(e.code, 500);
    176    Assert.equal(e.errno, 100);
    177    Assert.equal(e.error, "Bad Request");
    178    Assert.equal(e.message, "Something went wrong");
    179    run_next_test();
    180  });
    181 });
    182 
    183 // Test that we get a token, then if we get a 401 we revoke it, get a new one
    184 // and retry.
    185 add_test(function server401ResponseThenSuccess() {
    186  // The last token we handed out.
    187  let lastToken = -1;
    188  // The number of times our removeCachedOAuthToken function was called.
    189  let numTokensRemoved = 0;
    190 
    191  let mockFxaWithRemove = {
    192    getOAuthToken(options) {
    193      Assert.equal(options.scope, "profile");
    194      return "" + ++lastToken; // tokens are strings.
    195    },
    196    removeCachedOAuthToken(options) {
    197      // This test never has more than 1 token alive at once, so the token
    198      // being revoked must always be the last token we handed out.
    199      Assert.equal(parseInt(options.token), lastToken);
    200      ++numTokensRemoved;
    201    },
    202  };
    203  let profileOptions = {
    204    serverURL: "http://127.0.0.1:1111/v1",
    205    fxai: mockFxaWithRemove,
    206  };
    207  let client = new FxAccountsProfileClient(profileOptions);
    208 
    209  // 2 responses - first one implying the token has expired, second works.
    210  let responses = [
    211    {
    212      status: 401,
    213      body: '{ "code": 401, "errno": 100, "error": "Token expired", "message": "That token is too old", "reason": "Because security" }',
    214    },
    215    {
    216      success: true,
    217      status: STATUS_SUCCESS,
    218      headers: {},
    219      body: '{"avatar":"http://example.com/image.jpg","id":"0d5c1a89b8c54580b8e3e8adadae864a"}',
    220    },
    221  ];
    222 
    223  let numRequests = 0;
    224  let numAuthHeaders = 0;
    225  // Like mockResponse but we want access to headers etc.
    226  client._Request = function () {
    227    return {
    228      setHeader(name, value) {
    229        if (name == "Authorization") {
    230          numAuthHeaders++;
    231          Assert.equal(value, "Bearer " + lastToken);
    232        }
    233      },
    234      async dispatch() {
    235        this.response = responses[numRequests];
    236        ++numRequests;
    237        return this.response;
    238      },
    239    };
    240  };
    241 
    242  client.fetchProfile().then(result => {
    243    Assert.equal(result.body.avatar, "http://example.com/image.jpg");
    244    Assert.equal(result.body.id, "0d5c1a89b8c54580b8e3e8adadae864a");
    245    // should have been exactly 2 requests and exactly 2 auth headers.
    246    Assert.equal(numRequests, 2);
    247    Assert.equal(numAuthHeaders, 2);
    248    // and we should have seen one token revoked.
    249    Assert.equal(numTokensRemoved, 1);
    250 
    251    run_next_test();
    252  });
    253 });
    254 
    255 // Test that we get a token, then if we get a 401 we revoke it, get a new one
    256 // and retry - but we *still* get a 401 on the retry, so the caller sees that.
    257 add_test(function server401ResponsePersists() {
    258  // The last token we handed out.
    259  let lastToken = -1;
    260  // The number of times our removeCachedOAuthToken function was called.
    261  let numTokensRemoved = 0;
    262 
    263  let mockFxaWithRemove = {
    264    getOAuthToken(options) {
    265      Assert.equal(options.scope, "profile");
    266      return "" + ++lastToken; // tokens are strings.
    267    },
    268    removeCachedOAuthToken(options) {
    269      // This test never has more than 1 token alive at once, so the token
    270      // being revoked must always be the last token we handed out.
    271      Assert.equal(parseInt(options.token), lastToken);
    272      ++numTokensRemoved;
    273    },
    274  };
    275  let profileOptions = {
    276    serverURL: "http://127.0.0.1:1111/v1",
    277    fxai: mockFxaWithRemove,
    278  };
    279  let client = new FxAccountsProfileClient(profileOptions);
    280 
    281  let response = {
    282    status: 401,
    283    body: '{ "code": 401, "errno": 100, "error": "It\'s not your token, it\'s you!", "message": "I don\'t like you", "reason": "Because security" }',
    284  };
    285 
    286  let numRequests = 0;
    287  let numAuthHeaders = 0;
    288  client._Request = function () {
    289    return {
    290      setHeader(name, value) {
    291        if (name == "Authorization") {
    292          numAuthHeaders++;
    293          Assert.equal(value, "Bearer " + lastToken);
    294        }
    295      },
    296      async dispatch() {
    297        this.response = response;
    298        ++numRequests;
    299        return this.response;
    300      },
    301    };
    302  };
    303 
    304  client.fetchProfile().catch(function (e) {
    305    Assert.equal(e.name, "FxAccountsProfileClientError");
    306    Assert.equal(e.code, 401);
    307    Assert.equal(e.errno, 100);
    308    Assert.equal(e.error, "It's not your token, it's you!");
    309    // should have been exactly 2 requests and exactly 2 auth headers.
    310    Assert.equal(numRequests, 2);
    311    Assert.equal(numAuthHeaders, 2);
    312    // and we should have seen both tokens revoked.
    313    Assert.equal(numTokensRemoved, 2);
    314    run_next_test();
    315  });
    316 });
    317 
    318 add_test(function networkErrorResponse() {
    319  let client = new FxAccountsProfileClient({
    320    serverURL: "http://domain.dummy",
    321    fxai: mockFxaInternal,
    322  });
    323  client.fetchProfile().catch(function (e) {
    324    Assert.equal(e.name, "FxAccountsProfileClientError");
    325    Assert.equal(e.code, null);
    326    Assert.equal(e.errno, ERRNO_NETWORK);
    327    Assert.equal(e.error, ERROR_NETWORK);
    328    run_next_test();
    329  });
    330 });
    331 
    332 add_test(function unsupportedMethod() {
    333  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
    334 
    335  return client._createRequest("/profile", "PUT").catch(function (e) {
    336    Assert.equal(e.name, "FxAccountsProfileClientError");
    337    Assert.equal(e.code, ERROR_CODE_METHOD_NOT_ALLOWED);
    338    Assert.equal(e.errno, ERRNO_NETWORK);
    339    Assert.equal(e.error, ERROR_NETWORK);
    340    Assert.equal(e.message, ERROR_MSG_METHOD_NOT_ALLOWED);
    341    run_next_test();
    342  });
    343 });
    344 
    345 add_test(function onCompleteRequestError() {
    346  let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
    347  client._Request = new mockResponseError(new Error("onComplete error"));
    348  client.fetchProfile().catch(function (e) {
    349    Assert.equal(e.name, "FxAccountsProfileClientError");
    350    Assert.equal(e.code, null);
    351    Assert.equal(e.errno, ERRNO_NETWORK);
    352    Assert.equal(e.error, ERROR_NETWORK);
    353    Assert.equal(e.message, "Error: onComplete error");
    354    run_next_test();
    355  });
    356 });
    357 
    358 add_test(function constructorTests() {
    359  validationHelper(
    360    undefined,
    361    "Error: Missing 'serverURL' configuration option"
    362  );
    363 
    364  validationHelper({}, "Error: Missing 'serverURL' configuration option");
    365 
    366  validationHelper({ serverURL: "badUrl" }, "Error: Invalid 'serverURL'");
    367 
    368  run_next_test();
    369 });
    370 
    371 add_test(function errorTests() {
    372  let error1 = new FxAccountsProfileClientError();
    373  Assert.equal(error1.name, "FxAccountsProfileClientError");
    374  Assert.equal(error1.code, null);
    375  Assert.equal(error1.errno, ERRNO_UNKNOWN_ERROR);
    376  Assert.equal(error1.error, ERROR_UNKNOWN);
    377  Assert.equal(error1.message, null);
    378 
    379  let error2 = new FxAccountsProfileClientError({
    380    code: STATUS_SUCCESS,
    381    errno: 1,
    382    error: "Error",
    383    message: "Something",
    384  });
    385  let fields2 = error2._toStringFields();
    386  let statusCode = 1;
    387 
    388  Assert.equal(error2.name, "FxAccountsProfileClientError");
    389  Assert.equal(error2.code, STATUS_SUCCESS);
    390  Assert.equal(error2.errno, statusCode);
    391  Assert.equal(error2.error, "Error");
    392  Assert.equal(error2.message, "Something");
    393 
    394  Assert.equal(fields2.name, "FxAccountsProfileClientError");
    395  Assert.equal(fields2.code, STATUS_SUCCESS);
    396  Assert.equal(fields2.errno, statusCode);
    397  Assert.equal(fields2.error, "Error");
    398  Assert.equal(fields2.message, "Something");
    399 
    400  Assert.ok(error2.toString().includes("Something"));
    401  run_next_test();
    402 });
    403 
    404 /**
    405 * Quick way to test the "FxAccountsProfileClient" constructor.
    406 *
    407 * @param {object} options
    408 *        FxAccountsProfileClient constructor options
    409 * @param {string} expected
    410 *        Expected error message
    411 * @returns {*}
    412 */
    413 function validationHelper(options, expected) {
    414  // add fxai to options - that missing isn't what we are testing here.
    415  if (options) {
    416    options.fxai = mockFxaInternal;
    417  }
    418  try {
    419    new FxAccountsProfileClient(options);
    420  } catch (e) {
    421    return Assert.equal(e.toString(), expected);
    422  }
    423  throw new Error("Validation helper error");
    424 }