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 });