test_clients_engine.js (59048B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { ClientEngine, ClientsRec } = ChromeUtils.importESModule( 5 "resource://services-sync/engines/clients.sys.mjs" 6 ); 7 const { CryptoWrapper } = ChromeUtils.importESModule( 8 "resource://services-sync/record.sys.mjs" 9 ); 10 const { Service } = ChromeUtils.importESModule( 11 "resource://services-sync/service.sys.mjs" 12 ); 13 14 const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days 15 const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day 16 17 let engine; 18 19 /** 20 * Unpack the record with this ID, and verify that it has the same version that 21 * we should be putting into records. 22 */ 23 async function check_record_version(user, id) { 24 let payload = user.collection("clients").wbo(id).data; 25 26 let rec = new CryptoWrapper(); 27 rec.id = id; 28 rec.collection = "clients"; 29 rec.ciphertext = payload.ciphertext; 30 rec.hmac = payload.hmac; 31 rec.IV = payload.IV; 32 33 let cleartext = await rec.decrypt( 34 Service.collectionKeys.keyForCollection("clients") 35 ); 36 37 _("Payload is " + JSON.stringify(cleartext)); 38 equal(Services.appinfo.version, cleartext.version); 39 equal(1, cleartext.protocols.length); 40 equal("1.5", cleartext.protocols[0]); 41 } 42 43 // compare 2 different command arrays, taking into account that a flowID 44 // attribute must exist, be unique in the commands, but isn't specified in 45 // "expected" as the value isn't known. 46 function compareCommands(actual, expected, description) { 47 let tweakedActual = JSON.parse(JSON.stringify(actual)); 48 tweakedActual.map(elt => delete elt.flowID); 49 deepEqual(tweakedActual, expected, description); 50 // each item must have a unique flowID. 51 let allIDs = new Set(actual.map(elt => elt.flowID).filter(fid => !!fid)); 52 equal(allIDs.size, actual.length, "all items have unique IDs"); 53 } 54 55 async function syncClientsEngine(server) { 56 engine._lastFxADevicesFetch = 0; 57 engine.lastModified = server.getCollection("foo", "clients").timestamp; 58 await engine._sync(); 59 } 60 61 add_setup(async function () { 62 engine = Service.clientsEngine; 63 64 do_get_profile(); // FOG requires a profile directory. 65 Services.fog.initializeFOG(); 66 }); 67 68 async function cleanup() { 69 for (const pref of Svc.PrefBranch.getChildList("")) { 70 Svc.PrefBranch.clearUserPref(pref); 71 } 72 await engine._tracker.clearChangedIDs(); 73 await engine._resetClient(); 74 // un-cleanup the logs (the resetBranch will have reset their levels), since 75 // not all the tests use SyncTestingInfrastructure, and it's cheap. 76 syncTestLogging(); 77 // We don't finalize storage at cleanup, since we use the same clients engine 78 // instance across all tests. 79 } 80 81 add_task(async function test_bad_hmac() { 82 _("Ensure that Clients engine deletes corrupt records."); 83 let deletedCollections = []; 84 let deletedItems = []; 85 let callback = { 86 onItemDeleted(username, coll, wboID) { 87 deletedItems.push(coll + "/" + wboID); 88 }, 89 onCollectionDeleted(username, coll) { 90 deletedCollections.push(coll); 91 }, 92 }; 93 Object.setPrototypeOf(callback, SyncServerCallback); 94 let server = await serverForFoo(engine, callback); 95 let user = server.user("foo"); 96 97 function check_clients_count(expectedCount) { 98 let coll = user.collection("clients"); 99 100 // Treat a non-existent collection as empty. 101 equal(expectedCount, coll ? coll.count() : 0); 102 } 103 104 function check_client_deleted(id) { 105 let coll = user.collection("clients"); 106 let wbo = coll.wbo(id); 107 return !wbo || !wbo.payload; 108 } 109 110 async function uploadNewKeys() { 111 await generateNewKeys(Service.collectionKeys); 112 let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); 113 await serverKeys.encrypt(Service.identity.syncKeyBundle); 114 ok( 115 (await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success 116 ); 117 } 118 119 try { 120 await configureIdentity({ username: "foo" }, server); 121 await Service.login(); 122 123 await generateNewKeys(Service.collectionKeys); 124 125 _("First sync, client record is uploaded"); 126 equal(engine.lastRecordUpload, 0); 127 ok(engine.isFirstSync); 128 check_clients_count(0); 129 await syncClientsEngine(server); 130 check_clients_count(1); 131 Assert.greater(engine.lastRecordUpload, 0); 132 ok(!engine.isFirstSync); 133 134 // Our uploaded record has a version. 135 await check_record_version(user, engine.localID); 136 137 // Initial setup can wipe the server, so clean up. 138 deletedCollections = []; 139 deletedItems = []; 140 141 _("Change our keys and our client ID, reupload keys."); 142 let oldLocalID = engine.localID; // Preserve to test for deletion! 143 engine.localID = Utils.makeGUID(); 144 await engine.resetClient(); 145 await generateNewKeys(Service.collectionKeys); 146 let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); 147 await serverKeys.encrypt(Service.identity.syncKeyBundle); 148 ok( 149 (await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success 150 ); 151 152 _("Sync."); 153 await syncClientsEngine(server); 154 155 _("Old record " + oldLocalID + " was deleted, new one uploaded."); 156 check_clients_count(1); 157 check_client_deleted(oldLocalID); 158 159 _( 160 "Now change our keys but don't upload them. " + 161 "That means we get an HMAC error but redownload keys." 162 ); 163 Service.lastHMACEvent = 0; 164 engine.localID = Utils.makeGUID(); 165 await engine.resetClient(); 166 await generateNewKeys(Service.collectionKeys); 167 deletedCollections = []; 168 deletedItems = []; 169 check_clients_count(1); 170 await syncClientsEngine(server); 171 172 _("Old record was not deleted, new one uploaded."); 173 equal(deletedCollections.length, 0); 174 equal(deletedItems.length, 0); 175 check_clients_count(2); 176 177 _( 178 "Now try the scenario where our keys are wrong *and* there's a bad record." 179 ); 180 // Clean up and start fresh. 181 user.collection("clients")._wbos = {}; 182 Service.lastHMACEvent = 0; 183 engine.localID = Utils.makeGUID(); 184 await engine.resetClient(); 185 deletedCollections = []; 186 deletedItems = []; 187 check_clients_count(0); 188 189 await uploadNewKeys(); 190 191 // Sync once to upload a record. 192 await syncClientsEngine(server); 193 check_clients_count(1); 194 195 // Generate and upload new keys, so the old client record is wrong. 196 await uploadNewKeys(); 197 198 // Create a new client record and new keys. Now our keys are wrong, as well 199 // as the object on the server. We'll download the new keys and also delete 200 // the bad client record. 201 oldLocalID = engine.localID; // Preserve to test for deletion! 202 engine.localID = Utils.makeGUID(); 203 await engine.resetClient(); 204 await generateNewKeys(Service.collectionKeys); 205 let oldKey = Service.collectionKeys.keyForCollection(); 206 207 equal(deletedCollections.length, 0); 208 equal(deletedItems.length, 0); 209 await syncClientsEngine(server); 210 equal(deletedItems.length, 1); 211 check_client_deleted(oldLocalID); 212 check_clients_count(1); 213 let newKey = Service.collectionKeys.keyForCollection(); 214 ok(!oldKey.equals(newKey)); 215 } finally { 216 await cleanup(); 217 await promiseStopServer(server); 218 } 219 }); 220 221 add_task(async function test_properties() { 222 _("Test lastRecordUpload property"); 223 try { 224 equal( 225 Svc.PrefBranch.getPrefType("clients.lastRecordUpload"), 226 Ci.nsIPrefBranch.PREF_INVALID 227 ); 228 equal(engine.lastRecordUpload, 0); 229 230 let now = Date.now(); 231 engine.lastRecordUpload = now / 1000; 232 equal(engine.lastRecordUpload, Math.floor(now / 1000)); 233 } finally { 234 await cleanup(); 235 } 236 }); 237 238 add_task(async function test_full_sync() { 239 _("Ensure that Clients engine fetches all records for each sync."); 240 241 let now = new_timestamp(); 242 let server = await serverForFoo(engine); 243 let user = server.user("foo"); 244 245 await SyncTestingInfrastructure(server); 246 await generateNewKeys(Service.collectionKeys); 247 248 let activeID = Utils.makeGUID(); 249 user.collection("clients").insertRecord( 250 { 251 id: activeID, 252 name: "Active client", 253 type: "desktop", 254 commands: [], 255 version: "48", 256 protocols: ["1.5"], 257 }, 258 now - 10 259 ); 260 261 let deletedID = Utils.makeGUID(); 262 user.collection("clients").insertRecord( 263 { 264 id: deletedID, 265 name: "Client to delete", 266 type: "desktop", 267 commands: [], 268 version: "48", 269 protocols: ["1.5"], 270 }, 271 now - 10 272 ); 273 274 try { 275 let store = engine._store; 276 277 _("First sync. 2 records downloaded; our record uploaded."); 278 strictEqual(engine.lastRecordUpload, 0); 279 ok(engine.isFirstSync); 280 await syncClientsEngine(server); 281 Assert.greater(engine.lastRecordUpload, 0); 282 ok(!engine.isFirstSync); 283 deepEqual( 284 user.collection("clients").keys().sort(), 285 [activeID, deletedID, engine.localID].sort(), 286 "Our record should be uploaded on first sync" 287 ); 288 let ids = await store.getAllIDs(); 289 deepEqual( 290 Object.keys(ids).sort(), 291 [activeID, deletedID, engine.localID].sort(), 292 "Other clients should be downloaded on first sync" 293 ); 294 295 _("Delete a record, then sync again"); 296 let collection = server.getCollection("foo", "clients"); 297 collection.remove(deletedID); 298 // Simulate a timestamp update in info/collections. 299 await syncClientsEngine(server); 300 301 _("Record should be updated"); 302 ids = await store.getAllIDs(); 303 deepEqual( 304 Object.keys(ids).sort(), 305 [activeID, engine.localID].sort(), 306 "Deleted client should be removed on next sync" 307 ); 308 } finally { 309 await cleanup(); 310 311 try { 312 server.deleteCollections("foo"); 313 } finally { 314 await promiseStopServer(server); 315 } 316 } 317 }); 318 319 add_task(async function test_sync() { 320 _("Ensure that Clients engine uploads a new client record once a week."); 321 322 let server = await serverForFoo(engine); 323 let user = server.user("foo"); 324 325 await SyncTestingInfrastructure(server); 326 await generateNewKeys(Service.collectionKeys); 327 328 function clientWBO() { 329 return user.collection("clients").wbo(engine.localID); 330 } 331 332 try { 333 _("First sync. Client record is uploaded."); 334 equal(clientWBO(), undefined); 335 equal(engine.lastRecordUpload, 0); 336 ok(engine.isFirstSync); 337 await syncClientsEngine(server); 338 ok(!!clientWBO().payload); 339 Assert.greater(engine.lastRecordUpload, 0); 340 ok(!engine.isFirstSync); 341 342 _( 343 "Let's time travel more than a week back, new record should've been uploaded." 344 ); 345 engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH; 346 let lastweek = engine.lastRecordUpload; 347 clientWBO().payload = undefined; 348 await syncClientsEngine(server); 349 ok(!!clientWBO().payload); 350 Assert.greater(engine.lastRecordUpload, lastweek); 351 ok(!engine.isFirstSync); 352 353 _("Remove client record."); 354 await engine.removeClientData(); 355 equal(clientWBO().payload, undefined); 356 357 _("Time travel one day back, no record uploaded."); 358 engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH; 359 let yesterday = engine.lastRecordUpload; 360 await syncClientsEngine(server); 361 equal(clientWBO().payload, undefined); 362 equal(engine.lastRecordUpload, yesterday); 363 ok(!engine.isFirstSync); 364 } finally { 365 await cleanup(); 366 await promiseStopServer(server); 367 } 368 }); 369 370 add_task(async function test_client_name_change() { 371 _("Ensure client name change incurs a client record update."); 372 373 let tracker = engine._tracker; 374 375 engine.localID; // Needed to increase the tracker changedIDs count. 376 let initialName = engine.localName; 377 378 tracker.start(); 379 _("initial name: " + initialName); 380 381 // Tracker already has data, so clear it. 382 await tracker.clearChangedIDs(); 383 384 let initialScore = tracker.score; 385 386 let changedIDs = await tracker.getChangedIDs(); 387 equal(Object.keys(changedIDs).length, 0); 388 389 Services.prefs.setStringPref( 390 "identity.fxaccounts.account.device.name", 391 "new name" 392 ); 393 await tracker.asyncObserver.promiseObserversComplete(); 394 395 _("new name: " + engine.localName); 396 notEqual(initialName, engine.localName); 397 changedIDs = await tracker.getChangedIDs(); 398 equal(Object.keys(changedIDs).length, 1); 399 ok(engine.localID in changedIDs); 400 Assert.greater(tracker.score, initialScore); 401 Assert.greaterOrEqual(tracker.score, SCORE_INCREMENT_XLARGE); 402 403 await tracker.stop(); 404 405 await cleanup(); 406 }); 407 408 add_task(async function test_fxa_device_id_change() { 409 _("Ensure an FxA device ID change incurs a client record update."); 410 411 let tracker = engine._tracker; 412 413 engine.localID; // Needed to increase the tracker changedIDs count. 414 415 tracker.start(); 416 417 // Tracker already has data, so clear it. 418 await tracker.clearChangedIDs(); 419 420 let initialScore = tracker.score; 421 422 let changedIDs = await tracker.getChangedIDs(); 423 equal(Object.keys(changedIDs).length, 0); 424 425 Services.obs.notifyObservers(null, "fxaccounts:new_device_id"); 426 await tracker.asyncObserver.promiseObserversComplete(); 427 428 changedIDs = await tracker.getChangedIDs(); 429 equal(Object.keys(changedIDs).length, 1); 430 ok(engine.localID in changedIDs); 431 Assert.greater(tracker.score, initialScore); 432 Assert.greaterOrEqual(tracker.score, SINGLE_USER_THRESHOLD); 433 434 await tracker.stop(); 435 436 await cleanup(); 437 }); 438 439 add_task(async function test_last_modified() { 440 _("Ensure that remote records have a sane serverLastModified attribute."); 441 442 let now = new_timestamp(); 443 let server = await serverForFoo(engine); 444 let user = server.user("foo"); 445 446 await SyncTestingInfrastructure(server); 447 await generateNewKeys(Service.collectionKeys); 448 449 let activeID = Utils.makeGUID(); 450 user.collection("clients").insertRecord( 451 { 452 id: activeID, 453 name: "Active client", 454 type: "desktop", 455 commands: [], 456 version: "48", 457 protocols: ["1.5"], 458 }, 459 now - 10 460 ); 461 462 try { 463 let collection = user.collection("clients"); 464 465 _("Sync to download the record"); 466 await syncClientsEngine(server); 467 468 equal( 469 engine._store._remoteClients[activeID].serverLastModified, 470 now - 10, 471 "last modified in the local record is correctly the server last-modified" 472 ); 473 474 _("Modify the record and re-upload it"); 475 // set a new name to make sure we really did upload. 476 engine._store._remoteClients[activeID].name = "New name"; 477 engine._modified.set(activeID, 0); 478 // The sync above also did a POST, so adjust our lastModified. 479 engine.lastModified = server.getCollection("foo", "clients").timestamp; 480 await engine._uploadOutgoing(); 481 482 _("Local record should have updated timestamp"); 483 Assert.greaterOrEqual( 484 engine._store._remoteClients[activeID].serverLastModified, 485 now 486 ); 487 488 _("Record on the server should have new name but not serverLastModified"); 489 let payload = collection.cleartext(activeID); 490 equal(payload.name, "New name"); 491 equal(payload.serverLastModified, undefined); 492 } finally { 493 await cleanup(); 494 server.deleteCollections("foo"); 495 await promiseStopServer(server); 496 } 497 }); 498 499 add_task(async function test_send_command() { 500 _("Verifies _sendCommandToClient puts commands in the outbound queue."); 501 502 let store = engine._store; 503 let tracker = engine._tracker; 504 let remoteId = Utils.makeGUID(); 505 let rec = new ClientsRec("clients", remoteId); 506 507 await store.create(rec); 508 await store.createRecord(remoteId, "clients"); 509 510 let action = "testCommand"; 511 let args = ["foo", "bar"]; 512 let extra = { flowID: "flowy" }; 513 514 await engine._sendCommandToClient(action, args, remoteId, extra); 515 516 let newRecord = store._remoteClients[remoteId]; 517 let clientCommands = (await engine._readCommands())[remoteId]; 518 notEqual(newRecord, undefined); 519 equal(clientCommands.length, 1); 520 521 let command = clientCommands[0]; 522 equal(command.command, action); 523 equal(command.args.length, 2); 524 deepEqual(command.args, args); 525 ok(command.flowID); 526 527 const changes = await tracker.getChangedIDs(); 528 notEqual(changes[remoteId], undefined); 529 530 await cleanup(); 531 }); 532 533 // The browser UI might call _addClientCommand indirectly without awaiting on the returned promise. 534 // We need to make sure this doesn't result on commands not being saved. 535 add_task(async function test_add_client_command_race() { 536 let promises = []; 537 for (let i = 0; i < 100; i++) { 538 promises.push( 539 engine._addClientCommand(`client-${i}`, { command: "cmd", args: [] }) 540 ); 541 } 542 await Promise.all(promises); 543 544 let localCommands = await engine._readCommands(); 545 for (let i = 0; i < 100; i++) { 546 equal(localCommands[`client-${i}`].length, 1); 547 } 548 }); 549 550 add_task(async function test_command_validation() { 551 _("Verifies that command validation works properly."); 552 553 let store = engine._store; 554 555 let testCommands = [ 556 ["resetAll", [], true], 557 ["resetAll", ["foo"], false], 558 ["resetEngine", ["tabs"], true], 559 ["resetEngine", [], false], 560 ["wipeEngine", ["tabs"], true], 561 ["wipeEngine", [], false], 562 ["logout", [], true], 563 ["logout", ["foo"], false], 564 ["__UNKNOWN__", [], false], 565 ]; 566 567 for (let [action, args, expectedResult] of testCommands) { 568 let remoteId = Utils.makeGUID(); 569 let rec = new ClientsRec("clients", remoteId); 570 571 await store.create(rec); 572 await store.createRecord(remoteId, "clients"); 573 574 await engine.sendCommand(action, args, remoteId); 575 576 let newRecord = store._remoteClients[remoteId]; 577 notEqual(newRecord, undefined); 578 579 let clientCommands = (await engine._readCommands())[remoteId]; 580 581 if (expectedResult) { 582 _("Ensuring command is sent: " + action); 583 equal(clientCommands.length, 1); 584 585 let command = clientCommands[0]; 586 equal(command.command, action); 587 deepEqual(command.args, args); 588 589 notEqual(engine._tracker, undefined); 590 const changes = await engine._tracker.getChangedIDs(); 591 notEqual(changes[remoteId], undefined); 592 } else { 593 _("Ensuring command is scrubbed: " + action); 594 equal(clientCommands, undefined); 595 596 if (store._tracker) { 597 equal(engine._tracker[remoteId], undefined); 598 } 599 } 600 } 601 await cleanup(); 602 }); 603 604 add_task(async function test_command_duplication() { 605 _("Ensures duplicate commands are detected and not added"); 606 607 let store = engine._store; 608 let remoteId = Utils.makeGUID(); 609 let rec = new ClientsRec("clients", remoteId); 610 await store.create(rec); 611 await store.createRecord(remoteId, "clients"); 612 613 let action = "resetAll"; 614 let args = []; 615 616 await engine.sendCommand(action, args, remoteId); 617 await engine.sendCommand(action, args, remoteId); 618 619 let clientCommands = (await engine._readCommands())[remoteId]; 620 equal(clientCommands.length, 1); 621 622 _("Check variant args length"); 623 await engine._saveCommands({}); 624 625 action = "resetEngine"; 626 await engine.sendCommand(action, [{ x: "foo" }], remoteId); 627 await engine.sendCommand(action, [{ x: "bar" }], remoteId); 628 629 _("Make sure we spot a real dupe argument."); 630 await engine.sendCommand(action, [{ x: "bar" }], remoteId); 631 632 clientCommands = (await engine._readCommands())[remoteId]; 633 equal(clientCommands.length, 2); 634 635 await cleanup(); 636 }); 637 638 add_task(async function test_command_invalid_client() { 639 _("Ensures invalid client IDs are caught"); 640 641 let id = Utils.makeGUID(); 642 let error; 643 644 try { 645 await engine.sendCommand("wipeEngine", ["tabs"], id); 646 } catch (ex) { 647 error = ex; 648 } 649 650 equal(error.message.indexOf("Unknown remote client ID: "), 0); 651 652 await cleanup(); 653 }); 654 655 add_task(async function test_process_incoming_commands() { 656 _("Ensures local commands are executed"); 657 658 engine.localCommands = [{ command: "logout", args: [] }]; 659 660 let ev = "weave:service:logout:finish"; 661 662 let logoutPromise = new Promise(resolve => { 663 var handler = function () { 664 Svc.Obs.remove(ev, handler); 665 666 resolve(); 667 }; 668 669 Svc.Obs.add(ev, handler); 670 }); 671 672 // logout command causes processIncomingCommands to return explicit false. 673 ok(!(await engine.processIncomingCommands())); 674 675 await logoutPromise; 676 677 await cleanup(); 678 }); 679 680 add_task(async function test_filter_duplicate_names() { 681 _( 682 "Ensure that we exclude clients with identical names that haven't synced in a week." 683 ); 684 685 let now = new_timestamp(); 686 let server = await serverForFoo(engine); 687 let user = server.user("foo"); 688 689 await SyncTestingInfrastructure(server); 690 await generateNewKeys(Service.collectionKeys); 691 692 // Synced recently. 693 let recentID = Utils.makeGUID(); 694 user.collection("clients").insertRecord( 695 { 696 id: recentID, 697 name: "My Phone", 698 type: "mobile", 699 commands: [], 700 version: "48", 701 protocols: ["1.5"], 702 }, 703 now - 10 704 ); 705 706 // synced recently, but not as recent as the phone. 707 let tabletID = Utils.makeGUID(); 708 user.collection("clients").insertRecord( 709 { 710 id: tabletID, 711 name: "My Tablet", 712 type: "tablet", 713 commands: [], 714 version: "48", 715 protocols: ["1.5"], 716 }, 717 now - 100 718 ); 719 720 // Dupe of our client, synced more than 1 week ago. 721 let dupeID = Utils.makeGUID(); 722 user.collection("clients").insertRecord( 723 { 724 id: dupeID, 725 name: engine.localName, 726 type: "desktop", 727 commands: [], 728 version: "48", 729 protocols: ["1.5"], 730 }, 731 now - 604820 732 ); 733 734 // Synced more than 1 week ago, but not a dupe. 735 let oldID = Utils.makeGUID(); 736 user.collection("clients").insertRecord( 737 { 738 id: oldID, 739 name: "My old desktop", 740 type: "desktop", 741 commands: [], 742 version: "48", 743 protocols: ["1.5"], 744 }, 745 now - 604820 746 ); 747 748 try { 749 let store = engine._store; 750 751 _("First sync"); 752 strictEqual(engine.lastRecordUpload, 0); 753 ok(engine.isFirstSync); 754 await syncClientsEngine(server); 755 Assert.greater(engine.lastRecordUpload, 0); 756 ok(!engine.isFirstSync); 757 deepEqual( 758 user.collection("clients").keys().sort(), 759 [recentID, tabletID, dupeID, oldID, engine.localID].sort(), 760 "Our record should be uploaded on first sync" 761 ); 762 763 let ids = await store.getAllIDs(); 764 deepEqual( 765 Object.keys(ids).sort(), 766 [recentID, tabletID, dupeID, oldID, engine.localID].sort(), 767 "Duplicate ID should remain in getAllIDs" 768 ); 769 ok( 770 await engine._store.itemExists(dupeID), 771 "Dupe ID should be considered as existing for Sync methods." 772 ); 773 ok( 774 !engine.remoteClientExists(dupeID), 775 "Dupe ID should not be considered as existing for external methods." 776 ); 777 778 // dupe desktop should not appear in .deviceTypes. 779 equal(engine.deviceTypes.get("desktop"), 2); 780 equal(engine.deviceTypes.get("mobile"), 2); 781 782 // dupe desktop should not appear in stats 783 deepEqual(engine.stats, { 784 hasMobile: 1, 785 names: [engine.localName, "My Phone", "My Tablet", "My old desktop"], 786 numClients: 4, 787 }); 788 789 ok(engine.remoteClientExists(oldID), "non-dupe ID should exist."); 790 ok(!engine.remoteClientExists(dupeID), "dupe ID should not exist"); 791 equal( 792 engine.remoteClients.length, 793 3, 794 "dupe should not be in remoteClients" 795 ); 796 797 // Check that a subsequent Sync doesn't report anything as being processed. 798 let counts; 799 Svc.Obs.add("weave:engine:sync:applied", function observe(subject) { 800 Svc.Obs.remove("weave:engine:sync:applied", observe); 801 counts = subject; 802 }); 803 804 await syncClientsEngine(server); 805 equal(counts.applied, 0); // We didn't report applying any records. 806 equal(counts.reconciled, 5); // We reported reconcilliation for all records 807 equal(counts.succeeded, 0); 808 equal(counts.failed, 0); 809 equal(counts.newFailed, 0); 810 811 _("Broadcast logout to all clients"); 812 await engine.sendCommand("logout", []); 813 await syncClientsEngine(server); 814 815 let collection = server.getCollection("foo", "clients"); 816 let recentPayload = collection.cleartext(recentID); 817 compareCommands( 818 recentPayload.commands, 819 [{ command: "logout", args: [] }], 820 "Should send commands to the recent client" 821 ); 822 823 let oldPayload = collection.cleartext(oldID); 824 compareCommands( 825 oldPayload.commands, 826 [{ command: "logout", args: [] }], 827 "Should send commands to the week-old client" 828 ); 829 830 let dupePayload = collection.cleartext(dupeID); 831 deepEqual( 832 dupePayload.commands, 833 [], 834 "Should not send commands to the dupe client" 835 ); 836 837 _("Update the dupe client's modified time"); 838 collection.insertRecord( 839 { 840 id: dupeID, 841 name: engine.localName, 842 type: "desktop", 843 commands: [], 844 version: "48", 845 protocols: ["1.5"], 846 }, 847 now - 10 848 ); 849 850 _("Second sync."); 851 await syncClientsEngine(server); 852 853 ids = await store.getAllIDs(); 854 deepEqual( 855 Object.keys(ids).sort(), 856 [recentID, tabletID, oldID, dupeID, engine.localID].sort(), 857 "Stale client synced, so it should no longer be marked as a dupe" 858 ); 859 860 ok( 861 engine.remoteClientExists(dupeID), 862 "Dupe ID should appear as it synced." 863 ); 864 865 // Recently synced dupe desktop should appear in .deviceTypes. 866 equal(engine.deviceTypes.get("desktop"), 3); 867 868 // Recently synced dupe desktop should now appear in stats 869 deepEqual(engine.stats, { 870 hasMobile: 1, 871 names: [ 872 engine.localName, 873 "My Phone", 874 "My Tablet", 875 engine.localName, 876 "My old desktop", 877 ], 878 numClients: 5, 879 }); 880 881 ok( 882 engine.remoteClientExists(dupeID), 883 "recently synced dupe ID should now exist" 884 ); 885 equal( 886 engine.remoteClients.length, 887 4, 888 "recently synced dupe should now be in remoteClients" 889 ); 890 } finally { 891 await cleanup(); 892 893 try { 894 server.deleteCollections("foo"); 895 } finally { 896 await promiseStopServer(server); 897 } 898 } 899 }); 900 901 add_task(async function test_command_sync() { 902 _("Ensure that commands are synced across clients."); 903 904 await engine._store.wipe(); 905 await generateNewKeys(Service.collectionKeys); 906 907 let server = await serverForFoo(engine); 908 await SyncTestingInfrastructure(server); 909 910 let user = server.user("foo"); 911 let remoteId = Utils.makeGUID(); 912 913 function clientWBO(id) { 914 return user.collection("clients").wbo(id); 915 } 916 917 _("Create remote client record"); 918 user.collection("clients").insertRecord({ 919 id: remoteId, 920 name: "Remote client", 921 type: "desktop", 922 commands: [], 923 version: "48", 924 protocols: ["1.5"], 925 }); 926 927 try { 928 _("Syncing."); 929 await syncClientsEngine(server); 930 931 _("Checking remote record was downloaded."); 932 let clientRecord = engine._store._remoteClients[remoteId]; 933 notEqual(clientRecord, undefined); 934 equal(clientRecord.commands.length, 0); 935 936 _("Send a command to the remote client."); 937 await engine.sendCommand("wipeEngine", ["tabs"]); 938 let clientCommands = (await engine._readCommands())[remoteId]; 939 equal(clientCommands.length, 1); 940 await syncClientsEngine(server); 941 942 _("Checking record was uploaded."); 943 notEqual(clientWBO(engine.localID).payload, undefined); 944 Assert.greater(engine.lastRecordUpload, 0); 945 ok(!engine.isFirstSync); 946 947 notEqual(clientWBO(remoteId).payload, undefined); 948 949 Svc.PrefBranch.setStringPref("client.GUID", remoteId); 950 await engine._resetClient(); 951 equal(engine.localID, remoteId); 952 _("Performing sync on resetted client."); 953 await syncClientsEngine(server); 954 notEqual(engine.localCommands, undefined); 955 equal(engine.localCommands.length, 1); 956 957 let command = engine.localCommands[0]; 958 equal(command.command, "wipeEngine"); 959 equal(command.args.length, 1); 960 equal(command.args[0], "tabs"); 961 } finally { 962 await cleanup(); 963 964 try { 965 let collection = server.getCollection("foo", "clients"); 966 collection.remove(remoteId); 967 } finally { 968 await promiseStopServer(server); 969 } 970 } 971 }); 972 973 add_task(async function test_clients_not_in_fxa_list() { 974 _("Ensure that clients not in the FxA devices list are marked as stale."); 975 976 await engine._store.wipe(); 977 await generateNewKeys(Service.collectionKeys); 978 979 let server = await serverForFoo(engine); 980 await SyncTestingInfrastructure(server); 981 982 let remoteId = Utils.makeGUID(); 983 let remoteId2 = Utils.makeGUID(); 984 let collection = server.getCollection("foo", "clients"); 985 986 _("Create remote client records"); 987 collection.insertRecord({ 988 id: remoteId, 989 name: "Remote client", 990 type: "desktop", 991 commands: [], 992 version: "48", 993 fxaDeviceId: remoteId, 994 protocols: ["1.5"], 995 }); 996 997 collection.insertRecord({ 998 id: remoteId2, 999 name: "Remote client 2", 1000 type: "desktop", 1001 commands: [], 1002 version: "48", 1003 fxaDeviceId: remoteId2, 1004 protocols: ["1.5"], 1005 }); 1006 1007 let fxAccounts = engine.fxAccounts; 1008 engine.fxAccounts = { 1009 notifyDevices() { 1010 return Promise.resolve(true); 1011 }, 1012 device: { 1013 getLocalId() { 1014 return fxAccounts.device.getLocalId(); 1015 }, 1016 getLocalName() { 1017 return fxAccounts.device.getLocalName(); 1018 }, 1019 getLocalType() { 1020 return fxAccounts.device.getLocalType(); 1021 }, 1022 recentDeviceList: [{ id: remoteId }], 1023 refreshDeviceList() { 1024 return Promise.resolve(true); 1025 }, 1026 }, 1027 _internal: { 1028 now() { 1029 return Date.now(); 1030 }, 1031 }, 1032 }; 1033 1034 try { 1035 _("Syncing."); 1036 await syncClientsEngine(server); 1037 1038 ok(!engine._store._remoteClients[remoteId].stale); 1039 ok(engine._store._remoteClients[remoteId2].stale); 1040 } finally { 1041 engine.fxAccounts = fxAccounts; 1042 await cleanup(); 1043 1044 try { 1045 collection.remove(remoteId); 1046 } finally { 1047 await promiseStopServer(server); 1048 } 1049 } 1050 }); 1051 1052 add_task(async function test_dupe_device_ids() { 1053 _( 1054 "Ensure that we mark devices with duplicate fxaDeviceIds but older lastModified as stale." 1055 ); 1056 1057 await engine._store.wipe(); 1058 await generateNewKeys(Service.collectionKeys); 1059 1060 let server = await serverForFoo(engine); 1061 await SyncTestingInfrastructure(server); 1062 1063 let remoteId = Utils.makeGUID(); 1064 let remoteId2 = Utils.makeGUID(); 1065 let remoteDeviceId = Utils.makeGUID(); 1066 1067 let collection = server.getCollection("foo", "clients"); 1068 1069 _("Create remote client records"); 1070 collection.insertRecord( 1071 { 1072 id: remoteId, 1073 name: "Remote client", 1074 type: "desktop", 1075 commands: [], 1076 version: "48", 1077 fxaDeviceId: remoteDeviceId, 1078 protocols: ["1.5"], 1079 }, 1080 new_timestamp() - 3 1081 ); 1082 collection.insertRecord({ 1083 id: remoteId2, 1084 name: "Remote client", 1085 type: "desktop", 1086 commands: [], 1087 version: "48", 1088 fxaDeviceId: remoteDeviceId, 1089 protocols: ["1.5"], 1090 }); 1091 1092 let fxAccounts = engine.fxAccounts; 1093 engine.fxAccounts = { 1094 notifyDevices() { 1095 return Promise.resolve(true); 1096 }, 1097 device: { 1098 getLocalId() { 1099 return fxAccounts.device.getLocalId(); 1100 }, 1101 getLocalName() { 1102 return fxAccounts.device.getLocalName(); 1103 }, 1104 getLocalType() { 1105 return fxAccounts.device.getLocalType(); 1106 }, 1107 recentDeviceList: [{ id: remoteDeviceId }], 1108 refreshDeviceList() { 1109 return Promise.resolve(true); 1110 }, 1111 }, 1112 _internal: { 1113 now() { 1114 return Date.now(); 1115 }, 1116 }, 1117 }; 1118 1119 try { 1120 _("Syncing."); 1121 await syncClientsEngine(server); 1122 1123 ok(engine._store._remoteClients[remoteId].stale); 1124 ok(!engine._store._remoteClients[remoteId2].stale); 1125 } finally { 1126 engine.fxAccounts = fxAccounts; 1127 await cleanup(); 1128 1129 try { 1130 collection.remove(remoteId); 1131 } finally { 1132 await promiseStopServer(server); 1133 } 1134 } 1135 }); 1136 1137 add_task(async function test_refresh_fxa_device_list() { 1138 _("Ensure we refresh the fxa device list when we expect to."); 1139 1140 await engine._store.wipe(); 1141 engine._lastFxaDeviceRefresh = 0; 1142 await generateNewKeys(Service.collectionKeys); 1143 1144 let server = await serverForFoo(engine); 1145 await SyncTestingInfrastructure(server); 1146 1147 let numRefreshes = 0; 1148 let now = Date.now(); 1149 let fxAccounts = engine.fxAccounts; 1150 engine.fxAccounts = { 1151 notifyDevices() { 1152 return Promise.resolve(true); 1153 }, 1154 device: { 1155 getLocalId() { 1156 return fxAccounts.device.getLocalId(); 1157 }, 1158 getLocalName() { 1159 return fxAccounts.device.getLocalName(); 1160 }, 1161 getLocalType() { 1162 return fxAccounts.device.getLocalType(); 1163 }, 1164 recentDeviceList: [], 1165 refreshDeviceList() { 1166 numRefreshes += 1; 1167 return Promise.resolve(true); 1168 }, 1169 }, 1170 _internal: { 1171 now() { 1172 return now; 1173 }, 1174 }, 1175 }; 1176 1177 try { 1178 _("Syncing."); 1179 await syncClientsEngine(server); 1180 Assert.equal(numRefreshes, 1, "first sync should refresh"); 1181 now += 1000; // a second later. 1182 await syncClientsEngine(server); 1183 Assert.equal(numRefreshes, 1, "next sync should not refresh"); 1184 now += 60 * 60 * 2 * 1000; // 2 hours later 1185 await syncClientsEngine(server); 1186 Assert.equal(numRefreshes, 2, "2 hours later should refresh"); 1187 now += 1000; // a second later. 1188 Assert.equal(numRefreshes, 2, "next sync should not refresh"); 1189 } finally { 1190 await cleanup(); 1191 await promiseStopServer(server); 1192 } 1193 }); 1194 1195 add_task(async function test_optional_client_fields() { 1196 _("Ensure that we produce records with the fields added in Bug 1097222."); 1197 1198 const SUPPORTED_PROTOCOL_VERSIONS = ["1.5"]; 1199 let local = await engine._store.createRecord(engine.localID, "clients"); 1200 equal(local.name, engine.localName); 1201 equal(local.type, engine.localType); 1202 equal(local.version, Services.appinfo.version); 1203 deepEqual(local.protocols, SUPPORTED_PROTOCOL_VERSIONS); 1204 1205 // Optional fields. 1206 // Make sure they're what they ought to be... 1207 equal(local.os, Services.appinfo.OS); 1208 equal(local.appPackage, Services.appinfo.ID); 1209 1210 // ... and also that they're non-empty. 1211 ok(!!local.os); 1212 ok(!!local.appPackage); 1213 ok(!!local.application); 1214 1215 // We don't currently populate device or formfactor. 1216 // See Bug 1100722, Bug 1100723. 1217 1218 await cleanup(); 1219 }); 1220 1221 add_task(async function test_merge_commands() { 1222 _("Verifies local commands for remote clients are merged with the server's"); 1223 1224 let now = new_timestamp(); 1225 let server = await serverForFoo(engine); 1226 await SyncTestingInfrastructure(server); 1227 await generateNewKeys(Service.collectionKeys); 1228 1229 let collection = server.getCollection("foo", "clients"); 1230 1231 let desktopID = Utils.makeGUID(); 1232 collection.insertRecord( 1233 { 1234 id: desktopID, 1235 name: "Desktop client", 1236 type: "desktop", 1237 commands: [ 1238 { 1239 command: "wipeEngine", 1240 args: ["history"], 1241 flowID: Utils.makeGUID(), 1242 }, 1243 ], 1244 version: "48", 1245 protocols: ["1.5"], 1246 }, 1247 now - 10 1248 ); 1249 1250 let mobileID = Utils.makeGUID(); 1251 collection.insertRecord( 1252 { 1253 id: mobileID, 1254 name: "Mobile client", 1255 type: "mobile", 1256 commands: [ 1257 { 1258 command: "logout", 1259 args: [], 1260 flowID: Utils.makeGUID(), 1261 }, 1262 ], 1263 version: "48", 1264 protocols: ["1.5"], 1265 }, 1266 now - 10 1267 ); 1268 1269 try { 1270 _("First sync. 2 records downloaded."); 1271 strictEqual(engine.lastRecordUpload, 0); 1272 ok(engine.isFirstSync); 1273 await syncClientsEngine(server); 1274 1275 _("Broadcast logout to all clients"); 1276 await engine.sendCommand("logout", []); 1277 await syncClientsEngine(server); 1278 1279 let desktopPayload = collection.cleartext(desktopID); 1280 compareCommands( 1281 desktopPayload.commands, 1282 [ 1283 { 1284 command: "wipeEngine", 1285 args: ["history"], 1286 }, 1287 { 1288 command: "logout", 1289 args: [], 1290 }, 1291 ], 1292 "Should send the logout command to the desktop client" 1293 ); 1294 1295 let mobilePayload = collection.cleartext(mobileID); 1296 compareCommands( 1297 mobilePayload.commands, 1298 [{ command: "logout", args: [] }], 1299 "Should not send a duplicate logout to the mobile client" 1300 ); 1301 } finally { 1302 await cleanup(); 1303 1304 try { 1305 server.deleteCollections("foo"); 1306 } finally { 1307 await promiseStopServer(server); 1308 } 1309 } 1310 }); 1311 1312 add_task(async function test_duplicate_remote_commands() { 1313 _( 1314 "Verifies local commands for remote clients are sent only once (bug 1289287)" 1315 ); 1316 1317 let now = new_timestamp(); 1318 let server = await serverForFoo(engine); 1319 1320 await SyncTestingInfrastructure(server); 1321 await generateNewKeys(Service.collectionKeys); 1322 1323 let collection = server.getCollection("foo", "clients"); 1324 1325 let desktopID = Utils.makeGUID(); 1326 collection.insertRecord( 1327 { 1328 id: desktopID, 1329 name: "Desktop client", 1330 type: "desktop", 1331 commands: [], 1332 version: "48", 1333 protocols: ["1.5"], 1334 }, 1335 now - 10 1336 ); 1337 1338 try { 1339 _("First sync. 1 record downloaded."); 1340 strictEqual(engine.lastRecordUpload, 0); 1341 ok(engine.isFirstSync); 1342 await syncClientsEngine(server); 1343 1344 _("Send command to client to wipe history engine"); 1345 await engine.sendCommand("wipeEngine", ["history"]); 1346 await syncClientsEngine(server); 1347 1348 _( 1349 "Simulate the desktop client consuming the command and syncing to the server" 1350 ); 1351 collection.insertRecord( 1352 { 1353 id: desktopID, 1354 name: "Desktop client", 1355 type: "desktop", 1356 commands: [], 1357 version: "48", 1358 protocols: ["1.5"], 1359 }, 1360 now - 10 1361 ); 1362 1363 _("Send another command to the desktop client to wipe tabs engine"); 1364 await engine.sendCommand("wipeEngine", ["tabs"], desktopID); 1365 await syncClientsEngine(server); 1366 1367 let desktopPayload = collection.cleartext(desktopID); 1368 compareCommands( 1369 desktopPayload.commands, 1370 [ 1371 { 1372 command: "wipeEngine", 1373 args: ["tabs"], 1374 }, 1375 ], 1376 "Should only send the second command to the desktop client" 1377 ); 1378 } finally { 1379 await cleanup(); 1380 1381 try { 1382 server.deleteCollections("foo"); 1383 } finally { 1384 await promiseStopServer(server); 1385 } 1386 } 1387 }); 1388 1389 add_task(async function test_upload_after_reboot() { 1390 _("Multiple downloads, reboot, then upload (bug 1289287)"); 1391 1392 let now = new_timestamp(); 1393 let server = await serverForFoo(engine); 1394 1395 await SyncTestingInfrastructure(server); 1396 await generateNewKeys(Service.collectionKeys); 1397 1398 let collection = server.getCollection("foo", "clients"); 1399 1400 let deviceBID = Utils.makeGUID(); 1401 let deviceCID = Utils.makeGUID(); 1402 collection.insertRecord( 1403 { 1404 id: deviceBID, 1405 name: "Device B", 1406 type: "desktop", 1407 commands: [ 1408 { 1409 command: "wipeEngine", 1410 args: ["history"], 1411 flowID: Utils.makeGUID(), 1412 }, 1413 ], 1414 version: "48", 1415 protocols: ["1.5"], 1416 }, 1417 now - 10 1418 ); 1419 collection.insertRecord( 1420 { 1421 id: deviceCID, 1422 name: "Device C", 1423 type: "desktop", 1424 commands: [], 1425 version: "48", 1426 protocols: ["1.5"], 1427 }, 1428 now - 10 1429 ); 1430 1431 try { 1432 _("First sync. 2 records downloaded."); 1433 strictEqual(engine.lastRecordUpload, 0); 1434 ok(engine.isFirstSync); 1435 await syncClientsEngine(server); 1436 1437 _("Send command to client to wipe tab engine"); 1438 await engine.sendCommand("wipeEngine", ["tabs"], deviceBID); 1439 1440 const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing; 1441 SyncEngine.prototype._uploadOutgoing = async () => 1442 engine._onRecordsWritten([], [deviceBID]); 1443 await syncClientsEngine(server); 1444 1445 let deviceBPayload = collection.cleartext(deviceBID); 1446 compareCommands( 1447 deviceBPayload.commands, 1448 [ 1449 { 1450 command: "wipeEngine", 1451 args: ["history"], 1452 }, 1453 ], 1454 "Should be the same because the upload failed" 1455 ); 1456 1457 _("Simulate the client B consuming the command and syncing to the server"); 1458 collection.insertRecord( 1459 { 1460 id: deviceBID, 1461 name: "Device B", 1462 type: "desktop", 1463 commands: [], 1464 version: "48", 1465 protocols: ["1.5"], 1466 }, 1467 now - 10 1468 ); 1469 1470 // Simulate reboot 1471 SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing; 1472 engine = Service.clientsEngine = new ClientEngine(Service); 1473 await engine.initialize(); 1474 1475 await syncClientsEngine(server); 1476 1477 deviceBPayload = collection.cleartext(deviceBID); 1478 compareCommands( 1479 deviceBPayload.commands, 1480 [ 1481 { 1482 command: "wipeEngine", 1483 args: ["tabs"], 1484 }, 1485 ], 1486 "Should only had written our outgoing command" 1487 ); 1488 } finally { 1489 await cleanup(); 1490 1491 try { 1492 server.deleteCollections("foo"); 1493 } finally { 1494 await promiseStopServer(server); 1495 } 1496 } 1497 }); 1498 1499 add_task(async function test_keep_cleared_commands_after_reboot() { 1500 _( 1501 "Download commands, fail upload, reboot, then apply new commands (bug 1289287)" 1502 ); 1503 1504 let now = new_timestamp(); 1505 let server = await serverForFoo(engine); 1506 1507 await SyncTestingInfrastructure(server); 1508 await generateNewKeys(Service.collectionKeys); 1509 1510 let collection = server.getCollection("foo", "clients"); 1511 1512 let deviceBID = Utils.makeGUID(); 1513 let deviceCID = Utils.makeGUID(); 1514 collection.insertRecord( 1515 { 1516 id: engine.localID, 1517 name: "Device A", 1518 type: "desktop", 1519 commands: [ 1520 { 1521 command: "wipeEngine", 1522 args: ["history"], 1523 flowID: Utils.makeGUID(), 1524 }, 1525 { 1526 command: "wipeEngine", 1527 args: ["tabs"], 1528 flowID: Utils.makeGUID(), 1529 }, 1530 ], 1531 version: "48", 1532 protocols: ["1.5"], 1533 }, 1534 now - 10 1535 ); 1536 collection.insertRecord( 1537 { 1538 id: deviceBID, 1539 name: "Device B", 1540 type: "desktop", 1541 commands: [], 1542 version: "48", 1543 protocols: ["1.5"], 1544 }, 1545 now - 10 1546 ); 1547 collection.insertRecord( 1548 { 1549 id: deviceCID, 1550 name: "Device C", 1551 type: "desktop", 1552 commands: [], 1553 version: "48", 1554 protocols: ["1.5"], 1555 }, 1556 now - 10 1557 ); 1558 1559 try { 1560 _("First sync. Download remote and our record."); 1561 strictEqual(engine.lastRecordUpload, 0); 1562 ok(engine.isFirstSync); 1563 1564 const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing; 1565 SyncEngine.prototype._uploadOutgoing = async () => 1566 engine._onRecordsWritten([], [deviceBID]); 1567 let commandsProcessed = 0; 1568 engine.service.wipeClient = _engine => { 1569 commandsProcessed++; 1570 }; 1571 1572 await syncClientsEngine(server); 1573 await engine.processIncomingCommands(); // Not called by the engine.sync(), gotta call it ourselves 1574 equal(commandsProcessed, 2, "We processed 2 commands"); 1575 1576 let localRemoteRecord = collection.cleartext(engine.localID); 1577 compareCommands( 1578 localRemoteRecord.commands, 1579 [ 1580 { 1581 command: "wipeEngine", 1582 args: ["history"], 1583 }, 1584 { 1585 command: "wipeEngine", 1586 args: ["tabs"], 1587 }, 1588 ], 1589 "Should be the same because the upload failed" 1590 ); 1591 1592 // Another client sends a wipe command 1593 collection.insertRecord( 1594 { 1595 id: engine.localID, 1596 name: "Device A", 1597 type: "desktop", 1598 commands: [ 1599 { 1600 command: "wipeEngine", 1601 args: ["history"], 1602 flowID: Utils.makeGUID(), 1603 }, 1604 { 1605 command: "wipeEngine", 1606 args: ["tabs"], 1607 flowID: Utils.makeGUID(), 1608 }, 1609 { 1610 command: "wipeEngine", 1611 args: ["bookmarks"], 1612 flowID: Utils.makeGUID(), 1613 }, 1614 ], 1615 version: "48", 1616 protocols: ["1.5"], 1617 }, 1618 now - 5 1619 ); 1620 1621 // Simulate reboot 1622 SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing; 1623 engine = Service.clientsEngine = new ClientEngine(Service); 1624 await engine.initialize(); 1625 1626 commandsProcessed = 0; 1627 engine.service.wipeClient = _engine => { 1628 commandsProcessed++; 1629 }; 1630 await syncClientsEngine(server); 1631 await engine.processIncomingCommands(); 1632 equal( 1633 commandsProcessed, 1634 1, 1635 "We processed one command (the other were cleared)" 1636 ); 1637 1638 localRemoteRecord = collection.cleartext(deviceBID); 1639 deepEqual(localRemoteRecord.commands, [], "Should be empty"); 1640 } finally { 1641 await cleanup(); 1642 1643 // Reset service (remove mocks) 1644 engine = Service.clientsEngine = new ClientEngine(Service); 1645 await engine.initialize(); 1646 await engine._resetClient(); 1647 1648 try { 1649 server.deleteCollections("foo"); 1650 } finally { 1651 await promiseStopServer(server); 1652 } 1653 } 1654 }); 1655 1656 add_task(async function test_deleted_commands() { 1657 _("Verifies commands for a deleted client are discarded"); 1658 1659 let now = new_timestamp(); 1660 let server = await serverForFoo(engine); 1661 1662 await SyncTestingInfrastructure(server); 1663 await generateNewKeys(Service.collectionKeys); 1664 1665 let collection = server.getCollection("foo", "clients"); 1666 1667 let activeID = Utils.makeGUID(); 1668 collection.insertRecord( 1669 { 1670 id: activeID, 1671 name: "Active client", 1672 type: "desktop", 1673 commands: [], 1674 version: "48", 1675 protocols: ["1.5"], 1676 }, 1677 now - 10 1678 ); 1679 1680 let deletedID = Utils.makeGUID(); 1681 collection.insertRecord( 1682 { 1683 id: deletedID, 1684 name: "Client to delete", 1685 type: "desktop", 1686 commands: [], 1687 version: "48", 1688 protocols: ["1.5"], 1689 }, 1690 now - 10 1691 ); 1692 1693 try { 1694 _("First sync. 2 records downloaded."); 1695 await syncClientsEngine(server); 1696 1697 _("Delete a record on the server."); 1698 collection.remove(deletedID); 1699 1700 _("Broadcast a command to all clients"); 1701 await engine.sendCommand("logout", []); 1702 await syncClientsEngine(server); 1703 1704 deepEqual( 1705 collection.keys().sort(), 1706 [activeID, engine.localID].sort(), 1707 "Should not reupload deleted clients" 1708 ); 1709 1710 let activePayload = collection.cleartext(activeID); 1711 compareCommands( 1712 activePayload.commands, 1713 [{ command: "logout", args: [] }], 1714 "Should send the command to the active client" 1715 ); 1716 } finally { 1717 await cleanup(); 1718 1719 try { 1720 server.deleteCollections("foo"); 1721 } finally { 1722 await promiseStopServer(server); 1723 } 1724 } 1725 }); 1726 1727 add_task(async function test_command_sync() { 1728 _("Notify other clients when writing their record."); 1729 1730 await engine._store.wipe(); 1731 await generateNewKeys(Service.collectionKeys); 1732 1733 let server = await serverForFoo(engine); 1734 await SyncTestingInfrastructure(server); 1735 1736 let collection = server.getCollection("foo", "clients"); 1737 let remoteId = Utils.makeGUID(); 1738 let remoteId2 = Utils.makeGUID(); 1739 1740 _("Create remote client record 1"); 1741 collection.insertRecord({ 1742 id: remoteId, 1743 name: "Remote client", 1744 type: "desktop", 1745 commands: [], 1746 version: "48", 1747 protocols: ["1.5"], 1748 }); 1749 1750 _("Create remote client record 2"); 1751 collection.insertRecord({ 1752 id: remoteId2, 1753 name: "Remote client 2", 1754 type: "mobile", 1755 commands: [], 1756 version: "48", 1757 protocols: ["1.5"], 1758 }); 1759 1760 try { 1761 equal(collection.count(), 2, "2 remote records written"); 1762 await syncClientsEngine(server); 1763 equal( 1764 collection.count(), 1765 3, 1766 "3 remote records written (+1 for the synced local record)" 1767 ); 1768 1769 await engine.sendCommand("wipeEngine", ["tabs"]); 1770 await engine._tracker.addChangedID(engine.localID); 1771 const getClientFxaDeviceId = sinon 1772 .stub(engine, "getClientFxaDeviceId") 1773 .callsFake(id => "fxa-" + id); 1774 const engineMock = sinon.mock(engine); 1775 let _notifyCollectionChanged = engineMock 1776 .expects("_notifyCollectionChanged") 1777 .withArgs(["fxa-" + remoteId, "fxa-" + remoteId2]); 1778 _("Syncing."); 1779 await syncClientsEngine(server); 1780 _notifyCollectionChanged.verify(); 1781 1782 engineMock.restore(); 1783 getClientFxaDeviceId.restore(); 1784 } finally { 1785 await cleanup(); 1786 await engine._tracker.clearChangedIDs(); 1787 1788 try { 1789 server.deleteCollections("foo"); 1790 } finally { 1791 await promiseStopServer(server); 1792 } 1793 } 1794 }); 1795 1796 add_task(async function ensureSameFlowIDs() { 1797 let events = []; 1798 let origRecordTelemetryEvent = Service.recordTelemetryEvent; 1799 Service.recordTelemetryEvent = (object, method, value, extra) => { 1800 events.push({ object, method, value, extra }); 1801 }; 1802 // Clear events from other test cases. 1803 Services.fog.testResetFOG(); 1804 1805 let server = await serverForFoo(engine); 1806 try { 1807 // Setup 2 clients, send them a command, and ensure we get to events 1808 // written, both with the same flowID. 1809 await SyncTestingInfrastructure(server); 1810 let collection = server.getCollection("foo", "clients"); 1811 1812 let remoteId = Utils.makeGUID(); 1813 let remoteId2 = Utils.makeGUID(); 1814 1815 _("Create remote client record 1"); 1816 collection.insertRecord({ 1817 id: remoteId, 1818 name: "Remote client", 1819 type: "desktop", 1820 commands: [], 1821 version: "48", 1822 protocols: ["1.5"], 1823 }); 1824 1825 _("Create remote client record 2"); 1826 collection.insertRecord({ 1827 id: remoteId2, 1828 name: "Remote client 2", 1829 type: "mobile", 1830 commands: [], 1831 version: "48", 1832 protocols: ["1.5"], 1833 }); 1834 1835 await syncClientsEngine(server); 1836 await engine.sendCommand("wipeEngine", ["tabs"]); 1837 await syncClientsEngine(server); 1838 equal(events.length, 2); 1839 // we don't know what the flowID is, but do know it should be the same. 1840 equal(events[0].extra.flowID, events[1].extra.flowID); 1841 let wipeEvents = Glean.syncClient.sendcommand.testGetValue(); 1842 equal(wipeEvents.length, 2); 1843 equal(wipeEvents[0].extra.flow_id, wipeEvents[1].extra.flow_id); 1844 Services.fog.testResetFOG(); 1845 // Wipe remote clients to ensure deduping doesn't prevent us from adding the command. 1846 for (let client of Object.values(engine._store._remoteClients)) { 1847 client.commands = []; 1848 } 1849 // check it's correctly used when we specify a flow ID 1850 events.length = 0; 1851 let flowID = Utils.makeGUID(); 1852 await engine.sendCommand("wipeEngine", ["tabs"], null, { flowID }); 1853 await syncClientsEngine(server); 1854 equal(events.length, 2); 1855 equal(events[0].extra.flowID, flowID); 1856 equal(events[1].extra.flowID, flowID); 1857 wipeEvents = Glean.syncClient.sendcommand.testGetValue(); 1858 equal(wipeEvents.length, 2); 1859 equal(wipeEvents[0].extra.flow_id, flowID); 1860 equal(wipeEvents[1].extra.flow_id, flowID); 1861 Services.fog.testResetFOG(); 1862 1863 // Wipe remote clients to ensure deduping doesn't prevent us from adding the command. 1864 for (let client of Object.values(engine._store._remoteClients)) { 1865 client.commands = []; 1866 } 1867 1868 // and that it works when something else is in "extra" 1869 events.length = 0; 1870 await engine.sendCommand("wipeEngine", ["tabs"], null, { 1871 reason: "testing", 1872 }); 1873 await syncClientsEngine(server); 1874 equal(events.length, 2); 1875 equal(events[0].extra.flowID, events[1].extra.flowID); 1876 equal(events[0].extra.reason, "testing"); 1877 equal(events[1].extra.reason, "testing"); 1878 wipeEvents = Glean.syncClient.sendcommand.testGetValue(); 1879 equal(wipeEvents.length, 2); 1880 equal(wipeEvents[0].extra.reason, "testing"); 1881 equal(wipeEvents[1].extra.reason, "testing"); 1882 Services.fog.testResetFOG(); 1883 // Wipe remote clients to ensure deduping doesn't prevent us from adding the command. 1884 for (let client of Object.values(engine._store._remoteClients)) { 1885 client.commands = []; 1886 } 1887 1888 // and when both are specified. 1889 events.length = 0; 1890 await engine.sendCommand("wipeEngine", ["tabs"], null, { 1891 reason: "testing", 1892 flowID, 1893 }); 1894 await syncClientsEngine(server); 1895 equal(events.length, 2); 1896 equal(events[0].extra.flowID, flowID); 1897 equal(events[1].extra.flowID, flowID); 1898 equal(events[0].extra.reason, "testing"); 1899 equal(events[1].extra.reason, "testing"); 1900 wipeEvents = Glean.syncClient.sendcommand.testGetValue(); 1901 equal(wipeEvents.length, 2); 1902 equal(wipeEvents[0].extra.flow_id, flowID); 1903 equal(wipeEvents[1].extra.flow_id, flowID); 1904 equal(wipeEvents[0].extra.reason, "testing"); 1905 equal(wipeEvents[1].extra.reason, "testing"); 1906 // Wipe remote clients to ensure deduping doesn't prevent us from adding the command. 1907 for (let client of Object.values(engine._store._remoteClients)) { 1908 client.commands = []; 1909 } 1910 } finally { 1911 Service.recordTelemetryEvent = origRecordTelemetryEvent; 1912 cleanup(); 1913 await promiseStopServer(server); 1914 } 1915 }); 1916 1917 add_task(async function test_duplicate_commands_telemetry() { 1918 let events = []; 1919 let origRecordTelemetryEvent = Service.recordTelemetryEvent; 1920 Service.recordTelemetryEvent = (object, method, value, extra) => { 1921 events.push({ object, method, value, extra }); 1922 }; 1923 // Clear events from other test cases. 1924 Services.fog.testResetFOG(); 1925 1926 let server = await serverForFoo(engine); 1927 try { 1928 await SyncTestingInfrastructure(server); 1929 let collection = server.getCollection("foo", "clients"); 1930 1931 let remoteId = Utils.makeGUID(); 1932 let remoteId2 = Utils.makeGUID(); 1933 1934 _("Create remote client record 1"); 1935 collection.insertRecord({ 1936 id: remoteId, 1937 name: "Remote client", 1938 type: "desktop", 1939 commands: [], 1940 version: "48", 1941 protocols: ["1.5"], 1942 }); 1943 1944 _("Create remote client record 2"); 1945 collection.insertRecord({ 1946 id: remoteId2, 1947 name: "Remote client 2", 1948 type: "mobile", 1949 commands: [], 1950 version: "48", 1951 protocols: ["1.5"], 1952 }); 1953 1954 await syncClientsEngine(server); 1955 // Make sure deduping works before syncing 1956 await engine.sendCommand("wipeEngine", ["history"], remoteId); 1957 await engine.sendCommand("wipeEngine", ["history"], remoteId); 1958 equal(events.length, 1); 1959 equal(Glean.syncClient.sendcommand.testGetValue().length, 1); 1960 await syncClientsEngine(server); 1961 // And after syncing. 1962 await engine.sendCommand("wipeEngine", ["history"], remoteId); 1963 equal(events.length, 1); 1964 equal(Glean.syncClient.sendcommand.testGetValue().length, 1); 1965 // Ensure we aren't deduping commands to different clients 1966 await engine.sendCommand("wipeEngine", ["history"], remoteId2); 1967 equal(events.length, 2); 1968 equal(Glean.syncClient.sendcommand.testGetValue().length, 2); 1969 } finally { 1970 Service.recordTelemetryEvent = origRecordTelemetryEvent; 1971 cleanup(); 1972 await promiseStopServer(server); 1973 } 1974 }); 1975 1976 add_task(async function test_other_clients_notified_on_first_sync() { 1977 _( 1978 "Ensure that other clients are notified when we upload our client record for the first time." 1979 ); 1980 1981 await engine.resetLastSync(); 1982 await engine._store.wipe(); 1983 await generateNewKeys(Service.collectionKeys); 1984 1985 let server = await serverForFoo(engine); 1986 await SyncTestingInfrastructure(server); 1987 1988 const fxAccounts = engine.fxAccounts; 1989 let calls = 0; 1990 engine.fxAccounts = { 1991 device: { 1992 getLocalId() { 1993 return fxAccounts.device.getLocalId(); 1994 }, 1995 getLocalName() { 1996 return fxAccounts.device.getLocalName(); 1997 }, 1998 getLocalType() { 1999 return fxAccounts.device.getLocalType(); 2000 }, 2001 }, 2002 notifyDevices() { 2003 calls++; 2004 return Promise.resolve(true); 2005 }, 2006 _internal: { 2007 now() { 2008 return Date.now(); 2009 }, 2010 }, 2011 }; 2012 2013 try { 2014 engine.lastRecordUpload = 0; 2015 _("First sync, should notify other clients"); 2016 await syncClientsEngine(server); 2017 equal(calls, 1); 2018 2019 _("Second sync, should not notify other clients"); 2020 await syncClientsEngine(server); 2021 equal(calls, 1); 2022 } finally { 2023 engine.fxAccounts = fxAccounts; 2024 cleanup(); 2025 await promiseStopServer(server); 2026 } 2027 }); 2028 2029 add_task( 2030 async function device_disconnected_notification_updates_known_stale_clients() { 2031 const spyUpdate = sinon.spy(engine, "updateKnownStaleClients"); 2032 2033 Services.obs.notifyObservers( 2034 null, 2035 "fxaccounts:device_disconnected", 2036 JSON.stringify({ isLocalDevice: false }) 2037 ); 2038 ok(spyUpdate.calledOnce, "updateKnownStaleClients should be called"); 2039 spyUpdate.resetHistory(); 2040 2041 Services.obs.notifyObservers( 2042 null, 2043 "fxaccounts:device_disconnected", 2044 JSON.stringify({ isLocalDevice: true }) 2045 ); 2046 ok(spyUpdate.notCalled, "updateKnownStaleClients should not be called"); 2047 2048 spyUpdate.restore(); 2049 } 2050 ); 2051 2052 add_task(async function update_known_stale_clients() { 2053 const makeFakeClient = id => ({ id, fxaDeviceId: `fxa-${id}` }); 2054 const clients = [ 2055 makeFakeClient("one"), 2056 makeFakeClient("two"), 2057 makeFakeClient("three"), 2058 ]; 2059 const stubRemoteClients = sinon 2060 .stub(engine._store, "_remoteClients") 2061 .get(() => { 2062 return clients; 2063 }); 2064 const stubFetchFxADevices = sinon 2065 .stub(engine, "_fetchFxADevices") 2066 .callsFake(() => { 2067 engine._knownStaleFxADeviceIds = ["fxa-one", "fxa-two"]; 2068 }); 2069 2070 engine._knownStaleFxADeviceIds = null; 2071 await engine.updateKnownStaleClients(); 2072 ok(clients[0].stale); 2073 ok(clients[1].stale); 2074 ok(!clients[2].stale); 2075 2076 stubRemoteClients.restore(); 2077 stubFetchFxADevices.restore(); 2078 }); 2079 2080 add_task(async function test_create_record_command_limit() { 2081 await engine._store.wipe(); 2082 await generateNewKeys(Service.collectionKeys); 2083 2084 let server = await serverForFoo(engine); 2085 await SyncTestingInfrastructure(server); 2086 2087 const fakeLimit = 4 * 1024; 2088 2089 let maxSizeStub = sinon 2090 .stub(Service, "getMemcacheMaxRecordPayloadSize") 2091 .callsFake(() => fakeLimit); 2092 2093 let user = server.user("foo"); 2094 let remoteId = Utils.makeGUID(); 2095 2096 _("Create remote client record"); 2097 user.collection("clients").insertRecord({ 2098 id: remoteId, 2099 name: "Remote client", 2100 type: "desktop", 2101 commands: [], 2102 version: "57", 2103 protocols: ["1.5"], 2104 }); 2105 2106 try { 2107 _("Initial sync."); 2108 await syncClientsEngine(server); 2109 2110 _("Send a fairly sane number of commands."); 2111 2112 for (let i = 0; i < 5; ++i) { 2113 await engine.sendCommand("wipeEngine", [`history: ${i}`], remoteId); 2114 } 2115 2116 await syncClientsEngine(server); 2117 2118 _("Make sure they all fit and weren't dropped."); 2119 let parsedServerRecord = user.collection("clients").cleartext(remoteId); 2120 2121 equal(parsedServerRecord.commands.length, 5); 2122 2123 await engine.sendCommand("wipeEngine", ["history"], remoteId); 2124 2125 _("Send a not-sane number of commands."); 2126 // Much higher than the maximum number of commands we could actually fit. 2127 for (let i = 0; i < 500; ++i) { 2128 await engine.sendCommand("wipeEngine", [`tabs: ${i}`], remoteId); 2129 } 2130 2131 await syncClientsEngine(server); 2132 2133 _("Ensure we didn't overflow the server limit."); 2134 let wbo = user.collection("clients").wbo(remoteId); 2135 less(wbo.payload.length, fakeLimit); 2136 2137 _( 2138 "And that the data we uploaded is both sane json and containing some commands." 2139 ); 2140 let remoteCommands = wbo.getCleartext().commands; 2141 greater(remoteCommands.length, 2); 2142 let firstCommand = remoteCommands[0]; 2143 _( 2144 "The first command should still be present, since it had a high priority" 2145 ); 2146 equal(firstCommand.command, "wipeEngine"); 2147 _("And the last command in the list should be the last command we sent."); 2148 let lastCommand = remoteCommands[remoteCommands.length - 1]; 2149 equal(lastCommand.command, "wipeEngine"); 2150 deepEqual(lastCommand.args, ["tabs: 499"]); 2151 } finally { 2152 maxSizeStub.restore(); 2153 await cleanup(); 2154 try { 2155 let collection = server.getCollection("foo", "clients"); 2156 collection.remove(remoteId); 2157 } finally { 2158 await promiseStopServer(server); 2159 } 2160 } 2161 });