tor-browser

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

test_fxa_node_reassignment.js (12883B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 _("Test that node reassignment happens correctly using the FxA identity mgr.");
      5 // The node-reassignment logic is quite different for FxA than for the legacy
      6 // provider.  In particular, there's no special request necessary for
      7 // reassignment - it comes from the token server - so we need to ensure the
      8 // Fxa cluster manager grabs a new token.
      9 
     10 const { RESTRequest } = ChromeUtils.importESModule(
     11  "resource://services-common/rest.sys.mjs"
     12 );
     13 const { Service } = ChromeUtils.importESModule(
     14  "resource://services-sync/service.sys.mjs"
     15 );
     16 const { Status } = ChromeUtils.importESModule(
     17  "resource://services-sync/status.sys.mjs"
     18 );
     19 const { SyncAuthManager } = ChromeUtils.importESModule(
     20  "resource://services-sync/sync_auth.sys.mjs"
     21 );
     22 
     23 add_task(async function setup() {
     24  // Disables all built-in engines. Important for avoiding errors thrown by the
     25  // add-ons engine.
     26  await Service.engineManager.clear();
     27 
     28  // Setup the sync auth manager.
     29  Status.__authManager = Service.identity = new SyncAuthManager();
     30 });
     31 
     32 // API-compatible with SyncServer handler. Bind `handler` to something to use
     33 // as a ServerCollection handler.
     34 function handleReassign(handler, req, resp) {
     35  resp.setStatusLine(req.httpVersion, 401, "Node reassignment");
     36  resp.setHeader("Content-Type", "application/json");
     37  let reassignBody = JSON.stringify({ error: "401inator in place" });
     38  resp.bodyOutputStream.write(reassignBody, reassignBody.length);
     39 }
     40 
     41 var numTokenRequests = 0;
     42 
     43 function prepareServer(cbAfterTokenFetch) {
     44  syncTestLogging();
     45  let config = makeIdentityConfig({ username: "johndoe" });
     46  // A server callback to ensure we don't accidentally hit the wrong endpoint
     47  // after a node reassignment.
     48  let callback = {
     49    onRequest(req) {
     50      let full = `${req.scheme}://${req.host}:${req.port}${req.path}`;
     51      let expected = config.fxaccount.token.endpoint;
     52      Assert.ok(
     53        full.startsWith(expected),
     54        `request made to ${full}, expected ${expected}`
     55      );
     56    },
     57  };
     58  Object.setPrototypeOf(callback, SyncServerCallback);
     59  let server = new SyncServer(callback);
     60  server.registerUser("johndoe");
     61  server.start();
     62 
     63  // Set the token endpoint for the initial token request that's done implicitly
     64  // via configureIdentity.
     65  config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe/";
     66  // And future token fetches will do magic around numReassigns.
     67  let numReassigns = 0;
     68  return configureIdentity(config).then(() => {
     69    Service.identity._tokenServerClient = {
     70      getTokenUsingOAuth() {
     71        return new Promise(res => {
     72          // Build a new URL with trailing zeros for the SYNC_VERSION part - this
     73          // will still be seen as equivalent by the test server, but different
     74          // by sync itself.
     75          numReassigns += 1;
     76          let trailingZeros = new Array(numReassigns + 1).join("0");
     77          let token = config.fxaccount.token;
     78          token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
     79          token.uid = config.username;
     80          _(`test server saw token fetch - endpoint now ${token.endpoint}`);
     81          numTokenRequests += 1;
     82          res(token);
     83          if (cbAfterTokenFetch) {
     84            cbAfterTokenFetch();
     85          }
     86        });
     87      },
     88    };
     89    return server;
     90  });
     91 }
     92 
     93 function getReassigned() {
     94  try {
     95    return Services.prefs.getBoolPref("services.sync.lastSyncReassigned");
     96  } catch (ex) {
     97    if (ex.result != Cr.NS_ERROR_UNEXPECTED) {
     98      do_throw(
     99        "Got exception retrieving lastSyncReassigned: " + Log.exceptionStr(ex)
    100      );
    101    }
    102  }
    103  return false;
    104 }
    105 
    106 /**
    107 * Make a test request to `url`, then watch the result of two syncs
    108 * to ensure that a node request was made.
    109 * Runs `between` between the two. This can be used to undo deliberate failure
    110 * setup, detach observers, etc.
    111 */
    112 async function syncAndExpectNodeReassignment(
    113  server,
    114  firstNotification,
    115  between,
    116  secondNotification,
    117  url
    118 ) {
    119  _("Starting syncAndExpectNodeReassignment\n");
    120  let deferred = Promise.withResolvers();
    121  async function onwards() {
    122    let numTokenRequestsBefore;
    123    function onFirstSync() {
    124      _("First sync completed.");
    125      Svc.Obs.remove(firstNotification, onFirstSync);
    126      Svc.Obs.add(secondNotification, onSecondSync);
    127 
    128      Assert.equal(Service.clusterURL, "");
    129 
    130      // Track whether we fetched a new token.
    131      numTokenRequestsBefore = numTokenRequests;
    132 
    133      // Allow for tests to clean up error conditions.
    134      between();
    135    }
    136    function onSecondSync() {
    137      _("Second sync completed.");
    138      Svc.Obs.remove(secondNotification, onSecondSync);
    139      Service.scheduler.clearSyncTriggers();
    140 
    141      // Make absolutely sure that any event listeners are done with their work
    142      // before we proceed.
    143      waitForZeroTimer(function () {
    144        _("Second sync nextTick.");
    145        Assert.equal(
    146          numTokenRequests,
    147          numTokenRequestsBefore + 1,
    148          "fetched a new token"
    149        );
    150        Service.startOver().then(() => {
    151          server.stop(deferred.resolve);
    152        });
    153      });
    154    }
    155 
    156    Svc.Obs.add(firstNotification, onFirstSync);
    157    await Service.sync();
    158  }
    159 
    160  // Make sure that we really do get a 401 (but we can only do that if we are
    161  // already logged in, as the login process is what sets up the URLs)
    162  if (Service.isLoggedIn) {
    163    _("Making request to " + url + " which should 401");
    164    let request = new RESTRequest(url);
    165    await request.get();
    166    Assert.equal(request.response.status, 401);
    167    CommonUtils.nextTick(onwards);
    168  } else {
    169    _("Skipping preliminary validation check for a 401 as we aren't logged in");
    170    CommonUtils.nextTick(onwards);
    171  }
    172  await deferred.promise;
    173 }
    174 
    175 // Check that when we sync we don't request a new token by default - our
    176 // test setup has configured the client with a valid token, and that token
    177 // should be used to form the cluster URL.
    178 add_task(async function test_single_token_fetch() {
    179  enableValidationPrefs();
    180 
    181  _("Test a normal sync only fetches 1 token");
    182 
    183  let numTokenFetches = 0;
    184 
    185  function afterTokenFetch() {
    186    numTokenFetches++;
    187  }
    188 
    189  // Set the cluster URL to an "old" version - this is to ensure we don't
    190  // use that old cached version for the first sync but prefer the value
    191  // we got from the token (and as above, we are also checking we don't grab
    192  // a new token). If the test actually attempts to connect to this URL
    193  // it will crash.
    194  Service.clusterURL = "http://example.com/";
    195 
    196  let server = await prepareServer(afterTokenFetch);
    197 
    198  Assert.ok(!Service.isLoggedIn, "not already logged in");
    199  await Service.sync();
    200  Assert.equal(Status.sync, SYNC_SUCCEEDED, "sync succeeded");
    201  Assert.equal(numTokenFetches, 0, "didn't fetch a new token");
    202  // A bit hacky, but given we know how prepareServer works we can deduce
    203  // that clusterURL we expect.
    204  let expectedClusterURL = server.baseURI + "1.1/johndoe/";
    205  Assert.equal(Service.clusterURL, expectedClusterURL);
    206  await Service.startOver();
    207  await promiseStopServer(server);
    208 });
    209 
    210 add_task(async function test_momentary_401_engine() {
    211  enableValidationPrefs();
    212 
    213  _("Test a failure for engine URLs that's resolved by reassignment.");
    214  let server = await prepareServer();
    215  let john = server.user("johndoe");
    216 
    217  _("Enabling the Rotary engine.");
    218  let { engine, syncID, tracker } = await registerRotaryEngine();
    219 
    220  // We need the server to be correctly set up prior to experimenting. Do this
    221  // through a sync.
    222  let global = {
    223    syncID: Service.syncID,
    224    storageVersion: STORAGE_VERSION,
    225    rotary: { version: engine.version, syncID },
    226  };
    227  john.createCollection("meta").insert("global", global);
    228 
    229  _("First sync to prepare server contents.");
    230  await Service.sync();
    231 
    232  _("Setting up Rotary collection to 401.");
    233  let rotary = john.createCollection("rotary");
    234  let oldHandler = rotary.collectionHandler;
    235  rotary.collectionHandler = handleReassign.bind(this, undefined);
    236 
    237  // We want to verify that the clusterURL pref has been cleared after a 401
    238  // inside a sync. Flag the Rotary engine to need syncing.
    239  john.collection("rotary").timestamp += 1000;
    240 
    241  function between() {
    242    _("Undoing test changes.");
    243    rotary.collectionHandler = oldHandler;
    244 
    245    function onLoginStart() {
    246      // lastSyncReassigned shouldn't be cleared until a sync has succeeded.
    247      _("Ensuring that lastSyncReassigned is still set at next sync start.");
    248      Svc.Obs.remove("weave:service:login:start", onLoginStart);
    249      Assert.ok(getReassigned());
    250    }
    251 
    252    _("Adding observer that lastSyncReassigned is still set on login.");
    253    Svc.Obs.add("weave:service:login:start", onLoginStart);
    254  }
    255 
    256  await syncAndExpectNodeReassignment(
    257    server,
    258    "weave:service:sync:finish",
    259    between,
    260    "weave:service:sync:finish",
    261    Service.storageURL + "rotary"
    262  );
    263 
    264  await tracker.clearChangedIDs();
    265  await Service.engineManager.unregister(engine);
    266 });
    267 
    268 // This test ends up being a failing info fetch *after we're already logged in*.
    269 add_task(async function test_momentary_401_info_collections_loggedin() {
    270  enableValidationPrefs();
    271 
    272  _(
    273    "Test a failure for info/collections after login that's resolved by reassignment."
    274  );
    275  let server = await prepareServer();
    276 
    277  _("First sync to prepare server contents.");
    278  await Service.sync();
    279 
    280  _("Arrange for info/collections to return a 401.");
    281  let oldHandler = server.toplevelHandlers.info;
    282  server.toplevelHandlers.info = handleReassign;
    283 
    284  function undo() {
    285    _("Undoing test changes.");
    286    server.toplevelHandlers.info = oldHandler;
    287  }
    288 
    289  Assert.ok(Service.isLoggedIn, "already logged in");
    290 
    291  await syncAndExpectNodeReassignment(
    292    server,
    293    "weave:service:sync:error",
    294    undo,
    295    "weave:service:sync:finish",
    296    Service.infoURL
    297  );
    298 });
    299 
    300 // This test ends up being a failing info fetch *before we're logged in*.
    301 // In this case we expect to recover during the login phase - so the first
    302 // sync succeeds.
    303 add_task(async function test_momentary_401_info_collections_loggedout() {
    304  enableValidationPrefs();
    305 
    306  _(
    307    "Test a failure for info/collections before login that's resolved by reassignment."
    308  );
    309 
    310  let oldHandler;
    311  let sawTokenFetch = false;
    312 
    313  function afterTokenFetch() {
    314    // After a single token fetch, we undo our evil handleReassign hack, so
    315    // the next /info request returns the collection instead of a 401
    316    server.toplevelHandlers.info = oldHandler;
    317    sawTokenFetch = true;
    318  }
    319 
    320  let server = await prepareServer(afterTokenFetch);
    321 
    322  // Return a 401 for the next /info request - it will be reset immediately
    323  // after a new token is fetched.
    324  oldHandler = server.toplevelHandlers.info;
    325  server.toplevelHandlers.info = handleReassign;
    326 
    327  Assert.ok(!Service.isLoggedIn, "not already logged in");
    328 
    329  await Service.sync();
    330  Assert.equal(Status.sync, SYNC_SUCCEEDED, "sync succeeded");
    331  // sync was successful - check we grabbed a new token.
    332  Assert.ok(sawTokenFetch, "a new token was fetched by this test.");
    333  // and we are done.
    334  await Service.startOver();
    335  await promiseStopServer(server);
    336 });
    337 
    338 // This test ends up being a failing meta/global fetch *after we're already logged in*.
    339 add_task(async function test_momentary_401_storage_loggedin() {
    340  enableValidationPrefs();
    341 
    342  _(
    343    "Test a failure for any storage URL after login that's resolved by" +
    344      "reassignment."
    345  );
    346  let server = await prepareServer();
    347 
    348  _("First sync to prepare server contents.");
    349  await Service.sync();
    350 
    351  _("Arrange for meta/global to return a 401.");
    352  let oldHandler = server.toplevelHandlers.storage;
    353  server.toplevelHandlers.storage = handleReassign;
    354 
    355  function undo() {
    356    _("Undoing test changes.");
    357    server.toplevelHandlers.storage = oldHandler;
    358  }
    359 
    360  Assert.ok(Service.isLoggedIn, "already logged in");
    361 
    362  await syncAndExpectNodeReassignment(
    363    server,
    364    "weave:service:sync:error",
    365    undo,
    366    "weave:service:sync:finish",
    367    Service.storageURL + "meta/global"
    368  );
    369 });
    370 
    371 // This test ends up being a failing meta/global fetch *before we've logged in*.
    372 add_task(async function test_momentary_401_storage_loggedout() {
    373  enableValidationPrefs();
    374 
    375  _(
    376    "Test a failure for any storage URL before login, not just engine parts. " +
    377      "Resolved by reassignment."
    378  );
    379  let server = await prepareServer();
    380 
    381  // Return a 401 for all storage requests.
    382  let oldHandler = server.toplevelHandlers.storage;
    383  server.toplevelHandlers.storage = handleReassign;
    384 
    385  function undo() {
    386    _("Undoing test changes.");
    387    server.toplevelHandlers.storage = oldHandler;
    388  }
    389 
    390  Assert.ok(!Service.isLoggedIn, "already logged in");
    391 
    392  await syncAndExpectNodeReassignment(
    393    server,
    394    "weave:service:login:error",
    395    undo,
    396    "weave:service:sync:finish",
    397    Service.storageURL + "meta/global"
    398  );
    399 });