// Test the FxAMigration module Cu.import("resource://services-sync/FxaMigrator.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/FxAccounts.jsm"); Cu.import("resource://gre/modules/FxAccountsCommon.js"); Cu.import("resource://services-sync/browserid_identity.js"); // Set our username pref early so sync initializes with the legacy provider. Services.prefs.setCharPref("services.sync.username", "foo"); // And ensure all debug messages end up being printed. Services.prefs.setCharPref("services.sync.log.appender.dump", "Debug"); // Now import sync Cu.import("resource://services-sync/service.js"); Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-sync/util.js"); // And reset the username. Services.prefs.clearUserPref("services.sync.username"); Cu.import("resource://testing-common/services/sync/utils.js"); Cu.import("resource://testing-common/services/common/logging.js"); Cu.import("resource://testing-common/services/sync/rotaryengine.js"); const FXA_USERNAME = "someone@somewhere"; // Utilities function promiseOneObserver(topic) { return new Promise((resolve, reject) => { let observer = function(subject, topic, data) { Services.obs.removeObserver(observer, topic); resolve({ subject: subject, data: data }); } Services.obs.addObserver(observer, topic, false); }); } function promiseStopServer(server) { return new Promise((resolve, reject) => { server.stop(resolve); }); } // Helpers function configureLegacySync() { let engine = new RotaryEngine(Service); engine.enabled = true; Svc.Prefs.set("registerEngines", engine.name); Svc.Prefs.set("log.logger.engine.rotary", "Trace"); let contents = { meta: {global: {engines: {rotary: {version: engine.version, syncID: engine.syncID}}}}, crypto: {}, rotary: {} }; const USER = "foo"; const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea"; setBasicCredentials(USER, "password", PASSPHRASE); let onRequest = function(request, response) { // ideally we'd only do this while a legacy user is configured, but WTH. response.setHeader("x-weave-alert", JSON.stringify({code: "soft-eol"})); } let server = new SyncServer({onRequest: onRequest}); server.registerUser(USER, "password"); server.createContents(USER, contents); server.start(); Service.serverURL = server.baseURI; Service.clusterURL = server.baseURI; Service.identity.username = USER; Service._updateCachedURLs(); Service.engineManager._engines[engine.name] = engine; return [engine, server]; } function configureFxa() { Services.prefs.setCharPref("identity.fxaccounts.auth.uri", "http://localhost"); } add_task(function *testMigration() { configureFxa(); // when we do a .startOver we want the new provider. let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity"); Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false); // disable the addons engine - this engine choice is arbitrary, but we // want to check it remains disabled after migration. Services.prefs.setBoolPref("services.sync.engine.addons", false); do_register_cleanup(() => { Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue) Services.prefs.setBoolPref("services.sync.engine.addons", true); }); // No sync user - that should report no user-action necessary. Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null, "no user state when complete"); // Arrange for a legacy sync user and manually bump the migrator let [engine, server] = configureLegacySync(); // Check our disabling of the "addons" engine worked, and for good measure, // that the "passwords" engine is enabled. Assert.ok(!Service.engineManager.get("addons").enabled, "addons is disabled"); Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is enabled"); // monkey-patch the migration sentinel code so we know it was called. let haveStartedSentinel = false; let origSetFxAMigrationSentinel = Service.setFxAMigrationSentinel; let promiseSentinelWritten = new Promise((resolve, reject) => { Service.setFxAMigrationSentinel = function(arg) { haveStartedSentinel = true; return origSetFxAMigrationSentinel.call(Service, arg).then(result => { Service.setFxAMigrationSentinel = origSetFxAMigrationSentinel; resolve(result); return result; }); } }); // We are now configured for legacy sync, but we aren't in an EOL state yet, // so should still be not waiting for a user. Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null, "no user state before server EOL"); // Start a sync - this will cause an EOL notification which the migrator's // observer will notice. let promise = promiseOneObserver("fxa-migration:state-changed"); _("Starting sync"); Service.sync(); _("Finished sync"); // We should have seen the observer, so be waiting for an FxA user. Assert.equal((yield promise).data, fxaMigrator.STATE_USER_FXA, "now waiting for FxA.") // Re-calling our user-state promise should also reflect the same state. Assert.equal((yield fxaMigrator._queueCurrentUserState()), fxaMigrator.STATE_USER_FXA, "still waiting for FxA."); // arrange for an unverified FxA user. let config = makeIdentityConfig({username: FXA_USERNAME}); let fxa = new FxAccounts({}); config.fxaccount.user.email = config.username; delete config.fxaccount.user.verified; // *sob* - shouldn't need this boilerplate fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { this.cert = { validUntil: fxa.internal.now() + CERT_LIFETIME, cert: "certificate", }; return Promise.resolve(this.cert.cert); }; // As soon as we set the FxA user the observers should fire and magically // transition. promise = promiseOneObserver("fxa-migration:state-changed"); fxAccounts.setSignedInUser(config.fxaccount.user); let observerInfo = yield promise; Assert.equal(observerInfo.data, fxaMigrator.STATE_USER_FXA_VERIFIED, "now waiting for verification"); Assert.ok(observerInfo.subject instanceof Ci.nsISupportsString, "email was passed to observer"); Assert.equal(observerInfo.subject.data, FXA_USERNAME, "email passed to observer is correct"); // should have seen the user set, so state should automatically update. Assert.equal((yield fxaMigrator._queueCurrentUserState()), fxaMigrator.STATE_USER_FXA_VERIFIED, "now waiting for verification"); // Before we verify the user, fire off a sync that calls us back during // the sync and before it completes - this way we can ensure we do the right // thing in terms of blocking sync and waiting for it to complete. let wasWaiting = false; // This is a PITA as sync is pseudo-blocking. engine._syncFinish = function () { // We aren't in a generator here, so use a helper to block on promises. function getState() { let cb = Async.makeSpinningCallback(); fxaMigrator._queueCurrentUserState().then(state => cb(null, state)); return cb.wait(); } // should still be waiting for verification. Assert.equal(getState(), fxaMigrator.STATE_USER_FXA_VERIFIED, "still waiting for verification"); // arrange for the user to be verified. The fxAccount's mock story is // broken, so go behind its back. config.fxaccount.user.verified = true; fxAccounts.setSignedInUser(config.fxaccount.user); Services.obs.notifyObservers(null, ONVERIFIED_NOTIFICATION, null); // spinningly wait for the migrator to catch up - sync is running so // we should be in a 'null' user-state as there is no user-action // necessary. let cb = Async.makeSpinningCallback(); promiseOneObserver("fxa-migration:state-changed").then(({ data: state }) => cb(null, state)); Assert.equal(cb.wait(), null, "no user action necessary while sync completes."); // We must not have started writing the sentinel yet. Assert.ok(!haveStartedSentinel, "haven't written a sentinel yet"); // sync should be blocked from continuing Assert.ok(Service.scheduler.isBlocked, "sync is blocked.") wasWaiting = true; throw ex; }; _("Starting sync"); Service.sync(); _("Finished sync"); // mock sync so we can ensure the final sync is scheduled with the FxA user. // (letting a "normal" sync complete is a PITA without mocking huge amounts // of FxA infra) let promiseFinalSync = new Promise((resolve, reject) => { let oldSync = Service.sync; Service.sync = function() { Service.sync = oldSync; resolve(); } }); Assert.ok(wasWaiting, "everything was good while sync was running.") // The migration is now going to run to completion. // sync should still be "blocked" Assert.ok(Service.scheduler.isBlocked, "sync is blocked."); // We should see the migration sentinel written and it should return true. Assert.ok((yield promiseSentinelWritten), "wrote the sentinel"); // And we should see a new sync start yield promiseFinalSync; // and we should be configured for FxA let WeaveService = Cc["@mozilla.org/weave/service;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; Assert.ok(WeaveService.fxAccountsEnabled, "FxA is enabled"); Assert.ok(Service.identity instanceof BrowserIDManager, "sync is configured with the browserid_identity provider."); Assert.equal(Service.identity.username, config.username, "correct user configured") Assert.ok(!Service.scheduler.isBlocked, "sync is not blocked.") // and the user state should remain null. Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null, "still no user action necessary"); // and our engines should be in the same enabled/disabled state as before. Assert.ok(!Service.engineManager.get("addons").enabled, "addons is still disabled"); Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is still enabled"); // aaaand, we are done - clean up. yield promiseStopServer(server); }); function run_test() { initTestLogging(); do_register_cleanup(() => { fxaMigrator.finalize(); Svc.Prefs.resetBranch(""); }); run_next_test(); }