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