test_glean.js (40452B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { Service } = ChromeUtils.importESModule( 5 "resource://services-sync/service.sys.mjs" 6 ); 7 const { WBORecord } = ChromeUtils.importESModule( 8 "resource://services-sync/record.sys.mjs" 9 ); 10 const { RotaryEngine } = ChromeUtils.importESModule( 11 "resource://testing-common/services/sync/rotaryengine.sys.mjs" 12 ); 13 const { getFxAccountsSingleton } = ChromeUtils.importESModule( 14 "resource://gre/modules/FxAccounts.sys.mjs" 15 ); 16 const fxAccounts = getFxAccountsSingleton(); 17 18 function SteamStore(engine) { 19 Store.call(this, "Steam", engine); 20 } 21 Object.setPrototypeOf(SteamStore.prototype, Store.prototype); 22 23 function SteamTracker(name, engine) { 24 LegacyTracker.call(this, name || "Steam", engine); 25 } 26 Object.setPrototypeOf(SteamTracker.prototype, LegacyTracker.prototype); 27 28 function SteamEngine(service) { 29 SyncEngine.call(this, "steam", service); 30 } 31 32 SteamEngine.prototype = { 33 _storeObj: SteamStore, 34 _trackerObj: SteamTracker, 35 _errToThrow: null, 36 problemsToReport: null, 37 async _sync() { 38 if (this._errToThrow) { 39 throw this._errToThrow; 40 } 41 }, 42 getValidator() { 43 return new SteamValidator(); 44 }, 45 }; 46 Object.setPrototypeOf(SteamEngine.prototype, SyncEngine.prototype); 47 48 function BogusEngine(service) { 49 SyncEngine.call(this, "bogus", service); 50 } 51 52 BogusEngine.prototype = Object.create(SteamEngine.prototype); 53 54 class SteamValidator { 55 async canValidate() { 56 return true; 57 } 58 59 async validate(engine) { 60 return { 61 problems: new SteamValidationProblemData(engine.problemsToReport), 62 version: 1, 63 duration: 0, 64 recordCount: 0, 65 }; 66 } 67 } 68 69 class SteamValidationProblemData { 70 constructor(problemsToReport = []) { 71 this.problemsToReport = problemsToReport; 72 } 73 74 getSummary() { 75 return this.problemsToReport; 76 } 77 } 78 79 async function cleanAndGo(engine, server) { 80 await engine._tracker.clearChangedIDs(); 81 for (const pref of Svc.PrefBranch.getChildList("")) { 82 Svc.PrefBranch.clearUserPref(pref); 83 } 84 syncTestLogging(); 85 Service.recordManager.clearCache(); 86 await promiseStopServer(server); 87 } 88 89 async function sync_engine(engine, errorRegex = undefined) { 90 // Clear out status so failures from previous syncs won't show in the record. 91 let { Status } = ChromeUtils.importESModule( 92 "resource://services-sync/status.sys.mjs" 93 ); 94 Status._engines = {}; 95 Status.partial = false; 96 // Neuter the scheduler as it interacts badly with some of the tests - the 97 // engine being synced usually isn't the registered engine, so we see 98 // scored incremented and not removed, which schedules unexpected syncs. 99 let oldObserve = Service.scheduler.observe; 100 Service.scheduler.observe = () => {}; 101 try { 102 Svc.Obs.notify("weave:service:sync:start"); 103 let caughtError; 104 try { 105 await engine.sync(); 106 } catch (e) { 107 caughtError = e; 108 } 109 if (caughtError) { 110 ok( 111 errorRegex.test(JSON.stringify(caughtError)), 112 "Engine sync error expected." 113 ); 114 Svc.Obs.notify("weave:service:sync:error", caughtError); 115 } else { 116 ok(!errorRegex, "No engine sync error expected."); 117 Svc.Obs.notify("weave:service:sync:finish"); 118 } 119 } finally { 120 Service.scheduler.observe = oldObserve; 121 } 122 } 123 124 add_setup(async function () { 125 // Avoid addon manager complaining about not being initialized 126 await Service.engineManager.unregister("addons"); 127 await Service.engineManager.unregister("extension-storage"); 128 129 do_get_profile(); // FOG requires a profile dir. 130 Services.fog.initializeFOG(); 131 132 // We're not using `wait_for_ping` which means we aren't implicitly calling 133 // get_sync_test_telemetry() so often. So call it here for its side effects. 134 let telem = get_sync_test_telemetry(); 135 // Unlike the telemetry tests, we actually want the Glean APIs to be called. 136 // So pretend we're in production. 137 telem.isProductionSyncUser = () => true; 138 }); 139 140 add_task(async function test_basic() { 141 enableValidationPrefs(); 142 143 let helper = track_collections_helper(); 144 let upd = helper.with_updated_collection; 145 146 let handlers = { 147 "/1.1/johndoe/info/collections": helper.handler, 148 "/1.1/johndoe/storage/crypto/keys": upd( 149 "crypto", 150 new ServerWBO("keys").handler() 151 ), 152 "/1.1/johndoe/storage/meta/global": upd( 153 "meta", 154 new ServerWBO("global").handler() 155 ), 156 }; 157 158 let collections = [ 159 "clients", 160 "bookmarks", 161 "forms", 162 "history", 163 "passwords", 164 "prefs", 165 "tabs", 166 ]; 167 168 for (let coll of collections) { 169 handlers["/1.1/johndoe/storage/" + coll] = upd( 170 coll, 171 new ServerCollection({}, true).handler() 172 ); 173 } 174 175 let server = httpd_setup(handlers); 176 await configureIdentity({ username: "johndoe" }, server); 177 178 // Test that a basic batch can make it. 179 await GleanPings.sync.testSubmission( 180 reason => { 181 equal(reason, "schedule"); 182 Glean.syncs.syncs.testGetValue().forEach(assert_success_sync); 183 }, 184 async () => { 185 await Service.sync(); 186 } 187 ); 188 189 for (const pref of Svc.PrefBranch.getChildList("")) { 190 Svc.PrefBranch.clearUserPref(pref); 191 } 192 await promiseStopServer(server); 193 }); 194 195 add_task(async function test_processIncoming_error() { 196 let engine = Service.engineManager.get("bookmarks"); 197 await engine.initialize(); 198 let store = engine._store; 199 let server = await serverForFoo(engine); 200 await SyncTestingInfrastructure(server); 201 let collection = server.user("foo").collection("bookmarks"); 202 try { 203 // Create a bogus record that when synced down will provoke a 204 // network error which in turn provokes an exception in _processIncoming. 205 const BOGUS_GUID = "zzzzzzzzzzzz"; 206 let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!"); 207 bogus_record.get = function get() { 208 throw new Error("Sync this!"); 209 }; 210 // Make it 10 minutes old so it will only be synced in the toFetch phase. 211 bogus_record.modified = Date.now() / 1000 - 60 * 10; 212 await engine.setLastSync(Date.now() / 1000 - 60); 213 engine.toFetch = new SerializableSet([BOGUS_GUID]); 214 215 await GleanPings.sync.testSubmission( 216 reason => { 217 equal(reason, "schedule"); 218 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 219 220 const syncs = Glean.syncs.syncs.testGetValue(); 221 equal(syncs.length, 1); 222 223 const sync = syncs[0]; 224 deepEqual(sync.failureReason, { name: "httperror", code: 500 }); 225 equal(sync.engines.length, 1); 226 227 const e = sync.engines[0]; 228 equal(e.name, "bookmarks-buffered"); 229 deepEqual(e.failureReason, { name: "httperror", code: 500 }); 230 }, 231 async () => { 232 await sync_engine(engine, /500 Internal Server Error/); 233 } 234 ); 235 } finally { 236 await store.wipe(); 237 await cleanAndGo(engine, server); 238 } 239 }); 240 241 add_task(async function test_uploading() { 242 // Clear out status, so failures from previous syncs won't show up in the 243 // telemetry ping. 244 let { Status } = ChromeUtils.importESModule( 245 "resource://services-sync/status.sys.mjs" 246 ); 247 Status._engines = {}; 248 Status.partial = false; 249 250 let engine = Service.engineManager.get("bookmarks"); 251 await engine.initialize(); 252 let store = engine._store; 253 let server = await serverForFoo(engine); 254 await SyncTestingInfrastructure(server); 255 256 let bmk = await PlacesUtils.bookmarks.insert({ 257 parentGuid: PlacesUtils.bookmarks.toolbarGuid, 258 url: "http://getfirefox.com/", 259 title: "Get Firefox!", 260 }); 261 262 try { 263 await GleanPings.sync.testSubmission( 264 reason => { 265 equal(reason, "schedule"); 266 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 267 268 const syncs = Glean.syncs.syncs.testGetValue(); 269 equal(syncs.length, 1); 270 271 const sync = syncs[0]; 272 equal(sync.engines.length, 1); 273 274 const e = sync.engines[0]; 275 equal(e.name, "bookmarks-buffered"); 276 greater(e.outgoing[0].sent, 0); 277 ok(!e.incoming); 278 }, 279 async () => { 280 await sync_engine(engine); 281 } 282 ); 283 284 await PlacesUtils.bookmarks.update({ 285 guid: bmk.guid, 286 title: "New Title", 287 }); 288 289 await store.wipe(); 290 await engine.resetClient(); 291 // We don't sync via the service, so don't re-hit info/collections, so 292 // lastModified remaning at zero breaks things subtly... 293 engine.lastModified = null; 294 295 await GleanPings.sync.testSubmission( 296 reason => { 297 equal(reason, "schedule"); 298 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 299 300 const syncs = Glean.syncs.syncs.testGetValue(); 301 equal(syncs.length, 1); 302 303 const sync = syncs[0]; 304 equal(sync.engines.length, 1); 305 306 const e = sync.engines[0]; 307 equal(e.name, "bookmarks-buffered"); 308 equal(e.outgoing.length, 1); 309 ok(!!e.incoming); 310 }, 311 async () => { 312 await sync_engine(engine); 313 } 314 ); 315 } finally { 316 // Clean up. 317 await store.wipe(); 318 await cleanAndGo(engine, server); 319 } 320 }); 321 322 add_task(async function test_upload_failed() { 323 let collection = new ServerCollection(); 324 collection._wbos.flying = new ServerWBO("flying"); 325 326 let server = sync_httpd_setup({ 327 "/1.1/foo/storage/rotary": collection.handler(), 328 }); 329 330 await SyncTestingInfrastructure(server); 331 await configureIdentity({ username: "foo" }, server); 332 333 let engine = new RotaryEngine(Service); 334 engine._store.items = { 335 flying: "LNER Class A3 4472", 336 scotsman: "Flying Scotsman", 337 peppercorn: "Peppercorn Class", 338 }; 339 const FLYING_CHANGED = 12345; 340 const SCOTSMAN_CHANGED = 23456; 341 const PEPPERCORN_CHANGED = 34567; 342 await engine._tracker.addChangedID("flying", FLYING_CHANGED); 343 await engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED); 344 await engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED); 345 346 let syncID = await engine.resetLocalSyncID(); 347 let meta_global = Service.recordManager.set( 348 engine.metaURL, 349 new WBORecord(engine.metaURL) 350 ); 351 meta_global.payload.engines = { rotary: { version: engine.version, syncID } }; 352 353 try { 354 await engine.setLastSync(123); // needs to be non-zero so that tracker is queried 355 let changes = await engine._tracker.getChangedIDs(); 356 _( 357 `test_upload_failed: Rotary tracker contents at first sync: ${JSON.stringify( 358 changes 359 )}` 360 ); 361 engine.enabled = true; 362 await GleanPings.sync.testSubmission( 363 reason => { 364 equal(reason, "schedule"); 365 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 366 367 const syncs = Glean.syncs.syncs.testGetValue(); 368 equal(syncs.length, 1); 369 370 const sync = syncs[0]; 371 equal(sync.engines.length, 1); 372 373 const e = sync.engines[0]; 374 equal(e.incoming, null); 375 deepEqual(e.outgoing, [ 376 { 377 sent: 3, 378 failed: 2, 379 failedReasons: [ 380 { name: "scotsman", count: 1 }, 381 { name: "peppercorn", count: 1 }, 382 ], 383 }, 384 ]); 385 }, 386 async () => { 387 await sync_engine(engine); 388 } 389 ); 390 391 await engine.setLastSync(123); 392 393 changes = await engine._tracker.getChangedIDs(); 394 _( 395 `test_upload_failed: Rotary tracker contents at second sync: ${JSON.stringify( 396 changes 397 )}` 398 ); 399 await GleanPings.sync.testSubmission( 400 reason => { 401 equal(reason, "schedule"); 402 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 403 404 const syncs = Glean.syncs.syncs.testGetValue(); 405 equal(syncs.length, 1); 406 407 const sync = syncs[0]; 408 equal(sync.engines.length, 1); 409 410 const e = sync.engines[0]; 411 deepEqual(e.outgoing, [ 412 { 413 sent: 2, 414 failed: 2, 415 failedReasons: [ 416 { name: "scotsman", count: 1 }, 417 { name: "peppercorn", count: 1 }, 418 ], 419 }, 420 ]); 421 }, 422 async () => { 423 await sync_engine(engine); 424 } 425 ); 426 } finally { 427 await cleanAndGo(engine, server); 428 await engine.finalize(); 429 } 430 }); 431 432 add_task(async function test_sync_partialUpload() { 433 let collection = new ServerCollection(); 434 let server = sync_httpd_setup({ 435 "/1.1/foo/storage/rotary": collection.handler(), 436 }); 437 await SyncTestingInfrastructure(server); 438 await generateNewKeys(Service.collectionKeys); 439 440 let engine = new RotaryEngine(Service); 441 await engine.setLastSync(123); 442 443 // Create a bunch of records (and server side handlers) 444 for (let i = 0; i < 234; i++) { 445 let id = "record-no-" + i; 446 engine._store.items[id] = "Record No. " + i; 447 await engine._tracker.addChangedID(id, i); 448 // Let two items in the first upload batch fail. 449 if (i != 23 && i != 42) { 450 collection.insert(id); 451 } 452 } 453 454 let syncID = await engine.resetLocalSyncID(); 455 let meta_global = Service.recordManager.set( 456 engine.metaURL, 457 new WBORecord(engine.metaURL) 458 ); 459 meta_global.payload.engines = { rotary: { version: engine.version, syncID } }; 460 461 try { 462 let changes = await engine._tracker.getChangedIDs(); 463 _( 464 `test_sync_partialUpload: Rotary tracker contents at first sync: ${JSON.stringify( 465 changes 466 )}` 467 ); 468 engine.enabled = true; 469 await GleanPings.sync.testSubmission( 470 reason => { 471 equal(reason, "schedule"); 472 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 473 474 const syncs = Glean.syncs.syncs.testGetValue(); 475 equal(syncs.length, 1); 476 477 const sync = syncs[0]; 478 ok(!sync.failureReason); 479 equal(sync.engines.length, 1); 480 481 const e = sync.engines[0]; 482 equal(e.name, "rotary"); 483 ok(!e.incoming); 484 ok(!e.failureReason); 485 deepEqual(e.outgoing, [ 486 { 487 sent: 234, 488 failed: 2, 489 failedReasons: [ 490 { name: "record-no-23", count: 1 }, 491 { name: "record-no-42", count: 1 }, 492 ], 493 }, 494 ]); 495 }, 496 async () => { 497 await sync_engine(engine); 498 } 499 ); 500 501 collection.post = function () { 502 throw new Error("Failure"); 503 }; 504 505 engine._store.items["record-no-1000"] = "Record No. 1000"; 506 await engine._tracker.addChangedID("record-no-1000", 1000); 507 collection.insert("record-no-1000", 1000); 508 509 await engine.setLastSync(123); 510 511 changes = await engine._tracker.getChangedIDs(); 512 _( 513 `test_sync_partialUpload: Rotary tracker contents at second sync: ${JSON.stringify( 514 changes 515 )}` 516 ); 517 // It would be nice if we had a more descriptive error for this... 518 const uploadFailureError = { 519 name: "httperror", 520 code: 500, 521 }; 522 await GleanPings.sync.testSubmission( 523 reason => { 524 equal(reason, "schedule"); 525 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 526 527 const syncs = Glean.syncs.syncs.testGetValue(); 528 equal(syncs.length, 1); 529 530 const sync = syncs[0]; 531 deepEqual(sync.failureReason, uploadFailureError); 532 equal(sync.engines.length, 1); 533 534 const e = sync.engines[0]; 535 equal(e.name, "rotary"); 536 deepEqual(e.incoming, { 537 failed: 1, 538 failedReasons: [ 539 { name: "No ciphertext: nothing to decrypt?", count: 1 }, 540 ], 541 }); 542 ok(!e.outgoing); 543 deepEqual(e.failureReason, uploadFailureError); 544 }, 545 async () => { 546 await sync_engine(engine, /500 Internal Server Error/); 547 } 548 ); 549 } finally { 550 await cleanAndGo(engine, server); 551 await engine.finalize(); 552 } 553 }); 554 555 add_task(async function test_generic_engine_fail() { 556 enableValidationPrefs(); 557 558 await Service.engineManager.register(SteamEngine); 559 let engine = Service.engineManager.get("steam"); 560 engine.enabled = true; 561 let server = await serverForFoo(engine); 562 await SyncTestingInfrastructure(server); 563 let e = new Error("generic failure message"); 564 engine._errToThrow = e; 565 566 try { 567 const changes = await engine._tracker.getChangedIDs(); 568 _( 569 `test_generic_engine_fail: Steam tracker contents: ${JSON.stringify( 570 changes 571 )}` 572 ); 573 await GleanPings.sync.testSubmission( 574 reason => { 575 equal(reason, "schedule"); 576 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 577 578 const syncs = Glean.syncs.syncs.testGetValue(); 579 equal(syncs.length, 1); 580 581 const sync = syncs[0]; 582 equal(sync.status.service, SYNC_FAILED_PARTIAL); 583 deepEqual( 584 sync.engines.find(err => err.name === "steam").failureReason, 585 { 586 name: "unexpectederror", 587 error: String(e), 588 } 589 ); 590 }, 591 async () => { 592 await Service.sync(); 593 } 594 ); 595 } finally { 596 await cleanAndGo(engine, server); 597 await Service.engineManager.unregister(engine); 598 } 599 }); 600 601 add_task(async function test_engine_fail_weird_errors() { 602 enableValidationPrefs(); 603 await Service.engineManager.register(SteamEngine); 604 let engine = Service.engineManager.get("steam"); 605 engine.enabled = true; 606 let server = await serverForFoo(engine); 607 await SyncTestingInfrastructure(server); 608 try { 609 let msg = "Bad things happened!"; 610 engine._errToThrow = { message: msg }; 611 await GleanPings.sync.testSubmission( 612 reason => { 613 equal(reason, "schedule"); 614 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 615 616 const syncs = Glean.syncs.syncs.testGetValue(); 617 equal(syncs.length, 1); 618 619 const sync = syncs[0]; 620 equal(sync.status.service, SYNC_FAILED_PARTIAL); 621 deepEqual( 622 sync.engines.find(err => err.name === "steam").failureReason, 623 { 624 name: "unexpectederror", 625 error: msg, 626 } 627 ); 628 }, 629 async () => { 630 await Service.sync(); 631 } 632 ); 633 let e = { msg }; 634 engine._errToThrow = e; 635 await GleanPings.sync.testSubmission( 636 reason => { 637 equal(reason, "schedule"); 638 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 639 640 const syncs = Glean.syncs.syncs.testGetValue(); 641 equal(syncs.length, 1); 642 643 const sync = syncs[0]; 644 equal(sync.status.service, SYNC_FAILED_PARTIAL); 645 deepEqual( 646 sync.engines.find(err => err.name === "steam").failureReason, 647 { 648 name: "unexpectederror", 649 error: JSON.stringify(e), 650 } 651 ); 652 }, 653 async () => { 654 await Service.sync(); 655 } 656 ); 657 } finally { 658 await cleanAndGo(engine, server); 659 Service.engineManager.unregister(engine); 660 } 661 }); 662 663 add_task(async function test_overrideTelemetryName() { 664 enableValidationPrefs(["steam"]); 665 666 await Service.engineManager.register(SteamEngine); 667 let engine = Service.engineManager.get("steam"); 668 engine.overrideTelemetryName = "steam-but-better"; 669 engine.enabled = true; 670 let server = await serverForFoo(engine); 671 await SyncTestingInfrastructure(server); 672 673 const problemsToReport = [ 674 { name: "someProblem", count: 123 }, 675 { name: "anotherProblem", count: 456 }, 676 ]; 677 678 try { 679 info("Sync with validation problems"); 680 engine.problemsToReport = problemsToReport; 681 await GleanPings.sync.testSubmission( 682 reason => { 683 equal(reason, "schedule"); 684 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 685 686 const syncs = Glean.syncs.syncs.testGetValue(); 687 equal(syncs.length, 1); 688 689 const sync = syncs[0]; 690 ok(!sync.engines.find(e => e.name === "steam")); 691 const eng = sync.engines.find(e => e.name === "steam-but-better"); 692 ok(eng); 693 delete eng.validation.took; // can't compare real times. 694 deepEqual( 695 eng.validation, 696 { 697 version: 1, 698 checked: 0, 699 problems: problemsToReport, 700 }, 701 "Should include validation report with overridden name" 702 ); 703 }, 704 async () => { 705 await Service.sync(); 706 } 707 ); 708 709 info("Sync without validation problems"); 710 engine.problemsToReport = null; 711 await GleanPings.sync.testSubmission( 712 reason => { 713 equal(reason, "schedule"); 714 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 715 716 const syncs = Glean.syncs.syncs.testGetValue(); 717 equal(syncs.length, 1); 718 719 const sync = syncs[0]; 720 ok(!sync.engines.find(e => e.name === "steam")); 721 const eng = sync.engines.find(e => e.name === "steam-but-better"); 722 ok(eng); 723 ok( 724 !eng.validation, 725 "Should not include validation report when there are no problems" 726 ); 727 }, 728 async () => { 729 await Service.sync(); 730 } 731 ); 732 } finally { 733 await cleanAndGo(engine, server); 734 await Service.engineManager.unregister(engine); 735 } 736 }); 737 738 add_task(async function test_engine_fail_ioerror() { 739 enableValidationPrefs(); 740 741 await Service.engineManager.register(SteamEngine); 742 let engine = Service.engineManager.get("steam"); 743 engine.enabled = true; 744 let server = await serverForFoo(engine); 745 await SyncTestingInfrastructure(server); 746 // create an IOError to re-throw as part of Sync. 747 try { 748 // (Note that fakeservices.js has replaced Utils.jsonMove etc, but for 749 // this test we need the real one so we get real exceptions from the 750 // filesystem.) 751 await Utils._real_jsonMove("file-does-not-exist", "anything", {}); 752 } catch (ex) { 753 engine._errToThrow = ex; 754 } 755 ok(engine._errToThrow, "expecting exception"); 756 757 try { 758 const changes = await engine._tracker.getChangedIDs(); 759 _( 760 `test_engine_fail_ioerror: Steam tracker contents: ${JSON.stringify( 761 changes 762 )}` 763 ); 764 await GleanPings.sync.testSubmission( 765 reason => { 766 equal(reason, "schedule"); 767 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 768 769 const syncs = Glean.syncs.syncs.testGetValue(); 770 equal(syncs.length, 1); 771 772 const sync = syncs[0]; 773 equal(sync.status.service, SYNC_FAILED_PARTIAL); 774 const failureReason = sync.engines.find( 775 e => e.name === "steam" 776 ).failureReason; 777 equal(failureReason.name, "unexpectederror"); 778 // ensure the profile dir in the exception message has been stripped. 779 ok( 780 !failureReason.error.includes(PathUtils.profileDir), 781 failureReason.error 782 ); 783 ok(failureReason.error.includes("[profileDir]"), failureReason.error); 784 }, 785 async () => { 786 await Service.sync(); 787 } 788 ); 789 } finally { 790 await cleanAndGo(engine, server); 791 await Service.engineManager.unregister(engine); 792 } 793 }); 794 795 add_task(async function test_clean_urls() { 796 enableValidationPrefs(); 797 798 await Service.engineManager.register(SteamEngine); 799 let engine = Service.engineManager.get("steam"); 800 engine.enabled = true; 801 let server = await serverForFoo(engine); 802 await SyncTestingInfrastructure(server); 803 engine._errToThrow = new TypeError( 804 "http://www.google .com is not a valid URL." 805 ); 806 807 try { 808 const changes = await engine._tracker.getChangedIDs(); 809 _(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`); 810 await GleanPings.sync.testSubmission( 811 reason => { 812 equal(reason, "schedule"); 813 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 814 815 const syncs = Glean.syncs.syncs.testGetValue(); 816 equal(syncs.length, 1); 817 818 const sync = syncs[0]; 819 equal(sync.status.service, SYNC_FAILED_PARTIAL); 820 const failureReason = sync.engines.find( 821 e => e.name === "steam" 822 ).failureReason; 823 equal(failureReason.name, "unexpectederror"); 824 equal(failureReason.error, "<URL> is not a valid URL."); 825 }, 826 async () => { 827 await Service.sync(); 828 } 829 ); 830 // Handle other errors that include urls. 831 engine._errToThrow = 832 "Other error message that includes some:url/foo/bar/ in it."; 833 await GleanPings.sync.testSubmission( 834 reason => { 835 equal(reason, "schedule"); 836 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 837 838 const syncs = Glean.syncs.syncs.testGetValue(); 839 equal(syncs.length, 1); 840 841 const sync = syncs[0]; 842 equal(sync.status.service, SYNC_FAILED_PARTIAL); 843 const failureReason = sync.engines.find( 844 e => e.name === "steam" 845 ).failureReason; 846 equal(failureReason.name, "unexpectederror"); 847 equal( 848 failureReason.error, 849 "Other error message that includes <URL> in it." 850 ); 851 }, 852 async () => { 853 await Service.sync(); 854 } 855 ); 856 } finally { 857 await cleanAndGo(engine, server); 858 await Service.engineManager.unregister(engine); 859 } 860 }); 861 862 // Arrange for a sync to hit a "real" OS error during a sync and make sure it's sanitized. 863 add_task(async function test_clean_real_os_error() { 864 enableValidationPrefs(); 865 866 // Simulate a real error. 867 await Service.engineManager.register(SteamEngine); 868 let engine = Service.engineManager.get("steam"); 869 engine.enabled = true; 870 let server = await serverForFoo(engine); 871 await SyncTestingInfrastructure(server); 872 let path = PathUtils.join(PathUtils.profileDir, "no", "such", "path.json"); 873 try { 874 await IOUtils.readJSON(path); 875 throw new Error("should fail to read the file"); 876 } catch (ex) { 877 engine._errToThrow = ex; 878 } 879 880 try { 881 const changes = await engine._tracker.getChangedIDs(); 882 _(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`); 883 await GleanPings.sync.testSubmission( 884 reason => { 885 equal(reason, "schedule"); 886 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 887 888 const syncs = Glean.syncs.syncs.testGetValue(); 889 equal(syncs.length, 1); 890 891 const sync = syncs[0]; 892 equal(sync.status.service, SYNC_FAILED_PARTIAL); 893 const failureReason = sync.engines.find( 894 e => e.name === "steam" 895 ).failureReason; 896 equal(failureReason.name, "unexpectederror"); 897 equal( 898 failureReason.error, 899 "OS error [File/Path not found] Could not open `[profileDir]/no/such/path.json': file does not exist" 900 ); 901 }, 902 async () => { 903 await Service.sync(); 904 } 905 ); 906 } finally { 907 await cleanAndGo(engine, server); 908 await Service.engineManager.unregister(engine); 909 } 910 }); 911 912 add_task(async function test_initial_sync_engines() { 913 enableValidationPrefs(); 914 915 await Service.engineManager.register(SteamEngine); 916 let engine = Service.engineManager.get("steam"); 917 engine.enabled = true; 918 // These are the only ones who actually have things to sync at startup. 919 let telemetryEngineNames = ["clients", "prefs", "tabs", "bookmarks-buffered"]; 920 let server = await serverForEnginesWithKeys( 921 { foo: "password" }, 922 ["bookmarks", "prefs", "tabs"].map(name => Service.engineManager.get(name)) 923 ); 924 await SyncTestingInfrastructure(server); 925 try { 926 const changes = await engine._tracker.getChangedIDs(); 927 _( 928 `test_initial_sync_engines: Steam tracker contents: ${JSON.stringify( 929 changes 930 )}` 931 ); 932 await GleanPings.sync.testSubmission( 933 reason => { 934 equal(reason, "schedule"); 935 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 936 937 const syncs = Glean.syncs.syncs.testGetValue(); 938 equal(syncs.length, 1); 939 940 const sync = syncs[0]; 941 equal(sync.engines.find(e => e.name === "clients").outgoing[0].sent, 1); 942 equal(sync.engines.find(e => e.name === "tabs").outgoing[0].sent, 1); 943 944 sync.engines 945 .filter(e => telemetryEngineNames.includes(e.name)) 946 .forEach(e => { 947 greaterOrEqual(e.took, 0); 948 ok(!!e.outgoing); 949 equal(e.outgoing.length, 1); 950 notEqual(e.outgoing[0].sent, undefined); 951 equal(e.outgoing[0].failed, undefined); 952 equal(e.outgoing[0].failedReasons, undefined); 953 }); 954 }, 955 async () => { 956 await Service.sync(); 957 } 958 ); 959 } finally { 960 await cleanAndGo(engine, server); 961 await Service.engineManager.unregister(engine); 962 } 963 }); 964 965 add_task(async function test_nserror() { 966 enableValidationPrefs(); 967 968 await Service.engineManager.register(SteamEngine); 969 let engine = Service.engineManager.get("steam"); 970 engine.enabled = true; 971 let server = await serverForFoo(engine); 972 await SyncTestingInfrastructure(server); 973 engine._errToThrow = Components.Exception( 974 "NS_ERROR_UNKNOWN_HOST", 975 Cr.NS_ERROR_UNKNOWN_HOST 976 ); 977 try { 978 const changes = await engine._tracker.getChangedIDs(); 979 _(`test_nserror: Steam tracker contents: ${JSON.stringify(changes)}`); 980 await GleanPings.sync.testSubmission( 981 reason => { 982 equal(reason, "schedule"); 983 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 984 985 const syncs = Glean.syncs.syncs.testGetValue(); 986 equal(syncs.length, 1); 987 988 const sync = syncs[0]; 989 deepEqual(sync.status, { 990 service: SYNC_FAILED_PARTIAL, 991 sync: LOGIN_FAILED_NETWORK_ERROR, 992 }); 993 const eng = sync.engines.find(e => e.name === "steam"); 994 deepEqual(eng.failureReason, { 995 name: "httperror", 996 code: Cr.NS_ERROR_UNKNOWN_HOST, 997 }); 998 }, 999 async () => { 1000 await Service.sync(); 1001 } 1002 ); 1003 } finally { 1004 await cleanAndGo(engine, server); 1005 await Service.engineManager.unregister(engine); 1006 } 1007 }); 1008 1009 add_task(async function test_sync_why() { 1010 enableValidationPrefs(); 1011 1012 await Service.engineManager.register(SteamEngine); 1013 let engine = Service.engineManager.get("steam"); 1014 engine.enabled = true; 1015 let server = await serverForFoo(engine); 1016 await SyncTestingInfrastructure(server); 1017 let e = new Error("generic failure message"); 1018 engine._errToThrow = e; 1019 1020 try { 1021 const changes = await engine._tracker.getChangedIDs(); 1022 _( 1023 `test_generic_engine_fail: Steam tracker contents: ${JSON.stringify( 1024 changes 1025 )}` 1026 ); 1027 await GleanPings.sync.testSubmission( 1028 reason => { 1029 equal(reason, "schedule"); 1030 equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32)); 1031 1032 const syncs = Glean.syncs.syncs.testGetValue(); 1033 equal(syncs.length, 1); 1034 1035 const sync = syncs[0]; 1036 equal(sync.why, "user"); 1037 }, 1038 async () => { 1039 await Service.sync({ why: "user" }); 1040 } 1041 ); 1042 } finally { 1043 await cleanAndGo(engine, server); 1044 await Service.engineManager.unregister(engine); 1045 } 1046 }); 1047 1048 add_task(async function test_discarding() { 1049 enableValidationPrefs(); 1050 1051 let helper = track_collections_helper(); 1052 let upd = helper.with_updated_collection; 1053 let telem = get_sync_test_telemetry(); 1054 telem.maxPayloadCount = 2; 1055 telem.submissionInterval = Infinity; 1056 1057 let server; 1058 try { 1059 let handlers = { 1060 "/1.1/johndoe/info/collections": helper.handler, 1061 "/1.1/johndoe/storage/crypto/keys": upd( 1062 "crypto", 1063 new ServerWBO("keys").handler() 1064 ), 1065 "/1.1/johndoe/storage/meta/global": upd( 1066 "meta", 1067 new ServerWBO("global").handler() 1068 ), 1069 }; 1070 1071 let collections = [ 1072 "clients", 1073 "bookmarks", 1074 "forms", 1075 "history", 1076 "passwords", 1077 "prefs", 1078 "tabs", 1079 ]; 1080 1081 for (let coll of collections) { 1082 handlers["/1.1/johndoe/storage/" + coll] = upd( 1083 coll, 1084 new ServerCollection({}, true).handler() 1085 ); 1086 } 1087 1088 server = httpd_setup(handlers); 1089 await configureIdentity({ username: "johndoe" }, server); 1090 GleanPings.sync.testBeforeNextSubmit(() => { 1091 ok(false, "Submitted telemetry ping when we should not have."); 1092 }); 1093 1094 for (let i = 0; i < 5; ++i) { 1095 await Service.sync(); 1096 } 1097 telem.submissionInterval = -1; 1098 1099 await GleanPings.sync.testSubmission( 1100 reason => { 1101 equal(reason, "schedule"); 1102 equal(Glean.syncs.discarded.testGetValue(), 4); 1103 const syncs = Glean.syncs.syncs.testGetValue(); 1104 equal(syncs.length, 2); 1105 syncs.forEach(assert_success_sync); 1106 }, 1107 async () => { 1108 await Service.sync(); // Sixth time's the charm. 1109 } 1110 ); 1111 } finally { 1112 telem.maxPayloadCount = 500; 1113 telem.submissionInterval = -1; 1114 if (server) { 1115 await promiseStopServer(server); 1116 } 1117 } 1118 }); 1119 1120 add_task(async function test_no_foreign_engines_in_error_ping() { 1121 enableValidationPrefs(); 1122 1123 await Service.engineManager.register(BogusEngine); 1124 let engine = Service.engineManager.get("bogus"); 1125 engine.enabled = true; 1126 let server = await serverForFoo(engine); 1127 engine._errToThrow = new Error("Oh no!"); 1128 await SyncTestingInfrastructure(server); 1129 try { 1130 await GleanPings.sync.testSubmission( 1131 reason => { 1132 equal(reason, "schedule"); 1133 const syncs = Glean.syncs.syncs.testGetValue(); 1134 equal(syncs.length, 1); 1135 const sync = syncs[0]; 1136 equal(sync.status.service, SYNC_FAILED_PARTIAL); 1137 ok(sync.engines.every(e => e.name !== "bogus")); 1138 }, 1139 async () => { 1140 await Service.sync(); 1141 } 1142 ); 1143 } finally { 1144 await cleanAndGo(engine, server); 1145 await Service.engineManager.unregister(engine); 1146 } 1147 }); 1148 1149 add_task(async function test_no_foreign_engines_in_success_ping() { 1150 enableValidationPrefs(); 1151 1152 await Service.engineManager.register(BogusEngine); 1153 let engine = Service.engineManager.get("bogus"); 1154 engine.enabled = true; 1155 let server = await serverForFoo(engine); 1156 1157 await SyncTestingInfrastructure(server); 1158 try { 1159 await GleanPings.sync.testSubmission( 1160 reason => { 1161 equal(reason, "schedule"); 1162 const syncs = Glean.syncs.syncs.testGetValue(); 1163 equal(syncs.length, 1); 1164 const sync = syncs[0]; 1165 ok(sync.engines.every(e => e.name !== "bogus")); 1166 }, 1167 async () => { 1168 await Service.sync(); 1169 } 1170 ); 1171 } finally { 1172 await cleanAndGo(engine, server); 1173 await Service.engineManager.unregister(engine); 1174 } 1175 }); 1176 1177 add_task(async function test_no_node_type() { 1178 let server = sync_httpd_setup({}); 1179 await configureIdentity(null, server); 1180 1181 await GleanPings.sync.testSubmission( 1182 reason => { 1183 equal(reason, "schedule"); 1184 Assert.strictEqual(Glean.syncs.syncNodeType.testGetValue(), null); 1185 }, 1186 async () => { 1187 await Service.sync(); 1188 } 1189 ); 1190 await promiseStopServer(server); 1191 }); 1192 1193 add_task(async function test_node_type() { 1194 Service.identity.logout(); 1195 let server = sync_httpd_setup({}); 1196 await configureIdentity({ node_type: "the-node-type" }, server); 1197 1198 await GleanPings.sync.testSubmission( 1199 reason => { 1200 equal(reason, "schedule"); 1201 equal(Glean.syncs.syncNodeType.testGetValue(), "the-node-type"); 1202 }, 1203 async () => { 1204 await Service.sync(); 1205 } 1206 ); 1207 await promiseStopServer(server); 1208 }); 1209 1210 add_task(async function test_node_type_change() { 1211 Service.identity.logout(); 1212 let server = sync_httpd_setup({}); 1213 await configureIdentity({ node_type: "first-node-type" }, server); 1214 // Default to submitting each hour - we should still submit on node change. 1215 let telem = get_sync_test_telemetry(); 1216 telem.submissionInterval = 60 * 60 * 1000; 1217 // reset the node type from previous test or our first sync will submit. 1218 telem.lastSyncNodeType = null; 1219 // do 2 syncs with the same node type. 1220 await Service.sync(); 1221 await Service.sync(); 1222 // then another with a different node type. 1223 Service.identity.logout(); 1224 await configureIdentity({ node_type: "second-node-type" }, server); 1225 await GleanPings.sync.testSubmission( 1226 () => { 1227 equal( 1228 Glean.syncs.syncs.testGetValue().length, 1229 2, 1230 "2 syncs in first ping" 1231 ); 1232 equal(Glean.syncs.syncNodeType.testGetValue(), "first-node-type"); 1233 }, 1234 async () => { 1235 await Service.sync(); 1236 } 1237 ); 1238 await GleanPings.sync.testSubmission( 1239 () => { 1240 equal( 1241 Glean.syncs.syncs.testGetValue().length, 1242 1, 1243 "1 sync in second ping" 1244 ); 1245 equal(Glean.syncs.syncNodeType.testGetValue(), "second-node-type"); 1246 }, 1247 async () => { 1248 telem.finish(); 1249 } 1250 ); 1251 await promiseStopServer(server); 1252 }); 1253 1254 add_task(async function test_uid_change() { 1255 enableValidationPrefs(); 1256 1257 await Service.engineManager.register(BogusEngine); 1258 let engine = Service.engineManager.get("bogus"); 1259 engine.enabled = true; 1260 let server = await serverForFoo(engine); 1261 1262 await SyncTestingInfrastructure(server); 1263 let telem = get_sync_test_telemetry(); 1264 telem.maxPayloadCount = 500; 1265 telem.submissionInterval = Infinity; 1266 1267 // a sync with the "old" uid. 1268 await Service.sync(); 1269 1270 fxAccounts.telemetry._setHashedUID("deadbeef"); 1271 1272 try { 1273 await GleanPings.sync.testSubmission( 1274 reason => { 1275 equal(reason, "idchange"); 1276 equal( 1277 Glean.syncs.syncs.testGetValue().length, 1278 1, 1279 "first sync in its own ping" 1280 ); 1281 }, 1282 async () => { 1283 await Service.sync(); 1284 } 1285 ); 1286 await GleanPings.sync.testSubmission( 1287 () => { 1288 equal( 1289 Glean.syncs.syncs.testGetValue().length, 1290 1, 1291 "second sync in its own ping" 1292 ); 1293 }, 1294 async () => { 1295 telem.finish(); 1296 } 1297 ); 1298 } finally { 1299 await cleanAndGo(engine, server); 1300 await Service.engineManager.unregister(engine); 1301 } 1302 }); 1303 1304 add_task(async function test_deletion_request_ping() { 1305 async function assertRecordedSyncDeviceID(expected) { 1306 // `onAccountInitOrChange` sets the id asynchronously, so wait a tick. 1307 await Promise.resolve(); 1308 equal(Glean.deletionRequest.syncDeviceId.testGetValue(), expected); 1309 } 1310 Services.fog.testResetFOG(); 1311 1312 const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff"; 1313 const MOCK_DEVICE_ID1 = "ffeeddccbbaa99887766554433221100"; 1314 const MOCK_DEVICE_ID2 = "aabbccddeeff99887766554433221100"; 1315 1316 // Calculated by hand using SHA256(DEVICE_ID + HASHED_UID)[:32] 1317 const SANITIZED_DEVICE_ID1 = "dd7c845006df9baa1c6d756926519c8c"; 1318 const SANITIZED_DEVICE_ID2 = "0d06919a736fc029007e1786a091882c"; 1319 1320 let currentDeviceID = null; 1321 sinon.stub(fxAccounts.device, "getLocalId").callsFake(() => { 1322 return Promise.resolve(currentDeviceID); 1323 }); 1324 let telem = get_sync_test_telemetry(); 1325 sinon.stub(telem, "isProductionSyncUser").callsFake(() => true); 1326 fxAccounts.telemetry._setHashedUID(false); 1327 1328 try { 1329 // The scalar should start out undefined, since no user is actually logged in. 1330 await assertRecordedSyncDeviceID(null); 1331 1332 // If we start up without knowing the hashed UID, it should stay undefined. 1333 telem.observe(null, "weave:service:ready"); 1334 await assertRecordedSyncDeviceID(null); 1335 1336 // But now let's say we've discovered the hashed UID from the server. 1337 fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID); 1338 currentDeviceID = MOCK_DEVICE_ID1; 1339 1340 // Now when we load up, we'll record the sync device id. 1341 telem.observe(null, "weave:service:ready"); 1342 await assertRecordedSyncDeviceID(SANITIZED_DEVICE_ID1); 1343 1344 // When the device-id changes we'll update it. 1345 currentDeviceID = MOCK_DEVICE_ID2; 1346 telem.observe(null, "fxaccounts:new_device_id"); 1347 await assertRecordedSyncDeviceID(SANITIZED_DEVICE_ID2); 1348 1349 // When the user signs out we'll clear it. 1350 telem.observe(null, "fxaccounts:onlogout"); 1351 await assertRecordedSyncDeviceID(""); 1352 } finally { 1353 fxAccounts.telemetry._setHashedUID(false); 1354 telem.isProductionSyncUser.restore(); 1355 fxAccounts.device.getLocalId.restore(); 1356 } 1357 }); 1358 1359 // TODO: Is the topic `"weave:telemetry:migration"` presently hooked up? 1360 add_task(async function test_migration() { 1361 let telem = get_sync_test_telemetry(); 1362 const migrationInfo = { 1363 entries: 42, 1364 entries_successful: 42, 1365 extensions: 84, 1366 extensions_successful: 83, 1367 openFailure: false, 1368 }; 1369 telem._addMigrationRecord("webext-storage", migrationInfo); 1370 await GleanPings.sync.testSubmission( 1371 () => { 1372 const migrations = Glean.syncs.migrations.testGetValue(); 1373 equal(migrations.length, 1); 1374 deepEqual(migrations[0], { 1375 migration_type: "webext-storage", 1376 entries: 42, 1377 entriesSuccessful: 42, 1378 extensions: 84, 1379 extensionsSuccessful: 83, 1380 openFailure: false, 1381 }); 1382 }, 1383 () => { 1384 telem.finish(); 1385 } 1386 ); 1387 });