test_engine_changes_during_sync.js (19323B)
1 const { FormHistory } = ChromeUtils.importESModule( 2 "resource://gre/modules/FormHistory.sys.mjs" 3 ); 4 const { Service } = ChromeUtils.importESModule( 5 "resource://services-sync/service.sys.mjs" 6 ); 7 const { Bookmark, BookmarkFolder, BookmarkQuery } = ChromeUtils.importESModule( 8 "resource://services-sync/engines/bookmarks.sys.mjs" 9 ); 10 const { HistoryRec } = ChromeUtils.importESModule( 11 "resource://services-sync/engines/history.sys.mjs" 12 ); 13 const { FormRec } = ChromeUtils.importESModule( 14 "resource://services-sync/engines/forms.sys.mjs" 15 ); 16 const { LoginRec } = ChromeUtils.importESModule( 17 "resource://services-sync/engines/passwords.sys.mjs" 18 ); 19 const { PrefRec } = ChromeUtils.importESModule( 20 "resource://services-sync/engines/prefs.sys.mjs" 21 ); 22 23 const LoginInfo = Components.Constructor( 24 "@mozilla.org/login-manager/loginInfo;1", 25 Ci.nsILoginInfo, 26 "init" 27 ); 28 29 /** 30 * We don't test the clients or tabs engines because neither has conflict 31 * resolution logic. The clients engine syncs twice per global sync, and 32 * custom conflict resolution logic for commands that doesn't use 33 * timestamps. Tabs doesn't have conflict resolution at all, since it's 34 * read-only. 35 */ 36 37 async function assertChildGuids(folderGuid, expectedChildGuids, message) { 38 let tree = await PlacesUtils.promiseBookmarksTree(folderGuid); 39 let childGuids = tree.children.map(child => child.guid); 40 deepEqual(childGuids, expectedChildGuids, message); 41 } 42 43 async function cleanup(engine, server) { 44 await engine._tracker.stop(); 45 await engine._store.wipe(); 46 for (const pref of Svc.PrefBranch.getChildList("")) { 47 Svc.PrefBranch.clearUserPref(pref); 48 } 49 Service.recordManager.clearCache(); 50 await promiseStopServer(server); 51 } 52 53 add_task(async function test_history_change_during_sync() { 54 _("Ensure that we don't bump the score when applying history records."); 55 56 enableValidationPrefs(); 57 58 let engine = Service.engineManager.get("history"); 59 let server = await serverForEnginesWithKeys({ foo: "password" }, [engine]); 60 await SyncTestingInfrastructure(server); 61 let collection = server.user("foo").collection("history"); 62 63 // Override `uploadOutgoing` to insert a record while we're applying 64 // changes. The tracker should ignore this change. 65 let uploadOutgoing = engine._uploadOutgoing; 66 engine._uploadOutgoing = async function () { 67 engine._uploadOutgoing = uploadOutgoing; 68 try { 69 await uploadOutgoing.call(this); 70 } finally { 71 _("Inserting local history visit"); 72 await addVisit("during_sync"); 73 await engine._tracker.asyncObserver.promiseObserversComplete(); 74 } 75 }; 76 77 engine._tracker.start(); 78 79 try { 80 let remoteRec = new HistoryRec("history", "UrOOuzE5QM-e"); 81 remoteRec.histUri = "http://getfirefox.com/"; 82 remoteRec.title = "Get Firefox!"; 83 remoteRec.visits = [ 84 { 85 date: PlacesUtils.toPRTime(Date.now()), 86 type: PlacesUtils.history.TRANSITION_TYPED, 87 }, 88 ]; 89 collection.insert(remoteRec.id, encryptPayload(remoteRec.cleartext)); 90 91 await sync_engine_and_validate_telem(engine, true); 92 strictEqual( 93 Service.scheduler.globalScore, 94 0, 95 "Should not bump global score for visits added during sync" 96 ); 97 98 equal( 99 collection.count(), 100 1, 101 "New local visit should not exist on server after first sync" 102 ); 103 104 await sync_engine_and_validate_telem(engine, true); 105 strictEqual( 106 Service.scheduler.globalScore, 107 0, 108 "Should not bump global score during second history sync" 109 ); 110 111 equal( 112 collection.count(), 113 2, 114 "New local visit should exist on server after second sync" 115 ); 116 } finally { 117 engine._uploadOutgoing = uploadOutgoing; 118 await cleanup(engine, server); 119 } 120 }); 121 122 add_task(async function test_passwords_change_during_sync() { 123 _("Ensure that we don't bump the score when applying passwords."); 124 125 enableValidationPrefs(); 126 127 let engine = Service.engineManager.get("passwords"); 128 let server = await serverForEnginesWithKeys({ foo: "password" }, [engine]); 129 await SyncTestingInfrastructure(server); 130 let collection = server.user("foo").collection("passwords"); 131 132 let uploadOutgoing = engine._uploadOutgoing; 133 engine._uploadOutgoing = async function () { 134 engine._uploadOutgoing = uploadOutgoing; 135 try { 136 await uploadOutgoing.call(this); 137 } finally { 138 _("Inserting local password"); 139 let login = new LoginInfo( 140 "https://example.com", 141 "", 142 null, 143 "username", 144 "password", 145 "", 146 "" 147 ); 148 await Services.logins.addLoginAsync(login); 149 await engine._tracker.asyncObserver.promiseObserversComplete(); 150 } 151 }; 152 153 engine._tracker.start(); 154 155 try { 156 let remoteRec = new LoginRec( 157 "passwords", 158 "{765e3d6e-071d-d640-a83d-81a7eb62d3ed}" 159 ); 160 remoteRec.formSubmitURL = ""; 161 remoteRec.httpRealm = ""; 162 remoteRec.hostname = "https://mozilla.org"; 163 remoteRec.username = "username"; 164 remoteRec.password = "sekrit"; 165 remoteRec.timeCreated = Date.now(); 166 remoteRec.timePasswordChanged = Date.now(); 167 collection.insert(remoteRec.id, encryptPayload(remoteRec.cleartext)); 168 169 await sync_engine_and_validate_telem(engine, true); 170 strictEqual( 171 Service.scheduler.globalScore, 172 0, 173 "Should not bump global score for passwords added during first sync" 174 ); 175 176 equal( 177 collection.count(), 178 1, 179 "New local password should not exist on server after first sync" 180 ); 181 182 await sync_engine_and_validate_telem(engine, true); 183 strictEqual( 184 Service.scheduler.globalScore, 185 0, 186 "Should not bump global score during second passwords sync" 187 ); 188 189 equal( 190 collection.count(), 191 2, 192 "New local password should exist on server after second sync" 193 ); 194 } finally { 195 engine._uploadOutgoing = uploadOutgoing; 196 await cleanup(engine, server); 197 } 198 }); 199 200 add_task(async function test_prefs_change_during_sync() { 201 _("Ensure that we don't bump the score when applying prefs."); 202 203 const TEST_PREF = "test.duringSync"; 204 // create a "control pref" for the pref we sync. 205 Services.prefs.setBoolPref("services.sync.prefs.sync.test.duringSync", true); 206 207 enableValidationPrefs(); 208 209 let engine = Service.engineManager.get("prefs"); 210 let server = await serverForEnginesWithKeys({ foo: "password" }, [engine]); 211 await SyncTestingInfrastructure(server); 212 let collection = server.user("foo").collection("prefs"); 213 214 let uploadOutgoing = engine._uploadOutgoing; 215 engine._uploadOutgoing = async function () { 216 engine._uploadOutgoing = uploadOutgoing; 217 try { 218 await uploadOutgoing.call(this); 219 } finally { 220 _("Updating local pref value"); 221 // Change the value of a synced pref. 222 Services.prefs.setStringPref(TEST_PREF, "hello"); 223 await engine._tracker.asyncObserver.promiseObserversComplete(); 224 } 225 }; 226 227 engine._tracker.start(); 228 229 try { 230 // All synced prefs are stored in a single record, so we'll only ever 231 // have one record on the server. This test just checks that we don't 232 // track or upload prefs changed during the sync. 233 let guid = CommonUtils.encodeBase64URL(Services.appinfo.ID); 234 let remoteRec = new PrefRec("prefs", guid); 235 remoteRec.value = { 236 [TEST_PREF]: "world", 237 }; 238 collection.insert(remoteRec.id, encryptPayload(remoteRec.cleartext)); 239 240 await sync_engine_and_validate_telem(engine, true); 241 strictEqual( 242 Service.scheduler.globalScore, 243 0, 244 "Should not bump global score for prefs added during first sync" 245 ); 246 let payloads = collection.payloads(); 247 equal( 248 payloads.length, 249 1, 250 "Should not upload multiple prefs records after first sync" 251 ); 252 equal( 253 payloads[0].value[TEST_PREF], 254 "world", 255 "Should not upload pref value changed during first sync" 256 ); 257 258 await sync_engine_and_validate_telem(engine, true); 259 strictEqual( 260 Service.scheduler.globalScore, 261 0, 262 "Should not bump global score during second prefs sync" 263 ); 264 payloads = collection.payloads(); 265 equal( 266 payloads.length, 267 1, 268 "Should not upload multiple prefs records after second sync" 269 ); 270 equal( 271 payloads[0].value[TEST_PREF], 272 "hello", 273 "Should upload changed pref value during second sync" 274 ); 275 } finally { 276 engine._uploadOutgoing = uploadOutgoing; 277 await cleanup(engine, server); 278 Services.prefs.clearUserPref(TEST_PREF); 279 } 280 }); 281 282 add_task(async function test_forms_change_during_sync() { 283 _("Ensure that we don't bump the score when applying form records."); 284 285 enableValidationPrefs(); 286 287 let engine = Service.engineManager.get("forms"); 288 let server = await serverForEnginesWithKeys({ foo: "password" }, [engine]); 289 await SyncTestingInfrastructure(server); 290 let collection = server.user("foo").collection("forms"); 291 292 let uploadOutgoing = engine._uploadOutgoing; 293 engine._uploadOutgoing = async function () { 294 engine._uploadOutgoing = uploadOutgoing; 295 try { 296 await uploadOutgoing.call(this); 297 } finally { 298 _("Inserting local form history entry"); 299 await FormHistory.update([ 300 { 301 op: "add", 302 fieldname: "favoriteDrink", 303 value: "cocoa", 304 }, 305 ]); 306 await engine._tracker.asyncObserver.promiseObserversComplete(); 307 } 308 }; 309 310 engine._tracker.start(); 311 312 try { 313 // Add an existing remote form history entry. We shouldn't bump the score when 314 // we apply this record. 315 let remoteRec = new FormRec("forms", "Tl9dHgmJSR6FkyxS"); 316 remoteRec.name = "name"; 317 remoteRec.value = "alice"; 318 collection.insert(remoteRec.id, encryptPayload(remoteRec.cleartext)); 319 320 await sync_engine_and_validate_telem(engine, true); 321 strictEqual( 322 Service.scheduler.globalScore, 323 0, 324 "Should not bump global score for forms added during first sync" 325 ); 326 327 equal( 328 collection.count(), 329 1, 330 "New local form should not exist on server after first sync" 331 ); 332 333 await sync_engine_and_validate_telem(engine, true); 334 strictEqual( 335 Service.scheduler.globalScore, 336 0, 337 "Should not bump global score during second forms sync" 338 ); 339 340 equal( 341 collection.count(), 342 2, 343 "New local form should exist on server after second sync" 344 ); 345 } finally { 346 engine._uploadOutgoing = uploadOutgoing; 347 await cleanup(engine, server); 348 } 349 }); 350 351 add_task(async function test_bookmark_change_during_sync() { 352 _("Ensure that we track bookmark changes made during a sync."); 353 354 enableValidationPrefs(); 355 let schedulerProto = Object.getPrototypeOf(Service.scheduler); 356 let syncThresholdDescriptor = Object.getOwnPropertyDescriptor( 357 schedulerProto, 358 "syncThreshold" 359 ); 360 Object.defineProperty(Service.scheduler, "syncThreshold", { 361 // Trigger resync if any changes exist, rather than deciding based on the 362 // normal sync threshold. 363 get: () => 0, 364 }); 365 366 let engine = Service.engineManager.get("bookmarks"); 367 let server = await serverForEnginesWithKeys({ foo: "password" }, [engine]); 368 await SyncTestingInfrastructure(server); 369 370 // Already-tracked bookmarks that shouldn't be uploaded during the first sync. 371 let bzBmk = await PlacesUtils.bookmarks.insert({ 372 parentGuid: PlacesUtils.bookmarks.menuGuid, 373 url: "https://bugzilla.mozilla.org/", 374 title: "Bugzilla", 375 }); 376 _(`Bugzilla GUID: ${bzBmk.guid}`); 377 378 await PlacesTestUtils.setBookmarkSyncFields({ 379 guid: bzBmk.guid, 380 syncChangeCounter: 0, 381 syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL, 382 }); 383 384 let collection = server.user("foo").collection("bookmarks"); 385 386 let bmk3; // New child of Folder 1, created locally during sync. 387 388 let uploadOutgoing = engine._uploadOutgoing; 389 engine._uploadOutgoing = async function () { 390 engine._uploadOutgoing = uploadOutgoing; 391 try { 392 await uploadOutgoing.call(this); 393 } finally { 394 _("Inserting bookmark into local store"); 395 bmk3 = await PlacesUtils.bookmarks.insert({ 396 parentGuid: folder1.guid, 397 url: "https://mozilla.org/", 398 title: "Mozilla", 399 }); 400 await engine._tracker.asyncObserver.promiseObserversComplete(); 401 } 402 }; 403 404 // New bookmarks that should be uploaded during the first sync. 405 let folder1 = await PlacesUtils.bookmarks.insert({ 406 type: PlacesUtils.bookmarks.TYPE_FOLDER, 407 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 408 title: "Folder 1", 409 }); 410 _(`Folder GUID: ${folder1.guid}`); 411 412 let tbBmk = await PlacesUtils.bookmarks.insert({ 413 parentGuid: folder1.guid, 414 url: "http://getthunderbird.com/", 415 title: "Get Thunderbird!", 416 }); 417 _(`Thunderbird GUID: ${tbBmk.guid}`); 418 419 engine._tracker.start(); 420 421 try { 422 let bmk2_guid = "get-firefox1"; // New child of Folder 1, created remotely. 423 let folder2_guid = "folder2-1111"; // New folder, created remotely. 424 let tagQuery_guid = "tag-query111"; // New tag query child of Folder 2, created remotely. 425 let bmk4_guid = "example-org1"; // New tagged child of Folder 2, created remotely. 426 { 427 // An existing record changed on the server that should not trigger 428 // another sync when applied. 429 let remoteBzBmk = new Bookmark("bookmarks", bzBmk.guid); 430 remoteBzBmk.bmkUri = "https://bugzilla.mozilla.org/"; 431 remoteBzBmk.description = "New description"; 432 remoteBzBmk.title = "Bugzilla"; 433 remoteBzBmk.tags = ["new", "tags"]; 434 remoteBzBmk.parentName = "Bookmarks Menu"; 435 remoteBzBmk.parentid = "menu"; 436 collection.insert(bzBmk.guid, encryptPayload(remoteBzBmk.cleartext)); 437 438 let remoteFolder = new BookmarkFolder("bookmarks", folder2_guid); 439 remoteFolder.title = "Folder 2"; 440 remoteFolder.children = [bmk4_guid, tagQuery_guid]; 441 remoteFolder.parentName = "Bookmarks Menu"; 442 remoteFolder.parentid = "menu"; 443 collection.insert(folder2_guid, encryptPayload(remoteFolder.cleartext)); 444 445 let remoteFxBmk = new Bookmark("bookmarks", bmk2_guid); 446 remoteFxBmk.bmkUri = "http://getfirefox.com/"; 447 remoteFxBmk.description = "Firefox is awesome."; 448 remoteFxBmk.title = "Get Firefox!"; 449 remoteFxBmk.tags = ["firefox", "awesome", "browser"]; 450 remoteFxBmk.keyword = "awesome"; 451 remoteFxBmk.parentName = "Folder 1"; 452 remoteFxBmk.parentid = folder1.guid; 453 collection.insert(bmk2_guid, encryptPayload(remoteFxBmk.cleartext)); 454 455 // A tag query referencing a nonexistent tag folder, which we should 456 // create locally when applying the record. 457 let remoteTagQuery = new BookmarkQuery("bookmarks", tagQuery_guid); 458 remoteTagQuery.bmkUri = "place:type=7&folder=999"; 459 remoteTagQuery.title = "Taggy tags"; 460 remoteTagQuery.folderName = "taggy"; 461 remoteTagQuery.parentName = "Folder 2"; 462 remoteTagQuery.parentid = folder2_guid; 463 collection.insert( 464 tagQuery_guid, 465 encryptPayload(remoteTagQuery.cleartext) 466 ); 467 468 // A bookmark that should appear in the results for the tag query. 469 let remoteTaggedBmk = new Bookmark("bookmarks", bmk4_guid); 470 remoteTaggedBmk.bmkUri = "https://example.org/"; 471 remoteTaggedBmk.title = "Tagged bookmark"; 472 remoteTaggedBmk.tags = ["taggy"]; 473 remoteTaggedBmk.parentName = "Folder 2"; 474 remoteTaggedBmk.parentid = folder2_guid; 475 collection.insert(bmk4_guid, encryptPayload(remoteTaggedBmk.cleartext)); 476 477 collection.insert( 478 "toolbar", 479 encryptPayload({ 480 id: "toolbar", 481 type: "folder", 482 title: "toolbar", 483 children: [folder1.guid], 484 parentName: "places", 485 parentid: "places", 486 }) 487 ); 488 489 collection.insert( 490 "menu", 491 encryptPayload({ 492 id: "menu", 493 type: "folder", 494 title: "menu", 495 children: [bzBmk.guid, folder2_guid], 496 parentName: "places", 497 parentid: "places", 498 }) 499 ); 500 501 collection.insert( 502 folder1.guid, 503 encryptPayload({ 504 id: folder1.guid, 505 type: "folder", 506 title: "Folder 1", 507 children: [bmk2_guid], 508 parentName: "toolbar", 509 parentid: "toolbar", 510 }) 511 ); 512 } 513 514 await assertChildGuids( 515 folder1.guid, 516 [tbBmk.guid], 517 "Folder should have 1 child before first sync" 518 ); 519 520 let pingsPromise = wait_for_pings(2); 521 522 let changes = await PlacesSyncUtils.bookmarks.pullChanges(); 523 deepEqual( 524 Object.keys(changes).sort(), 525 [folder1.guid, tbBmk.guid, "menu", "mobile", "toolbar", "unfiled"].sort(), 526 "Should track bookmark and folder created before first sync" 527 ); 528 529 // Unlike the tests above, we can't use `sync_engine_and_validate_telem` 530 // because the bookmarks engine will automatically schedule a follow-up 531 // sync for us. 532 _("Perform first sync and immediate follow-up sync"); 533 Service.sync({ engines: ["bookmarks"] }); 534 535 let pings = await pingsPromise; 536 equal(pings.length, 2, "Should submit two pings"); 537 ok( 538 pings.every(p => { 539 assert_success_ping(p); 540 return p.syncs.length == 1; 541 }), 542 "Should submit 1 sync per ping" 543 ); 544 545 strictEqual( 546 Service.scheduler.globalScore, 547 0, 548 "Should reset global score after follow-up sync" 549 ); 550 ok(bmk3, "Should insert bookmark during first sync to simulate change"); 551 ok( 552 collection.wbo(bmk3.guid), 553 "Changed bookmark should be uploaded after follow-up sync" 554 ); 555 556 let bmk2 = await PlacesUtils.bookmarks.fetch({ 557 guid: bmk2_guid, 558 }); 559 ok(bmk2, "Remote bookmark should be applied during first sync"); 560 { 561 // We only check child GUIDs, and not their order, because the exact 562 // order is an implementation detail. 563 let folder1Children = await PlacesSyncUtils.bookmarks.fetchChildRecordIds( 564 folder1.guid 565 ); 566 deepEqual( 567 folder1Children.sort(), 568 [bmk2_guid, tbBmk.guid, bmk3.guid].sort(), 569 "Folder 1 should have 3 children after first sync" 570 ); 571 } 572 await assertChildGuids( 573 folder2_guid, 574 [bmk4_guid, tagQuery_guid], 575 "Folder 2 should have 2 children after first sync" 576 ); 577 let taggedURIs = []; 578 await PlacesUtils.bookmarks.fetch({ tags: ["taggy"] }, b => 579 taggedURIs.push(b.url) 580 ); 581 equal(taggedURIs.length, 1, "Should have 1 tagged URI"); 582 equal( 583 taggedURIs[0].href, 584 "https://example.org/", 585 "Synced tagged bookmark should appear in tagged URI list" 586 ); 587 588 changes = await PlacesSyncUtils.bookmarks.pullChanges(); 589 deepEqual( 590 changes, 591 {}, 592 "Should have already uploaded changes in follow-up sync" 593 ); 594 595 // First ping won't include validation data, since we've changed bookmarks 596 // and `canValidate` will indicate it can't proceed. 597 let engineData = pings.map(p => { 598 return p.syncs[0].engines.find(e => e.name == "bookmarks-buffered"); 599 }); 600 ok(engineData[0].validation, "Engine should validate after first sync"); 601 ok(engineData[1].validation, "Engine should validate after second sync"); 602 } finally { 603 Object.defineProperty( 604 schedulerProto, 605 "syncThreshold", 606 syncThresholdDescriptor 607 ); 608 engine._uploadOutgoing = uploadOutgoing; 609 await cleanup(engine, server); 610 } 611 });