/* -*- 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 methods that add, remove, and modify logins. */ "use strict"; // Globals /** * 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. */ function checkLoginInvalid(aLoginInfo, aExpectedError) { // Try to add the new login, and verify that no data is stored. Assert.throws(() => Services.logins.addLogin(aLoginInfo), aExpectedError); LoginTestUtils.checkLogins([]); // Add a login for the modification tests. let testLogin = TestData.formLogin({ hostname: "http://modify.example.com" }); Services.logins.addLogin(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({ hostname: aLoginInfo.hostname, formSubmitURL: aLoginInfo.formSubmitURL, 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(function test_addLogin_removeLogin() { // Each login from the test data should be valid and added to the list. for (let loginInfo of TestData.loginList()) { Services.logins.addLogin(loginInfo); } LoginTestUtils.checkLogins(TestData.loginList()); // Trying to add each login again should result in an error. for (let loginInfo of TestData.loginList()) { Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/); } // Removing each login should succeed. for (let loginInfo of TestData.loginList()) { Services.logins.removeLogin(loginInfo); } LoginTestUtils.checkLogins([]); }); /** * Tests invalid combinations of httpRealm and formSubmitURL. * * 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 formSubmitURL and a null value in * httpRealm is also supported for storage at the moment. */ add_task(function test_invalid_httpRealm_formSubmitURL() { // httpRealm === null, formSubmitURL === null checkLoginInvalid(TestData.formLogin({ formSubmitURL: null }), /without a httpRealm or formSubmitURL/); // httpRealm === "", formSubmitURL === null checkLoginInvalid(TestData.authLogin({ httpRealm: "" }), /without a httpRealm or formSubmitURL/); // httpRealm === null, formSubmitURL === "" // This is not enforced for now. // checkLoginInvalid(TestData.formLogin({ formSubmitURL: "" }), // /without a httpRealm or formSubmitURL/); // httpRealm === "", formSubmitURL === "" checkLoginInvalid(TestData.formLogin({ formSubmitURL: "", httpRealm: "" }), /both a httpRealm and formSubmitURL/); // !!httpRealm, !!formSubmitURL checkLoginInvalid(TestData.formLogin({ httpRealm: "The HTTP Realm" }), /both a httpRealm and formSubmitURL/); // httpRealm === "", !!formSubmitURL checkLoginInvalid(TestData.formLogin({ httpRealm: "" }), /both a httpRealm and formSubmitURL/); // !!httpRealm, formSubmitURL === "" checkLoginInvalid(TestData.authLogin({ formSubmitURL: "" }), /both a httpRealm and formSubmitURL/); }); /** * Tests null or empty values in required login properties. */ add_task(function test_missing_properties() { checkLoginInvalid(TestData.formLogin({ hostname: null }), /null or empty hostname/); checkLoginInvalid(TestData.formLogin({ hostname: "" }), /null or empty hostname/); checkLoginInvalid(TestData.formLogin({ username: null }), /null username/); checkLoginInvalid(TestData.formLogin({ password: null }), /null or empty password/); checkLoginInvalid(TestData.formLogin({ password: "" }), /null or empty password/); }); /** * Tests invalid NUL characters in nsILoginInfo properties. */ add_task(function test_invalid_characters() { let loginList = [ TestData.authLogin({ hostname: "http://null\0X.example.com" }), TestData.authLogin({ httpRealm: "realm\0" }), TestData.formLogin({ formSubmitURL: "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) { 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(function test_removeAllLogins() { for (let loginInfo of TestData.loginList()) { Services.logins.addLogin(loginInfo); } Services.logins.removeAllLogins(); LoginTestUtils.checkLogins([]); // The function should also work when there are no logins to delete. Services.logins.removeAllLogins(); }); /** * Tests the modifyLogin function with an nsILoginInfo argument. */ add_task(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. Services.logins.addLogin(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 hostname. Services.logins.modifyLogin(updatedLoginInfo, differentLoginInfo); LoginTestUtils.checkLogins([differentLoginInfo]); // It is now possible to add a login with the old type and hostname. Services.logins.addLogin(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(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({ hostname: differentLoginInfo.hostname, formSubmitURL: differentLoginInfo.formSubmitURL, 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. Services.logins.addLogin(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, }))); // The login can be changed to have a different type and hostname. Services.logins.modifyLogin(updatedLoginInfo, differentLoginProperties); LoginTestUtils.checkLogins([differentLoginInfo]); // It is now possible to add a login with the old type and hostname. Services.logins.addLogin(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: 13, }, { keyset: ["hostname", "username"], results: 17, }, { keyset: ["hostname", "username", "password"], results: 18, }, { keyset: ["hostname", "username", "password", "formSubmitURL"], results: 23, }, ]; 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({formSubmitURL: "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."); });