test_accounts.js (41361B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { FxAccounts, ERROR_INVALID_ACCOUNT_STATE } = ChromeUtils.importESModule( 7 "resource://gre/modules/FxAccounts.sys.mjs" 8 ); 9 const { FxAccountsClient } = ChromeUtils.importESModule( 10 "resource://gre/modules/FxAccountsClient.sys.mjs" 11 ); 12 const { 13 CLIENT_IS_THUNDERBIRD, 14 ERRNO_INVALID_AUTH_TOKEN, 15 ERROR_NO_ACCOUNT, 16 OAUTH_CLIENT_ID, 17 ONLOGOUT_NOTIFICATION, 18 DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY, 19 } = ChromeUtils.importESModule( 20 "resource://gre/modules/FxAccountsCommon.sys.mjs" 21 ); 22 23 // We grab some additional stuff via the system global. 24 var { AccountState } = ChromeUtils.importESModule( 25 "resource://gre/modules/FxAccounts.sys.mjs" 26 ); 27 28 const MOCK_TOKEN_RESPONSE = { 29 access_token: 30 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69", 31 token_type: "bearer", 32 scope: SCOPE_APP_SYNC, 33 expires_in: 21600, 34 auth_at: 1589579900, 35 }; 36 37 initTestLogging("Trace"); 38 39 var log = Log.repository.getLogger("Services.FxAccounts.test"); 40 log.level = Log.Level.Debug; 41 42 // See verbose logging from FxAccounts.sys.mjs and jwcrypto.sys.mjs. 43 Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace"); 44 Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace; 45 Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug"); 46 47 /* 48 * The FxAccountsClient communicates with the remote Firefox 49 * Accounts auth server. Mock the server calls, with a little 50 * lag time to simulate some latency. 51 * 52 * We add the _verified attribute to mock the change in verification 53 * state on the FXA server. 54 */ 55 56 function MockStorageManager() {} 57 58 MockStorageManager.prototype = { 59 promiseInitialized: Promise.resolve(), 60 61 initialize(accountData) { 62 this.accountData = accountData; 63 }, 64 65 finalize() { 66 return Promise.resolve(); 67 }, 68 69 getAccountData(fields = null) { 70 let result; 71 if (!this.accountData) { 72 result = null; 73 } else if (fields == null) { 74 // can't use cloneInto as the keys get upset... 75 result = {}; 76 for (let field of Object.keys(this.accountData)) { 77 result[field] = this.accountData[field]; 78 } 79 } else { 80 if (!Array.isArray(fields)) { 81 fields = [fields]; 82 } 83 result = {}; 84 for (let field of fields) { 85 result[field] = this.accountData[field]; 86 } 87 } 88 return Promise.resolve(result); 89 }, 90 91 updateAccountData(updatedFields) { 92 if (!this.accountData) { 93 return Promise.resolve(); 94 } 95 for (let [name, value] of Object.entries(updatedFields)) { 96 if (value == null) { 97 delete this.accountData[name]; 98 } else { 99 this.accountData[name] = value; 100 } 101 } 102 return Promise.resolve(); 103 }, 104 105 deleteAccountData() { 106 this.accountData = null; 107 return Promise.resolve(); 108 }, 109 }; 110 111 function MockFxAccountsClient() { 112 this._email = "nobody@example.com"; 113 this._verified = false; 114 this._deletedOnServer = false; // for our accountStatus mock 115 116 // mock calls up to the auth server to determine whether the 117 // user account has been verified 118 this.recoveryEmailStatus = async function () { 119 // simulate a call to /recovery_email/status 120 return { 121 email: this._email, 122 verified: this._verified, 123 }; 124 }; 125 126 this.accountStatus = async function (uid) { 127 return !!uid && !this._deletedOnServer; 128 }; 129 130 this.sessionStatus = async function () { 131 // If the sessionStatus check says an account is OK, we typically will not 132 // end up calling accountStatus - so this must return false if accountStatus 133 // would. 134 return !this._deletedOnServer; 135 }; 136 137 this.accountKeys = function () { 138 return new Promise(resolve => { 139 do_timeout(50, () => { 140 resolve({ 141 kA: expandBytes("11"), 142 wrapKB: expandBytes("22"), 143 }); 144 }); 145 }); 146 }; 147 148 this.getScopedKeyData = function (sessionToken, client_id, scopes) { 149 Assert.ok(sessionToken); 150 Assert.equal(client_id, OAUTH_CLIENT_ID); 151 Assert.equal(scopes, SCOPE_APP_SYNC); 152 return new Promise(resolve => { 153 do_timeout(50, () => { 154 resolve({ 155 [SCOPE_APP_SYNC]: { 156 identifier: SCOPE_APP_SYNC, 157 keyRotationSecret: 158 "0000000000000000000000000000000000000000000000000000000000000000", 159 keyRotationTimestamp: 1234567890123, 160 }, 161 }); 162 }); 163 }); 164 }; 165 166 this.resendVerificationEmail = function (sessionToken) { 167 // Return the session token to show that we received it in the first place 168 return Promise.resolve(sessionToken); 169 }; 170 171 this.signOut = () => Promise.resolve(); 172 173 FxAccountsClient.apply(this); 174 } 175 MockFxAccountsClient.prototype = {}; 176 Object.setPrototypeOf( 177 MockFxAccountsClient.prototype, 178 FxAccountsClient.prototype 179 ); 180 /* 181 * We need to mock the FxAccounts module's interfaces to external 182 * services, such as storage and the FxAccounts client. We also 183 * mock the now() method, so that we can simulate the passing of 184 * time and verify that signatures expire correctly. 185 */ 186 function MockFxAccounts() { 187 let result = new FxAccounts({ 188 VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms 189 190 _getCertificateSigned_calls: [], 191 _d_signCertificate: Promise.withResolvers(), 192 _now_is: new Date(), 193 now() { 194 return this._now_is; 195 }, 196 newAccountState(newCredentials) { 197 // we use a real accountState but mocked storage. 198 let storage = new MockStorageManager(); 199 storage.initialize(newCredentials); 200 return new AccountState(storage); 201 }, 202 fxAccountsClient: new MockFxAccountsClient(), 203 observerPreloads: [], 204 device: { 205 _registerOrUpdateDevice() {}, 206 _checkRemoteCommandsUpdateNeeded: async () => false, 207 }, 208 profile: { 209 getProfile() { 210 return null; 211 }, 212 }, 213 }); 214 // and for convenience so we don't have to touch as many lines in this test 215 // when we refactored FxAccounts.sys.mjs :) 216 result.setSignedInUser = function (creds) { 217 return result._internal.setSignedInUser(creds); 218 }; 219 return result; 220 } 221 222 /* 223 * Some tests want a "real" fxa instance - however, we still mock the storage 224 * to keep the tests fast on b2g. 225 */ 226 async function MakeFxAccounts({ internal = {}, credentials } = {}) { 227 if (!internal.newAccountState) { 228 // we use a real accountState but mocked storage. 229 internal.newAccountState = function (newCredentials) { 230 let storage = new MockStorageManager(); 231 storage.initialize(newCredentials); 232 return new AccountState(storage); 233 }; 234 } 235 if (!internal._signOutServer) { 236 internal._signOutServer = () => Promise.resolve(); 237 } 238 if (internal.device) { 239 if (!internal.device._registerOrUpdateDevice) { 240 internal.device._registerOrUpdateDevice = () => Promise.resolve(); 241 internal.device._checkRemoteCommandsUpdateNeeded = async () => false; 242 } 243 } else { 244 internal.device = { 245 _registerOrUpdateDevice() {}, 246 _checkRemoteCommandsUpdateNeeded: async () => false, 247 }; 248 } 249 if (!internal.observerPreloads) { 250 internal.observerPreloads = []; 251 } 252 let result = new FxAccounts(internal); 253 254 if (credentials) { 255 await result._internal.setSignedInUser(credentials); 256 } 257 return result; 258 } 259 260 add_task(async function test_get_signed_in_user_initially_unset() { 261 _("Check getSignedInUser initially and after signout reports no user"); 262 let account = await MakeFxAccounts(); 263 let credentials = { 264 email: "foo@example.com", 265 uid: "1234567890abcdef1234567890abcdef", 266 sessionToken: "dead", 267 verified: true, 268 ...MOCK_ACCOUNT_KEYS, 269 }; 270 let result = await account.getSignedInUser(); 271 Assert.equal(result, null); 272 273 await account._internal.setSignedInUser(credentials); 274 275 // getSignedInUser only returns a subset. 276 result = await account.getSignedInUser(); 277 Assert.deepEqual(result.email, credentials.email); 278 Assert.deepEqual(result.scopedKeys, undefined); 279 280 // for the sake of testing, use the low-level function to check it's all there 281 result = await account._internal.currentAccountState.getUserAccountData(); 282 Assert.deepEqual(result.email, credentials.email); 283 Assert.deepEqual(result.scopedKeys, credentials.scopedKeys); 284 285 // sign out 286 let localOnly = true; 287 await account.signOut(localOnly); 288 289 // user should be undefined after sign out 290 result = await account.getSignedInUser(); 291 Assert.equal(result, null); 292 }); 293 294 add_task(async function test_set_signed_in_user_signs_out_previous_account() { 295 _("Check setSignedInUser signs out the previous account."); 296 let signOutServerCalled = false; 297 let credentials = { 298 email: "foo@example.com", 299 uid: "1234567890abcdef1234567890abcdef", 300 sessionToken: "dead", 301 verified: true, 302 ...MOCK_ACCOUNT_KEYS, 303 }; 304 let account = await MakeFxAccounts({ credentials }); 305 306 account._internal._signOutServer = () => { 307 signOutServerCalled = true; 308 return Promise.resolve(true); 309 }; 310 311 await account._internal.setSignedInUser(credentials); 312 Assert.ok(signOutServerCalled); 313 }); 314 315 add_task(async function test_update_account_data() { 316 _("Check updateUserAccountData does the right thing."); 317 let credentials = { 318 email: "foo@example.com", 319 uid: "1234567890abcdef1234567890abcdef", 320 sessionToken: "dead", 321 verified: true, 322 ...MOCK_ACCOUNT_KEYS, 323 }; 324 let account = await MakeFxAccounts({ credentials }); 325 326 let newCreds = { 327 email: credentials.email, 328 uid: credentials.uid, 329 sessionToken: "alive", 330 }; 331 await account._internal.updateUserAccountData(newCreds); 332 Assert.equal( 333 (await account._internal.getUserAccountData()).sessionToken, 334 "alive", 335 "new field value was saved" 336 ); 337 338 // but we should fail attempting to change the uid. 339 newCreds = { 340 email: credentials.email, 341 uid: "11111111111111111111222222222222", 342 sessionToken: "alive", 343 }; 344 await Assert.rejects( 345 account._internal.updateUserAccountData(newCreds), 346 /The specified credentials aren't for the current user/ 347 ); 348 349 // should fail without the uid. 350 newCreds = { 351 sessionToken: "alive", 352 }; 353 await Assert.rejects( 354 account._internal.updateUserAccountData(newCreds), 355 /The specified credentials have no uid/ 356 ); 357 358 // and should fail with a field name that's not known by storage. 359 newCreds = { 360 email: credentials.email, 361 uid: "11111111111111111111222222222222", 362 foo: "bar", 363 }; 364 await Assert.rejects( 365 account._internal.updateUserAccountData(newCreds), 366 /The specified credentials aren't for the current user/ 367 ); 368 }); 369 370 // Sanity-check that our mocked client is working correctly 371 add_test(function test_client_mock() { 372 let fxa = new MockFxAccounts(); 373 let client = fxa._internal.fxAccountsClient; 374 Assert.equal(client._verified, false); 375 Assert.equal(typeof client.signIn, "function"); 376 377 // The recoveryEmailStatus function eventually fulfills its promise 378 client.recoveryEmailStatus().then(response => { 379 Assert.equal(response.verified, false); 380 run_next_test(); 381 }); 382 }); 383 384 add_test(function test_getKeyForScope() { 385 let fxa = new MockFxAccounts(); 386 let user = getTestUser("eusebius"); 387 388 // Once email has been verified, we will be able to get keys 389 user.verified = true; 390 391 fxa.setSignedInUser(user).then(() => { 392 fxa._internal.getUserAccountData().then(user2 => { 393 // Before getKeyForScope, we have no keys 394 Assert.equal(!!user2.scopedKeys, false); 395 // And we still have a key-fetch token and unwrapBKey to use 396 Assert.equal(!!user2.keyFetchToken, true); 397 Assert.equal(!!user2.unwrapBKey, true); 398 399 fxa.keys.getKeyForScope(SCOPE_APP_SYNC).then(() => { 400 fxa._internal.getUserAccountData().then(user3 => { 401 // Now we should have keys 402 Assert.equal(!!user3.verified, true); 403 Assert.notEqual(null, user3.scopedKeys); 404 Assert.equal(user3.keyFetchToken, undefined); 405 Assert.equal(user3.unwrapBKey, undefined); 406 run_next_test(); 407 }); 408 }); 409 }); 410 }); 411 }); 412 413 add_task(async function test_oauth_verification() { 414 let fxa = new MockFxAccounts(); 415 let user = getTestUser("eusebius"); 416 user.verified = false; 417 418 await fxa.setSignedInUser(user); 419 let fetched = await fxa.getSignedInUser(); 420 Assert.ok(!fetched.verified); 421 422 fxa._withCurrentAccountState(state => { 423 state.updateUserAccountData({ scopedKeys: { test: { foo: "bar" } } }); 424 }); 425 426 fetched = await fxa.getSignedInUser(); 427 Assert.ok(!fetched.verified); 428 429 // Simulate the follow-up login message that marks the account verified. 430 await fxa._internal.updateUserAccountData({ 431 uid: user.uid, 432 verified: true, 433 }); 434 435 fetched = await fxa.getSignedInUser(); 436 Assert.ok(fetched.verified); 437 }); 438 439 // Tests for hasKeysForScope - checking if sync keys exist locally 440 add_task(async function test_hasKeysForScope_not_signed_in() { 441 const fxa = await MakeFxAccounts(); 442 // Should return false when no user is signed in 443 Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC))); 444 }); 445 446 add_task(async function test_hasKeysForScope_not_verified() { 447 const credentials = { 448 email: "foo@example.com", 449 uid: "1234567890abcdef1234567890abcdef", 450 sessionToken: "dead", 451 verified: false, // Not verified 452 ...MOCK_ACCOUNT_KEYS, 453 }; 454 const fxa = await MakeFxAccounts({ credentials }); 455 // Should return true even keys exist locally 456 // (Keys are provided during OAuth before the account is marked verified) 457 Assert.ok(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)); 458 }); 459 460 add_task(async function test_hasKeysForScope_no_keys() { 461 const credentials = { 462 email: "foo@example.com", 463 uid: "1234567890abcdef1234567890abcdef", 464 sessionToken: "dead", 465 verified: true, 466 // NO scopedKeys - third party auth scenario 467 }; 468 const fxa = await MakeFxAccounts({ credentials }); 469 // Should return false when user has no sync keys (third-party auth) 470 Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC))); 471 }); 472 473 add_task(async function test_hasKeysForScope_with_keys() { 474 const credentials = { 475 email: "foo@example.com", 476 uid: "1234567890abcdef1234567890abcdef", 477 sessionToken: "dead", 478 verified: true, 479 ...MOCK_ACCOUNT_KEYS, // Has sync keys 480 }; 481 const fxa = await MakeFxAccounts({ credentials }); 482 // Should return true when user has sync keys 483 Assert.ok(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)); 484 }); 485 486 add_task(async function test_hasKeysForScope_wrong_scope() { 487 const credentials = { 488 email: "foo@example.com", 489 uid: "1234567890abcdef1234567890abcdef", 490 sessionToken: "dead", 491 verified: true, 492 ...MOCK_ACCOUNT_KEYS, 493 }; 494 const fxa = await MakeFxAccounts({ credentials }); 495 // Should return false for a scope we don't have keys for 496 Assert.ok( 497 !(await fxa.keys.hasKeysForScope( 498 "https://identity.mozilla.com/apps/unknown" 499 )) 500 ); 501 }); 502 503 add_task( 504 async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_high_level_keys() { 505 let fxa = new MockFxAccounts(); 506 let user = getTestUser("eusebius"); 507 508 user.verified = true; 509 510 // An account state with the deprecated kinto extension sync keys... 511 user.kExtSync = 512 "f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" + 513 "5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395"; 514 user.kExtKbHash = 515 "6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273"; 516 user.scopedKeys = { 517 ...MOCK_ACCOUNT_KEYS.scopedKeys, 518 }; 519 520 await fxa.setSignedInUser(user); 521 // getKeyForScope will run the migration 522 await fxa.keys.getKeyForScope(SCOPE_APP_SYNC); 523 let newUser = await fxa._internal.getUserAccountData(); 524 // Then, the deprecated keys will be removed 525 Assert.strictEqual(newUser.kExtSync, undefined); 526 Assert.strictEqual(newUser.kExtKbHash, undefined); 527 } 528 ); 529 530 add_task( 531 async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_scoped_keys() { 532 let fxa = new MockFxAccounts(); 533 let user = getTestUser("eusebius"); 534 const DEPRECATED_SCOPE_WEBEXT_SYNC = "sync:addon_storage"; 535 const EXTRA_SCOPE = "an unknown, but non-deprecated scope"; 536 user.verified = true; 537 user.ecosystemUserId = "ecoUserId"; 538 user.ecosystemAnonId = "ecoAnonId"; 539 user.scopedKeys = { 540 ...MOCK_ACCOUNT_KEYS.scopedKeys, 541 [DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY]: 542 MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC], 543 [DEPRECATED_SCOPE_WEBEXT_SYNC]: 544 MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC], 545 [EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC], 546 }; 547 548 await fxa.setSignedInUser(user); 549 await fxa.keys.getKeyForScope(SCOPE_APP_SYNC); 550 let newUser = await fxa._internal.getUserAccountData(); 551 // It should have removed the deprecated ecosystem_telemetry key, 552 // and the old kinto extension sync key 553 // but left the other keys intact. 554 const expectedScopedKeys = { 555 ...MOCK_ACCOUNT_KEYS.scopedKeys, 556 [EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC], 557 }; 558 Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys); 559 Assert.equal(newUser.ecosystemUserId, null); 560 Assert.equal(newUser.ecosystemAnonId, null); 561 } 562 ); 563 564 // Test vectors from 565 // https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#Test_Vectors 566 add_task(async function test_getKeyForScope_oldsync() { 567 let fxa = new MockFxAccounts(); 568 let client = fxa._internal.fxAccountsClient; 569 client.getScopedKeyData = () => 570 Promise.resolve({ 571 [SCOPE_APP_SYNC]: { 572 identifier: SCOPE_APP_SYNC, 573 keyRotationSecret: 574 "0000000000000000000000000000000000000000000000000000000000000000", 575 keyRotationTimestamp: 1510726317123, 576 }, 577 }); 578 579 // We mock the server returning the wrapKB from our test vectors 580 client.accountKeys = async () => { 581 return { 582 wrapKB: CommonUtils.hexToBytes( 583 "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f" 584 ), 585 }; 586 }; 587 588 // We set the user to have the keyFetchToken and unwrapBKey from our test vectors 589 let user = { 590 ...getTestUser("eusebius"), 591 uid: "aeaa1725c7a24ff983c6295725d5fc9b", 592 keyFetchToken: 593 "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 594 unwrapBKey: 595 "6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a", 596 sessionToken: "mock session token, used in metadata request", 597 verified: true, 598 }; 599 await fxa.setSignedInUser(user); 600 601 // We derive, persist and return the sync key 602 const key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC); 603 604 // We verify the key returned matches what we would expect from the test vectors 605 // kb = 2ee722fdd8ccaa721bdeb2d1b76560efef705b04349d9357c3e592cf4906e075 (from test vectors) 606 // 607 // kid can be verified by "${keyRotationTimestamp}-${sha256(kb)[0:16]}" 608 // 609 // k can be verified by HKDF(kb, undefined, "identity.mozilla.com/picl/v1/oldsync", 64) 610 Assert.deepEqual(key, { 611 scope: SCOPE_APP_SYNC, 612 kid: "1510726317123-BAik7hEOdpGnPZnPBSdaTg", 613 k: "fwM5VZu0Spf5XcFRZYX2zk6MrqZP7zvovCBcvuKwgYMif3hz98FHmIVa3qjKjrW0J244Zj-P5oWaOcQbvypmpw", 614 kty: "oct", 615 }); 616 }); 617 618 add_task(async function test_getScopedKeys_cached_key() { 619 let fxa = new MockFxAccounts(); 620 let user = { 621 ...getTestUser("eusebius"), 622 uid: "aeaa1725c7a24ff983c6295725d5fc9b", 623 verified: true, 624 ...MOCK_ACCOUNT_KEYS, 625 }; 626 627 await fxa.setSignedInUser(user); 628 let key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC); 629 Assert.deepEqual(key, { 630 scope: SCOPE_APP_SYNC, 631 ...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC], 632 }); 633 }); 634 635 add_task(async function test_getScopedKeys_unavailable_scope() { 636 let fxa = new MockFxAccounts(); 637 let user = { 638 ...getTestUser("eusebius"), 639 uid: "aeaa1725c7a24ff983c6295725d5fc9b", 640 verified: true, 641 ...MOCK_ACCOUNT_KEYS, 642 }; 643 await fxa.setSignedInUser(user); 644 await Assert.rejects( 645 fxa.keys.getKeyForScope("otherkeybearingscope"), 646 /Key not available for scope/ 647 ); 648 }); 649 650 add_task(async function test_getScopedKeys_misconfigured_fxa_server() { 651 let fxa = new MockFxAccounts(); 652 let client = fxa._internal.fxAccountsClient; 653 client.getScopedKeyData = () => 654 Promise.resolve({ 655 wrongscope: { 656 identifier: "wrongscope", 657 keyRotationSecret: 658 "0000000000000000000000000000000000000000000000000000000000000000", 659 keyRotationTimestamp: 1510726331712, 660 }, 661 }); 662 let user = { 663 ...getTestUser("eusebius"), 664 uid: "aeaa1725c7a24ff983c6295725d5fc9b", 665 verified: true, 666 keyFetchToken: 667 "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 668 unwrapBKey: 669 "6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a", 670 sessionToken: "mock session token, used in metadata request", 671 }; 672 await fxa.setSignedInUser(user); 673 await Assert.rejects( 674 fxa.keys.getKeyForScope(SCOPE_APP_SYNC), 675 /The FxA server did not grant Firefox the sync scope/ 676 ); 677 }); 678 679 add_task(async function test_setScopedKeys() { 680 const fxa = new MockFxAccounts(); 681 const user = { 682 ...getTestUser("foo"), 683 verified: true, 684 }; 685 await fxa.setSignedInUser(user); 686 await fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys); 687 const key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC); 688 Assert.deepEqual(key, { 689 scope: SCOPE_APP_SYNC, 690 ...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC], 691 }); 692 }); 693 694 add_task(async function test_setScopedKeys_user_not_signed_in() { 695 const fxa = new MockFxAccounts(); 696 await Assert.rejects( 697 fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys), 698 /Cannot persist keys, no user signed in/ 699 ); 700 }); 701 702 // _fetchAndUnwrapAndDeriveKeys with no keyFetchToken should trigger signOut 703 // XXX - actually, it probably shouldn't - bug 1572313. 704 add_test(function test_fetchAndUnwrapAndDeriveKeys_no_token() { 705 let fxa = new MockFxAccounts(); 706 let user = getTestUser("lettuce.protheroe"); 707 delete user.keyFetchToken; 708 709 makeObserver(ONLOGOUT_NOTIFICATION, function () { 710 log.debug("test_fetchAndUnwrapKeys_no_token observed logout"); 711 fxa._internal.getUserAccountData().then(() => { 712 fxa._internal.abortExistingFlow().then(run_next_test); 713 }); 714 }); 715 716 fxa 717 .setSignedInUser(user) 718 .then(() => { 719 return fxa.keys._fetchAndUnwrapAndDeriveKeys(); 720 }) 721 .catch(() => { 722 log.info("setSignedInUser correctly rejected"); 723 }); 724 }); 725 726 add_task(async function test_resend_email_not_signed_in() { 727 let fxa = new MockFxAccounts(); 728 729 try { 730 await fxa.resendVerificationEmail(); 731 } catch (err) { 732 Assert.equal(err.message, ERROR_NO_ACCOUNT); 733 return; 734 } 735 do_throw("Should not be able to resend email when nobody is signed in"); 736 }); 737 738 add_task(async function test_accountStatus() { 739 let fxa = new MockFxAccounts(); 740 let alice = getTestUser("alice"); 741 742 // If we have no user, we have no account server-side 743 let result = await fxa.checkAccountStatus(); 744 Assert.ok(!result); 745 // Set a user - the fxAccountsClient mock will say "ok". 746 await fxa.setSignedInUser(alice); 747 result = await fxa.checkAccountStatus(); 748 Assert.ok(result); 749 // flag the item as deleted on the server. 750 fxa._internal.fxAccountsClient._deletedOnServer = true; 751 result = await fxa.checkAccountStatus(); 752 Assert.ok(!result); 753 fxa._internal.fxAccountsClient._deletedOnServer = false; 754 await fxa.signOut(); 755 }); 756 757 add_task(async function test_resend_email_invalid_token() { 758 let fxa = new MockFxAccounts(); 759 let sophia = getTestUser("sophia"); 760 Assert.notEqual(sophia.sessionToken, null); 761 762 let client = fxa._internal.fxAccountsClient; 763 client.resendVerificationEmail = () => { 764 return Promise.reject({ 765 code: 401, 766 errno: ERRNO_INVALID_AUTH_TOKEN, 767 }); 768 }; 769 // This test wants the account to exist but the local session invalid. 770 client.accountStatus = uid => { 771 Assert.ok(uid, "got a uid to check"); 772 return Promise.resolve(true); 773 }; 774 client.sessionStatus = token => { 775 Assert.ok(token, "got a token to check"); 776 return Promise.resolve(false); 777 }; 778 779 await fxa.setSignedInUser(sophia); 780 let user = await fxa._internal.getUserAccountData(); 781 Assert.equal(user.email, sophia.email); 782 Assert.equal(user.verified, false); 783 log.debug("Sophia wants verification email resent"); 784 785 try { 786 await fxa.resendVerificationEmail(); 787 Assert.ok( 788 false, 789 "resendVerificationEmail should reject invalid session token" 790 ); 791 } catch (err) { 792 Assert.equal(err.code, 401); 793 Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN); 794 } 795 796 user = await fxa._internal.getUserAccountData(); 797 Assert.equal(user.email, sophia.email); 798 Assert.equal(user.sessionToken, null); 799 await fxa._internal.abortExistingFlow(); 800 }); 801 802 add_test(function test_resend_email() { 803 let fxa = new MockFxAccounts(); 804 let alice = getTestUser("alice"); 805 806 // Alice is the user signing in; her email is unverified. 807 fxa.setSignedInUser(alice).then(() => { 808 log.debug("Alice signing in"); 809 810 fxa._internal.getUserAccountData().then(user => { 811 Assert.equal(user.email, alice.email); 812 Assert.equal(user.verified, false); 813 log.debug("Alice wants verification email resent"); 814 815 fxa.resendVerificationEmail().then(result => { 816 // Mock server response; ensures that the session token actually was 817 // passed to the client to make the hawk call 818 Assert.equal(result, "alice's session token"); 819 fxa._internal.abortExistingFlow(); 820 run_next_test(); 821 }); 822 }); 823 }); 824 }); 825 826 Services.prefs.setStringPref( 827 "identity.fxaccounts.remote.oauth.uri", 828 "https://example.com/v1" 829 ); 830 831 add_test(async function test_getOAuthTokenWithSessionToken() { 832 Services.prefs.setBoolPref( 833 "identity.fxaccounts.useSessionTokensForOAuth", 834 true 835 ); 836 let fxa = new MockFxAccounts(); 837 let alice = getTestUser("alice"); 838 alice.verified = true; 839 let oauthTokenCalled = false; 840 841 let client = fxa._internal.fxAccountsClient; 842 client.accessTokenWithSessionToken = async ( 843 sessionTokenHex, 844 clientId, 845 scope, 846 ttl 847 ) => { 848 oauthTokenCalled = true; 849 Assert.equal(sessionTokenHex, "alice's session token"); 850 Assert.equal( 851 clientId, 852 CLIENT_IS_THUNDERBIRD ? "8269bacd7bbc7f80" : "5882386c6d801776" 853 ); 854 Assert.equal(scope, "profile"); 855 Assert.equal(ttl, undefined); 856 return MOCK_TOKEN_RESPONSE; 857 }; 858 859 await fxa.setSignedInUser(alice); 860 const result = await fxa.getOAuthToken({ scope: "profile" }); 861 Assert.ok(oauthTokenCalled); 862 Assert.equal(result, MOCK_TOKEN_RESPONSE.access_token); 863 Services.prefs.setBoolPref( 864 "identity.fxaccounts.useSessionTokensForOAuth", 865 false 866 ); 867 run_next_test(); 868 }); 869 870 add_task(async function test_getOAuthTokenCachedWithSessionToken() { 871 Services.prefs.setBoolPref( 872 "identity.fxaccounts.useSessionTokensForOAuth", 873 true 874 ); 875 let fxa = new MockFxAccounts(); 876 let alice = getTestUser("alice"); 877 alice.verified = true; 878 let numOauthTokenCalls = 0; 879 880 let client = fxa._internal.fxAccountsClient; 881 client.accessTokenWithSessionToken = async () => { 882 numOauthTokenCalls++; 883 return MOCK_TOKEN_RESPONSE; 884 }; 885 886 await fxa.setSignedInUser(alice); 887 let result = await fxa.getOAuthToken({ 888 scope: "profile", 889 service: "test-service", 890 }); 891 Assert.equal(numOauthTokenCalls, 1); 892 Assert.equal( 893 result, 894 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 895 ); 896 897 // requesting it again should not re-fetch the token. 898 result = await fxa.getOAuthToken({ 899 scope: "profile", 900 service: "test-service", 901 }); 902 Assert.equal(numOauthTokenCalls, 1); 903 Assert.equal( 904 result, 905 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 906 ); 907 // But requesting the same service and a different scope *will* get a new one. 908 result = await fxa.getOAuthToken({ 909 scope: "something-else", 910 service: "test-service", 911 }); 912 Assert.equal(numOauthTokenCalls, 2); 913 Assert.equal( 914 result, 915 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 916 ); 917 Services.prefs.setBoolPref( 918 "identity.fxaccounts.useSessionTokensForOAuth", 919 false 920 ); 921 }); 922 923 add_test(function test_getOAuthTokenScopedWithSessionToken() { 924 let fxa = new MockFxAccounts(); 925 let alice = getTestUser("alice"); 926 alice.verified = true; 927 let numOauthTokenCalls = 0; 928 929 let client = fxa._internal.fxAccountsClient; 930 client.accessTokenWithSessionToken = async ( 931 _sessionTokenHex, 932 _clientId, 933 scopeString 934 ) => { 935 equal(scopeString, "bar foo"); // scopes are sorted locally before request. 936 numOauthTokenCalls++; 937 return MOCK_TOKEN_RESPONSE; 938 }; 939 940 fxa.setSignedInUser(alice).then(() => { 941 fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => { 942 Assert.equal(numOauthTokenCalls, 1); 943 Assert.equal( 944 result, 945 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 946 ); 947 run_next_test(); 948 }); 949 }); 950 }); 951 952 add_task(async function test_getOAuthTokenCachedScopeNormalization() { 953 let fxa = new MockFxAccounts(); 954 let alice = getTestUser("alice"); 955 alice.verified = true; 956 let numOAuthTokenCalls = 0; 957 958 let client = fxa._internal.fxAccountsClient; 959 client.accessTokenWithSessionToken = async (_sessionTokenHex, _clientId) => { 960 numOAuthTokenCalls++; 961 return MOCK_TOKEN_RESPONSE; 962 }; 963 964 await fxa.setSignedInUser(alice); 965 let result = await fxa.getOAuthToken({ 966 scope: ["foo", "bar"], 967 service: "test-service", 968 }); 969 Assert.equal(numOAuthTokenCalls, 1); 970 Assert.equal( 971 result, 972 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 973 ); 974 975 // requesting it again with the scope array in a different order should not re-fetch the token. 976 result = await fxa.getOAuthToken({ 977 scope: ["bar", "foo"], 978 service: "test-service", 979 }); 980 Assert.equal(numOAuthTokenCalls, 1); 981 Assert.equal( 982 result, 983 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 984 ); 985 // requesting it again with the scope array in different case should not re-fetch the token. 986 result = await fxa.getOAuthToken({ 987 scope: ["Bar", "Foo"], 988 service: "test-service", 989 }); 990 Assert.equal(numOAuthTokenCalls, 1); 991 Assert.equal( 992 result, 993 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 994 ); 995 // But requesting with a new entry in the array does fetch one. 996 result = await fxa.getOAuthToken({ 997 scope: ["foo", "bar", "etc"], 998 service: "test-service", 999 }); 1000 Assert.equal(numOAuthTokenCalls, 2); 1001 Assert.equal( 1002 result, 1003 "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69" 1004 ); 1005 }); 1006 1007 add_test(function test_getOAuthToken_invalid_param() { 1008 let fxa = new MockFxAccounts(); 1009 1010 fxa.getOAuthToken().catch(err => { 1011 Assert.equal(err.message, "INVALID_PARAMETER"); 1012 fxa.signOut().then(run_next_test); 1013 }); 1014 }); 1015 1016 add_test(function test_getOAuthToken_invalid_scope_array() { 1017 let fxa = new MockFxAccounts(); 1018 1019 fxa.getOAuthToken({ scope: [] }).catch(err => { 1020 Assert.equal(err.message, "INVALID_PARAMETER"); 1021 fxa.signOut().then(run_next_test); 1022 }); 1023 }); 1024 1025 add_test(function test_getOAuthToken_misconfigure_oauth_uri() { 1026 let fxa = new MockFxAccounts(); 1027 1028 const prevServerURL = Services.prefs.getStringPref( 1029 "identity.fxaccounts.remote.oauth.uri" 1030 ); 1031 Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri"); 1032 1033 fxa.getOAuthToken().catch(err => { 1034 Assert.equal(err.message, "INVALID_PARAMETER"); 1035 // revert the pref 1036 Services.prefs.setStringPref( 1037 "identity.fxaccounts.remote.oauth.uri", 1038 prevServerURL 1039 ); 1040 fxa.signOut().then(run_next_test); 1041 }); 1042 }); 1043 1044 add_test(function test_getOAuthToken_no_account() { 1045 let fxa = new MockFxAccounts(); 1046 1047 fxa._internal.currentAccountState.getUserAccountData = function () { 1048 return Promise.resolve(null); 1049 }; 1050 1051 fxa.getOAuthToken({ scope: "profile" }).catch(err => { 1052 Assert.equal(err.message, "NO_ACCOUNT"); 1053 fxa.signOut().then(run_next_test); 1054 }); 1055 }); 1056 1057 add_test(function test_getOAuthToken_unverified() { 1058 let fxa = new MockFxAccounts(); 1059 let alice = getTestUser("alice"); 1060 1061 fxa.setSignedInUser(alice).then(() => { 1062 fxa.getOAuthToken({ scope: "profile" }).catch(err => { 1063 Assert.equal(err.message, "UNVERIFIED_ACCOUNT"); 1064 fxa.signOut().then(run_next_test); 1065 }); 1066 }); 1067 }); 1068 1069 add_test(function test_getOAuthToken_error() { 1070 let fxa = new MockFxAccounts(); 1071 let alice = getTestUser("alice"); 1072 alice.verified = true; 1073 1074 let client = fxa._internal.fxAccountsClient; 1075 client.accessTokenWithSessionToken = () => { 1076 return Promise.reject("boom"); 1077 }; 1078 1079 fxa.setSignedInUser(alice).then(() => { 1080 fxa.getOAuthToken({ scope: "profile" }).catch(err => { 1081 equal(err.details, "boom"); 1082 run_next_test(); 1083 }); 1084 }); 1085 }); 1086 1087 add_test(async function test_getOAuthTokenAndKey_errors_if_user_change() { 1088 const fxa = new MockFxAccounts(); 1089 const alice = getTestUser("alice"); 1090 const bob = getTestUser("bob"); 1091 alice.verified = true; 1092 bob.verified = true; 1093 1094 fxa.getOAuthToken = async () => { 1095 // We mock what would happen if the user got changed 1096 // after we got the access token 1097 await fxa.setSignedInUser(bob); 1098 return "access token"; 1099 }; 1100 fxa.keys.getKeyForScope = () => Promise.resolve("key!"); 1101 await fxa.setSignedInUser(alice); 1102 await Assert.rejects( 1103 fxa.getOAuthTokenAndKey({ scope: "foo", ttl: 10 }), 1104 err => { 1105 Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE); 1106 return true; // expected error 1107 } 1108 ); 1109 run_next_test(); 1110 }); 1111 1112 add_task(async function test_listAttachedOAuthClients() { 1113 const ONE_HOUR = 60 * 60 * 1000; 1114 const ONE_DAY = 24 * ONE_HOUR; 1115 1116 const timestamp = Date.now(); 1117 1118 let fxa = new MockFxAccounts(); 1119 let alice = getTestUser("alice"); 1120 alice.verified = true; 1121 1122 let client = fxa._internal.fxAccountsClient; 1123 client.attachedClients = async () => { 1124 return { 1125 body: [ 1126 // This entry was previously filtered but no longer is! 1127 { 1128 clientId: "a2270f727f45f648", 1129 deviceId: "deadbeef", 1130 sessionTokenId: null, 1131 name: "Firefox Preview (no session token)", 1132 scope: ["profile", SCOPE_APP_SYNC], 1133 lastAccessTime: Date.now(), 1134 }, 1135 { 1136 clientId: "802d56ef2a9af9fa", 1137 deviceId: null, 1138 sessionTokenId: null, 1139 name: "Firefox Monitor", 1140 scope: ["profile"], 1141 lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR, 1142 }, 1143 { 1144 clientId: "1f30e32975ae5112", 1145 deviceId: null, 1146 sessionTokenId: null, 1147 name: "Firefox Send", 1148 scope: ["profile", "https://identity.mozilla.com/apps/send"], 1149 lastAccessTime: Date.now() - ONE_DAY * 2 - ONE_HOUR, 1150 }, 1151 // One with a future date should be impossible, but having a negative 1152 // result here would almost certainly confuse something! 1153 { 1154 clientId: "future-date", 1155 deviceId: null, 1156 sessionTokenId: null, 1157 name: "Whatever", 1158 lastAccessTime: Date.now() + ONE_DAY, 1159 }, 1160 // A missing/null lastAccessTime should end up with a missing lastAccessedDaysAgo 1161 { 1162 clientId: "missing-date", 1163 deviceId: null, 1164 sessionTokenId: null, 1165 name: "Whatever", 1166 }, 1167 ], 1168 headers: { "x-timestamp": timestamp.toString() }, 1169 }; 1170 }; 1171 1172 await fxa.setSignedInUser(alice); 1173 const clients = await fxa.listAttachedOAuthClients(); 1174 Assert.deepEqual(clients, [ 1175 { 1176 id: "a2270f727f45f648", 1177 lastAccessedDaysAgo: 0, 1178 }, 1179 { 1180 id: "802d56ef2a9af9fa", 1181 lastAccessedDaysAgo: 1, 1182 }, 1183 { 1184 id: "1f30e32975ae5112", 1185 lastAccessedDaysAgo: 2, 1186 }, 1187 { 1188 id: "future-date", 1189 lastAccessedDaysAgo: 0, 1190 }, 1191 { 1192 id: "missing-date", 1193 lastAccessedDaysAgo: null, 1194 }, 1195 ]); 1196 }); 1197 1198 add_task(async function test_listAttachedOAuthClients_withCaching() { 1199 const ONE_HOUR = 60 * 60 * 1000; 1200 const ONE_DAY = 24 * ONE_HOUR; 1201 1202 const timestamp = Date.now(); 1203 1204 let fxa = new MockFxAccounts(); 1205 let alice = getTestUser("alice"); 1206 alice.verified = true; 1207 1208 let client = fxa._internal.fxAccountsClient; 1209 let originalResponse = { 1210 body: [ 1211 { 1212 clientId: "a2270f727f45f648", 1213 deviceId: "deadbeef", 1214 sessionTokenId: null, 1215 name: "Firefox Preview (no session token)", 1216 scope: ["profile", SCOPE_APP_SYNC], 1217 lastAccessTime: Date.now(), 1218 }, 1219 { 1220 clientId: "802d56ef2a9af9fa", 1221 deviceId: null, 1222 sessionTokenId: null, 1223 name: "Firefox Monitor", 1224 scope: ["profile"], 1225 lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR, 1226 }, 1227 ], 1228 headers: { "x-timestamp": timestamp.toString() }, 1229 }; 1230 1231 // Mock the client method. 1232 client.attachedClients = async () => { 1233 return originalResponse; 1234 }; 1235 1236 await fxa.setSignedInUser(alice); 1237 1238 // First call: should fetch from server 1239 const clientsFirstCall = await fxa.listAttachedOAuthClients(); 1240 Assert.deepEqual(clientsFirstCall, [ 1241 { id: "a2270f727f45f648", lastAccessedDaysAgo: 0 }, 1242 { id: "802d56ef2a9af9fa", lastAccessedDaysAgo: 1 }, 1243 ]); 1244 1245 // Now modify the client so if it calls again, we would get different clients. 1246 const updatedResponse = { 1247 body: [ 1248 { 1249 clientId: "updated-client", 1250 lastAccessTime: Date.now() - ONE_DAY * 3, 1251 }, 1252 ], 1253 headers: { "x-timestamp": (timestamp + 1000).toString() }, 1254 }; 1255 client.attachedClients = async () => { 1256 return updatedResponse; 1257 }; 1258 1259 // Second call without forceRefresh: should return cached data, not the updated one. 1260 const clientsSecondCall = await fxa.listAttachedOAuthClients(); 1261 Assert.deepEqual( 1262 clientsSecondCall, 1263 [ 1264 { id: "a2270f727f45f648", lastAccessedDaysAgo: 0 }, 1265 { id: "802d56ef2a9af9fa", lastAccessedDaysAgo: 1 }, 1266 ], 1267 "Should return cached clients from the first call, ignoring the updated mock" 1268 ); 1269 1270 // Now force refresh 1271 const clientsForceRefresh = await fxa.listAttachedOAuthClients(true); 1272 Assert.deepEqual( 1273 clientsForceRefresh, 1274 [{ id: "updated-client", lastAccessedDaysAgo: 3 }], 1275 "Forcing a refresh should return the updated data" 1276 ); 1277 }); 1278 1279 add_task(async function test_getSignedInUserProfile() { 1280 let alice = getTestUser("alice"); 1281 alice.verified = true; 1282 1283 let mockProfile = { 1284 getProfile() { 1285 return Promise.resolve({ avatar: "image" }); 1286 }, 1287 tearDown() {}, 1288 }; 1289 let fxa = new FxAccounts({ 1290 _signOutServer() { 1291 return Promise.resolve(); 1292 }, 1293 device: { 1294 _registerOrUpdateDevice() { 1295 return Promise.resolve(); 1296 }, 1297 }, 1298 }); 1299 1300 await fxa._internal.setSignedInUser(alice); 1301 fxa._internal._profile = mockProfile; 1302 let result = await fxa.getSignedInUser(); 1303 Assert.ok(!!result); 1304 Assert.equal(result.avatar, "image"); 1305 }); 1306 1307 add_task(async function test_getSignedInUserProfile_error_uses_account_data() { 1308 let fxa = new MockFxAccounts(); 1309 let alice = getTestUser("alice"); 1310 alice.verified = true; 1311 1312 fxa._internal.getSignedInUser = function () { 1313 return Promise.resolve({ email: "foo@bar.com" }); 1314 }; 1315 fxa._internal._profile = { 1316 getProfile() { 1317 return Promise.reject("boom"); 1318 }, 1319 tearDown() { 1320 teardownCalled = true; 1321 }, 1322 }; 1323 1324 let teardownCalled = false; 1325 await fxa.setSignedInUser(alice); 1326 let result = await fxa.getSignedInUser(); 1327 Assert.deepEqual(result.avatar, null); 1328 await fxa.signOut(); 1329 Assert.ok(teardownCalled); 1330 }); 1331 1332 add_task(async function test_flushLogFile() { 1333 _("Tests flushLogFile"); 1334 let account = await MakeFxAccounts(); 1335 let promiseObserved = new Promise(res => { 1336 log.info("Adding flush-log-file observer."); 1337 Services.obs.addObserver(function onFlushLogFile() { 1338 Services.obs.removeObserver( 1339 onFlushLogFile, 1340 "service:log-manager:flush-log-file" 1341 ); 1342 res(); 1343 }, "service:log-manager:flush-log-file"); 1344 }); 1345 account.flushLogFile(); 1346 await promiseObserved; 1347 }); 1348 1349 /* 1350 * End of tests. 1351 * Utility functions follow. 1352 */ 1353 1354 function expandHex(two_hex) { 1355 // Return a 64-character hex string, encoding 32 identical bytes. 1356 let eight_hex = two_hex + two_hex + two_hex + two_hex; 1357 let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex; 1358 return thirtytwo_hex + thirtytwo_hex; 1359 } 1360 1361 function expandBytes(two_hex) { 1362 return CommonUtils.hexToBytes(expandHex(two_hex)); 1363 } 1364 1365 function getTestUser(name) { 1366 return { 1367 email: name + "@example.com", 1368 uid: "1ad7f5024cc74ec1a209071fd2fae348", 1369 sessionToken: name + "'s session token", 1370 keyFetchToken: name + "'s keyfetch token", 1371 unwrapBKey: expandHex("44"), 1372 verified: false, 1373 encryptedSendTabKeys: name + "'s encrypted Send tab keys", 1374 }; 1375 } 1376 1377 function makeObserver(aObserveTopic, aObserveFunc) { 1378 let observer = { 1379 // nsISupports provides type management in C++ 1380 // nsIObserver is to be an observer 1381 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 1382 1383 observe(aSubject, aTopic, aData) { 1384 log.debug("observed " + aTopic + " " + aData); 1385 if (aTopic == aObserveTopic) { 1386 removeMe(); 1387 aObserveFunc(aSubject, aTopic, aData); 1388 } 1389 }, 1390 }; 1391 1392 function removeMe() { 1393 log.debug("removing observer for " + aObserveTopic); 1394 Services.obs.removeObserver(observer, aObserveTopic); 1395 } 1396 1397 Services.obs.addObserver(observer, aObserveTopic); 1398 return removeMe; 1399 }