summaryrefslogtreecommitdiffstats
path: root/services/sync/tests/unit/test_syncscheduler.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tests/unit/test_syncscheduler.js')
-rw-r--r--services/sync/tests/unit/test_syncscheduler.js1151
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();
+ }
+});