tor-browser

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

test_glean.js (40452B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { Service } = ChromeUtils.importESModule(
      5  "resource://services-sync/service.sys.mjs"
      6 );
      7 const { WBORecord } = ChromeUtils.importESModule(
      8  "resource://services-sync/record.sys.mjs"
      9 );
     10 const { RotaryEngine } = ChromeUtils.importESModule(
     11  "resource://testing-common/services/sync/rotaryengine.sys.mjs"
     12 );
     13 const { getFxAccountsSingleton } = ChromeUtils.importESModule(
     14  "resource://gre/modules/FxAccounts.sys.mjs"
     15 );
     16 const fxAccounts = getFxAccountsSingleton();
     17 
     18 function SteamStore(engine) {
     19  Store.call(this, "Steam", engine);
     20 }
     21 Object.setPrototypeOf(SteamStore.prototype, Store.prototype);
     22 
     23 function SteamTracker(name, engine) {
     24  LegacyTracker.call(this, name || "Steam", engine);
     25 }
     26 Object.setPrototypeOf(SteamTracker.prototype, LegacyTracker.prototype);
     27 
     28 function SteamEngine(service) {
     29  SyncEngine.call(this, "steam", service);
     30 }
     31 
     32 SteamEngine.prototype = {
     33  _storeObj: SteamStore,
     34  _trackerObj: SteamTracker,
     35  _errToThrow: null,
     36  problemsToReport: null,
     37  async _sync() {
     38    if (this._errToThrow) {
     39      throw this._errToThrow;
     40    }
     41  },
     42  getValidator() {
     43    return new SteamValidator();
     44  },
     45 };
     46 Object.setPrototypeOf(SteamEngine.prototype, SyncEngine.prototype);
     47 
     48 function BogusEngine(service) {
     49  SyncEngine.call(this, "bogus", service);
     50 }
     51 
     52 BogusEngine.prototype = Object.create(SteamEngine.prototype);
     53 
     54 class SteamValidator {
     55  async canValidate() {
     56    return true;
     57  }
     58 
     59  async validate(engine) {
     60    return {
     61      problems: new SteamValidationProblemData(engine.problemsToReport),
     62      version: 1,
     63      duration: 0,
     64      recordCount: 0,
     65    };
     66  }
     67 }
     68 
     69 class SteamValidationProblemData {
     70  constructor(problemsToReport = []) {
     71    this.problemsToReport = problemsToReport;
     72  }
     73 
     74  getSummary() {
     75    return this.problemsToReport;
     76  }
     77 }
     78 
     79 async function cleanAndGo(engine, server) {
     80  await engine._tracker.clearChangedIDs();
     81  for (const pref of Svc.PrefBranch.getChildList("")) {
     82    Svc.PrefBranch.clearUserPref(pref);
     83  }
     84  syncTestLogging();
     85  Service.recordManager.clearCache();
     86  await promiseStopServer(server);
     87 }
     88 
     89 async function sync_engine(engine, errorRegex = undefined) {
     90  // Clear out status so failures from previous syncs won't show in the record.
     91  let { Status } = ChromeUtils.importESModule(
     92    "resource://services-sync/status.sys.mjs"
     93  );
     94  Status._engines = {};
     95  Status.partial = false;
     96  // Neuter the scheduler as it interacts badly with some of the tests - the
     97  // engine being synced usually isn't the registered engine, so we see
     98  // scored incremented and not removed, which schedules unexpected syncs.
     99  let oldObserve = Service.scheduler.observe;
    100  Service.scheduler.observe = () => {};
    101  try {
    102    Svc.Obs.notify("weave:service:sync:start");
    103    let caughtError;
    104    try {
    105      await engine.sync();
    106    } catch (e) {
    107      caughtError = e;
    108    }
    109    if (caughtError) {
    110      ok(
    111        errorRegex.test(JSON.stringify(caughtError)),
    112        "Engine sync error expected."
    113      );
    114      Svc.Obs.notify("weave:service:sync:error", caughtError);
    115    } else {
    116      ok(!errorRegex, "No engine sync error expected.");
    117      Svc.Obs.notify("weave:service:sync:finish");
    118    }
    119  } finally {
    120    Service.scheduler.observe = oldObserve;
    121  }
    122 }
    123 
    124 add_setup(async function () {
    125  // Avoid addon manager complaining about not being initialized
    126  await Service.engineManager.unregister("addons");
    127  await Service.engineManager.unregister("extension-storage");
    128 
    129  do_get_profile(); // FOG requires a profile dir.
    130  Services.fog.initializeFOG();
    131 
    132  // We're not using `wait_for_ping` which means we aren't implicitly calling
    133  // get_sync_test_telemetry() so often. So call it here for its side effects.
    134  let telem = get_sync_test_telemetry();
    135  // Unlike the telemetry tests, we actually want the Glean APIs to be called.
    136  // So pretend we're in production.
    137  telem.isProductionSyncUser = () => true;
    138 });
    139 
    140 add_task(async function test_basic() {
    141  enableValidationPrefs();
    142 
    143  let helper = track_collections_helper();
    144  let upd = helper.with_updated_collection;
    145 
    146  let handlers = {
    147    "/1.1/johndoe/info/collections": helper.handler,
    148    "/1.1/johndoe/storage/crypto/keys": upd(
    149      "crypto",
    150      new ServerWBO("keys").handler()
    151    ),
    152    "/1.1/johndoe/storage/meta/global": upd(
    153      "meta",
    154      new ServerWBO("global").handler()
    155    ),
    156  };
    157 
    158  let collections = [
    159    "clients",
    160    "bookmarks",
    161    "forms",
    162    "history",
    163    "passwords",
    164    "prefs",
    165    "tabs",
    166  ];
    167 
    168  for (let coll of collections) {
    169    handlers["/1.1/johndoe/storage/" + coll] = upd(
    170      coll,
    171      new ServerCollection({}, true).handler()
    172    );
    173  }
    174 
    175  let server = httpd_setup(handlers);
    176  await configureIdentity({ username: "johndoe" }, server);
    177 
    178  // Test that a basic batch can make it.
    179  await GleanPings.sync.testSubmission(
    180    reason => {
    181      equal(reason, "schedule");
    182      Glean.syncs.syncs.testGetValue().forEach(assert_success_sync);
    183    },
    184    async () => {
    185      await Service.sync();
    186    }
    187  );
    188 
    189  for (const pref of Svc.PrefBranch.getChildList("")) {
    190    Svc.PrefBranch.clearUserPref(pref);
    191  }
    192  await promiseStopServer(server);
    193 });
    194 
    195 add_task(async function test_processIncoming_error() {
    196  let engine = Service.engineManager.get("bookmarks");
    197  await engine.initialize();
    198  let store = engine._store;
    199  let server = await serverForFoo(engine);
    200  await SyncTestingInfrastructure(server);
    201  let collection = server.user("foo").collection("bookmarks");
    202  try {
    203    // Create a bogus record that when synced down will provoke a
    204    // network error which in turn provokes an exception in _processIncoming.
    205    const BOGUS_GUID = "zzzzzzzzzzzz";
    206    let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
    207    bogus_record.get = function get() {
    208      throw new Error("Sync this!");
    209    };
    210    // Make it 10 minutes old so it will only be synced in the toFetch phase.
    211    bogus_record.modified = Date.now() / 1000 - 60 * 10;
    212    await engine.setLastSync(Date.now() / 1000 - 60);
    213    engine.toFetch = new SerializableSet([BOGUS_GUID]);
    214 
    215    await GleanPings.sync.testSubmission(
    216      reason => {
    217        equal(reason, "schedule");
    218        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    219 
    220        const syncs = Glean.syncs.syncs.testGetValue();
    221        equal(syncs.length, 1);
    222 
    223        const sync = syncs[0];
    224        deepEqual(sync.failureReason, { name: "httperror", code: 500 });
    225        equal(sync.engines.length, 1);
    226 
    227        const e = sync.engines[0];
    228        equal(e.name, "bookmarks-buffered");
    229        deepEqual(e.failureReason, { name: "httperror", code: 500 });
    230      },
    231      async () => {
    232        await sync_engine(engine, /500 Internal Server Error/);
    233      }
    234    );
    235  } finally {
    236    await store.wipe();
    237    await cleanAndGo(engine, server);
    238  }
    239 });
    240 
    241 add_task(async function test_uploading() {
    242  // Clear out status, so failures from previous syncs won't show up in the
    243  // telemetry ping.
    244  let { Status } = ChromeUtils.importESModule(
    245    "resource://services-sync/status.sys.mjs"
    246  );
    247  Status._engines = {};
    248  Status.partial = false;
    249 
    250  let engine = Service.engineManager.get("bookmarks");
    251  await engine.initialize();
    252  let store = engine._store;
    253  let server = await serverForFoo(engine);
    254  await SyncTestingInfrastructure(server);
    255 
    256  let bmk = await PlacesUtils.bookmarks.insert({
    257    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
    258    url: "http://getfirefox.com/",
    259    title: "Get Firefox!",
    260  });
    261 
    262  try {
    263    await GleanPings.sync.testSubmission(
    264      reason => {
    265        equal(reason, "schedule");
    266        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    267 
    268        const syncs = Glean.syncs.syncs.testGetValue();
    269        equal(syncs.length, 1);
    270 
    271        const sync = syncs[0];
    272        equal(sync.engines.length, 1);
    273 
    274        const e = sync.engines[0];
    275        equal(e.name, "bookmarks-buffered");
    276        greater(e.outgoing[0].sent, 0);
    277        ok(!e.incoming);
    278      },
    279      async () => {
    280        await sync_engine(engine);
    281      }
    282    );
    283 
    284    await PlacesUtils.bookmarks.update({
    285      guid: bmk.guid,
    286      title: "New Title",
    287    });
    288 
    289    await store.wipe();
    290    await engine.resetClient();
    291    // We don't sync via the service, so don't re-hit info/collections, so
    292    // lastModified remaning at zero breaks things subtly...
    293    engine.lastModified = null;
    294 
    295    await GleanPings.sync.testSubmission(
    296      reason => {
    297        equal(reason, "schedule");
    298        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    299 
    300        const syncs = Glean.syncs.syncs.testGetValue();
    301        equal(syncs.length, 1);
    302 
    303        const sync = syncs[0];
    304        equal(sync.engines.length, 1);
    305 
    306        const e = sync.engines[0];
    307        equal(e.name, "bookmarks-buffered");
    308        equal(e.outgoing.length, 1);
    309        ok(!!e.incoming);
    310      },
    311      async () => {
    312        await sync_engine(engine);
    313      }
    314    );
    315  } finally {
    316    // Clean up.
    317    await store.wipe();
    318    await cleanAndGo(engine, server);
    319  }
    320 });
    321 
    322 add_task(async function test_upload_failed() {
    323  let collection = new ServerCollection();
    324  collection._wbos.flying = new ServerWBO("flying");
    325 
    326  let server = sync_httpd_setup({
    327    "/1.1/foo/storage/rotary": collection.handler(),
    328  });
    329 
    330  await SyncTestingInfrastructure(server);
    331  await configureIdentity({ username: "foo" }, server);
    332 
    333  let engine = new RotaryEngine(Service);
    334  engine._store.items = {
    335    flying: "LNER Class A3 4472",
    336    scotsman: "Flying Scotsman",
    337    peppercorn: "Peppercorn Class",
    338  };
    339  const FLYING_CHANGED = 12345;
    340  const SCOTSMAN_CHANGED = 23456;
    341  const PEPPERCORN_CHANGED = 34567;
    342  await engine._tracker.addChangedID("flying", FLYING_CHANGED);
    343  await engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
    344  await engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
    345 
    346  let syncID = await engine.resetLocalSyncID();
    347  let meta_global = Service.recordManager.set(
    348    engine.metaURL,
    349    new WBORecord(engine.metaURL)
    350  );
    351  meta_global.payload.engines = { rotary: { version: engine.version, syncID } };
    352 
    353  try {
    354    await engine.setLastSync(123); // needs to be non-zero so that tracker is queried
    355    let changes = await engine._tracker.getChangedIDs();
    356    _(
    357      `test_upload_failed: Rotary tracker contents at first sync: ${JSON.stringify(
    358        changes
    359      )}`
    360    );
    361    engine.enabled = true;
    362    await GleanPings.sync.testSubmission(
    363      reason => {
    364        equal(reason, "schedule");
    365        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    366 
    367        const syncs = Glean.syncs.syncs.testGetValue();
    368        equal(syncs.length, 1);
    369 
    370        const sync = syncs[0];
    371        equal(sync.engines.length, 1);
    372 
    373        const e = sync.engines[0];
    374        equal(e.incoming, null);
    375        deepEqual(e.outgoing, [
    376          {
    377            sent: 3,
    378            failed: 2,
    379            failedReasons: [
    380              { name: "scotsman", count: 1 },
    381              { name: "peppercorn", count: 1 },
    382            ],
    383          },
    384        ]);
    385      },
    386      async () => {
    387        await sync_engine(engine);
    388      }
    389    );
    390 
    391    await engine.setLastSync(123);
    392 
    393    changes = await engine._tracker.getChangedIDs();
    394    _(
    395      `test_upload_failed: Rotary tracker contents at second sync: ${JSON.stringify(
    396        changes
    397      )}`
    398    );
    399    await GleanPings.sync.testSubmission(
    400      reason => {
    401        equal(reason, "schedule");
    402        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    403 
    404        const syncs = Glean.syncs.syncs.testGetValue();
    405        equal(syncs.length, 1);
    406 
    407        const sync = syncs[0];
    408        equal(sync.engines.length, 1);
    409 
    410        const e = sync.engines[0];
    411        deepEqual(e.outgoing, [
    412          {
    413            sent: 2,
    414            failed: 2,
    415            failedReasons: [
    416              { name: "scotsman", count: 1 },
    417              { name: "peppercorn", count: 1 },
    418            ],
    419          },
    420        ]);
    421      },
    422      async () => {
    423        await sync_engine(engine);
    424      }
    425    );
    426  } finally {
    427    await cleanAndGo(engine, server);
    428    await engine.finalize();
    429  }
    430 });
    431 
    432 add_task(async function test_sync_partialUpload() {
    433  let collection = new ServerCollection();
    434  let server = sync_httpd_setup({
    435    "/1.1/foo/storage/rotary": collection.handler(),
    436  });
    437  await SyncTestingInfrastructure(server);
    438  await generateNewKeys(Service.collectionKeys);
    439 
    440  let engine = new RotaryEngine(Service);
    441  await engine.setLastSync(123);
    442 
    443  // Create a bunch of records (and server side handlers)
    444  for (let i = 0; i < 234; i++) {
    445    let id = "record-no-" + i;
    446    engine._store.items[id] = "Record No. " + i;
    447    await engine._tracker.addChangedID(id, i);
    448    // Let two items in the first upload batch fail.
    449    if (i != 23 && i != 42) {
    450      collection.insert(id);
    451    }
    452  }
    453 
    454  let syncID = await engine.resetLocalSyncID();
    455  let meta_global = Service.recordManager.set(
    456    engine.metaURL,
    457    new WBORecord(engine.metaURL)
    458  );
    459  meta_global.payload.engines = { rotary: { version: engine.version, syncID } };
    460 
    461  try {
    462    let changes = await engine._tracker.getChangedIDs();
    463    _(
    464      `test_sync_partialUpload: Rotary tracker contents at first sync: ${JSON.stringify(
    465        changes
    466      )}`
    467    );
    468    engine.enabled = true;
    469    await GleanPings.sync.testSubmission(
    470      reason => {
    471        equal(reason, "schedule");
    472        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    473 
    474        const syncs = Glean.syncs.syncs.testGetValue();
    475        equal(syncs.length, 1);
    476 
    477        const sync = syncs[0];
    478        ok(!sync.failureReason);
    479        equal(sync.engines.length, 1);
    480 
    481        const e = sync.engines[0];
    482        equal(e.name, "rotary");
    483        ok(!e.incoming);
    484        ok(!e.failureReason);
    485        deepEqual(e.outgoing, [
    486          {
    487            sent: 234,
    488            failed: 2,
    489            failedReasons: [
    490              { name: "record-no-23", count: 1 },
    491              { name: "record-no-42", count: 1 },
    492            ],
    493          },
    494        ]);
    495      },
    496      async () => {
    497        await sync_engine(engine);
    498      }
    499    );
    500 
    501    collection.post = function () {
    502      throw new Error("Failure");
    503    };
    504 
    505    engine._store.items["record-no-1000"] = "Record No. 1000";
    506    await engine._tracker.addChangedID("record-no-1000", 1000);
    507    collection.insert("record-no-1000", 1000);
    508 
    509    await engine.setLastSync(123);
    510 
    511    changes = await engine._tracker.getChangedIDs();
    512    _(
    513      `test_sync_partialUpload: Rotary tracker contents at second sync: ${JSON.stringify(
    514        changes
    515      )}`
    516    );
    517    // It would be nice if we had a more descriptive error for this...
    518    const uploadFailureError = {
    519      name: "httperror",
    520      code: 500,
    521    };
    522    await GleanPings.sync.testSubmission(
    523      reason => {
    524        equal(reason, "schedule");
    525        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    526 
    527        const syncs = Glean.syncs.syncs.testGetValue();
    528        equal(syncs.length, 1);
    529 
    530        const sync = syncs[0];
    531        deepEqual(sync.failureReason, uploadFailureError);
    532        equal(sync.engines.length, 1);
    533 
    534        const e = sync.engines[0];
    535        equal(e.name, "rotary");
    536        deepEqual(e.incoming, {
    537          failed: 1,
    538          failedReasons: [
    539            { name: "No ciphertext: nothing to decrypt?", count: 1 },
    540          ],
    541        });
    542        ok(!e.outgoing);
    543        deepEqual(e.failureReason, uploadFailureError);
    544      },
    545      async () => {
    546        await sync_engine(engine, /500 Internal Server Error/);
    547      }
    548    );
    549  } finally {
    550    await cleanAndGo(engine, server);
    551    await engine.finalize();
    552  }
    553 });
    554 
    555 add_task(async function test_generic_engine_fail() {
    556  enableValidationPrefs();
    557 
    558  await Service.engineManager.register(SteamEngine);
    559  let engine = Service.engineManager.get("steam");
    560  engine.enabled = true;
    561  let server = await serverForFoo(engine);
    562  await SyncTestingInfrastructure(server);
    563  let e = new Error("generic failure message");
    564  engine._errToThrow = e;
    565 
    566  try {
    567    const changes = await engine._tracker.getChangedIDs();
    568    _(
    569      `test_generic_engine_fail: Steam tracker contents: ${JSON.stringify(
    570        changes
    571      )}`
    572    );
    573    await GleanPings.sync.testSubmission(
    574      reason => {
    575        equal(reason, "schedule");
    576        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    577 
    578        const syncs = Glean.syncs.syncs.testGetValue();
    579        equal(syncs.length, 1);
    580 
    581        const sync = syncs[0];
    582        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    583        deepEqual(
    584          sync.engines.find(err => err.name === "steam").failureReason,
    585          {
    586            name: "unexpectederror",
    587            error: String(e),
    588          }
    589        );
    590      },
    591      async () => {
    592        await Service.sync();
    593      }
    594    );
    595  } finally {
    596    await cleanAndGo(engine, server);
    597    await Service.engineManager.unregister(engine);
    598  }
    599 });
    600 
    601 add_task(async function test_engine_fail_weird_errors() {
    602  enableValidationPrefs();
    603  await Service.engineManager.register(SteamEngine);
    604  let engine = Service.engineManager.get("steam");
    605  engine.enabled = true;
    606  let server = await serverForFoo(engine);
    607  await SyncTestingInfrastructure(server);
    608  try {
    609    let msg = "Bad things happened!";
    610    engine._errToThrow = { message: msg };
    611    await GleanPings.sync.testSubmission(
    612      reason => {
    613        equal(reason, "schedule");
    614        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    615 
    616        const syncs = Glean.syncs.syncs.testGetValue();
    617        equal(syncs.length, 1);
    618 
    619        const sync = syncs[0];
    620        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    621        deepEqual(
    622          sync.engines.find(err => err.name === "steam").failureReason,
    623          {
    624            name: "unexpectederror",
    625            error: msg,
    626          }
    627        );
    628      },
    629      async () => {
    630        await Service.sync();
    631      }
    632    );
    633    let e = { msg };
    634    engine._errToThrow = e;
    635    await GleanPings.sync.testSubmission(
    636      reason => {
    637        equal(reason, "schedule");
    638        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    639 
    640        const syncs = Glean.syncs.syncs.testGetValue();
    641        equal(syncs.length, 1);
    642 
    643        const sync = syncs[0];
    644        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    645        deepEqual(
    646          sync.engines.find(err => err.name === "steam").failureReason,
    647          {
    648            name: "unexpectederror",
    649            error: JSON.stringify(e),
    650          }
    651        );
    652      },
    653      async () => {
    654        await Service.sync();
    655      }
    656    );
    657  } finally {
    658    await cleanAndGo(engine, server);
    659    Service.engineManager.unregister(engine);
    660  }
    661 });
    662 
    663 add_task(async function test_overrideTelemetryName() {
    664  enableValidationPrefs(["steam"]);
    665 
    666  await Service.engineManager.register(SteamEngine);
    667  let engine = Service.engineManager.get("steam");
    668  engine.overrideTelemetryName = "steam-but-better";
    669  engine.enabled = true;
    670  let server = await serverForFoo(engine);
    671  await SyncTestingInfrastructure(server);
    672 
    673  const problemsToReport = [
    674    { name: "someProblem", count: 123 },
    675    { name: "anotherProblem", count: 456 },
    676  ];
    677 
    678  try {
    679    info("Sync with validation problems");
    680    engine.problemsToReport = problemsToReport;
    681    await GleanPings.sync.testSubmission(
    682      reason => {
    683        equal(reason, "schedule");
    684        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    685 
    686        const syncs = Glean.syncs.syncs.testGetValue();
    687        equal(syncs.length, 1);
    688 
    689        const sync = syncs[0];
    690        ok(!sync.engines.find(e => e.name === "steam"));
    691        const eng = sync.engines.find(e => e.name === "steam-but-better");
    692        ok(eng);
    693        delete eng.validation.took; // can't compare real times.
    694        deepEqual(
    695          eng.validation,
    696          {
    697            version: 1,
    698            checked: 0,
    699            problems: problemsToReport,
    700          },
    701          "Should include validation report with overridden name"
    702        );
    703      },
    704      async () => {
    705        await Service.sync();
    706      }
    707    );
    708 
    709    info("Sync without validation problems");
    710    engine.problemsToReport = null;
    711    await GleanPings.sync.testSubmission(
    712      reason => {
    713        equal(reason, "schedule");
    714        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    715 
    716        const syncs = Glean.syncs.syncs.testGetValue();
    717        equal(syncs.length, 1);
    718 
    719        const sync = syncs[0];
    720        ok(!sync.engines.find(e => e.name === "steam"));
    721        const eng = sync.engines.find(e => e.name === "steam-but-better");
    722        ok(eng);
    723        ok(
    724          !eng.validation,
    725          "Should not include validation report when there are no problems"
    726        );
    727      },
    728      async () => {
    729        await Service.sync();
    730      }
    731    );
    732  } finally {
    733    await cleanAndGo(engine, server);
    734    await Service.engineManager.unregister(engine);
    735  }
    736 });
    737 
    738 add_task(async function test_engine_fail_ioerror() {
    739  enableValidationPrefs();
    740 
    741  await Service.engineManager.register(SteamEngine);
    742  let engine = Service.engineManager.get("steam");
    743  engine.enabled = true;
    744  let server = await serverForFoo(engine);
    745  await SyncTestingInfrastructure(server);
    746  // create an IOError to re-throw as part of Sync.
    747  try {
    748    // (Note that fakeservices.js has replaced Utils.jsonMove etc, but for
    749    // this test we need the real one so we get real exceptions from the
    750    // filesystem.)
    751    await Utils._real_jsonMove("file-does-not-exist", "anything", {});
    752  } catch (ex) {
    753    engine._errToThrow = ex;
    754  }
    755  ok(engine._errToThrow, "expecting exception");
    756 
    757  try {
    758    const changes = await engine._tracker.getChangedIDs();
    759    _(
    760      `test_engine_fail_ioerror: Steam tracker contents: ${JSON.stringify(
    761        changes
    762      )}`
    763    );
    764    await GleanPings.sync.testSubmission(
    765      reason => {
    766        equal(reason, "schedule");
    767        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    768 
    769        const syncs = Glean.syncs.syncs.testGetValue();
    770        equal(syncs.length, 1);
    771 
    772        const sync = syncs[0];
    773        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    774        const failureReason = sync.engines.find(
    775          e => e.name === "steam"
    776        ).failureReason;
    777        equal(failureReason.name, "unexpectederror");
    778        // ensure the profile dir in the exception message has been stripped.
    779        ok(
    780          !failureReason.error.includes(PathUtils.profileDir),
    781          failureReason.error
    782        );
    783        ok(failureReason.error.includes("[profileDir]"), failureReason.error);
    784      },
    785      async () => {
    786        await Service.sync();
    787      }
    788    );
    789  } finally {
    790    await cleanAndGo(engine, server);
    791    await Service.engineManager.unregister(engine);
    792  }
    793 });
    794 
    795 add_task(async function test_clean_urls() {
    796  enableValidationPrefs();
    797 
    798  await Service.engineManager.register(SteamEngine);
    799  let engine = Service.engineManager.get("steam");
    800  engine.enabled = true;
    801  let server = await serverForFoo(engine);
    802  await SyncTestingInfrastructure(server);
    803  engine._errToThrow = new TypeError(
    804    "http://www.google .com is not a valid URL."
    805  );
    806 
    807  try {
    808    const changes = await engine._tracker.getChangedIDs();
    809    _(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`);
    810    await GleanPings.sync.testSubmission(
    811      reason => {
    812        equal(reason, "schedule");
    813        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    814 
    815        const syncs = Glean.syncs.syncs.testGetValue();
    816        equal(syncs.length, 1);
    817 
    818        const sync = syncs[0];
    819        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    820        const failureReason = sync.engines.find(
    821          e => e.name === "steam"
    822        ).failureReason;
    823        equal(failureReason.name, "unexpectederror");
    824        equal(failureReason.error, "<URL> is not a valid URL.");
    825      },
    826      async () => {
    827        await Service.sync();
    828      }
    829    );
    830    // Handle other errors that include urls.
    831    engine._errToThrow =
    832      "Other error message that includes some:url/foo/bar/ in it.";
    833    await GleanPings.sync.testSubmission(
    834      reason => {
    835        equal(reason, "schedule");
    836        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    837 
    838        const syncs = Glean.syncs.syncs.testGetValue();
    839        equal(syncs.length, 1);
    840 
    841        const sync = syncs[0];
    842        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    843        const failureReason = sync.engines.find(
    844          e => e.name === "steam"
    845        ).failureReason;
    846        equal(failureReason.name, "unexpectederror");
    847        equal(
    848          failureReason.error,
    849          "Other error message that includes <URL> in it."
    850        );
    851      },
    852      async () => {
    853        await Service.sync();
    854      }
    855    );
    856  } finally {
    857    await cleanAndGo(engine, server);
    858    await Service.engineManager.unregister(engine);
    859  }
    860 });
    861 
    862 // Arrange for a sync to hit a "real" OS error during a sync and make sure it's sanitized.
    863 add_task(async function test_clean_real_os_error() {
    864  enableValidationPrefs();
    865 
    866  // Simulate a real error.
    867  await Service.engineManager.register(SteamEngine);
    868  let engine = Service.engineManager.get("steam");
    869  engine.enabled = true;
    870  let server = await serverForFoo(engine);
    871  await SyncTestingInfrastructure(server);
    872  let path = PathUtils.join(PathUtils.profileDir, "no", "such", "path.json");
    873  try {
    874    await IOUtils.readJSON(path);
    875    throw new Error("should fail to read the file");
    876  } catch (ex) {
    877    engine._errToThrow = ex;
    878  }
    879 
    880  try {
    881    const changes = await engine._tracker.getChangedIDs();
    882    _(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`);
    883    await GleanPings.sync.testSubmission(
    884      reason => {
    885        equal(reason, "schedule");
    886        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    887 
    888        const syncs = Glean.syncs.syncs.testGetValue();
    889        equal(syncs.length, 1);
    890 
    891        const sync = syncs[0];
    892        equal(sync.status.service, SYNC_FAILED_PARTIAL);
    893        const failureReason = sync.engines.find(
    894          e => e.name === "steam"
    895        ).failureReason;
    896        equal(failureReason.name, "unexpectederror");
    897        equal(
    898          failureReason.error,
    899          "OS error [File/Path not found] Could not open `[profileDir]/no/such/path.json': file does not exist"
    900        );
    901      },
    902      async () => {
    903        await Service.sync();
    904      }
    905    );
    906  } finally {
    907    await cleanAndGo(engine, server);
    908    await Service.engineManager.unregister(engine);
    909  }
    910 });
    911 
    912 add_task(async function test_initial_sync_engines() {
    913  enableValidationPrefs();
    914 
    915  await Service.engineManager.register(SteamEngine);
    916  let engine = Service.engineManager.get("steam");
    917  engine.enabled = true;
    918  // These are the only ones who actually have things to sync at startup.
    919  let telemetryEngineNames = ["clients", "prefs", "tabs", "bookmarks-buffered"];
    920  let server = await serverForEnginesWithKeys(
    921    { foo: "password" },
    922    ["bookmarks", "prefs", "tabs"].map(name => Service.engineManager.get(name))
    923  );
    924  await SyncTestingInfrastructure(server);
    925  try {
    926    const changes = await engine._tracker.getChangedIDs();
    927    _(
    928      `test_initial_sync_engines: Steam tracker contents: ${JSON.stringify(
    929        changes
    930      )}`
    931    );
    932    await GleanPings.sync.testSubmission(
    933      reason => {
    934        equal(reason, "schedule");
    935        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    936 
    937        const syncs = Glean.syncs.syncs.testGetValue();
    938        equal(syncs.length, 1);
    939 
    940        const sync = syncs[0];
    941        equal(sync.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
    942        equal(sync.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
    943 
    944        sync.engines
    945          .filter(e => telemetryEngineNames.includes(e.name))
    946          .forEach(e => {
    947            greaterOrEqual(e.took, 0);
    948            ok(!!e.outgoing);
    949            equal(e.outgoing.length, 1);
    950            notEqual(e.outgoing[0].sent, undefined);
    951            equal(e.outgoing[0].failed, undefined);
    952            equal(e.outgoing[0].failedReasons, undefined);
    953          });
    954      },
    955      async () => {
    956        await Service.sync();
    957      }
    958    );
    959  } finally {
    960    await cleanAndGo(engine, server);
    961    await Service.engineManager.unregister(engine);
    962  }
    963 });
    964 
    965 add_task(async function test_nserror() {
    966  enableValidationPrefs();
    967 
    968  await Service.engineManager.register(SteamEngine);
    969  let engine = Service.engineManager.get("steam");
    970  engine.enabled = true;
    971  let server = await serverForFoo(engine);
    972  await SyncTestingInfrastructure(server);
    973  engine._errToThrow = Components.Exception(
    974    "NS_ERROR_UNKNOWN_HOST",
    975    Cr.NS_ERROR_UNKNOWN_HOST
    976  );
    977  try {
    978    const changes = await engine._tracker.getChangedIDs();
    979    _(`test_nserror: Steam tracker contents: ${JSON.stringify(changes)}`);
    980    await GleanPings.sync.testSubmission(
    981      reason => {
    982        equal(reason, "schedule");
    983        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
    984 
    985        const syncs = Glean.syncs.syncs.testGetValue();
    986        equal(syncs.length, 1);
    987 
    988        const sync = syncs[0];
    989        deepEqual(sync.status, {
    990          service: SYNC_FAILED_PARTIAL,
    991          sync: LOGIN_FAILED_NETWORK_ERROR,
    992        });
    993        const eng = sync.engines.find(e => e.name === "steam");
    994        deepEqual(eng.failureReason, {
    995          name: "httperror",
    996          code: Cr.NS_ERROR_UNKNOWN_HOST,
    997        });
    998      },
    999      async () => {
   1000        await Service.sync();
   1001      }
   1002    );
   1003  } finally {
   1004    await cleanAndGo(engine, server);
   1005    await Service.engineManager.unregister(engine);
   1006  }
   1007 });
   1008 
   1009 add_task(async function test_sync_why() {
   1010  enableValidationPrefs();
   1011 
   1012  await Service.engineManager.register(SteamEngine);
   1013  let engine = Service.engineManager.get("steam");
   1014  engine.enabled = true;
   1015  let server = await serverForFoo(engine);
   1016  await SyncTestingInfrastructure(server);
   1017  let e = new Error("generic failure message");
   1018  engine._errToThrow = e;
   1019 
   1020  try {
   1021    const changes = await engine._tracker.getChangedIDs();
   1022    _(
   1023      `test_generic_engine_fail: Steam tracker contents: ${JSON.stringify(
   1024        changes
   1025      )}`
   1026    );
   1027    await GleanPings.sync.testSubmission(
   1028      reason => {
   1029        equal(reason, "schedule");
   1030        equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
   1031 
   1032        const syncs = Glean.syncs.syncs.testGetValue();
   1033        equal(syncs.length, 1);
   1034 
   1035        const sync = syncs[0];
   1036        equal(sync.why, "user");
   1037      },
   1038      async () => {
   1039        await Service.sync({ why: "user" });
   1040      }
   1041    );
   1042  } finally {
   1043    await cleanAndGo(engine, server);
   1044    await Service.engineManager.unregister(engine);
   1045  }
   1046 });
   1047 
   1048 add_task(async function test_discarding() {
   1049  enableValidationPrefs();
   1050 
   1051  let helper = track_collections_helper();
   1052  let upd = helper.with_updated_collection;
   1053  let telem = get_sync_test_telemetry();
   1054  telem.maxPayloadCount = 2;
   1055  telem.submissionInterval = Infinity;
   1056 
   1057  let server;
   1058  try {
   1059    let handlers = {
   1060      "/1.1/johndoe/info/collections": helper.handler,
   1061      "/1.1/johndoe/storage/crypto/keys": upd(
   1062        "crypto",
   1063        new ServerWBO("keys").handler()
   1064      ),
   1065      "/1.1/johndoe/storage/meta/global": upd(
   1066        "meta",
   1067        new ServerWBO("global").handler()
   1068      ),
   1069    };
   1070 
   1071    let collections = [
   1072      "clients",
   1073      "bookmarks",
   1074      "forms",
   1075      "history",
   1076      "passwords",
   1077      "prefs",
   1078      "tabs",
   1079    ];
   1080 
   1081    for (let coll of collections) {
   1082      handlers["/1.1/johndoe/storage/" + coll] = upd(
   1083        coll,
   1084        new ServerCollection({}, true).handler()
   1085      );
   1086    }
   1087 
   1088    server = httpd_setup(handlers);
   1089    await configureIdentity({ username: "johndoe" }, server);
   1090    GleanPings.sync.testBeforeNextSubmit(() => {
   1091      ok(false, "Submitted telemetry ping when we should not have.");
   1092    });
   1093 
   1094    for (let i = 0; i < 5; ++i) {
   1095      await Service.sync();
   1096    }
   1097    telem.submissionInterval = -1;
   1098 
   1099    await GleanPings.sync.testSubmission(
   1100      reason => {
   1101        equal(reason, "schedule");
   1102        equal(Glean.syncs.discarded.testGetValue(), 4);
   1103        const syncs = Glean.syncs.syncs.testGetValue();
   1104        equal(syncs.length, 2);
   1105        syncs.forEach(assert_success_sync);
   1106      },
   1107      async () => {
   1108        await Service.sync(); // Sixth time's the charm.
   1109      }
   1110    );
   1111  } finally {
   1112    telem.maxPayloadCount = 500;
   1113    telem.submissionInterval = -1;
   1114    if (server) {
   1115      await promiseStopServer(server);
   1116    }
   1117  }
   1118 });
   1119 
   1120 add_task(async function test_no_foreign_engines_in_error_ping() {
   1121  enableValidationPrefs();
   1122 
   1123  await Service.engineManager.register(BogusEngine);
   1124  let engine = Service.engineManager.get("bogus");
   1125  engine.enabled = true;
   1126  let server = await serverForFoo(engine);
   1127  engine._errToThrow = new Error("Oh no!");
   1128  await SyncTestingInfrastructure(server);
   1129  try {
   1130    await GleanPings.sync.testSubmission(
   1131      reason => {
   1132        equal(reason, "schedule");
   1133        const syncs = Glean.syncs.syncs.testGetValue();
   1134        equal(syncs.length, 1);
   1135        const sync = syncs[0];
   1136        equal(sync.status.service, SYNC_FAILED_PARTIAL);
   1137        ok(sync.engines.every(e => e.name !== "bogus"));
   1138      },
   1139      async () => {
   1140        await Service.sync();
   1141      }
   1142    );
   1143  } finally {
   1144    await cleanAndGo(engine, server);
   1145    await Service.engineManager.unregister(engine);
   1146  }
   1147 });
   1148 
   1149 add_task(async function test_no_foreign_engines_in_success_ping() {
   1150  enableValidationPrefs();
   1151 
   1152  await Service.engineManager.register(BogusEngine);
   1153  let engine = Service.engineManager.get("bogus");
   1154  engine.enabled = true;
   1155  let server = await serverForFoo(engine);
   1156 
   1157  await SyncTestingInfrastructure(server);
   1158  try {
   1159    await GleanPings.sync.testSubmission(
   1160      reason => {
   1161        equal(reason, "schedule");
   1162        const syncs = Glean.syncs.syncs.testGetValue();
   1163        equal(syncs.length, 1);
   1164        const sync = syncs[0];
   1165        ok(sync.engines.every(e => e.name !== "bogus"));
   1166      },
   1167      async () => {
   1168        await Service.sync();
   1169      }
   1170    );
   1171  } finally {
   1172    await cleanAndGo(engine, server);
   1173    await Service.engineManager.unregister(engine);
   1174  }
   1175 });
   1176 
   1177 add_task(async function test_no_node_type() {
   1178  let server = sync_httpd_setup({});
   1179  await configureIdentity(null, server);
   1180 
   1181  await GleanPings.sync.testSubmission(
   1182    reason => {
   1183      equal(reason, "schedule");
   1184      Assert.strictEqual(Glean.syncs.syncNodeType.testGetValue(), null);
   1185    },
   1186    async () => {
   1187      await Service.sync();
   1188    }
   1189  );
   1190  await promiseStopServer(server);
   1191 });
   1192 
   1193 add_task(async function test_node_type() {
   1194  Service.identity.logout();
   1195  let server = sync_httpd_setup({});
   1196  await configureIdentity({ node_type: "the-node-type" }, server);
   1197 
   1198  await GleanPings.sync.testSubmission(
   1199    reason => {
   1200      equal(reason, "schedule");
   1201      equal(Glean.syncs.syncNodeType.testGetValue(), "the-node-type");
   1202    },
   1203    async () => {
   1204      await Service.sync();
   1205    }
   1206  );
   1207  await promiseStopServer(server);
   1208 });
   1209 
   1210 add_task(async function test_node_type_change() {
   1211  Service.identity.logout();
   1212  let server = sync_httpd_setup({});
   1213  await configureIdentity({ node_type: "first-node-type" }, server);
   1214  // Default to submitting each hour - we should still submit on node change.
   1215  let telem = get_sync_test_telemetry();
   1216  telem.submissionInterval = 60 * 60 * 1000;
   1217  // reset the node type from previous test or our first sync will submit.
   1218  telem.lastSyncNodeType = null;
   1219  // do 2 syncs with the same node type.
   1220  await Service.sync();
   1221  await Service.sync();
   1222  // then another with a different node type.
   1223  Service.identity.logout();
   1224  await configureIdentity({ node_type: "second-node-type" }, server);
   1225  await GleanPings.sync.testSubmission(
   1226    () => {
   1227      equal(
   1228        Glean.syncs.syncs.testGetValue().length,
   1229        2,
   1230        "2 syncs in first ping"
   1231      );
   1232      equal(Glean.syncs.syncNodeType.testGetValue(), "first-node-type");
   1233    },
   1234    async () => {
   1235      await Service.sync();
   1236    }
   1237  );
   1238  await GleanPings.sync.testSubmission(
   1239    () => {
   1240      equal(
   1241        Glean.syncs.syncs.testGetValue().length,
   1242        1,
   1243        "1 sync in second ping"
   1244      );
   1245      equal(Glean.syncs.syncNodeType.testGetValue(), "second-node-type");
   1246    },
   1247    async () => {
   1248      telem.finish();
   1249    }
   1250  );
   1251  await promiseStopServer(server);
   1252 });
   1253 
   1254 add_task(async function test_uid_change() {
   1255  enableValidationPrefs();
   1256 
   1257  await Service.engineManager.register(BogusEngine);
   1258  let engine = Service.engineManager.get("bogus");
   1259  engine.enabled = true;
   1260  let server = await serverForFoo(engine);
   1261 
   1262  await SyncTestingInfrastructure(server);
   1263  let telem = get_sync_test_telemetry();
   1264  telem.maxPayloadCount = 500;
   1265  telem.submissionInterval = Infinity;
   1266 
   1267  // a sync with the "old" uid.
   1268  await Service.sync();
   1269 
   1270  fxAccounts.telemetry._setHashedUID("deadbeef");
   1271 
   1272  try {
   1273    await GleanPings.sync.testSubmission(
   1274      reason => {
   1275        equal(reason, "idchange");
   1276        equal(
   1277          Glean.syncs.syncs.testGetValue().length,
   1278          1,
   1279          "first sync in its own ping"
   1280        );
   1281      },
   1282      async () => {
   1283        await Service.sync();
   1284      }
   1285    );
   1286    await GleanPings.sync.testSubmission(
   1287      () => {
   1288        equal(
   1289          Glean.syncs.syncs.testGetValue().length,
   1290          1,
   1291          "second sync in its own ping"
   1292        );
   1293      },
   1294      async () => {
   1295        telem.finish();
   1296      }
   1297    );
   1298  } finally {
   1299    await cleanAndGo(engine, server);
   1300    await Service.engineManager.unregister(engine);
   1301  }
   1302 });
   1303 
   1304 add_task(async function test_deletion_request_ping() {
   1305  async function assertRecordedSyncDeviceID(expected) {
   1306    // `onAccountInitOrChange` sets the id asynchronously, so wait a tick.
   1307    await Promise.resolve();
   1308    equal(Glean.deletionRequest.syncDeviceId.testGetValue(), expected);
   1309  }
   1310  Services.fog.testResetFOG();
   1311 
   1312  const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff";
   1313  const MOCK_DEVICE_ID1 = "ffeeddccbbaa99887766554433221100";
   1314  const MOCK_DEVICE_ID2 = "aabbccddeeff99887766554433221100";
   1315 
   1316  // Calculated by hand using SHA256(DEVICE_ID + HASHED_UID)[:32]
   1317  const SANITIZED_DEVICE_ID1 = "dd7c845006df9baa1c6d756926519c8c";
   1318  const SANITIZED_DEVICE_ID2 = "0d06919a736fc029007e1786a091882c";
   1319 
   1320  let currentDeviceID = null;
   1321  sinon.stub(fxAccounts.device, "getLocalId").callsFake(() => {
   1322    return Promise.resolve(currentDeviceID);
   1323  });
   1324  let telem = get_sync_test_telemetry();
   1325  sinon.stub(telem, "isProductionSyncUser").callsFake(() => true);
   1326  fxAccounts.telemetry._setHashedUID(false);
   1327 
   1328  try {
   1329    // The scalar should start out undefined, since no user is actually logged in.
   1330    await assertRecordedSyncDeviceID(null);
   1331 
   1332    // If we start up without knowing the hashed UID, it should stay undefined.
   1333    telem.observe(null, "weave:service:ready");
   1334    await assertRecordedSyncDeviceID(null);
   1335 
   1336    // But now let's say we've discovered the hashed UID from the server.
   1337    fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID);
   1338    currentDeviceID = MOCK_DEVICE_ID1;
   1339 
   1340    // Now when we load up, we'll record the sync device id.
   1341    telem.observe(null, "weave:service:ready");
   1342    await assertRecordedSyncDeviceID(SANITIZED_DEVICE_ID1);
   1343 
   1344    // When the device-id changes we'll update it.
   1345    currentDeviceID = MOCK_DEVICE_ID2;
   1346    telem.observe(null, "fxaccounts:new_device_id");
   1347    await assertRecordedSyncDeviceID(SANITIZED_DEVICE_ID2);
   1348 
   1349    // When the user signs out we'll clear it.
   1350    telem.observe(null, "fxaccounts:onlogout");
   1351    await assertRecordedSyncDeviceID("");
   1352  } finally {
   1353    fxAccounts.telemetry._setHashedUID(false);
   1354    telem.isProductionSyncUser.restore();
   1355    fxAccounts.device.getLocalId.restore();
   1356  }
   1357 });
   1358 
   1359 // TODO: Is the topic `"weave:telemetry:migration"` presently hooked up?
   1360 add_task(async function test_migration() {
   1361  let telem = get_sync_test_telemetry();
   1362  const migrationInfo = {
   1363    entries: 42,
   1364    entries_successful: 42,
   1365    extensions: 84,
   1366    extensions_successful: 83,
   1367    openFailure: false,
   1368  };
   1369  telem._addMigrationRecord("webext-storage", migrationInfo);
   1370  await GleanPings.sync.testSubmission(
   1371    () => {
   1372      const migrations = Glean.syncs.migrations.testGetValue();
   1373      equal(migrations.length, 1);
   1374      deepEqual(migrations[0], {
   1375        migration_type: "webext-storage",
   1376        entries: 42,
   1377        entriesSuccessful: 42,
   1378        extensions: 84,
   1379        extensionsSuccessful: 83,
   1380        openFailure: false,
   1381      });
   1382    },
   1383    () => {
   1384      telem.finish();
   1385    }
   1386  );
   1387 });