diff options
Diffstat (limited to 'services/sync/tests/unit/test_syncscheduler.js')
-rw-r--r-- | services/sync/tests/unit/test_syncscheduler.js | 1033 |
1 files changed, 1033 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_syncscheduler.js b/services/sync/tests/unit/test_syncscheduler.js new file mode 100644 index 000000000..b066eae82 --- /dev/null +++ b/services/sync/tests/unit/test_syncscheduler.js @@ -0,0 +1,1033 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/engines/clients.js"); +Cu.import("resource://services-sync/policies.js"); +Cu.import("resource://services-sync/record.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/util.js"); +Cu.import("resource://testing-common/services/sync/utils.js"); + +Service.engineManager.clear(); + +function CatapultEngine() { + SyncEngine.call(this, "Catapult", Service); +} +CatapultEngine.prototype = { + __proto__: SyncEngine.prototype, + exception: null, // tests fill this in + _sync: function _sync() { + throw this.exception; + } +}; + +Service.engineManager.register(CatapultEngine); + +var scheduler = new SyncScheduler(Service); +var clientsEngine = Service.clientsEngine; + +// Don't remove stale clients when syncing. This is a test-only workaround +// that lets us add clients directly to the store, without losing them on +// the next sync. +clientsEngine._removeRemoteClient = id => {}; + +function sync_httpd_setup() { + let global = new ServerWBO("global", { + syncID: Service.syncID, + storageVersion: STORAGE_VERSION, + engines: {clients: {version: clientsEngine.version, + syncID: clientsEngine.syncID}} + }); + let clientsColl = new ServerCollection({}, true); + + // Tracking info/collections. + let collectionsHelper = track_collections_helper(); + let upd = collectionsHelper.with_updated_collection; + + return httpd_setup({ + "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()), + "/1.1/johndoe/info/collections": collectionsHelper.handler, + "/1.1/johndoe/storage/crypto/keys": + upd("crypto", (new ServerWBO("keys")).handler()), + "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()), + "/user/1.0/johndoe/node/weave": httpd_handler(200, "OK", "null") + }); +} + +function setUp(server) { + let deferred = Promise.defer(); + configureIdentity({username: "johndoe"}).then(() => { + Service.clusterURL = server.baseURI + "/"; + + generateNewKeys(Service.collectionKeys); + let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); + serverKeys.encrypt(Service.identity.syncKeyBundle); + let result = serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success; + deferred.resolve(result); + }); + return deferred.promise; +} + +function cleanUpAndGo(server) { + let deferred = Promise.defer(); + Utils.nextTick(function () { + clientsEngine._store.wipe(); + Service.startOver(); + if (server) { + server.stop(deferred.resolve); + } else { + deferred.resolve(); + } + }); + return deferred.promise; +} + +function run_test() { + initTestLogging("Trace"); + + Log.repository.getLogger("Sync.Service").level = Log.Level.Trace; + Log.repository.getLogger("Sync.scheduler").level = Log.Level.Trace; + validate_all_future_pings(); + + // The scheduler checks Weave.fxaEnabled to determine whether to use + // FxA defaults or legacy defaults. As .fxaEnabled checks the username, we + // set a username here then reset the default to ensure they are used. + ensureLegacyIdentityManager(); + setBasicCredentials("johndoe"); + scheduler.setDefaults(); + + run_next_test(); +} + +add_test(function test_prefAttributes() { + _("Test various attributes corresponding to preferences."); + + const INTERVAL = 42 * 60 * 1000; // 42 minutes + const THRESHOLD = 3142; + const SCORE = 2718; + const TIMESTAMP1 = 1275493471649; + + _("The 'nextSync' attribute stores a millisecond timestamp rounded down to the nearest second."); + do_check_eq(scheduler.nextSync, 0); + scheduler.nextSync = TIMESTAMP1; + do_check_eq(scheduler.nextSync, Math.floor(TIMESTAMP1 / 1000) * 1000); + + _("'syncInterval' defaults to singleDeviceInterval."); + do_check_eq(Svc.Prefs.get('syncInterval'), undefined); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + + _("'syncInterval' corresponds to a preference setting."); + scheduler.syncInterval = INTERVAL; + do_check_eq(scheduler.syncInterval, INTERVAL); + do_check_eq(Svc.Prefs.get('syncInterval'), INTERVAL); + + _("'syncThreshold' corresponds to preference, defaults to SINGLE_USER_THRESHOLD"); + do_check_eq(Svc.Prefs.get('syncThreshold'), undefined); + do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + scheduler.syncThreshold = THRESHOLD; + do_check_eq(scheduler.syncThreshold, THRESHOLD); + + _("'globalScore' corresponds to preference, defaults to zero."); + do_check_eq(Svc.Prefs.get('globalScore'), 0); + do_check_eq(scheduler.globalScore, 0); + scheduler.globalScore = SCORE; + do_check_eq(scheduler.globalScore, SCORE); + do_check_eq(Svc.Prefs.get('globalScore'), SCORE); + + _("Intervals correspond to default preferences."); + do_check_eq(scheduler.singleDeviceInterval, + Svc.Prefs.get("scheduler.sync11.singleDeviceInterval") * 1000); + do_check_eq(scheduler.idleInterval, + Svc.Prefs.get("scheduler.idleInterval") * 1000); + do_check_eq(scheduler.activeInterval, + Svc.Prefs.get("scheduler.activeInterval") * 1000); + do_check_eq(scheduler.immediateInterval, + Svc.Prefs.get("scheduler.immediateInterval") * 1000); + + _("Custom values for prefs will take effect after a restart."); + Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 420); + Svc.Prefs.set("scheduler.idleInterval", 230); + Svc.Prefs.set("scheduler.activeInterval", 180); + Svc.Prefs.set("scheduler.immediateInterval", 31415); + scheduler.setDefaults(); + do_check_eq(scheduler.idleInterval, 230000); + do_check_eq(scheduler.singleDeviceInterval, 420000); + do_check_eq(scheduler.activeInterval, 180000); + do_check_eq(scheduler.immediateInterval, 31415000); + + _("Custom values for interval prefs can't be less than 60 seconds."); + Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42); + Svc.Prefs.set("scheduler.idleInterval", 50); + Svc.Prefs.set("scheduler.activeInterval", 50); + Svc.Prefs.set("scheduler.immediateInterval", 10); + scheduler.setDefaults(); + do_check_eq(scheduler.idleInterval, 60000); + do_check_eq(scheduler.singleDeviceInterval, 60000); + do_check_eq(scheduler.activeInterval, 60000); + do_check_eq(scheduler.immediateInterval, 60000); + + Svc.Prefs.resetBranch(""); + scheduler.setDefaults(); + run_next_test(); +}); + +add_identity_test(this, function* test_updateClientMode() { + _("Test updateClientMode adjusts scheduling attributes based on # of clients appropriately"); + do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + do_check_false(scheduler.numClients > 1); + do_check_false(scheduler.idle); + + // Trigger a change in interval & threshold by adding a client. + clientsEngine._store.create({id: "foo", cleartext: "bar"}); + scheduler.updateClientMode(); + + do_check_eq(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD); + do_check_eq(scheduler.syncInterval, scheduler.activeInterval); + do_check_true(scheduler.numClients > 1); + do_check_false(scheduler.idle); + + // Resets the number of clients to 0. + clientsEngine.resetClient(); + scheduler.updateClientMode(); + + // Goes back to single user if # clients is 1. + do_check_eq(scheduler.numClients, 1); + do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + do_check_false(scheduler.numClients > 1); + do_check_false(scheduler.idle); + + yield cleanUpAndGo(); +}); + +add_identity_test(this, function* test_masterpassword_locked_retry_interval() { + _("Test Status.login = MASTER_PASSWORD_LOCKED results in reschedule at MASTER_PASSWORD interval"); + let loginFailed = false; + Svc.Obs.add("weave:service:login:error", function onLoginError() { + Svc.Obs.remove("weave:service:login:error", onLoginError); + loginFailed = true; + }); + + let rescheduleInterval = false; + + let oldScheduleAtInterval = SyncScheduler.prototype.scheduleAtInterval; + SyncScheduler.prototype.scheduleAtInterval = function (interval) { + rescheduleInterval = true; + do_check_eq(interval, MASTER_PASSWORD_LOCKED_RETRY_INTERVAL); + }; + + let oldVerifyLogin = Service.verifyLogin; + Service.verifyLogin = function () { + Status.login = MASTER_PASSWORD_LOCKED; + return false; + }; + + let server = sync_httpd_setup(); + yield setUp(server); + + Service.sync(); + + do_check_true(loginFailed); + do_check_eq(Status.login, MASTER_PASSWORD_LOCKED); + do_check_true(rescheduleInterval); + + Service.verifyLogin = oldVerifyLogin; + SyncScheduler.prototype.scheduleAtInterval = oldScheduleAtInterval; + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_calculateBackoff() { + do_check_eq(Status.backoffInterval, 0); + + // Test no interval larger than the maximum backoff is used if + // Status.backoffInterval is smaller. + Status.backoffInterval = 5; + let backoffInterval = Utils.calculateBackoff(50, MAXIMUM_BACKOFF_INTERVAL, + Status.backoffInterval); + + do_check_eq(backoffInterval, MAXIMUM_BACKOFF_INTERVAL); + + // Test Status.backoffInterval is used if it is + // larger than MAXIMUM_BACKOFF_INTERVAL. + Status.backoffInterval = MAXIMUM_BACKOFF_INTERVAL + 10; + backoffInterval = Utils.calculateBackoff(50, MAXIMUM_BACKOFF_INTERVAL, + Status.backoffInterval); + + do_check_eq(backoffInterval, MAXIMUM_BACKOFF_INTERVAL + 10); + + yield cleanUpAndGo(); +}); + +add_identity_test(this, function* test_scheduleNextSync_nowOrPast() { + let deferred = Promise.defer(); + Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() { + Svc.Obs.remove("weave:service:sync:finish", onSyncFinish); + cleanUpAndGo(server).then(deferred.resolve); + }); + + let server = sync_httpd_setup(); + yield setUp(server); + + // We're late for a sync... + scheduler.scheduleNextSync(-1); + yield deferred.promise; +}); + +add_identity_test(this, function* test_scheduleNextSync_future_noBackoff() { + _("scheduleNextSync() uses the current syncInterval if no interval is provided."); + // Test backoffInterval is 0 as expected. + do_check_eq(Status.backoffInterval, 0); + + _("Test setting sync interval when nextSync == 0"); + scheduler.nextSync = 0; + scheduler.scheduleNextSync(); + + // nextSync - Date.now() might be smaller than expectedInterval + // since some time has passed since we called scheduleNextSync(). + do_check_true(scheduler.nextSync - Date.now() + <= scheduler.syncInterval); + do_check_eq(scheduler.syncTimer.delay, scheduler.syncInterval); + + _("Test setting sync interval when nextSync != 0"); + scheduler.nextSync = Date.now() + scheduler.singleDeviceInterval; + scheduler.scheduleNextSync(); + + // nextSync - Date.now() might be smaller than expectedInterval + // since some time has passed since we called scheduleNextSync(). + do_check_true(scheduler.nextSync - Date.now() + <= scheduler.syncInterval); + do_check_true(scheduler.syncTimer.delay <= scheduler.syncInterval); + + _("Scheduling requests for intervals larger than the current one will be ignored."); + // Request a sync at a longer interval. The sync that's already scheduled + // for sooner takes precedence. + let nextSync = scheduler.nextSync; + let timerDelay = scheduler.syncTimer.delay; + let requestedInterval = scheduler.syncInterval * 10; + scheduler.scheduleNextSync(requestedInterval); + do_check_eq(scheduler.nextSync, nextSync); + do_check_eq(scheduler.syncTimer.delay, timerDelay); + + // We can schedule anything we want if there isn't a sync scheduled. + scheduler.nextSync = 0; + scheduler.scheduleNextSync(requestedInterval); + do_check_true(scheduler.nextSync <= Date.now() + requestedInterval); + do_check_eq(scheduler.syncTimer.delay, requestedInterval); + + // Request a sync at the smallest possible interval (0 triggers now). + scheduler.scheduleNextSync(1); + do_check_true(scheduler.nextSync <= Date.now() + 1); + do_check_eq(scheduler.syncTimer.delay, 1); + + yield cleanUpAndGo(); +}); + +add_identity_test(this, function* test_scheduleNextSync_future_backoff() { + _("scheduleNextSync() will honour backoff in all scheduling requests."); + // Let's take a backoff interval that's bigger than the default sync interval. + const BACKOFF = 7337; + Status.backoffInterval = scheduler.syncInterval + BACKOFF; + + _("Test setting sync interval when nextSync == 0"); + scheduler.nextSync = 0; + scheduler.scheduleNextSync(); + + // nextSync - Date.now() might be smaller than expectedInterval + // since some time has passed since we called scheduleNextSync(). + do_check_true(scheduler.nextSync - Date.now() + <= Status.backoffInterval); + do_check_eq(scheduler.syncTimer.delay, Status.backoffInterval); + + _("Test setting sync interval when nextSync != 0"); + scheduler.nextSync = Date.now() + scheduler.singleDeviceInterval; + scheduler.scheduleNextSync(); + + // nextSync - Date.now() might be smaller than expectedInterval + // since some time has passed since we called scheduleNextSync(). + do_check_true(scheduler.nextSync - Date.now() + <= Status.backoffInterval); + do_check_true(scheduler.syncTimer.delay <= Status.backoffInterval); + + // Request a sync at a longer interval. The sync that's already scheduled + // for sooner takes precedence. + let nextSync = scheduler.nextSync; + let timerDelay = scheduler.syncTimer.delay; + let requestedInterval = scheduler.syncInterval * 10; + do_check_true(requestedInterval > Status.backoffInterval); + scheduler.scheduleNextSync(requestedInterval); + do_check_eq(scheduler.nextSync, nextSync); + do_check_eq(scheduler.syncTimer.delay, timerDelay); + + // We can schedule anything we want if there isn't a sync scheduled. + scheduler.nextSync = 0; + scheduler.scheduleNextSync(requestedInterval); + do_check_true(scheduler.nextSync <= Date.now() + requestedInterval); + do_check_eq(scheduler.syncTimer.delay, requestedInterval); + + // Request a sync at the smallest possible interval (0 triggers now). + scheduler.scheduleNextSync(1); + do_check_true(scheduler.nextSync <= Date.now() + Status.backoffInterval); + do_check_eq(scheduler.syncTimer.delay, Status.backoffInterval); + + yield cleanUpAndGo(); +}); + +add_identity_test(this, function* test_handleSyncError() { + let server = sync_httpd_setup(); + yield setUp(server); + + // Force sync to fail. + Svc.Prefs.set("firstSync", "notReady"); + + _("Ensure expected initial environment."); + do_check_eq(scheduler._syncErrors, 0); + do_check_false(Status.enforceBackoff); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + do_check_eq(Status.backoffInterval, 0); + + // Trigger sync with an error several times & observe + // functionality of handleSyncError() + _("Test first error calls scheduleNextSync on default interval"); + Service.sync(); + do_check_true(scheduler.nextSync <= Date.now() + scheduler.singleDeviceInterval); + do_check_eq(scheduler.syncTimer.delay, scheduler.singleDeviceInterval); + do_check_eq(scheduler._syncErrors, 1); + do_check_false(Status.enforceBackoff); + scheduler.syncTimer.clear(); + + _("Test second error still calls scheduleNextSync on default interval"); + Service.sync(); + do_check_true(scheduler.nextSync <= Date.now() + scheduler.singleDeviceInterval); + do_check_eq(scheduler.syncTimer.delay, scheduler.singleDeviceInterval); + do_check_eq(scheduler._syncErrors, 2); + do_check_false(Status.enforceBackoff); + scheduler.syncTimer.clear(); + + _("Test third error sets Status.enforceBackoff and calls scheduleAtInterval"); + Service.sync(); + let maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL); + do_check_eq(Status.backoffInterval, 0); + do_check_true(scheduler.nextSync <= (Date.now() + maxInterval)); + do_check_true(scheduler.syncTimer.delay <= maxInterval); + do_check_eq(scheduler._syncErrors, 3); + do_check_true(Status.enforceBackoff); + + // Status.enforceBackoff is false but there are still errors. + Status.resetBackoff(); + do_check_false(Status.enforceBackoff); + do_check_eq(scheduler._syncErrors, 3); + scheduler.syncTimer.clear(); + + _("Test fourth error still calls scheduleAtInterval even if enforceBackoff was reset"); + Service.sync(); + maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL); + do_check_true(scheduler.nextSync <= Date.now() + maxInterval); + do_check_true(scheduler.syncTimer.delay <= maxInterval); + do_check_eq(scheduler._syncErrors, 4); + do_check_true(Status.enforceBackoff); + scheduler.syncTimer.clear(); + + _("Arrange for a successful sync to reset the scheduler error count"); + let deferred = Promise.defer(); + Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() { + Svc.Obs.remove("weave:service:sync:finish", onSyncFinish); + cleanUpAndGo(server).then(deferred.resolve); + }); + Svc.Prefs.set("firstSync", "wipeRemote"); + scheduler.scheduleNextSync(-1); + yield deferred.promise; +}); + +add_identity_test(this, function* test_client_sync_finish_updateClientMode() { + let server = sync_httpd_setup(); + yield setUp(server); + + // Confirm defaults. + do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + do_check_false(scheduler.idle); + + // Trigger a change in interval & threshold by adding a client. + clientsEngine._store.create({id: "foo", cleartext: "bar"}); + do_check_false(scheduler.numClients > 1); + scheduler.updateClientMode(); + Service.sync(); + + do_check_eq(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD); + do_check_eq(scheduler.syncInterval, scheduler.activeInterval); + do_check_true(scheduler.numClients > 1); + do_check_false(scheduler.idle); + + // Resets the number of clients to 0. + clientsEngine.resetClient(); + Service.sync(); + + // Goes back to single user if # clients is 1. + do_check_eq(scheduler.numClients, 1); + do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + do_check_false(scheduler.numClients > 1); + do_check_false(scheduler.idle); + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_autoconnect_nextSync_past() { + let deferred = Promise.defer(); + // nextSync will be 0 by default, so it's way in the past. + + Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() { + Svc.Obs.remove("weave:service:sync:finish", onSyncFinish); + cleanUpAndGo(server).then(deferred.resolve); + }); + + let server = sync_httpd_setup(); + yield setUp(server); + + scheduler.delayedAutoConnect(0); + yield deferred.promise; +}); + +add_identity_test(this, function* test_autoconnect_nextSync_future() { + let deferred = Promise.defer(); + let previousSync = Date.now() + scheduler.syncInterval / 2; + scheduler.nextSync = previousSync; + // nextSync rounds to the nearest second. + let expectedSync = scheduler.nextSync; + let expectedInterval = expectedSync - Date.now() - 1000; + + // Ensure we don't actually try to sync (or log in for that matter). + function onLoginStart() { + do_throw("Should not get here!"); + } + Svc.Obs.add("weave:service:login:start", onLoginStart); + + waitForZeroTimer(function () { + do_check_eq(scheduler.nextSync, expectedSync); + do_check_true(scheduler.syncTimer.delay >= expectedInterval); + + Svc.Obs.remove("weave:service:login:start", onLoginStart); + cleanUpAndGo().then(deferred.resolve); + }); + + yield configureIdentity({username: "johndoe"}); + scheduler.delayedAutoConnect(0); + yield deferred.promise; +}); + +// XXX - this test can't be run with the browserid identity as it relies +// on the syncKey getter behaving in a certain way... +add_task(function* test_autoconnect_mp_locked() { + let server = sync_httpd_setup(); + yield setUp(server); + + // Pretend user did not unlock master password. + let origLocked = Utils.mpLocked; + Utils.mpLocked = () => true; + + let origGetter = Service.identity.__lookupGetter__("syncKey"); + let origSetter = Service.identity.__lookupSetter__("syncKey"); + delete Service.identity.syncKey; + Service.identity.__defineGetter__("syncKey", function() { + _("Faking Master Password entry cancelation."); + throw "User canceled Master Password entry"; + }); + + let deferred = Promise.defer(); + // A locked master password will still trigger a sync, but then we'll hit + // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL. + Svc.Obs.add("weave:service:login:error", function onLoginError() { + Svc.Obs.remove("weave:service:login:error", onLoginError); + Utils.nextTick(function aLittleBitAfterLoginError() { + do_check_eq(Status.login, MASTER_PASSWORD_LOCKED); + + Utils.mpLocked = origLocked; + delete Service.identity.syncKey; + Service.identity.__defineGetter__("syncKey", origGetter); + Service.identity.__defineSetter__("syncKey", origSetter); + + cleanUpAndGo(server).then(deferred.resolve); + }); + }); + + scheduler.delayedAutoConnect(0); + yield deferred.promise; +}); + +add_identity_test(this, function* test_no_autoconnect_during_wizard() { + let server = sync_httpd_setup(); + yield setUp(server); + + // Simulate the Sync setup wizard. + Svc.Prefs.set("firstSync", "notReady"); + + // Ensure we don't actually try to sync (or log in for that matter). + function onLoginStart() { + do_throw("Should not get here!"); + } + Svc.Obs.add("weave:service:login:start", onLoginStart); + + let deferred = Promise.defer(); + waitForZeroTimer(function () { + Svc.Obs.remove("weave:service:login:start", onLoginStart); + cleanUpAndGo(server).then(deferred.resolve); + }); + + scheduler.delayedAutoConnect(0); + yield deferred.promise; +}); + +add_identity_test(this, function* test_no_autoconnect_status_not_ok() { + let server = sync_httpd_setup(); + + // Ensure we don't actually try to sync (or log in for that matter). + function onLoginStart() { + do_throw("Should not get here!"); + } + Svc.Obs.add("weave:service:login:start", onLoginStart); + + let deferred = Promise.defer(); + waitForZeroTimer(function () { + Svc.Obs.remove("weave:service:login:start", onLoginStart); + + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME); + + cleanUpAndGo(server).then(deferred.resolve); + }); + + scheduler.delayedAutoConnect(0); + yield deferred.promise; +}); + +add_identity_test(this, function* test_autoconnectDelay_pref() { + let deferred = Promise.defer(); + Svc.Obs.add("weave:service:sync:finish", function onSyncFinish() { + Svc.Obs.remove("weave:service:sync:finish", onSyncFinish); + cleanUpAndGo(server).then(deferred.resolve); + }); + + Svc.Prefs.set("autoconnectDelay", 1); + + let server = sync_httpd_setup(); + yield setUp(server); + + Svc.Obs.notify("weave:service:ready"); + + // autoconnectDelay pref is multiplied by 1000. + do_check_eq(scheduler._autoTimer.delay, 1000); + do_check_eq(Status.service, STATUS_OK); + yield deferred.promise; +}); + +add_identity_test(this, function* test_idle_adjustSyncInterval() { + // Confirm defaults. + do_check_eq(scheduler.idle, false); + + // Single device: nothing changes. + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + do_check_eq(scheduler.idle, true); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + + // Multiple devices: switch to idle interval. + scheduler.idle = false; + clientsEngine._store.create({id: "foo", cleartext: "bar"}); + scheduler.updateClientMode(); + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + do_check_eq(scheduler.idle, true); + do_check_eq(scheduler.syncInterval, scheduler.idleInterval); + + yield cleanUpAndGo(); +}); + +add_identity_test(this, function* test_back_triggersSync() { + // Confirm defaults. + do_check_false(scheduler.idle); + do_check_eq(Status.backoffInterval, 0); + + // Set up: Define 2 clients and put the system in idle. + scheduler.numClients = 2; + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + do_check_true(scheduler.idle); + + let deferred = Promise.defer(); + // We don't actually expect the sync (or the login, for that matter) to + // succeed. We just want to ensure that it was attempted. + Svc.Obs.add("weave:service:login:error", function onLoginError() { + Svc.Obs.remove("weave:service:login:error", onLoginError); + cleanUpAndGo().then(deferred.resolve); + }); + + // Send an 'active' event to trigger sync soonish. + scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime")); + yield deferred.promise; +}); + +add_identity_test(this, function* test_active_triggersSync_observesBackoff() { + // Confirm defaults. + do_check_false(scheduler.idle); + + // Set up: Set backoff, define 2 clients and put the system in idle. + const BACKOFF = 7337; + Status.backoffInterval = scheduler.idleInterval + BACKOFF; + scheduler.numClients = 2; + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + do_check_eq(scheduler.idle, true); + + function onLoginStart() { + do_throw("Shouldn't have kicked off a sync!"); + } + Svc.Obs.add("weave:service:login:start", onLoginStart); + + let deferred = Promise.defer(); + timer = Utils.namedTimer(function () { + Svc.Obs.remove("weave:service:login:start", onLoginStart); + + do_check_true(scheduler.nextSync <= Date.now() + Status.backoffInterval); + do_check_eq(scheduler.syncTimer.delay, Status.backoffInterval); + + cleanUpAndGo().then(deferred.resolve); + }, IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer"); + + // Send an 'active' event to try to trigger sync soonish. + scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime")); + yield deferred.promise; +}); + +add_identity_test(this, function* test_back_debouncing() { + _("Ensure spurious back-then-idle events, as observed on OS X, don't trigger a sync."); + + // Confirm defaults. + do_check_eq(scheduler.idle, false); + + // Set up: Define 2 clients and put the system in idle. + scheduler.numClients = 2; + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + do_check_eq(scheduler.idle, true); + + function onLoginStart() { + do_throw("Shouldn't have kicked off a sync!"); + } + Svc.Obs.add("weave:service:login:start", onLoginStart); + + // Create spurious back-then-idle events as observed on OS X: + scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime")); + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + + let deferred = Promise.defer(); + timer = Utils.namedTimer(function () { + Svc.Obs.remove("weave:service:login:start", onLoginStart); + cleanUpAndGo().then(deferred.resolve); + }, IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer"); + yield deferred.promise; +}); + +add_identity_test(this, function* test_no_sync_node() { + // Test when Status.sync == NO_SYNC_NODE_FOUND + // it is not overwritten on sync:finish + let server = sync_httpd_setup(); + yield setUp(server); + + Service.serverURL = server.baseURI + "/"; + + Service.sync(); + do_check_eq(Status.sync, NO_SYNC_NODE_FOUND); + do_check_eq(scheduler.syncTimer.delay, NO_SYNC_NODE_INTERVAL); + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_sync_failed_partial_500s() { + _("Test a 5xx status calls handleSyncError."); + scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF; + let server = sync_httpd_setup(); + + let engine = Service.engineManager.get("catapult"); + engine.enabled = true; + engine.exception = {status: 500}; + + do_check_eq(Status.sync, SYNC_SUCCEEDED); + + do_check_true(yield setUp(server)); + + Service.sync(); + + do_check_eq(Status.service, SYNC_FAILED_PARTIAL); + + let maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL); + do_check_eq(Status.backoffInterval, 0); + do_check_true(Status.enforceBackoff); + do_check_eq(scheduler._syncErrors, 4); + do_check_true(scheduler.nextSync <= (Date.now() + maxInterval)); + do_check_true(scheduler.syncTimer.delay <= maxInterval); + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_sync_failed_partial_400s() { + _("Test a non-5xx status doesn't call handleSyncError."); + scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF; + let server = sync_httpd_setup(); + + let engine = Service.engineManager.get("catapult"); + engine.enabled = true; + engine.exception = {status: 400}; + + // Have multiple devices for an active interval. + clientsEngine._store.create({id: "foo", cleartext: "bar"}); + + do_check_eq(Status.sync, SYNC_SUCCEEDED); + + do_check_true(yield setUp(server)); + + Service.sync(); + + do_check_eq(Status.service, SYNC_FAILED_PARTIAL); + do_check_eq(scheduler.syncInterval, scheduler.activeInterval); + + do_check_eq(Status.backoffInterval, 0); + do_check_false(Status.enforceBackoff); + do_check_eq(scheduler._syncErrors, 0); + do_check_true(scheduler.nextSync <= (Date.now() + scheduler.activeInterval)); + do_check_true(scheduler.syncTimer.delay <= scheduler.activeInterval); + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_sync_X_Weave_Backoff() { + let server = sync_httpd_setup(); + yield setUp(server); + + // Use an odd value on purpose so that it doesn't happen to coincide with one + // of the sync intervals. + const BACKOFF = 7337; + + // Extend info/collections so that we can put it into server maintenance mode. + const INFO_COLLECTIONS = "/1.1/johndoe/info/collections"; + let infoColl = server._handler._overridePaths[INFO_COLLECTIONS]; + let serverBackoff = false; + function infoCollWithBackoff(request, response) { + if (serverBackoff) { + response.setHeader("X-Weave-Backoff", "" + BACKOFF); + } + infoColl(request, response); + } + server.registerPathHandler(INFO_COLLECTIONS, infoCollWithBackoff); + + // Pretend we have two clients so that the regular sync interval is + // sufficiently low. + clientsEngine._store.create({id: "foo", cleartext: "bar"}); + let rec = clientsEngine._store.createRecord("foo", "clients"); + rec.encrypt(Service.collectionKeys.keyForCollection("clients")); + rec.upload(Service.resource(clientsEngine.engineURL + rec.id)); + + // Sync once to log in and get everything set up. Let's verify our initial + // values. + Service.sync(); + do_check_eq(Status.backoffInterval, 0); + do_check_eq(Status.minimumNextSync, 0); + do_check_eq(scheduler.syncInterval, scheduler.activeInterval); + do_check_true(scheduler.nextSync <= + Date.now() + scheduler.syncInterval); + // Sanity check that we picked the right value for BACKOFF: + do_check_true(scheduler.syncInterval < BACKOFF * 1000); + + // Turn on server maintenance and sync again. + serverBackoff = true; + Service.sync(); + + do_check_true(Status.backoffInterval >= BACKOFF * 1000); + // Allowing 3 seconds worth of of leeway between when Status.minimumNextSync + // was set and when this line gets executed. + let minimumExpectedDelay = (BACKOFF - 3) * 1000; + do_check_true(Status.minimumNextSync >= Date.now() + minimumExpectedDelay); + + // Verify that the next sync is actually going to wait that long. + do_check_true(scheduler.nextSync >= Date.now() + minimumExpectedDelay); + do_check_true(scheduler.syncTimer.delay >= minimumExpectedDelay); + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_sync_503_Retry_After() { + let server = sync_httpd_setup(); + yield setUp(server); + + // Use an odd value on purpose so that it doesn't happen to coincide with one + // of the sync intervals. + const BACKOFF = 7337; + + // Extend info/collections so that we can put it into server maintenance mode. + const INFO_COLLECTIONS = "/1.1/johndoe/info/collections"; + let infoColl = server._handler._overridePaths[INFO_COLLECTIONS]; + let serverMaintenance = false; + function infoCollWithMaintenance(request, response) { + if (!serverMaintenance) { + infoColl(request, response); + return; + } + response.setHeader("Retry-After", "" + BACKOFF); + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); + } + server.registerPathHandler(INFO_COLLECTIONS, infoCollWithMaintenance); + + // Pretend we have two clients so that the regular sync interval is + // sufficiently low. + clientsEngine._store.create({id: "foo", cleartext: "bar"}); + let rec = clientsEngine._store.createRecord("foo", "clients"); + rec.encrypt(Service.collectionKeys.keyForCollection("clients")); + rec.upload(Service.resource(clientsEngine.engineURL + rec.id)); + + // Sync once to log in and get everything set up. Let's verify our initial + // values. + Service.sync(); + do_check_false(Status.enforceBackoff); + do_check_eq(Status.backoffInterval, 0); + do_check_eq(Status.minimumNextSync, 0); + do_check_eq(scheduler.syncInterval, scheduler.activeInterval); + do_check_true(scheduler.nextSync <= + Date.now() + scheduler.syncInterval); + // Sanity check that we picked the right value for BACKOFF: + do_check_true(scheduler.syncInterval < BACKOFF * 1000); + + // Turn on server maintenance and sync again. + serverMaintenance = true; + Service.sync(); + + do_check_true(Status.enforceBackoff); + do_check_true(Status.backoffInterval >= BACKOFF * 1000); + // Allowing 3 seconds worth of of leeway between when Status.minimumNextSync + // was set and when this line gets executed. + let minimumExpectedDelay = (BACKOFF - 3) * 1000; + do_check_true(Status.minimumNextSync >= Date.now() + minimumExpectedDelay); + + // Verify that the next sync is actually going to wait that long. + do_check_true(scheduler.nextSync >= Date.now() + minimumExpectedDelay); + do_check_true(scheduler.syncTimer.delay >= minimumExpectedDelay); + + yield cleanUpAndGo(server); +}); + +add_identity_test(this, function* test_loginError_recoverable_reschedules() { + _("Verify that a recoverable login error schedules a new sync."); + yield configureIdentity({username: "johndoe"}); + Service.serverURL = "http://localhost:1234/"; + Service.clusterURL = Service.serverURL; + Service.persistLogin(); + Status.resetSync(); // reset Status.login + + let deferred = Promise.defer(); + Svc.Obs.add("weave:service:login:error", function onLoginError() { + Svc.Obs.remove("weave:service:login:error", onLoginError); + Utils.nextTick(function aLittleBitAfterLoginError() { + do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR); + + let expectedNextSync = Date.now() + scheduler.syncInterval; + do_check_true(scheduler.nextSync > Date.now()); + do_check_true(scheduler.nextSync <= expectedNextSync); + do_check_true(scheduler.syncTimer.delay > 0); + do_check_true(scheduler.syncTimer.delay <= scheduler.syncInterval); + + Svc.Obs.remove("weave:service:sync:start", onSyncStart); + cleanUpAndGo().then(deferred.resolve); + }); + }); + + // Let's set it up so that a sync is overdue, both in terms of previously + // scheduled syncs and the global score. We still do not expect an immediate + // sync because we just tried (duh). + scheduler.nextSync = Date.now() - 100000; + scheduler.globalScore = SINGLE_USER_THRESHOLD + 1; + function onSyncStart() { + do_throw("Shouldn't have started a sync!"); + } + Svc.Obs.add("weave:service:sync:start", onSyncStart); + + // Sanity check. + do_check_eq(scheduler.syncTimer, null); + do_check_eq(Status.checkSetup(), STATUS_OK); + do_check_eq(Status.login, LOGIN_SUCCEEDED); + + scheduler.scheduleNextSync(0); + yield deferred.promise; +}); + +add_identity_test(this, function* test_loginError_fatal_clearsTriggers() { + _("Verify that a fatal login error clears sync triggers."); + yield configureIdentity({username: "johndoe"}); + + let server = httpd_setup({ + "/1.1/johndoe/info/collections": httpd_handler(401, "Unauthorized") + }); + + Service.serverURL = server.baseURI + "/"; + Service.clusterURL = Service.serverURL; + Service.persistLogin(); + Status.resetSync(); // reset Status.login + + let deferred = Promise.defer(); + Svc.Obs.add("weave:service:login:error", function onLoginError() { + Svc.Obs.remove("weave:service:login:error", onLoginError); + Utils.nextTick(function aLittleBitAfterLoginError() { + + if (isConfiguredWithLegacyIdentity()) { + // for the "legacy" identity, a 401 on info/collections means the + // password is wrong, so we enter a "login rejected" state. + do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED); + + do_check_eq(scheduler.nextSync, 0); + do_check_eq(scheduler.syncTimer, null); + } else { + // For the FxA identity, a 401 on info/collections means a transient + // error, probably due to an inability to fetch a token. + do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR); + // syncs should still be scheduled. + do_check_true(scheduler.nextSync > Date.now()); + do_check_true(scheduler.syncTimer.delay > 0); + } + cleanUpAndGo(server).then(deferred.resolve); + }); + }); + + // Sanity check. + do_check_eq(scheduler.nextSync, 0); + do_check_eq(scheduler.syncTimer, null); + do_check_eq(Status.checkSetup(), STATUS_OK); + do_check_eq(Status.login, LOGIN_SUCCEEDED); + + scheduler.scheduleNextSync(0); + yield deferred.promise; +}); + +add_identity_test(this, function* test_proper_interval_on_only_failing() { + _("Ensure proper behavior when only failed records are applied."); + + // If an engine reports that no records succeeded, we shouldn't decrease the + // sync interval. + do_check_false(scheduler.hasIncomingItems); + const INTERVAL = 10000000; + scheduler.syncInterval = INTERVAL; + + Svc.Obs.notify("weave:service:sync:applied", { + applied: 2, + succeeded: 0, + failed: 2, + newFailed: 2, + reconciled: 0 + }); + + let deferred = Promise.defer(); + Utils.nextTick(function() { + scheduler.adjustSyncInterval(); + do_check_false(scheduler.hasIncomingItems); + do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval); + + deferred.resolve(); + }); + yield deferred.promise; +}); |