tor-browser

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

test_password_engine.js (38728B)


      1 const { FXA_PWDMGR_HOST, FXA_PWDMGR_REALM } = ChromeUtils.importESModule(
      2  "resource://gre/modules/FxAccountsCommon.sys.mjs"
      3 );
      4 const { LoginRec } = ChromeUtils.importESModule(
      5  "resource://services-sync/engines/passwords.sys.mjs"
      6 );
      7 const { Service } = ChromeUtils.importESModule(
      8  "resource://services-sync/service.sys.mjs"
      9 );
     10 
     11 const LoginInfo = Components.Constructor(
     12  "@mozilla.org/login-manager/loginInfo;1",
     13  Ci.nsILoginInfo,
     14  "init"
     15 );
     16 
     17 const { LoginCSVImport } = ChromeUtils.importESModule(
     18  "resource://gre/modules/LoginCSVImport.sys.mjs"
     19 );
     20 
     21 const { FileTestUtils } = ChromeUtils.importESModule(
     22  "resource://testing-common/FileTestUtils.sys.mjs"
     23 );
     24 
     25 const PropertyBag = Components.Constructor(
     26  "@mozilla.org/hash-property-bag;1",
     27  Ci.nsIWritablePropertyBag
     28 );
     29 
     30 async function cleanup(engine, server) {
     31  await engine._tracker.stop();
     32  await engine.wipeClient();
     33  engine.lastModified = null;
     34  for (const pref of Svc.PrefBranch.getChildList("")) {
     35    Svc.PrefBranch.clearUserPref(pref);
     36  }
     37  Service.recordManager.clearCache();
     38  if (server) {
     39    await promiseStopServer(server);
     40  }
     41 }
     42 
     43 add_task(async function setup() {
     44  // Disable addon sync because AddonManager won't be initialized here.
     45  await Service.engineManager.unregister("addons");
     46  await Service.engineManager.unregister("extension-storage");
     47 });
     48 
     49 add_task(async function test_ignored_fields() {
     50  _("Only changes to syncable fields should be tracked");
     51 
     52  let engine = Service.engineManager.get("passwords");
     53 
     54  let server = await serverForFoo(engine);
     55  await SyncTestingInfrastructure(server);
     56 
     57  enableValidationPrefs();
     58 
     59  let loginInfo = new LoginInfo(
     60    "https://example.com",
     61    "",
     62    null,
     63    "username",
     64    "password",
     65    "",
     66    ""
     67  );
     68 
     69  // Setting syncCounter to -1 so that it will be incremented to 0 when added.
     70  loginInfo.syncCounter = -1;
     71  let login = await Services.logins.addLoginAsync(loginInfo);
     72  login.QueryInterface(Ci.nsILoginMetaInfo); // For `guid`.
     73 
     74  engine._tracker.start();
     75 
     76  try {
     77    let nonSyncableProps = new PropertyBag();
     78    nonSyncableProps.setProperty("timeLastUsed", Date.now());
     79    nonSyncableProps.setProperty("timesUsed", 3);
     80    await Services.logins.modifyLoginAsync(login, nonSyncableProps);
     81 
     82    let noChanges = await engine.pullNewChanges();
     83    deepEqual(noChanges, {}, "Should not track non-syncable fields");
     84 
     85    let syncableProps = new PropertyBag();
     86    syncableProps.setProperty("username", "newuser");
     87    await Services.logins.modifyLoginAsync(login, syncableProps);
     88 
     89    let changes = await engine.pullNewChanges();
     90    deepEqual(
     91      Object.keys(changes),
     92      [login.guid],
     93      "Should track syncable fields"
     94    );
     95  } finally {
     96    await cleanup(engine, server);
     97  }
     98 });
     99 
    100 add_task(async function test_ignored_sync_credentials() {
    101  _("Sync credentials in login manager should be ignored");
    102 
    103  let engine = Service.engineManager.get("passwords");
    104 
    105  let server = await serverForFoo(engine);
    106  await SyncTestingInfrastructure(server);
    107 
    108  enableValidationPrefs();
    109 
    110  engine._tracker.start();
    111 
    112  try {
    113    let login = await Services.logins.addLoginAsync(
    114      new LoginInfo(
    115        FXA_PWDMGR_HOST,
    116        null,
    117        FXA_PWDMGR_REALM,
    118        "fxa-uid",
    119        "creds",
    120        "",
    121        ""
    122      )
    123    );
    124 
    125    let noChanges = await engine.pullNewChanges();
    126    deepEqual(noChanges, {}, "Should not track new FxA credentials");
    127 
    128    let props = new PropertyBag();
    129    props.setProperty("password", "newcreds");
    130    await Services.logins.modifyLoginAsync(login, props);
    131 
    132    noChanges = await engine.pullNewChanges();
    133    deepEqual(noChanges, {}, "Should not track changes to FxA credentials");
    134 
    135    let foundLogins = await Services.logins.searchLoginsAsync({
    136      origin: FXA_PWDMGR_HOST,
    137    });
    138    equal(foundLogins.length, 1);
    139    equal(foundLogins[0].syncCounter, 0);
    140    equal(foundLogins[0].everSynced, false);
    141  } finally {
    142    await cleanup(engine, server);
    143  }
    144 });
    145 
    146 add_task(async function test_password_engine() {
    147  _("Basic password sync test");
    148 
    149  let engine = Service.engineManager.get("passwords");
    150 
    151  let server = await serverForFoo(engine);
    152  await SyncTestingInfrastructure(server);
    153  let collection = server.user("foo").collection("passwords");
    154 
    155  enableValidationPrefs();
    156 
    157  _("Add new login to upload during first sync");
    158  let newLogin;
    159  {
    160    let login = new LoginInfo(
    161      "https://example.com",
    162      "",
    163      null,
    164      "username",
    165      "password",
    166      "",
    167      ""
    168    );
    169    await Services.logins.addLoginAsync(login);
    170 
    171    let logins = await Services.logins.searchLoginsAsync({
    172      origin: "https://example.com",
    173    });
    174    equal(logins.length, 1, "Should find new login in login manager");
    175    newLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
    176 
    177    // Insert a server record that's older, so that we prefer the local one.
    178    let rec = new LoginRec("passwords", newLogin.guid);
    179    rec.formSubmitURL = newLogin.formActionOrigin;
    180    rec.httpRealm = newLogin.httpRealm;
    181    rec.hostname = newLogin.origin;
    182    rec.username = newLogin.username;
    183    rec.password = "sekrit";
    184    let remotePasswordChangeTime = Date.now() - 1 * 60 * 60 * 24 * 1000;
    185    rec.timeCreated = remotePasswordChangeTime;
    186    rec.timePasswordChanged = remotePasswordChangeTime;
    187    collection.insert(
    188      newLogin.guid,
    189      encryptPayload(rec.cleartext),
    190      remotePasswordChangeTime / 1000
    191    );
    192  }
    193 
    194  _("Add login with older password change time to replace during first sync");
    195  let oldLogin;
    196  {
    197    let login = new LoginInfo(
    198      "https://mozilla.com",
    199      "",
    200      null,
    201      "us3r",
    202      "0ldpa55",
    203      "",
    204      ""
    205    );
    206    await Services.logins.addLoginAsync(login);
    207 
    208    let props = new PropertyBag();
    209    let localPasswordChangeTime = Date.now() - 1 * 60 * 60 * 24 * 1000;
    210    props.setProperty("timePasswordChanged", localPasswordChangeTime);
    211    await Services.logins.modifyLoginAsync(login, props);
    212 
    213    let logins = await Services.logins.searchLoginsAsync({
    214      origin: "https://mozilla.com",
    215    });
    216    equal(logins.length, 1, "Should find old login in login manager");
    217    oldLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
    218    equal(oldLogin.timePasswordChanged, localPasswordChangeTime);
    219 
    220    let rec = new LoginRec("passwords", oldLogin.guid);
    221    rec.hostname = oldLogin.origin;
    222    rec.formSubmitURL = oldLogin.formActionOrigin;
    223    rec.httpRealm = oldLogin.httpRealm;
    224    rec.username = oldLogin.username;
    225    // Change the password and bump the password change time to ensure we prefer
    226    // the remote one during reconciliation.
    227    rec.password = "n3wpa55";
    228    rec.usernameField = oldLogin.usernameField;
    229    rec.passwordField = oldLogin.usernameField;
    230    rec.timeCreated = oldLogin.timeCreated;
    231    rec.timePasswordChanged = Date.now();
    232    collection.insert(oldLogin.guid, encryptPayload(rec.cleartext));
    233  }
    234 
    235  await engine._tracker.stop();
    236 
    237  try {
    238    await sync_engine_and_validate_telem(engine, false);
    239 
    240    let newRec = collection.cleartext(newLogin.guid);
    241    equal(
    242      newRec.password,
    243      "password",
    244      "Should update remote password for newer login"
    245    );
    246 
    247    let logins = await Services.logins.searchLoginsAsync({
    248      origin: "https://mozilla.com",
    249    });
    250    equal(
    251      logins[0].password,
    252      "n3wpa55",
    253      "Should update local password for older login"
    254    );
    255  } finally {
    256    await cleanup(engine, server);
    257  }
    258 });
    259 
    260 add_task(async function test_sync_outgoing() {
    261  _("Test syncing outgoing records");
    262 
    263  let engine = Service.engineManager.get("passwords");
    264 
    265  let server = await serverForFoo(engine);
    266  await SyncTestingInfrastructure(server);
    267 
    268  let collection = server.user("foo").collection("passwords");
    269 
    270  let loginInfo = new LoginInfo(
    271    "http://mozilla.com",
    272    "http://mozilla.com",
    273    null,
    274    "theuser",
    275    "thepassword",
    276    "username",
    277    "password"
    278  );
    279  let login = await Services.logins.addLoginAsync(loginInfo);
    280 
    281  engine._tracker.start();
    282 
    283  try {
    284    let foundLogins = await Services.logins.searchLoginsAsync({
    285      origin: "http://mozilla.com",
    286    });
    287    equal(foundLogins.length, 1);
    288    equal(foundLogins[0].syncCounter, 1);
    289    equal(foundLogins[0].everSynced, false);
    290    equal(collection.count(), 0);
    291 
    292    let guid = foundLogins[0].QueryInterface(Ci.nsILoginMetaInfo).guid;
    293 
    294    let changes = await engine.getChangedIDs();
    295    let change = changes[guid];
    296    equal(Object.keys(changes).length, 1);
    297    equal(change.counter, 1);
    298    ok(!change.deleted);
    299 
    300    // This test modifies the password and then performs a sync and
    301    // then ensures that the synced record is correct. This is done twice
    302    // to ensure that syncing occurs correctly when the server record does not
    303    // yet exist and when it does already exist.
    304    for (let i = 1; i <= 2; i++) {
    305      _("Modify the password iteration " + i);
    306      foundLogins[0].password = "newpassword" + i;
    307      await Services.logins.modifyLoginAsync(login, foundLogins[0]);
    308      foundLogins = await Services.logins.searchLoginsAsync({
    309        origin: "http://mozilla.com",
    310      });
    311      equal(foundLogins.length, 1);
    312      // On the first pass, the counter should be 2, one for the add and one for the modify.
    313      // No sync has occurred yet so everSynced should be false.
    314      // On the second pass, the counter will only be 1 for the modify. The everSynced
    315      // property should be true as the sync happened on the last iteration.
    316      equal(foundLogins[0].syncCounter, i == 2 ? 1 : 2);
    317      equal(foundLogins[0].everSynced, i == 2);
    318 
    319      changes = await engine.getChangedIDs();
    320      change = changes[guid];
    321      equal(Object.keys(changes).length, 1);
    322      equal(change.counter, i == 2 ? 1 : 2);
    323      ok(!change.deleted);
    324 
    325      _("Perform sync after modifying the password");
    326      await sync_engine_and_validate_telem(engine, false);
    327 
    328      equal(Object.keys(await engine.getChangedIDs()), 0);
    329 
    330      // The remote login should have the updated password.
    331      let newRec = collection.cleartext(guid);
    332      equal(
    333        newRec.password,
    334        "newpassword" + i,
    335        "Should update remote password for login"
    336      );
    337 
    338      foundLogins = await Services.logins.searchLoginsAsync({
    339        origin: "http://mozilla.com",
    340      });
    341      equal(foundLogins.length, 1);
    342      equal(foundLogins[0].syncCounter, 0);
    343      equal(foundLogins[0].everSynced, true);
    344 
    345      login.password = "newpassword" + i;
    346    }
    347 
    348    // Next, modify the username and sync.
    349    _("Modify the username");
    350    foundLogins[0].username = "newuser";
    351    await Services.logins.modifyLoginAsync(login, foundLogins[0]);
    352    foundLogins = await Services.logins.searchLoginsAsync({
    353      origin: "http://mozilla.com",
    354    });
    355    equal(foundLogins.length, 1);
    356    equal(foundLogins[0].syncCounter, 1);
    357    equal(foundLogins[0].everSynced, true);
    358 
    359    _("Perform sync after modifying the username");
    360    await sync_engine_and_validate_telem(engine, false);
    361 
    362    // The remote login should have the updated password.
    363    let newRec = collection.cleartext(guid);
    364    equal(
    365      newRec.username,
    366      "newuser",
    367      "Should update remote username for login"
    368    );
    369 
    370    foundLogins = await Services.logins.searchLoginsAsync({
    371      origin: "http://mozilla.com",
    372    });
    373    equal(foundLogins.length, 1);
    374    equal(foundLogins[0].syncCounter, 0);
    375    equal(foundLogins[0].everSynced, true);
    376 
    377    // Finally, remove the login. The server record should be marked as deleted.
    378    _("Remove the login");
    379    equal(collection.count(), 1);
    380    equal(Services.logins.countLogins("", "", ""), 2);
    381    equal((await Services.logins.getAllLogins()).length, 2);
    382    ok(await engine._store.itemExists(guid));
    383 
    384    ok((await engine._store.getAllIDs())[guid]);
    385 
    386    Services.logins.removeLogin(foundLogins[0]);
    387    foundLogins = await Services.logins.searchLoginsAsync({
    388      origin: "http://mozilla.com",
    389    });
    390    equal(foundLogins.length, 0);
    391 
    392    changes = await engine.getChangedIDs();
    393    change = changes[guid];
    394    equal(Object.keys(changes).length, 1);
    395    equal(change.counter, 1);
    396    ok(change.deleted);
    397 
    398    _("Perform sync after removing the login");
    399    await sync_engine_and_validate_telem(engine, false);
    400 
    401    equal(collection.count(), 1);
    402    let payload = collection.payloads()[0];
    403    ok(payload.deleted);
    404 
    405    equal(Object.keys(await engine.getChangedIDs()), 0);
    406 
    407    // All of these should not include the deleted login. Only the FxA password should exist.
    408    equal(Services.logins.countLogins("", "", ""), 1);
    409    equal((await Services.logins.getAllLogins()).length, 1);
    410    ok(!(await engine._store.itemExists(guid)));
    411 
    412    // getAllIDs includes deleted items but skips the FxA login.
    413    ok((await engine._store.getAllIDs())[guid]);
    414    let deletedLogin = await engine._store._getLoginFromGUID(guid);
    415 
    416    equal(deletedLogin.hostname, null, "deleted login hostname");
    417    equal(
    418      deletedLogin.formActionOrigin,
    419      null,
    420      "deleted login formActionOrigin"
    421    );
    422    equal(deletedLogin.formSubmitURL, null, "deleted login formSubmitURL");
    423    equal(deletedLogin.httpRealm, null, "deleted login httpRealm");
    424    equal(deletedLogin.username, null, "deleted login username");
    425    equal(deletedLogin.password, null, "deleted login password");
    426    equal(deletedLogin.usernameField, "", "deleted login usernameField");
    427    equal(deletedLogin.passwordField, "", "deleted login passwordField");
    428    equal(deletedLogin.unknownFields, null, "deleted login unknownFields");
    429    equal(deletedLogin.timeCreated, 0, "deleted login timeCreated");
    430    equal(deletedLogin.timeLastUsed, 0, "deleted login timeLastUsed");
    431    equal(deletedLogin.timesUsed, 0, "deleted login timesUsed");
    432 
    433    // These fields are not reset when the login is removed.
    434    equal(deletedLogin.guid, guid, "deleted login guid");
    435    equal(deletedLogin.everSynced, true, "deleted login everSynced");
    436    equal(deletedLogin.syncCounter, 0, "deleted login syncCounter");
    437    Assert.greater(
    438      deletedLogin.timePasswordChanged,
    439      0,
    440      "deleted login timePasswordChanged"
    441    );
    442  } finally {
    443    await engine._tracker.stop();
    444 
    445    await cleanup(engine, server);
    446  }
    447 });
    448 
    449 add_task(async function test_sync_incoming() {
    450  _("Test syncing incoming records");
    451 
    452  let engine = Service.engineManager.get("passwords");
    453 
    454  let server = await serverForFoo(engine);
    455  await SyncTestingInfrastructure(server);
    456 
    457  let collection = server.user("foo").collection("passwords");
    458 
    459  const checkFields = [
    460    "formSubmitURL",
    461    "hostname",
    462    "httpRealm",
    463    "username",
    464    "password",
    465    "usernameField",
    466    "passwordField",
    467    "timeCreated",
    468  ];
    469 
    470  let guid1 = Utils.makeGUID();
    471  let details = {
    472    formSubmitURL: "https://www.example.com",
    473    hostname: "https://www.example.com",
    474    httpRealm: null,
    475    username: "camel",
    476    password: "llama",
    477    usernameField: "username-field",
    478    passwordField: "password-field",
    479    timeCreated: Date.now(),
    480    timePasswordChanged: Date.now(),
    481  };
    482 
    483  try {
    484    // This test creates a remote server record and then verifies that the login
    485    // has been added locally after the sync occurs.
    486    _("Create remote login");
    487    collection.insertRecord(Object.assign({}, details, { id: guid1 }));
    488 
    489    _("Perform sync when remote login has been added");
    490    await sync_engine_and_validate_telem(engine, false);
    491 
    492    let logins = await Services.logins.searchLoginsAsync({
    493      origin: "https://www.example.com",
    494    });
    495    equal(logins.length, 1);
    496 
    497    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
    498    checkFields.forEach(field => {
    499      equal(logins[0][field], details[field]);
    500    });
    501    equal(logins[0].timePasswordChanged, details.timePasswordChanged);
    502    equal(logins[0].syncCounter, 0);
    503    equal(logins[0].everSynced, true);
    504 
    505    // Modify the password within the remote record and then sync again.
    506    _("Perform sync when remote login's password has been modified");
    507    let newTime = Date.now();
    508    collection.updateRecord(
    509      guid1,
    510      cleartext => {
    511        cleartext.password = "alpaca";
    512      },
    513      newTime / 1000 + 10
    514    );
    515 
    516    await engine.setLastSync(newTime / 1000 - 30);
    517    await sync_engine_and_validate_telem(engine, false);
    518 
    519    logins = await Services.logins.searchLoginsAsync({
    520      origin: "https://www.example.com",
    521    });
    522    equal(logins.length, 1);
    523 
    524    details.password = "alpaca";
    525    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
    526    checkFields.forEach(field => {
    527      equal(logins[0][field], details[field]);
    528    });
    529    Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged);
    530    equal(logins[0].syncCounter, 0);
    531    equal(logins[0].everSynced, true);
    532 
    533    // Modify the username within the remote record and then sync again.
    534    _("Perform sync when remote login's username has been modified");
    535    newTime = Date.now();
    536    collection.updateRecord(
    537      guid1,
    538      cleartext => {
    539        cleartext.username = "guanaco";
    540      },
    541      newTime / 1000 + 10
    542    );
    543 
    544    await engine.setLastSync(newTime / 1000 - 30);
    545    await sync_engine_and_validate_telem(engine, false);
    546 
    547    logins = await Services.logins.searchLoginsAsync({
    548      origin: "https://www.example.com",
    549    });
    550    equal(logins.length, 1);
    551 
    552    details.username = "guanaco";
    553    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
    554    checkFields.forEach(field => {
    555      equal(logins[0][field], details[field]);
    556    });
    557    Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged);
    558    equal(logins[0].syncCounter, 0);
    559    equal(logins[0].everSynced, true);
    560 
    561    // Mark the remote record as deleted and then sync again.
    562    _("Perform sync when remote login has been marked for deletion");
    563    newTime = Date.now();
    564    collection.updateRecord(
    565      guid1,
    566      cleartext => {
    567        cleartext.deleted = true;
    568      },
    569      newTime / 1000 + 10
    570    );
    571 
    572    await engine.setLastSync(newTime / 1000 - 30);
    573    await sync_engine_and_validate_telem(engine, false);
    574 
    575    logins = await Services.logins.searchLoginsAsync({
    576      origin: "https://www.example.com",
    577    });
    578    equal(logins.length, 0);
    579  } finally {
    580    await cleanup(engine, server);
    581  }
    582 });
    583 
    584 add_task(async function test_sync_incoming_deleted() {
    585  _("Test syncing incoming deleted records");
    586 
    587  let engine = Service.engineManager.get("passwords");
    588 
    589  let server = await serverForFoo(engine);
    590  await SyncTestingInfrastructure(server);
    591 
    592  let collection = server.user("foo").collection("passwords");
    593 
    594  let guid1 = Utils.makeGUID();
    595  let details2 = {
    596    formSubmitURL: "https://www.example.org",
    597    hostname: "https://www.example.org",
    598    httpRealm: null,
    599    username: "capybara",
    600    password: "beaver",
    601    usernameField: "username-field",
    602    passwordField: "password-field",
    603    timeCreated: Date.now(),
    604    timePasswordChanged: Date.now(),
    605    deleted: true,
    606  };
    607 
    608  try {
    609    // This test creates a remote server record that has been deleted
    610    // and then verifies that the login is not imported locally.
    611    _("Create remote login");
    612    collection.insertRecord(Object.assign({}, details2, { id: guid1 }));
    613 
    614    _("Perform sync when remote login has been deleted");
    615    await sync_engine_and_validate_telem(engine, false);
    616 
    617    let logins = await Services.logins.searchLoginsAsync({
    618      origin: "https://www.example.com",
    619    });
    620    equal(logins.length, 0);
    621    ok(!(await engine._store.getAllIDs())[guid1]);
    622    ok(!(await engine._store.itemExists(guid1)));
    623  } finally {
    624    await cleanup(engine, server);
    625  }
    626 });
    627 
    628 add_task(async function test_sync_incoming_deleted_localchanged_remotenewer() {
    629  _(
    630    "Test syncing incoming deleted records where the local login has been changed but the remote record is newer"
    631  );
    632 
    633  let engine = Service.engineManager.get("passwords");
    634 
    635  let server = await serverForFoo(engine);
    636  await SyncTestingInfrastructure(server);
    637 
    638  let collection = server.user("foo").collection("passwords");
    639 
    640  let loginInfo = new LoginInfo(
    641    "http://mozilla.com",
    642    "http://mozilla.com",
    643    null,
    644    "kangaroo",
    645    "kaola",
    646    "username",
    647    "password"
    648  );
    649  let login = await Services.logins.addLoginAsync(loginInfo);
    650  let guid = login.QueryInterface(Ci.nsILoginMetaInfo).guid;
    651 
    652  try {
    653    _("Perform sync on new login");
    654    await sync_engine_and_validate_telem(engine, false);
    655 
    656    let foundLogins = await Services.logins.searchLoginsAsync({
    657      origin: "http://mozilla.com",
    658    });
    659    foundLogins[0].password = "wallaby";
    660    await Services.logins.modifyLoginAsync(login, foundLogins[0]);
    661 
    662    // Use a time in the future to ensure that the remote record is newer.
    663    collection.updateRecord(
    664      guid,
    665      cleartext => {
    666        cleartext.deleted = true;
    667      },
    668      Date.now() / 1000 + 1000
    669    );
    670 
    671    _(
    672      "Perform sync when remote login has been deleted and local login has been changed"
    673    );
    674    await sync_engine_and_validate_telem(engine, false);
    675 
    676    let logins = await Services.logins.searchLoginsAsync({
    677      origin: "https://mozilla.com",
    678    });
    679    equal(logins.length, 0);
    680    ok(await engine._store.getAllIDs());
    681  } finally {
    682    await cleanup(engine, server);
    683  }
    684 });
    685 
    686 add_task(async function test_sync_incoming_deleted_localchanged_localnewer() {
    687  _(
    688    "Test syncing incoming deleted records where the local login has been changed but the local record is newer"
    689  );
    690 
    691  let engine = Service.engineManager.get("passwords");
    692 
    693  let server = await serverForFoo(engine);
    694  await SyncTestingInfrastructure(server);
    695 
    696  let collection = server.user("foo").collection("passwords");
    697 
    698  let loginInfo = new LoginInfo(
    699    "http://www.mozilla.com",
    700    "http://www.mozilla.com",
    701    null,
    702    "lion",
    703    "tiger",
    704    "username",
    705    "password"
    706  );
    707  let login = await Services.logins.addLoginAsync(loginInfo);
    708  let guid = login.QueryInterface(Ci.nsILoginMetaInfo).guid;
    709 
    710  try {
    711    _("Perform sync on new login");
    712    await sync_engine_and_validate_telem(engine, false);
    713 
    714    let foundLogins = await Services.logins.searchLoginsAsync({
    715      origin: "http://www.mozilla.com",
    716    });
    717    foundLogins[0].password = "cheetah";
    718    await Services.logins.modifyLoginAsync(login, foundLogins[0]);
    719 
    720    // Use a time in the past to ensure that the local record is newer.
    721    collection.updateRecord(
    722      guid,
    723      cleartext => {
    724        cleartext.deleted = true;
    725      },
    726      Date.now() / 1000 - 1000
    727    );
    728 
    729    _(
    730      "Perform sync when remote login has been deleted and local login has been changed"
    731    );
    732    await sync_engine_and_validate_telem(engine, false);
    733 
    734    let logins = await Services.logins.searchLoginsAsync({
    735      origin: "http://www.mozilla.com",
    736    });
    737    equal(logins.length, 1);
    738    equal(logins[0].password, "cheetah");
    739    equal(logins[0].syncCounter, 0);
    740    equal(logins[0].everSynced, true);
    741    ok(await engine._store.getAllIDs());
    742  } finally {
    743    await cleanup(engine, server);
    744  }
    745 });
    746 
    747 add_task(async function test_sync_incoming_no_formactionorigin() {
    748  _("Test syncing incoming a record where there is no formActionOrigin");
    749 
    750  let engine = Service.engineManager.get("passwords");
    751 
    752  let server = await serverForFoo(engine);
    753  await SyncTestingInfrastructure(server);
    754 
    755  let collection = server.user("foo").collection("passwords");
    756 
    757  const checkFields = [
    758    "formSubmitURL",
    759    "hostname",
    760    "httpRealm",
    761    "username",
    762    "password",
    763    "usernameField",
    764    "passwordField",
    765    "timeCreated",
    766  ];
    767 
    768  let guid1 = Utils.makeGUID();
    769  let details = {
    770    formSubmitURL: "",
    771    hostname: "https://www.example.com",
    772    httpRealm: null,
    773    username: "rabbit",
    774    password: "squirrel",
    775    usernameField: "username-field",
    776    passwordField: "password-field",
    777    timeCreated: Date.now(),
    778    timePasswordChanged: Date.now(),
    779  };
    780 
    781  try {
    782    // This test creates a remote server record and then verifies that the login
    783    // has been added locally after the sync occurs.
    784    _("Create remote login");
    785    collection.insertRecord(Object.assign({}, details, { id: guid1 }));
    786 
    787    _("Perform sync when remote login has been added");
    788    await sync_engine_and_validate_telem(engine, false);
    789 
    790    let logins = await Services.logins.searchLoginsAsync({
    791      origin: "https://www.example.com",
    792      formActionOrigin: "",
    793    });
    794    equal(logins.length, 1);
    795 
    796    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
    797    checkFields.forEach(field => {
    798      equal(logins[0][field], details[field]);
    799    });
    800    equal(logins[0].timePasswordChanged, details.timePasswordChanged);
    801    equal(logins[0].syncCounter, 0);
    802    equal(logins[0].everSynced, true);
    803  } finally {
    804    await cleanup(engine, server);
    805  }
    806 });
    807 
    808 add_task(async function test_password_dupe() {
    809  let engine = Service.engineManager.get("passwords");
    810 
    811  let server = await serverForFoo(engine);
    812  await SyncTestingInfrastructure(server);
    813  let collection = server.user("foo").collection("passwords");
    814 
    815  let guid1 = Utils.makeGUID();
    816  let rec1 = new LoginRec("passwords", guid1);
    817  let guid2 = Utils.makeGUID();
    818  let cleartext = {
    819    formSubmitURL: "https://www.example.com",
    820    hostname: "https://www.example.com",
    821    httpRealm: null,
    822    username: "foo",
    823    password: "bar",
    824    usernameField: "username-field",
    825    passwordField: "password-field",
    826    timeCreated: Math.round(Date.now()),
    827    timePasswordChanged: Math.round(Date.now()),
    828  };
    829  rec1.cleartext = cleartext;
    830 
    831  _("Create remote record with same details and guid1");
    832  collection.insert(guid1, encryptPayload(rec1.cleartext));
    833 
    834  _("Create remote record with guid2");
    835  collection.insert(guid2, encryptPayload(cleartext));
    836 
    837  _("Create local record with same details and guid1");
    838  await engine._store.create(rec1);
    839 
    840  try {
    841    _("Perform sync");
    842    await sync_engine_and_validate_telem(engine, true);
    843 
    844    let logins = await Services.logins.searchLoginsAsync({
    845      origin: "https://www.example.com",
    846    });
    847 
    848    equal(logins.length, 1);
    849    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid2);
    850    equal(null, collection.payload(guid1));
    851  } finally {
    852    await cleanup(engine, server);
    853  }
    854 });
    855 
    856 add_task(async function test_updated_null_password_sync() {
    857  _("Ensure updated null login username is converted to a string");
    858 
    859  let engine = Service.engineManager.get("passwords");
    860 
    861  let server = await serverForFoo(engine);
    862  await SyncTestingInfrastructure(server);
    863  let collection = server.user("foo").collection("passwords");
    864 
    865  let guid1 = Utils.makeGUID();
    866  let guid2 = Utils.makeGUID();
    867  let remoteDetails = {
    868    formSubmitURL: "https://www.nullupdateexample.com",
    869    hostname: "https://www.nullupdateexample.com",
    870    httpRealm: null,
    871    username: null,
    872    password: "bar",
    873    usernameField: "username-field",
    874    passwordField: "password-field",
    875    timeCreated: Date.now(),
    876    timePasswordChanged: Date.now(),
    877  };
    878  let localDetails = {
    879    formSubmitURL: "https://www.nullupdateexample.com",
    880    hostname: "https://www.nullupdateexample.com",
    881    httpRealm: null,
    882    username: "foo",
    883    password: "foobar",
    884    usernameField: "username-field",
    885    passwordField: "password-field",
    886    timeCreated: Date.now(),
    887    timePasswordChanged: Date.now(),
    888  };
    889 
    890  _("Create remote record with same details and guid1");
    891  collection.insertRecord(Object.assign({}, remoteDetails, { id: guid1 }));
    892 
    893  try {
    894    _("Create local updated login with null password");
    895    await engine._store.update(Object.assign({}, localDetails, { id: guid2 }));
    896 
    897    _("Perform sync");
    898    await sync_engine_and_validate_telem(engine, false);
    899 
    900    let logins = await Services.logins.searchLoginsAsync({
    901      origin: "https://www.nullupdateexample.com",
    902    });
    903 
    904    equal(logins.length, 1);
    905    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
    906  } finally {
    907    await cleanup(engine, server);
    908  }
    909 });
    910 
    911 add_task(async function test_updated_undefined_password_sync() {
    912  _("Ensure updated undefined login username is converted to a string");
    913 
    914  let engine = Service.engineManager.get("passwords");
    915 
    916  let server = await serverForFoo(engine);
    917  await SyncTestingInfrastructure(server);
    918  let collection = server.user("foo").collection("passwords");
    919 
    920  let guid1 = Utils.makeGUID();
    921  let guid2 = Utils.makeGUID();
    922  let remoteDetails = {
    923    formSubmitURL: "https://www.undefinedupdateexample.com",
    924    hostname: "https://www.undefinedupdateexample.com",
    925    httpRealm: null,
    926    username: undefined,
    927    password: "bar",
    928    usernameField: "username-field",
    929    passwordField: "password-field",
    930    timeCreated: Date.now(),
    931    timePasswordChanged: Date.now(),
    932  };
    933  let localDetails = {
    934    formSubmitURL: "https://www.undefinedupdateexample.com",
    935    hostname: "https://www.undefinedupdateexample.com",
    936    httpRealm: null,
    937    username: "foo",
    938    password: "foobar",
    939    usernameField: "username-field",
    940    passwordField: "password-field",
    941    timeCreated: Date.now(),
    942    timePasswordChanged: Date.now(),
    943  };
    944 
    945  _("Create remote record with same details and guid1");
    946  collection.insertRecord(Object.assign({}, remoteDetails, { id: guid1 }));
    947 
    948  try {
    949    _("Create local updated login with undefined password");
    950    await engine._store.update(Object.assign({}, localDetails, { id: guid2 }));
    951 
    952    _("Perform sync");
    953    await sync_engine_and_validate_telem(engine, false);
    954 
    955    let logins = await Services.logins.searchLoginsAsync({
    956      origin: "https://www.undefinedupdateexample.com",
    957    });
    958 
    959    equal(logins.length, 1);
    960    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
    961  } finally {
    962    await cleanup(engine, server);
    963  }
    964 });
    965 
    966 add_task(async function test_new_null_password_sync() {
    967  _("Ensure new null login username is converted to a string");
    968 
    969  let engine = Service.engineManager.get("passwords");
    970 
    971  let server = await serverForFoo(engine);
    972  await SyncTestingInfrastructure(server);
    973 
    974  let guid1 = Utils.makeGUID();
    975  let rec1 = new LoginRec("passwords", guid1);
    976  rec1.cleartext = {
    977    formSubmitURL: "https://www.example.com",
    978    hostname: "https://www.example.com",
    979    httpRealm: null,
    980    username: null,
    981    password: "bar",
    982    usernameField: "username-field",
    983    passwordField: "password-field",
    984    timeCreated: Date.now(),
    985    timePasswordChanged: Date.now(),
    986  };
    987 
    988  try {
    989    _("Create local login with null password");
    990    await engine._store.create(rec1);
    991 
    992    _("Perform sync");
    993    await sync_engine_and_validate_telem(engine, false);
    994 
    995    let logins = await Services.logins.searchLoginsAsync({
    996      origin: "https://www.example.com",
    997    });
    998 
    999    equal(logins.length, 1);
   1000    notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, null);
   1001    notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, undefined);
   1002    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, "");
   1003  } finally {
   1004    await cleanup(engine, server);
   1005  }
   1006 });
   1007 
   1008 add_task(async function test_new_undefined_password_sync() {
   1009  _("Ensure new undefined login username is converted to a string");
   1010 
   1011  let engine = Service.engineManager.get("passwords");
   1012 
   1013  let server = await serverForFoo(engine);
   1014  await SyncTestingInfrastructure(server);
   1015 
   1016  let guid1 = Utils.makeGUID();
   1017  let rec1 = new LoginRec("passwords", guid1);
   1018  rec1.cleartext = {
   1019    formSubmitURL: "https://www.example.com",
   1020    hostname: "https://www.example.com",
   1021    httpRealm: null,
   1022    username: undefined,
   1023    password: "bar",
   1024    usernameField: "username-field",
   1025    passwordField: "password-field",
   1026    timeCreated: Date.now(),
   1027    timePasswordChanged: Date.now(),
   1028  };
   1029 
   1030  try {
   1031    _("Create local login with undefined password");
   1032    await engine._store.create(rec1);
   1033 
   1034    _("Perform sync");
   1035    await sync_engine_and_validate_telem(engine, false);
   1036 
   1037    let logins = await Services.logins.searchLoginsAsync({
   1038      origin: "https://www.example.com",
   1039    });
   1040 
   1041    equal(logins.length, 1);
   1042    notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, null);
   1043    notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, undefined);
   1044    equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, "");
   1045  } finally {
   1046    await cleanup(engine, server);
   1047  }
   1048 });
   1049 
   1050 add_task(async function test_sync_password_validation() {
   1051  // This test isn't in test_password_validator to avoid duplicating cleanup.
   1052  _("Ensure that if a password validation happens, it ends up in the ping");
   1053 
   1054  let engine = Service.engineManager.get("passwords");
   1055 
   1056  let server = await serverForFoo(engine);
   1057  await SyncTestingInfrastructure(server);
   1058 
   1059  Svc.PrefBranch.setIntPref("engine.passwords.validation.interval", 0);
   1060  Svc.PrefBranch.setIntPref(
   1061    "engine.passwords.validation.percentageChance",
   1062    100
   1063  );
   1064  Svc.PrefBranch.setIntPref("engine.passwords.validation.maxRecords", -1);
   1065  Svc.PrefBranch.setBoolPref("engine.passwords.validation.enabled", true);
   1066 
   1067  try {
   1068    let ping = await wait_for_ping(() => Service.sync());
   1069 
   1070    let engineInfo = ping.engines.find(e => e.name == "passwords");
   1071    ok(engineInfo, "Engine should be in ping");
   1072 
   1073    let validation = engineInfo.validation;
   1074    ok(validation, "Engine should have validation info");
   1075  } finally {
   1076    await cleanup(engine, server);
   1077  }
   1078 });
   1079 
   1080 add_task(async function test_roundtrip_unknown_fields() {
   1081  _(
   1082    "Testing that unknown fields from other clients get roundtripped back to server"
   1083  );
   1084 
   1085  let engine = Service.engineManager.get("passwords");
   1086 
   1087  let server = await serverForFoo(engine);
   1088  await SyncTestingInfrastructure(server);
   1089  let collection = server.user("foo").collection("passwords");
   1090 
   1091  enableValidationPrefs();
   1092 
   1093  _("Add login with older password change time to replace during first sync");
   1094  let oldLogin;
   1095  {
   1096    let login = new LoginInfo(
   1097      "https://mozilla.com",
   1098      "",
   1099      null,
   1100      "us3r",
   1101      "0ldpa55",
   1102      "",
   1103      ""
   1104    );
   1105    await Services.logins.addLoginAsync(login);
   1106 
   1107    let props = new PropertyBag();
   1108    let localPasswordChangeTime = Math.round(
   1109      Date.now() - 1 * 60 * 60 * 24 * 1000
   1110    );
   1111    props.setProperty("timePasswordChanged", localPasswordChangeTime);
   1112    await Services.logins.modifyLoginAsync(login, props);
   1113 
   1114    let logins = await Services.logins.searchLoginsAsync({
   1115      origin: "https://mozilla.com",
   1116    });
   1117    equal(logins.length, 1, "Should find old login in login manager");
   1118    oldLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
   1119    equal(oldLogin.timePasswordChanged, localPasswordChangeTime);
   1120 
   1121    let rec = new LoginRec("passwords", oldLogin.guid);
   1122    rec.hostname = oldLogin.origin;
   1123    rec.formSubmitURL = oldLogin.formActionOrigin;
   1124    rec.httpRealm = oldLogin.httpRealm;
   1125    rec.username = oldLogin.username;
   1126    // Change the password and bump the password change time to ensure we prefer
   1127    // the remote one during reconciliation.
   1128    rec.password = "n3wpa55";
   1129    rec.usernameField = oldLogin.usernameField;
   1130    rec.passwordField = oldLogin.usernameField;
   1131    rec.timeCreated = oldLogin.timeCreated;
   1132    rec.timePasswordChanged = Math.round(Date.now());
   1133 
   1134    // pretend other clients have some snazzy new fields
   1135    // we don't quite understand yet
   1136    rec.cleartext.someStrField = "I am a str";
   1137    rec.cleartext.someObjField = { newField: "I am a new field" };
   1138    collection.insert(oldLogin.guid, encryptPayload(rec.cleartext));
   1139  }
   1140 
   1141  await engine._tracker.stop();
   1142 
   1143  try {
   1144    await sync_engine_and_validate_telem(engine, false);
   1145 
   1146    let logins = await Services.logins.searchLoginsAsync({
   1147      origin: "https://mozilla.com",
   1148    });
   1149    equal(
   1150      logins[0].password,
   1151      "n3wpa55",
   1152      "Should update local password for older login"
   1153    );
   1154    let expectedUnknowns = JSON.stringify({
   1155      someStrField: "I am a str",
   1156      someObjField: { newField: "I am a new field" },
   1157    });
   1158    // Check that the local record has all unknown fields properly
   1159    // stringified
   1160    equal(logins[0].unknownFields, expectedUnknowns);
   1161 
   1162    // Check that the server has the unknown fields unfurled and on the
   1163    // top-level record
   1164    let serverRec = collection.cleartext(oldLogin.guid);
   1165    equal(serverRec.someStrField, "I am a str");
   1166    equal(serverRec.someObjField.newField, "I am a new field");
   1167  } finally {
   1168    await cleanup(engine, server);
   1169  }
   1170 });
   1171 
   1172 add_task(async function test_new_passwords_from_csv() {
   1173  _("Test syncing records imported from a csv file");
   1174 
   1175  let engine = Service.engineManager.get("passwords");
   1176 
   1177  let server = await serverForFoo(engine);
   1178  await SyncTestingInfrastructure(server);
   1179 
   1180  let collection = server.user("foo").collection("passwords");
   1181 
   1182  engine._tracker.start();
   1183 
   1184  let data = [
   1185    {
   1186      hostname: "https://example.com",
   1187      url: "https://example.com/path",
   1188      username: "exampleuser",
   1189      password: "examplepassword",
   1190    },
   1191    {
   1192      hostname: "https://mozilla.org",
   1193      url: "https://mozilla.org",
   1194      username: "mozillauser",
   1195      password: "mozillapassword",
   1196    },
   1197    {
   1198      hostname: "https://www.example.org",
   1199      url: "https://www.example.org/example1/example2",
   1200      username: "person",
   1201      password: "mypassword",
   1202    },
   1203  ];
   1204 
   1205  let csvData = ["url,username,login_password"];
   1206  for (let row of data) {
   1207    csvData.push(row.url + "," + row.username + "," + row.password);
   1208  }
   1209 
   1210  let csvFile = FileTestUtils.getTempFile(`firefox_logins.csv`);
   1211  await IOUtils.writeUTF8(csvFile.path, csvData.join("\r\n"));
   1212 
   1213  await LoginCSVImport.importFromCSV(csvFile.path);
   1214 
   1215  equal(
   1216    engine._tracker.score,
   1217    SCORE_INCREMENT_XLARGE,
   1218    "Should only get one update notification for import"
   1219  );
   1220 
   1221  _("Ensure that the csv import is correct");
   1222  for (let item of data) {
   1223    let foundLogins = await Services.logins.searchLoginsAsync({
   1224      origin: item.hostname,
   1225    });
   1226    equal(foundLogins.length, 1);
   1227    equal(foundLogins[0].syncCounter, 1);
   1228    equal(foundLogins[0].everSynced, false);
   1229    equal(foundLogins[0].username, item.username);
   1230    equal(foundLogins[0].password, item.password);
   1231  }
   1232 
   1233  _("Perform sync after modifying the password");
   1234  await sync_engine_and_validate_telem(engine, false);
   1235 
   1236  _("Verify that the sync counter and status are updated");
   1237  for (let item of data) {
   1238    let foundLogins = await Services.logins.searchLoginsAsync({
   1239      origin: item.hostname,
   1240    });
   1241    equal(foundLogins.length, 1);
   1242    equal(foundLogins[0].syncCounter, 0);
   1243    equal(foundLogins[0].everSynced, true);
   1244    equal(foundLogins[0].username, item.username);
   1245    equal(foundLogins[0].password, item.password);
   1246    item.guid = foundLogins[0].guid;
   1247  }
   1248 
   1249  equal(Object.keys(await engine.getChangedIDs()), 0);
   1250  equal(collection.count(), 3);
   1251 
   1252  for (let item of data) {
   1253    // The remote login should have the imported username and password.
   1254    let newRec = collection.cleartext(item.guid);
   1255    equal(newRec.username, item.username);
   1256    equal(newRec.password, item.password);
   1257  }
   1258 });