test_profile.js (19819B)
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, log } = ChromeUtils.importESModule( 7 "resource://gre/modules/FxAccountsCommon.sys.mjs" 8 ); 9 const { FxAccountsProfileClient } = ChromeUtils.importESModule( 10 "resource://gre/modules/FxAccountsProfileClient.sys.mjs" 11 ); 12 const { FxAccountsProfile } = ChromeUtils.importESModule( 13 "resource://gre/modules/FxAccountsProfile.sys.mjs" 14 ); 15 const { setTimeout } = ChromeUtils.importESModule( 16 "resource://gre/modules/Timer.sys.mjs" 17 ); 18 19 let mockClient = function (fxa) { 20 let options = { 21 serverURL: "http://127.0.0.1:1111/v1", 22 fxa, 23 }; 24 return new FxAccountsProfileClient(options); 25 }; 26 27 const ACCOUNT_UID = "abc123"; 28 const ACCOUNT_EMAIL = "foo@bar.com"; 29 const ACCOUNT_DATA = { 30 uid: ACCOUNT_UID, 31 email: ACCOUNT_EMAIL, 32 }; 33 34 let mockFxa = function () { 35 let fxa = { 36 // helpers to make the tests using this mock less verbose... 37 set _testProfileCache(profileCache) { 38 this._internal.currentAccountState._data.profileCache = profileCache; 39 }, 40 get _testProfileCache() { 41 return this._internal.currentAccountState._data.profileCache; 42 }, 43 }; 44 fxa._internal = Object.assign( 45 {}, 46 { 47 currentAccountState: Object.assign( 48 {}, 49 { 50 _data: Object.assign({}, ACCOUNT_DATA), 51 52 get isCurrent() { 53 return true; 54 }, 55 56 async getUserAccountData() { 57 return this._data; 58 }, 59 60 async updateUserAccountData(data) { 61 this._data = Object.assign(this._data, data); 62 }, 63 } 64 ), 65 66 withCurrentAccountState(cb) { 67 return cb(this.currentAccountState); 68 }, 69 70 async _handleTokenError(err) { 71 // handleTokenError always rethrows. 72 throw err; 73 }, 74 } 75 ); 76 return fxa; 77 }; 78 79 function CreateFxAccountsProfile(fxa = null, client = null) { 80 if (!fxa) { 81 fxa = mockFxa(); 82 } 83 let options = { 84 fxai: fxa._internal, 85 profileServerUrl: "http://127.0.0.1:1111/v1", 86 }; 87 if (client) { 88 options.profileClient = client; 89 } 90 return new FxAccountsProfile(options); 91 } 92 93 add_test(function cacheProfile_change() { 94 let setProfileCacheCalled = false; 95 let fxa = mockFxa(); 96 fxa._internal.currentAccountState.updateUserAccountData = data => { 97 setProfileCacheCalled = true; 98 Assert.equal(data.profileCache.profile.avatar, "myurl"); 99 Assert.equal(data.profileCache.etag, "bogusetag"); 100 return Promise.resolve(); 101 }; 102 let profile = CreateFxAccountsProfile(fxa); 103 104 makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { 105 Assert.equal(data, ACCOUNT_DATA.uid); 106 Assert.ok(setProfileCacheCalled); 107 run_next_test(); 108 }); 109 110 return profile._cacheProfile({ 111 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myurl" }, 112 etag: "bogusetag", 113 }); 114 }); 115 116 add_test(function fetchAndCacheProfile_ok() { 117 let client = mockClient(mockFxa()); 118 client.fetchProfile = function () { 119 return Promise.resolve({ body: { uid: ACCOUNT_UID, avatar: "myimg" } }); 120 }; 121 let profile = CreateFxAccountsProfile(null, client); 122 profile._cachedAt = 12345; 123 124 profile._cacheProfile = function (toCache) { 125 Assert.equal(toCache.body.avatar, "myimg"); 126 return Promise.resolve(toCache.body); 127 }; 128 129 return profile._fetchAndCacheProfile().then(result => { 130 Assert.equal(result.avatar, "myimg"); 131 Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped"); 132 run_next_test(); 133 }); 134 }); 135 136 add_test(function fetchAndCacheProfile_always_bumps_cachedAt() { 137 let client = mockClient(mockFxa()); 138 client.fetchProfile = function () { 139 return Promise.reject(new Error("oops")); 140 }; 141 let profile = CreateFxAccountsProfile(null, client); 142 profile._cachedAt = 12345; 143 144 return profile._fetchAndCacheProfile().then( 145 () => { 146 do_throw("Should not succeed"); 147 }, 148 () => { 149 Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped"); 150 run_next_test(); 151 } 152 ); 153 }); 154 155 add_test(function fetchAndCacheProfile_sendsETag() { 156 let fxa = mockFxa(); 157 fxa._testProfileCache = { profile: {}, etag: "bogusETag" }; 158 let client = mockClient(fxa); 159 client.fetchProfile = function (etag) { 160 Assert.equal(etag, "bogusETag"); 161 return Promise.resolve({ 162 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, 163 }); 164 }; 165 let profile = CreateFxAccountsProfile(fxa, client); 166 167 return profile._fetchAndCacheProfile().then(() => { 168 run_next_test(); 169 }); 170 }); 171 172 // Check that a second profile request when one is already in-flight reuses 173 // the in-flight one. 174 add_task(async function fetchAndCacheProfileOnce() { 175 // A promise that remains unresolved while we fire off 2 requests for 176 // a profile. 177 let resolveProfile; 178 let promiseProfile = new Promise(resolve => { 179 resolveProfile = resolve; 180 }); 181 let numFetches = 0; 182 let client = mockClient(mockFxa()); 183 client.fetchProfile = function () { 184 numFetches += 1; 185 return promiseProfile; 186 }; 187 let fxa = mockFxa(); 188 let profile = CreateFxAccountsProfile(fxa, client); 189 190 let request1 = profile._fetchAndCacheProfile(); 191 profile._fetchAndCacheProfile(); 192 await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise) 193 194 // should be one request made to fetch the profile (but the promise returned 195 // by it remains unresolved) 196 Assert.equal(numFetches, 1); 197 198 // resolve the promise. 199 resolveProfile({ 200 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, 201 }); 202 203 // both requests should complete with the same data. 204 let got1 = await request1; 205 Assert.equal(got1.avatar, "myimg"); 206 let got2 = await request1; 207 Assert.equal(got2.avatar, "myimg"); 208 209 // and still only 1 request was made. 210 Assert.equal(numFetches, 1); 211 }); 212 213 // Check that sharing a single fetch promise works correctly when the promise 214 // is rejected. 215 add_task(async function fetchAndCacheProfileOnce() { 216 // A promise that remains unresolved while we fire off 2 requests for 217 // a profile. 218 let rejectProfile; 219 let promiseProfile = new Promise((resolve, reject) => { 220 rejectProfile = reject; 221 }); 222 let numFetches = 0; 223 let client = mockClient(mockFxa()); 224 client.fetchProfile = function () { 225 numFetches += 1; 226 return promiseProfile; 227 }; 228 let fxa = mockFxa(); 229 let profile = CreateFxAccountsProfile(fxa, client); 230 231 let request1 = profile._fetchAndCacheProfile(); 232 let request2 = profile._fetchAndCacheProfile(); 233 await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise) 234 235 // should be one request made to fetch the profile (but the promise returned 236 // by it remains unresolved) 237 Assert.equal(numFetches, 1); 238 239 // reject the promise. 240 rejectProfile("oh noes"); 241 242 // both requests should reject. 243 try { 244 await request1; 245 throw new Error("should have rejected"); 246 } catch (ex) { 247 if (ex != "oh noes") { 248 throw ex; 249 } 250 } 251 try { 252 await request2; 253 throw new Error("should have rejected"); 254 } catch (ex) { 255 if (ex != "oh noes") { 256 throw ex; 257 } 258 } 259 260 // but a new request should works. 261 client.fetchProfile = function () { 262 return Promise.resolve({ 263 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, 264 }); 265 }; 266 267 let got = await profile._fetchAndCacheProfile(); 268 Assert.equal(got.avatar, "myimg"); 269 }); 270 271 add_test(function fetchAndCacheProfile_alreadyCached() { 272 let cachedUrl = "cachedurl"; 273 let fxa = mockFxa(); 274 fxa._testProfileCache = { 275 profile: { uid: ACCOUNT_UID, avatar: cachedUrl }, 276 etag: "bogusETag", 277 }; 278 let client = mockClient(fxa); 279 client.fetchProfile = function (etag) { 280 Assert.equal(etag, "bogusETag"); 281 return Promise.resolve(null); 282 }; 283 284 let profile = CreateFxAccountsProfile(fxa, client); 285 profile._cacheProfile = function () { 286 do_throw("This method should not be called."); 287 }; 288 289 return profile._fetchAndCacheProfile().then(result => { 290 Assert.equal(result, null); 291 Assert.equal(fxa._testProfileCache.profile.avatar, cachedUrl); 292 run_next_test(); 293 }); 294 }); 295 296 // Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the 297 // last one doesn't kick off a new request to check the cached copy is fresh. 298 add_task(async function fetchAndCacheProfileAfterThreshold() { 299 /* 300 * This test was observed to cause a timeout for... any timer precision reduction. 301 * Even 1 us. Exact reason is still undiagnosed. 302 */ 303 Services.prefs.setBoolPref("privacy.reduceTimerPrecision", false); 304 305 registerCleanupFunction(async () => { 306 Services.prefs.clearUserPref("privacy.reduceTimerPrecision"); 307 }); 308 309 let numFetches = 0; 310 let client = mockClient(mockFxa()); 311 client.fetchProfile = async function () { 312 numFetches += 1; 313 return { 314 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, 315 }; 316 }; 317 let profile = CreateFxAccountsProfile(null, client); 318 profile.PROFILE_FRESHNESS_THRESHOLD = 1000; 319 320 // first fetch should return null as we don't have data. 321 let p = await profile.getProfile(); 322 Assert.equal(p, null); 323 // ensure we kicked off a fetch. 324 Assert.notEqual(profile._currentFetchPromise, null); 325 // wait for that fetch to finish 326 await profile._currentFetchPromise; 327 Assert.equal(numFetches, 1); 328 Assert.equal(profile._currentFetchPromise, null); 329 330 await profile.getProfile(); 331 Assert.equal(numFetches, 1); 332 Assert.equal(profile._currentFetchPromise, null); 333 334 await new Promise(resolve => { 335 do_timeout(1000, resolve); 336 }); 337 338 let origFetchAndCatch = profile._fetchAndCacheProfile; 339 let backgroundFetchDone = Promise.withResolvers(); 340 profile._fetchAndCacheProfile = async () => { 341 await origFetchAndCatch.call(profile); 342 backgroundFetchDone.resolve(); 343 }; 344 await profile.getProfile(); 345 await backgroundFetchDone.promise; 346 Assert.equal(numFetches, 2); 347 }); 348 349 add_task(async function test_ensureProfile() { 350 let client = new FxAccountsProfileClient({ 351 serverURL: "http://127.0.0.1:1111/v1", 352 fxa: mockFxa(), 353 }); 354 let profile = CreateFxAccountsProfile(null, client); 355 356 const testCases = [ 357 // profile retrieval when there is no cached profile info 358 { 359 threshold: 1000, 360 expectsCachedProfileReturned: false, 361 cachedProfile: null, 362 fetchedProfile: { 363 uid: ACCOUNT_UID, 364 email: ACCOUNT_EMAIL, 365 avatar: "myimg", 366 }, 367 }, 368 // profile retrieval when the cached profile is recent 369 { 370 // Note: The threshold for this test case is being set to an arbitrary value that will 371 // be greater than Date.now() so the retrieved cached profile will be deemed recent. 372 threshold: Date.now() + 5000, 373 expectsCachedProfileReturned: true, 374 cachedProfile: { 375 uid: `${ACCOUNT_UID}2`, 376 email: `${ACCOUNT_EMAIL}2`, 377 avatar: "myimg2", 378 }, 379 }, 380 // profile retrieval when the cached profile is old and a new profile is fetched 381 { 382 threshold: 1000, 383 expectsCachedProfileReturned: false, 384 cachedProfile: { 385 uid: `${ACCOUNT_UID}3`, 386 email: `${ACCOUNT_EMAIL}3`, 387 avatar: "myimg3", 388 }, 389 fetchAndCacheProfileResolves: true, 390 fetchedProfile: { 391 uid: `${ACCOUNT_UID}4`, 392 email: `${ACCOUNT_EMAIL}4`, 393 avatar: "myimg4", 394 }, 395 }, 396 // profile retrieval when the cached profile is old and a null profile is fetched 397 { 398 threshold: 1000, 399 expectsCachedProfileReturned: false, 400 cachedProfile: { 401 uid: `${ACCOUNT_UID}5`, 402 email: `${ACCOUNT_EMAIL}5`, 403 avatar: "myimg5", 404 }, 405 fetchAndCacheProfileResolves: true, 406 fetchedProfile: null, 407 }, 408 // profile retrieval when the cached profile is old and fetching a new profile errors 409 { 410 threshold: 1000, 411 expectsCachedProfileReturned: false, 412 cachedProfile: { 413 uid: `${ACCOUNT_UID}6`, 414 email: `${ACCOUNT_EMAIL}6`, 415 avatar: "myimg6", 416 }, 417 fetchAndCacheProfileResolves: false, 418 }, 419 // profile retrieval when we've cached a failure to fetch profile data 420 { 421 // Note: The threshold for this test case is being set to an arbitrary value that will 422 // be greater than Date.now() so the retrieved cached profile will be deemed recent. 423 threshold: Date.now() + 5000, 424 expectsCachedProfileReturned: false, 425 cachedProfile: null, 426 fetchedProfile: { 427 uid: `${ACCOUNT_UID}7`, 428 email: `${ACCOUNT_EMAIL}7`, 429 avatar: "myimg7", 430 }, 431 fetchAndCacheProfileResolves: true, 432 }, 433 // profile retrieval when the cached profile is old but staleOk is true. 434 { 435 threshold: 1000, 436 expectsCachedProfileReturned: true, 437 cachedProfile: { 438 uid: `${ACCOUNT_UID}8`, 439 email: `${ACCOUNT_EMAIL}8`, 440 avatar: "myimg8", 441 }, 442 fetchAndCacheProfileResolves: false, 443 options: { staleOk: true }, 444 }, 445 // staleOk but no cached profile 446 { 447 threshold: 1000, 448 expectsCachedProfileReturned: false, 449 cachedProfile: null, 450 fetchedProfile: { 451 uid: `${ACCOUNT_UID}9`, 452 email: `${ACCOUNT_EMAIL}9`, 453 avatar: "myimg9", 454 }, 455 options: { staleOk: true }, 456 }, 457 // fresh profile but forceFresh = true 458 { 459 // Note: The threshold for this test case is being set to an arbitrary value that will 460 // be greater than Date.now() so the retrieved cached profile will be deemed recent. 461 threshold: Date.now() + 5000, 462 expectsCachedProfileReturned: false, 463 fetchedProfile: { 464 uid: `${ACCOUNT_UID}10`, 465 email: `${ACCOUNT_EMAIL}10`, 466 avatar: "myimg10", 467 }, 468 options: { forceFresh: true }, 469 }, 470 ]; 471 472 for (const tc of testCases) { 473 print(`test case: ${JSON.stringify(tc)}`); 474 let mockProfile = sinon.mock(profile); 475 mockProfile 476 .expects("_getProfileCache") 477 .once() 478 .returns( 479 tc.cachedProfile 480 ? { 481 profile: tc.cachedProfile, 482 } 483 : null 484 ); 485 profile.PROFILE_FRESHNESS_THRESHOLD = tc.threshold; 486 487 let options = tc.options || {}; 488 if (tc.expectsCachedProfileReturned) { 489 mockProfile.expects("_fetchAndCacheProfile").never(); 490 let actualProfile = await profile.ensureProfile(options); 491 mockProfile.verify(); 492 Assert.equal(actualProfile, tc.cachedProfile); 493 } else if (tc.fetchAndCacheProfileResolves) { 494 mockProfile 495 .expects("_fetchAndCacheProfile") 496 .once() 497 .resolves(tc.fetchedProfile); 498 499 let actualProfile = await profile.ensureProfile(options); 500 let expectedProfile = tc.fetchedProfile 501 ? tc.fetchedProfile 502 : tc.cachedProfile; 503 mockProfile.verify(); 504 Assert.equal(actualProfile, expectedProfile); 505 } else { 506 mockProfile.expects("_fetchAndCacheProfile").once().rejects(); 507 508 let actualProfile = await profile.ensureProfile(options); 509 mockProfile.verify(); 510 Assert.equal(actualProfile, tc.cachedProfile); 511 } 512 } 513 }); 514 515 // Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the 516 // last one *does* kick off a new request if ON_PROFILE_CHANGE_NOTIFICATION 517 // is sent. 518 add_task(async function fetchAndCacheProfileBeforeThresholdOnNotification() { 519 let numFetches = 0; 520 let client = mockClient(mockFxa()); 521 client.fetchProfile = async function () { 522 numFetches += 1; 523 return { 524 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, 525 }; 526 }; 527 let profile = CreateFxAccountsProfile(null, client); 528 profile.PROFILE_FRESHNESS_THRESHOLD = 1000; 529 530 // first fetch should return null as we don't have data. 531 let p = await profile.getProfile(); 532 Assert.equal(p, null); 533 // ensure we kicked off a fetch. 534 Assert.notEqual(profile._currentFetchPromise, null); 535 // wait for that fetch to finish 536 await profile._currentFetchPromise; 537 Assert.equal(numFetches, 1); 538 Assert.equal(profile._currentFetchPromise, null); 539 540 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION); 541 542 let origFetchAndCatch = profile._fetchAndCacheProfile; 543 let backgroundFetchDone = Promise.withResolvers(); 544 profile._fetchAndCacheProfile = async () => { 545 await origFetchAndCatch.call(profile); 546 backgroundFetchDone.resolve(); 547 }; 548 await profile.getProfile(); 549 await backgroundFetchDone.promise; 550 Assert.equal(numFetches, 2); 551 }); 552 553 add_test(function tearDown_ok() { 554 let profile = CreateFxAccountsProfile(); 555 556 Assert.ok(!!profile.client); 557 Assert.ok(!!profile.fxai); 558 559 profile.tearDown(); 560 Assert.equal(null, profile.fxai); 561 Assert.equal(null, profile.client); 562 563 run_next_test(); 564 }); 565 566 add_task(async function getProfile_ok() { 567 let cachedUrl = "myurl"; 568 let didFetch = false; 569 570 let fxa = mockFxa(); 571 fxa._testProfileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl } }; 572 let profile = CreateFxAccountsProfile(fxa); 573 574 profile._fetchAndCacheProfile = function () { 575 didFetch = true; 576 return Promise.resolve(); 577 }; 578 579 let result = await profile.getProfile(); 580 581 Assert.equal(result.avatar, cachedUrl); 582 Assert.ok(didFetch); 583 }); 584 585 add_task(async function getProfile_no_cache() { 586 let fetchedUrl = "newUrl"; 587 let fxa = mockFxa(); 588 let profile = CreateFxAccountsProfile(fxa); 589 590 profile._fetchAndCacheProfileInternal = function () { 591 return Promise.resolve({ uid: ACCOUNT_UID, avatar: fetchedUrl }); 592 }; 593 594 await profile.getProfile(); // returns null. 595 let result = await profile._currentFetchPromise; 596 Assert.equal(result.avatar, fetchedUrl); 597 }); 598 599 add_test(function getProfile_has_cached_fetch_deleted() { 600 let cachedUrl = "myurl"; 601 602 let fxa = mockFxa(); 603 let client = mockClient(fxa); 604 client.fetchProfile = function () { 605 return Promise.resolve({ 606 body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: null }, 607 }); 608 }; 609 610 let profile = CreateFxAccountsProfile(fxa, client); 611 fxa._testProfileCache = { 612 profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: cachedUrl }, 613 }; 614 615 // instead of checking this in a mocked "save" function, just check after the 616 // observer 617 makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function () { 618 profile.getProfile().then(profileData => { 619 Assert.equal(null, profileData.avatar); 620 run_next_test(); 621 }); 622 }); 623 624 return profile.getProfile().then(result => { 625 Assert.equal(result.avatar, "myurl"); 626 }); 627 }); 628 629 add_test(function getProfile_fetchAndCacheProfile_throws() { 630 let fxa = mockFxa(); 631 fxa._testProfileCache = { 632 profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" }, 633 }; 634 let profile = CreateFxAccountsProfile(fxa); 635 636 profile._fetchAndCacheProfile = () => Promise.reject(new Error()); 637 638 return profile.getProfile().then(result => { 639 Assert.equal(result.avatar, "myimg"); 640 run_next_test(); 641 }); 642 }); 643 644 add_test(function getProfile_email_changed() { 645 let fxa = mockFxa(); 646 let client = mockClient(fxa); 647 client.fetchProfile = function () { 648 return Promise.resolve({ 649 body: { uid: ACCOUNT_UID, email: "newemail@bar.com" }, 650 }); 651 }; 652 fxa._internal._handleEmailUpdated = email => { 653 Assert.equal(email, "newemail@bar.com"); 654 run_next_test(); 655 }; 656 657 let profile = CreateFxAccountsProfile(fxa, client); 658 return profile._fetchAndCacheProfile(); 659 }); 660 661 function makeObserver(aObserveTopic, aObserveFunc) { 662 let callback = function (aSubject, aTopic, aData) { 663 log.debug("observed " + aTopic + " " + aData); 664 if (aTopic == aObserveTopic) { 665 removeMe(); 666 aObserveFunc(aSubject, aTopic, aData); 667 } 668 }; 669 670 function removeMe() { 671 log.debug("removing observer for " + aObserveTopic); 672 Services.obs.removeObserver(callback, aObserveTopic); 673 } 674 675 Services.obs.addObserver(callback, aObserveTopic); 676 return removeMe; 677 }