diff --git a/services/sync/Weave.js b/services/sync/Weave.js index d7befe15b93..1d982296f91 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -72,6 +72,11 @@ WeaveService.prototype = { Ci.nsISupportsWeakReference]), ensureLoaded: function () { + // If we are loaded and not using FxA, load the migration module. + if (!this.fxAccountsEnabled) { + Cu.import("resource://services-sync/FxaMigrator.jsm"); + } + Components.utils.import("resource://services-sync/main.js"); // Side-effect of accessing the service is that it is instantiated. diff --git a/services/sync/modules/FxaMigrator.jsm b/services/sync/modules/FxaMigrator.jsm new file mode 100644 index 00000000000..b292d0eaf0c --- /dev/null +++ b/services/sync/modules/FxaMigrator.jsm @@ -0,0 +1,381 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict;" + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", + "resource://gre/modules/FxAccounts.jsm"); + +XPCOMUtils.defineLazyGetter(this, "WeaveService", function() { + return Cc["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "Weave", + "resource://services-sync/main.js"); + +// FxAccountsCommon.js doesn't use a "namespace", so create one here. +let fxAccountsCommon = {}; +Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); + +// We send this notification whenever the migration state changes. +const OBSERVER_STATE_CHANGE_TOPIC = "fxa-migration:state-changed"; +// We also send the state notification when we *receive* this. This allows +// consumers to avoid loading this module until it receives a notification +// from us (which may never happen if there's no migration to do) +const OBSERVER_STATE_REQUEST_TOPIC = "fxa-migration:state-request"; + +const OBSERVER_TOPICS = [ + "xpcom-shutdown", + "weave:service:sync:start", + "weave:service:sync:finish", + "weave:service:sync:error", + "weave:eol", + OBSERVER_STATE_REQUEST_TOPIC, + fxAccountsCommon.ONLOGIN_NOTIFICATION, + fxAccountsCommon.ONLOGOUT_NOTIFICATION, + fxAccountsCommon.ONVERIFIED_NOTIFICATION, +]; + +// A list of preference names we write to the migration sentinel. We only +// write ones that have a user-set value. +const FXA_SENTINEL_PREFS = [ + "identity.fxaccounts.auth.uri", + "identity.fxaccounts.remote.force_auth.uri", + "identity.fxaccounts.remote.signup.uri", + "identity.fxaccounts.remote.signin.uri", + "identity.fxaccounts.settings.uri", + "services.sync.tokenServerURI", +]; + +function Migrator() { + // Leave the log-level as Debug - Sync will setup log appenders such that + // these messages generally will not be seen unless other log related + // prefs are set. + this.level = Log.Level.Debug; + + this._nextUserStatePromise = Promise.resolve(); + + for (let topic of OBSERVER_TOPICS) { + Services.obs.addObserver(this, topic, false); + } + // ._state is an optimization so we avoid sending redundant observer + // notifications when the state hasn't actually changed. + this._state = null; +} + +Migrator.prototype = { + log: Log.repository.getLogger("Sync.SyncMigration"), + + // What user action is necessary to push the migration forward? + // A |null| state means there is nothing to do. Note that a null state implies + // either. (a) no migration is necessary or (b) that the migrator module is + // waiting for something outside of the user's control - eg, sync to complete, + // the migration sentinel to be uploaded, etc. In most cases the wait will be + // short, but edge cases (eg, no network, sync bugs that prevent it stopping + // until shutdown) may require a significantly longer wait. + STATE_USER_FXA: "waiting for user to be signed in to FxA", + STATE_USER_FXA_VERIFIED: "waiting for a verified FxA user", + + finalize() { + for (let topic of OBSERVER_TOPICS) { + Services.obs.removeObserver(this, topic); + } + }, + + observe(subject, topic, data) { + this.log.debug("observed " + topic); + switch (topic) { + case "xpcom-shutdown": + this.finalize(); + break; + + case OBSERVER_STATE_REQUEST_TOPIC: + // someone has requested the state - send it. + this._queueCurrentUserState(true); + break; + + default: + // some other observer that may affect our state has fired, so update. + this._queueCurrentUserState().then( + () => this.log.debug("update state from observer " + topic + " complete") + ).catch(err => { + let msg = "Failed to handle topic " + topic + ": " + err; + Cu.reportError(msg); + this.log.error(msg); + }); + } + }, + + // Try and move to a state where we are blocked on a user action. + // This needs to be restartable, and the states may, in edge-cases, end + // up going backwards (eg, user logs out while we are waiting to be told + // about verification) + // This is called by our observer notifications - so if there is already + // a promise in-flight, it's possible we will miss something important - so + // we wait for the in-flight one to complete then fire another (ie, this + // is effectively a queue of promises) + _queueCurrentUserState(forceObserver = false) { + return this._nextUserStatePromise = this._nextUserStatePromise.then( + () => this._promiseCurrentUserState(forceObserver), + err => { + let msg = "Failed to determine the current user state: " + err; + Cu.reportError(msg); + this.log.error(msg); + return this._promiseCurrentUserState(forceObserver) + } + ); + }, + + _promiseCurrentUserState: Task.async(function* (forceObserver) { + this.log.trace("starting _promiseCurrentUserState"); + let update = newState => { + this.log.info("Migration state: '${state}' => '${newState}'", + {state: this._state, newState: newState}); + if (forceObserver || newState !== this._state) { + this._state = newState; + Services.obs.notifyObservers(null, OBSERVER_STATE_CHANGE_TOPIC, newState); + } + return newState; + } + + // If we have no sync user, or are already using an FxA account we must + // be done. + if (WeaveService.fxAccountsEnabled) { + // should not be necessary, but if we somehow ended up with FxA enabled + // and sync blocked it would be bad - so better safe than sorry. + this._unblockSync(); + return update(null); + } + + // so we need to migrate - let's see how far along we are. + // If sync isn't in EOL mode, then we are still waiting for the server + // to offer the migration process - so no user action necessary. + let isEOL = false; + try { + isEOL = !!Services.prefs.getCharPref("services.sync.errorhandler.alert.mode"); + } catch (e) {} + + if (!isEOL) { + return update(null); + } + + // So we are in EOL mode - have we a user? + let fxauser = yield fxAccounts.getSignedInUser(); + if (!fxauser) { + return update(this.STATE_USER_FXA); + } + if (!fxauser.verified) { + return update(this.STATE_USER_FXA_VERIFIED); + } + + // So we just have housekeeping to do - we aren't blocked on a user, so + // reflect that. + this.log.info("No next user state - doing some housekeeping"); + update(null); + + // We need to disable sync from automatically starting, + // and if we are currently syncing wait for it to complete. + this._blockSync(); + + // Are we currently syncing? + if (Weave.Service._locked) { + // our observers will kick us further along when complete. + this.log.info("waiting for sync to complete") + return null; + } + + // Write the migration sentinel if necessary. + yield this._setMigrationSentinelIfNecessary(); + + // Must be ready to perform the actual migration. + this.log.info("Performing final sync migration steps"); + // Do the actual migration. + let startOverComplete = new Promise((resolve, reject) => { + Services.obs.addObserver(observe = () => { + this.log.info("observed that startOver is complete"); + Services.obs.removeObserver(observe, "weave:service:start-over:finish"); + resolve(); + }, "weave:service:start-over:finish", false); + }); + + Weave.Service.startOver(); + // need to wait for an observer. + yield startOverComplete; + // observer fired, now kick things off with the FxA user. + this.log.info("scheduling initial FxA sync."); + this._unblockSync(); + Weave.Service.scheduler.scheduleNextSync(0); + return null; + }), + + /* Return an object with the preferences we care about */ + _getSentinelPrefs() { + let result = {}; + for (let pref of FXA_SENTINEL_PREFS) { + if (Services.prefs.prefHasUserValue(pref)) { + result[pref] = Services.prefs.getCharPref(pref); + } + } + return result; + }, + + /* Apply any preferences we've obtained from the sentinel */ + _applySentinelPrefs(savedPrefs) { + for (let pref of FXA_SENTINEL_PREFS) { + if (savedPrefs[pref]) { + Services.prefs.setCharPref(pref, savedPrefs[pref]); + } + } + }, + + /* Ask sync to upload the migration sentinel */ + _setSyncMigrationSentinel: Task.async(function* () { + yield WeaveService.whenLoaded(); + let signedInUser = yield fxAccounts.getSignedInUser(); + let sentinel = { + email: signedInUser.email, + uid: signedInUser.uid, + verified: signedInUser.verified, + prefs: this._getSentinelPrefs(), + }; + if (Weave.Service.setFxaMigrationSentinel) { + yield Weave.Service.setFxaMigrationSentinel(sentinel); + } else { + this.log.warn("Waiting on bug 1017433; no sync sentinel"); + } + }), + + /* Ask sync to upload the migration sentinal if we (or any other linked device) + haven't previously written one. + */ + _setMigrationSentinelIfNecessary: Task.async(function* () { + if (!(yield this._getSyncMigrationSentinel())) { + this.log.info("writing the migration sentinel"); + yield this._setSyncMigrationSentinel(); + } + }), + + /* Ask sync to return a migration sentinel if one exists, otherwise return null */ + _getSyncMigrationSentinel: Task.async(function* () { + yield WeaveService.whenLoaded(); + if (!Weave.Service.getFxaMigrationSentinel) { + this.log.warn("Waiting on bug 1017433; no sync sentinel"); + return null; + } + let sentinel = yield Weave.Service.getFxaMigrationSentinel(); + this.log.debug("got migration sentinel ${}", sentinel); + return sentinel; + }), + + _getDefaultAccountName: Task.async(function* (sentinel) { + // Requires looking to see if other devices have written a migration + // sentinel (eg, see _haveSynchedMigrationSentinel), and if not, see if + // the legacy account name appears to be a valid email address (via the + // services.sync.account pref), otherwise return null. + // NOTE: Sync does all this synchronously via nested event loops, but we + // expose a promise to make future migration to an async-sync easier. + if (sentinel && sentinel.email) { + this.log.info("defaultAccountName found via sentinel: ${}", sentinel.email); + return sentinel.email; + } + // No previous migrations, so check the existing account name. + let account = Weave.Service.identity.account; + if (account && account.contains("@")) { + this.log.info("defaultAccountName found via legacy account name: {}", account); + return account; + } + this.log.info("defaultAccountName could not find an account"); + return null; + }), + + // Prevent sync from automatically starting + _blockSync() { + if (Weave.Service.scheduler.blockSync) { + Weave.Service.scheduler.blockSync(); + } else { + this.log.warn("Waiting on bug 1019408; sync not blocked"); + } + }, + + _unblockSync() { + if (Weave.Service.scheduler.unblockSync) { + Weave.Service.scheduler.unblockSync(); + } else { + this.log.warn("Waiting on bug 1019408; sync not unblocked"); + } + }, + + /* + * Some helpers for the UI to try and move to the next state. + */ + + // Open a UI for the user to create a Firefox Account. This should only be + // called while we are in the STATE_USER_FXA state. When the user completes + // the creation we'll see an ONLOGIN_NOTIFICATION notification from FxA and + // we'll move to either the STATE_USER_FXA_VERIFIED state or we'll just + // complete the migration if they login as an already verified user. + createFxAccount: Task.async(function* (win) { + // warn if we aren't in the expected state - but go ahead anyway! + if (this._state != this.STATE_USER_FXA) { + this.log.warn("createFxAccount called in an unexpected state: ${}", this._state); + } + // We need to obtain the sentinel and apply any prefs that might be + // specified *before* attempting to setup FxA as the prefs might + // specify custom servers etc. + let sentinel = yield this._getSyncMigrationSentinel(); + if (sentinel && sentinel.prefs) { + this._applySentinelPrefs(sentinel.prefs); + } + // If we already have a sentinel then we assume the user has previously + // created the specified account, so just ask to sign-in. + let action = sentinel ? "signin" : "signup"; + // See if we can find a default account name to use. + let email = yield this._getDefaultAccountName(sentinel); + let tail = email ? "&email=" + encodeURIComponent(email) : ""; + win.switchToTabHavingURI("about:accounts?" + action + tail, true, + {ignoreFragment: true, replaceQueryString: true}); + // An FxA observer will fire when the user completes this, which will + // cause us to move to the next "user blocked" state and notify via our + // observer notification. + }), + + // Ask the FxA servers to re-send a verification mail for the currently + // logged in user. This should only be called while we are in the + // STATE_USER_FXA_VERIFIED state. When the user clicks on the link in + // the mail we should see an ONVERIFIED_NOTIFICATION which will cause us + // to complete the migration. + resendVerificationMail: Task.async(function * () { + // warn if we aren't in the expected state - but go ahead anyway! + if (this._state != this.STATE_USER_FXA_VERIFIED) { + this.log.warn("createFxAccount called in an unexpected state: ${}", this._state); + } + return fxAccounts.resendVerificationEmail(); + }), + + // "forget" about the current Firefox account. This should only be called + // while we are in the STATE_USER_FXA_VERIFIED state. After this we will + // see an ONLOGOUT_NOTIFICATION, which will cause the migrator to return back + // to the STATE_USER_FXA state, from where they can choose a different account. + forgetFxAccount: Task.async(function * () { + // warn if we aren't in the expected state - but go ahead anyway! + if (this._state != this.STATE_USER_FXA_VERIFIED) { + this.log.warn("createFxAccount called in an unexpected state: ${}", this._state); + } + return fxAccounts.signOut(); + }), + +} + +// We expose a singleton +this.EXPORTED_SYMBOLS = ["fxaMigrator"]; +let fxaMigrator = new Migrator(); diff --git a/services/sync/moz.build b/services/sync/moz.build index acd23384f11..3a825c2cce5 100644 --- a/services/sync/moz.build +++ b/services/sync/moz.build @@ -20,6 +20,7 @@ EXTRA_JS_MODULES['services-sync'] += [ 'modules/addonutils.js', 'modules/browserid_identity.js', 'modules/engines.js', + 'modules/FxaMigrator.jsm', 'modules/healthreport.jsm', 'modules/identity.js', 'modules/jpakeclient.js', diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js index 3fe3d951327..c917c4988c2 100644 --- a/services/sync/tests/unit/head_http_server.js +++ b/services/sync/tests/unit/head_http_server.js @@ -514,8 +514,11 @@ let SyncServerCallback = { * * Allows the test to inspect the request. Hooks should be careful not to * modify or change state of the request or they may impact future processing. + * The response is also passed so the callback can set headers etc - but care + * must be taken to not screw with the response body or headers that may + * conflict with normal operation of this server. */ - onRequest: function onRequest(request) {}, + onRequest: function onRequest(request, response) {}, }; /** @@ -796,7 +799,7 @@ SyncServer.prototype = { this._log.debug("SyncServer: Handling request: " + req.method + " " + req.path); if (this.callback.onRequest) { - this.callback.onRequest(req); + this.callback.onRequest(req, resp); } let parts = this.pathRE.exec(req.path); diff --git a/services/sync/tests/unit/test_fxa_migration.js b/services/sync/tests/unit/test_fxa_migration.js new file mode 100644 index 00000000000..f85c99ce2f1 --- /dev/null +++ b/services/sync/tests/unit/test_fxa_migration.js @@ -0,0 +1,270 @@ +// 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(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); + do_register_cleanup(() => { + Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue) + }); + + // 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(); + + // monkey-patch the migration sentinel code so we know it was called. + let haveStartedSentinel = false; +// (This is waiting on bug 1017433) +/** + 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), 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); + + Assert.equal((yield promise), + fxaMigrator.STATE_USER_FXA_VERIFIED, + "now waiting for verification"); + + // 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(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 +// (This is waiting on bug 1019408) +/** + 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" +// (This is waiting on bug 1019408) +/** + Assert.ok(Service.scheduler.isBlocked, "sync is blocked."); +**/ + + // We should see the migration sentinel written and it should return true. +// (This is waiting on bug 1017433) +/** + 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"); + + // aaaand, we are done - clean up. + yield promiseStopServer(server); +}); + + +function run_test() { + initTestLogging(); + do_register_cleanup(() => { + fxaMigrator.finalize(); + Svc.Prefs.resetBranch(""); + }); + run_next_test(); +} diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini index ab750423b67..43e887c8d77 100644 --- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -172,3 +172,6 @@ skip-if = debug skip-if = ! healthreport [test_warn_on_truncated_response.js] + +# FxA migration +[test_fxa_migration.js]