diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /services/sync/tests/unit/test_fxa_node_reassignment.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'services/sync/tests/unit/test_fxa_node_reassignment.js')
-rw-r--r-- | services/sync/tests/unit/test_fxa_node_reassignment.js | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_fxa_node_reassignment.js b/services/sync/tests/unit/test_fxa_node_reassignment.js new file mode 100644 index 0000000000..e038dec6c4 --- /dev/null +++ b/services/sync/tests/unit/test_fxa_node_reassignment.js @@ -0,0 +1,416 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +_("Test that node reassignment happens correctly using the FxA identity mgr."); +// The node-reassignment logic is quite different for FxA than for the legacy +// provider. In particular, there's no special request necessary for +// reassignment - it comes from the token server - so we need to ensure the +// Fxa cluster manager grabs a new token. + +const { RESTRequest } = ChromeUtils.import( + "resource://services-common/rest.js" +); +const { Service } = ChromeUtils.import("resource://services-sync/service.js"); +const { Status } = ChromeUtils.import("resource://services-sync/status.js"); +const { BrowserIDManager } = ChromeUtils.import( + "resource://services-sync/browserid_identity.js" +); +const { PromiseUtils } = ChromeUtils.import( + "resource://gre/modules/PromiseUtils.jsm" +); + +add_task(async function setup() { + // Disables all built-in engines. Important for avoiding errors thrown by the + // add-ons engine. + await Service.engineManager.clear(); + + // Setup the FxA identity manager and cluster manager. + Status.__authManager = Service.identity = new BrowserIDManager(); +}); + +// API-compatible with SyncServer handler. Bind `handler` to something to use +// as a ServerCollection handler. +function handleReassign(handler, req, resp) { + resp.setStatusLine(req.httpVersion, 401, "Node reassignment"); + resp.setHeader("Content-Type", "application/json"); + let reassignBody = JSON.stringify({ error: "401inator in place" }); + resp.bodyOutputStream.write(reassignBody, reassignBody.length); +} + +var numTokenRequests = 0; + +function prepareServer(cbAfterTokenFetch) { + syncTestLogging(); + let config = makeIdentityConfig({ username: "johndoe" }); + // A server callback to ensure we don't accidentally hit the wrong endpoint + // after a node reassignment. + let callback = { + __proto__: SyncServerCallback, + onRequest(req, resp) { + let full = `${req.scheme}://${req.host}:${req.port}${req.path}`; + let expected = config.fxaccount.token.endpoint; + Assert.ok( + full.startsWith(expected), + `request made to ${full}, expected ${expected}` + ); + }, + }; + let server = new SyncServer(callback); + server.registerUser("johndoe"); + server.start(); + + // Set the token endpoint for the initial token request that's done implicitly + // via configureIdentity. + config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe/"; + // And future token fetches will do magic around numReassigns. + let numReassigns = 0; + return configureIdentity(config).then(() => { + Service.identity._tokenServerClient = { + getTokenFromBrowserIDAssertion(uri, assertion) { + return new Promise(res => { + // Build a new URL with trailing zeros for the SYNC_VERSION part - this + // will still be seen as equivalent by the test server, but different + // by sync itself. + numReassigns += 1; + let trailingZeros = new Array(numReassigns + 1).join("0"); + let token = config.fxaccount.token; + token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe"; + token.uid = config.username; + _(`test server saw token fetch - endpoint now ${token.endpoint}`); + numTokenRequests += 1; + res(token); + if (cbAfterTokenFetch) { + cbAfterTokenFetch(); + } + }); + }, + getTokenFromOAuthToken() { + return new Promise(res => { + // Build a new URL with trailing zeros for the SYNC_VERSION part - this + // will still be seen as equivalent by the test server, but different + // by sync itself. + numReassigns += 1; + let trailingZeros = new Array(numReassigns + 1).join("0"); + let token = config.fxaccount.token; + token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe"; + token.uid = config.username; + _(`test server saw token fetch - endpoint now ${token.endpoint}`); + numTokenRequests += 1; + res(token); + if (cbAfterTokenFetch) { + cbAfterTokenFetch(); + } + }); + }, + }; + return server; + }); +} + +function getReassigned() { + try { + return Services.prefs.getBoolPref("services.sync.lastSyncReassigned"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_UNEXPECTED) { + do_throw( + "Got exception retrieving lastSyncReassigned: " + Log.exceptionStr(ex) + ); + } + } + return false; +} + +/** + * Make a test request to `url`, then watch the result of two syncs + * to ensure that a node request was made. + * Runs `between` between the two. This can be used to undo deliberate failure + * setup, detach observers, etc. + */ +async function syncAndExpectNodeReassignment( + server, + firstNotification, + between, + secondNotification, + url +) { + _("Starting syncAndExpectNodeReassignment\n"); + let deferred = PromiseUtils.defer(); + async function onwards() { + let numTokenRequestsBefore; + function onFirstSync() { + _("First sync completed."); + Svc.Obs.remove(firstNotification, onFirstSync); + Svc.Obs.add(secondNotification, onSecondSync); + + Assert.equal(Service.clusterURL, ""); + + // Track whether we fetched a new token. + numTokenRequestsBefore = numTokenRequests; + + // Allow for tests to clean up error conditions. + between(); + } + function onSecondSync() { + _("Second sync completed."); + Svc.Obs.remove(secondNotification, onSecondSync); + Service.scheduler.clearSyncTriggers(); + + // Make absolutely sure that any event listeners are done with their work + // before we proceed. + waitForZeroTimer(function() { + _("Second sync nextTick."); + Assert.equal( + numTokenRequests, + numTokenRequestsBefore + 1, + "fetched a new token" + ); + Service.startOver().then(() => { + server.stop(deferred.resolve); + }); + }); + } + + Svc.Obs.add(firstNotification, onFirstSync); + await Service.sync(); + } + + // Make sure that we really do get a 401 (but we can only do that if we are + // already logged in, as the login process is what sets up the URLs) + if (Service.isLoggedIn) { + _("Making request to " + url + " which should 401"); + let request = new RESTRequest(url); + await request.get(); + Assert.equal(request.response.status, 401); + CommonUtils.nextTick(onwards); + } else { + _("Skipping preliminary validation check for a 401 as we aren't logged in"); + CommonUtils.nextTick(onwards); + } + await deferred.promise; +} + +// Check that when we sync we don't request a new token by default - our +// test setup has configured the client with a valid token, and that token +// should be used to form the cluster URL. +add_task(async function test_single_token_fetch() { + enableValidationPrefs(); + + _("Test a normal sync only fetches 1 token"); + + let numTokenFetches = 0; + + function afterTokenFetch() { + numTokenFetches++; + } + + // Set the cluster URL to an "old" version - this is to ensure we don't + // use that old cached version for the first sync but prefer the value + // we got from the token (and as above, we are also checking we don't grab + // a new token). If the test actually attempts to connect to this URL + // it will crash. + Service.clusterURL = "http://example.com/"; + + let server = await prepareServer(afterTokenFetch); + + Assert.ok(!Service.isLoggedIn, "not already logged in"); + await Service.sync(); + Assert.equal(Status.sync, SYNC_SUCCEEDED, "sync succeeded"); + Assert.equal(numTokenFetches, 0, "didn't fetch a new token"); + // A bit hacky, but given we know how prepareServer works we can deduce + // that clusterURL we expect. + let expectedClusterURL = server.baseURI + "1.1/johndoe/"; + Assert.equal(Service.clusterURL, expectedClusterURL); + await Service.startOver(); + await promiseStopServer(server); +}); + +add_task(async function test_momentary_401_engine() { + enableValidationPrefs(); + + _("Test a failure for engine URLs that's resolved by reassignment."); + let server = await prepareServer(); + let john = server.user("johndoe"); + + _("Enabling the Rotary engine."); + let { engine, syncID, tracker } = await registerRotaryEngine(); + + // We need the server to be correctly set up prior to experimenting. Do this + // through a sync. + let global = { + syncID: Service.syncID, + storageVersion: STORAGE_VERSION, + rotary: { version: engine.version, syncID }, + }; + john.createCollection("meta").insert("global", global); + + _("First sync to prepare server contents."); + await Service.sync(); + + _("Setting up Rotary collection to 401."); + let rotary = john.createCollection("rotary"); + let oldHandler = rotary.collectionHandler; + rotary.collectionHandler = handleReassign.bind(this, undefined); + + // We want to verify that the clusterURL pref has been cleared after a 401 + // inside a sync. Flag the Rotary engine to need syncing. + john.collection("rotary").timestamp += 1000; + + function between() { + _("Undoing test changes."); + rotary.collectionHandler = oldHandler; + + function onLoginStart() { + // lastSyncReassigned shouldn't be cleared until a sync has succeeded. + _("Ensuring that lastSyncReassigned is still set at next sync start."); + Svc.Obs.remove("weave:service:login:start", onLoginStart); + Assert.ok(getReassigned()); + } + + _("Adding observer that lastSyncReassigned is still set on login."); + Svc.Obs.add("weave:service:login:start", onLoginStart); + } + + await syncAndExpectNodeReassignment( + server, + "weave:service:sync:finish", + between, + "weave:service:sync:finish", + Service.storageURL + "rotary" + ); + + await tracker.clearChangedIDs(); + await Service.engineManager.unregister(engine); +}); + +// This test ends up being a failing info fetch *after we're already logged in*. +add_task(async function test_momentary_401_info_collections_loggedin() { + enableValidationPrefs(); + + _( + "Test a failure for info/collections after login that's resolved by reassignment." + ); + let server = await prepareServer(); + + _("First sync to prepare server contents."); + await Service.sync(); + + _("Arrange for info/collections to return a 401."); + let oldHandler = server.toplevelHandlers.info; + server.toplevelHandlers.info = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.info = oldHandler; + } + + Assert.ok(Service.isLoggedIn, "already logged in"); + + await syncAndExpectNodeReassignment( + server, + "weave:service:sync:error", + undo, + "weave:service:sync:finish", + Service.infoURL + ); +}); + +// This test ends up being a failing info fetch *before we're logged in*. +// In this case we expect to recover during the login phase - so the first +// sync succeeds. +add_task(async function test_momentary_401_info_collections_loggedout() { + enableValidationPrefs(); + + _( + "Test a failure for info/collections before login that's resolved by reassignment." + ); + + let oldHandler; + let sawTokenFetch = false; + + function afterTokenFetch() { + // After a single token fetch, we undo our evil handleReassign hack, so + // the next /info request returns the collection instead of a 401 + server.toplevelHandlers.info = oldHandler; + sawTokenFetch = true; + } + + let server = await prepareServer(afterTokenFetch); + + // Return a 401 for the next /info request - it will be reset immediately + // after a new token is fetched. + oldHandler = server.toplevelHandlers.info; + server.toplevelHandlers.info = handleReassign; + + Assert.ok(!Service.isLoggedIn, "not already logged in"); + + await Service.sync(); + Assert.equal(Status.sync, SYNC_SUCCEEDED, "sync succeeded"); + // sync was successful - check we grabbed a new token. + Assert.ok(sawTokenFetch, "a new token was fetched by this test."); + // and we are done. + await Service.startOver(); + await promiseStopServer(server); +}); + +// This test ends up being a failing meta/global fetch *after we're already logged in*. +add_task(async function test_momentary_401_storage_loggedin() { + enableValidationPrefs(); + + _( + "Test a failure for any storage URL after login that's resolved by" + + "reassignment." + ); + let server = await prepareServer(); + + _("First sync to prepare server contents."); + await Service.sync(); + + _("Arrange for meta/global to return a 401."); + let oldHandler = server.toplevelHandlers.storage; + server.toplevelHandlers.storage = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.storage = oldHandler; + } + + Assert.ok(Service.isLoggedIn, "already logged in"); + + await syncAndExpectNodeReassignment( + server, + "weave:service:sync:error", + undo, + "weave:service:sync:finish", + Service.storageURL + "meta/global" + ); +}); + +// This test ends up being a failing meta/global fetch *before we've logged in*. +add_task(async function test_momentary_401_storage_loggedout() { + enableValidationPrefs(); + + _( + "Test a failure for any storage URL before login, not just engine parts. " + + "Resolved by reassignment." + ); + let server = await prepareServer(); + + // Return a 401 for all storage requests. + let oldHandler = server.toplevelHandlers.storage; + server.toplevelHandlers.storage = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.storage = oldHandler; + } + + Assert.ok(!Service.isLoggedIn, "already logged in"); + + await syncAndExpectNodeReassignment( + server, + "weave:service:login:error", + undo, + "weave:service:sync:finish", + Service.storageURL + "meta/global" + ); +}); |