test_bookmark_engine.js (48453B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { BookmarkHTMLUtils } = ChromeUtils.importESModule( 5 "resource://gre/modules/BookmarkHTMLUtils.sys.mjs" 6 ); 7 const { BookmarkJSONUtils } = ChromeUtils.importESModule( 8 "resource://gre/modules/BookmarkJSONUtils.sys.mjs" 9 ); 10 const { Bookmark, BookmarkFolder, BookmarksEngine, Livemark } = 11 ChromeUtils.importESModule( 12 "resource://services-sync/engines/bookmarks.sys.mjs" 13 ); 14 const { Service } = ChromeUtils.importESModule( 15 "resource://services-sync/service.sys.mjs" 16 ); 17 const { SyncedRecordsTelemetry } = ChromeUtils.importESModule( 18 "resource://services-sync/telemetry.sys.mjs" 19 ); 20 21 var recordedEvents = []; 22 23 function checkRecordedEvents(object, expected, message) { 24 // Ignore event telemetry from the merger. 25 let checkEvents = recordedEvents.filter(event => event.object == object); 26 deepEqual(checkEvents, expected, message); 27 // and clear the list so future checks are easier to write. 28 recordedEvents = []; 29 } 30 31 async function fetchAllRecordIds() { 32 let db = await PlacesUtils.promiseDBConnection(); 33 let rows = await db.executeCached(` 34 WITH RECURSIVE 35 syncedItems(id, guid) AS ( 36 SELECT b.id, b.guid FROM moz_bookmarks b 37 WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____', 38 'mobile______') 39 UNION ALL 40 SELECT b.id, b.guid FROM moz_bookmarks b 41 JOIN syncedItems s ON b.parent = s.id 42 ) 43 SELECT guid FROM syncedItems`); 44 let recordIds = new Set(); 45 for (let row of rows) { 46 let recordId = PlacesSyncUtils.bookmarks.guidToRecordId( 47 row.getResultByName("guid") 48 ); 49 recordIds.add(recordId); 50 } 51 return recordIds; 52 } 53 54 async function cleanupEngine(engine) { 55 await engine.resetClient(); 56 await engine._store.wipe(); 57 for (const pref of Svc.PrefBranch.getChildList("")) { 58 Svc.PrefBranch.clearUserPref(pref); 59 } 60 Service.recordManager.clearCache(); 61 // Note we don't finalize the engine here as add_bookmark_test() does. 62 } 63 64 async function cleanup(engine, server) { 65 await promiseStopServer(server); 66 await cleanupEngine(engine); 67 } 68 69 add_setup(async function () { 70 await generateNewKeys(Service.collectionKeys); 71 await Service.engineManager.unregister("bookmarks"); 72 73 do_get_profile; // FOG needs a profile dir 74 Services.fog.initializeFOG(); 75 76 Service.recordTelemetryEvent = (object, method, value, extra = undefined) => { 77 recordedEvents.push({ object, method, value, extra }); 78 }; 79 }); 80 81 add_task(async function test_buffer_timeout() { 82 await Service.recordManager.clearCache(); 83 await PlacesSyncUtils.bookmarks.reset(); 84 let engine = new BookmarksEngine(Service); 85 engine._newWatchdog = function () { 86 // Return an already-aborted watchdog, so that we can abort merges 87 // immediately. 88 let watchdog = Async.watchdog(); 89 watchdog.controller.abort(); 90 return watchdog; 91 }; 92 await engine.initialize(); 93 let server = await serverForFoo(engine); 94 await SyncTestingInfrastructure(server); 95 let collection = server.user("foo").collection("bookmarks"); 96 97 try { 98 info("Insert local bookmarks"); 99 await PlacesUtils.bookmarks.insertTree({ 100 guid: PlacesUtils.bookmarks.unfiledGuid, 101 children: [ 102 { 103 guid: "bookmarkAAAA", 104 url: "http://example.com/a", 105 title: "A", 106 }, 107 { 108 guid: "bookmarkBBBB", 109 url: "http://example.com/b", 110 title: "B", 111 }, 112 ], 113 }); 114 115 info("Insert remote bookmarks"); 116 collection.insert( 117 "menu", 118 encryptPayload({ 119 id: "menu", 120 type: "folder", 121 parentid: "places", 122 title: "menu", 123 children: ["bookmarkCCCC", "bookmarkDDDD"], 124 }) 125 ); 126 collection.insert( 127 "bookmarkCCCC", 128 encryptPayload({ 129 id: "bookmarkCCCC", 130 type: "bookmark", 131 parentid: "menu", 132 bmkUri: "http://example.com/c", 133 title: "C", 134 }) 135 ); 136 collection.insert( 137 "bookmarkDDDD", 138 encryptPayload({ 139 id: "bookmarkDDDD", 140 type: "bookmark", 141 parentid: "menu", 142 bmkUri: "http://example.com/d", 143 title: "D", 144 }) 145 ); 146 147 info("We expect this sync to fail"); 148 await Assert.rejects( 149 sync_engine_and_validate_telem(engine, true), 150 ex => ex.name == "InterruptedError" 151 ); 152 } finally { 153 await cleanup(engine, server); 154 await engine.finalize(); 155 } 156 }); 157 158 add_bookmark_test(async function test_maintenance_after_failure(engine) { 159 _("Ensure we try to run maintenance if the engine fails to sync"); 160 161 let server = await serverForFoo(engine); 162 await SyncTestingInfrastructure(server); 163 164 try { 165 let syncStartup = engine._syncStartup; 166 let syncError = new Error("Something is rotten in the state of Places"); 167 engine._syncStartup = function () { 168 throw syncError; 169 }; 170 171 Services.prefs.clearUserPref("places.database.lastMaintenance"); 172 173 _("Ensure the sync fails and we run maintenance"); 174 await Assert.rejects( 175 sync_engine_and_validate_telem(engine, true), 176 ex => ex == syncError 177 ); 178 checkRecordedEvents( 179 "maintenance", 180 [ 181 { 182 object: "maintenance", 183 method: "run", 184 value: "bookmarks", 185 extra: undefined, 186 }, 187 ], 188 "Should record event for first maintenance run" 189 ); 190 Assert.equal(Glean.sync.maintenanceRunBookmarks.testGetValue().length, 1); 191 Services.fog.testResetFOG(); 192 193 _("Sync again, but ensure maintenance doesn't run"); 194 await Assert.rejects( 195 sync_engine_and_validate_telem(engine, true), 196 ex => ex == syncError 197 ); 198 checkRecordedEvents( 199 "maintenance", 200 [], 201 "Should not record event if maintenance didn't run" 202 ); 203 Assert.equal(Glean.sync.maintenanceRunBookmarks.testGetValue(), null); 204 Services.fog.testResetFOG(); 205 206 _("Fast-forward last maintenance pref; ensure maintenance runs"); 207 Services.prefs.setIntPref( 208 "places.database.lastMaintenance", 209 Date.now() / 1000 - 14400 210 ); 211 await Assert.rejects( 212 sync_engine_and_validate_telem(engine, true), 213 ex => ex == syncError 214 ); 215 checkRecordedEvents( 216 "maintenance", 217 [ 218 { 219 object: "maintenance", 220 method: "run", 221 value: "bookmarks", 222 extra: undefined, 223 }, 224 ], 225 "Should record event for second maintenance run" 226 ); 227 Assert.equal(Glean.sync.maintenanceRunBookmarks.testGetValue().length, 1); 228 Services.fog.testResetFOG(); 229 230 _("Fix sync failure; ensure we report success after maintenance"); 231 engine._syncStartup = syncStartup; 232 await sync_engine_and_validate_telem(engine, false); 233 checkRecordedEvents( 234 "maintenance", 235 [ 236 { 237 object: "maintenance", 238 method: "fix", 239 value: "bookmarks", 240 extra: undefined, 241 }, 242 ], 243 "Should record event for successful sync after second maintenance" 244 ); 245 Assert.equal(Glean.sync.maintenanceFixBookmarks.testGetValue().length, 1); 246 Services.fog.testResetFOG(); 247 248 await sync_engine_and_validate_telem(engine, false); 249 checkRecordedEvents( 250 "maintenance", 251 [], 252 "Should not record maintenance events after successful sync" 253 ); 254 Assert.equal(Glean.sync.maintenanceFixBookmarks.testGetValue(), null); 255 Services.fog.testResetFOG(); 256 } finally { 257 await cleanup(engine, server); 258 } 259 }); 260 261 add_bookmark_test(async function test_delete_invalid_roots_from_server(engine) { 262 _("Ensure that we delete the Places and Reading List roots from the server."); 263 264 enableValidationPrefs(); 265 266 let store = engine._store; 267 let server = await serverForFoo(engine); 268 await SyncTestingInfrastructure(server); 269 270 let collection = server.user("foo").collection("bookmarks"); 271 272 engine._tracker.start(); 273 274 try { 275 let placesRecord = await store.createRecord("places"); 276 collection.insert("places", encryptPayload(placesRecord.cleartext)); 277 278 let listBmk = new Bookmark("bookmarks", Utils.makeGUID()); 279 listBmk.bmkUri = "https://example.com"; 280 listBmk.title = "Example reading list entry"; 281 listBmk.parentName = "Reading List"; 282 listBmk.parentid = "readinglist"; 283 collection.insert(listBmk.id, encryptPayload(listBmk.cleartext)); 284 285 let readingList = new BookmarkFolder("bookmarks", "readinglist"); 286 readingList.title = "Reading List"; 287 readingList.children = [listBmk.id]; 288 readingList.parentName = ""; 289 readingList.parentid = "places"; 290 collection.insert("readinglist", encryptPayload(readingList.cleartext)); 291 292 // Note that we don't insert a record for the toolbar, so the engine will 293 // report a parent-child disagreement, since Firefox's `parentid` is 294 // `toolbar`. 295 let newBmk = new Bookmark("bookmarks", Utils.makeGUID()); 296 newBmk.bmkUri = "http://getfirefox.com"; 297 newBmk.title = "Get Firefox!"; 298 newBmk.parentName = "Bookmarks Toolbar"; 299 newBmk.parentid = "toolbar"; 300 collection.insert(newBmk.id, encryptPayload(newBmk.cleartext)); 301 302 deepEqual( 303 collection.keys().sort(), 304 ["places", "readinglist", listBmk.id, newBmk.id].sort(), 305 "Should store Places root, reading list items, and new bookmark on server" 306 ); 307 308 let ping = await sync_engine_and_validate_telem(engine, true); 309 // In a real sync, the engine is named `bookmarks-buffered`. 310 // However, `sync_engine_and_validate_telem` simulates a sync where 311 // the engine isn't registered with the engine manager, so the recorder 312 // doesn't see its `overrideTelemetryName`. 313 let engineData = ping.engines.find(e => e.name == "bookmarks"); 314 ok(engineData.validation, "Bookmarks engine should always run validation"); 315 equal( 316 engineData.validation.checked, 317 6, 318 "Bookmarks engine should validate all items" 319 ); 320 deepEqual( 321 engineData.validation.problems, 322 [ 323 { 324 name: "parentChildDisagreements", 325 count: 1, 326 }, 327 ], 328 "Bookmarks engine should report parent-child disagreement" 329 ); 330 deepEqual( 331 engineData.steps.map(step => step.name), 332 [ 333 "fetchLocalTree", 334 "fetchRemoteTree", 335 "merge", 336 "apply", 337 "notifyObservers", 338 "fetchLocalChangeRecords", 339 ], 340 "Bookmarks engine should report all merge steps" 341 ); 342 343 deepEqual( 344 collection.keys().sort(), 345 ["menu", "mobile", "toolbar", "unfiled", newBmk.id].sort(), 346 "Should remove Places root and reading list items from server; upload local roots" 347 ); 348 } finally { 349 await cleanup(engine, server); 350 } 351 }); 352 353 add_bookmark_test( 354 async function test_processIncoming_error_orderChildren(engine) { 355 _( 356 "Ensure that _orderChildren() is called even when _processIncoming() throws an error." 357 ); 358 359 let store = engine._store; 360 let server = await serverForFoo(engine); 361 await SyncTestingInfrastructure(server); 362 363 let collection = server.user("foo").collection("bookmarks"); 364 365 try { 366 let folder1 = await PlacesUtils.bookmarks.insert({ 367 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 368 type: PlacesUtils.bookmarks.TYPE_FOLDER, 369 title: "Folder 1", 370 }); 371 372 let bmk1 = await PlacesUtils.bookmarks.insert({ 373 parentGuid: folder1.guid, 374 url: "http://getfirefox.com/", 375 title: "Get Firefox!", 376 }); 377 let bmk2 = await PlacesUtils.bookmarks.insert({ 378 parentGuid: folder1.guid, 379 url: "http://getthunderbird.com/", 380 title: "Get Thunderbird!", 381 }); 382 383 let toolbar_record = await store.createRecord("toolbar"); 384 collection.insert("toolbar", encryptPayload(toolbar_record.cleartext)); 385 386 let bmk1_record = await store.createRecord(bmk1.guid); 387 collection.insert(bmk1.guid, encryptPayload(bmk1_record.cleartext)); 388 389 let bmk2_record = await store.createRecord(bmk2.guid); 390 collection.insert(bmk2.guid, encryptPayload(bmk2_record.cleartext)); 391 392 // Create a server record for folder1 where we flip the order of 393 // the children. 394 let folder1_record = await store.createRecord(folder1.guid); 395 let folder1_payload = folder1_record.cleartext; 396 folder1_payload.children.reverse(); 397 collection.insert(folder1.guid, encryptPayload(folder1_payload)); 398 399 // Create a bogus record that when synced down will provoke a 400 // network error which in turn provokes an exception in _processIncoming. 401 const BOGUS_GUID = "zzzzzzzzzzzz"; 402 let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!"); 403 bogus_record.get = function get() { 404 throw new Error("Sync this!"); 405 }; 406 407 // Make the 10 minutes old so it will only be synced in the toFetch phase. 408 bogus_record.modified = new_timestamp() - 60 * 10; 409 await engine.setLastSync(new_timestamp() - 60); 410 engine.toFetch = new SerializableSet([BOGUS_GUID]); 411 412 let error; 413 try { 414 await sync_engine_and_validate_telem(engine, true); 415 } catch (ex) { 416 error = ex; 417 } 418 ok(!!error); 419 420 // Verify that the bookmark order has been applied. 421 folder1_record = await store.createRecord(folder1.guid); 422 let new_children = folder1_record.children; 423 Assert.deepEqual( 424 new_children.sort(), 425 [folder1_payload.children[0], folder1_payload.children[1]].sort() 426 ); 427 428 let localChildIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds( 429 folder1.guid 430 ); 431 Assert.deepEqual(localChildIds.sort(), [bmk2.guid, bmk1.guid].sort()); 432 } finally { 433 await cleanup(engine, server); 434 } 435 } 436 ); 437 438 add_bookmark_test(async function test_restorePromptsReupload(engine) { 439 await test_restoreOrImport(engine, { replace: true }); 440 }); 441 442 add_bookmark_test(async function test_importPromptsReupload(engine) { 443 await test_restoreOrImport(engine, { replace: false }); 444 }); 445 446 // Test a JSON restore or HTML import. Use JSON if `replace` is `true`, or 447 // HTML otherwise. 448 async function test_restoreOrImport(engine, { replace }) { 449 let verb = replace ? "restore" : "import"; 450 let verbing = replace ? "restoring" : "importing"; 451 let bookmarkUtils = replace ? BookmarkJSONUtils : BookmarkHTMLUtils; 452 453 _(`Ensure that ${verbing} from a backup will reupload all records.`); 454 455 let server = await serverForFoo(engine); 456 await SyncTestingInfrastructure(server); 457 458 let collection = server.user("foo").collection("bookmarks"); 459 460 engine._tracker.start(); // We skip usual startup... 461 462 try { 463 let folder1 = await PlacesUtils.bookmarks.insert({ 464 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 465 type: PlacesUtils.bookmarks.TYPE_FOLDER, 466 title: "Folder 1", 467 }); 468 469 _("Create a single record."); 470 let bmk1 = await PlacesUtils.bookmarks.insert({ 471 parentGuid: folder1.guid, 472 url: "http://getfirefox.com/", 473 title: "Get Firefox!", 474 }); 475 _(`Get Firefox!: ${bmk1.guid}`); 476 477 let backupFilePath = PathUtils.join( 478 PathUtils.tempDir, 479 `t_b_e_${Date.now()}.json` 480 ); 481 482 _("Make a backup."); 483 484 await bookmarkUtils.exportToFile(backupFilePath); 485 486 _("Create a different record and sync."); 487 let bmk2 = await PlacesUtils.bookmarks.insert({ 488 parentGuid: folder1.guid, 489 url: "http://getthunderbird.com/", 490 title: "Get Thunderbird!", 491 }); 492 _(`Get Thunderbird!: ${bmk2.guid}`); 493 494 await PlacesUtils.bookmarks.remove(bmk1.guid); 495 496 let error; 497 try { 498 await sync_engine_and_validate_telem(engine, false); 499 } catch (ex) { 500 error = ex; 501 _("Got error: " + Log.exceptionStr(ex)); 502 } 503 Assert.ok(!error); 504 505 _( 506 "Verify that there's only one bookmark on the server, and it's Thunderbird." 507 ); 508 // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu... 509 let wbos = collection.keys(function (id) { 510 return !["menu", "toolbar", "mobile", "unfiled", folder1.guid].includes( 511 id 512 ); 513 }); 514 Assert.equal(wbos.length, 1); 515 Assert.equal(wbos[0], bmk2.guid); 516 517 _(`Now ${verb} from a backup.`); 518 await bookmarkUtils.importFromFile(backupFilePath, { replace }); 519 520 // If `replace` is `true`, we'll wipe the server on the next sync. 521 let bookmarksCollection = server.user("foo").collection("bookmarks"); 522 _("Verify that we didn't wipe the server."); 523 Assert.ok(!!bookmarksCollection); 524 525 _("Ensure we have the bookmarks we expect locally."); 526 let recordIds = await fetchAllRecordIds(); 527 _("GUIDs: " + JSON.stringify([...recordIds])); 528 529 let bookmarkRecordIds = new Map(); 530 let count = 0; 531 for (let recordId of recordIds) { 532 count++; 533 let info = await PlacesUtils.bookmarks.fetch( 534 PlacesSyncUtils.bookmarks.recordIdToGuid(recordId) 535 ); 536 // Only one bookmark, so _all_ should be Firefox! 537 if (info.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) { 538 _(`Found URI ${info.url.href} for record ID ${recordId}`); 539 bookmarkRecordIds.set(info.url.href, recordId); 540 } 541 } 542 Assert.ok(bookmarkRecordIds.has("http://getfirefox.com/")); 543 if (!replace) { 544 Assert.ok(bookmarkRecordIds.has("http://getthunderbird.com/")); 545 } 546 547 _("Have the correct number of IDs locally, too."); 548 let expectedResults = [ 549 "menu", 550 "toolbar", 551 "mobile", 552 "unfiled", 553 folder1.guid, 554 bmk1.guid, 555 ]; 556 if (!replace) { 557 expectedResults.push("toolbar", folder1.guid, bmk2.guid); 558 } 559 Assert.equal(count, expectedResults.length); 560 561 _("Sync again. This'll wipe bookmarks from the server."); 562 try { 563 await sync_engine_and_validate_telem(engine, false); 564 } catch (ex) { 565 error = ex; 566 _("Got error: " + Log.exceptionStr(ex)); 567 } 568 Assert.ok(!error); 569 570 _("Verify that there's the right bookmarks on the server."); 571 // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu... 572 let payloads = server.user("foo").collection("bookmarks").payloads(); 573 let bookmarkWBOs = payloads.filter(function (wbo) { 574 return wbo.type == "bookmark"; 575 }); 576 577 let folderWBOs = payloads.filter(function (wbo) { 578 return ( 579 wbo.type == "folder" && 580 wbo.id != "menu" && 581 wbo.id != "toolbar" && 582 wbo.id != "unfiled" && 583 wbo.id != "mobile" && 584 wbo.parentid != "menu" 585 ); 586 }); 587 588 let expectedFX = { 589 id: bookmarkRecordIds.get("http://getfirefox.com/"), 590 bmkUri: "http://getfirefox.com/", 591 title: "Get Firefox!", 592 }; 593 let expectedTB = { 594 id: bookmarkRecordIds.get("http://getthunderbird.com/"), 595 bmkUri: "http://getthunderbird.com/", 596 title: "Get Thunderbird!", 597 }; 598 599 let expectedBookmarks; 600 if (replace) { 601 expectedBookmarks = [expectedFX]; 602 } else { 603 expectedBookmarks = [expectedTB, expectedFX]; 604 } 605 606 doCheckWBOs(bookmarkWBOs, expectedBookmarks); 607 608 _("Our old friend Folder 1 is still in play."); 609 let expectedFolder1 = { title: "Folder 1" }; 610 611 let expectedFolders; 612 if (replace) { 613 expectedFolders = [expectedFolder1]; 614 } else { 615 expectedFolders = [expectedFolder1, expectedFolder1]; 616 } 617 618 doCheckWBOs(folderWBOs, expectedFolders); 619 } finally { 620 await cleanup(engine, server); 621 } 622 } 623 624 function doCheckWBOs(WBOs, expected) { 625 Assert.equal(WBOs.length, expected.length); 626 for (let i = 0; i < expected.length; i++) { 627 let lhs = WBOs[i]; 628 let rhs = expected[i]; 629 if ("id" in rhs) { 630 Assert.equal(lhs.id, rhs.id); 631 } 632 if ("bmkUri" in rhs) { 633 Assert.equal(lhs.bmkUri, rhs.bmkUri); 634 } 635 if ("title" in rhs) { 636 Assert.equal(lhs.title, rhs.title); 637 } 638 } 639 } 640 641 function FakeRecord(constructor, r) { 642 this.defaultCleartext = constructor.prototype.defaultCleartext; 643 constructor.call(this, "bookmarks", r.id); 644 for (let x in r) { 645 this[x] = r[x]; 646 } 647 // Borrow the constructor's conversion functions. 648 this.toSyncBookmark = constructor.prototype.toSyncBookmark; 649 this.cleartextToString = constructor.prototype.cleartextToString; 650 } 651 652 // Bug 632287. 653 // (Note that `test_mismatched_folder_types()` in 654 // toolkit/components/places/tests/sync/test_bookmark_kinds.js is an exact 655 // copy of this test, so it's fine to remove it as part of bug 1449730) 656 add_task(async function test_mismatched_types() { 657 _( 658 "Ensure that handling a record that changes type causes deletion " + 659 "then re-adding." 660 ); 661 662 let oldRecord = { 663 id: "l1nZZXfB8nC7", 664 type: "folder", 665 parentName: "Bookmarks Toolbar", 666 title: "Innerst i Sneglehode", 667 description: null, 668 parentid: "toolbar", 669 }; 670 671 let newRecord = { 672 id: "l1nZZXfB8nC7", 673 type: "livemark", 674 siteUri: "http://sneglehode.wordpress.com/", 675 feedUri: "http://sneglehode.wordpress.com/feed/", 676 parentName: "Bookmarks Toolbar", 677 title: "Innerst i Sneglehode", 678 description: null, 679 children: [ 680 "HCRq40Rnxhrd", 681 "YeyWCV1RVsYw", 682 "GCceVZMhvMbP", 683 "sYi2hevdArlF", 684 "vjbZlPlSyGY8", 685 "UtjUhVyrpeG6", 686 "rVq8WMG2wfZI", 687 "Lx0tcy43ZKhZ", 688 "oT74WwV8_j4P", 689 "IztsItWVSo3-", 690 ], 691 parentid: "toolbar", 692 }; 693 694 let engine = new BookmarksEngine(Service); 695 await engine.initialize(); 696 let store = engine._store; 697 let server = await serverForFoo(engine); 698 await SyncTestingInfrastructure(server); 699 700 try { 701 let oldR = new FakeRecord(BookmarkFolder, oldRecord); 702 let newR = new FakeRecord(Livemark, newRecord); 703 oldR.parentid = PlacesUtils.bookmarks.toolbarGuid; 704 newR.parentid = PlacesUtils.bookmarks.toolbarGuid; 705 706 await store.applyIncoming(oldR); 707 await engine._apply(); 708 _("Applied old. It's a folder."); 709 let oldID = await PlacesTestUtils.promiseItemId(oldR.id); 710 _("Old ID: " + oldID); 711 let oldInfo = await PlacesUtils.bookmarks.fetch(oldR.id); 712 Assert.equal(oldInfo.type, PlacesUtils.bookmarks.TYPE_FOLDER); 713 714 await store.applyIncoming(newR); 715 await engine._apply(); 716 } finally { 717 await cleanup(engine, server); 718 await engine.finalize(); 719 } 720 }); 721 722 add_bookmark_test(async function test_misreconciled_root(engine) { 723 _("Ensure that we don't reconcile an arbitrary record with a root."); 724 725 let store = engine._store; 726 let server = await serverForFoo(engine); 727 await SyncTestingInfrastructure(server); 728 729 // Log real hard for this test. 730 store._log.trace = store._log.debug; 731 engine._log.trace = engine._log.debug; 732 733 await engine._syncStartup(); 734 735 // Let's find out where the toolbar is right now. 736 let toolbarBefore = await store.createRecord("toolbar", "bookmarks"); 737 let toolbarIDBefore = await PlacesTestUtils.promiseItemId( 738 PlacesUtils.bookmarks.toolbarGuid 739 ); 740 Assert.notEqual(-1, toolbarIDBefore); 741 742 let parentRecordIDBefore = toolbarBefore.parentid; 743 let parentGUIDBefore = 744 PlacesSyncUtils.bookmarks.recordIdToGuid(parentRecordIDBefore); 745 let parentIDBefore = await PlacesTestUtils.promiseItemId(parentGUIDBefore); 746 Assert.equal("string", typeof parentGUIDBefore); 747 748 _("Current parent: " + parentGUIDBefore + " (" + parentIDBefore + ")."); 749 750 let to_apply = { 751 id: "zzzzzzzzzzzz", 752 type: "folder", 753 title: "Bookmarks Toolbar", 754 description: "Now you're for it.", 755 parentName: "", 756 parentid: "mobile", // Why not? 757 children: [], 758 }; 759 760 let rec = new FakeRecord(BookmarkFolder, to_apply); 761 762 _("Applying record."); 763 let countTelemetry = new SyncedRecordsTelemetry(); 764 await store.applyIncomingBatch([rec], countTelemetry); 765 766 // Ensure that afterwards, toolbar is still there. 767 // As of 2012-12-05, this only passes because Places doesn't use "toolbar" as 768 // the real GUID, instead using a generated one. Sync does the translation. 769 let toolbarAfter = await store.createRecord("toolbar", "bookmarks"); 770 let parentRecordIDAfter = toolbarAfter.parentid; 771 let parentGUIDAfter = 772 PlacesSyncUtils.bookmarks.recordIdToGuid(parentRecordIDAfter); 773 let parentIDAfter = await PlacesTestUtils.promiseItemId(parentGUIDAfter); 774 Assert.equal( 775 await PlacesTestUtils.promiseItemGuid(toolbarIDBefore), 776 PlacesUtils.bookmarks.toolbarGuid 777 ); 778 Assert.equal(parentGUIDBefore, parentGUIDAfter); 779 Assert.equal(parentIDBefore, parentIDAfter); 780 781 await cleanup(engine, server); 782 }); 783 784 add_bookmark_test(async function test_invalid_url(engine) { 785 _("Ensure an incoming invalid bookmark URL causes an outgoing tombstone."); 786 787 let server = await serverForFoo(engine); 788 let collection = server.user("foo").collection("bookmarks"); 789 790 await SyncTestingInfrastructure(server); 791 await engine._syncStartup(); 792 793 // check the URL really is invalid. 794 let url = "https://www.42registry.42/"; 795 Assert.throws(() => Services.io.newURI(url), /invalid/); 796 797 let guid = "abcdefabcdef"; 798 799 let toolbar = new BookmarkFolder("bookmarks", "toolbar"); 800 toolbar.title = "toolbar"; 801 toolbar.parentName = ""; 802 toolbar.parentid = "places"; 803 toolbar.children = [guid]; 804 collection.insert("toolbar", encryptPayload(toolbar.cleartext)); 805 806 let item1 = new Bookmark("bookmarks", guid); 807 item1.bmkUri = "https://www.42registry.42/"; 808 item1.title = "invalid url"; 809 item1.parentName = "Bookmarks Toolbar"; 810 item1.parentid = "toolbar"; 811 item1.dateAdded = 1234; 812 collection.insert(guid, encryptPayload(item1.cleartext)); 813 814 _("syncing."); 815 await sync_engine_and_validate_telem(engine, false); 816 817 // We should find the record now exists on the server as a tombstone. 818 let updated = collection.cleartext(guid); 819 Assert.ok(updated.deleted, "record was deleted"); 820 821 let local = await PlacesUtils.bookmarks.fetch(guid); 822 Assert.deepEqual(local, null, "no local bookmark exists"); 823 824 await cleanup(engine, server); 825 }); 826 827 add_bookmark_test(async function test_sync_dateAdded(engine) { 828 await Service.recordManager.clearCache(); 829 await PlacesSyncUtils.bookmarks.reset(); 830 let store = engine._store; 831 let server = await serverForFoo(engine); 832 await SyncTestingInfrastructure(server); 833 834 let collection = server.user("foo").collection("bookmarks"); 835 836 // TODO: Avoid random orange (bug 1374599), this is only necessary 837 // intermittently - reset the last sync date so that we'll get all bookmarks. 838 await engine.setLastSync(1); 839 840 engine._tracker.start(); // We skip usual startup... 841 842 // Just matters that it's in the past, not how far. 843 let now = Date.now(); 844 let oneYearMS = 365 * 24 * 60 * 60 * 1000; 845 846 try { 847 let toolbar = new BookmarkFolder("bookmarks", "toolbar"); 848 toolbar.title = "toolbar"; 849 toolbar.parentName = ""; 850 toolbar.parentid = "places"; 851 toolbar.children = [ 852 "abcdefabcdef", 853 "aaaaaaaaaaaa", 854 "bbbbbbbbbbbb", 855 "cccccccccccc", 856 "dddddddddddd", 857 "eeeeeeeeeeee", 858 ]; 859 collection.insert("toolbar", encryptPayload(toolbar.cleartext)); 860 861 let item1GUID = "abcdefabcdef"; 862 let item1 = new Bookmark("bookmarks", item1GUID); 863 item1.bmkUri = "https://example.com"; 864 item1.title = "asdf"; 865 item1.parentName = "Bookmarks Toolbar"; 866 item1.parentid = "toolbar"; 867 item1.dateAdded = now - oneYearMS; 868 collection.insert(item1GUID, encryptPayload(item1.cleartext)); 869 870 let item2GUID = "aaaaaaaaaaaa"; 871 let item2 = new Bookmark("bookmarks", item2GUID); 872 item2.bmkUri = "https://example.com/2"; 873 item2.title = "asdf2"; 874 item2.parentName = "Bookmarks Toolbar"; 875 item2.parentid = "toolbar"; 876 item2.dateAdded = now + oneYearMS; 877 const item2LastModified = now / 1000 - 100; 878 collection.insert( 879 item2GUID, 880 encryptPayload(item2.cleartext), 881 item2LastModified 882 ); 883 884 let item3GUID = "bbbbbbbbbbbb"; 885 let item3 = new Bookmark("bookmarks", item3GUID); 886 item3.bmkUri = "https://example.com/3"; 887 item3.title = "asdf3"; 888 item3.parentName = "Bookmarks Toolbar"; 889 item3.parentid = "toolbar"; 890 // no dateAdded 891 collection.insert(item3GUID, encryptPayload(item3.cleartext)); 892 893 let item4GUID = "cccccccccccc"; 894 let item4 = new Bookmark("bookmarks", item4GUID); 895 item4.bmkUri = "https://example.com/4"; 896 item4.title = "asdf4"; 897 item4.parentName = "Bookmarks Toolbar"; 898 item4.parentid = "toolbar"; 899 // no dateAdded, but lastModified in past 900 const item4LastModified = (now - oneYearMS) / 1000; 901 collection.insert( 902 item4GUID, 903 encryptPayload(item4.cleartext), 904 item4LastModified 905 ); 906 907 let item5GUID = "dddddddddddd"; 908 let item5 = new Bookmark("bookmarks", item5GUID); 909 item5.bmkUri = "https://example.com/5"; 910 item5.title = "asdf5"; 911 item5.parentName = "Bookmarks Toolbar"; 912 item5.parentid = "toolbar"; 913 // no dateAdded, lastModified in (near) future. 914 const item5LastModified = (now + 60000) / 1000; 915 collection.insert( 916 item5GUID, 917 encryptPayload(item5.cleartext), 918 item5LastModified 919 ); 920 921 let item6GUID = "eeeeeeeeeeee"; 922 let item6 = new Bookmark("bookmarks", item6GUID); 923 item6.bmkUri = "https://example.com/6"; 924 item6.title = "asdf6"; 925 item6.parentName = "Bookmarks Toolbar"; 926 item6.parentid = "toolbar"; 927 const item6LastModified = (now - oneYearMS) / 1000; 928 collection.insert( 929 item6GUID, 930 encryptPayload(item6.cleartext), 931 item6LastModified 932 ); 933 934 await sync_engine_and_validate_telem(engine, false); 935 936 let record1 = await store.createRecord(item1GUID); 937 let record2 = await store.createRecord(item2GUID); 938 939 equal( 940 item1.dateAdded, 941 record1.dateAdded, 942 "dateAdded in past should be synced" 943 ); 944 equal( 945 record2.dateAdded, 946 item2LastModified * 1000, 947 "dateAdded in future should be ignored in favor of last modified" 948 ); 949 950 let record3 = await store.createRecord(item3GUID); 951 952 ok(record3.dateAdded); 953 // Make sure it's within 24 hours of the right timestamp... This is a little 954 // dodgey but we only really care that it's basically accurate and has the 955 // right day. 956 Assert.less(Math.abs(Date.now() - record3.dateAdded), 24 * 60 * 60 * 1000); 957 958 let record4 = await store.createRecord(item4GUID); 959 equal( 960 record4.dateAdded, 961 item4LastModified * 1000, 962 "If no dateAdded is provided, lastModified should be used" 963 ); 964 965 let record5 = await store.createRecord(item5GUID); 966 equal( 967 record5.dateAdded, 968 item5LastModified * 1000, 969 "If no dateAdded is provided, lastModified should be used (even if it's in the future)" 970 ); 971 972 // Update item2 and try resyncing it. 973 item2.dateAdded = now - 100000; 974 collection.insert( 975 item2GUID, 976 encryptPayload(item2.cleartext), 977 now / 1000 - 50 978 ); 979 980 // Also, add a local bookmark and make sure its date added makes it up to the server 981 let bz = await PlacesUtils.bookmarks.insert({ 982 parentGuid: PlacesUtils.bookmarks.menuGuid, 983 url: "https://bugzilla.mozilla.org/", 984 title: "Bugzilla", 985 }); 986 987 // last sync did a POST, which doesn't advance its lastModified value. 988 // Next sync of the engine doesn't hit info/collections, so lastModified 989 // remains stale. Setting it to null side-steps that. 990 engine.lastModified = null; 991 await sync_engine_and_validate_telem(engine, false); 992 993 let newRecord2 = await store.createRecord(item2GUID); 994 equal( 995 newRecord2.dateAdded, 996 item2.dateAdded, 997 "dateAdded update should work for earlier date" 998 ); 999 1000 let bzWBO = collection.cleartext(bz.guid); 1001 ok(bzWBO.dateAdded, "Locally added dateAdded lost"); 1002 1003 let localRecord = await store.createRecord(bz.guid); 1004 equal( 1005 bzWBO.dateAdded, 1006 localRecord.dateAdded, 1007 "dateAdded should not change during upload" 1008 ); 1009 1010 item2.dateAdded += 10000; 1011 collection.insert( 1012 item2GUID, 1013 encryptPayload(item2.cleartext), 1014 now / 1000 - 10 1015 ); 1016 1017 engine.lastModified = null; 1018 await sync_engine_and_validate_telem(engine, false); 1019 1020 let newerRecord2 = await store.createRecord(item2GUID); 1021 equal( 1022 newerRecord2.dateAdded, 1023 newRecord2.dateAdded, 1024 "dateAdded update should be ignored for later date if we know an earlier one " 1025 ); 1026 } finally { 1027 await cleanup(engine, server); 1028 } 1029 }); 1030 1031 add_task(async function test_buffer_hasDupe() { 1032 await Service.recordManager.clearCache(); 1033 await PlacesSyncUtils.bookmarks.reset(); 1034 let engine = new BookmarksEngine(Service); 1035 await engine.initialize(); 1036 let server = await serverForFoo(engine); 1037 await SyncTestingInfrastructure(server); 1038 let collection = server.user("foo").collection("bookmarks"); 1039 engine._tracker.start(); // We skip usual startup... 1040 try { 1041 let guid1 = Utils.makeGUID(); 1042 let guid2 = Utils.makeGUID(); 1043 await PlacesUtils.bookmarks.insert({ 1044 guid: guid1, 1045 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 1046 url: "https://www.example.com", 1047 title: "example.com", 1048 }); 1049 await PlacesUtils.bookmarks.insert({ 1050 guid: guid2, 1051 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 1052 url: "https://www.example.com", 1053 title: "example.com", 1054 }); 1055 1056 await sync_engine_and_validate_telem(engine, false); 1057 // Make sure we set hasDupe on outgoing records 1058 Assert.ok(collection.payloads().every(payload => payload.hasDupe)); 1059 1060 await PlacesUtils.bookmarks.remove(guid1); 1061 1062 await sync_engine_and_validate_telem(engine, false); 1063 1064 let tombstone = JSON.parse( 1065 JSON.parse(collection.payload(guid1)).ciphertext 1066 ); 1067 // We shouldn't set hasDupe on tombstones. 1068 Assert.ok(tombstone.deleted); 1069 Assert.ok(!tombstone.hasDupe); 1070 1071 let record = JSON.parse(JSON.parse(collection.payload(guid2)).ciphertext); 1072 // We should set hasDupe on weakly uploaded records. 1073 Assert.ok(!record.deleted); 1074 Assert.ok( 1075 record.hasDupe, 1076 "Bookmarks bookmark engine should set hasDupe for weakly uploaded records." 1077 ); 1078 1079 await sync_engine_and_validate_telem(engine, false); 1080 } finally { 1081 await cleanup(engine, server); 1082 await engine.finalize(); 1083 } 1084 }); 1085 1086 // Bug 890217. 1087 add_bookmark_test(async function test_sync_imap_URLs(engine) { 1088 await Service.recordManager.clearCache(); 1089 await PlacesSyncUtils.bookmarks.reset(); 1090 let server = await serverForFoo(engine); 1091 await SyncTestingInfrastructure(server); 1092 1093 let collection = server.user("foo").collection("bookmarks"); 1094 1095 engine._tracker.start(); // We skip usual startup... 1096 1097 try { 1098 collection.insert( 1099 "menu", 1100 encryptPayload({ 1101 id: "menu", 1102 type: "folder", 1103 parentid: "places", 1104 title: "Bookmarks Menu", 1105 children: ["bookmarkAAAA"], 1106 }) 1107 ); 1108 collection.insert( 1109 "bookmarkAAAA", 1110 encryptPayload({ 1111 id: "bookmarkAAAA", 1112 type: "bookmark", 1113 parentid: "menu", 1114 bmkUri: 1115 "imap://vs@eleven.vs.solnicky.cz:993/fetch%3EUID%3E/" + 1116 "INBOX%3E56291?part=1.2&type=image/jpeg&filename=" + 1117 "invalidazPrahy.jpg", 1118 title: 1119 "invalidazPrahy.jpg (JPEG Image, 1280x1024 pixels) - Scaled (71%)", 1120 }) 1121 ); 1122 1123 await PlacesUtils.bookmarks.insert({ 1124 guid: "bookmarkBBBB", 1125 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 1126 url: 1127 "imap://eleven.vs.solnicky.cz:993/fetch%3EUID%3E/" + 1128 "CURRENT%3E2433?part=1.2&type=text/html&filename=TomEdwards.html", 1129 title: "TomEdwards.html", 1130 }); 1131 1132 await sync_engine_and_validate_telem(engine, false); 1133 1134 let aInfo = await PlacesUtils.bookmarks.fetch("bookmarkAAAA"); 1135 equal( 1136 aInfo.url.href, 1137 "imap://vs@eleven.vs.solnicky.cz:993/" + 1138 "fetch%3EUID%3E/INBOX%3E56291?part=1.2&type=image/jpeg&filename=" + 1139 "invalidazPrahy.jpg", 1140 "Remote bookmark A with IMAP URL should exist locally" 1141 ); 1142 1143 let bPayload = collection.cleartext("bookmarkBBBB"); 1144 equal( 1145 bPayload.bmkUri, 1146 "imap://eleven.vs.solnicky.cz:993/" + 1147 "fetch%3EUID%3E/CURRENT%3E2433?part=1.2&type=text/html&filename=" + 1148 "TomEdwards.html", 1149 "Local bookmark B with IMAP URL should exist remotely" 1150 ); 1151 } finally { 1152 await cleanup(engine, server); 1153 } 1154 }); 1155 1156 add_task(async function test_resume_buffer() { 1157 await Service.recordManager.clearCache(); 1158 let engine = new BookmarksEngine(Service); 1159 await engine.initialize(); 1160 await engine._store.wipe(); 1161 await engine.resetClient(); 1162 1163 let server = await serverForFoo(engine); 1164 await SyncTestingInfrastructure(server); 1165 1166 let collection = server.user("foo").collection("bookmarks"); 1167 1168 engine._tracker.start(); // We skip usual startup... 1169 1170 const batchChunkSize = 50; 1171 1172 engine._store._batchChunkSize = batchChunkSize; 1173 try { 1174 let children = []; 1175 1176 let timestamp = round_timestamp(Date.now()); 1177 // Add two chunks worth of records to the server 1178 for (let i = 0; i < batchChunkSize * 2; ++i) { 1179 let cleartext = { 1180 id: Utils.makeGUID(), 1181 type: "bookmark", 1182 parentid: "toolbar", 1183 title: `Bookmark ${i}`, 1184 parentName: "Bookmarks Toolbar", 1185 bmkUri: `https://example.com/${i}`, 1186 }; 1187 let wbo = collection.insert( 1188 cleartext.id, 1189 encryptPayload(cleartext), 1190 timestamp + 10 * i 1191 ); 1192 // Something that is effectively random, but deterministic. 1193 // (This is just to ensure we don't accidentally start using the 1194 // sortindex again). 1195 wbo.sortindex = 1000 + Math.round(Math.sin(i / 5) * 100); 1196 children.push(cleartext.id); 1197 } 1198 1199 // Add the parent of those records, and ensure its timestamp is the most recent. 1200 collection.insert( 1201 "toolbar", 1202 encryptPayload({ 1203 id: "toolbar", 1204 type: "folder", 1205 parentid: "places", 1206 title: "Bookmarks Toolbar", 1207 children, 1208 }), 1209 timestamp + 10 * children.length 1210 ); 1211 1212 // Replace applyIncomingBatch with a custom one that calls the original, 1213 // but forces it to throw on the 2nd chunk. 1214 let origApplyIncomingBatch = engine._store.applyIncomingBatch; 1215 engine._store.applyIncomingBatch = function (records) { 1216 if (records.length > batchChunkSize) { 1217 // Hacky way to make reading from the batchChunkSize'th record throw. 1218 delete records[batchChunkSize]; 1219 Object.defineProperty(records, batchChunkSize, { 1220 get() { 1221 throw new Error("D:"); 1222 }, 1223 }); 1224 } 1225 return origApplyIncomingBatch.call(this, records); 1226 }; 1227 1228 let caughtError; 1229 _("We expect this to fail"); 1230 try { 1231 await sync_engine_and_validate_telem(engine, true); 1232 } catch (e) { 1233 caughtError = e; 1234 } 1235 Assert.ok(caughtError, "Expected engine.sync to throw"); 1236 Assert.equal(caughtError.message, "D:"); 1237 1238 // The buffer subtracts one second from the actual timestamp. 1239 let lastSync = (await engine.getLastSync()) + 1; 1240 // We poisoned the batchChunkSize'th record, so the last successfully 1241 // applied record will be batchChunkSize - 1. 1242 let expectedLastSync = timestamp + 10 * (batchChunkSize - 1); 1243 Assert.equal(expectedLastSync, lastSync); 1244 1245 engine._store.applyIncomingBatch = origApplyIncomingBatch; 1246 1247 await sync_engine_and_validate_telem(engine, false); 1248 1249 // Check that all the children made it onto the correct record. 1250 let toolbarRecord = await engine._store.createRecord("toolbar"); 1251 Assert.deepEqual(toolbarRecord.children.sort(), children.sort()); 1252 } finally { 1253 await cleanup(engine, server); 1254 await engine.finalize(); 1255 } 1256 }); 1257 1258 add_bookmark_test(async function test_livemarks(engine) { 1259 _("Ensure we replace new and existing livemarks with tombstones"); 1260 1261 let server = await serverForFoo(engine); 1262 await SyncTestingInfrastructure(server); 1263 1264 let collection = server.user("foo").collection("bookmarks"); 1265 let now = Date.now(); 1266 1267 try { 1268 _("Insert existing livemark"); 1269 let modifiedForA = now - 5 * 60 * 1000; 1270 await PlacesUtils.bookmarks.insert({ 1271 guid: "livemarkAAAA", 1272 type: PlacesUtils.bookmarks.TYPE_FOLDER, 1273 parentGuid: PlacesUtils.bookmarks.menuGuid, 1274 title: "A", 1275 lastModified: new Date(modifiedForA), 1276 dateAdded: new Date(modifiedForA), 1277 source: PlacesUtils.bookmarks.SOURCE_SYNC, 1278 }); 1279 collection.insert( 1280 "menu", 1281 encryptPayload({ 1282 id: "menu", 1283 type: "folder", 1284 parentName: "", 1285 title: "menu", 1286 children: ["livemarkAAAA"], 1287 parentid: "places", 1288 }), 1289 round_timestamp(modifiedForA) 1290 ); 1291 collection.insert( 1292 "livemarkAAAA", 1293 encryptPayload({ 1294 id: "livemarkAAAA", 1295 type: "livemark", 1296 feedUri: "http://example.com/a", 1297 parentName: "menu", 1298 title: "A", 1299 parentid: "menu", 1300 }), 1301 round_timestamp(modifiedForA) 1302 ); 1303 1304 _("Insert remotely updated livemark"); 1305 await PlacesUtils.bookmarks.insert({ 1306 guid: "livemarkBBBB", 1307 type: PlacesUtils.bookmarks.TYPE_FOLDER, 1308 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 1309 title: "B", 1310 lastModified: new Date(now), 1311 dateAdded: new Date(now), 1312 }); 1313 collection.insert( 1314 "toolbar", 1315 encryptPayload({ 1316 id: "toolbar", 1317 type: "folder", 1318 parentName: "", 1319 title: "toolbar", 1320 children: ["livemarkBBBB"], 1321 parentid: "places", 1322 }), 1323 round_timestamp(now) 1324 ); 1325 collection.insert( 1326 "livemarkBBBB", 1327 encryptPayload({ 1328 id: "livemarkBBBB", 1329 type: "livemark", 1330 feedUri: "http://example.com/b", 1331 parentName: "toolbar", 1332 title: "B", 1333 parentid: "toolbar", 1334 }), 1335 round_timestamp(now) 1336 ); 1337 1338 _("Insert new remote livemark"); 1339 collection.insert( 1340 "unfiled", 1341 encryptPayload({ 1342 id: "unfiled", 1343 type: "folder", 1344 parentName: "", 1345 title: "unfiled", 1346 children: ["livemarkCCCC"], 1347 parentid: "places", 1348 }), 1349 round_timestamp(now) 1350 ); 1351 collection.insert( 1352 "livemarkCCCC", 1353 encryptPayload({ 1354 id: "livemarkCCCC", 1355 type: "livemark", 1356 feedUri: "http://example.com/c", 1357 parentName: "unfiled", 1358 title: "C", 1359 parentid: "unfiled", 1360 }), 1361 round_timestamp(now) 1362 ); 1363 1364 _("Bump last sync time to ignore A"); 1365 await engine.setLastSync(round_timestamp(now) - 60); 1366 1367 _("Sync"); 1368 await sync_engine_and_validate_telem(engine, false); 1369 1370 deepEqual( 1371 collection.keys().sort(), 1372 [ 1373 "livemarkAAAA", 1374 "livemarkBBBB", 1375 "livemarkCCCC", 1376 "menu", 1377 "mobile", 1378 "toolbar", 1379 "unfiled", 1380 ], 1381 "Should store original livemark A and tombstones for B and C on server" 1382 ); 1383 1384 let payloads = collection.payloads(); 1385 1386 deepEqual( 1387 payloads.find(payload => payload.id == "menu").children, 1388 ["livemarkAAAA"], 1389 "Should keep A in menu" 1390 ); 1391 ok( 1392 !payloads.find(payload => payload.id == "livemarkAAAA").deleted, 1393 "Should not upload tombstone for A" 1394 ); 1395 1396 deepEqual( 1397 payloads.find(payload => payload.id == "toolbar").children, 1398 [], 1399 "Should remove B from toolbar" 1400 ); 1401 ok( 1402 payloads.find(payload => payload.id == "livemarkBBBB").deleted, 1403 "Should upload tombstone for B" 1404 ); 1405 1406 deepEqual( 1407 payloads.find(payload => payload.id == "unfiled").children, 1408 [], 1409 "Should remove C from unfiled" 1410 ); 1411 ok( 1412 payloads.find(payload => payload.id == "livemarkCCCC").deleted, 1413 "Should replace C with tombstone" 1414 ); 1415 1416 await assertBookmarksTreeMatches( 1417 "", 1418 [ 1419 { 1420 guid: PlacesUtils.bookmarks.menuGuid, 1421 index: 0, 1422 children: [ 1423 { 1424 guid: "livemarkAAAA", 1425 index: 0, 1426 }, 1427 ], 1428 }, 1429 { 1430 guid: PlacesUtils.bookmarks.toolbarGuid, 1431 index: 1, 1432 }, 1433 { 1434 guid: PlacesUtils.bookmarks.unfiledGuid, 1435 index: 3, 1436 }, 1437 { 1438 guid: PlacesUtils.bookmarks.mobileGuid, 1439 index: 4, 1440 }, 1441 ], 1442 "Should keep A and remove B locally" 1443 ); 1444 } finally { 1445 await cleanup(engine, server); 1446 } 1447 }); 1448 1449 add_bookmark_test(async function test_unknown_fields(engine) { 1450 let store = engine._store; 1451 let server = await serverForFoo(engine); 1452 await SyncTestingInfrastructure(server); 1453 let collection = server.user("foo").collection("bookmarks"); 1454 try { 1455 let folder1 = await PlacesUtils.bookmarks.insert({ 1456 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 1457 type: PlacesUtils.bookmarks.TYPE_FOLDER, 1458 title: "Folder 1", 1459 }); 1460 let bmk1 = await PlacesUtils.bookmarks.insert({ 1461 parentGuid: folder1.guid, 1462 url: "http://getfirefox.com/", 1463 title: "Get Firefox!", 1464 }); 1465 let bmk2 = await PlacesUtils.bookmarks.insert({ 1466 parentGuid: folder1.guid, 1467 url: "http://getthunderbird.com/", 1468 title: "Get Thunderbird!", 1469 }); 1470 let toolbar_record = await store.createRecord("toolbar"); 1471 collection.insert("toolbar", encryptPayload(toolbar_record.cleartext)); 1472 1473 let folder1_record_without_unknown_fields = await store.createRecord( 1474 folder1.guid 1475 ); 1476 collection.insert( 1477 folder1.guid, 1478 encryptPayload(folder1_record_without_unknown_fields.cleartext) 1479 ); 1480 1481 // First bookmark record has an unknown string field 1482 let bmk1_record = await store.createRecord(bmk1.guid); 1483 console.log("bmk1_record: ", bmk1_record); 1484 bmk1_record.cleartext.unknownStrField = 1485 "an unknown field from another client"; 1486 collection.insert(bmk1.guid, encryptPayload(bmk1_record.cleartext)); 1487 1488 // Second bookmark record as an unknown object field 1489 let bmk2_record = await store.createRecord(bmk2.guid); 1490 bmk2_record.cleartext.unknownObjField = { 1491 name: "an unknown object from another client", 1492 }; 1493 collection.insert(bmk2.guid, encryptPayload(bmk2_record.cleartext)); 1494 1495 // Sync the two bookmarks 1496 await sync_engine_and_validate_telem(engine, true); 1497 1498 // Add a folder could also have an unknown field 1499 let folder1_record = await store.createRecord(folder1.guid); 1500 folder1_record.cleartext.unknownStrField = 1501 "a folder could also have an unknown field!"; 1502 collection.insert(folder1.guid, encryptPayload(folder1_record.cleartext)); 1503 1504 // sync the new updates 1505 await engine.setLastSync(1); 1506 await sync_engine_and_validate_telem(engine, true); 1507 1508 let payloads = collection.payloads(); 1509 // Validate the server has the unknown fields at the top level (and now unknownFields) 1510 let server_bmk1 = payloads.find(payload => payload.id == bmk1.guid); 1511 deepEqual( 1512 server_bmk1.unknownStrField, 1513 "an unknown field from another client", 1514 "unknown fields correctly on the record" 1515 ); 1516 Assert.equal(server_bmk1.unknownFields, null); 1517 1518 // Check that the mirror table has unknown fields 1519 let db = await PlacesUtils.promiseDBConnection(); 1520 let rows = await db.executeCached( 1521 ` 1522 SELECT guid, title, unknownFields from items WHERE guid IN 1523 (:bmk1, :bmk2, :folder1)`, 1524 { bmk1: bmk1.guid, bmk2: bmk2.guid, folder1: folder1.guid } 1525 ); 1526 // We should have 3 rows that came from the server 1527 Assert.equal(rows.length, 3); 1528 1529 // Bookmark 1 - unknown string field 1530 let remote_bmk1 = rows.find( 1531 row => row.getResultByName("guid") == bmk1.guid 1532 ); 1533 Assert.equal(remote_bmk1.getResultByName("title"), "Get Firefox!"); 1534 deepEqual(JSON.parse(remote_bmk1.getResultByName("unknownFields")), { 1535 unknownStrField: "an unknown field from another client", 1536 }); 1537 1538 // Bookmark 2 - unknown object field 1539 let remote_bmk2 = rows.find( 1540 row => row.getResultByName("guid") == bmk2.guid 1541 ); 1542 Assert.equal(remote_bmk2.getResultByName("title"), "Get Thunderbird!"); 1543 deepEqual(JSON.parse(remote_bmk2.getResultByName("unknownFields")), { 1544 unknownObjField: { 1545 name: "an unknown object from another client", 1546 }, 1547 }); 1548 1549 // Folder with unknown field 1550 1551 // check the server still has the unknown field 1552 deepEqual( 1553 payloads.find(payload => payload.id == folder1.guid).unknownStrField, 1554 "a folder could also have an unknown field!", 1555 "Server still has the unknown field" 1556 ); 1557 1558 let remote_folder = rows.find( 1559 row => row.getResultByName("guid") == folder1.guid 1560 ); 1561 Assert.equal(remote_folder.getResultByName("title"), "Folder 1"); 1562 deepEqual(JSON.parse(remote_folder.getResultByName("unknownFields")), { 1563 unknownStrField: "a folder could also have an unknown field!", 1564 }); 1565 } finally { 1566 await cleanup(engine, server); 1567 } 1568 });