tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_syncscheduler.js (39715B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { FxAccounts } = ChromeUtils.importESModule(
      5  "resource://gre/modules/FxAccounts.sys.mjs"
      6 );
      7 const { SyncAuthManager } = ChromeUtils.importESModule(
      8  "resource://services-sync/sync_auth.sys.mjs"
      9 );
     10 const { SyncScheduler } = ChromeUtils.importESModule(
     11  "resource://services-sync/policies.sys.mjs"
     12 );
     13 const { Service } = ChromeUtils.importESModule(
     14  "resource://services-sync/service.sys.mjs"
     15 );
     16 const { Status } = ChromeUtils.importESModule(
     17  "resource://services-sync/status.sys.mjs"
     18 );
     19 
     20 function CatapultEngine() {
     21  SyncEngine.call(this, "Catapult", Service);
     22 }
     23 CatapultEngine.prototype = {
     24  exception: null, // tests fill this in
     25  async _sync() {
     26    throw this.exception;
     27  },
     28 };
     29 Object.setPrototypeOf(CatapultEngine.prototype, SyncEngine.prototype);
     30 
     31 var scheduler = new SyncScheduler(Service);
     32 let clientsEngine;
     33 
     34 async function sync_httpd_setup() {
     35  let clientsSyncID = await clientsEngine.resetLocalSyncID();
     36  let global = new ServerWBO("global", {
     37    syncID: Service.syncID,
     38    storageVersion: STORAGE_VERSION,
     39    engines: {
     40      clients: { version: clientsEngine.version, syncID: clientsSyncID },
     41    },
     42  });
     43  let clientsColl = new ServerCollection({}, true);
     44 
     45  // Tracking info/collections.
     46  let collectionsHelper = track_collections_helper();
     47  let upd = collectionsHelper.with_updated_collection;
     48 
     49  return httpd_setup({
     50    "/1.1/johndoe@mozilla.com/storage/meta/global": upd(
     51      "meta",
     52      global.handler()
     53    ),
     54    "/1.1/johndoe@mozilla.com/info/collections": collectionsHelper.handler,
     55    "/1.1/johndoe@mozilla.com/storage/crypto/keys": upd(
     56      "crypto",
     57      new ServerWBO("keys").handler()
     58    ),
     59    "/1.1/johndoe@mozilla.com/storage/clients": upd(
     60      "clients",
     61      clientsColl.handler()
     62    ),
     63  });
     64 }
     65 
     66 async function setUp(server) {
     67  await configureIdentity({ username: "johndoe@mozilla.com" }, server);
     68 
     69  await generateNewKeys(Service.collectionKeys);
     70  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
     71  await serverKeys.encrypt(Service.identity.syncKeyBundle);
     72  let result = (
     73    await serverKeys.upload(Service.resource(Service.cryptoKeysURL))
     74  ).success;
     75  return result;
     76 }
     77 
     78 async function cleanUpAndGo(server) {
     79  await Async.promiseYield();
     80  await clientsEngine._store.wipe();
     81  await Service.startOver();
     82  // Re-enable logging, which we just disabled.
     83  syncTestLogging();
     84  if (server) {
     85    await promiseStopServer(server);
     86  }
     87 }
     88 
     89 add_task(async function setup() {
     90  await Service.promiseInitialized;
     91  clientsEngine = Service.clientsEngine;
     92  // Don't remove stale clients when syncing. This is a test-only workaround
     93  // that lets us add clients directly to the store, without losing them on
     94  // the next sync.
     95  clientsEngine._removeRemoteClient = async () => {};
     96  await Service.engineManager.clear();
     97 
     98  validate_all_future_pings();
     99 
    100  scheduler.setDefaults();
    101 
    102  await Service.engineManager.register(CatapultEngine);
    103 });
    104 
    105 add_test(function test_prefAttributes() {
    106  _("Test various attributes corresponding to preferences.");
    107 
    108  const INTERVAL = 42 * 60 * 1000; // 42 minutes
    109  const THRESHOLD = 3142;
    110  const SCORE = 2718;
    111  const TIMESTAMP1 = 1275493471649;
    112 
    113  _(
    114    "The 'nextSync' attribute stores a millisecond timestamp rounded down to the nearest second."
    115  );
    116  Assert.equal(scheduler.nextSync, 0);
    117  scheduler.nextSync = TIMESTAMP1;
    118  Assert.equal(scheduler.nextSync, Math.floor(TIMESTAMP1 / 1000) * 1000);
    119 
    120  _("'syncInterval' defaults to singleDeviceInterval.");
    121  Assert.equal(
    122    Svc.PrefBranch.getPrefType("syncInterval"),
    123    Ci.nsIPrefBranch.PREF_INVALID
    124  );
    125  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    126 
    127  _("'syncInterval' corresponds to a preference setting.");
    128  scheduler.syncInterval = INTERVAL;
    129  Assert.equal(scheduler.syncInterval, INTERVAL);
    130  Assert.equal(Svc.PrefBranch.getIntPref("syncInterval"), INTERVAL);
    131 
    132  _(
    133    "'syncThreshold' corresponds to preference, defaults to SINGLE_USER_THRESHOLD"
    134  );
    135  Assert.equal(
    136    Svc.PrefBranch.getPrefType("syncThreshold"),
    137    Ci.nsIPrefBranch.PREF_INVALID
    138  );
    139  Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
    140  scheduler.syncThreshold = THRESHOLD;
    141  Assert.equal(scheduler.syncThreshold, THRESHOLD);
    142 
    143  _("'globalScore' corresponds to preference, defaults to zero.");
    144  Assert.equal(Svc.PrefBranch.getIntPref("globalScore"), 0);
    145  Assert.equal(scheduler.globalScore, 0);
    146  scheduler.globalScore = SCORE;
    147  Assert.equal(scheduler.globalScore, SCORE);
    148  Assert.equal(Svc.PrefBranch.getIntPref("globalScore"), SCORE);
    149 
    150  _("Intervals correspond to default preferences.");
    151  Assert.equal(
    152    scheduler.singleDeviceInterval,
    153    Svc.PrefBranch.getIntPref("scheduler.fxa.singleDeviceInterval") * 1000
    154  );
    155  Assert.equal(
    156    scheduler.idleInterval,
    157    Svc.PrefBranch.getIntPref("scheduler.idleInterval") * 1000
    158  );
    159  Assert.equal(
    160    scheduler.activeInterval,
    161    Svc.PrefBranch.getIntPref("scheduler.activeInterval") * 1000
    162  );
    163  Assert.equal(
    164    scheduler.immediateInterval,
    165    Svc.PrefBranch.getIntPref("scheduler.immediateInterval") * 1000
    166  );
    167 
    168  _("Custom values for prefs will take effect after a restart.");
    169  Svc.PrefBranch.setIntPref("scheduler.fxa.singleDeviceInterval", 420);
    170  Svc.PrefBranch.setIntPref("scheduler.idleInterval", 230);
    171  Svc.PrefBranch.setIntPref("scheduler.activeInterval", 180);
    172  Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 31415);
    173  scheduler.setDefaults();
    174  Assert.equal(scheduler.idleInterval, 230000);
    175  Assert.equal(scheduler.singleDeviceInterval, 420000);
    176  Assert.equal(scheduler.activeInterval, 180000);
    177  Assert.equal(scheduler.immediateInterval, 31415000);
    178 
    179  _("Custom values for interval prefs can't be less than 60 seconds.");
    180  Svc.PrefBranch.setIntPref("scheduler.fxa.singleDeviceInterval", 42);
    181  Svc.PrefBranch.setIntPref("scheduler.idleInterval", 50);
    182  Svc.PrefBranch.setIntPref("scheduler.activeInterval", 50);
    183  Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 10);
    184  scheduler.setDefaults();
    185  Assert.equal(scheduler.idleInterval, 60000);
    186  Assert.equal(scheduler.singleDeviceInterval, 60000);
    187  Assert.equal(scheduler.activeInterval, 60000);
    188  Assert.equal(scheduler.immediateInterval, 60000);
    189 
    190  for (const pref of Svc.PrefBranch.getChildList("")) {
    191    Svc.PrefBranch.clearUserPref(pref);
    192  }
    193  scheduler.setDefaults();
    194  run_next_test();
    195 });
    196 
    197 add_task(async function test_sync_skipped_low_score_no_resync() {
    198  enableValidationPrefs();
    199  let server = await sync_httpd_setup();
    200 
    201  function SkipEngine() {
    202    SyncEngine.call(this, "Skip", Service);
    203    this.syncs = 0;
    204  }
    205 
    206  SkipEngine.prototype = {
    207    _sync() {
    208      do_throw("Should have been skipped");
    209    },
    210    shouldSkipSync() {
    211      return true;
    212    },
    213  };
    214  Object.setPrototypeOf(SkipEngine.prototype, SyncEngine.prototype);
    215  await Service.engineManager.register(SkipEngine);
    216 
    217  let engine = Service.engineManager.get("skip");
    218  engine.enabled = true;
    219  engine._tracker._score = 30;
    220 
    221  Assert.equal(Status.sync, SYNC_SUCCEEDED);
    222 
    223  Assert.ok(await setUp(server));
    224 
    225  let resyncDoneObserver = promiseOneObserver("weave:service:resyncs-finished");
    226 
    227  let synced = false;
    228  function onSyncStarted() {
    229    Assert.ok(!synced, "Only should sync once");
    230    synced = true;
    231  }
    232 
    233  await Service.sync();
    234 
    235  Assert.equal(Status.sync, SYNC_SUCCEEDED);
    236 
    237  Svc.Obs.add("weave:service:sync:start", onSyncStarted);
    238  await resyncDoneObserver;
    239 
    240  Svc.Obs.remove("weave:service:sync:start", onSyncStarted);
    241  engine._tracker._store = 0;
    242  await cleanUpAndGo(server);
    243 });
    244 
    245 add_task(async function test_updateClientMode() {
    246  _(
    247    "Test updateClientMode adjusts scheduling attributes based on # of clients appropriately"
    248  );
    249  Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
    250  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    251  Assert.equal(false, scheduler.numClients > 1);
    252  Assert.ok(!scheduler.idle);
    253 
    254  // Trigger a change in interval & threshold by noting there are multiple clients.
    255  Svc.PrefBranch.setIntPref("clients.devices.desktop", 1);
    256  Svc.PrefBranch.setIntPref("clients.devices.mobile", 1);
    257  scheduler.updateClientMode();
    258 
    259  Assert.equal(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD);
    260  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);
    261  Assert.greater(scheduler.numClients, 1);
    262  Assert.ok(!scheduler.idle);
    263 
    264  // Resets the number of clients to 0.
    265  await clientsEngine.resetClient();
    266  Svc.PrefBranch.clearUserPref("clients.devices.mobile");
    267  scheduler.updateClientMode();
    268 
    269  // Goes back to single user if # clients is 1.
    270  Assert.equal(scheduler.numClients, 1);
    271  Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
    272  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    273  Assert.equal(false, scheduler.numClients > 1);
    274  Assert.ok(!scheduler.idle);
    275 
    276  await cleanUpAndGo();
    277 });
    278 
    279 add_task(async function test_masterpassword_locked_retry_interval() {
    280  enableValidationPrefs();
    281 
    282  _(
    283    "Test Status.login = MASTER_PASSWORD_LOCKED results in reschedule at MASTER_PASSWORD interval"
    284  );
    285  let loginFailed = false;
    286  Svc.Obs.add("weave:service:login:error", function onLoginError() {
    287    Svc.Obs.remove("weave:service:login:error", onLoginError);
    288    loginFailed = true;
    289  });
    290 
    291  let rescheduleInterval = false;
    292 
    293  let oldScheduleAtInterval = SyncScheduler.prototype.scheduleAtInterval;
    294  SyncScheduler.prototype.scheduleAtInterval = function (interval) {
    295    rescheduleInterval = true;
    296    Assert.equal(interval, MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
    297  };
    298 
    299  let oldVerifyLogin = Service.verifyLogin;
    300  Service.verifyLogin = async function () {
    301    Status.login = MASTER_PASSWORD_LOCKED;
    302    return false;
    303  };
    304 
    305  let server = await sync_httpd_setup();
    306  await setUp(server);
    307 
    308  await Service.sync();
    309 
    310  Assert.ok(loginFailed);
    311  Assert.equal(Status.login, MASTER_PASSWORD_LOCKED);
    312  Assert.ok(rescheduleInterval);
    313 
    314  Service.verifyLogin = oldVerifyLogin;
    315  SyncScheduler.prototype.scheduleAtInterval = oldScheduleAtInterval;
    316 
    317  await cleanUpAndGo(server);
    318 });
    319 
    320 add_task(async function test_calculateBackoff() {
    321  Assert.equal(Status.backoffInterval, 0);
    322 
    323  // Test no interval larger than the maximum backoff is used if
    324  // Status.backoffInterval is smaller.
    325  Status.backoffInterval = 5;
    326  let backoffInterval = Utils.calculateBackoff(
    327    50,
    328    MAXIMUM_BACKOFF_INTERVAL,
    329    Status.backoffInterval
    330  );
    331 
    332  Assert.equal(backoffInterval, MAXIMUM_BACKOFF_INTERVAL);
    333 
    334  // Test Status.backoffInterval is used if it is
    335  // larger than MAXIMUM_BACKOFF_INTERVAL.
    336  Status.backoffInterval = MAXIMUM_BACKOFF_INTERVAL + 10;
    337  backoffInterval = Utils.calculateBackoff(
    338    50,
    339    MAXIMUM_BACKOFF_INTERVAL,
    340    Status.backoffInterval
    341  );
    342 
    343  Assert.equal(backoffInterval, MAXIMUM_BACKOFF_INTERVAL + 10);
    344 
    345  await cleanUpAndGo();
    346 });
    347 
    348 add_task(async function test_scheduleNextSync_nowOrPast() {
    349  enableValidationPrefs();
    350 
    351  let promiseObserved = promiseOneObserver("weave:service:sync:finish");
    352 
    353  let server = await sync_httpd_setup();
    354  await setUp(server);
    355 
    356  // We're late for a sync...
    357  scheduler.scheduleNextSync(-1);
    358  await promiseObserved;
    359  await cleanUpAndGo(server);
    360 });
    361 
    362 add_task(async function test_scheduleNextSync_future_noBackoff() {
    363  enableValidationPrefs();
    364 
    365  _(
    366    "scheduleNextSync() uses the current syncInterval if no interval is provided."
    367  );
    368  // Test backoffInterval is 0 as expected.
    369  Assert.equal(Status.backoffInterval, 0);
    370 
    371  _("Test setting sync interval when nextSync == 0");
    372  scheduler.nextSync = 0;
    373  scheduler.scheduleNextSync();
    374 
    375  // nextSync - Date.now() might be smaller than expectedInterval
    376  // since some time has passed since we called scheduleNextSync().
    377  Assert.lessOrEqual(scheduler.nextSync - Date.now(), scheduler.syncInterval);
    378  Assert.equal(scheduler.syncTimer.delay, scheduler.syncInterval);
    379 
    380  _("Test setting sync interval when nextSync != 0");
    381  scheduler.nextSync = Date.now() + scheduler.singleDeviceInterval;
    382  scheduler.scheduleNextSync();
    383 
    384  // nextSync - Date.now() might be smaller than expectedInterval
    385  // since some time has passed since we called scheduleNextSync().
    386  Assert.lessOrEqual(scheduler.nextSync - Date.now(), scheduler.syncInterval);
    387  Assert.lessOrEqual(scheduler.syncTimer.delay, scheduler.syncInterval);
    388 
    389  _(
    390    "Scheduling requests for intervals larger than the current one will be ignored."
    391  );
    392  // Request a sync at a longer interval. The sync that's already scheduled
    393  // for sooner takes precedence.
    394  let nextSync = scheduler.nextSync;
    395  let timerDelay = scheduler.syncTimer.delay;
    396  let requestedInterval = scheduler.syncInterval * 10;
    397  scheduler.scheduleNextSync(requestedInterval);
    398  Assert.equal(scheduler.nextSync, nextSync);
    399  Assert.equal(scheduler.syncTimer.delay, timerDelay);
    400 
    401  // We can schedule anything we want if there isn't a sync scheduled.
    402  scheduler.nextSync = 0;
    403  scheduler.scheduleNextSync(requestedInterval);
    404  Assert.lessOrEqual(scheduler.nextSync, Date.now() + requestedInterval);
    405  Assert.equal(scheduler.syncTimer.delay, requestedInterval);
    406 
    407  // Request a sync at the smallest possible interval (0 triggers now).
    408  scheduler.scheduleNextSync(1);
    409  Assert.lessOrEqual(scheduler.nextSync, Date.now() + 1);
    410  Assert.equal(scheduler.syncTimer.delay, 1);
    411 
    412  await cleanUpAndGo();
    413 });
    414 
    415 add_task(async function test_scheduleNextSync_future_backoff() {
    416  enableValidationPrefs();
    417 
    418  _("scheduleNextSync() will honour backoff in all scheduling requests.");
    419  // Let's take a backoff interval that's bigger than the default sync interval.
    420  const BACKOFF = 7337;
    421  Status.backoffInterval = scheduler.syncInterval + BACKOFF;
    422 
    423  _("Test setting sync interval when nextSync == 0");
    424  scheduler.nextSync = 0;
    425  scheduler.scheduleNextSync();
    426 
    427  // nextSync - Date.now() might be smaller than expectedInterval
    428  // since some time has passed since we called scheduleNextSync().
    429  Assert.lessOrEqual(scheduler.nextSync - Date.now(), Status.backoffInterval);
    430  Assert.equal(scheduler.syncTimer.delay, Status.backoffInterval);
    431 
    432  _("Test setting sync interval when nextSync != 0");
    433  scheduler.nextSync = Date.now() + scheduler.singleDeviceInterval;
    434  scheduler.scheduleNextSync();
    435 
    436  // nextSync - Date.now() might be smaller than expectedInterval
    437  // since some time has passed since we called scheduleNextSync().
    438  Assert.lessOrEqual(scheduler.nextSync - Date.now(), Status.backoffInterval);
    439  Assert.lessOrEqual(scheduler.syncTimer.delay, Status.backoffInterval);
    440 
    441  // Request a sync at a longer interval. The sync that's already scheduled
    442  // for sooner takes precedence.
    443  let nextSync = scheduler.nextSync;
    444  let timerDelay = scheduler.syncTimer.delay;
    445  let requestedInterval = scheduler.syncInterval * 10;
    446  Assert.greater(requestedInterval, Status.backoffInterval);
    447  scheduler.scheduleNextSync(requestedInterval);
    448  Assert.equal(scheduler.nextSync, nextSync);
    449  Assert.equal(scheduler.syncTimer.delay, timerDelay);
    450 
    451  // We can schedule anything we want if there isn't a sync scheduled.
    452  scheduler.nextSync = 0;
    453  scheduler.scheduleNextSync(requestedInterval);
    454  Assert.lessOrEqual(scheduler.nextSync, Date.now() + requestedInterval);
    455  Assert.equal(scheduler.syncTimer.delay, requestedInterval);
    456 
    457  // Request a sync at the smallest possible interval (0 triggers now).
    458  scheduler.scheduleNextSync(1);
    459  Assert.lessOrEqual(scheduler.nextSync, Date.now() + Status.backoffInterval);
    460  Assert.equal(scheduler.syncTimer.delay, Status.backoffInterval);
    461 
    462  await cleanUpAndGo();
    463 });
    464 
    465 add_task(async function test_handleSyncError() {
    466  enableValidationPrefs();
    467 
    468  let server = await sync_httpd_setup();
    469  await setUp(server);
    470 
    471  // Force sync to fail.
    472  Svc.PrefBranch.setStringPref("firstSync", "notReady");
    473 
    474  _("Ensure expected initial environment.");
    475  Assert.equal(scheduler._syncErrors, 0);
    476  Assert.ok(!Status.enforceBackoff);
    477  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    478  Assert.equal(Status.backoffInterval, 0);
    479 
    480  // Trigger sync with an error several times & observe
    481  // functionality of handleSyncError()
    482  _("Test first error calls scheduleNextSync on default interval");
    483  await Service.sync();
    484  Assert.lessOrEqual(
    485    scheduler.nextSync,
    486    Date.now() + scheduler.singleDeviceInterval
    487  );
    488  Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval);
    489  Assert.equal(scheduler._syncErrors, 1);
    490  Assert.ok(!Status.enforceBackoff);
    491  scheduler.syncTimer.clear();
    492 
    493  _("Test second error still calls scheduleNextSync on default interval");
    494  await Service.sync();
    495  Assert.lessOrEqual(
    496    scheduler.nextSync,
    497    Date.now() + scheduler.singleDeviceInterval
    498  );
    499  Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval);
    500  Assert.equal(scheduler._syncErrors, 2);
    501  Assert.ok(!Status.enforceBackoff);
    502  scheduler.syncTimer.clear();
    503 
    504  _("Test third error sets Status.enforceBackoff and calls scheduleAtInterval");
    505  await Service.sync();
    506  let maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL);
    507  Assert.equal(Status.backoffInterval, 0);
    508  Assert.lessOrEqual(scheduler.nextSync, Date.now() + maxInterval);
    509  Assert.lessOrEqual(scheduler.syncTimer.delay, maxInterval);
    510  Assert.equal(scheduler._syncErrors, 3);
    511  Assert.ok(Status.enforceBackoff);
    512 
    513  // Status.enforceBackoff is false but there are still errors.
    514  Status.resetBackoff();
    515  Assert.ok(!Status.enforceBackoff);
    516  Assert.equal(scheduler._syncErrors, 3);
    517  scheduler.syncTimer.clear();
    518 
    519  _(
    520    "Test fourth error still calls scheduleAtInterval even if enforceBackoff was reset"
    521  );
    522  await Service.sync();
    523  maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL);
    524  Assert.lessOrEqual(scheduler.nextSync, Date.now() + maxInterval);
    525  Assert.lessOrEqual(scheduler.syncTimer.delay, maxInterval);
    526  Assert.equal(scheduler._syncErrors, 4);
    527  Assert.ok(Status.enforceBackoff);
    528  scheduler.syncTimer.clear();
    529 
    530  _("Arrange for a successful sync to reset the scheduler error count");
    531  let promiseObserved = promiseOneObserver("weave:service:sync:finish");
    532  Svc.PrefBranch.setStringPref("firstSync", "wipeRemote");
    533  scheduler.scheduleNextSync(-1);
    534  await promiseObserved;
    535  await cleanUpAndGo(server);
    536 });
    537 
    538 add_task(async function test_client_sync_finish_updateClientMode() {
    539  enableValidationPrefs();
    540 
    541  let server = await sync_httpd_setup();
    542  await setUp(server);
    543 
    544  // Confirm defaults.
    545  Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
    546  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    547  Assert.ok(!scheduler.idle);
    548 
    549  // Trigger a change in interval & threshold by adding a client.
    550  await clientsEngine._store.create({
    551    id: "foo",
    552    cleartext: { os: "mobile", version: "0.01", type: "desktop" },
    553  });
    554  Assert.equal(false, scheduler.numClients > 1);
    555  scheduler.updateClientMode();
    556  await Service.sync();
    557 
    558  Assert.equal(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD);
    559  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);
    560  Assert.greater(scheduler.numClients, 1);
    561  Assert.ok(!scheduler.idle);
    562 
    563  // Resets the number of clients to 0.
    564  await clientsEngine.resetClient();
    565  // Also re-init the server, or we suck our "foo" client back down.
    566  await setUp(server);
    567 
    568  await Service.sync();
    569 
    570  // Goes back to single user if # clients is 1.
    571  Assert.equal(scheduler.numClients, 1);
    572  Assert.equal(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
    573  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    574  Assert.equal(false, scheduler.numClients > 1);
    575  Assert.ok(!scheduler.idle);
    576 
    577  await cleanUpAndGo(server);
    578 });
    579 
    580 add_task(async function test_autoconnect_nextSync_past() {
    581  enableValidationPrefs();
    582 
    583  let promiseObserved = promiseOneObserver("weave:service:sync:finish");
    584  // nextSync will be 0 by default, so it's way in the past.
    585 
    586  let server = await sync_httpd_setup();
    587  await setUp(server);
    588 
    589  scheduler.autoConnect();
    590  await promiseObserved;
    591  await cleanUpAndGo(server);
    592 });
    593 
    594 add_task(async function test_autoconnect_nextSync_future() {
    595  enableValidationPrefs();
    596 
    597  let previousSync = Date.now() + scheduler.syncInterval / 2;
    598  scheduler.nextSync = previousSync;
    599  // nextSync rounds to the nearest second.
    600  let expectedSync = scheduler.nextSync;
    601  let expectedInterval = expectedSync - Date.now() - 1000;
    602 
    603  // Ensure we don't actually try to sync (or log in for that matter).
    604  function onLoginStart() {
    605    do_throw("Should not get here!");
    606  }
    607  Svc.Obs.add("weave:service:login:start", onLoginStart);
    608 
    609  await configureIdentity({ username: "johndoe@mozilla.com" });
    610  scheduler.autoConnect();
    611  await promiseZeroTimer();
    612 
    613  Assert.equal(scheduler.nextSync, expectedSync);
    614  Assert.greaterOrEqual(scheduler.syncTimer.delay, expectedInterval);
    615 
    616  Svc.Obs.remove("weave:service:login:start", onLoginStart);
    617  await cleanUpAndGo();
    618 });
    619 
    620 add_task(async function test_autoconnect_mp_locked() {
    621  let server = await sync_httpd_setup();
    622  await setUp(server);
    623 
    624  // Pretend user did not unlock master password.
    625  let origLocked = Utils.mpLocked;
    626  Utils.mpLocked = () => true;
    627 
    628  let origEnsureMPUnlocked = Utils.ensureMPUnlocked;
    629  Utils.ensureMPUnlocked = () => {
    630    _("Faking Master Password entry cancelation.");
    631    return false;
    632  };
    633  let origFxA = Service.identity._fxaService;
    634  Service.identity._fxaService = new FxAccounts({
    635    currentAccountState: {
    636      getUserAccountData(...args) {
    637        return origFxA._internal.currentAccountState.getUserAccountData(
    638          ...args
    639        );
    640      },
    641    },
    642    keys: {
    643      canGetKeyForScope() {
    644        return false;
    645      },
    646    },
    647  });
    648  // A locked master password will still trigger a sync, but then we'll hit
    649  // MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
    650  let promiseObserved = promiseOneObserver("weave:service:login:error");
    651 
    652  scheduler.autoConnect();
    653  await promiseObserved;
    654 
    655  await Async.promiseYield();
    656 
    657  Assert.equal(Status.login, MASTER_PASSWORD_LOCKED);
    658 
    659  Utils.mpLocked = origLocked;
    660  Utils.ensureMPUnlocked = origEnsureMPUnlocked;
    661  Service.identity._fxaService = origFxA;
    662 
    663  await cleanUpAndGo(server);
    664 });
    665 
    666 add_task(async function test_no_autoconnect_during_wizard() {
    667  let server = await sync_httpd_setup();
    668  await setUp(server);
    669 
    670  // Simulate the Sync setup wizard.
    671  Svc.PrefBranch.setStringPref("firstSync", "notReady");
    672 
    673  // Ensure we don't actually try to sync (or log in for that matter).
    674  function onLoginStart() {
    675    do_throw("Should not get here!");
    676  }
    677  Svc.Obs.add("weave:service:login:start", onLoginStart);
    678 
    679  scheduler.autoConnect(0);
    680  await promiseZeroTimer();
    681  Svc.Obs.remove("weave:service:login:start", onLoginStart);
    682  await cleanUpAndGo(server);
    683 });
    684 
    685 add_task(async function test_no_autoconnect_status_not_ok() {
    686  let server = await sync_httpd_setup();
    687  Status.__authManager = Service.identity = new SyncAuthManager();
    688 
    689  // Ensure we don't actually try to sync (or log in for that matter).
    690  function onLoginStart() {
    691    do_throw("Should not get here!");
    692  }
    693  Svc.Obs.add("weave:service:login:start", onLoginStart);
    694 
    695  scheduler.autoConnect();
    696  await promiseZeroTimer();
    697  Svc.Obs.remove("weave:service:login:start", onLoginStart);
    698 
    699  Assert.equal(Status.service, CLIENT_NOT_CONFIGURED);
    700  Assert.equal(Status.login, LOGIN_FAILED_NO_USERNAME);
    701 
    702  await cleanUpAndGo(server);
    703 });
    704 
    705 add_task(async function test_idle_adjustSyncInterval() {
    706  // Confirm defaults.
    707  Assert.equal(scheduler.idle, false);
    708 
    709  // Single device: nothing changes.
    710  scheduler.observe(
    711    null,
    712    "idle",
    713    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    714  );
    715  Assert.equal(scheduler.idle, true);
    716  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
    717 
    718  // Multiple devices: switch to idle interval.
    719  scheduler.idle = false;
    720  Svc.PrefBranch.setIntPref("clients.devices.desktop", 1);
    721  Svc.PrefBranch.setIntPref("clients.devices.mobile", 1);
    722  scheduler.updateClientMode();
    723  scheduler.observe(
    724    null,
    725    "idle",
    726    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    727  );
    728  Assert.equal(scheduler.idle, true);
    729  Assert.equal(scheduler.syncInterval, scheduler.idleInterval);
    730 
    731  await cleanUpAndGo();
    732 });
    733 
    734 add_task(async function test_back_triggersSync() {
    735  // Confirm defaults.
    736  Assert.ok(!scheduler.idle);
    737  Assert.equal(Status.backoffInterval, 0);
    738 
    739  // Set up: Define 2 clients and put the system in idle.
    740  Svc.PrefBranch.setIntPref("clients.devices.desktop", 1);
    741  Svc.PrefBranch.setIntPref("clients.devices.mobile", 1);
    742  scheduler.observe(
    743    null,
    744    "idle",
    745    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    746  );
    747  Assert.ok(scheduler.idle);
    748 
    749  // We don't actually expect the sync (or the login, for that matter) to
    750  // succeed. We just want to ensure that it was attempted.
    751  let promiseObserved = promiseOneObserver("weave:service:login:error");
    752 
    753  // Send an 'active' event to trigger sync soonish.
    754  scheduler.observe(
    755    null,
    756    "active",
    757    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    758  );
    759  await promiseObserved;
    760  await cleanUpAndGo();
    761 });
    762 
    763 add_task(async function test_active_triggersSync_observesBackoff() {
    764  // Confirm defaults.
    765  Assert.ok(!scheduler.idle);
    766 
    767  // Set up: Set backoff, define 2 clients and put the system in idle.
    768  const BACKOFF = 7337;
    769  Status.backoffInterval = scheduler.idleInterval + BACKOFF;
    770  Svc.PrefBranch.setIntPref("clients.devices.desktop", 1);
    771  Svc.PrefBranch.setIntPref("clients.devices.mobile", 1);
    772  scheduler.observe(
    773    null,
    774    "idle",
    775    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    776  );
    777  Assert.equal(scheduler.idle, true);
    778 
    779  function onLoginStart() {
    780    do_throw("Shouldn't have kicked off a sync!");
    781  }
    782  Svc.Obs.add("weave:service:login:start", onLoginStart);
    783 
    784  let promiseTimer = promiseNamedTimer(
    785    IDLE_OBSERVER_BACK_DELAY * 1.5,
    786    {},
    787    "timer"
    788  );
    789 
    790  // Send an 'active' event to try to trigger sync soonish.
    791  scheduler.observe(
    792    null,
    793    "active",
    794    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    795  );
    796  await promiseTimer;
    797  Svc.Obs.remove("weave:service:login:start", onLoginStart);
    798 
    799  Assert.lessOrEqual(scheduler.nextSync, Date.now() + Status.backoffInterval);
    800  Assert.equal(scheduler.syncTimer.delay, Status.backoffInterval);
    801 
    802  await cleanUpAndGo();
    803 });
    804 
    805 add_task(async function test_back_debouncing() {
    806  _(
    807    "Ensure spurious back-then-idle events, as observed on OS X, don't trigger a sync."
    808  );
    809 
    810  // Confirm defaults.
    811  Assert.equal(scheduler.idle, false);
    812 
    813  // Set up: Define 2 clients and put the system in idle.
    814  Svc.PrefBranch.setIntPref("clients.devices.desktop", 1);
    815  Svc.PrefBranch.setIntPref("clients.devices.mobile", 1);
    816  scheduler.observe(
    817    null,
    818    "idle",
    819    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    820  );
    821  Assert.equal(scheduler.idle, true);
    822 
    823  function onLoginStart() {
    824    do_throw("Shouldn't have kicked off a sync!");
    825  }
    826  Svc.Obs.add("weave:service:login:start", onLoginStart);
    827 
    828  // Create spurious back-then-idle events as observed on OS X:
    829  scheduler.observe(
    830    null,
    831    "active",
    832    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    833  );
    834  scheduler.observe(
    835    null,
    836    "idle",
    837    Svc.PrefBranch.getIntPref("scheduler.idleTime")
    838  );
    839 
    840  await promiseNamedTimer(IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer");
    841  Svc.Obs.remove("weave:service:login:start", onLoginStart);
    842  await cleanUpAndGo();
    843 });
    844 
    845 add_task(async function test_no_sync_node() {
    846  enableValidationPrefs();
    847 
    848  // Test when Status.sync == NO_SYNC_NODE_FOUND
    849  // it is not overwritten on sync:finish
    850  let server = await sync_httpd_setup();
    851  await setUp(server);
    852 
    853  let oldfc = Service.identity._findCluster;
    854  Service.identity._findCluster = () => null;
    855  Service.clusterURL = "";
    856  try {
    857    await Service.sync();
    858    Assert.equal(Status.sync, NO_SYNC_NODE_FOUND);
    859    Assert.equal(scheduler.syncTimer.delay, NO_SYNC_NODE_INTERVAL);
    860 
    861    await cleanUpAndGo(server);
    862  } finally {
    863    Service.identity._findCluster = oldfc;
    864  }
    865 });
    866 
    867 add_task(async function test_sync_failed_partial_500s() {
    868  enableValidationPrefs();
    869 
    870  _("Test a 5xx status calls handleSyncError.");
    871  scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
    872  let server = await sync_httpd_setup();
    873 
    874  let engine = Service.engineManager.get("catapult");
    875  engine.enabled = true;
    876  engine.exception = { status: 500 };
    877 
    878  Assert.equal(Status.sync, SYNC_SUCCEEDED);
    879 
    880  Assert.ok(await setUp(server));
    881 
    882  await Service.sync();
    883 
    884  Assert.equal(Status.service, SYNC_FAILED_PARTIAL);
    885 
    886  let maxInterval = scheduler._syncErrors * (2 * MINIMUM_BACKOFF_INTERVAL);
    887  Assert.equal(Status.backoffInterval, 0);
    888  Assert.ok(Status.enforceBackoff);
    889  Assert.equal(scheduler._syncErrors, 4);
    890  Assert.lessOrEqual(scheduler.nextSync, Date.now() + maxInterval);
    891  Assert.lessOrEqual(scheduler.syncTimer.delay, maxInterval);
    892 
    893  await cleanUpAndGo(server);
    894 });
    895 
    896 add_task(async function test_sync_failed_partial_noresync() {
    897  enableValidationPrefs();
    898  let server = await sync_httpd_setup();
    899 
    900  let engine = Service.engineManager.get("catapult");
    901  engine.enabled = true;
    902  engine.exception = "Bad news";
    903  engine._tracker._score = MULTI_DEVICE_THRESHOLD + 1;
    904 
    905  Assert.equal(Status.sync, SYNC_SUCCEEDED);
    906 
    907  Assert.ok(await setUp(server));
    908 
    909  let resyncDoneObserver = promiseOneObserver("weave:service:resyncs-finished");
    910 
    911  await Service.sync();
    912 
    913  Assert.equal(Status.service, SYNC_FAILED_PARTIAL);
    914 
    915  function onSyncStarted() {
    916    do_throw("Should not start resync when previous sync failed");
    917  }
    918 
    919  Svc.Obs.add("weave:service:sync:start", onSyncStarted);
    920  await resyncDoneObserver;
    921 
    922  Svc.Obs.remove("weave:service:sync:start", onSyncStarted);
    923  engine._tracker._store = 0;
    924  await cleanUpAndGo(server);
    925 });
    926 
    927 add_task(async function test_sync_failed_partial_400s() {
    928  enableValidationPrefs();
    929 
    930  _("Test a non-5xx status doesn't call handleSyncError.");
    931  scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
    932  let server = await sync_httpd_setup();
    933 
    934  let engine = Service.engineManager.get("catapult");
    935  engine.enabled = true;
    936  engine.exception = { status: 400 };
    937 
    938  // Have multiple devices for an active interval.
    939  await clientsEngine._store.create({
    940    id: "foo",
    941    cleartext: { os: "mobile", version: "0.01", type: "desktop" },
    942  });
    943 
    944  Assert.equal(Status.sync, SYNC_SUCCEEDED);
    945 
    946  Assert.ok(await setUp(server));
    947 
    948  await Service.sync();
    949 
    950  Assert.equal(Status.service, SYNC_FAILED_PARTIAL);
    951  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);
    952 
    953  Assert.equal(Status.backoffInterval, 0);
    954  Assert.ok(!Status.enforceBackoff);
    955  Assert.equal(scheduler._syncErrors, 0);
    956  Assert.lessOrEqual(scheduler.nextSync, Date.now() + scheduler.activeInterval);
    957  Assert.lessOrEqual(scheduler.syncTimer.delay, scheduler.activeInterval);
    958 
    959  await cleanUpAndGo(server);
    960 });
    961 
    962 add_task(async function test_sync_X_Weave_Backoff() {
    963  enableValidationPrefs();
    964 
    965  let server = await sync_httpd_setup();
    966  await setUp(server);
    967 
    968  // Use an odd value on purpose so that it doesn't happen to coincide with one
    969  // of the sync intervals.
    970  const BACKOFF = 7337;
    971 
    972  // Extend info/collections so that we can put it into server maintenance mode.
    973  const INFO_COLLECTIONS = "/1.1/johndoe@mozilla.com/info/collections";
    974  let infoColl = server._handler._overridePaths[INFO_COLLECTIONS];
    975  let serverBackoff = false;
    976  function infoCollWithBackoff(request, response) {
    977    if (serverBackoff) {
    978      response.setHeader("X-Weave-Backoff", "" + BACKOFF);
    979    }
    980    infoColl(request, response);
    981  }
    982  server.registerPathHandler(INFO_COLLECTIONS, infoCollWithBackoff);
    983 
    984  // Pretend we have two clients so that the regular sync interval is
    985  // sufficiently low.
    986  await clientsEngine._store.create({
    987    id: "foo",
    988    cleartext: { os: "mobile", version: "0.01", type: "desktop" },
    989  });
    990  let rec = await clientsEngine._store.createRecord("foo", "clients");
    991  await rec.encrypt(Service.collectionKeys.keyForCollection("clients"));
    992  await rec.upload(Service.resource(clientsEngine.engineURL + rec.id));
    993 
    994  // Sync once to log in and get everything set up. Let's verify our initial
    995  // values.
    996  await Service.sync();
    997  Assert.equal(Status.backoffInterval, 0);
    998  Assert.equal(Status.minimumNextSync, 0);
    999  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);
   1000  Assert.lessOrEqual(scheduler.nextSync, Date.now() + scheduler.syncInterval);
   1001  // Sanity check that we picked the right value for BACKOFF:
   1002  Assert.less(scheduler.syncInterval, BACKOFF * 1000);
   1003 
   1004  // Turn on server maintenance and sync again.
   1005  serverBackoff = true;
   1006  await Service.sync();
   1007 
   1008  Assert.greaterOrEqual(Status.backoffInterval, BACKOFF * 1000);
   1009  // Allowing 20 seconds worth of of leeway between when Status.minimumNextSync
   1010  // was set and when this line gets executed.
   1011  let minimumExpectedDelay = (BACKOFF - 20) * 1000;
   1012  Assert.greaterOrEqual(
   1013    Status.minimumNextSync,
   1014    Date.now() + minimumExpectedDelay
   1015  );
   1016 
   1017  // Verify that the next sync is actually going to wait that long.
   1018  Assert.greaterOrEqual(scheduler.nextSync, Date.now() + minimumExpectedDelay);
   1019  Assert.greaterOrEqual(scheduler.syncTimer.delay, minimumExpectedDelay);
   1020 
   1021  await cleanUpAndGo(server);
   1022 });
   1023 
   1024 add_task(async function test_sync_503_Retry_After() {
   1025  enableValidationPrefs();
   1026 
   1027  let server = await sync_httpd_setup();
   1028  await setUp(server);
   1029 
   1030  // Use an odd value on purpose so that it doesn't happen to coincide with one
   1031  // of the sync intervals.
   1032  const BACKOFF = 7337;
   1033 
   1034  // Extend info/collections so that we can put it into server maintenance mode.
   1035  const INFO_COLLECTIONS = "/1.1/johndoe@mozilla.com/info/collections";
   1036  let infoColl = server._handler._overridePaths[INFO_COLLECTIONS];
   1037  let serverMaintenance = false;
   1038  function infoCollWithMaintenance(request, response) {
   1039    if (!serverMaintenance) {
   1040      infoColl(request, response);
   1041      return;
   1042    }
   1043    response.setHeader("Retry-After", "" + BACKOFF);
   1044    response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
   1045  }
   1046  server.registerPathHandler(INFO_COLLECTIONS, infoCollWithMaintenance);
   1047 
   1048  // Pretend we have two clients so that the regular sync interval is
   1049  // sufficiently low.
   1050  await clientsEngine._store.create({
   1051    id: "foo",
   1052    cleartext: { os: "mobile", version: "0.01", type: "desktop" },
   1053  });
   1054  let rec = await clientsEngine._store.createRecord("foo", "clients");
   1055  await rec.encrypt(Service.collectionKeys.keyForCollection("clients"));
   1056  await rec.upload(Service.resource(clientsEngine.engineURL + rec.id));
   1057 
   1058  // Sync once to log in and get everything set up. Let's verify our initial
   1059  // values.
   1060  await Service.sync();
   1061  Assert.ok(!Status.enforceBackoff);
   1062  Assert.equal(Status.backoffInterval, 0);
   1063  Assert.equal(Status.minimumNextSync, 0);
   1064  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);
   1065  Assert.lessOrEqual(scheduler.nextSync, Date.now() + scheduler.syncInterval);
   1066  // Sanity check that we picked the right value for BACKOFF:
   1067  Assert.less(scheduler.syncInterval, BACKOFF * 1000);
   1068 
   1069  // Turn on server maintenance and sync again.
   1070  serverMaintenance = true;
   1071  await Service.sync();
   1072 
   1073  Assert.ok(Status.enforceBackoff);
   1074  Assert.greaterOrEqual(Status.backoffInterval, BACKOFF * 1000);
   1075  // Allowing 3 seconds worth of of leeway between when Status.minimumNextSync
   1076  // was set and when this line gets executed.
   1077  let minimumExpectedDelay = (BACKOFF - 3) * 1000;
   1078  Assert.greaterOrEqual(
   1079    Status.minimumNextSync,
   1080    Date.now() + minimumExpectedDelay
   1081  );
   1082 
   1083  // Verify that the next sync is actually going to wait that long.
   1084  Assert.greaterOrEqual(scheduler.nextSync, Date.now() + minimumExpectedDelay);
   1085  Assert.greaterOrEqual(scheduler.syncTimer.delay, minimumExpectedDelay);
   1086 
   1087  await cleanUpAndGo(server);
   1088 });
   1089 
   1090 add_task(async function test_loginError_recoverable_reschedules() {
   1091  _("Verify that a recoverable login error schedules a new sync.");
   1092  await configureIdentity({ username: "johndoe@mozilla.com" });
   1093  Service.clusterURL = "http://localhost:1234/";
   1094  Status.resetSync(); // reset Status.login
   1095 
   1096  let promiseObserved = promiseOneObserver("weave:service:login:error");
   1097 
   1098  // Let's set it up so that a sync is overdue, both in terms of previously
   1099  // scheduled syncs and the global score. We still do not expect an immediate
   1100  // sync because we just tried (duh).
   1101  scheduler.nextSync = Date.now() - 100000;
   1102  scheduler.globalScore = SINGLE_USER_THRESHOLD + 1;
   1103  function onSyncStart() {
   1104    do_throw("Shouldn't have started a sync!");
   1105  }
   1106  Svc.Obs.add("weave:service:sync:start", onSyncStart);
   1107 
   1108  // Sanity check.
   1109  Assert.equal(scheduler.syncTimer, null);
   1110  Assert.equal(Status.checkSetup(), STATUS_OK);
   1111  Assert.equal(Status.login, LOGIN_SUCCEEDED);
   1112 
   1113  scheduler.scheduleNextSync(0);
   1114  await promiseObserved;
   1115  await Async.promiseYield();
   1116 
   1117  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR);
   1118 
   1119  let expectedNextSync = Date.now() + scheduler.syncInterval;
   1120  Assert.greater(scheduler.nextSync, Date.now());
   1121  Assert.lessOrEqual(scheduler.nextSync, expectedNextSync);
   1122  Assert.greater(scheduler.syncTimer.delay, 0);
   1123  Assert.lessOrEqual(scheduler.syncTimer.delay, scheduler.syncInterval);
   1124 
   1125  Svc.Obs.remove("weave:service:sync:start", onSyncStart);
   1126  await cleanUpAndGo();
   1127 });
   1128 
   1129 add_task(async function test_loginError_fatal_clearsTriggers() {
   1130  _("Verify that a fatal login error clears sync triggers.");
   1131  await configureIdentity({ username: "johndoe@mozilla.com" });
   1132 
   1133  let server = httpd_setup({
   1134    "/1.1/johndoe@mozilla.com/info/collections": httpd_handler(
   1135      401,
   1136      "Unauthorized"
   1137    ),
   1138  });
   1139 
   1140  Service.clusterURL = server.baseURI + "/";
   1141  Status.resetSync(); // reset Status.login
   1142 
   1143  let promiseObserved = promiseOneObserver("weave:service:login:error");
   1144 
   1145  // Sanity check.
   1146  Assert.equal(scheduler.nextSync, 0);
   1147  Assert.equal(scheduler.syncTimer, null);
   1148  Assert.equal(Status.checkSetup(), STATUS_OK);
   1149  Assert.equal(Status.login, LOGIN_SUCCEEDED);
   1150 
   1151  scheduler.scheduleNextSync(0);
   1152  await promiseObserved;
   1153  await Async.promiseYield();
   1154 
   1155  // For the FxA identity, a 401 on info/collections means a transient
   1156  // error, probably due to an inability to fetch a token.
   1157  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR);
   1158  // syncs should still be scheduled.
   1159  Assert.greater(scheduler.nextSync, Date.now());
   1160  Assert.greater(scheduler.syncTimer.delay, 0);
   1161 
   1162  await cleanUpAndGo(server);
   1163 });
   1164 
   1165 add_task(async function test_proper_interval_on_only_failing() {
   1166  _("Ensure proper behavior when only failed records are applied.");
   1167 
   1168  // If an engine reports that no records succeeded, we shouldn't decrease the
   1169  // sync interval.
   1170  Assert.ok(!scheduler.hasIncomingItems);
   1171  const INTERVAL = 10000000;
   1172  scheduler.syncInterval = INTERVAL;
   1173 
   1174  Svc.Obs.notify("weave:service:sync:applied", {
   1175    applied: 2,
   1176    succeeded: 0,
   1177    failed: 2,
   1178    newFailed: 2,
   1179    reconciled: 0,
   1180  });
   1181 
   1182  await Async.promiseYield();
   1183  scheduler.adjustSyncInterval();
   1184  Assert.ok(!scheduler.hasIncomingItems);
   1185  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
   1186 });
   1187 
   1188 add_task(async function test_link_status_change() {
   1189  _("Check that we only attempt to sync when link status is up");
   1190  try {
   1191    sinon.spy(scheduler, "scheduleNextSync");
   1192 
   1193    Svc.Obs.notify("network:link-status-changed", null, "down");
   1194    equal(scheduler.scheduleNextSync.callCount, 0);
   1195 
   1196    Svc.Obs.notify("network:link-status-changed", null, "change");
   1197    equal(scheduler.scheduleNextSync.callCount, 0);
   1198 
   1199    Svc.Obs.notify("network:link-status-changed", null, "up");
   1200    equal(scheduler.scheduleNextSync.callCount, 1);
   1201 
   1202    Svc.Obs.notify("network:link-status-changed", null, "change");
   1203    equal(scheduler.scheduleNextSync.callCount, 1);
   1204  } finally {
   1205    scheduler.scheduleNextSync.restore();
   1206  }
   1207 });