test_sync.js (29323B)
1 /** 2 * Tests sync functionality. 3 */ 4 5 /* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */ 6 /* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */ 7 /* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */ 8 /* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */ 9 10 "use strict"; 11 12 const { Service } = ChromeUtils.importESModule( 13 "resource://services-sync/service.sys.mjs" 14 ); 15 const { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule( 16 "resource://services-sync/constants.sys.mjs" 17 ); 18 19 const { sanitizeStorageObject, AutofillRecord, AddressesEngine } = 20 ChromeUtils.importESModule("resource://autofill/FormAutofillSync.sys.mjs"); 21 22 Services.prefs.setCharPref("extensions.formautofill.loglevel", "Trace"); 23 initTestLogging("Trace"); 24 25 const TEST_STORE_FILE_NAME = "test-profile.json"; 26 27 const TEST_PROFILE_1 = { 28 name: "Timothy John Berners-Lee", 29 organization: "World Wide Web Consortium", 30 "street-address": "32 Vassar Street\nMIT Room 32-G524", 31 "address-level2": "Cambridge", 32 "address-level1": "MA", 33 "postal-code": "02139", 34 country: "US", 35 tel: "+16172535702", 36 email: "timbl@w3.org", 37 // A field this client doesn't "understand" from another client 38 "unknown-1": "some unknown data from another client", 39 }; 40 41 const TEST_PROFILE_2 = { 42 "street-address": "Some Address", 43 country: "US", 44 }; 45 46 async function expectLocalProfiles(profileStorage, expected) { 47 let profiles = await profileStorage.addresses.getAll({ 48 rawData: true, 49 includeDeleted: true, 50 }); 51 expected.sort((a, b) => a.guid.localeCompare(b.guid)); 52 profiles.sort((a, b) => a.guid.localeCompare(b.guid)); 53 try { 54 deepEqual( 55 profiles.map(p => p.guid), 56 expected.map(p => p.guid) 57 ); 58 for (let i = 0; i < expected.length; i++) { 59 let thisExpected = expected[i]; 60 let thisGot = profiles[i]; 61 // always check "deleted". 62 equal(thisExpected.deleted, thisGot.deleted); 63 ok(objectMatches(thisGot, thisExpected)); 64 } 65 } catch (ex) { 66 info("Comparing expected profiles:"); 67 info(JSON.stringify(expected, undefined, 2)); 68 info("against actual profiles:"); 69 info(JSON.stringify(profiles, undefined, 2)); 70 throw ex; 71 } 72 } 73 74 async function setup() { 75 let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); 76 // should always start with no profiles. 77 Assert.equal( 78 (await profileStorage.addresses.getAll({ includeDeleted: true })).length, 79 0 80 ); 81 82 Services.prefs.setCharPref( 83 "services.sync.log.logger.engine.addresses", 84 "Trace" 85 ); 86 let engine = new AddressesEngine(Service); 87 await engine.initialize(); 88 // Avoid accidental automatic sync due to our own changes 89 Service.scheduler.syncThreshold = 10000000; 90 let syncID = await engine.resetLocalSyncID(); 91 let server = serverForUsers( 92 { foo: "password" }, 93 { 94 meta: { 95 global: { engines: { addresses: { version: engine.version, syncID } } }, 96 }, 97 addresses: {}, 98 } 99 ); 100 101 Service.engineManager._engines.addresses = engine; 102 engine.enabled = true; 103 engine._store._storage = profileStorage.addresses; 104 105 generateNewKeys(Service.collectionKeys); 106 107 await SyncTestingInfrastructure(server); 108 109 let collection = server.user("foo").collection("addresses"); 110 111 return { profileStorage, server, collection, engine }; 112 } 113 114 async function cleanup(server) { 115 let promiseStartOver = promiseOneObserver("weave:service:start-over:finish"); 116 await Service.startOver(); 117 await promiseStartOver; 118 await promiseStopServer(server); 119 } 120 121 add_task(async function test_log_sanitization() { 122 let sanitized = sanitizeStorageObject(TEST_PROFILE_1); 123 // all strings have been mangled. 124 for (let key of Object.keys(TEST_PROFILE_1)) { 125 let val = TEST_PROFILE_1[key]; 126 if (typeof val == "string") { 127 notEqual(sanitized[key], val); 128 } 129 } 130 // And check that stringifying a sync record is sanitized. 131 let record = new AutofillRecord("collection", "some-id"); 132 record.entry = TEST_PROFILE_1; 133 let serialized = record.toString(); 134 // None of the string values should appear in the output. 135 for (let key of Object.keys(TEST_PROFILE_1)) { 136 let val = TEST_PROFILE_1[key]; 137 if (typeof val == "string") { 138 ok(!serialized.includes(val), `"${val}" shouldn't be in: ${serialized}`); 139 } 140 } 141 }); 142 143 add_task(async function test_outgoing() { 144 let { profileStorage, server, collection, engine } = await setup(); 145 try { 146 equal(engine._tracker.score, 0); 147 let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1); 148 // And a deleted item. 149 let deletedGUID = profileStorage.addresses._generateGUID(); 150 await profileStorage.addresses.add({ guid: deletedGUID, deleted: true }); 151 152 await expectLocalProfiles(profileStorage, [ 153 { 154 guid: existingGUID, 155 }, 156 { 157 guid: deletedGUID, 158 deleted: true, 159 }, 160 ]); 161 162 await engine._tracker.asyncObserver.promiseObserversComplete(); 163 // The tracker should have a score recorded for the 2 additions we had. 164 equal(engine._tracker.score, SCORE_INCREMENT_XLARGE * 2); 165 166 await engine.setLastSync(0); 167 await engine.sync(); 168 169 Assert.equal(collection.count(), 2); 170 Assert.ok(collection.wbo(existingGUID)); 171 Assert.ok(collection.wbo(deletedGUID)); 172 173 await expectLocalProfiles(profileStorage, [ 174 { 175 guid: existingGUID, 176 }, 177 { 178 guid: deletedGUID, 179 deleted: true, 180 }, 181 ]); 182 183 strictEqual( 184 getSyncChangeCounter(profileStorage.addresses, existingGUID), 185 0 186 ); 187 strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedGUID), 0); 188 } finally { 189 await cleanup(server); 190 } 191 }); 192 193 add_task(async function test_incoming_new() { 194 let { profileStorage, server, engine } = await setup(); 195 try { 196 let profileID = Utils.makeGUID(); 197 let deletedID = Utils.makeGUID(); 198 199 server.insertWBO( 200 "foo", 201 "addresses", 202 new ServerWBO( 203 profileID, 204 encryptPayload({ 205 id: profileID, 206 entry: Object.assign( 207 { 208 version: 1, 209 }, 210 TEST_PROFILE_1 211 ), 212 }), 213 getDateForSync() 214 ) 215 ); 216 server.insertWBO( 217 "foo", 218 "addresses", 219 new ServerWBO( 220 deletedID, 221 encryptPayload({ 222 id: deletedID, 223 deleted: true, 224 }), 225 getDateForSync() 226 ) 227 ); 228 229 // The tracker should start with no score. 230 equal(engine._tracker.score, 0); 231 232 await engine.setLastSync(0); 233 await engine.sync(); 234 235 await expectLocalProfiles(profileStorage, [ 236 { 237 guid: profileID, 238 }, 239 { 240 guid: deletedID, 241 deleted: true, 242 }, 243 ]); 244 245 strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0); 246 strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedID), 0); 247 248 // Validate incoming records with unknown fields get stored 249 let localRecord = await profileStorage.addresses.get(profileID); 250 equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]); 251 252 // The sync applied new records - ensure our tracker knew it came from 253 // sync and didn't bump the score. 254 equal(engine._tracker.score, 0); 255 } finally { 256 await cleanup(server); 257 } 258 }); 259 260 add_task(async function test_incoming_existing() { 261 let { profileStorage, server, engine } = await setup(); 262 try { 263 let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1); 264 let guid2 = await profileStorage.addresses.add(TEST_PROFILE_2); 265 266 // an initial sync so we don't think they are locally modified. 267 await engine.setLastSync(0); 268 await engine.sync(); 269 270 // now server records that modify the existing items. 271 let modifiedEntry1 = Object.assign({}, TEST_PROFILE_1, { 272 version: 1, 273 name: "NewName", 274 }); 275 276 let lastSync = await engine.getLastSync(); 277 server.insertWBO( 278 "foo", 279 "addresses", 280 new ServerWBO( 281 guid1, 282 encryptPayload({ 283 id: guid1, 284 entry: modifiedEntry1, 285 }), 286 lastSync + 10 287 ) 288 ); 289 server.insertWBO( 290 "foo", 291 "addresses", 292 new ServerWBO( 293 guid2, 294 encryptPayload({ 295 id: guid2, 296 deleted: true, 297 }), 298 lastSync + 10 299 ) 300 ); 301 302 await engine.sync(); 303 304 await expectLocalProfiles(profileStorage, [ 305 Object.assign({}, modifiedEntry1, { guid: guid1 }), 306 { guid: guid2, deleted: true }, 307 ]); 308 } finally { 309 await cleanup(server); 310 } 311 }); 312 313 add_task(async function test_tombstones() { 314 let { profileStorage, server, collection, engine } = await setup(); 315 try { 316 let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1); 317 318 await engine.setLastSync(0); 319 await engine.sync(); 320 321 Assert.equal(collection.count(), 1); 322 let payload = collection.payloads()[0]; 323 equal(payload.id, existingGUID); 324 equal(payload.deleted, undefined); 325 326 profileStorage.addresses.remove(existingGUID); 327 await engine.sync(); 328 329 // should still exist, but now be a tombstone. 330 Assert.equal(collection.count(), 1); 331 payload = collection.payloads()[0]; 332 equal(payload.id, existingGUID); 333 equal(payload.deleted, true); 334 } finally { 335 await cleanup(server); 336 } 337 }); 338 339 add_task(async function test_applyIncoming_both_deleted() { 340 let { profileStorage, server, engine } = await setup(); 341 try { 342 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 343 344 await engine.setLastSync(0); 345 await engine.sync(); 346 347 // Delete synced record locally. 348 profileStorage.addresses.remove(guid); 349 350 // Delete same record remotely. 351 let lastSync = await engine.getLastSync(); 352 let collection = server.user("foo").collection("addresses"); 353 collection.insert( 354 guid, 355 encryptPayload({ 356 id: guid, 357 deleted: true, 358 }), 359 lastSync + 10 360 ); 361 362 await engine.sync(); 363 364 ok( 365 !(await profileStorage.addresses.get(guid)), 366 "Should not return record for locally deleted item" 367 ); 368 369 let localRecords = await profileStorage.addresses.getAll({ 370 includeDeleted: true, 371 }); 372 equal(localRecords.length, 1, "Only tombstone should exist locally"); 373 374 equal(collection.count(), 1, "Only tombstone should exist on server"); 375 } finally { 376 await cleanup(server); 377 } 378 }); 379 380 add_task(async function test_applyIncoming_nonexistent_tombstone() { 381 let { profileStorage, server, engine } = await setup(); 382 try { 383 let guid = profileStorage.addresses._generateGUID(); 384 let collection = server.user("foo").collection("addresses"); 385 collection.insert( 386 guid, 387 encryptPayload({ 388 id: guid, 389 deleted: true, 390 }), 391 getDateForSync() 392 ); 393 394 await engine.setLastSync(0); 395 await engine.sync(); 396 397 ok( 398 !(await profileStorage.addresses.get(guid)), 399 "Should not return record for unknown deleted item" 400 ); 401 let localTombstone = ( 402 await profileStorage.addresses.getAll({ 403 includeDeleted: true, 404 }) 405 ).find(record => record.guid == guid); 406 ok(localTombstone, "Should store tombstone for unknown item"); 407 } finally { 408 await cleanup(server); 409 } 410 }); 411 412 add_task(async function test_applyIncoming_incoming_deleted() { 413 let { profileStorage, server, engine } = await setup(); 414 try { 415 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 416 417 await engine.setLastSync(0); 418 await engine.sync(); 419 420 // Delete the record remotely. 421 let lastSync = await engine.getLastSync(); 422 let collection = server.user("foo").collection("addresses"); 423 collection.insert( 424 guid, 425 encryptPayload({ 426 id: guid, 427 deleted: true, 428 }), 429 lastSync + 10 430 ); 431 432 await engine.sync(); 433 434 ok( 435 !(await profileStorage.addresses.get(guid)), 436 "Should delete unmodified item locally" 437 ); 438 439 let localTombstone = ( 440 await profileStorage.addresses.getAll({ 441 includeDeleted: true, 442 }) 443 ).find(record => record.guid == guid); 444 ok(localTombstone, "Should keep local tombstone for remotely deleted item"); 445 strictEqual( 446 getSyncChangeCounter(profileStorage.addresses, guid), 447 0, 448 "Local tombstone should be marked as syncing" 449 ); 450 } finally { 451 await cleanup(server); 452 } 453 }); 454 455 add_task(async function test_applyIncoming_incoming_restored() { 456 let { profileStorage, server, engine } = await setup(); 457 try { 458 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 459 460 // Upload the record to the server. 461 await engine.setLastSync(0); 462 await engine.sync(); 463 464 // Removing a synced record should write a tombstone. 465 profileStorage.addresses.remove(guid); 466 467 // Modify the deleted record remotely. 468 let collection = server.user("foo").collection("addresses"); 469 let serverPayload = JSON.parse( 470 JSON.parse(collection.payload(guid)).ciphertext 471 ); 472 serverPayload.entry["street-address"] = "I moved!"; 473 let lastSync = await engine.getLastSync(); 474 collection.insert(guid, encryptPayload(serverPayload), lastSync + 10); 475 476 // Sync again. 477 await engine.sync(); 478 479 // We should replace our tombstone with the server's version. 480 let localRecord = await profileStorage.addresses.get(guid); 481 ok( 482 objectMatches(localRecord, { 483 name: "Timothy John Berners-Lee", 484 "street-address": "I moved!", 485 }) 486 ); 487 488 let maybeNewServerPayload = JSON.parse( 489 JSON.parse(collection.payload(guid)).ciphertext 490 ); 491 deepEqual( 492 maybeNewServerPayload, 493 serverPayload, 494 "Should not change record on server" 495 ); 496 } finally { 497 await cleanup(server); 498 } 499 }); 500 501 add_task(async function test_applyIncoming_outgoing_restored() { 502 let { profileStorage, server, engine } = await setup(); 503 try { 504 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 505 506 // Upload the record to the server. 507 await engine.setLastSync(0); 508 await engine.sync(); 509 510 // Modify the local record. 511 let localCopy = Object.assign({}, TEST_PROFILE_1); 512 localCopy["street-address"] = "I moved!"; 513 await profileStorage.addresses.update(guid, localCopy); 514 515 // Replace the record with a tombstone on the server. 516 let lastSync = await engine.getLastSync(); 517 let collection = server.user("foo").collection("addresses"); 518 collection.insert( 519 guid, 520 encryptPayload({ 521 id: guid, 522 deleted: true, 523 }), 524 lastSync + 10 525 ); 526 527 // Sync again. 528 await engine.sync(); 529 530 // We should resurrect the record on the server. 531 let serverPayload = JSON.parse( 532 JSON.parse(collection.payload(guid)).ciphertext 533 ); 534 ok(!serverPayload.deleted, "Should resurrect record on server"); 535 ok( 536 objectMatches(serverPayload.entry, { 537 name: "Timothy John Berners-Lee", 538 "street-address": "I moved!", 539 // resurrection also beings back any unknown fields we had 540 "unknown-1": "some unknown data from another client", 541 }) 542 ); 543 544 let localRecord = await profileStorage.addresses.get(guid); 545 ok(localRecord, "Modified record should not be deleted locally"); 546 } finally { 547 await cleanup(server); 548 } 549 }); 550 551 // Unlike most sync engines, we want "both modified" to inspect the records, 552 // and if materially different, create a duplicate. 553 add_task(async function test_reconcile_both_modified_identical() { 554 let { profileStorage, server, engine } = await setup(); 555 try { 556 // create a record locally. 557 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 558 559 // and an identical record on the server. 560 server.insertWBO( 561 "foo", 562 "addresses", 563 new ServerWBO( 564 guid, 565 encryptPayload({ 566 id: guid, 567 entry: TEST_PROFILE_1, 568 }), 569 getDateForSync() 570 ) 571 ); 572 573 await engine.setLastSync(0); 574 await engine.sync(); 575 576 await expectLocalProfiles(profileStorage, [{ guid }]); 577 } finally { 578 await cleanup(server); 579 } 580 }); 581 582 add_task(async function test_incoming_dupes() { 583 let { profileStorage, server, engine } = await setup(); 584 try { 585 // Create a profile locally, then sync to upload the new profile to the 586 // server. 587 let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1); 588 589 await engine.setLastSync(0); 590 await engine.sync(); 591 592 // Create another profile locally, but don't sync it yet. 593 await profileStorage.addresses.add(TEST_PROFILE_2); 594 595 // Now create two records on the server with the same contents as our local 596 // profiles, but different GUIDs. 597 let lastSync = await engine.getLastSync(); 598 let guid1_dupe = Utils.makeGUID(); 599 server.insertWBO( 600 "foo", 601 "addresses", 602 new ServerWBO( 603 guid1_dupe, 604 encryptPayload({ 605 id: guid1_dupe, 606 entry: Object.assign( 607 { 608 version: 1, 609 }, 610 TEST_PROFILE_1 611 ), 612 }), 613 lastSync + 10 614 ) 615 ); 616 let guid2_dupe = Utils.makeGUID(); 617 server.insertWBO( 618 "foo", 619 "addresses", 620 new ServerWBO( 621 guid2_dupe, 622 encryptPayload({ 623 id: guid2_dupe, 624 entry: Object.assign( 625 { 626 version: 1, 627 }, 628 TEST_PROFILE_2 629 ), 630 }), 631 lastSync + 10 632 ) 633 ); 634 635 // Sync again. We should download `guid1_dupe` and `guid2_dupe`, then 636 // reconcile changes. 637 await engine.sync(); 638 639 await expectLocalProfiles(profileStorage, [ 640 // We uploaded `guid1` during the first sync. Even though its contents 641 // are the same as `guid1_dupe`, we keep both. 642 Object.assign({}, TEST_PROFILE_1, { guid: guid1 }), 643 Object.assign({}, TEST_PROFILE_1, { guid: guid1_dupe }), 644 // However, we didn't upload `guid2` before downloading `guid2_dupe`, so 645 // we *should* dedupe `guid2` to `guid2_dupe`. 646 Object.assign({}, TEST_PROFILE_2, { guid: guid2_dupe }), 647 ]); 648 } finally { 649 await cleanup(server); 650 } 651 }); 652 653 add_task(async function test_dedupe_identical_unsynced_singlelineaddress() { 654 let { profileStorage, server, engine } = await setup(); 655 try { 656 let profile = structuredClone(TEST_PROFILE_1); 657 // Change the street address so that it will parse correctly. 658 profile["street-address"] = "36 Main Street"; 659 660 // create a record locally. 661 let localGuid = await profileStorage.addresses.add(profile); 662 663 // and an identical record on the server but different GUID. 664 let remoteGuid = Utils.makeGUID(); 665 notEqual(localGuid, remoteGuid); 666 server.insertWBO( 667 "foo", 668 "addresses", 669 new ServerWBO( 670 remoteGuid, 671 encryptPayload({ 672 id: remoteGuid, 673 entry: Object.assign( 674 { 675 version: 1, 676 }, 677 profile 678 ), 679 }), 680 getDateForSync() 681 ) 682 ); 683 684 await engine.setLastSync(0); 685 await engine.sync(); 686 687 // Should have 1 item locally with GUID changed to the remote one. 688 // There's no tombstone as the original was unsynced. 689 await expectLocalProfiles(profileStorage, [ 690 { 691 guid: remoteGuid, 692 }, 693 ]); 694 } finally { 695 await cleanup(server); 696 } 697 }); 698 699 add_task(async function test_dedupe_identical_unsynced() { 700 let { profileStorage, server, engine } = await setup(); 701 try { 702 // create a record locally. 703 let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1); 704 705 // and an identical record on the server but different GUID. 706 let remoteGuid = Utils.makeGUID(); 707 notEqual(localGuid, remoteGuid); 708 server.insertWBO( 709 "foo", 710 "addresses", 711 new ServerWBO( 712 remoteGuid, 713 encryptPayload({ 714 id: remoteGuid, 715 entry: Object.assign( 716 { 717 version: 1, 718 }, 719 TEST_PROFILE_1 720 ), 721 }), 722 getDateForSync() 723 ) 724 ); 725 726 await engine.setLastSync(0); 727 await engine.sync(); 728 729 // Should have 1 item locally with GUID changed to the remote one. 730 // There's no tombstone as the original was unsynced. 731 await expectLocalProfiles(profileStorage, [ 732 { 733 guid: remoteGuid, 734 }, 735 ]); 736 } finally { 737 await cleanup(server); 738 } 739 }); 740 741 add_task(async function test_dedupe_identical_synced() { 742 let { profileStorage, server, engine } = await setup(); 743 try { 744 // create a record locally. 745 let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1); 746 747 // sync it - it will no longer be a candidate for de-duping. 748 await engine.setLastSync(0); 749 await engine.sync(); 750 751 // and an identical record on the server but different GUID. 752 let lastSync = await engine.getLastSync(); 753 let remoteGuid = Utils.makeGUID(); 754 server.insertWBO( 755 "foo", 756 "addresses", 757 new ServerWBO( 758 remoteGuid, 759 encryptPayload({ 760 id: remoteGuid, 761 entry: Object.assign( 762 { 763 version: 1, 764 }, 765 TEST_PROFILE_1 766 ), 767 }), 768 lastSync + 10 769 ) 770 ); 771 772 await engine.sync(); 773 774 // Should have 2 items locally, since the first was synced. 775 await expectLocalProfiles(profileStorage, [ 776 { guid: localGuid }, 777 { guid: remoteGuid }, 778 ]); 779 } finally { 780 await cleanup(server); 781 } 782 }); 783 784 add_task(async function test_dedupe_multiple_candidates() { 785 let { profileStorage, server, engine } = await setup(); 786 try { 787 // It's possible to have duplicate local profiles, with the same fields but 788 // different GUIDs. After a node reassignment, or after disconnecting and 789 // reconnecting to Sync, we might dedupe a local record A to a remote record 790 // B, if we see B before we download and apply A. Since A and B are dupes, 791 // that's OK. We'll write a tombstone for A when we dedupe A to B, and 792 // overwrite that tombstone when we see A. 793 794 let localRecord = { 795 name: "Mark Hammond", 796 organization: "Mozilla", 797 country: "AU", 798 tel: "+12345678910", 799 }; 800 let serverRecord = Object.assign( 801 { 802 version: 1, 803 }, 804 localRecord 805 ); 806 807 // We don't pass `sourceSync` so that the records are marked as NEW. 808 let aGuid = await profileStorage.addresses.add(localRecord); 809 let bGuid = await profileStorage.addresses.add(localRecord); 810 811 // Insert B before A. 812 server.insertWBO( 813 "foo", 814 "addresses", 815 new ServerWBO( 816 bGuid, 817 encryptPayload({ 818 id: bGuid, 819 entry: serverRecord, 820 }), 821 getDateForSync() 822 ) 823 ); 824 server.insertWBO( 825 "foo", 826 "addresses", 827 new ServerWBO( 828 aGuid, 829 encryptPayload({ 830 id: aGuid, 831 entry: serverRecord, 832 }), 833 getDateForSync() 834 ) 835 ); 836 837 await engine.setLastSync(0); 838 await engine.sync(); 839 840 await expectLocalProfiles(profileStorage, [ 841 { 842 guid: aGuid, 843 name: "Mark Hammond", 844 organization: "Mozilla", 845 country: "AU", 846 tel: "+12345678910", 847 }, 848 { 849 guid: bGuid, 850 name: "Mark Hammond", 851 organization: "Mozilla", 852 country: "AU", 853 tel: "+12345678910", 854 }, 855 ]); 856 // Make sure these are both syncing. 857 strictEqual( 858 getSyncChangeCounter(profileStorage.addresses, aGuid), 859 0, 860 "A should be marked as syncing" 861 ); 862 strictEqual( 863 getSyncChangeCounter(profileStorage.addresses, bGuid), 864 0, 865 "B should be marked as syncing" 866 ); 867 } finally { 868 await cleanup(server); 869 } 870 }); 871 872 // Unlike most sync engines, we want "both modified" to inspect the records, 873 // and if materially different, create a duplicate. 874 add_task(async function test_reconcile_both_modified_conflict() { 875 let { profileStorage, server, engine } = await setup(); 876 try { 877 // create a record locally. 878 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 879 880 // Upload the record to the server. 881 await engine.setLastSync(0); 882 await engine.sync(); 883 884 strictEqual( 885 getSyncChangeCounter(profileStorage.addresses, guid), 886 0, 887 "Original record should be marked as syncing" 888 ); 889 890 // Change the same field locally and on the server. 891 let localCopy = Object.assign({}, TEST_PROFILE_1); 892 localCopy["street-address"] = "I moved!"; 893 await profileStorage.addresses.update(guid, localCopy); 894 895 let lastSync = await engine.getLastSync(); 896 let collection = server.user("foo").collection("addresses"); 897 let serverPayload = JSON.parse( 898 JSON.parse(collection.payload(guid)).ciphertext 899 ); 900 serverPayload.entry["street-address"] = "I moved, too!"; 901 collection.insert(guid, encryptPayload(serverPayload), lastSync + 10); 902 903 // Sync again. 904 await engine.sync(); 905 906 // Since we wait to pull changes until we're ready to upload, both records 907 // should now exist on the server; we don't need a follow-up sync. 908 let serverPayloads = collection.payloads(); 909 equal(serverPayloads.length, 2, "Both records should exist on server"); 910 911 let forkedPayload = serverPayloads.find(payload => payload.id != guid); 912 ok(forkedPayload, "Forked record should exist on server"); 913 914 await expectLocalProfiles(profileStorage, [ 915 { 916 guid, 917 name: "Timothy John Berners-Lee", 918 "street-address": "I moved, too!", 919 }, 920 { 921 guid: forkedPayload.id, 922 name: "Timothy John Berners-Lee", 923 "street-address": "I moved!", 924 }, 925 ]); 926 927 let changeCounter = getSyncChangeCounter( 928 profileStorage.addresses, 929 forkedPayload.id 930 ); 931 strictEqual(changeCounter, 0, "Forked record should be marked as syncing"); 932 } finally { 933 await cleanup(server); 934 } 935 }); 936 937 add_task(async function test_wipe() { 938 let { profileStorage, server, engine } = await setup(); 939 try { 940 let guid = await profileStorage.addresses.add(TEST_PROFILE_1); 941 942 await expectLocalProfiles(profileStorage, [{ guid }]); 943 944 let promiseObserved = promiseOneObserver("formautofill-storage-changed"); 945 946 await engine._wipeClient(); 947 948 let { subject, data } = await promiseObserved; 949 Assert.equal( 950 subject.wrappedJSObject.sourceSync, 951 true, 952 "it should be noted this came from sync" 953 ); 954 Assert.equal( 955 subject.wrappedJSObject.collectionName, 956 "addresses", 957 "got the correct collection" 958 ); 959 Assert.equal(data, "removeAll", "a removeAll should be noted"); 960 961 await expectLocalProfiles(profileStorage, []); 962 } finally { 963 await cleanup(server); 964 } 965 }); 966 967 // Other clients might have data that we aren't able to process/understand yet 968 // We should keep that data and ensure when we sync we don't lose that data 969 add_task(async function test_full_roundtrip_unknown_data() { 970 let { profileStorage, server, engine } = await setup(); 971 try { 972 let profileID = Utils.makeGUID(); 973 974 info("Incoming records with unknown fields are properly stored"); 975 // Insert a record onto the server 976 server.insertWBO( 977 "foo", 978 "addresses", 979 new ServerWBO( 980 profileID, 981 encryptPayload({ 982 id: profileID, 983 entry: Object.assign( 984 { 985 version: 1, 986 }, 987 TEST_PROFILE_1 988 ), 989 }), 990 getDateForSync() 991 ) 992 ); 993 994 // The tracker should start with no score. 995 equal(engine._tracker.score, 0); 996 997 await engine.setLastSync(0); 998 await engine.sync(); 999 1000 await expectLocalProfiles(profileStorage, [ 1001 { 1002 guid: profileID, 1003 }, 1004 ]); 1005 1006 strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0); 1007 1008 // The sync applied new records - ensure our tracker knew it came from 1009 // sync and didn't bump the score. 1010 equal(engine._tracker.score, 0); 1011 1012 // Validate incoming records with unknown fields are correctly stored 1013 let localRecord = await profileStorage.addresses.get(profileID); 1014 equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]); 1015 1016 let onChanged = TestUtils.topicObserved( 1017 "formautofill-storage-changed", 1018 (subject, data) => data == "update" 1019 ); 1020 1021 // Validate we can update the records locally and not drop any unknown fields 1022 info("Unknown fields are sent back up to the server"); 1023 1024 // Modify the local copy 1025 let localCopy = Object.assign({}, TEST_PROFILE_1); 1026 localCopy["street-address"] = "I moved!"; 1027 await profileStorage.addresses.update(profileID, localCopy); 1028 await onChanged; 1029 await profileStorage._saveImmediately(); 1030 1031 let updatedCopy = await profileStorage.addresses.get(profileID); 1032 equal(updatedCopy["street-address"], "I moved!"); 1033 1034 // Sync our changes to the server 1035 await engine.setLastSync(0); 1036 await engine.sync(); 1037 1038 let collection = server.user("foo").collection("addresses"); 1039 1040 Assert.ok(collection.wbo(profileID)); 1041 let serverPayload = JSON.parse( 1042 JSON.parse(collection.payload(profileID)).ciphertext 1043 ); 1044 1045 // The server has the updated field as well as any unknown fields 1046 equal( 1047 serverPayload.entry["unknown-1"], 1048 "some unknown data from another client" 1049 ); 1050 equal(serverPayload.entry["street-address"], "I moved!"); 1051 } finally { 1052 await cleanup(server); 1053 } 1054 });