/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests the case where there are logins that cannot be decrypted.
 */

"use strict";

// Globals

/**
 * Resets the token used to decrypt logins.  This is equivalent to resetting the
 * primary password when it is not known.
 */
function resetPrimaryPassword() {
  let token = Cc["@mozilla.org/security/pk11tokendb;1"]
    .getService(Ci.nsIPK11TokenDB)
    .getInternalKeyToken();
  token.reset();
  token.initPassword("");
}

// Tests

/**
 * Resets the primary password after some logins were added to the database.
 */
add_task(async function test_logins_decrypt_failure() {
  let logins = TestData.loginList();
  await Services.logins.addLogins(logins);

  // This makes the existing logins non-decryptable.
  resetPrimaryPassword();

  // These functions don't see the non-decryptable entries anymore.
  Assert.equal(Services.logins.getAllLogins().length, 0);
  Assert.equal(
    (await Services.logins.getAllLoginsAsync()).length,
    0,
    "getAllLoginsAsync length"
  );
  Assert.equal(Services.logins.findLogins("", "", "").length, 0);
  Assert.equal(Services.logins.searchLogins(newPropertyBag()).length, 0);
  Assert.throws(
    () => Services.logins.modifyLogin(logins[0], newPropertyBag()),
    /No matching logins/
  );
  Assert.throws(
    () => Services.logins.removeLogin(logins[0]),
    /No matching logins/
  );

  // The function that counts logins sees the non-decryptable entries also.
  Assert.equal(Services.logins.countLogins("", "", ""), logins.length);

  // Equivalent logins can be added.
  await Services.logins.addLogins(logins);
  LoginTestUtils.checkLogins(logins);
  Assert.equal(
    (await Services.logins.getAllLoginsAsync()).length,
    logins.length,
    "getAllLoginsAsync length"
  );
  Assert.equal(Services.logins.countLogins("", "", ""), logins.length * 2);

  // Finding logins doesn't return the non-decryptable duplicates.
  Assert.equal(
    Services.logins.findLogins("http://www.example.com", "", "").length,
    1
  );
  let matchData = newPropertyBag({ origin: "http://www.example.com" });
  Assert.equal(Services.logins.searchLogins(matchData).length, 1);

  // Removing single logins does not remove non-decryptable logins.
  for (let loginInfo of TestData.loginList()) {
    Services.logins.removeLogin(loginInfo);
  }
  Assert.equal(Services.logins.getAllLogins().length, 0);
  Assert.equal(Services.logins.countLogins("", "", ""), logins.length);

  // Removing all logins removes the non-decryptable entries also.
  Services.logins.removeAllUserFacingLogins();
  Assert.equal(Services.logins.getAllLogins().length, 0);
  Assert.equal(Services.logins.countLogins("", "", ""), 0);
});

// Bug 621846 - If a login has a GUID but can't be decrypted, a search for
// that GUID will (correctly) fail. Ensure we can add a new login with that
// same GUID.
add_task(async function test_add_logins_with_decrypt_failure() {
  // a login with a GUID.
  let login = new LoginInfo(
    "http://www.example2.com",
    "http://www.example2.com",
    null,
    "the username",
    "the password for www.example.com",
    "form_field_username",
    "form_field_password"
  );

  login.QueryInterface(Ci.nsILoginMetaInfo);
  login.guid = "{4bc50d2f-dbb6-4aa3-807c-c4c2065a2c35}";

  // A different login but with the same GUID.
  let loginDupeGuid = new LoginInfo(
    "http://www.example3.com",
    "http://www.example3.com",
    null,
    "the username",
    "the password",
    "form_field_username",
    "form_field_password"
  );
  loginDupeGuid.QueryInterface(Ci.nsILoginMetaInfo);
  loginDupeGuid.guid = login.guid;

  await Services.logins.addLoginAsync(login);

  // We can search for this login by GUID.
  let searchProp = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
    Ci.nsIWritablePropertyBag2
  );
  searchProp.setPropertyAsAUTF8String("guid", login.guid);

  equal(Services.logins.searchLogins(searchProp).length, 1);

  // We should fail to re-add it as it remains good.
  await Assert.rejects(
    Services.logins.addLoginAsync(login),
    /This login already exists./
  );
  // We should fail to re-add a different login with the same GUID.
  await Assert.rejects(
    Services.logins.addLoginAsync(loginDupeGuid),
    /specified GUID already exists/
  );

  // This makes the existing login non-decryptable.
  resetPrimaryPassword();

  // We can no longer find it in our search.
  equal(Services.logins.searchLogins(searchProp).length, 0);

  // So we should be able to re-add a login with that same GUID.
  await Services.logins.addLoginAsync(login);
  equal(Services.logins.searchLogins(searchProp).length, 1);

  Services.logins.removeAllUserFacingLogins();
});

// Test the "syncID" metadata works as expected on decryption failure.
add_task(async function test_sync_metadata_with_decrypt_failure() {
  // And some sync metadata
  await Services.logins.setSyncID("sync-id");
  await Services.logins.setLastSync(123);
  equal(await Services.logins.getSyncID(), "sync-id");
  equal(await Services.logins.getLastSync(), 123);

  // This makes the existing login and syncID non-decryptable.
  resetPrimaryPassword();

  // The syncID is now null.
  equal(await Services.logins.getSyncID(), null);
  // The sync timestamp isn't impacted.
  equal(await Services.logins.getLastSync(), 123);

  // But we should be able to set it again.
  await Services.logins.setSyncID("new-id");
  equal(await Services.logins.getSyncID(), "new-id");
});
