1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_fxa_node_reassignment.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,321 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +_("Test that node reassignment happens correctly using the FxA identity mgr."); 1.8 +// The node-reassignment logic is quite different for FxA than for the legacy 1.9 +// provider. In particular, there's no special request necessary for 1.10 +// reassignment - it comes from the token server - so we need to ensure the 1.11 +// Fxa cluster manager grabs a new token. 1.12 + 1.13 +Cu.import("resource://gre/modules/Log.jsm"); 1.14 +Cu.import("resource://services-common/rest.js"); 1.15 +Cu.import("resource://services-sync/constants.js"); 1.16 +Cu.import("resource://services-sync/service.js"); 1.17 +Cu.import("resource://services-sync/status.js"); 1.18 +Cu.import("resource://services-sync/util.js"); 1.19 +Cu.import("resource://testing-common/services/sync/rotaryengine.js"); 1.20 +Cu.import("resource://services-sync/browserid_identity.js"); 1.21 +Cu.import("resource://testing-common/services/sync/utils.js"); 1.22 + 1.23 +Service.engineManager.clear(); 1.24 + 1.25 +function run_test() { 1.26 + Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace; 1.27 + Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace; 1.28 + Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace; 1.29 + Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace; 1.30 + Log.repository.getLogger("Sync.Service").level = Log.Level.Trace; 1.31 + Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace; 1.32 + initTestLogging(); 1.33 + 1.34 + Service.engineManager.register(RotaryEngine); 1.35 + 1.36 + // Setup the FxA identity manager and cluster manager. 1.37 + Status.__authManager = Service.identity = new BrowserIDManager(); 1.38 + Service._clusterManager = Service.identity.createClusterManager(Service); 1.39 + 1.40 + // None of the failures in this file should result in a UI error. 1.41 + function onUIError() { 1.42 + do_throw("Errors should not be presented in the UI."); 1.43 + } 1.44 + Svc.Obs.add("weave:ui:login:error", onUIError); 1.45 + Svc.Obs.add("weave:ui:sync:error", onUIError); 1.46 + 1.47 + run_next_test(); 1.48 +} 1.49 + 1.50 + 1.51 +// API-compatible with SyncServer handler. Bind `handler` to something to use 1.52 +// as a ServerCollection handler. 1.53 +function handleReassign(handler, req, resp) { 1.54 + resp.setStatusLine(req.httpVersion, 401, "Node reassignment"); 1.55 + resp.setHeader("Content-Type", "application/json"); 1.56 + let reassignBody = JSON.stringify({error: "401inator in place"}); 1.57 + resp.bodyOutputStream.write(reassignBody, reassignBody.length); 1.58 +} 1.59 + 1.60 +let numTokenRequests = 0; 1.61 + 1.62 +function prepareServer(cbAfterTokenFetch) { 1.63 + let config = makeIdentityConfig({username: "johndoe"}); 1.64 + let server = new SyncServer(); 1.65 + server.registerUser("johndoe"); 1.66 + server.start(); 1.67 + 1.68 + // Set the token endpoint for the initial token request that's done implicitly 1.69 + // via configureIdentity. 1.70 + config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe"; 1.71 + // And future token fetches will do magic around numReassigns. 1.72 + let numReassigns = 0; 1.73 + return configureIdentity(config).then(() => { 1.74 + Service.identity._tokenServerClient = { 1.75 + getTokenFromBrowserIDAssertion: function(uri, assertion, cb) { 1.76 + // Build a new URL with trailing zeros for the SYNC_VERSION part - this 1.77 + // will still be seen as equivalent by the test server, but different 1.78 + // by sync itself. 1.79 + numReassigns += 1; 1.80 + let trailingZeros = new Array(numReassigns + 1).join('0'); 1.81 + let token = config.fxaccount.token; 1.82 + token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe"; 1.83 + token.uid = config.username; 1.84 + numTokenRequests += 1; 1.85 + cb(null, token); 1.86 + if (cbAfterTokenFetch) { 1.87 + cbAfterTokenFetch(); 1.88 + } 1.89 + }, 1.90 + }; 1.91 + Service.clusterURL = config.fxaccount.token.endpoint; 1.92 + return server; 1.93 + }); 1.94 +} 1.95 + 1.96 +function getReassigned() { 1.97 + try { 1.98 + return Services.prefs.getBoolPref("services.sync.lastSyncReassigned"); 1.99 + } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) { 1.100 + return false; 1.101 + } catch (ex) { 1.102 + do_throw("Got exception retrieving lastSyncReassigned: " + 1.103 + Utils.exceptionStr(ex)); 1.104 + } 1.105 +} 1.106 + 1.107 +/** 1.108 + * Make a test request to `url`, then watch the result of two syncs 1.109 + * to ensure that a node request was made. 1.110 + * Runs `between` between the two. This can be used to undo deliberate failure 1.111 + * setup, detach observers, etc. 1.112 + */ 1.113 +function syncAndExpectNodeReassignment(server, firstNotification, between, 1.114 + secondNotification, url) { 1.115 + _("Starting syncAndExpectNodeReassignment\n"); 1.116 + let deferred = Promise.defer(); 1.117 + function onwards() { 1.118 + let numTokenRequestsBefore; 1.119 + function onFirstSync() { 1.120 + _("First sync completed."); 1.121 + Svc.Obs.remove(firstNotification, onFirstSync); 1.122 + Svc.Obs.add(secondNotification, onSecondSync); 1.123 + 1.124 + do_check_eq(Service.clusterURL, ""); 1.125 + 1.126 + // Track whether we fetched a new token. 1.127 + numTokenRequestsBefore = numTokenRequests; 1.128 + 1.129 + // Allow for tests to clean up error conditions. 1.130 + between(); 1.131 + } 1.132 + function onSecondSync() { 1.133 + _("Second sync completed."); 1.134 + Svc.Obs.remove(secondNotification, onSecondSync); 1.135 + Service.scheduler.clearSyncTriggers(); 1.136 + 1.137 + // Make absolutely sure that any event listeners are done with their work 1.138 + // before we proceed. 1.139 + waitForZeroTimer(function () { 1.140 + _("Second sync nextTick."); 1.141 + do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token"); 1.142 + Service.startOver(); 1.143 + server.stop(deferred.resolve); 1.144 + }); 1.145 + } 1.146 + 1.147 + Svc.Obs.add(firstNotification, onFirstSync); 1.148 + Service.sync(); 1.149 + } 1.150 + 1.151 + // Make sure that it works! 1.152 + _("Making request to " + url + " which should 401"); 1.153 + let request = new RESTRequest(url); 1.154 + request.get(function () { 1.155 + do_check_eq(request.response.status, 401); 1.156 + Utils.nextTick(onwards); 1.157 + }); 1.158 + yield deferred.promise; 1.159 +} 1.160 + 1.161 +add_task(function test_momentary_401_engine() { 1.162 + _("Test a failure for engine URLs that's resolved by reassignment."); 1.163 + let server = yield prepareServer(); 1.164 + let john = server.user("johndoe"); 1.165 + 1.166 + _("Enabling the Rotary engine."); 1.167 + let engine = Service.engineManager.get("rotary"); 1.168 + engine.enabled = true; 1.169 + 1.170 + // We need the server to be correctly set up prior to experimenting. Do this 1.171 + // through a sync. 1.172 + let global = {syncID: Service.syncID, 1.173 + storageVersion: STORAGE_VERSION, 1.174 + rotary: {version: engine.version, 1.175 + syncID: engine.syncID}} 1.176 + john.createCollection("meta").insert("global", global); 1.177 + 1.178 + _("First sync to prepare server contents."); 1.179 + Service.sync(); 1.180 + 1.181 + _("Setting up Rotary collection to 401."); 1.182 + let rotary = john.createCollection("rotary"); 1.183 + let oldHandler = rotary.collectionHandler; 1.184 + rotary.collectionHandler = handleReassign.bind(this, undefined); 1.185 + 1.186 + // We want to verify that the clusterURL pref has been cleared after a 401 1.187 + // inside a sync. Flag the Rotary engine to need syncing. 1.188 + john.collection("rotary").timestamp += 1000; 1.189 + 1.190 + function between() { 1.191 + _("Undoing test changes."); 1.192 + rotary.collectionHandler = oldHandler; 1.193 + 1.194 + function onLoginStart() { 1.195 + // lastSyncReassigned shouldn't be cleared until a sync has succeeded. 1.196 + _("Ensuring that lastSyncReassigned is still set at next sync start."); 1.197 + Svc.Obs.remove("weave:service:login:start", onLoginStart); 1.198 + do_check_true(getReassigned()); 1.199 + } 1.200 + 1.201 + _("Adding observer that lastSyncReassigned is still set on login."); 1.202 + Svc.Obs.add("weave:service:login:start", onLoginStart); 1.203 + } 1.204 + 1.205 + yield syncAndExpectNodeReassignment(server, 1.206 + "weave:service:sync:finish", 1.207 + between, 1.208 + "weave:service:sync:finish", 1.209 + Service.storageURL + "rotary"); 1.210 +}); 1.211 + 1.212 +// This test ends up being a failing info fetch *after we're already logged in*. 1.213 +add_task(function test_momentary_401_info_collections_loggedin() { 1.214 + _("Test a failure for info/collections after login that's resolved by reassignment."); 1.215 + let server = yield prepareServer(); 1.216 + 1.217 + _("First sync to prepare server contents."); 1.218 + Service.sync(); 1.219 + 1.220 + _("Arrange for info/collections to return a 401."); 1.221 + let oldHandler = server.toplevelHandlers.info; 1.222 + server.toplevelHandlers.info = handleReassign; 1.223 + 1.224 + function undo() { 1.225 + _("Undoing test changes."); 1.226 + server.toplevelHandlers.info = oldHandler; 1.227 + } 1.228 + 1.229 + do_check_true(Service.isLoggedIn, "already logged in"); 1.230 + 1.231 + yield syncAndExpectNodeReassignment(server, 1.232 + "weave:service:sync:error", 1.233 + undo, 1.234 + "weave:service:sync:finish", 1.235 + Service.infoURL); 1.236 +}); 1.237 + 1.238 +// This test ends up being a failing info fetch *before we're logged in*. 1.239 +// In this case we expect to recover during the login phase - so the first 1.240 +// sync succeeds. 1.241 +add_task(function test_momentary_401_info_collections_loggedout() { 1.242 + _("Test a failure for info/collections before login that's resolved by reassignment."); 1.243 + 1.244 + let oldHandler; 1.245 + let sawTokenFetch = false; 1.246 + 1.247 + function afterTokenFetch() { 1.248 + // After a single token fetch, we undo our evil handleReassign hack, so 1.249 + // the next /info request returns the collection instead of a 401 1.250 + server.toplevelHandlers.info = oldHandler; 1.251 + sawTokenFetch = true; 1.252 + } 1.253 + 1.254 + let server = yield prepareServer(afterTokenFetch); 1.255 + 1.256 + // Return a 401 for the next /info request - it will be reset immediately 1.257 + // after a new token is fetched. 1.258 + oldHandler = server.toplevelHandlers.info 1.259 + server.toplevelHandlers.info = handleReassign; 1.260 + 1.261 + do_check_false(Service.isLoggedIn, "not already logged in"); 1.262 + 1.263 + Service.sync(); 1.264 + do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded"); 1.265 + // sync was successful - check we grabbed a new token. 1.266 + do_check_true(sawTokenFetch, "a new token was fetched by this test.") 1.267 + // and we are done. 1.268 + Service.startOver(); 1.269 + let deferred = Promise.defer(); 1.270 + server.stop(deferred.resolve); 1.271 + yield deferred.promise; 1.272 +}); 1.273 + 1.274 +// This test ends up being a failing meta/global fetch *after we're already logged in*. 1.275 +add_task(function test_momentary_401_storage_loggedin() { 1.276 + _("Test a failure for any storage URL after login that's resolved by" + 1.277 + "reassignment."); 1.278 + let server = yield prepareServer(); 1.279 + 1.280 + _("First sync to prepare server contents."); 1.281 + Service.sync(); 1.282 + 1.283 + _("Arrange for meta/global to return a 401."); 1.284 + let oldHandler = server.toplevelHandlers.storage; 1.285 + server.toplevelHandlers.storage = handleReassign; 1.286 + 1.287 + function undo() { 1.288 + _("Undoing test changes."); 1.289 + server.toplevelHandlers.storage = oldHandler; 1.290 + } 1.291 + 1.292 + do_check_true(Service.isLoggedIn, "already logged in"); 1.293 + 1.294 + yield syncAndExpectNodeReassignment(server, 1.295 + "weave:service:sync:error", 1.296 + undo, 1.297 + "weave:service:sync:finish", 1.298 + Service.storageURL + "meta/global"); 1.299 +}); 1.300 + 1.301 +// This test ends up being a failing meta/global fetch *before we've logged in*. 1.302 +add_task(function test_momentary_401_storage_loggedout() { 1.303 + _("Test a failure for any storage URL before login, not just engine parts. " + 1.304 + "Resolved by reassignment."); 1.305 + let server = yield prepareServer(); 1.306 + 1.307 + // Return a 401 for all storage requests. 1.308 + let oldHandler = server.toplevelHandlers.storage; 1.309 + server.toplevelHandlers.storage = handleReassign; 1.310 + 1.311 + function undo() { 1.312 + _("Undoing test changes."); 1.313 + server.toplevelHandlers.storage = oldHandler; 1.314 + } 1.315 + 1.316 + do_check_false(Service.isLoggedIn, "already logged in"); 1.317 + 1.318 + yield syncAndExpectNodeReassignment(server, 1.319 + "weave:service:login:error", 1.320 + undo, 1.321 + "weave:service:sync:finish", 1.322 + Service.storageURL + "meta/global"); 1.323 +}); 1.324 +