summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/unit/test_logins_change.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/unit/test_logins_change.js')
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_change.js628
1 files changed, 628 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_change.js b/toolkit/components/passwordmgr/test/unit/test_logins_change.js
new file mode 100644
index 0000000000..ee3fee04d0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_change.js
@@ -0,0 +1,628 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests methods that add, remove, and modify logins.
+ */
+
+"use strict";
+
+// Globals
+
+const MAX_DATE_MS = 8640000000000000;
+
+/**
+ * Verifies that the specified login is considered invalid by addLogin and by
+ * modifyLogin with both nsILoginInfo and nsIPropertyBag arguments.
+ *
+ * This test requires that the login store is empty.
+ *
+ * @param aLoginInfo
+ * nsILoginInfo corresponding to an invalid login.
+ * @param aExpectedError
+ * This argument is passed to the "Assert.throws" test to determine which
+ * error is expected from the modification functions.
+ */
+async function checkLoginInvalid(aLoginInfo, aExpectedError) {
+ // Try to add the new login, and verify that no data is stored.
+ await Assert.rejects(
+ Services.logins.addLoginAsync(aLoginInfo),
+ aExpectedError
+ );
+ LoginTestUtils.checkLogins([]);
+
+ // Add a login for the modification tests.
+ let testLogin = TestData.formLogin({ origin: "http://modify.example.com" });
+ await Services.logins.addLoginAsync(testLogin);
+
+ // Try to modify the existing login using nsILoginInfo and nsIPropertyBag.
+ Assert.throws(
+ () => Services.logins.modifyLogin(testLogin, aLoginInfo),
+ aExpectedError
+ );
+ Assert.throws(
+ () =>
+ Services.logins.modifyLogin(
+ testLogin,
+ newPropertyBag({
+ origin: aLoginInfo.origin,
+ formActionOrigin: aLoginInfo.formActionOrigin,
+ httpRealm: aLoginInfo.httpRealm,
+ username: aLoginInfo.username,
+ password: aLoginInfo.password,
+ usernameField: aLoginInfo.usernameField,
+ passwordField: aLoginInfo.passwordField,
+ })
+ ),
+ aExpectedError
+ );
+
+ // Verify that no data was stored by the previous calls.
+ LoginTestUtils.checkLogins([testLogin]);
+ Services.logins.removeLogin(testLogin);
+}
+
+/**
+ * Verifies that two objects are not the same instance
+ * but have equal attributes.
+ *
+ * @param {Object} objectA
+ * An object to compare.
+ *
+ * @param {Object} objectB
+ * Another object to compare.
+ *
+ * @param {string[]} attributes
+ * Attributes to compare.
+ *
+ * @return true if all passed attributes are equal for both objects, false otherwise.
+ */
+function compareAttributes(objectA, objectB, attributes) {
+ // If it's the same object, we want to return false.
+ if (objectA == objectB) {
+ return false;
+ }
+ return attributes.every(attr => objectA[attr] == objectB[attr]);
+}
+
+// Tests
+
+/**
+ * Tests that adding logins to the database works.
+ */
+add_task(async function test_addLogin_removeLogin() {
+ // Each login from the test data should be valid and added to the list.
+ await Services.logins.addLogins(TestData.loginList());
+ LoginTestUtils.checkLogins(TestData.loginList());
+
+ // Trying to add each login again should result in an error.
+ for (let loginInfo of TestData.loginList()) {
+ await Assert.rejects(
+ Services.logins.addLoginAsync(loginInfo),
+ /This login already exists./
+ );
+ }
+
+ // Removing each login should succeed.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.removeLogin(loginInfo);
+ }
+
+ LoginTestUtils.checkLogins([]);
+});
+
+add_task(async function add_login_works_with_empty_array() {
+ const result = await Services.logins.addLogins([]);
+ Assert.equal(result.length, 0, "no logins added");
+});
+
+add_task(async function duplicated_logins_are_not_added() {
+ const login = TestData.formLogin({
+ username: "user",
+ });
+ await Services.logins.addLogins([login]);
+ const result = await Services.logins.addLogins([login]);
+ Assert.equal(result, 0, "no logins added");
+ Services.logins.removeAllUserFacingLogins();
+});
+
+add_task(async function logins_containing_nul_in_username_are_not_added() {
+ const result = await Services.logins.addLogins([
+ TestData.formLogin({ username: "user\0name" }),
+ ]);
+ Assert.equal(result, 0, "no logins added");
+});
+
+add_task(async function logins_containing_nul_in_password_are_not_added() {
+ const result = await Services.logins.addLogins([
+ TestData.formLogin({ password: "pass\0word" }),
+ ]);
+ Assert.equal(result, 0, "no logins added");
+});
+
+add_task(
+ async function return_value_includes_plaintext_username_and_password() {
+ const login = TestData.formLogin({});
+ const [result] = await Services.logins.addLogins([login]);
+ Assert.equal(result.username, login.username, "plaintext username is set");
+ Assert.equal(result.password, login.password, "plaintext password is set");
+ Services.logins.removeAllUserFacingLogins();
+ }
+);
+
+add_task(async function event_data_includes_plaintext_username_and_password() {
+ const login = TestData.formLogin({});
+ const TestObserver = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ observe(subject, topic, data) {
+ Assert.ok(subject instanceof Ci.nsILoginInfo);
+ Assert.ok(subject instanceof Ci.nsILoginMetaInfo);
+ Assert.equal(
+ subject.username,
+ login.username,
+ "plaintext username is set"
+ );
+ Assert.equal(
+ subject.password,
+ login.password,
+ "plaintext password is set"
+ );
+ },
+ };
+ Services.obs.addObserver(TestObserver, "passwordmgr-storage-changed");
+ await Services.logins.addLogins([login]);
+ Services.obs.removeObserver(TestObserver, "passwordmgr-storage-changed");
+ Services.logins.removeAllUserFacingLogins();
+});
+
+/**
+ * Tests invalid combinations of httpRealm and formActionOrigin.
+ *
+ * For an nsILoginInfo to be valid for storage, one of the two properties should
+ * be strictly equal to null, and the other must not be null or an empty string.
+ *
+ * The legacy case of an empty string in formActionOrigin and a null value in
+ * httpRealm is also supported for storage at the moment.
+ */
+add_task(async function test_invalid_httpRealm_formActionOrigin() {
+ // httpRealm === null, formActionOrigin === null
+ await checkLoginInvalid(
+ TestData.formLogin({ formActionOrigin: null }),
+ /without a httpRealm or formActionOrigin/
+ );
+
+ // httpRealm === "", formActionOrigin === null
+ await checkLoginInvalid(
+ TestData.authLogin({ httpRealm: "" }),
+ /without a httpRealm or formActionOrigin/
+ );
+
+ // httpRealm === null, formActionOrigin === ""
+ // TODO: This is not enforced for now.
+ // await checkLoginInvalid(TestData.formLogin({ formActionOrigin: "" }),
+ // /without a httpRealm or formActionOrigin/);
+
+ // httpRealm === "", formActionOrigin === ""
+ let login = TestData.formLogin({ formActionOrigin: "" });
+ login.httpRealm = "";
+ await checkLoginInvalid(login, /both a httpRealm and formActionOrigin/);
+
+ // !!httpRealm, !!formActionOrigin
+ login = TestData.formLogin();
+ login.httpRealm = "The HTTP Realm";
+ await checkLoginInvalid(login, /both a httpRealm and formActionOrigin/);
+
+ // httpRealm === "", !!formActionOrigin
+ login = TestData.formLogin();
+ login.httpRealm = "";
+ await checkLoginInvalid(login, /both a httpRealm and formActionOrigin/);
+
+ // !!httpRealm, formActionOrigin === ""
+ login = TestData.authLogin();
+ login.formActionOrigin = "";
+ await checkLoginInvalid(login, /both a httpRealm and formActionOrigin/);
+});
+
+/**
+ * Tests null or empty values in required login properties.
+ */
+add_task(async function test_missing_properties() {
+ await checkLoginInvalid(
+ TestData.formLogin({ origin: null }),
+ /null or empty origin/
+ );
+
+ await checkLoginInvalid(
+ TestData.formLogin({ origin: "" }),
+ /null or empty origin/
+ );
+
+ await checkLoginInvalid(
+ TestData.formLogin({ username: null }),
+ /null username/
+ );
+
+ await checkLoginInvalid(
+ TestData.formLogin({ password: null }),
+ /null or empty password/
+ );
+
+ await checkLoginInvalid(
+ TestData.formLogin({ password: "" }),
+ /null or empty password/
+ );
+});
+
+/**
+ * Tests invalid NUL characters in nsILoginInfo properties.
+ */
+add_task(async function test_invalid_characters() {
+ let loginList = [
+ TestData.authLogin({ origin: "http://null\0X.example.com" }),
+ TestData.authLogin({ httpRealm: "realm\0" }),
+ TestData.formLogin({ formActionOrigin: "http://null\0X.example.com" }),
+ TestData.formLogin({ usernameField: "field\0_null" }),
+ TestData.formLogin({ usernameField: ".\0" }), // Special single dot case
+ TestData.formLogin({ passwordField: "field\0_null" }),
+ TestData.formLogin({ username: "user\0name" }),
+ TestData.formLogin({ password: "pass\0word" }),
+ ];
+ for (let loginInfo of loginList) {
+ await checkLoginInvalid(loginInfo, /login values can't contain nulls/);
+ }
+});
+
+/**
+ * Tests removing a login that does not exists.
+ */
+add_task(function test_removeLogin_nonexisting() {
+ Assert.throws(
+ () => Services.logins.removeLogin(TestData.formLogin()),
+ /No matching logins/
+ );
+});
+
+/**
+ * Tests removing all logins at once.
+ */
+add_task(async function test_removeAllUserFacingLogins() {
+ await Services.logins.addLogins(TestData.loginList());
+
+ Services.logins.removeAllUserFacingLogins();
+ LoginTestUtils.checkLogins([]);
+
+ // The function should also work when there are no logins to delete.
+ Services.logins.removeAllUserFacingLogins();
+});
+
+/**
+ * Tests the modifyLogin function with an nsILoginInfo argument.
+ */
+add_task(async function test_modifyLogin_nsILoginInfo() {
+ let loginInfo = TestData.formLogin();
+ let updatedLoginInfo = TestData.formLogin({
+ username: "new username",
+ password: "new password",
+ usernameField: "new_form_field_username",
+ passwordField: "new_form_field_password",
+ });
+ let differentLoginInfo = TestData.authLogin();
+
+ // Trying to modify a login that does not exist should throw.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, updatedLoginInfo),
+ /No matching logins/
+ );
+
+ // Add the first form login, then modify it to match the second.
+ await Services.logins.addLoginAsync(loginInfo);
+ Services.logins.modifyLogin(loginInfo, updatedLoginInfo);
+
+ // The data should now match the second login.
+ LoginTestUtils.checkLogins([updatedLoginInfo]);
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, updatedLoginInfo),
+ /No matching logins/
+ );
+
+ // The login can be changed to have a different type and origin.
+ Services.logins.modifyLogin(updatedLoginInfo, differentLoginInfo);
+ LoginTestUtils.checkLogins([differentLoginInfo]);
+
+ // It is now possible to add a login with the old type and origin.
+ await Services.logins.addLoginAsync(loginInfo);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ // Modifying a login to match an existing one should not be possible.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, differentLoginInfo),
+ /already exists/
+ );
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests the modifyLogin function with an nsIPropertyBag argument.
+ */
+add_task(async function test_modifyLogin_nsIProperyBag() {
+ let loginInfo = TestData.formLogin();
+ let updatedLoginInfo = TestData.formLogin({
+ username: "new username",
+ password: "new password",
+ usernameField: "",
+ passwordField: "new_form_field_password",
+ });
+ let differentLoginInfo = TestData.authLogin();
+ let differentLoginProperties = newPropertyBag({
+ origin: differentLoginInfo.origin,
+ formActionOrigin: differentLoginInfo.formActionOrigin,
+ httpRealm: differentLoginInfo.httpRealm,
+ username: differentLoginInfo.username,
+ password: differentLoginInfo.password,
+ usernameField: differentLoginInfo.usernameField,
+ passwordField: differentLoginInfo.passwordField,
+ });
+
+ // Trying to modify a login that does not exist should throw.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, newPropertyBag()),
+ /No matching logins/
+ );
+
+ // Add the first form login, then modify it to match the second, changing
+ // only some of its properties and checking the behavior with an empty string.
+ await Services.logins.addLoginAsync(loginInfo);
+ Services.logins.modifyLogin(
+ loginInfo,
+ newPropertyBag({
+ username: "new username",
+ password: "new password",
+ usernameField: "",
+ passwordField: "new_form_field_password",
+ })
+ );
+
+ // The data should now match the second login.
+ LoginTestUtils.checkLogins([updatedLoginInfo]);
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, newPropertyBag()),
+ /No matching logins/
+ );
+
+ // It is also possible to provide no properties to be modified.
+ Services.logins.modifyLogin(updatedLoginInfo, newPropertyBag());
+
+ // Specifying a null property for a required value should throw.
+ Assert.throws(
+ () =>
+ Services.logins.modifyLogin(
+ loginInfo,
+ newPropertyBag({
+ usernameField: null,
+ })
+ ),
+ /No matching logins/
+ );
+
+ // The login can be changed to have a different type and origin.
+ Services.logins.modifyLogin(updatedLoginInfo, differentLoginProperties);
+ LoginTestUtils.checkLogins([differentLoginInfo]);
+
+ // It is now possible to add a login with the old type and origin.
+ await Services.logins.addLoginAsync(loginInfo);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ // Modifying a login to match an existing one should not be possible.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, differentLoginProperties),
+ /already exists/
+ );
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests the login deduplication function.
+ */
+add_task(function test_deduplicate_logins() {
+ // Different key attributes combinations and the amount of unique
+ // results expected for the TestData login list.
+ let keyCombinations = [
+ {
+ keyset: ["username", "password"],
+ results: 17,
+ },
+ {
+ keyset: ["origin", "username"],
+ results: 21,
+ },
+ {
+ keyset: ["origin", "username", "password"],
+ results: 22,
+ },
+ {
+ keyset: ["origin", "username", "password", "formActionOrigin"],
+ results: 27,
+ },
+ ];
+
+ let logins = TestData.loginList();
+
+ for (let testCase of keyCombinations) {
+ // Deduplicate the logins using the current testcase keyset.
+ let deduped = LoginHelper.dedupeLogins(logins, testCase.keyset);
+ Assert.equal(
+ deduped.length,
+ testCase.results,
+ "Correct amount of results."
+ );
+
+ // Checks that every login after deduping is unique.
+ Assert.ok(
+ deduped.every(loginA =>
+ deduped.every(
+ loginB => !compareAttributes(loginA, loginB, testCase.keyset)
+ )
+ ),
+ "Every login is unique."
+ );
+ }
+});
+
+/**
+ * Ensure that the login deduplication function keeps the most recent login.
+ */
+add_task(function test_deduplicate_keeps_most_recent() {
+ // Logins to deduplicate.
+ let logins = [
+ TestData.formLogin({ timeLastUsed: Date.UTC(2004, 11, 4, 0, 0, 0) }),
+ TestData.formLogin({
+ formActionOrigin: "http://example.com",
+ timeLastUsed: Date.UTC(2015, 11, 4, 0, 0, 0),
+ }),
+ ];
+
+ // Deduplicate the logins.
+ let deduped = LoginHelper.dedupeLogins(logins);
+ Assert.equal(deduped.length, 1, "Deduplicated the logins array.");
+
+ // Verify that the remaining login have the most recent date.
+ let loginTimeLastUsed = deduped[0].QueryInterface(
+ Ci.nsILoginMetaInfo
+ ).timeLastUsed;
+ Assert.equal(
+ loginTimeLastUsed,
+ Date.UTC(2015, 11, 4, 0, 0, 0),
+ "Most recent login was kept."
+ );
+
+ // Deduplicate the reverse logins array.
+ deduped = LoginHelper.dedupeLogins(logins.reverse());
+ Assert.equal(deduped.length, 1, "Deduplicated the reversed logins array.");
+
+ // Verify that the remaining login have the most recent date.
+ loginTimeLastUsed = deduped[0].QueryInterface(
+ Ci.nsILoginMetaInfo
+ ).timeLastUsed;
+ Assert.equal(
+ loginTimeLastUsed,
+ Date.UTC(2015, 11, 4, 0, 0, 0),
+ "Most recent login was kept."
+ );
+});
+
+/**
+ * Tests handling when adding a login with bad date values
+ */
+add_task(async function test_addLogin_badDates() {
+ LoginTestUtils.clearData();
+
+ let now = Date.now();
+ let defaultLoginDates = {
+ timeCreated: now,
+ timeLastUsed: now,
+ timePasswordChanged: now,
+ };
+
+ let defaultsLogin = TestData.formLogin();
+ for (let pname of ["timeCreated", "timeLastUsed", "timePasswordChanged"]) {
+ Assert.ok(!defaultsLogin[pname]);
+ }
+ Assert.ok(
+ !!(await Services.logins.addLoginAsync(defaultsLogin)),
+ "Sanity check adding defaults formLogin"
+ );
+ Services.logins.removeAllUserFacingLogins();
+
+ // 0 is a valid date in this context - new nsLoginInfo timestamps init to 0
+ for (let pname of ["timeCreated", "timeLastUsed", "timePasswordChanged"]) {
+ let loginInfo = TestData.formLogin(
+ Object.assign({}, defaultLoginDates, {
+ [pname]: 0,
+ })
+ );
+ Assert.ok(
+ !!(await Services.logins.addLoginAsync(loginInfo)),
+ "Check 0 value for " + pname
+ );
+ Services.logins.removeAllUserFacingLogins();
+ }
+
+ // negative dates get clamped to 0 and are ok
+ for (let pname of ["timeCreated", "timeLastUsed", "timePasswordChanged"]) {
+ let loginInfo = TestData.formLogin(
+ Object.assign({}, defaultLoginDates, {
+ [pname]: -1,
+ })
+ );
+ Assert.ok(
+ !!(await Services.logins.addLoginAsync(loginInfo)),
+ "Check -1 value for " + pname
+ );
+ Services.logins.removeAllUserFacingLogins();
+ }
+
+ // out-of-range dates will throw
+ for (let pname of ["timeCreated", "timeLastUsed", "timePasswordChanged"]) {
+ let loginInfo = TestData.formLogin(
+ Object.assign({}, defaultLoginDates, {
+ [pname]: MAX_DATE_MS + 1,
+ })
+ );
+ await Assert.rejects(
+ Services.logins.addLoginAsync(loginInfo),
+ /invalid date properties/
+ );
+ Assert.equal(Services.logins.getAllLogins().length, 0);
+ }
+
+ LoginTestUtils.checkLogins([]);
+});
+
+/**
+ * Tests handling when adding multiple logins with bad date values
+ */
+add_task(async function test_addLogins_badDates() {
+ LoginTestUtils.clearData();
+
+ let defaultsLogin = TestData.formLogin({
+ username: "defaults",
+ });
+ await Services.logins.addLoginAsync(defaultsLogin);
+
+ // -11644473600000 is the value you get if you convert Dec 31 1600 16:07:02 to unix epoch time
+ let timeCreatedLogin = TestData.formLogin({
+ username: "tc",
+ timeCreated: -11644473600000,
+ });
+ await Assert.rejects(
+ Services.logins.addLoginAsync(timeCreatedLogin),
+ /Can\'t add a login with invalid date properties./
+ );
+
+ let timeLastUsedLogin = TestData.formLogin({
+ username: "tlu",
+ timeLastUsed: -11644473600000,
+ });
+ await Assert.rejects(
+ Services.logins.addLoginAsync(timeLastUsedLogin),
+ /Can\'t add a login with invalid date properties./
+ );
+
+ let timePasswordChangedLogin = TestData.formLogin({
+ username: "tpc",
+ timePasswordChanged: -11644473600000,
+ });
+ await Assert.rejects(
+ Services.logins.addLoginAsync(timePasswordChangedLogin),
+ /Can\'t add a login with invalid date properties./
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+});