test_web_channel.js (37214B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { ON_PROFILE_CHANGE_NOTIFICATION, WEBCHANNEL_ID, log } = 7 ChromeUtils.importESModule("resource://gre/modules/FxAccountsCommon.sys.mjs"); 8 const { CryptoUtils } = ChromeUtils.importESModule( 9 "moz-src:///services/crypto/modules/utils.sys.mjs" 10 ); 11 const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } = 12 ChromeUtils.importESModule( 13 "resource://gre/modules/FxAccountsWebChannel.sys.mjs" 14 ); 15 16 const { PREF_LAST_FXA_USER_EMAIL, PREF_LAST_FXA_USER_UID } = 17 ChromeUtils.importESModule("resource://gre/modules/FxAccountsCommon.sys.mjs"); 18 19 const URL_STRING = "https://example.com"; 20 21 const mockSendingContext = { 22 browsingContext: { top: { embedderElement: {} } }, 23 principal: {}, 24 eventTarget: {}, 25 }; 26 27 add_setup(function setup() { 28 // The profile service requires the directory service to have been initialized. 29 Cc["@mozilla.org/xre/directory-provider;1"].getService(Ci.nsIXREDirProvider); 30 }); 31 32 add_test(function () { 33 validationHelper(undefined, "Error: Missing configuration options"); 34 35 validationHelper( 36 { 37 channel_id: WEBCHANNEL_ID, 38 }, 39 "Error: Missing 'content_uri' option" 40 ); 41 42 validationHelper( 43 { 44 content_uri: "bad uri", 45 channel_id: WEBCHANNEL_ID, 46 }, 47 /NS_ERROR_MALFORMED_URI/ 48 ); 49 50 validationHelper( 51 { 52 content_uri: URL_STRING, 53 }, 54 "Error: Missing 'channel_id' option" 55 ); 56 57 run_next_test(); 58 }); 59 60 add_task(async function test_rejection_reporting() { 61 Services.prefs.setBoolPref( 62 "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", 63 false 64 ); 65 66 let mockMessage = { 67 command: "fxaccounts:login", 68 messageId: "1234", 69 data: { email: "testuser@testuser.com", uid: "testuser" }, 70 }; 71 72 let channel = new FxAccountsWebChannel({ 73 channel_id: WEBCHANNEL_ID, 74 content_uri: URL_STRING, 75 helpers: { 76 login(accountData) { 77 equal( 78 accountData.email, 79 "testuser@testuser.com", 80 "Should forward incoming message data to the helper" 81 ); 82 return Promise.reject(new Error("oops")); 83 }, 84 }, 85 }); 86 87 let promiseSend = new Promise(resolve => { 88 channel._channel.send = (message, context) => { 89 resolve({ message, context }); 90 }; 91 }); 92 93 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 94 95 let { message, context } = await promiseSend; 96 97 equal(context, mockSendingContext, "Should forward the original context"); 98 equal( 99 message.command, 100 "fxaccounts:login", 101 "Should include the incoming command" 102 ); 103 equal(message.messageId, "1234", "Should include the message ID"); 104 equal( 105 message.data.error.message, 106 "Error: oops", 107 "Should convert the error message to a string" 108 ); 109 notStrictEqual( 110 message.data.error.stack, 111 null, 112 "Should include the stack for JS error rejections" 113 ); 114 }); 115 116 add_test(function test_exception_reporting() { 117 let mockMessage = { 118 command: "fxaccounts:sync_preferences", 119 messageId: "5678", 120 data: { entryPoint: "fxa:verification_complete" }, 121 }; 122 123 let channel = new FxAccountsWebChannel({ 124 channel_id: WEBCHANNEL_ID, 125 content_uri: URL_STRING, 126 helpers: { 127 openSyncPreferences(browser, entryPoint) { 128 equal( 129 entryPoint, 130 "fxa:verification_complete", 131 "Should forward incoming message data to the helper" 132 ); 133 throw new TypeError("splines not reticulated"); 134 }, 135 }, 136 }); 137 138 channel._channel.send = (message, context) => { 139 equal(context, mockSendingContext, "Should forward the original context"); 140 equal( 141 message.command, 142 "fxaccounts:sync_preferences", 143 "Should include the incoming command" 144 ); 145 equal(message.messageId, "5678", "Should include the message ID"); 146 equal( 147 message.data.error.message, 148 "TypeError: splines not reticulated", 149 "Should convert the exception to a string" 150 ); 151 notStrictEqual( 152 message.data.error.stack, 153 null, 154 "Should include the stack for JS exceptions" 155 ); 156 157 run_next_test(); 158 }; 159 160 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 161 }); 162 163 add_test(function test_error_message_remove_profile_path() { 164 const errors = { 165 windows: { 166 err: new Error( 167 "Win error 183 during operation rename on file C:\\Users\\Some Computer\\AppData\\Roaming\\" + 168 "Mozilla\\Firefox\\Profiles\\dbzjmzxa.default\\signedInUser.json (Cannot create a file)" 169 ), 170 expected: 171 "Error: Win error 183 during operation rename on file C:[REDACTED]signedInUser.json (Cannot create a file)", 172 }, 173 unix: { 174 err: new Error( 175 "Unix error 28 during operation write on file /Users/someuser/Library/Application Support/" + 176 "Firefox/Profiles/dbzjmzxa.default-release-7/signedInUser.json (No space left on device)" 177 ), 178 expected: 179 "Error: Unix error 28 during operation write on file [REDACTED]signedInUser.json (No space left on device)", 180 }, 181 netpath: { 182 err: new Error( 183 "Win error 32 during operation rename on file \\\\SVC.LOC\\HOMEDIRS$\\USERNAME\\Mozilla\\" + 184 "Firefox\\Profiles\\dbzjmzxa.default-release-7\\signedInUser.json (No space left on device)" 185 ), 186 expected: 187 "Error: Win error 32 during operation rename on file [REDACTED]signedInUser.json (No space left on device)", 188 }, 189 mount: { 190 err: new Error( 191 "Win error 649 during operation rename on file C:\\SnapVolumes\\MountPoints\\" + 192 "{9e399ec5-0000-0000-0000-100000000000}\\SVROOT\\Users\\username\\AppData\\Roaming\\Mozilla\\Firefox\\" + 193 "Profiles\\dbzjmzxa.default-release\\signedInUser.json (The create operation failed)" 194 ), 195 expected: 196 "Error: Win error 649 during operation rename on file C:[REDACTED]signedInUser.json " + 197 "(The create operation failed)", 198 }, 199 }; 200 const mockMessage = { 201 command: "fxaccounts:sync_preferences", 202 messageId: "1234", 203 }; 204 const channel = new FxAccountsWebChannel({ 205 channel_id: WEBCHANNEL_ID, 206 content_uri: URL_STRING, 207 }); 208 209 let testNum = 0; 210 const toTest = Object.keys(errors).length; 211 for (const key in errors) { 212 let error = errors[key]; 213 channel._channel.send = message => { 214 equal( 215 message.data.error.message, 216 error.expected, 217 "Should remove the profile path from the error message" 218 ); 219 testNum++; 220 if (testNum === toTest) { 221 run_next_test(); 222 } 223 }; 224 channel._sendError(error.err, mockMessage, mockSendingContext); 225 } 226 }); 227 228 add_test(function test_profile_image_change_message() { 229 var mockMessage = { 230 command: "profile:change", 231 data: { uid: "foo" }, 232 }; 233 234 makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { 235 Assert.equal(data, "foo"); 236 run_next_test(); 237 }); 238 239 var channel = new FxAccountsWebChannel({ 240 channel_id: WEBCHANNEL_ID, 241 content_uri: URL_STRING, 242 }); 243 244 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 245 }); 246 247 add_test(function test_login_message() { 248 let mockMessage = { 249 command: "fxaccounts:login", 250 data: { email: "testuser@testuser.com" }, 251 }; 252 253 let channel = new FxAccountsWebChannel({ 254 channel_id: WEBCHANNEL_ID, 255 content_uri: URL_STRING, 256 helpers: { 257 login(accountData) { 258 Assert.equal(accountData.email, "testuser@testuser.com"); 259 run_next_test(); 260 return Promise.resolve(); 261 }, 262 }, 263 }); 264 265 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 266 }); 267 268 add_test(function test_oauth_login() { 269 const mockData = { 270 code: "oauth code", 271 state: "state parameter", 272 declinedSyncEngines: ["tabs", "creditcards"], 273 offeredSyncEngines: ["tabs", "creditcards", "history"], 274 }; 275 const mockMessage = { 276 command: "fxaccounts:oauth_login", 277 data: mockData, 278 }; 279 const channel = new FxAccountsWebChannel({ 280 channel_id: WEBCHANNEL_ID, 281 content_uri: URL_STRING, 282 helpers: { 283 oauthLogin(data) { 284 Assert.deepEqual(data, mockData); 285 run_next_test(); 286 return Promise.resolve(); 287 }, 288 }, 289 }); 290 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 291 }); 292 293 add_test(function test_logout_message() { 294 let mockMessage = { 295 command: "fxaccounts:logout", 296 data: { uid: "foo" }, 297 }; 298 299 let channel = new FxAccountsWebChannel({ 300 channel_id: WEBCHANNEL_ID, 301 content_uri: URL_STRING, 302 helpers: { 303 logout(uid) { 304 Assert.equal(uid, "foo"); 305 run_next_test(); 306 return Promise.resolve(); 307 }, 308 }, 309 }); 310 311 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 312 }); 313 314 add_test(function test_delete_message() { 315 let mockMessage = { 316 command: "fxaccounts:delete", 317 data: { uid: "foo" }, 318 }; 319 320 let channel = new FxAccountsWebChannel({ 321 channel_id: WEBCHANNEL_ID, 322 content_uri: URL_STRING, 323 helpers: { 324 logout(uid) { 325 Assert.equal(uid, "foo"); 326 run_next_test(); 327 return Promise.resolve(); 328 }, 329 }, 330 }); 331 332 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 333 }); 334 335 add_test(function test_can_link_account_message() { 336 let mockMessage = { 337 command: "fxaccounts:can_link_account", 338 data: { email: "testuser@testuser.com", uid: "testuser" }, 339 }; 340 341 let channel = new FxAccountsWebChannel({ 342 channel_id: WEBCHANNEL_ID, 343 content_uri: URL_STRING, 344 helpers: { 345 _selectableProfilesEnabled() { 346 return false; 347 }, 348 shouldAllowRelink(acctData) { 349 Assert.deepEqual(acctData, mockMessage.data); 350 run_next_test(); 351 }, 352 }, 353 }); 354 355 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 356 }); 357 358 add_test(function test_sync_preferences_message() { 359 let mockMessage = { 360 command: "fxaccounts:sync_preferences", 361 data: { entryPoint: "fxa:verification_complete" }, 362 }; 363 364 let channel = new FxAccountsWebChannel({ 365 channel_id: WEBCHANNEL_ID, 366 content_uri: URL_STRING, 367 helpers: { 368 openSyncPreferences(browser, entryPoint) { 369 Assert.equal(entryPoint, "fxa:verification_complete"); 370 Assert.equal( 371 browser, 372 mockSendingContext.browsingContext.top.embedderElement 373 ); 374 run_next_test(); 375 }, 376 }, 377 }); 378 379 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 380 }); 381 382 add_test(function test_fxa_status_message() { 383 let mockMessage = { 384 command: "fxaccounts:fxa_status", 385 messageId: 123, 386 data: { 387 service: "sync", 388 context: "fx_desktop_v3", 389 }, 390 }; 391 392 let channel = new FxAccountsWebChannel({ 393 channel_id: WEBCHANNEL_ID, 394 content_uri: URL_STRING, 395 helpers: { 396 async getFxaStatus(service, sendingContext, isPairing, context) { 397 Assert.equal(service, "sync"); 398 Assert.equal(sendingContext, mockSendingContext); 399 Assert.ok(!isPairing); 400 Assert.equal(context, "fx_desktop_v3"); 401 return { 402 signedInUser: { 403 email: "testuser@testuser.com", 404 sessionToken: "session-token", 405 uid: "uid", 406 verified: true, 407 }, 408 capabilities: { 409 engines: ["creditcards", "addresses"], 410 }, 411 }; 412 }, 413 }, 414 }); 415 416 channel._channel = { 417 send(response) { 418 Assert.equal(response.command, "fxaccounts:fxa_status"); 419 Assert.equal(response.messageId, 123); 420 421 let signedInUser = response.data.signedInUser; 422 Assert.ok(!!signedInUser); 423 Assert.equal(signedInUser.email, "testuser@testuser.com"); 424 Assert.equal(signedInUser.sessionToken, "session-token"); 425 Assert.equal(signedInUser.uid, "uid"); 426 Assert.equal(signedInUser.verified, true); 427 428 deepEqual(response.data.capabilities.engines, [ 429 "creditcards", 430 "addresses", 431 ]); 432 433 run_next_test(); 434 }, 435 }; 436 437 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 438 }); 439 440 add_test(function test_respond_to_invalid_commands() { 441 let mockMessageLogout = { 442 command: "fxaccounts:lagaut", // intentional typo. 443 messageId: 123, 444 data: {}, 445 }; 446 447 let channel = new FxAccountsWebChannel({ 448 channel_id: WEBCHANNEL_ID, 449 content_uri: URL_STRING, 450 }); 451 channel._channel = { 452 send(response) { 453 Assert.equal("fxaccounts:lagaut", response.command); 454 Assert.ok(!!response.data); 455 Assert.ok(!!response.data.error); 456 457 run_next_test(); 458 }, 459 }; 460 461 channel._channelCallback( 462 WEBCHANNEL_ID, 463 mockMessageLogout, 464 mockSendingContext 465 ); 466 }); 467 468 add_test(function test_unrecognized_message() { 469 let mockMessage = { 470 command: "fxaccounts:unrecognized", 471 data: {}, 472 }; 473 474 let channel = new FxAccountsWebChannel({ 475 channel_id: WEBCHANNEL_ID, 476 content_uri: URL_STRING, 477 }); 478 479 // no error is expected. 480 channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); 481 run_next_test(); 482 }); 483 484 add_test(function test_helpers_should_allow_relink_same_account() { 485 let helpers = new FxAccountsWebChannelHelpers(); 486 487 helpers.setPreviousAccountHashPref("testuser"); 488 Assert.ok( 489 helpers.shouldAllowRelink({ 490 email: "testuser@testuser.com", 491 uid: "testuser", 492 }) 493 ); 494 495 run_next_test(); 496 }); 497 498 add_test(function test_helpers_should_allow_relink_different_email() { 499 let helpers = new FxAccountsWebChannelHelpers(); 500 501 helpers.setPreviousAccountHashPref("testuser"); 502 503 helpers._promptForRelink = acctName => { 504 return acctName === "allowed_to_relink@testuser.com"; 505 }; 506 507 Assert.ok( 508 helpers.shouldAllowRelink({ 509 uid: "uid", 510 email: "allowed_to_relink@testuser.com", 511 }) 512 ); 513 Assert.ok( 514 !helpers.shouldAllowRelink({ 515 uid: "uid", 516 email: "not_allowed_to_relink@testuser.com", 517 }) 518 ); 519 520 run_next_test(); 521 }); 522 523 add_task(async function test_helpers_login_without_customize_sync() { 524 let helpers = new FxAccountsWebChannelHelpers({ 525 fxAccounts: { 526 getSignedInUser() { 527 return Promise.resolve(null); 528 }, 529 _internal: { 530 setSignedInUser(accountData) { 531 return new Promise(resolve => { 532 // ensure fxAccounts is informed of the new user being signed in. 533 Assert.equal(accountData.email, "testuser@testuser.com"); 534 535 // verifiedCanLinkAccount should be stripped in the data. 536 Assert.equal(false, "verifiedCanLinkAccount" in accountData); 537 538 resolve(); 539 }); 540 }, 541 }, 542 telemetry: { 543 recordConnection: sinon.spy(), 544 }, 545 }, 546 weaveXPCOM: { 547 whenLoaded() {}, 548 Weave: { 549 Service: { 550 configure() {}, 551 }, 552 }, 553 }, 554 }); 555 556 // ensure the previous account pref is overwritten. 557 helpers.setPreviousAccountHashPref("lastuser"); 558 559 await helpers.login({ 560 uid: "testuser", 561 email: "testuser@testuser.com", 562 verifiedCanLinkAccount: true, 563 customizeSync: false, 564 }); 565 Assert.ok( 566 helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel") 567 ); 568 }); 569 570 add_task(async function test_helpers_login_set_previous_account_hash() { 571 let helpers = new FxAccountsWebChannelHelpers({ 572 fxAccounts: { 573 getSignedInUser() { 574 return Promise.resolve(null); 575 }, 576 _internal: { 577 setSignedInUser() { 578 return new Promise(resolve => { 579 // previously signed in user preference is updated. 580 Assert.equal( 581 Services.prefs.getStringPref(PREF_LAST_FXA_USER_UID), 582 CryptoUtils.sha256Base64("new_uid") 583 ); 584 Assert.equal( 585 Services.prefs.getStringPref(PREF_LAST_FXA_USER_EMAIL, ""), 586 "" 587 ); 588 resolve(); 589 }); 590 }, 591 }, 592 telemetry: { 593 recordConnection() {}, 594 }, 595 }, 596 weaveXPCOM: { 597 whenLoaded() {}, 598 Weave: { 599 Service: { 600 configure() {}, 601 }, 602 }, 603 }, 604 }); 605 606 // ensure the previous account pref is overwritten. 607 helpers.setPreviousAccountHashPref("last_uid"); 608 609 await helpers.login({ 610 uid: "new_uid", 611 email: "newuser@testuser.com", 612 verifiedCanLinkAccount: true, 613 customizeSync: false, 614 verified: true, 615 }); 616 }); 617 618 add_task(async function test_helpers_login_another_user_signed_in() { 619 let helpers = new FxAccountsWebChannelHelpers({ 620 fxAccounts: { 621 getSignedInUser() { 622 return Promise.resolve({ uid: "foo" }); 623 }, 624 _internal: { 625 setSignedInUser(accountData) { 626 return new Promise(resolve => { 627 // ensure fxAccounts is informed of the new user being signed in. 628 Assert.equal(accountData.email, "testuser@testuser.com"); 629 resolve(); 630 }); 631 }, 632 }, 633 telemetry: { 634 recordConnection: sinon.spy(), 635 }, 636 }, 637 weaveXPCOM: { 638 whenLoaded() {}, 639 Weave: { 640 Service: { 641 configure() {}, 642 }, 643 }, 644 }, 645 }); 646 helpers._disconnect = sinon.spy(); 647 648 await helpers.login({ 649 uid: "testuser", 650 email: "testuser@testuser.com", 651 verifiedCanLinkAccount: true, 652 customizeSync: false, 653 }); 654 Assert.ok( 655 helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel") 656 ); 657 Assert.ok(helpers._disconnect.called); 658 }); 659 660 // The FxA server sends the `login` command after the user is signed in 661 // when upgrading from third-party auth to password + sync 662 add_task(async function test_helpers_login_same_user_signed_in() { 663 let updateUserAccountDataCalled = false; 664 let setSignedInUserCalled = false; 665 666 let helpers = new FxAccountsWebChannelHelpers({ 667 fxAccounts: { 668 getSignedInUser() { 669 return Promise.resolve({ 670 uid: "testuser", 671 email: "testuser@testuser.com", 672 }); 673 }, 674 _internal: { 675 updateUserAccountData(accountData) { 676 updateUserAccountDataCalled = true; 677 Assert.equal(accountData.email, "testuser@testuser.com"); 678 Assert.equal(accountData.uid, "testuser"); 679 return Promise.resolve(); 680 }, 681 setSignedInUser() { 682 setSignedInUserCalled = true; 683 return Promise.resolve(); 684 }, 685 }, 686 telemetry: { 687 recordConnection: sinon.spy(), 688 }, 689 }, 690 weaveXPCOM: { 691 whenLoaded() {}, 692 Weave: { 693 Service: { 694 configure() {}, 695 }, 696 }, 697 }, 698 }); 699 helpers._disconnect = sinon.spy(); 700 701 await helpers.login({ 702 uid: "testuser", 703 email: "testuser@testuser.com", 704 verifiedCanLinkAccount: true, 705 customizeSync: false, 706 }); 707 708 Assert.ok( 709 updateUserAccountDataCalled, 710 "updateUserAccountData should be called" 711 ); 712 Assert.ok(!setSignedInUserCalled, "setSignedInUser should not be called"); 713 Assert.ok(!helpers._disconnect.called, "_disconnect should not be called"); 714 Assert.ok( 715 helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel") 716 ); 717 }); 718 719 add_task(async function test_helpers_login_with_customize_sync() { 720 let helpers = new FxAccountsWebChannelHelpers({ 721 fxAccounts: { 722 _internal: { 723 setSignedInUser(accountData) { 724 return new Promise(resolve => { 725 // ensure fxAccounts is informed of the new user being signed in. 726 Assert.equal(accountData.email, "testuser@testuser.com"); 727 728 // customizeSync should be stripped in the data. 729 Assert.equal(false, "customizeSync" in accountData); 730 731 resolve(); 732 }); 733 }, 734 }, 735 getSignedInUser() { 736 return Promise.resolve(null); 737 }, 738 telemetry: { 739 recordConnection: sinon.spy(), 740 }, 741 }, 742 weaveXPCOM: { 743 whenLoaded() {}, 744 Weave: { 745 Service: { 746 configure() {}, 747 }, 748 }, 749 }, 750 }); 751 752 await helpers.login({ 753 uid: "testuser", 754 email: "testuser@testuser.com", 755 verifiedCanLinkAccount: true, 756 customizeSync: true, 757 }); 758 Assert.ok( 759 helpers._fxAccounts.telemetry.recordConnection.calledWith([], "webchannel") 760 ); 761 }); 762 763 add_task(async function test_helpers_persist_requested_services() { 764 let accountData = null; 765 const helpers = new FxAccountsWebChannelHelpers({ 766 fxAccounts: { 767 _internal: { 768 async setSignedInUser(newAccountData) { 769 accountData = newAccountData; 770 return accountData; 771 }, 772 async updateUserAccountData(updatedFields) { 773 accountData = { ...accountData, ...updatedFields }; 774 return accountData; 775 }, 776 }, 777 async getSignedInUser() { 778 return accountData; 779 }, 780 telemetry: { 781 recordConnection() {}, 782 }, 783 }, 784 weaveXPCOM: { 785 whenLoaded() {}, 786 Weave: { 787 Service: {}, 788 }, 789 }, 790 }); 791 792 await helpers.login({ 793 uid: "auid", 794 email: "testuser@testuser.com", 795 verifiedCanLinkAccount: true, 796 services: { 797 first_only: { x: 10 }, // this data is not in the update below. 798 sync: { important: true }, 799 }, 800 }); 801 802 Assert.deepEqual(JSON.parse(accountData.requestedServices), { 803 first_only: { x: 10 }, 804 sync: { important: true }, 805 }); 806 // A second "login" message without the services. 807 await helpers.login({ 808 uid: "auid", 809 email: "testuser@testuser.com", 810 verifiedCanLinkAccount: true, 811 services: { 812 // the service is mentioned, but data is empty, so it's the old version of the data we want. 813 sync: {}, 814 // a new service we never saw before, but we still want it. 815 new: { name: "opted in" }, // not in original, but we want in the final. 816 }, 817 }); 818 // the version with the data should remain. 819 Assert.deepEqual(JSON.parse(accountData.requestedServices), { 820 first_only: { x: 10 }, 821 sync: { important: true }, 822 new: { name: "opted in" }, 823 }); 824 }); 825 826 add_task(async function test_helpers_oauth_login_defers_sync_without_keys() { 827 const accountState = { 828 uid: "uid123", 829 sessionToken: "session-token", 830 email: "user@example.com", 831 requestedServices: "", 832 }; 833 const destroyOAuthToken = sinon.stub().resolves(); 834 const completeOAuthFlow = sinon 835 .stub() 836 .resolves({ scopedKeys: null, refreshToken: "refresh-token" }); 837 const setScopedKeys = sinon.spy(); 838 const setUserVerified = sinon.spy(); 839 const updateUserAccountData = sinon.stub().resolves(); 840 841 const helpers = new FxAccountsWebChannelHelpers({ 842 fxAccounts: { 843 _internal: { 844 async getUserAccountData() { 845 return accountState; 846 }, 847 completeOAuthFlow, 848 destroyOAuthToken, 849 setScopedKeys, 850 updateUserAccountData, 851 setUserVerified, 852 }, 853 }, 854 }); 855 856 await helpers.oauthLogin({ code: "code", state: "state" }); 857 858 Assert.ok(setScopedKeys.notCalled); 859 Assert.ok(updateUserAccountData.calledOnce); 860 Assert.deepEqual( 861 JSON.parse(updateUserAccountData.firstCall.args[0].requestedServices), 862 null 863 ); 864 }); 865 866 add_test(function test_helpers_open_sync_preferences() { 867 let helpers = new FxAccountsWebChannelHelpers({ 868 fxAccounts: {}, 869 }); 870 871 let mockBrowser = { 872 loadURI(uri) { 873 Assert.equal( 874 uri.spec, 875 "about:preferences?entrypoint=fxa%3Averification_complete#sync" 876 ); 877 run_next_test(); 878 }, 879 }; 880 881 helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete"); 882 }); 883 884 add_task(async function test_helpers_getFxAStatus_engines_oauth() { 885 let helpers = new FxAccountsWebChannelHelpers({ 886 fxAccounts: { 887 _internal: { 888 getUserAccountData() { 889 return Promise.resolve({ 890 email: "testuser@testuser.com", 891 sessionToken: "sessionToken", 892 uid: "uid", 893 verified: true, 894 }); 895 }, 896 }, 897 }, 898 privateBrowsingUtils: { 899 isBrowserPrivate: () => true, 900 }, 901 }); 902 903 // disable the "addresses" engine. 904 Services.prefs.setBoolPref("services.sync.engine.addresses.available", false); 905 let fxaStatus = await helpers.getFxaStatus("sync", mockSendingContext); 906 ok(!!fxaStatus); 907 ok(!!fxaStatus.signedInUser); 908 // in the oauth flows we expect all engines. 909 deepEqual(fxaStatus.capabilities.engines.toSorted(), [ 910 "addons", 911 "bookmarks", 912 "creditcards", 913 "history", 914 "passwords", 915 "prefs", 916 "tabs", 917 ]); 918 919 // try again with addresses enabled. 920 Services.prefs.setBoolPref("services.sync.engine.addresses.available", true); 921 fxaStatus = await helpers.getFxaStatus("sync", mockSendingContext); 922 deepEqual(fxaStatus.capabilities.engines.toSorted(), [ 923 "addons", 924 "addresses", 925 "bookmarks", 926 "creditcards", 927 "history", 928 "passwords", 929 "prefs", 930 "tabs", 931 ]); 932 }); 933 934 add_task(async function test_helpers_getFxaStatus_allowed_signedInUser() { 935 let wasCalled = { 936 getUserAccountData: false, 937 shouldAllowFxaStatus: false, 938 }; 939 940 let helpers = new FxAccountsWebChannelHelpers({ 941 fxAccounts: { 942 _internal: { 943 getUserAccountData() { 944 wasCalled.getUserAccountData = true; 945 return Promise.resolve({ 946 email: "testuser@testuser.com", 947 sessionToken: "sessionToken", 948 uid: "uid", 949 verified: true, 950 }); 951 }, 952 }, 953 }, 954 }); 955 956 helpers.shouldAllowFxaStatus = (service, sendingContext) => { 957 wasCalled.shouldAllowFxaStatus = true; 958 Assert.equal(service, "sync"); 959 Assert.equal(sendingContext, mockSendingContext); 960 961 return true; 962 }; 963 964 return helpers.getFxaStatus("sync", mockSendingContext).then(fxaStatus => { 965 Assert.ok(!!fxaStatus); 966 Assert.ok(wasCalled.getUserAccountData); 967 Assert.ok(wasCalled.shouldAllowFxaStatus); 968 969 Assert.ok(!!fxaStatus.signedInUser); 970 let { signedInUser } = fxaStatus; 971 972 Assert.equal(signedInUser.email, "testuser@testuser.com"); 973 Assert.equal(signedInUser.sessionToken, "sessionToken"); 974 Assert.equal(signedInUser.uid, "uid"); 975 Assert.ok(signedInUser.verified); 976 977 // These properties are filtered and should not 978 // be returned to the requester. 979 Assert.equal(false, "scopedKeys" in signedInUser); 980 }); 981 }); 982 983 add_task(async function test_helpers_getFxaStatus_allowed_no_signedInUser() { 984 let wasCalled = { 985 getUserAccountData: false, 986 shouldAllowFxaStatus: false, 987 }; 988 989 let helpers = new FxAccountsWebChannelHelpers({ 990 fxAccounts: { 991 _internal: { 992 getUserAccountData() { 993 wasCalled.getUserAccountData = true; 994 return Promise.resolve(null); 995 }, 996 }, 997 }, 998 }); 999 1000 helpers.shouldAllowFxaStatus = (service, sendingContext) => { 1001 wasCalled.shouldAllowFxaStatus = true; 1002 Assert.equal(service, "sync"); 1003 Assert.equal(sendingContext, mockSendingContext); 1004 1005 return true; 1006 }; 1007 1008 return helpers.getFxaStatus("sync", mockSendingContext).then(fxaStatus => { 1009 Assert.ok(!!fxaStatus); 1010 Assert.ok(wasCalled.getUserAccountData); 1011 Assert.ok(wasCalled.shouldAllowFxaStatus); 1012 1013 Assert.equal(null, fxaStatus.signedInUser); 1014 }); 1015 }); 1016 1017 add_task(async function test_helpers_getFxaStatus_not_allowed() { 1018 let wasCalled = { 1019 getUserAccountData: false, 1020 shouldAllowFxaStatus: false, 1021 }; 1022 1023 let helpers = new FxAccountsWebChannelHelpers({ 1024 fxAccounts: { 1025 _internal: { 1026 getUserAccountData() { 1027 wasCalled.getUserAccountData = true; 1028 return Promise.resolve(null); 1029 }, 1030 }, 1031 }, 1032 }); 1033 1034 helpers.shouldAllowFxaStatus = ( 1035 service, 1036 sendingContext, 1037 isPairing, 1038 context 1039 ) => { 1040 wasCalled.shouldAllowFxaStatus = true; 1041 Assert.equal(service, "sync"); 1042 Assert.equal(sendingContext, mockSendingContext); 1043 Assert.ok(!isPairing); 1044 Assert.equal(context, "fx_desktop_v3"); 1045 1046 return false; 1047 }; 1048 1049 return helpers 1050 .getFxaStatus("sync", mockSendingContext, false, "fx_desktop_v3") 1051 .then(fxaStatus => { 1052 Assert.ok(!!fxaStatus); 1053 Assert.ok(!wasCalled.getUserAccountData); 1054 Assert.ok(wasCalled.shouldAllowFxaStatus); 1055 1056 Assert.equal(null, fxaStatus.signedInUser); 1057 }); 1058 }); 1059 1060 add_task( 1061 async function test_helpers_shouldAllowFxaStatus_sync_service_not_private_browsing() { 1062 let wasCalled = { 1063 isPrivateBrowsingMode: false, 1064 }; 1065 let helpers = new FxAccountsWebChannelHelpers({}); 1066 1067 helpers.isPrivateBrowsingMode = sendingContext => { 1068 wasCalled.isPrivateBrowsingMode = true; 1069 Assert.equal(sendingContext, mockSendingContext); 1070 return false; 1071 }; 1072 1073 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1074 "sync", 1075 mockSendingContext, 1076 false 1077 ); 1078 Assert.ok(shouldAllowFxaStatus); 1079 Assert.ok(wasCalled.isPrivateBrowsingMode); 1080 } 1081 ); 1082 1083 add_task( 1084 async function test_helpers_shouldAllowFxaStatus_desktop_context_not_private_browsing() { 1085 let wasCalled = { 1086 isPrivateBrowsingMode: false, 1087 }; 1088 let helpers = new FxAccountsWebChannelHelpers({}); 1089 1090 helpers.isPrivateBrowsingMode = sendingContext => { 1091 wasCalled.isPrivateBrowsingMode = true; 1092 Assert.equal(sendingContext, mockSendingContext); 1093 return false; 1094 }; 1095 1096 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1097 "", 1098 mockSendingContext, 1099 false, 1100 "fx_desktop_v3" 1101 ); 1102 Assert.ok(shouldAllowFxaStatus); 1103 Assert.ok(wasCalled.isPrivateBrowsingMode); 1104 } 1105 ); 1106 1107 add_task( 1108 async function test_helpers_shouldAllowFxaStatus_oauth_service_not_private_browsing() { 1109 let wasCalled = { 1110 isPrivateBrowsingMode: false, 1111 }; 1112 let helpers = new FxAccountsWebChannelHelpers({}); 1113 1114 helpers.isPrivateBrowsingMode = sendingContext => { 1115 wasCalled.isPrivateBrowsingMode = true; 1116 Assert.equal(sendingContext, mockSendingContext); 1117 return false; 1118 }; 1119 1120 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1121 "dcdb5ae7add825d2", 1122 mockSendingContext, 1123 false 1124 ); 1125 Assert.ok(shouldAllowFxaStatus); 1126 Assert.ok(wasCalled.isPrivateBrowsingMode); 1127 } 1128 ); 1129 1130 add_task( 1131 async function test_helpers_shouldAllowFxaStatus_no_service_not_private_browsing() { 1132 let wasCalled = { 1133 isPrivateBrowsingMode: false, 1134 }; 1135 let helpers = new FxAccountsWebChannelHelpers({}); 1136 1137 helpers.isPrivateBrowsingMode = sendingContext => { 1138 wasCalled.isPrivateBrowsingMode = true; 1139 Assert.equal(sendingContext, mockSendingContext); 1140 return false; 1141 }; 1142 1143 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1144 "", 1145 mockSendingContext, 1146 false 1147 ); 1148 Assert.ok(shouldAllowFxaStatus); 1149 Assert.ok(wasCalled.isPrivateBrowsingMode); 1150 } 1151 ); 1152 1153 add_task( 1154 async function test_helpers_shouldAllowFxaStatus_sync_service_private_browsing() { 1155 let wasCalled = { 1156 isPrivateBrowsingMode: false, 1157 }; 1158 let helpers = new FxAccountsWebChannelHelpers({}); 1159 1160 helpers.isPrivateBrowsingMode = sendingContext => { 1161 wasCalled.isPrivateBrowsingMode = true; 1162 Assert.equal(sendingContext, mockSendingContext); 1163 return true; 1164 }; 1165 1166 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1167 "sync", 1168 mockSendingContext, 1169 false 1170 ); 1171 Assert.ok(shouldAllowFxaStatus); 1172 Assert.ok(wasCalled.isPrivateBrowsingMode); 1173 } 1174 ); 1175 1176 add_task( 1177 async function test_helpers_shouldAllowFxaStatus_oauth_service_private_browsing() { 1178 let wasCalled = { 1179 isPrivateBrowsingMode: false, 1180 }; 1181 let helpers = new FxAccountsWebChannelHelpers({}); 1182 1183 helpers.isPrivateBrowsingMode = sendingContext => { 1184 wasCalled.isPrivateBrowsingMode = true; 1185 Assert.equal(sendingContext, mockSendingContext); 1186 return true; 1187 }; 1188 1189 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1190 "dcdb5ae7add825d2", 1191 mockSendingContext, 1192 false 1193 ); 1194 Assert.ok(!shouldAllowFxaStatus); 1195 Assert.ok(wasCalled.isPrivateBrowsingMode); 1196 } 1197 ); 1198 1199 add_task( 1200 async function test_helpers_shouldAllowFxaStatus_oauth_service_pairing_private_browsing() { 1201 let wasCalled = { 1202 isPrivateBrowsingMode: false, 1203 }; 1204 let helpers = new FxAccountsWebChannelHelpers({}); 1205 1206 helpers.isPrivateBrowsingMode = sendingContext => { 1207 wasCalled.isPrivateBrowsingMode = true; 1208 Assert.equal(sendingContext, mockSendingContext); 1209 return true; 1210 }; 1211 1212 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1213 "dcdb5ae7add825d2", 1214 mockSendingContext, 1215 true 1216 ); 1217 Assert.ok(shouldAllowFxaStatus); 1218 Assert.ok(wasCalled.isPrivateBrowsingMode); 1219 } 1220 ); 1221 1222 add_task( 1223 async function test_helpers_shouldAllowFxaStatus_no_service_private_browsing() { 1224 let wasCalled = { 1225 isPrivateBrowsingMode: false, 1226 }; 1227 let helpers = new FxAccountsWebChannelHelpers({}); 1228 1229 helpers.isPrivateBrowsingMode = sendingContext => { 1230 wasCalled.isPrivateBrowsingMode = true; 1231 Assert.equal(sendingContext, mockSendingContext); 1232 return true; 1233 }; 1234 1235 let shouldAllowFxaStatus = helpers.shouldAllowFxaStatus( 1236 "", 1237 mockSendingContext, 1238 false 1239 ); 1240 Assert.ok(!shouldAllowFxaStatus); 1241 Assert.ok(wasCalled.isPrivateBrowsingMode); 1242 } 1243 ); 1244 1245 add_task(async function test_helpers_isPrivateBrowsingMode_private_browsing() { 1246 let wasCalled = { 1247 isBrowserPrivate: false, 1248 }; 1249 let helpers = new FxAccountsWebChannelHelpers({ 1250 privateBrowsingUtils: { 1251 isBrowserPrivate(browser) { 1252 wasCalled.isBrowserPrivate = true; 1253 Assert.equal( 1254 browser, 1255 mockSendingContext.browsingContext.top.embedderElement 1256 ); 1257 return true; 1258 }, 1259 }, 1260 }); 1261 1262 let isPrivateBrowsingMode = helpers.isPrivateBrowsingMode(mockSendingContext); 1263 Assert.ok(isPrivateBrowsingMode); 1264 Assert.ok(wasCalled.isBrowserPrivate); 1265 }); 1266 1267 add_task(async function test_helpers_isPrivateBrowsingMode_private_browsing() { 1268 let wasCalled = { 1269 isBrowserPrivate: false, 1270 }; 1271 let helpers = new FxAccountsWebChannelHelpers({ 1272 privateBrowsingUtils: { 1273 isBrowserPrivate(browser) { 1274 wasCalled.isBrowserPrivate = true; 1275 Assert.equal( 1276 browser, 1277 mockSendingContext.browsingContext.top.embedderElement 1278 ); 1279 return false; 1280 }, 1281 }, 1282 }); 1283 1284 let isPrivateBrowsingMode = helpers.isPrivateBrowsingMode(mockSendingContext); 1285 Assert.ok(!isPrivateBrowsingMode); 1286 Assert.ok(wasCalled.isBrowserPrivate); 1287 }); 1288 1289 add_task(async function test_helpers_change_password() { 1290 let wasCalled = { 1291 updateUserAccountData: false, 1292 updateDeviceRegistration: false, 1293 }; 1294 let helpers = new FxAccountsWebChannelHelpers({ 1295 fxAccounts: { 1296 _internal: { 1297 updateUserAccountData(credentials) { 1298 return new Promise(resolve => { 1299 Assert.ok(credentials.hasOwnProperty("email")); 1300 Assert.ok(credentials.hasOwnProperty("uid")); 1301 Assert.ok(credentials.hasOwnProperty("unwrapBKey")); 1302 Assert.ok(credentials.hasOwnProperty("device")); 1303 Assert.equal(null, credentials.device); 1304 Assert.equal(null, credentials.encryptedSendTabKeys); 1305 // "foo" isn't a field known by storage, so should be dropped. 1306 Assert.ok(!credentials.hasOwnProperty("foo")); 1307 wasCalled.updateUserAccountData = true; 1308 1309 resolve(); 1310 }); 1311 }, 1312 1313 updateDeviceRegistration() { 1314 Assert.equal(arguments.length, 0); 1315 wasCalled.updateDeviceRegistration = true; 1316 return Promise.resolve(); 1317 }, 1318 }, 1319 }, 1320 }); 1321 await helpers.changePassword({ 1322 email: "email", 1323 uid: "uid", 1324 unwrapBKey: "unwrapBKey", 1325 foo: "foo", 1326 }); 1327 Assert.ok(wasCalled.updateUserAccountData); 1328 Assert.ok(wasCalled.updateDeviceRegistration); 1329 }); 1330 1331 add_task(async function test_helpers_change_password_with_error() { 1332 let wasCalled = { 1333 updateUserAccountData: false, 1334 updateDeviceRegistration: false, 1335 }; 1336 let helpers = new FxAccountsWebChannelHelpers({ 1337 fxAccounts: { 1338 _internal: { 1339 updateUserAccountData() { 1340 wasCalled.updateUserAccountData = true; 1341 return Promise.reject(); 1342 }, 1343 1344 updateDeviceRegistration() { 1345 wasCalled.updateDeviceRegistration = true; 1346 return Promise.resolve(); 1347 }, 1348 }, 1349 }, 1350 }); 1351 try { 1352 await helpers.changePassword({}); 1353 Assert.equal(false, "changePassword should have rejected"); 1354 } catch (_) { 1355 Assert.ok(wasCalled.updateUserAccountData); 1356 Assert.ok(!wasCalled.updateDeviceRegistration); 1357 } 1358 }); 1359 1360 function makeObserver(aObserveTopic, aObserveFunc) { 1361 let callback = function (aSubject, aTopic, aData) { 1362 log.debug("observed " + aTopic + " " + aData); 1363 if (aTopic == aObserveTopic) { 1364 removeMe(); 1365 aObserveFunc(aSubject, aTopic, aData); 1366 } 1367 }; 1368 1369 function removeMe() { 1370 log.debug("removing observer for " + aObserveTopic); 1371 Services.obs.removeObserver(callback, aObserveTopic); 1372 } 1373 1374 Services.obs.addObserver(callback, aObserveTopic); 1375 return removeMe; 1376 } 1377 1378 function validationHelper(params, expected) { 1379 try { 1380 new FxAccountsWebChannel(params); 1381 } catch (e) { 1382 if (typeof expected === "string") { 1383 return Assert.equal(e.toString(), expected); 1384 } 1385 return Assert.ok(e.toString().match(expected)); 1386 } 1387 throw new Error("Validation helper error"); 1388 }