tor-browser

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

test_telemetry.js (45204B)


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