summaryrefslogtreecommitdiffstats
path: root/services/sync/tests/unit/test_password_engine.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/sync/tests/unit/test_password_engine.js1257
1 files changed, 1257 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_password_engine.js b/services/sync/tests/unit/test_password_engine.js
new file mode 100644
index 0000000000..081403f63d
--- /dev/null
+++ b/services/sync/tests/unit/test_password_engine.js
@@ -0,0 +1,1257 @@
+const { FXA_PWDMGR_HOST, FXA_PWDMGR_REALM } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsCommon.sys.mjs"
+);
+const { LoginRec } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/passwords.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+
+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(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+
+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(
+ "https://example.com",
+ "",
+ 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(
+ "https://example.com",
+ "",
+ 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(
+ "https://mozilla.com",
+ "",
+ 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(
+ "http://mozilla.com",
+ "http://mozilla.com",
+ 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");
+ ok(
+ 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",
+ hostname: "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({
+ origin: "https://www.example.com",
+ });
+ 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({
+ origin: "https://www.example.com",
+ });
+ equal(logins.length, 1);
+
+ details.password = "alpaca";
+ equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
+ checkFields.forEach(field => {
+ equal(logins[0][field], details[field]);
+ });
+ ok(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({
+ origin: "https://www.example.com",
+ });
+ equal(logins.length, 1);
+
+ details.username = "guanaco";
+ equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1);
+ checkFields.forEach(field => {
+ equal(logins[0][field], details[field]);
+ });
+ ok(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({
+ origin: "https://www.example.com",
+ });
+ 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",
+ hostname: "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({
+ origin: "https://www.example.com",
+ });
+ 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(
+ "http://mozilla.com",
+ "http://mozilla.com",
+ 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(
+ "http://www.mozilla.com",
+ "http://www.mozilla.com",
+ 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({
+ origin: "http://www.mozilla.com",
+ });
+ 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({
+ origin: "http://www.mozilla.com",
+ });
+ 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: "",
+ hostname: "https://www.example.com",
+ 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({
+ origin: "https://www.example.com",
+ 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",
+ hostname: "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({
+ origin: "https://www.example.com",
+ });
+
+ 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 = {
+ formSubmitURL: "https://www.nullupdateexample.com",
+ hostname: "https://www.nullupdateexample.com",
+ httpRealm: null,
+ username: null,
+ password: "bar",
+ usernameField: "username-field",
+ passwordField: "password-field",
+ timeCreated: Date.now(),
+ timePasswordChanged: Date.now(),
+ };
+ let localDetails = {
+ formSubmitURL: "https://www.nullupdateexample.com",
+ hostname: "https://www.nullupdateexample.com",
+ 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({
+ origin: "https://www.nullupdateexample.com",
+ });
+
+ 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 = {
+ formSubmitURL: "https://www.undefinedupdateexample.com",
+ hostname: "https://www.undefinedupdateexample.com",
+ httpRealm: null,
+ username: undefined,
+ password: "bar",
+ usernameField: "username-field",
+ passwordField: "password-field",
+ timeCreated: Date.now(),
+ timePasswordChanged: Date.now(),
+ };
+ let localDetails = {
+ formSubmitURL: "https://www.undefinedupdateexample.com",
+ hostname: "https://www.undefinedupdateexample.com",
+ 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({
+ origin: "https://www.undefinedupdateexample.com",
+ });
+
+ 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",
+ hostname: "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({
+ origin: "https://www.example.com",
+ });
+
+ 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",
+ hostname: "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({
+ origin: "https://www.example.com",
+ });
+
+ 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(
+ "https://mozilla.com",
+ "",
+ 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",
+ url: "https://example.com/path",
+ username: "exampleuser",
+ password: "examplepassword",
+ },
+ {
+ hostname: "https://mozilla.org",
+ url: "https://mozilla.org",
+ username: "mozillauser",
+ password: "mozillapassword",
+ },
+ {
+ hostname: "https://www.example.org",
+ url: "https://www.example.org/example1/example2",
+ 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);
+ }
+});