michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: _("Test that node reassignment happens correctly using the FxA identity mgr."); michael@0: // The node-reassignment logic is quite different for FxA than for the legacy michael@0: // provider. In particular, there's no special request necessary for michael@0: // reassignment - it comes from the token server - so we need to ensure the michael@0: // Fxa cluster manager grabs a new token. michael@0: michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/rest.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-sync/service.js"); michael@0: Cu.import("resource://services-sync/status.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://testing-common/services/sync/rotaryengine.js"); michael@0: Cu.import("resource://services-sync/browserid_identity.js"); michael@0: Cu.import("resource://testing-common/services/sync/utils.js"); michael@0: michael@0: Service.engineManager.clear(); michael@0: michael@0: function run_test() { michael@0: Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Sync.Service").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace; michael@0: initTestLogging(); michael@0: michael@0: Service.engineManager.register(RotaryEngine); michael@0: michael@0: // Setup the FxA identity manager and cluster manager. michael@0: Status.__authManager = Service.identity = new BrowserIDManager(); michael@0: Service._clusterManager = Service.identity.createClusterManager(Service); michael@0: michael@0: // None of the failures in this file should result in a UI error. michael@0: function onUIError() { michael@0: do_throw("Errors should not be presented in the UI."); michael@0: } michael@0: Svc.Obs.add("weave:ui:login:error", onUIError); michael@0: Svc.Obs.add("weave:ui:sync:error", onUIError); michael@0: michael@0: run_next_test(); michael@0: } michael@0: michael@0: michael@0: // API-compatible with SyncServer handler. Bind `handler` to something to use michael@0: // as a ServerCollection handler. michael@0: function handleReassign(handler, req, resp) { michael@0: resp.setStatusLine(req.httpVersion, 401, "Node reassignment"); michael@0: resp.setHeader("Content-Type", "application/json"); michael@0: let reassignBody = JSON.stringify({error: "401inator in place"}); michael@0: resp.bodyOutputStream.write(reassignBody, reassignBody.length); michael@0: } michael@0: michael@0: let numTokenRequests = 0; michael@0: michael@0: function prepareServer(cbAfterTokenFetch) { michael@0: let config = makeIdentityConfig({username: "johndoe"}); michael@0: let server = new SyncServer(); michael@0: server.registerUser("johndoe"); michael@0: server.start(); michael@0: michael@0: // Set the token endpoint for the initial token request that's done implicitly michael@0: // via configureIdentity. michael@0: config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe"; michael@0: // And future token fetches will do magic around numReassigns. michael@0: let numReassigns = 0; michael@0: return configureIdentity(config).then(() => { michael@0: Service.identity._tokenServerClient = { michael@0: getTokenFromBrowserIDAssertion: function(uri, assertion, cb) { michael@0: // Build a new URL with trailing zeros for the SYNC_VERSION part - this michael@0: // will still be seen as equivalent by the test server, but different michael@0: // by sync itself. michael@0: numReassigns += 1; michael@0: let trailingZeros = new Array(numReassigns + 1).join('0'); michael@0: let token = config.fxaccount.token; michael@0: token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe"; michael@0: token.uid = config.username; michael@0: numTokenRequests += 1; michael@0: cb(null, token); michael@0: if (cbAfterTokenFetch) { michael@0: cbAfterTokenFetch(); michael@0: } michael@0: }, michael@0: }; michael@0: Service.clusterURL = config.fxaccount.token.endpoint; michael@0: return server; michael@0: }); michael@0: } michael@0: michael@0: function getReassigned() { michael@0: try { michael@0: return Services.prefs.getBoolPref("services.sync.lastSyncReassigned"); michael@0: } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) { michael@0: return false; michael@0: } catch (ex) { michael@0: do_throw("Got exception retrieving lastSyncReassigned: " + michael@0: Utils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Make a test request to `url`, then watch the result of two syncs michael@0: * to ensure that a node request was made. michael@0: * Runs `between` between the two. This can be used to undo deliberate failure michael@0: * setup, detach observers, etc. michael@0: */ michael@0: function syncAndExpectNodeReassignment(server, firstNotification, between, michael@0: secondNotification, url) { michael@0: _("Starting syncAndExpectNodeReassignment\n"); michael@0: let deferred = Promise.defer(); michael@0: function onwards() { michael@0: let numTokenRequestsBefore; michael@0: function onFirstSync() { michael@0: _("First sync completed."); michael@0: Svc.Obs.remove(firstNotification, onFirstSync); michael@0: Svc.Obs.add(secondNotification, onSecondSync); michael@0: michael@0: do_check_eq(Service.clusterURL, ""); michael@0: michael@0: // Track whether we fetched a new token. michael@0: numTokenRequestsBefore = numTokenRequests; michael@0: michael@0: // Allow for tests to clean up error conditions. michael@0: between(); michael@0: } michael@0: function onSecondSync() { michael@0: _("Second sync completed."); michael@0: Svc.Obs.remove(secondNotification, onSecondSync); michael@0: Service.scheduler.clearSyncTriggers(); michael@0: michael@0: // Make absolutely sure that any event listeners are done with their work michael@0: // before we proceed. michael@0: waitForZeroTimer(function () { michael@0: _("Second sync nextTick."); michael@0: do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token"); michael@0: Service.startOver(); michael@0: server.stop(deferred.resolve); michael@0: }); michael@0: } michael@0: michael@0: Svc.Obs.add(firstNotification, onFirstSync); michael@0: Service.sync(); michael@0: } michael@0: michael@0: // Make sure that it works! michael@0: _("Making request to " + url + " which should 401"); michael@0: let request = new RESTRequest(url); michael@0: request.get(function () { michael@0: do_check_eq(request.response.status, 401); michael@0: Utils.nextTick(onwards); michael@0: }); michael@0: yield deferred.promise; michael@0: } michael@0: michael@0: add_task(function test_momentary_401_engine() { michael@0: _("Test a failure for engine URLs that's resolved by reassignment."); michael@0: let server = yield prepareServer(); michael@0: let john = server.user("johndoe"); michael@0: michael@0: _("Enabling the Rotary engine."); michael@0: let engine = Service.engineManager.get("rotary"); michael@0: engine.enabled = true; michael@0: michael@0: // We need the server to be correctly set up prior to experimenting. Do this michael@0: // through a sync. michael@0: let global = {syncID: Service.syncID, michael@0: storageVersion: STORAGE_VERSION, michael@0: rotary: {version: engine.version, michael@0: syncID: engine.syncID}} michael@0: john.createCollection("meta").insert("global", global); michael@0: michael@0: _("First sync to prepare server contents."); michael@0: Service.sync(); michael@0: michael@0: _("Setting up Rotary collection to 401."); michael@0: let rotary = john.createCollection("rotary"); michael@0: let oldHandler = rotary.collectionHandler; michael@0: rotary.collectionHandler = handleReassign.bind(this, undefined); michael@0: michael@0: // We want to verify that the clusterURL pref has been cleared after a 401 michael@0: // inside a sync. Flag the Rotary engine to need syncing. michael@0: john.collection("rotary").timestamp += 1000; michael@0: michael@0: function between() { michael@0: _("Undoing test changes."); michael@0: rotary.collectionHandler = oldHandler; michael@0: michael@0: function onLoginStart() { michael@0: // lastSyncReassigned shouldn't be cleared until a sync has succeeded. michael@0: _("Ensuring that lastSyncReassigned is still set at next sync start."); michael@0: Svc.Obs.remove("weave:service:login:start", onLoginStart); michael@0: do_check_true(getReassigned()); michael@0: } michael@0: michael@0: _("Adding observer that lastSyncReassigned is still set on login."); michael@0: Svc.Obs.add("weave:service:login:start", onLoginStart); michael@0: } michael@0: michael@0: yield syncAndExpectNodeReassignment(server, michael@0: "weave:service:sync:finish", michael@0: between, michael@0: "weave:service:sync:finish", michael@0: Service.storageURL + "rotary"); michael@0: }); michael@0: michael@0: // This test ends up being a failing info fetch *after we're already logged in*. michael@0: add_task(function test_momentary_401_info_collections_loggedin() { michael@0: _("Test a failure for info/collections after login that's resolved by reassignment."); michael@0: let server = yield prepareServer(); michael@0: michael@0: _("First sync to prepare server contents."); michael@0: Service.sync(); michael@0: michael@0: _("Arrange for info/collections to return a 401."); michael@0: let oldHandler = server.toplevelHandlers.info; michael@0: server.toplevelHandlers.info = handleReassign; michael@0: michael@0: function undo() { michael@0: _("Undoing test changes."); michael@0: server.toplevelHandlers.info = oldHandler; michael@0: } michael@0: michael@0: do_check_true(Service.isLoggedIn, "already logged in"); michael@0: michael@0: yield syncAndExpectNodeReassignment(server, michael@0: "weave:service:sync:error", michael@0: undo, michael@0: "weave:service:sync:finish", michael@0: Service.infoURL); michael@0: }); michael@0: michael@0: // This test ends up being a failing info fetch *before we're logged in*. michael@0: // In this case we expect to recover during the login phase - so the first michael@0: // sync succeeds. michael@0: add_task(function test_momentary_401_info_collections_loggedout() { michael@0: _("Test a failure for info/collections before login that's resolved by reassignment."); michael@0: michael@0: let oldHandler; michael@0: let sawTokenFetch = false; michael@0: michael@0: function afterTokenFetch() { michael@0: // After a single token fetch, we undo our evil handleReassign hack, so michael@0: // the next /info request returns the collection instead of a 401 michael@0: server.toplevelHandlers.info = oldHandler; michael@0: sawTokenFetch = true; michael@0: } michael@0: michael@0: let server = yield prepareServer(afterTokenFetch); michael@0: michael@0: // Return a 401 for the next /info request - it will be reset immediately michael@0: // after a new token is fetched. michael@0: oldHandler = server.toplevelHandlers.info michael@0: server.toplevelHandlers.info = handleReassign; michael@0: michael@0: do_check_false(Service.isLoggedIn, "not already logged in"); michael@0: michael@0: Service.sync(); michael@0: do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded"); michael@0: // sync was successful - check we grabbed a new token. michael@0: do_check_true(sawTokenFetch, "a new token was fetched by this test.") michael@0: // and we are done. michael@0: Service.startOver(); michael@0: let deferred = Promise.defer(); michael@0: server.stop(deferred.resolve); michael@0: yield deferred.promise; michael@0: }); michael@0: michael@0: // This test ends up being a failing meta/global fetch *after we're already logged in*. michael@0: add_task(function test_momentary_401_storage_loggedin() { michael@0: _("Test a failure for any storage URL after login that's resolved by" + michael@0: "reassignment."); michael@0: let server = yield prepareServer(); michael@0: michael@0: _("First sync to prepare server contents."); michael@0: Service.sync(); michael@0: michael@0: _("Arrange for meta/global to return a 401."); michael@0: let oldHandler = server.toplevelHandlers.storage; michael@0: server.toplevelHandlers.storage = handleReassign; michael@0: michael@0: function undo() { michael@0: _("Undoing test changes."); michael@0: server.toplevelHandlers.storage = oldHandler; michael@0: } michael@0: michael@0: do_check_true(Service.isLoggedIn, "already logged in"); michael@0: michael@0: yield syncAndExpectNodeReassignment(server, michael@0: "weave:service:sync:error", michael@0: undo, michael@0: "weave:service:sync:finish", michael@0: Service.storageURL + "meta/global"); michael@0: }); michael@0: michael@0: // This test ends up being a failing meta/global fetch *before we've logged in*. michael@0: add_task(function test_momentary_401_storage_loggedout() { michael@0: _("Test a failure for any storage URL before login, not just engine parts. " + michael@0: "Resolved by reassignment."); michael@0: let server = yield prepareServer(); michael@0: michael@0: // Return a 401 for all storage requests. michael@0: let oldHandler = server.toplevelHandlers.storage; michael@0: server.toplevelHandlers.storage = handleReassign; michael@0: michael@0: function undo() { michael@0: _("Undoing test changes."); michael@0: server.toplevelHandlers.storage = oldHandler; michael@0: } michael@0: michael@0: do_check_false(Service.isLoggedIn, "already logged in"); michael@0: michael@0: yield syncAndExpectNodeReassignment(server, michael@0: "weave:service:login:error", michael@0: undo, michael@0: "weave:service:sync:finish", michael@0: Service.storageURL + "meta/global"); michael@0: }); michael@0: