diff options
Diffstat (limited to 'services/sync/tests/unit/test_syncscheduler.js')
-rw-r--r-- | services/sync/tests/unit/test_syncscheduler.js | 1151 |
1 files changed, 1151 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 0000000000..7faccbc911 --- /dev/null +++ b/services/sync/tests/unit/test_syncscheduler.js @@ -0,0 +1,1151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { SyncAuthManager } = ChromeUtils.importESModule( + "resource://services-sync/sync_auth.sys.mjs" +); +const { SyncScheduler } = ChromeUtils.importESModule( + "resource://services-sync/policies.sys.mjs" +); +const { Service } = ChromeUtils.importESModule( + "resource://services-sync/service.sys.mjs" +); +const { Status } = ChromeUtils.importESModule( + "resource://services-sync/status.sys.mjs" +); + +function CatapultEngine() { + SyncEngine.call(this, "Catapult", Service); +} +CatapultEngine.prototype = { + exception: null, // tests fill this in + async _sync() { + throw this.exception; + }, +}; +Object.setPrototypeOf(CatapultEngine.prototype, SyncEngine.prototype); + +var scheduler = new SyncScheduler(Service); +let clientsEngine; + +async function sync_httpd_setup() { + let clientsSyncID = await clientsEngine.resetLocalSyncID(); + let global = new ServerWBO("global", { + syncID: Service.syncID, + storageVersion: STORAGE_VERSION, + engines: { + clients: { version: clientsEngine.version, syncID: clientsSyncID }, + }, + }); + 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@mozilla.com/storage/meta/global": upd( + "meta", + global.handler() + ), + "/1.1/johndoe@mozilla.com/info/collections": collectionsHelper.handler, + "/1.1/johndoe@mozilla.com/storage/crypto/keys": upd( + "crypto", + new ServerWBO("keys").handler() + ), + "/1.1/johndoe@mozilla.com/storage/clients": upd( + "clients", + clientsColl.handler() + ), + }); +} + +async function setUp(server) { + await configureIdentity({ username: "johndoe@mozilla.com" }, server); + + await generateNewKeys(Service.collectionKeys); + let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); + await serverKeys.encrypt(Service.identity.syncKeyBundle); + let result = ( + await serverKeys.upload(Service.resource(Service.cryptoKeysURL)) + ).success; + return result; +} + +async function cleanUpAndGo(server) { + await Async.promiseYield(); + await clientsEngine._store.wipe(); + await Service.startOver(); + // Re-enable logging, which we just disabled. + syncTestLogging(); + if (server) { + await promiseStopServer(server); + } +} + +add_task(async function setup() { + await Service.promiseInitialized; + 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 = async id => {}; + await Service.engineManager.clear(); + + validate_all_future_pings(); + + scheduler.setDefaults(); + + await Service.engineManager.register(CatapultEngine); +}); + +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." + ); + Assert.equal(scheduler.nextSync, 0); + scheduler.nextSync = TIMESTAMP1; + Assert.equal(scheduler.nextSync, Math.floor(TIMESTAMP1 / 1000) * 1000); + + _("'syncInterval' defaults to singleDeviceInterval."); + Assert.equal(Svc.Prefs.get("syncInterval"), undefined); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + + _("'syncInterval' corresponds to a preference setting."); + scheduler.syncInterval = INTERVAL; + Assert.equal(scheduler.syncInterval, INTERVAL); + Assert.equal(Svc.Prefs.get("syncInterval"), INTERVAL); + + _( + "'syncThreshold' corresponds to preference, defaults to SINGLE_USER_THRESHOLD" + ); + Assert.equal(Svc.Prefs.get("syncThreshold"), undefined); + Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + scheduler.syncThreshold = THRESHOLD; + Assert.equal(scheduler.syncThreshold, THRESHOLD); + + _("'globalScore' corresponds to preference, defaults to zero."); + Assert.equal(Svc.Prefs.get("globalScore"), 0); + Assert.equal(scheduler.globalScore, 0); + scheduler.globalScore = SCORE; + Assert.equal(scheduler.globalScore, SCORE); + Assert.equal(Svc.Prefs.get("globalScore"), SCORE); + + _("Intervals correspond to default preferences."); + Assert.equal( + scheduler.singleDeviceInterval, + Svc.Prefs.get("scheduler.fxa.singleDeviceInterval") * 1000 + ); + Assert.equal( + scheduler.idleInterval, + Svc.Prefs.get("scheduler.idleInterval") * 1000 + ); + Assert.equal( + scheduler.activeInterval, + Svc.Prefs.get("scheduler.activeInterval") * 1000 + ); + Assert.equal( + scheduler.immediateInterval, + Svc.Prefs.get("scheduler.immediateInterval") * 1000 + ); + + _("Custom values for prefs will take effect after a restart."); + Svc.Prefs.set("scheduler.fxa.singleDeviceInterval", 420); + Svc.Prefs.set("scheduler.idleInterval", 230); + Svc.Prefs.set("scheduler.activeInterval", 180); + Svc.Prefs.set("scheduler.immediateInterval", 31415); + scheduler.setDefaults(); + Assert.equal(scheduler.idleInterval, 230000); + Assert.equal(scheduler.singleDeviceInterval, 420000); + Assert.equal(scheduler.activeInterval, 180000); + Assert.equal(scheduler.immediateInterval, 31415000); + + _("Custom values for interval prefs can't be less than 60 seconds."); + Svc.Prefs.set("scheduler.fxa.singleDeviceInterval", 42); + Svc.Prefs.set("scheduler.idleInterval", 50); + Svc.Prefs.set("scheduler.activeInterval", 50); + Svc.Prefs.set("scheduler.immediateInterval", 10); + scheduler.setDefaults(); + Assert.equal(scheduler.idleInterval, 60000); + Assert.equal(scheduler.singleDeviceInterval, 60000); + Assert.equal(scheduler.activeInterval, 60000); + Assert.equal(scheduler.immediateInterval, 60000); + + Svc.Prefs.resetBranch(""); + scheduler.setDefaults(); + run_next_test(); +}); + +add_task(async function test_sync_skipped_low_score_no_resync() { + enableValidationPrefs(); + let server = await sync_httpd_setup(); + + function SkipEngine() { + SyncEngine.call(this, "Skip", Service); + this.syncs = 0; + } + + SkipEngine.prototype = { + _sync() { + do_throw("Should have been skipped"); + }, + shouldSkipSync() { + return true; + }, + }; + Object.setPrototypeOf(SkipEngine.prototype, SyncEngine.prototype); + await Service.engineManager.register(SkipEngine); + + let engine = Service.engineManager.get("skip"); + engine.enabled = true; + engine._tracker._score = 30; + + Assert.equal(Status.sync, SYNC_SUCCEEDED); + + Assert.ok(await setUp(server)); + + let resyncDoneObserver = promiseOneObserver("weave:service:resyncs-finished"); + + let synced = false; + function onSyncStarted() { + Assert.ok(!synced, "Only should sync once"); + synced = true; + } + + await Service.sync(); + + Assert.equal(Status.sync, SYNC_SUCCEEDED); + + Svc.Obs.add("weave:service:sync:start", onSyncStarted); + await resyncDoneObserver; + + Svc.Obs.remove("weave:service:sync:start", onSyncStarted); + engine._tracker._store = 0; + await cleanUpAndGo(server); +}); + +add_task(async function test_updateClientMode() { + _( + "Test updateClientMode adjusts scheduling attributes based on # of clients appropriately" + ); + Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + Assert.equal(false, scheduler.numClients > 1); + Assert.ok(!scheduler.idle); + + // Trigger a change in interval & threshold by noting there are multiple clients. + Svc.Prefs.set("clients.devices.desktop", 1); + Svc.Prefs.set("clients.devices.mobile", 1); + scheduler.updateClientMode(); + + Assert.equal(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD); + Assert.equal(scheduler.syncInterval, scheduler.activeInterval); + Assert.ok(scheduler.numClients > 1); + Assert.ok(!scheduler.idle); + + // Resets the number of clients to 0. + await clientsEngine.resetClient(); + Svc.Prefs.reset("clients.devices.mobile"); + scheduler.updateClientMode(); + + // Goes back to single user if # clients is 1. + Assert.equal(scheduler.numClients, 1); + Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + Assert.equal(false, scheduler.numClients > 1); + Assert.ok(!scheduler.idle); + + await cleanUpAndGo(); +}); + +add_task(async function test_masterpassword_locked_retry_interval() { + enableValidationPrefs(); + + _( + "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; + Assert.equal(interval, MASTER_PASSWORD_LOCKED_RETRY_INTERVAL); + }; + + let oldVerifyLogin = Service.verifyLogin; + Service.verifyLogin = async function () { + Status.login = MASTER_PASSWORD_LOCKED; + return false; + }; + + let server = await sync_httpd_setup(); + await setUp(server); + + await Service.sync(); + + Assert.ok(loginFailed); + Assert.equal(Status.login, MASTER_PASSWORD_LOCKED); + Assert.ok(rescheduleInterval); + + Service.verifyLogin = oldVerifyLogin; + SyncScheduler.prototype.scheduleAtInterval = oldScheduleAtInterval; + + await cleanUpAndGo(server); +}); + +add_task(async function test_calculateBackoff() { + Assert.equal(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 + ); + + Assert.equal(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 + ); + + Assert.equal(backoffInterval, MAXIMUM_BACKOFF_INTERVAL + 10); + + await cleanUpAndGo(); +}); + +add_task(async function test_scheduleNextSync_nowOrPast() { + enableValidationPrefs(); + + let promiseObserved = promiseOneObserver("weave:service:sync:finish"); + + let server = await sync_httpd_setup(); + await setUp(server); + + // We're late for a sync... + scheduler.scheduleNextSync(-1); + await promiseObserved; + await cleanUpAndGo(server); +}); + +add_task(async function test_scheduleNextSync_future_noBackoff() { + enableValidationPrefs(); + + _( + "scheduleNextSync() uses the current syncInterval if no interval is provided." + ); + // Test backoffInterval is 0 as expected. + Assert.equal(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(). + Assert.ok(scheduler.nextSync - Date.now() <= scheduler.syncInterval); + Assert.equal(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(). + Assert.ok(scheduler.nextSync - Date.now() <= scheduler.syncInterval); + Assert.ok(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); + Assert.equal(scheduler.nextSync, nextSync); + Assert.equal(scheduler.syncTimer.delay, timerDelay); + + // We can schedule anything we want if there isn't a sync scheduled. + scheduler.nextSync = 0; + scheduler.scheduleNextSync(requestedInterval); + Assert.ok(scheduler.nextSync <= Date.now() + requestedInterval); + Assert.equal(scheduler.syncTimer.delay, requestedInterval); + + // Request a sync at the smallest possible interval (0 triggers now). + scheduler.scheduleNextSync(1); + Assert.ok(scheduler.nextSync <= Date.now() + 1); + Assert.equal(scheduler.syncTimer.delay, 1); + + await cleanUpAndGo(); +}); + +add_task(async function test_scheduleNextSync_future_backoff() { + enableValidationPrefs(); + + _("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(). + Assert.ok(scheduler.nextSync - Date.now() <= Status.backoffInterval); + Assert.equal(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(). + Assert.ok(scheduler.nextSync - Date.now() <= Status.backoffInterval); + Assert.ok(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; + Assert.ok(requestedInterval > Status.backoffInterval); + scheduler.scheduleNextSync(requestedInterval); + Assert.equal(scheduler.nextSync, nextSync); + Assert.equal(scheduler.syncTimer.delay, timerDelay); + + // We can schedule anything we want if there isn't a sync scheduled. + scheduler.nextSync = 0; + scheduler.scheduleNextSync(requestedInterval); + Assert.ok(scheduler.nextSync <= Date.now() + requestedInterval); + Assert.equal(scheduler.syncTimer.delay, requestedInterval); + + // Request a sync at the smallest possible interval (0 triggers now). + scheduler.scheduleNextSync(1); + Assert.ok(scheduler.nextSync <= Date.now() + Status.backoffInterval); + Assert.equal(scheduler.syncTimer.delay, Status.backoffInterval); + + await cleanUpAndGo(); +}); + +add_task(async function test_handleSyncError() { + enableValidationPrefs(); + + let server = await sync_httpd_setup(); + await setUp(server); + + // Force sync to fail. + Svc.Prefs.set("firstSync", "notReady"); + + _("Ensure expected initial environment."); + Assert.equal(scheduler._syncErrors, 0); + Assert.ok(!Status.enforceBackoff); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + Assert.equal(Status.backoffInterval, 0); + + // Trigger sync with an error several times & observe + // functionality of handleSyncError() + _("Test first error calls scheduleNextSync on default interval"); + await Service.sync(); + Assert.ok(scheduler.nextSync <= Date.now() + scheduler.singleDeviceInterval); + Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval); + Assert.equal(scheduler._syncErrors, 1); + Assert.ok(!Status.enforceBackoff); + scheduler.syncTimer.clear(); + + _("Test second error still calls scheduleNextSync on default interval"); + await Service.sync(); + Assert.ok(scheduler.nextSync <= Date.now() + scheduler.singleDeviceInterval); + Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval); + Assert.equal(scheduler._syncErrors, 2); + Assert.ok(!Status.enforceBackoff); + scheduler.syncTimer.clear(); + + _("Test third error sets Status.enforceBackoff and calls scheduleAtInterval"); + await Service.sync(); + let maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL); + Assert.equal(Status.backoffInterval, 0); + Assert.ok(scheduler.nextSync <= Date.now() + maxInterval); + Assert.ok(scheduler.syncTimer.delay <= maxInterval); + Assert.equal(scheduler._syncErrors, 3); + Assert.ok(Status.enforceBackoff); + + // Status.enforceBackoff is false but there are still errors. + Status.resetBackoff(); + Assert.ok(!Status.enforceBackoff); + Assert.equal(scheduler._syncErrors, 3); + scheduler.syncTimer.clear(); + + _( + "Test fourth error still calls scheduleAtInterval even if enforceBackoff was reset" + ); + await Service.sync(); + maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL); + Assert.ok(scheduler.nextSync <= Date.now() + maxInterval); + Assert.ok(scheduler.syncTimer.delay <= maxInterval); + Assert.equal(scheduler._syncErrors, 4); + Assert.ok(Status.enforceBackoff); + scheduler.syncTimer.clear(); + + _("Arrange for a successful sync to reset the scheduler error count"); + let promiseObserved = promiseOneObserver("weave:service:sync:finish"); + Svc.Prefs.set("firstSync", "wipeRemote"); + scheduler.scheduleNextSync(-1); + await promiseObserved; + await cleanUpAndGo(server); +}); + +add_task(async function test_client_sync_finish_updateClientMode() { + enableValidationPrefs(); + + let server = await sync_httpd_setup(); + await setUp(server); + + // Confirm defaults. + Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + Assert.ok(!scheduler.idle); + + // Trigger a change in interval & threshold by adding a client. + await clientsEngine._store.create({ + id: "foo", + cleartext: { os: "mobile", version: "0.01", type: "desktop" }, + }); + Assert.equal(false, scheduler.numClients > 1); + scheduler.updateClientMode(); + await Service.sync(); + + Assert.equal(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD); + Assert.equal(scheduler.syncInterval, scheduler.activeInterval); + Assert.ok(scheduler.numClients > 1); + Assert.ok(!scheduler.idle); + + // Resets the number of clients to 0. + await clientsEngine.resetClient(); + // Also re-init the server, or we suck our "foo" client back down. + await setUp(server); + + await Service.sync(); + + // Goes back to single user if # clients is 1. + Assert.equal(scheduler.numClients, 1); + Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + Assert.equal(false, scheduler.numClients > 1); + Assert.ok(!scheduler.idle); + + await cleanUpAndGo(server); +}); + +add_task(async function test_autoconnect_nextSync_past() { + enableValidationPrefs(); + + let promiseObserved = promiseOneObserver("weave:service:sync:finish"); + // nextSync will be 0 by default, so it's way in the past. + + let server = await sync_httpd_setup(); + await setUp(server); + + scheduler.autoConnect(); + await promiseObserved; + await cleanUpAndGo(server); +}); + +add_task(async function test_autoconnect_nextSync_future() { + enableValidationPrefs(); + + 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); + + await configureIdentity({ username: "johndoe@mozilla.com" }); + scheduler.autoConnect(); + await promiseZeroTimer(); + + Assert.equal(scheduler.nextSync, expectedSync); + Assert.ok(scheduler.syncTimer.delay >= expectedInterval); + + Svc.Obs.remove("weave:service:login:start", onLoginStart); + await cleanUpAndGo(); +}); + +add_task(async function test_autoconnect_mp_locked() { + let server = await sync_httpd_setup(); + await setUp(server); + + // Pretend user did not unlock master password. + let origLocked = Utils.mpLocked; + Utils.mpLocked = () => true; + + let origEnsureMPUnlocked = Utils.ensureMPUnlocked; + Utils.ensureMPUnlocked = () => { + _("Faking Master Password entry cancelation."); + return false; + }; + let origFxA = Service.identity._fxaService; + Service.identity._fxaService = new FxAccounts({ + currentAccountState: { + getUserAccountData(...args) { + return origFxA._internal.currentAccountState.getUserAccountData( + ...args + ); + }, + }, + keys: { + canGetKeyForScope() { + return false; + }, + }, + }); + // A locked master password will still trigger a sync, but then we'll hit + // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL. + let promiseObserved = promiseOneObserver("weave:service:login:error"); + + scheduler.autoConnect(); + await promiseObserved; + + await Async.promiseYield(); + + Assert.equal(Status.login, MASTER_PASSWORD_LOCKED); + + Utils.mpLocked = origLocked; + Utils.ensureMPUnlocked = origEnsureMPUnlocked; + Service.identity._fxaService = origFxA; + + await cleanUpAndGo(server); +}); + +add_task(async function test_no_autoconnect_during_wizard() { + let server = await sync_httpd_setup(); + await 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); + + scheduler.autoConnect(0); + await promiseZeroTimer(); + Svc.Obs.remove("weave:service:login:start", onLoginStart); + await cleanUpAndGo(server); +}); + +add_task(async function test_no_autoconnect_status_not_ok() { + let server = await sync_httpd_setup(); + Status.__authManager = Service.identity = new SyncAuthManager(); + + // 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); + + scheduler.autoConnect(); + await promiseZeroTimer(); + Svc.Obs.remove("weave:service:login:start", onLoginStart); + + Assert.equal(Status.service, CLIENT_NOT_CONFIGURED); + Assert.equal(Status.login, LOGIN_FAILED_NO_USERNAME); + + await cleanUpAndGo(server); +}); + +add_task(async function test_idle_adjustSyncInterval() { + // Confirm defaults. + Assert.equal(scheduler.idle, false); + + // Single device: nothing changes. + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + Assert.equal(scheduler.idle, true); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); + + // Multiple devices: switch to idle interval. + scheduler.idle = false; + Svc.Prefs.set("clients.devices.desktop", 1); + Svc.Prefs.set("clients.devices.mobile", 1); + scheduler.updateClientMode(); + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + Assert.equal(scheduler.idle, true); + Assert.equal(scheduler.syncInterval, scheduler.idleInterval); + + await cleanUpAndGo(); +}); + +add_task(async function test_back_triggersSync() { + // Confirm defaults. + Assert.ok(!scheduler.idle); + Assert.equal(Status.backoffInterval, 0); + + // Set up: Define 2 clients and put the system in idle. + Svc.Prefs.set("clients.devices.desktop", 1); + Svc.Prefs.set("clients.devices.mobile", 1); + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + Assert.ok(scheduler.idle); + + // We don't actually expect the sync (or the login, for that matter) to + // succeed. We just want to ensure that it was attempted. + let promiseObserved = promiseOneObserver("weave:service:login:error"); + + // Send an 'active' event to trigger sync soonish. + scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime")); + await promiseObserved; + await cleanUpAndGo(); +}); + +add_task(async function test_active_triggersSync_observesBackoff() { + // Confirm defaults. + Assert.ok(!scheduler.idle); + + // Set up: Set backoff, define 2 clients and put the system in idle. + const BACKOFF = 7337; + Status.backoffInterval = scheduler.idleInterval + BACKOFF; + Svc.Prefs.set("clients.devices.desktop", 1); + Svc.Prefs.set("clients.devices.mobile", 1); + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + Assert.equal(scheduler.idle, true); + + function onLoginStart() { + do_throw("Shouldn't have kicked off a sync!"); + } + Svc.Obs.add("weave:service:login:start", onLoginStart); + + let promiseTimer = promiseNamedTimer( + 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")); + await promiseTimer; + Svc.Obs.remove("weave:service:login:start", onLoginStart); + + Assert.ok(scheduler.nextSync <= Date.now() + Status.backoffInterval); + Assert.equal(scheduler.syncTimer.delay, Status.backoffInterval); + + await cleanUpAndGo(); +}); + +add_task(async function test_back_debouncing() { + _( + "Ensure spurious back-then-idle events, as observed on OS X, don't trigger a sync." + ); + + // Confirm defaults. + Assert.equal(scheduler.idle, false); + + // Set up: Define 2 clients and put the system in idle. + Svc.Prefs.set("clients.devices.desktop", 1); + Svc.Prefs.set("clients.devices.mobile", 1); + scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime")); + Assert.equal(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")); + + await promiseNamedTimer(IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer"); + Svc.Obs.remove("weave:service:login:start", onLoginStart); + await cleanUpAndGo(); +}); + +add_task(async function test_no_sync_node() { + enableValidationPrefs(); + + // Test when Status.sync == NO_SYNC_NODE_FOUND + // it is not overwritten on sync:finish + let server = await sync_httpd_setup(); + await setUp(server); + + let oldfc = Service.identity._findCluster; + Service.identity._findCluster = () => null; + Service.clusterURL = ""; + try { + await Service.sync(); + Assert.equal(Status.sync, NO_SYNC_NODE_FOUND); + Assert.equal(scheduler.syncTimer.delay, NO_SYNC_NODE_INTERVAL); + + await cleanUpAndGo(server); + } finally { + Service.identity._findCluster = oldfc; + } +}); + +add_task(async function test_sync_failed_partial_500s() { + enableValidationPrefs(); + + _("Test a 5xx status calls handleSyncError."); + scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF; + let server = await sync_httpd_setup(); + + let engine = Service.engineManager.get("catapult"); + engine.enabled = true; + engine.exception = { status: 500 }; + + Assert.equal(Status.sync, SYNC_SUCCEEDED); + + Assert.ok(await setUp(server)); + + await Service.sync(); + + Assert.equal(Status.service, SYNC_FAILED_PARTIAL); + + let maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL); + Assert.equal(Status.backoffInterval, 0); + Assert.ok(Status.enforceBackoff); + Assert.equal(scheduler._syncErrors, 4); + Assert.ok(scheduler.nextSync <= Date.now() + maxInterval); + Assert.ok(scheduler.syncTimer.delay <= maxInterval); + + await cleanUpAndGo(server); +}); + +add_task(async function test_sync_failed_partial_noresync() { + enableValidationPrefs(); + let server = await sync_httpd_setup(); + + let engine = Service.engineManager.get("catapult"); + engine.enabled = true; + engine.exception = "Bad news"; + engine._tracker._score = MULTI_DEVICE_THRESHOLD + 1; + + Assert.equal(Status.sync, SYNC_SUCCEEDED); + + Assert.ok(await setUp(server)); + + let resyncDoneObserver = promiseOneObserver("weave:service:resyncs-finished"); + + await Service.sync(); + + Assert.equal(Status.service, SYNC_FAILED_PARTIAL); + + function onSyncStarted() { + do_throw("Should not start resync when previous sync failed"); + } + + Svc.Obs.add("weave:service:sync:start", onSyncStarted); + await resyncDoneObserver; + + Svc.Obs.remove("weave:service:sync:start", onSyncStarted); + engine._tracker._store = 0; + await cleanUpAndGo(server); +}); + +add_task(async function test_sync_failed_partial_400s() { + enableValidationPrefs(); + + _("Test a non-5xx status doesn't call handleSyncError."); + scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF; + let server = await sync_httpd_setup(); + + let engine = Service.engineManager.get("catapult"); + engine.enabled = true; + engine.exception = { status: 400 }; + + // Have multiple devices for an active interval. + await clientsEngine._store.create({ + id: "foo", + cleartext: { os: "mobile", version: "0.01", type: "desktop" }, + }); + + Assert.equal(Status.sync, SYNC_SUCCEEDED); + + Assert.ok(await setUp(server)); + + await Service.sync(); + + Assert.equal(Status.service, SYNC_FAILED_PARTIAL); + Assert.equal(scheduler.syncInterval, scheduler.activeInterval); + + Assert.equal(Status.backoffInterval, 0); + Assert.ok(!Status.enforceBackoff); + Assert.equal(scheduler._syncErrors, 0); + Assert.ok(scheduler.nextSync <= Date.now() + scheduler.activeInterval); + Assert.ok(scheduler.syncTimer.delay <= scheduler.activeInterval); + + await cleanUpAndGo(server); +}); + +add_task(async function test_sync_X_Weave_Backoff() { + enableValidationPrefs(); + + let server = await sync_httpd_setup(); + await 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@mozilla.com/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. + await clientsEngine._store.create({ + id: "foo", + cleartext: { os: "mobile", version: "0.01", type: "desktop" }, + }); + let rec = await clientsEngine._store.createRecord("foo", "clients"); + await rec.encrypt(Service.collectionKeys.keyForCollection("clients")); + await rec.upload(Service.resource(clientsEngine.engineURL + rec.id)); + + // Sync once to log in and get everything set up. Let's verify our initial + // values. + await Service.sync(); + Assert.equal(Status.backoffInterval, 0); + Assert.equal(Status.minimumNextSync, 0); + Assert.equal(scheduler.syncInterval, scheduler.activeInterval); + Assert.ok(scheduler.nextSync <= Date.now() + scheduler.syncInterval); + // Sanity check that we picked the right value for BACKOFF: + Assert.ok(scheduler.syncInterval < BACKOFF * 1000); + + // Turn on server maintenance and sync again. + serverBackoff = true; + await Service.sync(); + + Assert.ok(Status.backoffInterval >= BACKOFF * 1000); + // Allowing 20 seconds worth of of leeway between when Status.minimumNextSync + // was set and when this line gets executed. + let minimumExpectedDelay = (BACKOFF - 20) * 1000; + Assert.ok(Status.minimumNextSync >= Date.now() + minimumExpectedDelay); + + // Verify that the next sync is actually going to wait that long. + Assert.ok(scheduler.nextSync >= Date.now() + minimumExpectedDelay); + Assert.ok(scheduler.syncTimer.delay >= minimumExpectedDelay); + + await cleanUpAndGo(server); +}); + +add_task(async function test_sync_503_Retry_After() { + enableValidationPrefs(); + + let server = await sync_httpd_setup(); + await 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@mozilla.com/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. + await clientsEngine._store.create({ + id: "foo", + cleartext: { os: "mobile", version: "0.01", type: "desktop" }, + }); + let rec = await clientsEngine._store.createRecord("foo", "clients"); + await rec.encrypt(Service.collectionKeys.keyForCollection("clients")); + await rec.upload(Service.resource(clientsEngine.engineURL + rec.id)); + + // Sync once to log in and get everything set up. Let's verify our initial + // values. + await Service.sync(); + Assert.ok(!Status.enforceBackoff); + Assert.equal(Status.backoffInterval, 0); + Assert.equal(Status.minimumNextSync, 0); + Assert.equal(scheduler.syncInterval, scheduler.activeInterval); + Assert.ok(scheduler.nextSync <= Date.now() + scheduler.syncInterval); + // Sanity check that we picked the right value for BACKOFF: + Assert.ok(scheduler.syncInterval < BACKOFF * 1000); + + // Turn on server maintenance and sync again. + serverMaintenance = true; + await Service.sync(); + + Assert.ok(Status.enforceBackoff); + Assert.ok(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; + Assert.ok(Status.minimumNextSync >= Date.now() + minimumExpectedDelay); + + // Verify that the next sync is actually going to wait that long. + Assert.ok(scheduler.nextSync >= Date.now() + minimumExpectedDelay); + Assert.ok(scheduler.syncTimer.delay >= minimumExpectedDelay); + + await cleanUpAndGo(server); +}); + +add_task(async function test_loginError_recoverable_reschedules() { + _("Verify that a recoverable login error schedules a new sync."); + await configureIdentity({ username: "johndoe@mozilla.com" }); + Service.clusterURL = "http://localhost:1234/"; + Status.resetSync(); // reset Status.login + + let promiseObserved = promiseOneObserver("weave:service:login:error"); + + // 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. + Assert.equal(scheduler.syncTimer, null); + Assert.equal(Status.checkSetup(), STATUS_OK); + Assert.equal(Status.login, LOGIN_SUCCEEDED); + + scheduler.scheduleNextSync(0); + await promiseObserved; + await Async.promiseYield(); + + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR); + + let expectedNextSync = Date.now() + scheduler.syncInterval; + Assert.ok(scheduler.nextSync > Date.now()); + Assert.ok(scheduler.nextSync <= expectedNextSync); + Assert.ok(scheduler.syncTimer.delay > 0); + Assert.ok(scheduler.syncTimer.delay <= scheduler.syncInterval); + + Svc.Obs.remove("weave:service:sync:start", onSyncStart); + await cleanUpAndGo(); +}); + +add_task(async function test_loginError_fatal_clearsTriggers() { + _("Verify that a fatal login error clears sync triggers."); + await configureIdentity({ username: "johndoe@mozilla.com" }); + + let server = httpd_setup({ + "/1.1/johndoe@mozilla.com/info/collections": httpd_handler( + 401, + "Unauthorized" + ), + }); + + Service.clusterURL = server.baseURI + "/"; + Status.resetSync(); // reset Status.login + + let promiseObserved = promiseOneObserver("weave:service:login:error"); + + // Sanity check. + Assert.equal(scheduler.nextSync, 0); + Assert.equal(scheduler.syncTimer, null); + Assert.equal(Status.checkSetup(), STATUS_OK); + Assert.equal(Status.login, LOGIN_SUCCEEDED); + + scheduler.scheduleNextSync(0); + await promiseObserved; + await Async.promiseYield(); + + // For the FxA identity, a 401 on info/collections means a transient + // error, probably due to an inability to fetch a token. + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR); + // syncs should still be scheduled. + Assert.ok(scheduler.nextSync > Date.now()); + Assert.ok(scheduler.syncTimer.delay > 0); + + await cleanUpAndGo(server); +}); + +add_task(async 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. + Assert.ok(!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, + }); + + await Async.promiseYield(); + scheduler.adjustSyncInterval(); + Assert.ok(!scheduler.hasIncomingItems); + Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval); +}); + +add_task(async function test_link_status_change() { + _("Check that we only attempt to sync when link status is up"); + try { + sinon.spy(scheduler, "scheduleNextSync"); + + Svc.Obs.notify("network:link-status-changed", null, "down"); + equal(scheduler.scheduleNextSync.callCount, 0); + + Svc.Obs.notify("network:link-status-changed", null, "change"); + equal(scheduler.scheduleNextSync.callCount, 0); + + Svc.Obs.notify("network:link-status-changed", null, "up"); + equal(scheduler.scheduleNextSync.callCount, 1); + + Svc.Obs.notify("network:link-status-changed", null, "change"); + equal(scheduler.scheduleNextSync.callCount, 1); + } finally { + scheduler.scheduleNextSync.restore(); + } +}); |