tor-browser

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

test_sync.js (29323B)


      1 /**
      2 * Tests sync functionality.
      3 */
      4 
      5 /* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */
      6 /* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */
      7 /* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */
      8 /* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */
      9 
     10 "use strict";
     11 
     12 const { Service } = ChromeUtils.importESModule(
     13  "resource://services-sync/service.sys.mjs"
     14 );
     15 const { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule(
     16  "resource://services-sync/constants.sys.mjs"
     17 );
     18 
     19 const { sanitizeStorageObject, AutofillRecord, AddressesEngine } =
     20  ChromeUtils.importESModule("resource://autofill/FormAutofillSync.sys.mjs");
     21 
     22 Services.prefs.setCharPref("extensions.formautofill.loglevel", "Trace");
     23 initTestLogging("Trace");
     24 
     25 const TEST_STORE_FILE_NAME = "test-profile.json";
     26 
     27 const TEST_PROFILE_1 = {
     28  name: "Timothy John Berners-Lee",
     29  organization: "World Wide Web Consortium",
     30  "street-address": "32 Vassar Street\nMIT Room 32-G524",
     31  "address-level2": "Cambridge",
     32  "address-level1": "MA",
     33  "postal-code": "02139",
     34  country: "US",
     35  tel: "+16172535702",
     36  email: "timbl@w3.org",
     37  // A field this client doesn't "understand" from another client
     38  "unknown-1": "some unknown data from another client",
     39 };
     40 
     41 const TEST_PROFILE_2 = {
     42  "street-address": "Some Address",
     43  country: "US",
     44 };
     45 
     46 async function expectLocalProfiles(profileStorage, expected) {
     47  let profiles = await profileStorage.addresses.getAll({
     48    rawData: true,
     49    includeDeleted: true,
     50  });
     51  expected.sort((a, b) => a.guid.localeCompare(b.guid));
     52  profiles.sort((a, b) => a.guid.localeCompare(b.guid));
     53  try {
     54    deepEqual(
     55      profiles.map(p => p.guid),
     56      expected.map(p => p.guid)
     57    );
     58    for (let i = 0; i < expected.length; i++) {
     59      let thisExpected = expected[i];
     60      let thisGot = profiles[i];
     61      // always check "deleted".
     62      equal(thisExpected.deleted, thisGot.deleted);
     63      ok(objectMatches(thisGot, thisExpected));
     64    }
     65  } catch (ex) {
     66    info("Comparing expected profiles:");
     67    info(JSON.stringify(expected, undefined, 2));
     68    info("against actual profiles:");
     69    info(JSON.stringify(profiles, undefined, 2));
     70    throw ex;
     71  }
     72 }
     73 
     74 async function setup() {
     75  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
     76  // should always start with no profiles.
     77  Assert.equal(
     78    (await profileStorage.addresses.getAll({ includeDeleted: true })).length,
     79    0
     80  );
     81 
     82  Services.prefs.setCharPref(
     83    "services.sync.log.logger.engine.addresses",
     84    "Trace"
     85  );
     86  let engine = new AddressesEngine(Service);
     87  await engine.initialize();
     88  // Avoid accidental automatic sync due to our own changes
     89  Service.scheduler.syncThreshold = 10000000;
     90  let syncID = await engine.resetLocalSyncID();
     91  let server = serverForUsers(
     92    { foo: "password" },
     93    {
     94      meta: {
     95        global: { engines: { addresses: { version: engine.version, syncID } } },
     96      },
     97      addresses: {},
     98    }
     99  );
    100 
    101  Service.engineManager._engines.addresses = engine;
    102  engine.enabled = true;
    103  engine._store._storage = profileStorage.addresses;
    104 
    105  generateNewKeys(Service.collectionKeys);
    106 
    107  await SyncTestingInfrastructure(server);
    108 
    109  let collection = server.user("foo").collection("addresses");
    110 
    111  return { profileStorage, server, collection, engine };
    112 }
    113 
    114 async function cleanup(server) {
    115  let promiseStartOver = promiseOneObserver("weave:service:start-over:finish");
    116  await Service.startOver();
    117  await promiseStartOver;
    118  await promiseStopServer(server);
    119 }
    120 
    121 add_task(async function test_log_sanitization() {
    122  let sanitized = sanitizeStorageObject(TEST_PROFILE_1);
    123  // all strings have been mangled.
    124  for (let key of Object.keys(TEST_PROFILE_1)) {
    125    let val = TEST_PROFILE_1[key];
    126    if (typeof val == "string") {
    127      notEqual(sanitized[key], val);
    128    }
    129  }
    130  // And check that stringifying a sync record is sanitized.
    131  let record = new AutofillRecord("collection", "some-id");
    132  record.entry = TEST_PROFILE_1;
    133  let serialized = record.toString();
    134  // None of the string values should appear in the output.
    135  for (let key of Object.keys(TEST_PROFILE_1)) {
    136    let val = TEST_PROFILE_1[key];
    137    if (typeof val == "string") {
    138      ok(!serialized.includes(val), `"${val}" shouldn't be in: ${serialized}`);
    139    }
    140  }
    141 });
    142 
    143 add_task(async function test_outgoing() {
    144  let { profileStorage, server, collection, engine } = await setup();
    145  try {
    146    equal(engine._tracker.score, 0);
    147    let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1);
    148    // And a deleted item.
    149    let deletedGUID = profileStorage.addresses._generateGUID();
    150    await profileStorage.addresses.add({ guid: deletedGUID, deleted: true });
    151 
    152    await expectLocalProfiles(profileStorage, [
    153      {
    154        guid: existingGUID,
    155      },
    156      {
    157        guid: deletedGUID,
    158        deleted: true,
    159      },
    160    ]);
    161 
    162    await engine._tracker.asyncObserver.promiseObserversComplete();
    163    // The tracker should have a score recorded for the 2 additions we had.
    164    equal(engine._tracker.score, SCORE_INCREMENT_XLARGE * 2);
    165 
    166    await engine.setLastSync(0);
    167    await engine.sync();
    168 
    169    Assert.equal(collection.count(), 2);
    170    Assert.ok(collection.wbo(existingGUID));
    171    Assert.ok(collection.wbo(deletedGUID));
    172 
    173    await expectLocalProfiles(profileStorage, [
    174      {
    175        guid: existingGUID,
    176      },
    177      {
    178        guid: deletedGUID,
    179        deleted: true,
    180      },
    181    ]);
    182 
    183    strictEqual(
    184      getSyncChangeCounter(profileStorage.addresses, existingGUID),
    185      0
    186    );
    187    strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedGUID), 0);
    188  } finally {
    189    await cleanup(server);
    190  }
    191 });
    192 
    193 add_task(async function test_incoming_new() {
    194  let { profileStorage, server, engine } = await setup();
    195  try {
    196    let profileID = Utils.makeGUID();
    197    let deletedID = Utils.makeGUID();
    198 
    199    server.insertWBO(
    200      "foo",
    201      "addresses",
    202      new ServerWBO(
    203        profileID,
    204        encryptPayload({
    205          id: profileID,
    206          entry: Object.assign(
    207            {
    208              version: 1,
    209            },
    210            TEST_PROFILE_1
    211          ),
    212        }),
    213        getDateForSync()
    214      )
    215    );
    216    server.insertWBO(
    217      "foo",
    218      "addresses",
    219      new ServerWBO(
    220        deletedID,
    221        encryptPayload({
    222          id: deletedID,
    223          deleted: true,
    224        }),
    225        getDateForSync()
    226      )
    227    );
    228 
    229    // The tracker should start with no score.
    230    equal(engine._tracker.score, 0);
    231 
    232    await engine.setLastSync(0);
    233    await engine.sync();
    234 
    235    await expectLocalProfiles(profileStorage, [
    236      {
    237        guid: profileID,
    238      },
    239      {
    240        guid: deletedID,
    241        deleted: true,
    242      },
    243    ]);
    244 
    245    strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0);
    246    strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedID), 0);
    247 
    248    // Validate incoming records with unknown fields get stored
    249    let localRecord = await profileStorage.addresses.get(profileID);
    250    equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]);
    251 
    252    // The sync applied new records - ensure our tracker knew it came from
    253    // sync and didn't bump the score.
    254    equal(engine._tracker.score, 0);
    255  } finally {
    256    await cleanup(server);
    257  }
    258 });
    259 
    260 add_task(async function test_incoming_existing() {
    261  let { profileStorage, server, engine } = await setup();
    262  try {
    263    let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1);
    264    let guid2 = await profileStorage.addresses.add(TEST_PROFILE_2);
    265 
    266    // an initial sync so we don't think they are locally modified.
    267    await engine.setLastSync(0);
    268    await engine.sync();
    269 
    270    // now server records that modify the existing items.
    271    let modifiedEntry1 = Object.assign({}, TEST_PROFILE_1, {
    272      version: 1,
    273      name: "NewName",
    274    });
    275 
    276    let lastSync = await engine.getLastSync();
    277    server.insertWBO(
    278      "foo",
    279      "addresses",
    280      new ServerWBO(
    281        guid1,
    282        encryptPayload({
    283          id: guid1,
    284          entry: modifiedEntry1,
    285        }),
    286        lastSync + 10
    287      )
    288    );
    289    server.insertWBO(
    290      "foo",
    291      "addresses",
    292      new ServerWBO(
    293        guid2,
    294        encryptPayload({
    295          id: guid2,
    296          deleted: true,
    297        }),
    298        lastSync + 10
    299      )
    300    );
    301 
    302    await engine.sync();
    303 
    304    await expectLocalProfiles(profileStorage, [
    305      Object.assign({}, modifiedEntry1, { guid: guid1 }),
    306      { guid: guid2, deleted: true },
    307    ]);
    308  } finally {
    309    await cleanup(server);
    310  }
    311 });
    312 
    313 add_task(async function test_tombstones() {
    314  let { profileStorage, server, collection, engine } = await setup();
    315  try {
    316    let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1);
    317 
    318    await engine.setLastSync(0);
    319    await engine.sync();
    320 
    321    Assert.equal(collection.count(), 1);
    322    let payload = collection.payloads()[0];
    323    equal(payload.id, existingGUID);
    324    equal(payload.deleted, undefined);
    325 
    326    profileStorage.addresses.remove(existingGUID);
    327    await engine.sync();
    328 
    329    // should still exist, but now be a tombstone.
    330    Assert.equal(collection.count(), 1);
    331    payload = collection.payloads()[0];
    332    equal(payload.id, existingGUID);
    333    equal(payload.deleted, true);
    334  } finally {
    335    await cleanup(server);
    336  }
    337 });
    338 
    339 add_task(async function test_applyIncoming_both_deleted() {
    340  let { profileStorage, server, engine } = await setup();
    341  try {
    342    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    343 
    344    await engine.setLastSync(0);
    345    await engine.sync();
    346 
    347    // Delete synced record locally.
    348    profileStorage.addresses.remove(guid);
    349 
    350    // Delete same record remotely.
    351    let lastSync = await engine.getLastSync();
    352    let collection = server.user("foo").collection("addresses");
    353    collection.insert(
    354      guid,
    355      encryptPayload({
    356        id: guid,
    357        deleted: true,
    358      }),
    359      lastSync + 10
    360    );
    361 
    362    await engine.sync();
    363 
    364    ok(
    365      !(await profileStorage.addresses.get(guid)),
    366      "Should not return record for locally deleted item"
    367    );
    368 
    369    let localRecords = await profileStorage.addresses.getAll({
    370      includeDeleted: true,
    371    });
    372    equal(localRecords.length, 1, "Only tombstone should exist locally");
    373 
    374    equal(collection.count(), 1, "Only tombstone should exist on server");
    375  } finally {
    376    await cleanup(server);
    377  }
    378 });
    379 
    380 add_task(async function test_applyIncoming_nonexistent_tombstone() {
    381  let { profileStorage, server, engine } = await setup();
    382  try {
    383    let guid = profileStorage.addresses._generateGUID();
    384    let collection = server.user("foo").collection("addresses");
    385    collection.insert(
    386      guid,
    387      encryptPayload({
    388        id: guid,
    389        deleted: true,
    390      }),
    391      getDateForSync()
    392    );
    393 
    394    await engine.setLastSync(0);
    395    await engine.sync();
    396 
    397    ok(
    398      !(await profileStorage.addresses.get(guid)),
    399      "Should not return record for unknown deleted item"
    400    );
    401    let localTombstone = (
    402      await profileStorage.addresses.getAll({
    403        includeDeleted: true,
    404      })
    405    ).find(record => record.guid == guid);
    406    ok(localTombstone, "Should store tombstone for unknown item");
    407  } finally {
    408    await cleanup(server);
    409  }
    410 });
    411 
    412 add_task(async function test_applyIncoming_incoming_deleted() {
    413  let { profileStorage, server, engine } = await setup();
    414  try {
    415    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    416 
    417    await engine.setLastSync(0);
    418    await engine.sync();
    419 
    420    // Delete the record remotely.
    421    let lastSync = await engine.getLastSync();
    422    let collection = server.user("foo").collection("addresses");
    423    collection.insert(
    424      guid,
    425      encryptPayload({
    426        id: guid,
    427        deleted: true,
    428      }),
    429      lastSync + 10
    430    );
    431 
    432    await engine.sync();
    433 
    434    ok(
    435      !(await profileStorage.addresses.get(guid)),
    436      "Should delete unmodified item locally"
    437    );
    438 
    439    let localTombstone = (
    440      await profileStorage.addresses.getAll({
    441        includeDeleted: true,
    442      })
    443    ).find(record => record.guid == guid);
    444    ok(localTombstone, "Should keep local tombstone for remotely deleted item");
    445    strictEqual(
    446      getSyncChangeCounter(profileStorage.addresses, guid),
    447      0,
    448      "Local tombstone should be marked as syncing"
    449    );
    450  } finally {
    451    await cleanup(server);
    452  }
    453 });
    454 
    455 add_task(async function test_applyIncoming_incoming_restored() {
    456  let { profileStorage, server, engine } = await setup();
    457  try {
    458    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    459 
    460    // Upload the record to the server.
    461    await engine.setLastSync(0);
    462    await engine.sync();
    463 
    464    // Removing a synced record should write a tombstone.
    465    profileStorage.addresses.remove(guid);
    466 
    467    // Modify the deleted record remotely.
    468    let collection = server.user("foo").collection("addresses");
    469    let serverPayload = JSON.parse(
    470      JSON.parse(collection.payload(guid)).ciphertext
    471    );
    472    serverPayload.entry["street-address"] = "I moved!";
    473    let lastSync = await engine.getLastSync();
    474    collection.insert(guid, encryptPayload(serverPayload), lastSync + 10);
    475 
    476    // Sync again.
    477    await engine.sync();
    478 
    479    // We should replace our tombstone with the server's version.
    480    let localRecord = await profileStorage.addresses.get(guid);
    481    ok(
    482      objectMatches(localRecord, {
    483        name: "Timothy John Berners-Lee",
    484        "street-address": "I moved!",
    485      })
    486    );
    487 
    488    let maybeNewServerPayload = JSON.parse(
    489      JSON.parse(collection.payload(guid)).ciphertext
    490    );
    491    deepEqual(
    492      maybeNewServerPayload,
    493      serverPayload,
    494      "Should not change record on server"
    495    );
    496  } finally {
    497    await cleanup(server);
    498  }
    499 });
    500 
    501 add_task(async function test_applyIncoming_outgoing_restored() {
    502  let { profileStorage, server, engine } = await setup();
    503  try {
    504    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    505 
    506    // Upload the record to the server.
    507    await engine.setLastSync(0);
    508    await engine.sync();
    509 
    510    // Modify the local record.
    511    let localCopy = Object.assign({}, TEST_PROFILE_1);
    512    localCopy["street-address"] = "I moved!";
    513    await profileStorage.addresses.update(guid, localCopy);
    514 
    515    // Replace the record with a tombstone on the server.
    516    let lastSync = await engine.getLastSync();
    517    let collection = server.user("foo").collection("addresses");
    518    collection.insert(
    519      guid,
    520      encryptPayload({
    521        id: guid,
    522        deleted: true,
    523      }),
    524      lastSync + 10
    525    );
    526 
    527    // Sync again.
    528    await engine.sync();
    529 
    530    // We should resurrect the record on the server.
    531    let serverPayload = JSON.parse(
    532      JSON.parse(collection.payload(guid)).ciphertext
    533    );
    534    ok(!serverPayload.deleted, "Should resurrect record on server");
    535    ok(
    536      objectMatches(serverPayload.entry, {
    537        name: "Timothy John Berners-Lee",
    538        "street-address": "I moved!",
    539        // resurrection also beings back any unknown fields we had
    540        "unknown-1": "some unknown data from another client",
    541      })
    542    );
    543 
    544    let localRecord = await profileStorage.addresses.get(guid);
    545    ok(localRecord, "Modified record should not be deleted locally");
    546  } finally {
    547    await cleanup(server);
    548  }
    549 });
    550 
    551 // Unlike most sync engines, we want "both modified" to inspect the records,
    552 // and if materially different, create a duplicate.
    553 add_task(async function test_reconcile_both_modified_identical() {
    554  let { profileStorage, server, engine } = await setup();
    555  try {
    556    // create a record locally.
    557    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    558 
    559    // and an identical record on the server.
    560    server.insertWBO(
    561      "foo",
    562      "addresses",
    563      new ServerWBO(
    564        guid,
    565        encryptPayload({
    566          id: guid,
    567          entry: TEST_PROFILE_1,
    568        }),
    569        getDateForSync()
    570      )
    571    );
    572 
    573    await engine.setLastSync(0);
    574    await engine.sync();
    575 
    576    await expectLocalProfiles(profileStorage, [{ guid }]);
    577  } finally {
    578    await cleanup(server);
    579  }
    580 });
    581 
    582 add_task(async function test_incoming_dupes() {
    583  let { profileStorage, server, engine } = await setup();
    584  try {
    585    // Create a profile locally, then sync to upload the new profile to the
    586    // server.
    587    let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1);
    588 
    589    await engine.setLastSync(0);
    590    await engine.sync();
    591 
    592    // Create another profile locally, but don't sync it yet.
    593    await profileStorage.addresses.add(TEST_PROFILE_2);
    594 
    595    // Now create two records on the server with the same contents as our local
    596    // profiles, but different GUIDs.
    597    let lastSync = await engine.getLastSync();
    598    let guid1_dupe = Utils.makeGUID();
    599    server.insertWBO(
    600      "foo",
    601      "addresses",
    602      new ServerWBO(
    603        guid1_dupe,
    604        encryptPayload({
    605          id: guid1_dupe,
    606          entry: Object.assign(
    607            {
    608              version: 1,
    609            },
    610            TEST_PROFILE_1
    611          ),
    612        }),
    613        lastSync + 10
    614      )
    615    );
    616    let guid2_dupe = Utils.makeGUID();
    617    server.insertWBO(
    618      "foo",
    619      "addresses",
    620      new ServerWBO(
    621        guid2_dupe,
    622        encryptPayload({
    623          id: guid2_dupe,
    624          entry: Object.assign(
    625            {
    626              version: 1,
    627            },
    628            TEST_PROFILE_2
    629          ),
    630        }),
    631        lastSync + 10
    632      )
    633    );
    634 
    635    // Sync again. We should download `guid1_dupe` and `guid2_dupe`, then
    636    // reconcile changes.
    637    await engine.sync();
    638 
    639    await expectLocalProfiles(profileStorage, [
    640      // We uploaded `guid1` during the first sync. Even though its contents
    641      // are the same as `guid1_dupe`, we keep both.
    642      Object.assign({}, TEST_PROFILE_1, { guid: guid1 }),
    643      Object.assign({}, TEST_PROFILE_1, { guid: guid1_dupe }),
    644      // However, we didn't upload `guid2` before downloading `guid2_dupe`, so
    645      // we *should* dedupe `guid2` to `guid2_dupe`.
    646      Object.assign({}, TEST_PROFILE_2, { guid: guid2_dupe }),
    647    ]);
    648  } finally {
    649    await cleanup(server);
    650  }
    651 });
    652 
    653 add_task(async function test_dedupe_identical_unsynced_singlelineaddress() {
    654  let { profileStorage, server, engine } = await setup();
    655  try {
    656    let profile = structuredClone(TEST_PROFILE_1);
    657    // Change the street address so that it will parse correctly.
    658    profile["street-address"] = "36 Main Street";
    659 
    660    // create a record locally.
    661    let localGuid = await profileStorage.addresses.add(profile);
    662 
    663    // and an identical record on the server but different GUID.
    664    let remoteGuid = Utils.makeGUID();
    665    notEqual(localGuid, remoteGuid);
    666    server.insertWBO(
    667      "foo",
    668      "addresses",
    669      new ServerWBO(
    670        remoteGuid,
    671        encryptPayload({
    672          id: remoteGuid,
    673          entry: Object.assign(
    674            {
    675              version: 1,
    676            },
    677            profile
    678          ),
    679        }),
    680        getDateForSync()
    681      )
    682    );
    683 
    684    await engine.setLastSync(0);
    685    await engine.sync();
    686 
    687    // Should have 1 item locally with GUID changed to the remote one.
    688    // There's no tombstone as the original was unsynced.
    689    await expectLocalProfiles(profileStorage, [
    690      {
    691        guid: remoteGuid,
    692      },
    693    ]);
    694  } finally {
    695    await cleanup(server);
    696  }
    697 });
    698 
    699 add_task(async function test_dedupe_identical_unsynced() {
    700  let { profileStorage, server, engine } = await setup();
    701  try {
    702    // create a record locally.
    703    let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1);
    704 
    705    // and an identical record on the server but different GUID.
    706    let remoteGuid = Utils.makeGUID();
    707    notEqual(localGuid, remoteGuid);
    708    server.insertWBO(
    709      "foo",
    710      "addresses",
    711      new ServerWBO(
    712        remoteGuid,
    713        encryptPayload({
    714          id: remoteGuid,
    715          entry: Object.assign(
    716            {
    717              version: 1,
    718            },
    719            TEST_PROFILE_1
    720          ),
    721        }),
    722        getDateForSync()
    723      )
    724    );
    725 
    726    await engine.setLastSync(0);
    727    await engine.sync();
    728 
    729    // Should have 1 item locally with GUID changed to the remote one.
    730    // There's no tombstone as the original was unsynced.
    731    await expectLocalProfiles(profileStorage, [
    732      {
    733        guid: remoteGuid,
    734      },
    735    ]);
    736  } finally {
    737    await cleanup(server);
    738  }
    739 });
    740 
    741 add_task(async function test_dedupe_identical_synced() {
    742  let { profileStorage, server, engine } = await setup();
    743  try {
    744    // create a record locally.
    745    let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1);
    746 
    747    // sync it - it will no longer be a candidate for de-duping.
    748    await engine.setLastSync(0);
    749    await engine.sync();
    750 
    751    // and an identical record on the server but different GUID.
    752    let lastSync = await engine.getLastSync();
    753    let remoteGuid = Utils.makeGUID();
    754    server.insertWBO(
    755      "foo",
    756      "addresses",
    757      new ServerWBO(
    758        remoteGuid,
    759        encryptPayload({
    760          id: remoteGuid,
    761          entry: Object.assign(
    762            {
    763              version: 1,
    764            },
    765            TEST_PROFILE_1
    766          ),
    767        }),
    768        lastSync + 10
    769      )
    770    );
    771 
    772    await engine.sync();
    773 
    774    // Should have 2 items locally, since the first was synced.
    775    await expectLocalProfiles(profileStorage, [
    776      { guid: localGuid },
    777      { guid: remoteGuid },
    778    ]);
    779  } finally {
    780    await cleanup(server);
    781  }
    782 });
    783 
    784 add_task(async function test_dedupe_multiple_candidates() {
    785  let { profileStorage, server, engine } = await setup();
    786  try {
    787    // It's possible to have duplicate local profiles, with the same fields but
    788    // different GUIDs. After a node reassignment, or after disconnecting and
    789    // reconnecting to Sync, we might dedupe a local record A to a remote record
    790    // B, if we see B before we download and apply A. Since A and B are dupes,
    791    // that's OK. We'll write a tombstone for A when we dedupe A to B, and
    792    // overwrite that tombstone when we see A.
    793 
    794    let localRecord = {
    795      name: "Mark Hammond",
    796      organization: "Mozilla",
    797      country: "AU",
    798      tel: "+12345678910",
    799    };
    800    let serverRecord = Object.assign(
    801      {
    802        version: 1,
    803      },
    804      localRecord
    805    );
    806 
    807    // We don't pass `sourceSync` so that the records are marked as NEW.
    808    let aGuid = await profileStorage.addresses.add(localRecord);
    809    let bGuid = await profileStorage.addresses.add(localRecord);
    810 
    811    // Insert B before A.
    812    server.insertWBO(
    813      "foo",
    814      "addresses",
    815      new ServerWBO(
    816        bGuid,
    817        encryptPayload({
    818          id: bGuid,
    819          entry: serverRecord,
    820        }),
    821        getDateForSync()
    822      )
    823    );
    824    server.insertWBO(
    825      "foo",
    826      "addresses",
    827      new ServerWBO(
    828        aGuid,
    829        encryptPayload({
    830          id: aGuid,
    831          entry: serverRecord,
    832        }),
    833        getDateForSync()
    834      )
    835    );
    836 
    837    await engine.setLastSync(0);
    838    await engine.sync();
    839 
    840    await expectLocalProfiles(profileStorage, [
    841      {
    842        guid: aGuid,
    843        name: "Mark Hammond",
    844        organization: "Mozilla",
    845        country: "AU",
    846        tel: "+12345678910",
    847      },
    848      {
    849        guid: bGuid,
    850        name: "Mark Hammond",
    851        organization: "Mozilla",
    852        country: "AU",
    853        tel: "+12345678910",
    854      },
    855    ]);
    856    // Make sure these are both syncing.
    857    strictEqual(
    858      getSyncChangeCounter(profileStorage.addresses, aGuid),
    859      0,
    860      "A should be marked as syncing"
    861    );
    862    strictEqual(
    863      getSyncChangeCounter(profileStorage.addresses, bGuid),
    864      0,
    865      "B should be marked as syncing"
    866    );
    867  } finally {
    868    await cleanup(server);
    869  }
    870 });
    871 
    872 // Unlike most sync engines, we want "both modified" to inspect the records,
    873 // and if materially different, create a duplicate.
    874 add_task(async function test_reconcile_both_modified_conflict() {
    875  let { profileStorage, server, engine } = await setup();
    876  try {
    877    // create a record locally.
    878    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    879 
    880    // Upload the record to the server.
    881    await engine.setLastSync(0);
    882    await engine.sync();
    883 
    884    strictEqual(
    885      getSyncChangeCounter(profileStorage.addresses, guid),
    886      0,
    887      "Original record should be marked as syncing"
    888    );
    889 
    890    // Change the same field locally and on the server.
    891    let localCopy = Object.assign({}, TEST_PROFILE_1);
    892    localCopy["street-address"] = "I moved!";
    893    await profileStorage.addresses.update(guid, localCopy);
    894 
    895    let lastSync = await engine.getLastSync();
    896    let collection = server.user("foo").collection("addresses");
    897    let serverPayload = JSON.parse(
    898      JSON.parse(collection.payload(guid)).ciphertext
    899    );
    900    serverPayload.entry["street-address"] = "I moved, too!";
    901    collection.insert(guid, encryptPayload(serverPayload), lastSync + 10);
    902 
    903    // Sync again.
    904    await engine.sync();
    905 
    906    // Since we wait to pull changes until we're ready to upload, both records
    907    // should now exist on the server; we don't need a follow-up sync.
    908    let serverPayloads = collection.payloads();
    909    equal(serverPayloads.length, 2, "Both records should exist on server");
    910 
    911    let forkedPayload = serverPayloads.find(payload => payload.id != guid);
    912    ok(forkedPayload, "Forked record should exist on server");
    913 
    914    await expectLocalProfiles(profileStorage, [
    915      {
    916        guid,
    917        name: "Timothy John Berners-Lee",
    918        "street-address": "I moved, too!",
    919      },
    920      {
    921        guid: forkedPayload.id,
    922        name: "Timothy John Berners-Lee",
    923        "street-address": "I moved!",
    924      },
    925    ]);
    926 
    927    let changeCounter = getSyncChangeCounter(
    928      profileStorage.addresses,
    929      forkedPayload.id
    930    );
    931    strictEqual(changeCounter, 0, "Forked record should be marked as syncing");
    932  } finally {
    933    await cleanup(server);
    934  }
    935 });
    936 
    937 add_task(async function test_wipe() {
    938  let { profileStorage, server, engine } = await setup();
    939  try {
    940    let guid = await profileStorage.addresses.add(TEST_PROFILE_1);
    941 
    942    await expectLocalProfiles(profileStorage, [{ guid }]);
    943 
    944    let promiseObserved = promiseOneObserver("formautofill-storage-changed");
    945 
    946    await engine._wipeClient();
    947 
    948    let { subject, data } = await promiseObserved;
    949    Assert.equal(
    950      subject.wrappedJSObject.sourceSync,
    951      true,
    952      "it should be noted this came from sync"
    953    );
    954    Assert.equal(
    955      subject.wrappedJSObject.collectionName,
    956      "addresses",
    957      "got the correct collection"
    958    );
    959    Assert.equal(data, "removeAll", "a removeAll should be noted");
    960 
    961    await expectLocalProfiles(profileStorage, []);
    962  } finally {
    963    await cleanup(server);
    964  }
    965 });
    966 
    967 // Other clients might have data that we aren't able to process/understand yet
    968 // We should keep that data and ensure when we sync we don't lose that data
    969 add_task(async function test_full_roundtrip_unknown_data() {
    970  let { profileStorage, server, engine } = await setup();
    971  try {
    972    let profileID = Utils.makeGUID();
    973 
    974    info("Incoming records with unknown fields are properly stored");
    975    // Insert a record onto the server
    976    server.insertWBO(
    977      "foo",
    978      "addresses",
    979      new ServerWBO(
    980        profileID,
    981        encryptPayload({
    982          id: profileID,
    983          entry: Object.assign(
    984            {
    985              version: 1,
    986            },
    987            TEST_PROFILE_1
    988          ),
    989        }),
    990        getDateForSync()
    991      )
    992    );
    993 
    994    // The tracker should start with no score.
    995    equal(engine._tracker.score, 0);
    996 
    997    await engine.setLastSync(0);
    998    await engine.sync();
    999 
   1000    await expectLocalProfiles(profileStorage, [
   1001      {
   1002        guid: profileID,
   1003      },
   1004    ]);
   1005 
   1006    strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0);
   1007 
   1008    // The sync applied new records - ensure our tracker knew it came from
   1009    // sync and didn't bump the score.
   1010    equal(engine._tracker.score, 0);
   1011 
   1012    // Validate incoming records with unknown fields are correctly stored
   1013    let localRecord = await profileStorage.addresses.get(profileID);
   1014    equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]);
   1015 
   1016    let onChanged = TestUtils.topicObserved(
   1017      "formautofill-storage-changed",
   1018      (subject, data) => data == "update"
   1019    );
   1020 
   1021    // Validate we can update the records locally and not drop any unknown fields
   1022    info("Unknown fields are sent back up to the server");
   1023 
   1024    // Modify the local copy
   1025    let localCopy = Object.assign({}, TEST_PROFILE_1);
   1026    localCopy["street-address"] = "I moved!";
   1027    await profileStorage.addresses.update(profileID, localCopy);
   1028    await onChanged;
   1029    await profileStorage._saveImmediately();
   1030 
   1031    let updatedCopy = await profileStorage.addresses.get(profileID);
   1032    equal(updatedCopy["street-address"], "I moved!");
   1033 
   1034    // Sync our changes to the server
   1035    await engine.setLastSync(0);
   1036    await engine.sync();
   1037 
   1038    let collection = server.user("foo").collection("addresses");
   1039 
   1040    Assert.ok(collection.wbo(profileID));
   1041    let serverPayload = JSON.parse(
   1042      JSON.parse(collection.payload(profileID)).ciphertext
   1043    );
   1044 
   1045    // The server has the updated field as well as any unknown fields
   1046    equal(
   1047      serverPayload.entry["unknown-1"],
   1048      "some unknown data from another client"
   1049    );
   1050    equal(serverPayload.entry["street-address"], "I moved!");
   1051  } finally {
   1052    await cleanup(server);
   1053  }
   1054 });