test_password_engine.js (38728B)
1 const { FXA_PWDMGR_HOST, FXA_PWDMGR_REALM } = ChromeUtils.importESModule( 2 "resource://gre/modules/FxAccountsCommon.sys.mjs" 3 ); 4 const { LoginRec } = ChromeUtils.importESModule( 5 "resource://services-sync/engines/passwords.sys.mjs" 6 ); 7 const { Service } = ChromeUtils.importESModule( 8 "resource://services-sync/service.sys.mjs" 9 ); 10 11 const LoginInfo = Components.Constructor( 12 "@mozilla.org/login-manager/loginInfo;1", 13 Ci.nsILoginInfo, 14 "init" 15 ); 16 17 const { LoginCSVImport } = ChromeUtils.importESModule( 18 "resource://gre/modules/LoginCSVImport.sys.mjs" 19 ); 20 21 const { FileTestUtils } = ChromeUtils.importESModule( 22 "resource://testing-common/FileTestUtils.sys.mjs" 23 ); 24 25 const PropertyBag = Components.Constructor( 26 "@mozilla.org/hash-property-bag;1", 27 Ci.nsIWritablePropertyBag 28 ); 29 30 async function cleanup(engine, server) { 31 await engine._tracker.stop(); 32 await engine.wipeClient(); 33 engine.lastModified = null; 34 for (const pref of Svc.PrefBranch.getChildList("")) { 35 Svc.PrefBranch.clearUserPref(pref); 36 } 37 Service.recordManager.clearCache(); 38 if (server) { 39 await promiseStopServer(server); 40 } 41 } 42 43 add_task(async function setup() { 44 // Disable addon sync because AddonManager won't be initialized here. 45 await Service.engineManager.unregister("addons"); 46 await Service.engineManager.unregister("extension-storage"); 47 }); 48 49 add_task(async function test_ignored_fields() { 50 _("Only changes to syncable fields should be tracked"); 51 52 let engine = Service.engineManager.get("passwords"); 53 54 let server = await serverForFoo(engine); 55 await SyncTestingInfrastructure(server); 56 57 enableValidationPrefs(); 58 59 let loginInfo = new LoginInfo( 60 "https://example.com", 61 "", 62 null, 63 "username", 64 "password", 65 "", 66 "" 67 ); 68 69 // Setting syncCounter to -1 so that it will be incremented to 0 when added. 70 loginInfo.syncCounter = -1; 71 let login = await Services.logins.addLoginAsync(loginInfo); 72 login.QueryInterface(Ci.nsILoginMetaInfo); // For `guid`. 73 74 engine._tracker.start(); 75 76 try { 77 let nonSyncableProps = new PropertyBag(); 78 nonSyncableProps.setProperty("timeLastUsed", Date.now()); 79 nonSyncableProps.setProperty("timesUsed", 3); 80 await Services.logins.modifyLoginAsync(login, nonSyncableProps); 81 82 let noChanges = await engine.pullNewChanges(); 83 deepEqual(noChanges, {}, "Should not track non-syncable fields"); 84 85 let syncableProps = new PropertyBag(); 86 syncableProps.setProperty("username", "newuser"); 87 await Services.logins.modifyLoginAsync(login, syncableProps); 88 89 let changes = await engine.pullNewChanges(); 90 deepEqual( 91 Object.keys(changes), 92 [login.guid], 93 "Should track syncable fields" 94 ); 95 } finally { 96 await cleanup(engine, server); 97 } 98 }); 99 100 add_task(async function test_ignored_sync_credentials() { 101 _("Sync credentials in login manager should be ignored"); 102 103 let engine = Service.engineManager.get("passwords"); 104 105 let server = await serverForFoo(engine); 106 await SyncTestingInfrastructure(server); 107 108 enableValidationPrefs(); 109 110 engine._tracker.start(); 111 112 try { 113 let login = await Services.logins.addLoginAsync( 114 new LoginInfo( 115 FXA_PWDMGR_HOST, 116 null, 117 FXA_PWDMGR_REALM, 118 "fxa-uid", 119 "creds", 120 "", 121 "" 122 ) 123 ); 124 125 let noChanges = await engine.pullNewChanges(); 126 deepEqual(noChanges, {}, "Should not track new FxA credentials"); 127 128 let props = new PropertyBag(); 129 props.setProperty("password", "newcreds"); 130 await Services.logins.modifyLoginAsync(login, props); 131 132 noChanges = await engine.pullNewChanges(); 133 deepEqual(noChanges, {}, "Should not track changes to FxA credentials"); 134 135 let foundLogins = await Services.logins.searchLoginsAsync({ 136 origin: FXA_PWDMGR_HOST, 137 }); 138 equal(foundLogins.length, 1); 139 equal(foundLogins[0].syncCounter, 0); 140 equal(foundLogins[0].everSynced, false); 141 } finally { 142 await cleanup(engine, server); 143 } 144 }); 145 146 add_task(async function test_password_engine() { 147 _("Basic password sync test"); 148 149 let engine = Service.engineManager.get("passwords"); 150 151 let server = await serverForFoo(engine); 152 await SyncTestingInfrastructure(server); 153 let collection = server.user("foo").collection("passwords"); 154 155 enableValidationPrefs(); 156 157 _("Add new login to upload during first sync"); 158 let newLogin; 159 { 160 let login = new LoginInfo( 161 "https://example.com", 162 "", 163 null, 164 "username", 165 "password", 166 "", 167 "" 168 ); 169 await Services.logins.addLoginAsync(login); 170 171 let logins = await Services.logins.searchLoginsAsync({ 172 origin: "https://example.com", 173 }); 174 equal(logins.length, 1, "Should find new login in login manager"); 175 newLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo); 176 177 // Insert a server record that's older, so that we prefer the local one. 178 let rec = new LoginRec("passwords", newLogin.guid); 179 rec.formSubmitURL = newLogin.formActionOrigin; 180 rec.httpRealm = newLogin.httpRealm; 181 rec.hostname = newLogin.origin; 182 rec.username = newLogin.username; 183 rec.password = "sekrit"; 184 let remotePasswordChangeTime = Date.now() - 1 * 60 * 60 * 24 * 1000; 185 rec.timeCreated = remotePasswordChangeTime; 186 rec.timePasswordChanged = remotePasswordChangeTime; 187 collection.insert( 188 newLogin.guid, 189 encryptPayload(rec.cleartext), 190 remotePasswordChangeTime / 1000 191 ); 192 } 193 194 _("Add login with older password change time to replace during first sync"); 195 let oldLogin; 196 { 197 let login = new LoginInfo( 198 "https://mozilla.com", 199 "", 200 null, 201 "us3r", 202 "0ldpa55", 203 "", 204 "" 205 ); 206 await Services.logins.addLoginAsync(login); 207 208 let props = new PropertyBag(); 209 let localPasswordChangeTime = Date.now() - 1 * 60 * 60 * 24 * 1000; 210 props.setProperty("timePasswordChanged", localPasswordChangeTime); 211 await Services.logins.modifyLoginAsync(login, props); 212 213 let logins = await Services.logins.searchLoginsAsync({ 214 origin: "https://mozilla.com", 215 }); 216 equal(logins.length, 1, "Should find old login in login manager"); 217 oldLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo); 218 equal(oldLogin.timePasswordChanged, localPasswordChangeTime); 219 220 let rec = new LoginRec("passwords", oldLogin.guid); 221 rec.hostname = oldLogin.origin; 222 rec.formSubmitURL = oldLogin.formActionOrigin; 223 rec.httpRealm = oldLogin.httpRealm; 224 rec.username = oldLogin.username; 225 // Change the password and bump the password change time to ensure we prefer 226 // the remote one during reconciliation. 227 rec.password = "n3wpa55"; 228 rec.usernameField = oldLogin.usernameField; 229 rec.passwordField = oldLogin.usernameField; 230 rec.timeCreated = oldLogin.timeCreated; 231 rec.timePasswordChanged = Date.now(); 232 collection.insert(oldLogin.guid, encryptPayload(rec.cleartext)); 233 } 234 235 await engine._tracker.stop(); 236 237 try { 238 await sync_engine_and_validate_telem(engine, false); 239 240 let newRec = collection.cleartext(newLogin.guid); 241 equal( 242 newRec.password, 243 "password", 244 "Should update remote password for newer login" 245 ); 246 247 let logins = await Services.logins.searchLoginsAsync({ 248 origin: "https://mozilla.com", 249 }); 250 equal( 251 logins[0].password, 252 "n3wpa55", 253 "Should update local password for older login" 254 ); 255 } finally { 256 await cleanup(engine, server); 257 } 258 }); 259 260 add_task(async function test_sync_outgoing() { 261 _("Test syncing outgoing records"); 262 263 let engine = Service.engineManager.get("passwords"); 264 265 let server = await serverForFoo(engine); 266 await SyncTestingInfrastructure(server); 267 268 let collection = server.user("foo").collection("passwords"); 269 270 let loginInfo = new LoginInfo( 271 "http://mozilla.com", 272 "http://mozilla.com", 273 null, 274 "theuser", 275 "thepassword", 276 "username", 277 "password" 278 ); 279 let login = await Services.logins.addLoginAsync(loginInfo); 280 281 engine._tracker.start(); 282 283 try { 284 let foundLogins = await Services.logins.searchLoginsAsync({ 285 origin: "http://mozilla.com", 286 }); 287 equal(foundLogins.length, 1); 288 equal(foundLogins[0].syncCounter, 1); 289 equal(foundLogins[0].everSynced, false); 290 equal(collection.count(), 0); 291 292 let guid = foundLogins[0].QueryInterface(Ci.nsILoginMetaInfo).guid; 293 294 let changes = await engine.getChangedIDs(); 295 let change = changes[guid]; 296 equal(Object.keys(changes).length, 1); 297 equal(change.counter, 1); 298 ok(!change.deleted); 299 300 // This test modifies the password and then performs a sync and 301 // then ensures that the synced record is correct. This is done twice 302 // to ensure that syncing occurs correctly when the server record does not 303 // yet exist and when it does already exist. 304 for (let i = 1; i <= 2; i++) { 305 _("Modify the password iteration " + i); 306 foundLogins[0].password = "newpassword" + i; 307 await Services.logins.modifyLoginAsync(login, foundLogins[0]); 308 foundLogins = await Services.logins.searchLoginsAsync({ 309 origin: "http://mozilla.com", 310 }); 311 equal(foundLogins.length, 1); 312 // On the first pass, the counter should be 2, one for the add and one for the modify. 313 // No sync has occurred yet so everSynced should be false. 314 // On the second pass, the counter will only be 1 for the modify. The everSynced 315 // property should be true as the sync happened on the last iteration. 316 equal(foundLogins[0].syncCounter, i == 2 ? 1 : 2); 317 equal(foundLogins[0].everSynced, i == 2); 318 319 changes = await engine.getChangedIDs(); 320 change = changes[guid]; 321 equal(Object.keys(changes).length, 1); 322 equal(change.counter, i == 2 ? 1 : 2); 323 ok(!change.deleted); 324 325 _("Perform sync after modifying the password"); 326 await sync_engine_and_validate_telem(engine, false); 327 328 equal(Object.keys(await engine.getChangedIDs()), 0); 329 330 // The remote login should have the updated password. 331 let newRec = collection.cleartext(guid); 332 equal( 333 newRec.password, 334 "newpassword" + i, 335 "Should update remote password for login" 336 ); 337 338 foundLogins = await Services.logins.searchLoginsAsync({ 339 origin: "http://mozilla.com", 340 }); 341 equal(foundLogins.length, 1); 342 equal(foundLogins[0].syncCounter, 0); 343 equal(foundLogins[0].everSynced, true); 344 345 login.password = "newpassword" + i; 346 } 347 348 // Next, modify the username and sync. 349 _("Modify the username"); 350 foundLogins[0].username = "newuser"; 351 await Services.logins.modifyLoginAsync(login, foundLogins[0]); 352 foundLogins = await Services.logins.searchLoginsAsync({ 353 origin: "http://mozilla.com", 354 }); 355 equal(foundLogins.length, 1); 356 equal(foundLogins[0].syncCounter, 1); 357 equal(foundLogins[0].everSynced, true); 358 359 _("Perform sync after modifying the username"); 360 await sync_engine_and_validate_telem(engine, false); 361 362 // The remote login should have the updated password. 363 let newRec = collection.cleartext(guid); 364 equal( 365 newRec.username, 366 "newuser", 367 "Should update remote username for login" 368 ); 369 370 foundLogins = await Services.logins.searchLoginsAsync({ 371 origin: "http://mozilla.com", 372 }); 373 equal(foundLogins.length, 1); 374 equal(foundLogins[0].syncCounter, 0); 375 equal(foundLogins[0].everSynced, true); 376 377 // Finally, remove the login. The server record should be marked as deleted. 378 _("Remove the login"); 379 equal(collection.count(), 1); 380 equal(Services.logins.countLogins("", "", ""), 2); 381 equal((await Services.logins.getAllLogins()).length, 2); 382 ok(await engine._store.itemExists(guid)); 383 384 ok((await engine._store.getAllIDs())[guid]); 385 386 Services.logins.removeLogin(foundLogins[0]); 387 foundLogins = await Services.logins.searchLoginsAsync({ 388 origin: "http://mozilla.com", 389 }); 390 equal(foundLogins.length, 0); 391 392 changes = await engine.getChangedIDs(); 393 change = changes[guid]; 394 equal(Object.keys(changes).length, 1); 395 equal(change.counter, 1); 396 ok(change.deleted); 397 398 _("Perform sync after removing the login"); 399 await sync_engine_and_validate_telem(engine, false); 400 401 equal(collection.count(), 1); 402 let payload = collection.payloads()[0]; 403 ok(payload.deleted); 404 405 equal(Object.keys(await engine.getChangedIDs()), 0); 406 407 // All of these should not include the deleted login. Only the FxA password should exist. 408 equal(Services.logins.countLogins("", "", ""), 1); 409 equal((await Services.logins.getAllLogins()).length, 1); 410 ok(!(await engine._store.itemExists(guid))); 411 412 // getAllIDs includes deleted items but skips the FxA login. 413 ok((await engine._store.getAllIDs())[guid]); 414 let deletedLogin = await engine._store._getLoginFromGUID(guid); 415 416 equal(deletedLogin.hostname, null, "deleted login hostname"); 417 equal( 418 deletedLogin.formActionOrigin, 419 null, 420 "deleted login formActionOrigin" 421 ); 422 equal(deletedLogin.formSubmitURL, null, "deleted login formSubmitURL"); 423 equal(deletedLogin.httpRealm, null, "deleted login httpRealm"); 424 equal(deletedLogin.username, null, "deleted login username"); 425 equal(deletedLogin.password, null, "deleted login password"); 426 equal(deletedLogin.usernameField, "", "deleted login usernameField"); 427 equal(deletedLogin.passwordField, "", "deleted login passwordField"); 428 equal(deletedLogin.unknownFields, null, "deleted login unknownFields"); 429 equal(deletedLogin.timeCreated, 0, "deleted login timeCreated"); 430 equal(deletedLogin.timeLastUsed, 0, "deleted login timeLastUsed"); 431 equal(deletedLogin.timesUsed, 0, "deleted login timesUsed"); 432 433 // These fields are not reset when the login is removed. 434 equal(deletedLogin.guid, guid, "deleted login guid"); 435 equal(deletedLogin.everSynced, true, "deleted login everSynced"); 436 equal(deletedLogin.syncCounter, 0, "deleted login syncCounter"); 437 Assert.greater( 438 deletedLogin.timePasswordChanged, 439 0, 440 "deleted login timePasswordChanged" 441 ); 442 } finally { 443 await engine._tracker.stop(); 444 445 await cleanup(engine, server); 446 } 447 }); 448 449 add_task(async function test_sync_incoming() { 450 _("Test syncing incoming records"); 451 452 let engine = Service.engineManager.get("passwords"); 453 454 let server = await serverForFoo(engine); 455 await SyncTestingInfrastructure(server); 456 457 let collection = server.user("foo").collection("passwords"); 458 459 const checkFields = [ 460 "formSubmitURL", 461 "hostname", 462 "httpRealm", 463 "username", 464 "password", 465 "usernameField", 466 "passwordField", 467 "timeCreated", 468 ]; 469 470 let guid1 = Utils.makeGUID(); 471 let details = { 472 formSubmitURL: "https://www.example.com", 473 hostname: "https://www.example.com", 474 httpRealm: null, 475 username: "camel", 476 password: "llama", 477 usernameField: "username-field", 478 passwordField: "password-field", 479 timeCreated: Date.now(), 480 timePasswordChanged: Date.now(), 481 }; 482 483 try { 484 // This test creates a remote server record and then verifies that the login 485 // has been added locally after the sync occurs. 486 _("Create remote login"); 487 collection.insertRecord(Object.assign({}, details, { id: guid1 })); 488 489 _("Perform sync when remote login has been added"); 490 await sync_engine_and_validate_telem(engine, false); 491 492 let logins = await Services.logins.searchLoginsAsync({ 493 origin: "https://www.example.com", 494 }); 495 equal(logins.length, 1); 496 497 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1); 498 checkFields.forEach(field => { 499 equal(logins[0][field], details[field]); 500 }); 501 equal(logins[0].timePasswordChanged, details.timePasswordChanged); 502 equal(logins[0].syncCounter, 0); 503 equal(logins[0].everSynced, true); 504 505 // Modify the password within the remote record and then sync again. 506 _("Perform sync when remote login's password has been modified"); 507 let newTime = Date.now(); 508 collection.updateRecord( 509 guid1, 510 cleartext => { 511 cleartext.password = "alpaca"; 512 }, 513 newTime / 1000 + 10 514 ); 515 516 await engine.setLastSync(newTime / 1000 - 30); 517 await sync_engine_and_validate_telem(engine, false); 518 519 logins = await Services.logins.searchLoginsAsync({ 520 origin: "https://www.example.com", 521 }); 522 equal(logins.length, 1); 523 524 details.password = "alpaca"; 525 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1); 526 checkFields.forEach(field => { 527 equal(logins[0][field], details[field]); 528 }); 529 Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged); 530 equal(logins[0].syncCounter, 0); 531 equal(logins[0].everSynced, true); 532 533 // Modify the username within the remote record and then sync again. 534 _("Perform sync when remote login's username has been modified"); 535 newTime = Date.now(); 536 collection.updateRecord( 537 guid1, 538 cleartext => { 539 cleartext.username = "guanaco"; 540 }, 541 newTime / 1000 + 10 542 ); 543 544 await engine.setLastSync(newTime / 1000 - 30); 545 await sync_engine_and_validate_telem(engine, false); 546 547 logins = await Services.logins.searchLoginsAsync({ 548 origin: "https://www.example.com", 549 }); 550 equal(logins.length, 1); 551 552 details.username = "guanaco"; 553 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1); 554 checkFields.forEach(field => { 555 equal(logins[0][field], details[field]); 556 }); 557 Assert.greater(logins[0].timePasswordChanged, details.timePasswordChanged); 558 equal(logins[0].syncCounter, 0); 559 equal(logins[0].everSynced, true); 560 561 // Mark the remote record as deleted and then sync again. 562 _("Perform sync when remote login has been marked for deletion"); 563 newTime = Date.now(); 564 collection.updateRecord( 565 guid1, 566 cleartext => { 567 cleartext.deleted = true; 568 }, 569 newTime / 1000 + 10 570 ); 571 572 await engine.setLastSync(newTime / 1000 - 30); 573 await sync_engine_and_validate_telem(engine, false); 574 575 logins = await Services.logins.searchLoginsAsync({ 576 origin: "https://www.example.com", 577 }); 578 equal(logins.length, 0); 579 } finally { 580 await cleanup(engine, server); 581 } 582 }); 583 584 add_task(async function test_sync_incoming_deleted() { 585 _("Test syncing incoming deleted records"); 586 587 let engine = Service.engineManager.get("passwords"); 588 589 let server = await serverForFoo(engine); 590 await SyncTestingInfrastructure(server); 591 592 let collection = server.user("foo").collection("passwords"); 593 594 let guid1 = Utils.makeGUID(); 595 let details2 = { 596 formSubmitURL: "https://www.example.org", 597 hostname: "https://www.example.org", 598 httpRealm: null, 599 username: "capybara", 600 password: "beaver", 601 usernameField: "username-field", 602 passwordField: "password-field", 603 timeCreated: Date.now(), 604 timePasswordChanged: Date.now(), 605 deleted: true, 606 }; 607 608 try { 609 // This test creates a remote server record that has been deleted 610 // and then verifies that the login is not imported locally. 611 _("Create remote login"); 612 collection.insertRecord(Object.assign({}, details2, { id: guid1 })); 613 614 _("Perform sync when remote login has been deleted"); 615 await sync_engine_and_validate_telem(engine, false); 616 617 let logins = await Services.logins.searchLoginsAsync({ 618 origin: "https://www.example.com", 619 }); 620 equal(logins.length, 0); 621 ok(!(await engine._store.getAllIDs())[guid1]); 622 ok(!(await engine._store.itemExists(guid1))); 623 } finally { 624 await cleanup(engine, server); 625 } 626 }); 627 628 add_task(async function test_sync_incoming_deleted_localchanged_remotenewer() { 629 _( 630 "Test syncing incoming deleted records where the local login has been changed but the remote record is newer" 631 ); 632 633 let engine = Service.engineManager.get("passwords"); 634 635 let server = await serverForFoo(engine); 636 await SyncTestingInfrastructure(server); 637 638 let collection = server.user("foo").collection("passwords"); 639 640 let loginInfo = new LoginInfo( 641 "http://mozilla.com", 642 "http://mozilla.com", 643 null, 644 "kangaroo", 645 "kaola", 646 "username", 647 "password" 648 ); 649 let login = await Services.logins.addLoginAsync(loginInfo); 650 let guid = login.QueryInterface(Ci.nsILoginMetaInfo).guid; 651 652 try { 653 _("Perform sync on new login"); 654 await sync_engine_and_validate_telem(engine, false); 655 656 let foundLogins = await Services.logins.searchLoginsAsync({ 657 origin: "http://mozilla.com", 658 }); 659 foundLogins[0].password = "wallaby"; 660 await Services.logins.modifyLoginAsync(login, foundLogins[0]); 661 662 // Use a time in the future to ensure that the remote record is newer. 663 collection.updateRecord( 664 guid, 665 cleartext => { 666 cleartext.deleted = true; 667 }, 668 Date.now() / 1000 + 1000 669 ); 670 671 _( 672 "Perform sync when remote login has been deleted and local login has been changed" 673 ); 674 await sync_engine_and_validate_telem(engine, false); 675 676 let logins = await Services.logins.searchLoginsAsync({ 677 origin: "https://mozilla.com", 678 }); 679 equal(logins.length, 0); 680 ok(await engine._store.getAllIDs()); 681 } finally { 682 await cleanup(engine, server); 683 } 684 }); 685 686 add_task(async function test_sync_incoming_deleted_localchanged_localnewer() { 687 _( 688 "Test syncing incoming deleted records where the local login has been changed but the local record is newer" 689 ); 690 691 let engine = Service.engineManager.get("passwords"); 692 693 let server = await serverForFoo(engine); 694 await SyncTestingInfrastructure(server); 695 696 let collection = server.user("foo").collection("passwords"); 697 698 let loginInfo = new LoginInfo( 699 "http://www.mozilla.com", 700 "http://www.mozilla.com", 701 null, 702 "lion", 703 "tiger", 704 "username", 705 "password" 706 ); 707 let login = await Services.logins.addLoginAsync(loginInfo); 708 let guid = login.QueryInterface(Ci.nsILoginMetaInfo).guid; 709 710 try { 711 _("Perform sync on new login"); 712 await sync_engine_and_validate_telem(engine, false); 713 714 let foundLogins = await Services.logins.searchLoginsAsync({ 715 origin: "http://www.mozilla.com", 716 }); 717 foundLogins[0].password = "cheetah"; 718 await Services.logins.modifyLoginAsync(login, foundLogins[0]); 719 720 // Use a time in the past to ensure that the local record is newer. 721 collection.updateRecord( 722 guid, 723 cleartext => { 724 cleartext.deleted = true; 725 }, 726 Date.now() / 1000 - 1000 727 ); 728 729 _( 730 "Perform sync when remote login has been deleted and local login has been changed" 731 ); 732 await sync_engine_and_validate_telem(engine, false); 733 734 let logins = await Services.logins.searchLoginsAsync({ 735 origin: "http://www.mozilla.com", 736 }); 737 equal(logins.length, 1); 738 equal(logins[0].password, "cheetah"); 739 equal(logins[0].syncCounter, 0); 740 equal(logins[0].everSynced, true); 741 ok(await engine._store.getAllIDs()); 742 } finally { 743 await cleanup(engine, server); 744 } 745 }); 746 747 add_task(async function test_sync_incoming_no_formactionorigin() { 748 _("Test syncing incoming a record where there is no formActionOrigin"); 749 750 let engine = Service.engineManager.get("passwords"); 751 752 let server = await serverForFoo(engine); 753 await SyncTestingInfrastructure(server); 754 755 let collection = server.user("foo").collection("passwords"); 756 757 const checkFields = [ 758 "formSubmitURL", 759 "hostname", 760 "httpRealm", 761 "username", 762 "password", 763 "usernameField", 764 "passwordField", 765 "timeCreated", 766 ]; 767 768 let guid1 = Utils.makeGUID(); 769 let details = { 770 formSubmitURL: "", 771 hostname: "https://www.example.com", 772 httpRealm: null, 773 username: "rabbit", 774 password: "squirrel", 775 usernameField: "username-field", 776 passwordField: "password-field", 777 timeCreated: Date.now(), 778 timePasswordChanged: Date.now(), 779 }; 780 781 try { 782 // This test creates a remote server record and then verifies that the login 783 // has been added locally after the sync occurs. 784 _("Create remote login"); 785 collection.insertRecord(Object.assign({}, details, { id: guid1 })); 786 787 _("Perform sync when remote login has been added"); 788 await sync_engine_and_validate_telem(engine, false); 789 790 let logins = await Services.logins.searchLoginsAsync({ 791 origin: "https://www.example.com", 792 formActionOrigin: "", 793 }); 794 equal(logins.length, 1); 795 796 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1); 797 checkFields.forEach(field => { 798 equal(logins[0][field], details[field]); 799 }); 800 equal(logins[0].timePasswordChanged, details.timePasswordChanged); 801 equal(logins[0].syncCounter, 0); 802 equal(logins[0].everSynced, true); 803 } finally { 804 await cleanup(engine, server); 805 } 806 }); 807 808 add_task(async function test_password_dupe() { 809 let engine = Service.engineManager.get("passwords"); 810 811 let server = await serverForFoo(engine); 812 await SyncTestingInfrastructure(server); 813 let collection = server.user("foo").collection("passwords"); 814 815 let guid1 = Utils.makeGUID(); 816 let rec1 = new LoginRec("passwords", guid1); 817 let guid2 = Utils.makeGUID(); 818 let cleartext = { 819 formSubmitURL: "https://www.example.com", 820 hostname: "https://www.example.com", 821 httpRealm: null, 822 username: "foo", 823 password: "bar", 824 usernameField: "username-field", 825 passwordField: "password-field", 826 timeCreated: Math.round(Date.now()), 827 timePasswordChanged: Math.round(Date.now()), 828 }; 829 rec1.cleartext = cleartext; 830 831 _("Create remote record with same details and guid1"); 832 collection.insert(guid1, encryptPayload(rec1.cleartext)); 833 834 _("Create remote record with guid2"); 835 collection.insert(guid2, encryptPayload(cleartext)); 836 837 _("Create local record with same details and guid1"); 838 await engine._store.create(rec1); 839 840 try { 841 _("Perform sync"); 842 await sync_engine_and_validate_telem(engine, true); 843 844 let logins = await Services.logins.searchLoginsAsync({ 845 origin: "https://www.example.com", 846 }); 847 848 equal(logins.length, 1); 849 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid2); 850 equal(null, collection.payload(guid1)); 851 } finally { 852 await cleanup(engine, server); 853 } 854 }); 855 856 add_task(async function test_updated_null_password_sync() { 857 _("Ensure updated null login username is converted to a string"); 858 859 let engine = Service.engineManager.get("passwords"); 860 861 let server = await serverForFoo(engine); 862 await SyncTestingInfrastructure(server); 863 let collection = server.user("foo").collection("passwords"); 864 865 let guid1 = Utils.makeGUID(); 866 let guid2 = Utils.makeGUID(); 867 let remoteDetails = { 868 formSubmitURL: "https://www.nullupdateexample.com", 869 hostname: "https://www.nullupdateexample.com", 870 httpRealm: null, 871 username: null, 872 password: "bar", 873 usernameField: "username-field", 874 passwordField: "password-field", 875 timeCreated: Date.now(), 876 timePasswordChanged: Date.now(), 877 }; 878 let localDetails = { 879 formSubmitURL: "https://www.nullupdateexample.com", 880 hostname: "https://www.nullupdateexample.com", 881 httpRealm: null, 882 username: "foo", 883 password: "foobar", 884 usernameField: "username-field", 885 passwordField: "password-field", 886 timeCreated: Date.now(), 887 timePasswordChanged: Date.now(), 888 }; 889 890 _("Create remote record with same details and guid1"); 891 collection.insertRecord(Object.assign({}, remoteDetails, { id: guid1 })); 892 893 try { 894 _("Create local updated login with null password"); 895 await engine._store.update(Object.assign({}, localDetails, { id: guid2 })); 896 897 _("Perform sync"); 898 await sync_engine_and_validate_telem(engine, false); 899 900 let logins = await Services.logins.searchLoginsAsync({ 901 origin: "https://www.nullupdateexample.com", 902 }); 903 904 equal(logins.length, 1); 905 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1); 906 } finally { 907 await cleanup(engine, server); 908 } 909 }); 910 911 add_task(async function test_updated_undefined_password_sync() { 912 _("Ensure updated undefined login username is converted to a string"); 913 914 let engine = Service.engineManager.get("passwords"); 915 916 let server = await serverForFoo(engine); 917 await SyncTestingInfrastructure(server); 918 let collection = server.user("foo").collection("passwords"); 919 920 let guid1 = Utils.makeGUID(); 921 let guid2 = Utils.makeGUID(); 922 let remoteDetails = { 923 formSubmitURL: "https://www.undefinedupdateexample.com", 924 hostname: "https://www.undefinedupdateexample.com", 925 httpRealm: null, 926 username: undefined, 927 password: "bar", 928 usernameField: "username-field", 929 passwordField: "password-field", 930 timeCreated: Date.now(), 931 timePasswordChanged: Date.now(), 932 }; 933 let localDetails = { 934 formSubmitURL: "https://www.undefinedupdateexample.com", 935 hostname: "https://www.undefinedupdateexample.com", 936 httpRealm: null, 937 username: "foo", 938 password: "foobar", 939 usernameField: "username-field", 940 passwordField: "password-field", 941 timeCreated: Date.now(), 942 timePasswordChanged: Date.now(), 943 }; 944 945 _("Create remote record with same details and guid1"); 946 collection.insertRecord(Object.assign({}, remoteDetails, { id: guid1 })); 947 948 try { 949 _("Create local updated login with undefined password"); 950 await engine._store.update(Object.assign({}, localDetails, { id: guid2 })); 951 952 _("Perform sync"); 953 await sync_engine_and_validate_telem(engine, false); 954 955 let logins = await Services.logins.searchLoginsAsync({ 956 origin: "https://www.undefinedupdateexample.com", 957 }); 958 959 equal(logins.length, 1); 960 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).guid, guid1); 961 } finally { 962 await cleanup(engine, server); 963 } 964 }); 965 966 add_task(async function test_new_null_password_sync() { 967 _("Ensure new null login username is converted to a string"); 968 969 let engine = Service.engineManager.get("passwords"); 970 971 let server = await serverForFoo(engine); 972 await SyncTestingInfrastructure(server); 973 974 let guid1 = Utils.makeGUID(); 975 let rec1 = new LoginRec("passwords", guid1); 976 rec1.cleartext = { 977 formSubmitURL: "https://www.example.com", 978 hostname: "https://www.example.com", 979 httpRealm: null, 980 username: null, 981 password: "bar", 982 usernameField: "username-field", 983 passwordField: "password-field", 984 timeCreated: Date.now(), 985 timePasswordChanged: Date.now(), 986 }; 987 988 try { 989 _("Create local login with null password"); 990 await engine._store.create(rec1); 991 992 _("Perform sync"); 993 await sync_engine_and_validate_telem(engine, false); 994 995 let logins = await Services.logins.searchLoginsAsync({ 996 origin: "https://www.example.com", 997 }); 998 999 equal(logins.length, 1); 1000 notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, null); 1001 notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, undefined); 1002 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, ""); 1003 } finally { 1004 await cleanup(engine, server); 1005 } 1006 }); 1007 1008 add_task(async function test_new_undefined_password_sync() { 1009 _("Ensure new undefined login username is converted to a string"); 1010 1011 let engine = Service.engineManager.get("passwords"); 1012 1013 let server = await serverForFoo(engine); 1014 await SyncTestingInfrastructure(server); 1015 1016 let guid1 = Utils.makeGUID(); 1017 let rec1 = new LoginRec("passwords", guid1); 1018 rec1.cleartext = { 1019 formSubmitURL: "https://www.example.com", 1020 hostname: "https://www.example.com", 1021 httpRealm: null, 1022 username: undefined, 1023 password: "bar", 1024 usernameField: "username-field", 1025 passwordField: "password-field", 1026 timeCreated: Date.now(), 1027 timePasswordChanged: Date.now(), 1028 }; 1029 1030 try { 1031 _("Create local login with undefined password"); 1032 await engine._store.create(rec1); 1033 1034 _("Perform sync"); 1035 await sync_engine_and_validate_telem(engine, false); 1036 1037 let logins = await Services.logins.searchLoginsAsync({ 1038 origin: "https://www.example.com", 1039 }); 1040 1041 equal(logins.length, 1); 1042 notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, null); 1043 notEqual(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, undefined); 1044 equal(logins[0].QueryInterface(Ci.nsILoginMetaInfo).username, ""); 1045 } finally { 1046 await cleanup(engine, server); 1047 } 1048 }); 1049 1050 add_task(async function test_sync_password_validation() { 1051 // This test isn't in test_password_validator to avoid duplicating cleanup. 1052 _("Ensure that if a password validation happens, it ends up in the ping"); 1053 1054 let engine = Service.engineManager.get("passwords"); 1055 1056 let server = await serverForFoo(engine); 1057 await SyncTestingInfrastructure(server); 1058 1059 Svc.PrefBranch.setIntPref("engine.passwords.validation.interval", 0); 1060 Svc.PrefBranch.setIntPref( 1061 "engine.passwords.validation.percentageChance", 1062 100 1063 ); 1064 Svc.PrefBranch.setIntPref("engine.passwords.validation.maxRecords", -1); 1065 Svc.PrefBranch.setBoolPref("engine.passwords.validation.enabled", true); 1066 1067 try { 1068 let ping = await wait_for_ping(() => Service.sync()); 1069 1070 let engineInfo = ping.engines.find(e => e.name == "passwords"); 1071 ok(engineInfo, "Engine should be in ping"); 1072 1073 let validation = engineInfo.validation; 1074 ok(validation, "Engine should have validation info"); 1075 } finally { 1076 await cleanup(engine, server); 1077 } 1078 }); 1079 1080 add_task(async function test_roundtrip_unknown_fields() { 1081 _( 1082 "Testing that unknown fields from other clients get roundtripped back to server" 1083 ); 1084 1085 let engine = Service.engineManager.get("passwords"); 1086 1087 let server = await serverForFoo(engine); 1088 await SyncTestingInfrastructure(server); 1089 let collection = server.user("foo").collection("passwords"); 1090 1091 enableValidationPrefs(); 1092 1093 _("Add login with older password change time to replace during first sync"); 1094 let oldLogin; 1095 { 1096 let login = new LoginInfo( 1097 "https://mozilla.com", 1098 "", 1099 null, 1100 "us3r", 1101 "0ldpa55", 1102 "", 1103 "" 1104 ); 1105 await Services.logins.addLoginAsync(login); 1106 1107 let props = new PropertyBag(); 1108 let localPasswordChangeTime = Math.round( 1109 Date.now() - 1 * 60 * 60 * 24 * 1000 1110 ); 1111 props.setProperty("timePasswordChanged", localPasswordChangeTime); 1112 await Services.logins.modifyLoginAsync(login, props); 1113 1114 let logins = await Services.logins.searchLoginsAsync({ 1115 origin: "https://mozilla.com", 1116 }); 1117 equal(logins.length, 1, "Should find old login in login manager"); 1118 oldLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo); 1119 equal(oldLogin.timePasswordChanged, localPasswordChangeTime); 1120 1121 let rec = new LoginRec("passwords", oldLogin.guid); 1122 rec.hostname = oldLogin.origin; 1123 rec.formSubmitURL = oldLogin.formActionOrigin; 1124 rec.httpRealm = oldLogin.httpRealm; 1125 rec.username = oldLogin.username; 1126 // Change the password and bump the password change time to ensure we prefer 1127 // the remote one during reconciliation. 1128 rec.password = "n3wpa55"; 1129 rec.usernameField = oldLogin.usernameField; 1130 rec.passwordField = oldLogin.usernameField; 1131 rec.timeCreated = oldLogin.timeCreated; 1132 rec.timePasswordChanged = Math.round(Date.now()); 1133 1134 // pretend other clients have some snazzy new fields 1135 // we don't quite understand yet 1136 rec.cleartext.someStrField = "I am a str"; 1137 rec.cleartext.someObjField = { newField: "I am a new field" }; 1138 collection.insert(oldLogin.guid, encryptPayload(rec.cleartext)); 1139 } 1140 1141 await engine._tracker.stop(); 1142 1143 try { 1144 await sync_engine_and_validate_telem(engine, false); 1145 1146 let logins = await Services.logins.searchLoginsAsync({ 1147 origin: "https://mozilla.com", 1148 }); 1149 equal( 1150 logins[0].password, 1151 "n3wpa55", 1152 "Should update local password for older login" 1153 ); 1154 let expectedUnknowns = JSON.stringify({ 1155 someStrField: "I am a str", 1156 someObjField: { newField: "I am a new field" }, 1157 }); 1158 // Check that the local record has all unknown fields properly 1159 // stringified 1160 equal(logins[0].unknownFields, expectedUnknowns); 1161 1162 // Check that the server has the unknown fields unfurled and on the 1163 // top-level record 1164 let serverRec = collection.cleartext(oldLogin.guid); 1165 equal(serverRec.someStrField, "I am a str"); 1166 equal(serverRec.someObjField.newField, "I am a new field"); 1167 } finally { 1168 await cleanup(engine, server); 1169 } 1170 }); 1171 1172 add_task(async function test_new_passwords_from_csv() { 1173 _("Test syncing records imported from a csv file"); 1174 1175 let engine = Service.engineManager.get("passwords"); 1176 1177 let server = await serverForFoo(engine); 1178 await SyncTestingInfrastructure(server); 1179 1180 let collection = server.user("foo").collection("passwords"); 1181 1182 engine._tracker.start(); 1183 1184 let data = [ 1185 { 1186 hostname: "https://example.com", 1187 url: "https://example.com/path", 1188 username: "exampleuser", 1189 password: "examplepassword", 1190 }, 1191 { 1192 hostname: "https://mozilla.org", 1193 url: "https://mozilla.org", 1194 username: "mozillauser", 1195 password: "mozillapassword", 1196 }, 1197 { 1198 hostname: "https://www.example.org", 1199 url: "https://www.example.org/example1/example2", 1200 username: "person", 1201 password: "mypassword", 1202 }, 1203 ]; 1204 1205 let csvData = ["url,username,login_password"]; 1206 for (let row of data) { 1207 csvData.push(row.url + "," + row.username + "," + row.password); 1208 } 1209 1210 let csvFile = FileTestUtils.getTempFile(`firefox_logins.csv`); 1211 await IOUtils.writeUTF8(csvFile.path, csvData.join("\r\n")); 1212 1213 await LoginCSVImport.importFromCSV(csvFile.path); 1214 1215 equal( 1216 engine._tracker.score, 1217 SCORE_INCREMENT_XLARGE, 1218 "Should only get one update notification for import" 1219 ); 1220 1221 _("Ensure that the csv import is correct"); 1222 for (let item of data) { 1223 let foundLogins = await Services.logins.searchLoginsAsync({ 1224 origin: item.hostname, 1225 }); 1226 equal(foundLogins.length, 1); 1227 equal(foundLogins[0].syncCounter, 1); 1228 equal(foundLogins[0].everSynced, false); 1229 equal(foundLogins[0].username, item.username); 1230 equal(foundLogins[0].password, item.password); 1231 } 1232 1233 _("Perform sync after modifying the password"); 1234 await sync_engine_and_validate_telem(engine, false); 1235 1236 _("Verify that the sync counter and status are updated"); 1237 for (let item of data) { 1238 let foundLogins = await Services.logins.searchLoginsAsync({ 1239 origin: item.hostname, 1240 }); 1241 equal(foundLogins.length, 1); 1242 equal(foundLogins[0].syncCounter, 0); 1243 equal(foundLogins[0].everSynced, true); 1244 equal(foundLogins[0].username, item.username); 1245 equal(foundLogins[0].password, item.password); 1246 item.guid = foundLogins[0].guid; 1247 } 1248 1249 equal(Object.keys(await engine.getChangedIDs()), 0); 1250 equal(collection.count(), 3); 1251 1252 for (let item of data) { 1253 // The remote login should have the imported username and password. 1254 let newRec = collection.cleartext(item.guid); 1255 equal(newRec.username, item.username); 1256 equal(newRec.password, item.password); 1257 } 1258 });