Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

const { FXA_PWDMGR_HOST, FXA_PWDMGR_REALM } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccountsCommon.sys.mjs"
);
const { LoginRec } = ChromeUtils.importESModule(
);
const { Service } = ChromeUtils.importESModule(
);
const LoginInfo = Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo,
"init"
);
const { LoginCSVImport } = ChromeUtils.importESModule(
"resource://gre/modules/LoginCSVImport.sys.mjs"
);
const { FileTestUtils } = ChromeUtils.importESModule(
);
const PropertyBag = Components.Constructor(
"@mozilla.org/hash-property-bag;1",
Ci.nsIWritablePropertyBag
);
async function cleanup(engine, server) {
await engine._tracker.stop();
await engine.wipeClient();
engine.lastModified = null;
for (const pref of Svc.PrefBranch.getChildList("")) {
Svc.PrefBranch.clearUserPref(pref);
}
Service.recordManager.clearCache();
if (server) {
await promiseStopServer(server);
}
}
add_task(async function setup() {
// Disable addon sync because AddonManager won't be initialized here.
await Service.engineManager.unregister("addons");
await Service.engineManager.unregister("extension-storage");
});
add_task(async function test_ignored_fields() {
_("Only changes to syncable fields should be tracked");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
enableValidationPrefs();
let loginInfo = new LoginInfo(
"",
null,
"username",
"password",
"",
""
);
// Setting syncCounter to -1 so that it will be incremented to 0 when added.
loginInfo.syncCounter = -1;
let login = await Services.logins.addLoginAsync(loginInfo);
login.QueryInterface(Ci.nsILoginMetaInfo); // For `guid`.
engine._tracker.start();
try {
let nonSyncableProps = new PropertyBag();
nonSyncableProps.setProperty("timeLastUsed", Date.now());
nonSyncableProps.setProperty("timesUsed", 3);
Services.logins.modifyLogin(login, nonSyncableProps);
let noChanges = await engine.pullNewChanges();
deepEqual(noChanges, {}, "Should not track non-syncable fields");
let syncableProps = new PropertyBag();
syncableProps.setProperty("username", "newuser");
Services.logins.modifyLogin(login, syncableProps);
let changes = await engine.pullNewChanges();
deepEqual(
Object.keys(changes),
[login.guid],
"Should track syncable fields"
);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_ignored_sync_credentials() {
_("Sync credentials in login manager should be ignored");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
enableValidationPrefs();
engine._tracker.start();
try {
let login = await Services.logins.addLoginAsync(
new LoginInfo(
FXA_PWDMGR_HOST,
null,
FXA_PWDMGR_REALM,
"fxa-uid",
"creds",
"",
""
)
);
let noChanges = await engine.pullNewChanges();
deepEqual(noChanges, {}, "Should not track new FxA credentials");
let props = new PropertyBag();
props.setProperty("password", "newcreds");
Services.logins.modifyLogin(login, props);
noChanges = await engine.pullNewChanges();
deepEqual(noChanges, {}, "Should not track changes to FxA credentials");
let foundLogins = await Services.logins.searchLoginsAsync({
origin: FXA_PWDMGR_HOST,
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 0);
equal(foundLogins[0].everSynced, false);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_password_engine() {
_("Basic password sync test");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
enableValidationPrefs();
_("Add new login to upload during first sync");
let newLogin;
{
let login = new LoginInfo(
"",
null,
"username",
"password",
"",
""
);
await Services.logins.addLoginAsync(login);
let logins = await Services.logins.searchLoginsAsync({
origin: "https://example.com",
});
equal(logins.length, 1, "Should find new login in login manager");
newLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
// Insert a server record that's older, so that we prefer the local one.
let rec = new LoginRec("passwords", newLogin.guid);
rec.formSubmitURL = newLogin.formActionOrigin;
rec.httpRealm = newLogin.httpRealm;
rec.hostname = newLogin.origin;
rec.username = newLogin.username;
rec.password = "sekrit";
let remotePasswordChangeTime = Date.now() - 1 * 60 * 60 * 24 * 1000;
rec.timeCreated = remotePasswordChangeTime;
rec.timePasswordChanged = remotePasswordChangeTime;
collection.insert(
newLogin.guid,
encryptPayload(rec.cleartext),
remotePasswordChangeTime / 1000
);
}
_("Add login with older password change time to replace during first sync");
let oldLogin;
{
let login = new LoginInfo(
"",
null,
"us3r",
"0ldpa55",
"",
""
);
await Services.logins.addLoginAsync(login);
let props = new PropertyBag();
let localPasswordChangeTime = Date.now() - 1 * 60 * 60 * 24 * 1000;
props.setProperty("timePasswordChanged", localPasswordChangeTime);
Services.logins.modifyLogin(login, props);
let logins = await Services.logins.searchLoginsAsync({
origin: "https://mozilla.com",
});
equal(logins.length, 1, "Should find old login in login manager");
oldLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
equal(oldLogin.timePasswordChanged, localPasswordChangeTime);
let rec = new LoginRec("passwords", oldLogin.guid);
rec.hostname = oldLogin.origin;
rec.formSubmitURL = oldLogin.formActionOrigin;
rec.httpRealm = oldLogin.httpRealm;
rec.username = oldLogin.username;
// Change the password and bump the password change time to ensure we prefer
// the remote one during reconciliation.
rec.password = "n3wpa55";
rec.usernameField = oldLogin.usernameField;
rec.passwordField = oldLogin.usernameField;
rec.timeCreated = oldLogin.timeCreated;
rec.timePasswordChanged = Date.now();
collection.insert(oldLogin.guid, encryptPayload(rec.cleartext));
}
await engine._tracker.stop();
try {
await sync_engine_and_validate_telem(engine, false);
let newRec = collection.cleartext(newLogin.guid);
equal(
newRec.password,
"password",
"Should update remote password for newer login"
);
let logins = await Services.logins.searchLoginsAsync({
origin: "https://mozilla.com",
});
equal(
logins[0].password,
"n3wpa55",
"Should update local password for older login"
);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_sync_outgoing() {
_("Test syncing outgoing records");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let loginInfo = new LoginInfo(
null,
"theuser",
"thepassword",
"username",
"password"
);
let login = await Services.logins.addLoginAsync(loginInfo);
engine._tracker.start();
try {
let foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 1);
equal(foundLogins[0].everSynced, false);
equal(collection.count(), 0);
let guid = foundLogins[0].QueryInterface(Ci.nsILoginMetaInfo).guid;
let changes = await engine.getChangedIDs();
let change = changes[guid];
equal(Object.keys(changes).length, 1);
equal(change.counter, 1);
ok(!change.deleted);
// This test modifies the password and then performs a sync and
// then ensures that the synced record is correct. This is done twice
// to ensure that syncing occurs correctly when the server record does not
// yet exist and when it does already exist.
for (let i = 1; i <= 2; i++) {
_("Modify the password iteration " + i);
foundLogins[0].password = "newpassword" + i;
Services.logins.modifyLogin(login, foundLogins[0]);
foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
equal(foundLogins.length, 1);
// On the first pass, the counter should be 2, one for the add and one for the modify.
// No sync has occurred yet so everSynced should be false.
// On the second pass, the counter will only be 1 for the modify. The everSynced
// property should be true as the sync happened on the last iteration.
equal(foundLogins[0].syncCounter, i == 2 ? 1 : 2);
equal(foundLogins[0].everSynced, i == 2);
changes = await engine.getChangedIDs();
change = changes[guid];
equal(Object.keys(changes).length, 1);
equal(change.counter, i == 2 ? 1 : 2);
ok(!change.deleted);
_("Perform sync after modifying the password");
await sync_engine_and_validate_telem(engine, false);
equal(Object.keys(await engine.getChangedIDs()), 0);
// The remote login should have the updated password.
let newRec = collection.cleartext(guid);
equal(
newRec.password,
"newpassword" + i,
"Should update remote password for login"
);
foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 0);
equal(foundLogins[0].everSynced, true);
login.password = "newpassword" + i;
}
// Next, modify the username and sync.
_("Modify the username");
foundLogins[0].username = "newuser";
Services.logins.modifyLogin(login, foundLogins[0]);
foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 1);
equal(foundLogins[0].everSynced, true);
_("Perform sync after modifying the username");
await sync_engine_and_validate_telem(engine, false);
// The remote login should have the updated password.
let newRec = collection.cleartext(guid);
equal(
newRec.username,
"newuser",
"Should update remote username for login"
);
foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 0);
equal(foundLogins[0].everSynced, true);
// Finally, remove the login. The server record should be marked as deleted.
_("Remove the login");
equal(collection.count(), 1);
equal(Services.logins.countLogins("", "", ""), 2);
equal((await Services.logins.getAllLogins()).length, 2);
ok(await engine._store.itemExists(guid));
ok((await engine._store.getAllIDs())[guid]);
Services.logins.removeLogin(foundLogins[0]);
foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
equal(foundLogins.length, 0);
changes = await engine.getChangedIDs();
change = changes[guid];
equal(Object.keys(changes).length, 1);
equal(change.counter, 1);
ok(change.deleted);
_("Perform sync after removing the login");
await sync_engine_and_validate_telem(engine, false);
equal(collection.count(), 1);
let payload = collection.payloads()[0];
ok(payload.deleted);
equal(Object.keys(await engine.getChangedIDs()), 0);
// All of these should not include the deleted login. Only the FxA password should exist.
equal(Services.logins.countLogins("", "", ""), 1);
equal((await Services.logins.getAllLogins()).length, 1);
ok(!(await engine._store.itemExists(guid)));
// getAllIDs includes deleted items but skips the FxA login.
ok((await engine._store.getAllIDs())[guid]);
let deletedLogin = await engine._store._getLoginFromGUID(guid);
equal(deletedLogin.hostname, null, "deleted login hostname");
equal(
deletedLogin.formActionOrigin,
null,
"deleted login formActionOrigin"
);
equal(deletedLogin.formSubmitURL, null, "deleted login formSubmitURL");
equal(deletedLogin.httpRealm, null, "deleted login httpRealm");
equal(deletedLogin.username, null, "deleted login username");
equal(deletedLogin.password, null, "deleted login password");
equal(deletedLogin.usernameField, "", "deleted login usernameField");
equal(deletedLogin.passwordField, "", "deleted login passwordField");
equal(deletedLogin.unknownFields, null, "deleted login unknownFields");
equal(deletedLogin.timeCreated, 0, "deleted login timeCreated");
equal(deletedLogin.timeLastUsed, 0, "deleted login timeLastUsed");
equal(deletedLogin.timesUsed, 0, "deleted login timesUsed");
// These fields are not reset when the login is removed.
equal(deletedLogin.guid, guid, "deleted login guid");
equal(deletedLogin.everSynced, true, "deleted login everSynced");
equal(deletedLogin.syncCounter, 0, "deleted login syncCounter");
Assert.greater(
deletedLogin.timePasswordChanged,
0,
"deleted login timePasswordChanged"
);
} finally {
await engine._tracker.stop();
await cleanup(engine, server);
}
});
add_task(async function test_sync_incoming() {
_("Test syncing incoming records");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
const checkFields = [
"formSubmitURL",
"hostname",
"httpRealm",
"username",
"password",
"usernameField",
"passwordField",
"timeCreated",
];
let guid1 = Utils.makeGUID();
let details = {
formSubmitURL: "https://www.example.com",
httpRealm: null,
username: "camel",
password: "llama",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
try {
// This test creates a remote server record and then verifies that the login
// has been added locally after the sync occurs.
_("Create remote login");
collection.insertRecord(Object.assign({}, details, { id: guid1 }));
_("Perform sync when remote login has been added");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
checkFields.forEach(field => {
equal(logins[0][field], details[field]);
});
equal(logins[0].timePasswordChanged, details.timePasswordChanged);
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
// Modify the password within the remote record and then sync again.
_("Perform sync when remote login's password has been modified");
let newTime = Date.now();
collection.updateRecord(
guid1,
cleartext => {
cleartext.password = "alpaca";
},
newTime / 1000 + 10
);
await engine.setLastSync(newTime / 1000 - 30);
await sync_engine_and_validate_telem(engine, false);
logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
details.password = "alpaca";
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
checkFields.forEach(field => {
equal(logins[0][field], details[field]);
});
Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged);
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
// Modify the username within the remote record and then sync again.
_("Perform sync when remote login's username has been modified");
newTime = Date.now();
collection.updateRecord(
guid1,
cleartext => {
cleartext.username = "guanaco";
},
newTime / 1000 + 10
);
await engine.setLastSync(newTime / 1000 - 30);
await sync_engine_and_validate_telem(engine, false);
logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
details.username = "guanaco";
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
checkFields.forEach(field => {
equal(logins[0][field], details[field]);
});
Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged);
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
// Mark the remote record as deleted and then sync again.
_("Perform sync when remote login has been marked for deletion");
newTime = Date.now();
collection.updateRecord(
guid1,
cleartext => {
cleartext.deleted = true;
},
newTime / 1000 + 10
);
await engine.setLastSync(newTime / 1000 - 30);
await sync_engine_and_validate_telem(engine, false);
logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 0);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_sync_incoming_deleted() {
_("Test syncing incoming deleted records");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let guid1 = Utils.makeGUID();
let details2 = {
formSubmitURL: "https://www.example.org",
httpRealm: null,
username: "capybara",
password: "beaver",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
deleted: true,
};
try {
// This test creates a remote server record that has been deleted
// and then verifies that the login is not imported locally.
_("Create remote login");
collection.insertRecord(Object.assign({}, details2, { id: guid1 }));
_("Perform sync when remote login has been deleted");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 0);
ok(!(await engine._store.getAllIDs())[guid1]);
ok(!(await engine._store.itemExists(guid1)));
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_sync_incoming_deleted_localchanged_remotenewer() {
_(
"Test syncing incoming deleted records where the local login has been changed but the remote record is newer"
);
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let loginInfo = new LoginInfo(
null,
"kangaroo",
"kaola",
"username",
"password"
);
let login = await Services.logins.addLoginAsync(loginInfo);
let guid = login.QueryInterface(Ci.nsILoginMetaInfo).guid;
try {
_("Perform sync on new login");
await sync_engine_and_validate_telem(engine, false);
let foundLogins = await Services.logins.searchLoginsAsync({
origin: "http://mozilla.com",
});
foundLogins[0].password = "wallaby";
Services.logins.modifyLogin(login, foundLogins[0]);
// Use a time in the future to ensure that the remote record is newer.
collection.updateRecord(
guid,
cleartext => {
cleartext.deleted = true;
},
Date.now() / 1000 + 1000
);
_(
"Perform sync when remote login has been deleted and local login has been changed"
);
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
origin: "https://mozilla.com",
});
equal(logins.length, 0);
ok(await engine._store.getAllIDs());
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_sync_incoming_deleted_localchanged_localnewer() {
_(
"Test syncing incoming deleted records where the local login has been changed but the local record is newer"
);
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let loginInfo = new LoginInfo(
null,
"lion",
"tiger",
"username",
"password"
);
let login = await Services.logins.addLoginAsync(loginInfo);
let guid = login.QueryInterface(Ci.nsILoginMetaInfo).guid;
try {
_("Perform sync on new login");
await sync_engine_and_validate_telem(engine, false);
let foundLogins = await Services.logins.searchLoginsAsync({
});
foundLogins[0].password = "cheetah";
Services.logins.modifyLogin(login, foundLogins[0]);
// Use a time in the past to ensure that the local record is newer.
collection.updateRecord(
guid,
cleartext => {
cleartext.deleted = true;
},
Date.now() / 1000 - 1000
);
_(
"Perform sync when remote login has been deleted and local login has been changed"
);
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
equal(logins[0].password, "cheetah");
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
ok(await engine._store.getAllIDs());
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_sync_incoming_no_formactionorigin() {
_("Test syncing incoming a record where there is no formActionOrigin");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
const checkFields = [
"formSubmitURL",
"hostname",
"httpRealm",
"username",
"password",
"usernameField",
"passwordField",
"timeCreated",
];
let guid1 = Utils.makeGUID();
let details = {
formSubmitURL: "",
httpRealm: null,
username: "rabbit",
password: "squirrel",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
try {
// This test creates a remote server record and then verifies that the login
// has been added locally after the sync occurs.
_("Create remote login");
collection.insertRecord(Object.assign({}, details, { id: guid1 }));
_("Perform sync when remote login has been added");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
formActionOrigin: "",
});
equal(logins.length, 1);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
checkFields.forEach(field => {
equal(logins[0][field], details[field]);
});
equal(logins[0].timePasswordChanged, details.timePasswordChanged);
equal(logins[0].syncCounter, 0);
equal(logins[0].everSynced, true);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_password_dupe() {
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let guid1 = Utils.makeGUID();
let rec1 = new LoginRec("passwords", guid1);
let guid2 = Utils.makeGUID();
let cleartext = {
formSubmitURL: "https://www.example.com",
httpRealm: null,
username: "foo",
password: "bar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Math.round(Date.now()),
timePasswordChanged: Math.round(Date.now()),
};
rec1.cleartext = cleartext;
_("Create remote record with same details and guid1");
collection.insert(guid1, encryptPayload(rec1.cleartext));
_("Create remote record with guid2");
collection.insert(guid2, encryptPayload(cleartext));
_("Create local record with same details and guid1");
await engine._store.create(rec1);
try {
_("Perform sync");
await sync_engine_and_validate_telem(engine, true);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid2);
equal(null, collection.payload(guid1));
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_updated_null_password_sync() {
_("Ensure updated null login username is converted to a string");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let guid1 = Utils.makeGUID();
let guid2 = Utils.makeGUID();
let remoteDetails = {
httpRealm: null,
username: null,
password: "bar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
let localDetails = {
httpRealm: null,
username: "foo",
password: "foobar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
_("Create remote record with same details and guid1");
collection.insertRecord(Object.assign({}, remoteDetails, { id: guid1 }));
try {
_("Create local updated login with null password");
await engine._store.update(Object.assign({}, localDetails, { id: guid2 }));
_("Perform sync");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_updated_undefined_password_sync() {
_("Ensure updated undefined login username is converted to a string");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let guid1 = Utils.makeGUID();
let guid2 = Utils.makeGUID();
let remoteDetails = {
httpRealm: null,
username: undefined,
password: "bar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
let localDetails = {
httpRealm: null,
username: "foo",
password: "foobar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
_("Create remote record with same details and guid1");
collection.insertRecord(Object.assign({}, remoteDetails, { id: guid1 }));
try {
_("Create local updated login with undefined password");
await engine._store.update(Object.assign({}, localDetails, { id: guid2 }));
_("Perform sync");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_new_null_password_sync() {
_("Ensure new null login username is converted to a string");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let guid1 = Utils.makeGUID();
let rec1 = new LoginRec("passwords", guid1);
rec1.cleartext = {
formSubmitURL: "https://www.example.com",
httpRealm: null,
username: null,
password: "bar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
try {
_("Create local login with null password");
await engine._store.create(rec1);
_("Perform sync");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, null);
notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, undefined);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, "");
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_new_undefined_password_sync() {
_("Ensure new undefined login username is converted to a string");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let guid1 = Utils.makeGUID();
let rec1 = new LoginRec("passwords", guid1);
rec1.cleartext = {
formSubmitURL: "https://www.example.com",
httpRealm: null,
username: undefined,
password: "bar",
usernameField: "username-field",
passwordField: "password-field",
timeCreated: Date.now(),
timePasswordChanged: Date.now(),
};
try {
_("Create local login with undefined password");
await engine._store.create(rec1);
_("Perform sync");
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
});
equal(logins.length, 1);
notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, null);
notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, undefined);
equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, "");
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_sync_password_validation() {
// This test isn't in test_password_validator to avoid duplicating cleanup.
_("Ensure that if a password validation happens, it ends up in the ping");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
Svc.PrefBranch.setIntPref("engine.passwords.validation.interval", 0);
Svc.PrefBranch.setIntPref(
"engine.passwords.validation.percentageChance",
100
);
Svc.PrefBranch.setIntPref("engine.passwords.validation.maxRecords", -1);
Svc.PrefBranch.setBoolPref("engine.passwords.validation.enabled", true);
try {
let ping = await wait_for_ping(() => Service.sync());
let engineInfo = ping.engines.find(e => e.name == "passwords");
ok(engineInfo, "Engine should be in ping");
let validation = engineInfo.validation;
ok(validation, "Engine should have validation info");
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_roundtrip_unknown_fields() {
_(
"Testing that unknown fields from other clients get roundtripped back to server"
);
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
enableValidationPrefs();
_("Add login with older password change time to replace during first sync");
let oldLogin;
{
let login = new LoginInfo(
"",
null,
"us3r",
"0ldpa55",
"",
""
);
await Services.logins.addLoginAsync(login);
let props = new PropertyBag();
let localPasswordChangeTime = Math.round(
Date.now() - 1 * 60 * 60 * 24 * 1000
);
props.setProperty("timePasswordChanged", localPasswordChangeTime);
Services.logins.modifyLogin(login, props);
let logins = await Services.logins.searchLoginsAsync({
origin: "https://mozilla.com",
});
equal(logins.length, 1, "Should find old login in login manager");
oldLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
equal(oldLogin.timePasswordChanged, localPasswordChangeTime);
let rec = new LoginRec("passwords", oldLogin.guid);
rec.hostname = oldLogin.origin;
rec.formSubmitURL = oldLogin.formActionOrigin;
rec.httpRealm = oldLogin.httpRealm;
rec.username = oldLogin.username;
// Change the password and bump the password change time to ensure we prefer
// the remote one during reconciliation.
rec.password = "n3wpa55";
rec.usernameField = oldLogin.usernameField;
rec.passwordField = oldLogin.usernameField;
rec.timeCreated = oldLogin.timeCreated;
rec.timePasswordChanged = Math.round(Date.now());
// pretend other clients have some snazzy new fields
// we don't quite understand yet
rec.cleartext.someStrField = "I am a str";
rec.cleartext.someObjField = { newField: "I am a new field" };
collection.insert(oldLogin.guid, encryptPayload(rec.cleartext));
}
await engine._tracker.stop();
try {
await sync_engine_and_validate_telem(engine, false);
let logins = await Services.logins.searchLoginsAsync({
origin: "https://mozilla.com",
});
equal(
logins[0].password,
"n3wpa55",
"Should update local password for older login"
);
let expectedUnknowns = JSON.stringify({
someStrField: "I am a str",
someObjField: { newField: "I am a new field" },
});
// Check that the local record has all unknown fields properly
// stringified
equal(logins[0].unknownFields, expectedUnknowns);
// Check that the server has the unknown fields unfurled and on the
// top-level record
let serverRec = collection.cleartext(oldLogin.guid);
equal(serverRec.someStrField, "I am a str");
equal(serverRec.someObjField.newField, "I am a new field");
} finally {
await cleanup(engine, server);
}
});
add_task(async function test_new_passwords_from_csv() {
_("Test syncing records imported from a csv file");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
engine._tracker.start();
let data = [
{
hostname: "https://example.com",
username: "exampleuser",
password: "examplepassword",
},
{
hostname: "https://mozilla.org",
username: "mozillauser",
password: "mozillapassword",
},
{
username: "person",
password: "mypassword",
},
];
let csvData = ["url,username,login_password"];
for (let row of data) {
csvData.push(row.url + "," + row.username + "," + row.password);
}
let csvFile = FileTestUtils.getTempFile(`firefox_logins.csv`);
await IOUtils.writeUTF8(csvFile.path, csvData.join("\r\n"));
await LoginCSVImport.importFromCSV(csvFile.path);
equal(
engine._tracker.score,
SCORE_INCREMENT_XLARGE,
"Should only get one update notification for import"
);
_("Ensure that the csv import is correct");
for (let item of data) {
let foundLogins = await Services.logins.searchLoginsAsync({
origin: item.hostname,
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 1);
equal(foundLogins[0].everSynced, false);
equal(foundLogins[0].username, item.username);
equal(foundLogins[0].password, item.password);
}
_("Perform sync after modifying the password");
await sync_engine_and_validate_telem(engine, false);
_("Verify that the sync counter and status are updated");
for (let item of data) {
let foundLogins = await Services.logins.searchLoginsAsync({
origin: item.hostname,
});
equal(foundLogins.length, 1);
equal(foundLogins[0].syncCounter, 0);
equal(foundLogins[0].everSynced, true);
equal(foundLogins[0].username, item.username);
equal(foundLogins[0].password, item.password);
item.guid = foundLogins[0].guid;
}
equal(Object.keys(await engine.getChangedIDs()), 0);
equal(collection.count(), 3);
for (let item of data) {
// The remote login should have the imported username and password.
let newRec = collection.cleartext(item.guid);
equal(newRec.username, item.username);
equal(newRec.password, item.password);
}
});