/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests the handling of nsILoginMetaInfo by methods that add, remove, modify,
 * and find logins.
 */

"use strict";

// Globals

XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");

var gLooksLikeUUIDRegex = /^\{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\}$/;

/**
 * Retrieves the only login among the current data that matches the hostname of
 * the given nsILoginInfo.  In case there is more than one login for the
 * hostname, the test fails.
 */
function retrieveLoginMatching(aLoginInfo)
{
  let logins = Services.logins.findLogins({}, aLoginInfo.hostname, "", "");
  do_check_eq(logins.length, 1);
  return logins[0].QueryInterface(Ci.nsILoginMetaInfo);
}

/**
 * Checks that the nsILoginInfo and nsILoginMetaInfo properties of two different
 * login instances are equal.
 */
function assertMetaInfoEqual(aActual, aExpected)
{
  do_check_neq(aActual, aExpected);

  // Check the nsILoginInfo properties.
  do_check_true(aActual.equals(aExpected));

  // Check the nsILoginMetaInfo properties.
  do_check_eq(aActual.guid, aExpected.guid);
  do_check_eq(aActual.timeCreated, aExpected.timeCreated);
  do_check_eq(aActual.timeLastUsed, aExpected.timeLastUsed);
  do_check_eq(aActual.timePasswordChanged, aExpected.timePasswordChanged);
  do_check_eq(aActual.timesUsed, aExpected.timesUsed);
}

/**
 * nsILoginInfo instances with or without nsILoginMetaInfo properties.
 */
var gLoginInfo1;
var gLoginInfo2;
var gLoginInfo3;

/**
 * nsILoginInfo instances reloaded with all the nsILoginMetaInfo properties.
 * These are often used to provide the reference values to test against.
 */
var gLoginMetaInfo1;
var gLoginMetaInfo2;
var gLoginMetaInfo3;

// Tests

/**
 * Prepare the test objects that will be used by the following tests.
 */
add_task(function test_initialize()
{
  // Use a reference time from ten minutes ago to initialize one instance of
  // nsILoginMetaInfo, to test that reference times are updated when needed.
  let baseTimeMs = Date.now() - 600000;

  gLoginInfo1 = TestData.formLogin();
  gLoginInfo2 = TestData.formLogin({
    hostname: "http://other.example.com",
    guid: gUUIDGenerator.generateUUID().toString(),
    timeCreated: baseTimeMs,
    timeLastUsed: baseTimeMs + 2,
    timePasswordChanged: baseTimeMs + 1,
    timesUsed: 2,
  });
  gLoginInfo3 = TestData.authLogin();
});

/**
 * Tests the behavior of addLogin with regard to metadata.  The logins added
 * here are also used by the following tests.
 */
add_task(function test_addLogin_metainfo()
{
  // Add a login without metadata to the database.
  Services.logins.addLogin(gLoginInfo1);

  // The object provided to addLogin should not have been modified.
  do_check_eq(gLoginInfo1.guid, null);
  do_check_eq(gLoginInfo1.timeCreated, 0);
  do_check_eq(gLoginInfo1.timeLastUsed, 0);
  do_check_eq(gLoginInfo1.timePasswordChanged, 0);
  do_check_eq(gLoginInfo1.timesUsed, 0);

  // A login with valid metadata should have been stored.
  gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1);
  do_check_true(gLooksLikeUUIDRegex.test(gLoginMetaInfo1.guid));
  let creationTime = gLoginMetaInfo1.timeCreated;
  LoginTestUtils.assertTimeIsAboutNow(creationTime);
  do_check_eq(gLoginMetaInfo1.timeLastUsed, creationTime);
  do_check_eq(gLoginMetaInfo1.timePasswordChanged, creationTime);
  do_check_eq(gLoginMetaInfo1.timesUsed, 1);

  // Add a login without metadata to the database.
  let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo);
  Services.logins.addLogin(gLoginInfo2);

  // The object provided to addLogin should not have been modified.
  assertMetaInfoEqual(gLoginInfo2, originalLogin);

  // A login with the provided metadata should have been stored.
  gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
  assertMetaInfoEqual(gLoginMetaInfo2, gLoginInfo2);

  // Add an authentication login to the database before continuing.
  Services.logins.addLogin(gLoginInfo3);
  gLoginMetaInfo3 = retrieveLoginMatching(gLoginInfo3);
  LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
});

/**
 * Tests that adding a login with a duplicate GUID throws an exception.
 */
add_task(function test_addLogin_metainfo_duplicate()
{
  let loginInfo = TestData.formLogin({
    hostname: "http://duplicate.example.com",
    guid: gLoginMetaInfo2.guid,
  });
  Assert.throws(() => Services.logins.addLogin(loginInfo),
                /specified GUID already exists/);

  // Verify that no data was stored by the previous call.
  LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
});

/**
 * Tests that the existing metadata is not changed when modifyLogin is called
 * with an nsILoginInfo argument.
 */
add_task(function test_modifyLogin_nsILoginInfo_metainfo_ignored()
{
  let newLoginInfo = gLoginInfo1.clone().QueryInterface(Ci.nsILoginMetaInfo);
  newLoginInfo.guid = gUUIDGenerator.generateUUID().toString();
  newLoginInfo.timeCreated = Date.now();
  newLoginInfo.timeLastUsed = Date.now();
  newLoginInfo.timePasswordChanged = Date.now();
  newLoginInfo.timesUsed = 12;
  Services.logins.modifyLogin(gLoginInfo1, newLoginInfo);

  newLoginInfo = retrieveLoginMatching(gLoginInfo1);
  assertMetaInfoEqual(newLoginInfo, gLoginMetaInfo1);
});

/**
 * Tests the modifyLogin function with an nsIProperyBag argument.
 */
add_task(function test_modifyLogin_nsIProperyBag_metainfo()
{
  // Use a new reference time that is two minutes from now.
  let newTimeMs = Date.now() + 120000;
  let newUUIDValue = gUUIDGenerator.generateUUID().toString();

  // Check that properties are changed as requested.
  Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({
    guid: newUUIDValue,
    timeCreated: newTimeMs,
    timeLastUsed: newTimeMs + 2,
    timePasswordChanged: newTimeMs + 1,
    timesUsed: 2,
  }));

  gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1);
  do_check_eq(gLoginMetaInfo1.guid, newUUIDValue);
  do_check_eq(gLoginMetaInfo1.timeCreated, newTimeMs);
  do_check_eq(gLoginMetaInfo1.timeLastUsed, newTimeMs + 2);
  do_check_eq(gLoginMetaInfo1.timePasswordChanged, newTimeMs + 1);
  do_check_eq(gLoginMetaInfo1.timesUsed, 2);

  // Check that timePasswordChanged is updated when changing the password.
  let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo);
  Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
    password: "new password",
  }));
  gLoginInfo2.password = "new password";

  gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
  do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password);
  do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
  do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
  LoginTestUtils.assertTimeIsAboutNow(gLoginMetaInfo2.timePasswordChanged);

  // Check that timePasswordChanged is not set to the current time when changing
  // the password and specifying a new value for the property at the same time.
  Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
    password: "other password",
    timePasswordChanged: newTimeMs,
  }));
  gLoginInfo2.password = "other password";

  gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
  do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password);
  do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
  do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
  do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs);

  // Check the special timesUsedIncrement property.
  Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
    timesUsedIncrement: 2,
  }));

  gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
  do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
  do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
  do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs);
  do_check_eq(gLoginMetaInfo2.timesUsed, 4);
});

/**
 * Tests that modifying a login to a duplicate GUID throws an exception.
 */
add_task(function test_modifyLogin_nsIProperyBag_metainfo_duplicate()
{
  Assert.throws(() => Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({
    guid: gLoginInfo2.guid,
  })), /specified GUID already exists/);
  LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
});

/**
 * Tests searching logins using nsILoginMetaInfo properties.
 */
add_task(function test_searchLogins_metainfo()
{
  // Find by GUID.
  let logins = Services.logins.searchLogins({}, newPropertyBag({
    guid: gLoginMetaInfo1.guid,
  }));
  do_check_eq(logins.length, 1);
  let foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
  assertMetaInfoEqual(foundLogin, gLoginMetaInfo1);

  // Find by timestamp.
  logins = Services.logins.searchLogins({}, newPropertyBag({
    timePasswordChanged: gLoginMetaInfo2.timePasswordChanged,
  }));
  do_check_eq(logins.length, 1);
  foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
  assertMetaInfoEqual(foundLogin, gLoginMetaInfo2);

  // Find using two properties at the same time.
  logins = Services.logins.searchLogins({}, newPropertyBag({
    guid: gLoginMetaInfo3.guid,
    timePasswordChanged: gLoginMetaInfo3.timePasswordChanged,
  }));
  do_check_eq(logins.length, 1);
  foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
  assertMetaInfoEqual(foundLogin, gLoginMetaInfo3);
});

/**
 * Tests that the default nsILoginManagerStorage module attached to the Login
 * Manager service is able to save and reload nsILoginMetaInfo properties.
 */
add_task(async function test_storage_metainfo()
{
  await LoginTestUtils.reloadData();
  LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);

  assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo1), gLoginMetaInfo1);
  assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo2), gLoginMetaInfo2);
  assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo3), gLoginMetaInfo3);
});
