Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
/**
* 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
);
await 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.
await 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());
await 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);
}
await 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.
*
* 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 === 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();
await 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.
await 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);
await LoginTestUtils.checkLogins([differentLoginInfo]);
// It is now possible to add a login with the old type and origin.
await Services.logins.addLoginAsync(loginInfo);
await 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/
);
await 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.
await 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);
await LoginTestUtils.checkLogins([differentLoginInfo]);
// It is now possible to add a login with the old type and origin.
await Services.logins.addLoginAsync(loginInfo);
await 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/
);
await 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((await Services.logins.getAllLogins()).length, 0);
}
await 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();
});