summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/tests/xpcshell/test_profile.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_profile.js')
-rw-r--r--services/fxaccounts/tests/xpcshell/test_profile.js677
1 files changed, 677 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_profile.js b/services/fxaccounts/tests/xpcshell/test_profile.js
new file mode 100644
index 0000000000..f8137b5691
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -0,0 +1,677 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ON_PROFILE_CHANGE_NOTIFICATION, log } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsCommon.sys.mjs"
+);
+const { FxAccountsProfileClient } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsProfileClient.sys.mjs"
+);
+const { FxAccountsProfile } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsProfile.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+let mockClient = function (fxa) {
+ let options = {
+ serverURL: "http://127.0.0.1:1111/v1",
+ fxa,
+ };
+ return new FxAccountsProfileClient(options);
+};
+
+const ACCOUNT_UID = "abc123";
+const ACCOUNT_EMAIL = "foo@bar.com";
+const ACCOUNT_DATA = {
+ uid: ACCOUNT_UID,
+ email: ACCOUNT_EMAIL,
+};
+
+let mockFxa = function () {
+ let fxa = {
+ // helpers to make the tests using this mock less verbose...
+ set _testProfileCache(profileCache) {
+ this._internal.currentAccountState._data.profileCache = profileCache;
+ },
+ get _testProfileCache() {
+ return this._internal.currentAccountState._data.profileCache;
+ },
+ };
+ fxa._internal = Object.assign(
+ {},
+ {
+ currentAccountState: Object.assign(
+ {},
+ {
+ _data: Object.assign({}, ACCOUNT_DATA),
+
+ get isCurrent() {
+ return true;
+ },
+
+ async getUserAccountData() {
+ return this._data;
+ },
+
+ async updateUserAccountData(data) {
+ this._data = Object.assign(this._data, data);
+ },
+ }
+ ),
+
+ withCurrentAccountState(cb) {
+ return cb(this.currentAccountState);
+ },
+
+ async _handleTokenError(err) {
+ // handleTokenError always rethrows.
+ throw err;
+ },
+ }
+ );
+ return fxa;
+};
+
+function CreateFxAccountsProfile(fxa = null, client = null) {
+ if (!fxa) {
+ fxa = mockFxa();
+ }
+ let options = {
+ fxai: fxa._internal,
+ profileServerUrl: "http://127.0.0.1:1111/v1",
+ };
+ if (client) {
+ options.profileClient = client;
+ }
+ return new FxAccountsProfile(options);
+}
+
+add_test(function cacheProfile_change() {
+ let setProfileCacheCalled = false;
+ let fxa = mockFxa();
+ fxa._internal.currentAccountState.updateUserAccountData = data => {
+ setProfileCacheCalled = true;
+ Assert.equal(data.profileCache.profile.avatar, "myurl");
+ Assert.equal(data.profileCache.etag, "bogusetag");
+ return Promise.resolve();
+ };
+ let profile = CreateFxAccountsProfile(fxa);
+
+ makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
+ Assert.equal(data, ACCOUNT_DATA.uid);
+ Assert.ok(setProfileCacheCalled);
+ run_next_test();
+ });
+
+ return profile._cacheProfile({
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myurl" },
+ etag: "bogusetag",
+ });
+});
+
+add_test(function fetchAndCacheProfile_ok() {
+ let client = mockClient(mockFxa());
+ client.fetchProfile = function () {
+ return Promise.resolve({ body: { uid: ACCOUNT_UID, avatar: "myimg" } });
+ };
+ let profile = CreateFxAccountsProfile(null, client);
+ profile._cachedAt = 12345;
+
+ profile._cacheProfile = function (toCache) {
+ Assert.equal(toCache.body.avatar, "myimg");
+ return Promise.resolve(toCache.body);
+ };
+
+ return profile._fetchAndCacheProfile().then(result => {
+ Assert.equal(result.avatar, "myimg");
+ Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped");
+ run_next_test();
+ });
+});
+
+add_test(function fetchAndCacheProfile_always_bumps_cachedAt() {
+ let client = mockClient(mockFxa());
+ client.fetchProfile = function () {
+ return Promise.reject(new Error("oops"));
+ };
+ let profile = CreateFxAccountsProfile(null, client);
+ profile._cachedAt = 12345;
+
+ return profile._fetchAndCacheProfile().then(
+ result => {
+ do_throw("Should not succeed");
+ },
+ err => {
+ Assert.notEqual(profile._cachedAt, 12345, "cachedAt has been bumped");
+ run_next_test();
+ }
+ );
+});
+
+add_test(function fetchAndCacheProfile_sendsETag() {
+ let fxa = mockFxa();
+ fxa._testProfileCache = { profile: {}, etag: "bogusETag" };
+ let client = mockClient(fxa);
+ client.fetchProfile = function (etag) {
+ Assert.equal(etag, "bogusETag");
+ return Promise.resolve({
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
+ });
+ };
+ let profile = CreateFxAccountsProfile(fxa, client);
+
+ return profile._fetchAndCacheProfile().then(result => {
+ run_next_test();
+ });
+});
+
+// Check that a second profile request when one is already in-flight reuses
+// the in-flight one.
+add_task(async function fetchAndCacheProfileOnce() {
+ // A promise that remains unresolved while we fire off 2 requests for
+ // a profile.
+ let resolveProfile;
+ let promiseProfile = new Promise(resolve => {
+ resolveProfile = resolve;
+ });
+ let numFetches = 0;
+ let client = mockClient(mockFxa());
+ client.fetchProfile = function () {
+ numFetches += 1;
+ return promiseProfile;
+ };
+ let fxa = mockFxa();
+ let profile = CreateFxAccountsProfile(fxa, client);
+
+ let request1 = profile._fetchAndCacheProfile();
+ profile._fetchAndCacheProfile();
+ await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise)
+
+ // should be one request made to fetch the profile (but the promise returned
+ // by it remains unresolved)
+ Assert.equal(numFetches, 1);
+
+ // resolve the promise.
+ resolveProfile({
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
+ });
+
+ // both requests should complete with the same data.
+ let got1 = await request1;
+ Assert.equal(got1.avatar, "myimg");
+ let got2 = await request1;
+ Assert.equal(got2.avatar, "myimg");
+
+ // and still only 1 request was made.
+ Assert.equal(numFetches, 1);
+});
+
+// Check that sharing a single fetch promise works correctly when the promise
+// is rejected.
+add_task(async function fetchAndCacheProfileOnce() {
+ // A promise that remains unresolved while we fire off 2 requests for
+ // a profile.
+ let rejectProfile;
+ let promiseProfile = new Promise((resolve, reject) => {
+ rejectProfile = reject;
+ });
+ let numFetches = 0;
+ let client = mockClient(mockFxa());
+ client.fetchProfile = function () {
+ numFetches += 1;
+ return promiseProfile;
+ };
+ let fxa = mockFxa();
+ let profile = CreateFxAccountsProfile(fxa, client);
+
+ let request1 = profile._fetchAndCacheProfile();
+ let request2 = profile._fetchAndCacheProfile();
+ await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise)
+
+ // should be one request made to fetch the profile (but the promise returned
+ // by it remains unresolved)
+ Assert.equal(numFetches, 1);
+
+ // reject the promise.
+ rejectProfile("oh noes");
+
+ // both requests should reject.
+ try {
+ await request1;
+ throw new Error("should have rejected");
+ } catch (ex) {
+ if (ex != "oh noes") {
+ throw ex;
+ }
+ }
+ try {
+ await request2;
+ throw new Error("should have rejected");
+ } catch (ex) {
+ if (ex != "oh noes") {
+ throw ex;
+ }
+ }
+
+ // but a new request should works.
+ client.fetchProfile = function () {
+ return Promise.resolve({
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
+ });
+ };
+
+ let got = await profile._fetchAndCacheProfile();
+ Assert.equal(got.avatar, "myimg");
+});
+
+add_test(function fetchAndCacheProfile_alreadyCached() {
+ let cachedUrl = "cachedurl";
+ let fxa = mockFxa();
+ fxa._testProfileCache = {
+ profile: { uid: ACCOUNT_UID, avatar: cachedUrl },
+ etag: "bogusETag",
+ };
+ let client = mockClient(fxa);
+ client.fetchProfile = function (etag) {
+ Assert.equal(etag, "bogusETag");
+ return Promise.resolve(null);
+ };
+
+ let profile = CreateFxAccountsProfile(fxa, client);
+ profile._cacheProfile = function (toCache) {
+ do_throw("This method should not be called.");
+ };
+
+ return profile._fetchAndCacheProfile().then(result => {
+ Assert.equal(result, null);
+ Assert.equal(fxa._testProfileCache.profile.avatar, cachedUrl);
+ run_next_test();
+ });
+});
+
+// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
+// last one doesn't kick off a new request to check the cached copy is fresh.
+add_task(async function fetchAndCacheProfileAfterThreshold() {
+ /*
+ * This test was observed to cause a timeout for... any timer precision reduction.
+ * Even 1 us. Exact reason is still undiagnosed.
+ */
+ Services.prefs.setBoolPref("privacy.reduceTimerPrecision", false);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("privacy.reduceTimerPrecision");
+ });
+
+ let numFetches = 0;
+ let client = mockClient(mockFxa());
+ client.fetchProfile = async function () {
+ numFetches += 1;
+ return {
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
+ };
+ };
+ let profile = CreateFxAccountsProfile(null, client);
+ profile.PROFILE_FRESHNESS_THRESHOLD = 1000;
+
+ // first fetch should return null as we don't have data.
+ let p = await profile.getProfile();
+ Assert.equal(p, null);
+ // ensure we kicked off a fetch.
+ Assert.notEqual(profile._currentFetchPromise, null);
+ // wait for that fetch to finish
+ await profile._currentFetchPromise;
+ Assert.equal(numFetches, 1);
+ Assert.equal(profile._currentFetchPromise, null);
+
+ await profile.getProfile();
+ Assert.equal(numFetches, 1);
+ Assert.equal(profile._currentFetchPromise, null);
+
+ await new Promise(resolve => {
+ do_timeout(1000, resolve);
+ });
+
+ let origFetchAndCatch = profile._fetchAndCacheProfile;
+ let backgroundFetchDone = Promise.withResolvers();
+ profile._fetchAndCacheProfile = async () => {
+ await origFetchAndCatch.call(profile);
+ backgroundFetchDone.resolve();
+ };
+ await profile.getProfile();
+ await backgroundFetchDone.promise;
+ Assert.equal(numFetches, 2);
+});
+
+add_task(async function test_ensureProfile() {
+ let client = new FxAccountsProfileClient({
+ serverURL: "http://127.0.0.1:1111/v1",
+ fxa: mockFxa(),
+ });
+ let profile = CreateFxAccountsProfile(null, client);
+
+ const testCases = [
+ // profile retrieval when there is no cached profile info
+ {
+ threshold: 1000,
+ expectsCachedProfileReturned: false,
+ cachedProfile: null,
+ fetchedProfile: {
+ uid: ACCOUNT_UID,
+ email: ACCOUNT_EMAIL,
+ avatar: "myimg",
+ },
+ },
+ // profile retrieval when the cached profile is recent
+ {
+ // Note: The threshold for this test case is being set to an arbitrary value that will
+ // be greater than Date.now() so the retrieved cached profile will be deemed recent.
+ threshold: Date.now() + 5000,
+ expectsCachedProfileReturned: true,
+ cachedProfile: {
+ uid: `${ACCOUNT_UID}2`,
+ email: `${ACCOUNT_EMAIL}2`,
+ avatar: "myimg2",
+ },
+ },
+ // profile retrieval when the cached profile is old and a new profile is fetched
+ {
+ threshold: 1000,
+ expectsCachedProfileReturned: false,
+ cachedProfile: {
+ uid: `${ACCOUNT_UID}3`,
+ email: `${ACCOUNT_EMAIL}3`,
+ avatar: "myimg3",
+ },
+ fetchAndCacheProfileResolves: true,
+ fetchedProfile: {
+ uid: `${ACCOUNT_UID}4`,
+ email: `${ACCOUNT_EMAIL}4`,
+ avatar: "myimg4",
+ },
+ },
+ // profile retrieval when the cached profile is old and a null profile is fetched
+ {
+ threshold: 1000,
+ expectsCachedProfileReturned: false,
+ cachedProfile: {
+ uid: `${ACCOUNT_UID}5`,
+ email: `${ACCOUNT_EMAIL}5`,
+ avatar: "myimg5",
+ },
+ fetchAndCacheProfileResolves: true,
+ fetchedProfile: null,
+ },
+ // profile retrieval when the cached profile is old and fetching a new profile errors
+ {
+ threshold: 1000,
+ expectsCachedProfileReturned: false,
+ cachedProfile: {
+ uid: `${ACCOUNT_UID}6`,
+ email: `${ACCOUNT_EMAIL}6`,
+ avatar: "myimg6",
+ },
+ fetchAndCacheProfileResolves: false,
+ },
+ // profile retrieval when we've cached a failure to fetch profile data
+ {
+ // Note: The threshold for this test case is being set to an arbitrary value that will
+ // be greater than Date.now() so the retrieved cached profile will be deemed recent.
+ threshold: Date.now() + 5000,
+ expectsCachedProfileReturned: false,
+ cachedProfile: null,
+ fetchedProfile: {
+ uid: `${ACCOUNT_UID}7`,
+ email: `${ACCOUNT_EMAIL}7`,
+ avatar: "myimg7",
+ },
+ fetchAndCacheProfileResolves: true,
+ },
+ // profile retrieval when the cached profile is old but staleOk is true.
+ {
+ threshold: 1000,
+ expectsCachedProfileReturned: true,
+ cachedProfile: {
+ uid: `${ACCOUNT_UID}8`,
+ email: `${ACCOUNT_EMAIL}8`,
+ avatar: "myimg8",
+ },
+ fetchAndCacheProfileResolves: false,
+ options: { staleOk: true },
+ },
+ // staleOk but no cached profile
+ {
+ threshold: 1000,
+ expectsCachedProfileReturned: false,
+ cachedProfile: null,
+ fetchedProfile: {
+ uid: `${ACCOUNT_UID}9`,
+ email: `${ACCOUNT_EMAIL}9`,
+ avatar: "myimg9",
+ },
+ options: { staleOk: true },
+ },
+ // fresh profile but forceFresh = true
+ {
+ // Note: The threshold for this test case is being set to an arbitrary value that will
+ // be greater than Date.now() so the retrieved cached profile will be deemed recent.
+ threshold: Date.now() + 5000,
+ expectsCachedProfileReturned: false,
+ fetchedProfile: {
+ uid: `${ACCOUNT_UID}10`,
+ email: `${ACCOUNT_EMAIL}10`,
+ avatar: "myimg10",
+ },
+ options: { forceFresh: true },
+ },
+ ];
+
+ for (const tc of testCases) {
+ print(`test case: ${JSON.stringify(tc)}`);
+ let mockProfile = sinon.mock(profile);
+ mockProfile
+ .expects("_getProfileCache")
+ .once()
+ .returns(
+ tc.cachedProfile
+ ? {
+ profile: tc.cachedProfile,
+ }
+ : null
+ );
+ profile.PROFILE_FRESHNESS_THRESHOLD = tc.threshold;
+
+ let options = tc.options || {};
+ if (tc.expectsCachedProfileReturned) {
+ mockProfile.expects("_fetchAndCacheProfile").never();
+ let actualProfile = await profile.ensureProfile(options);
+ mockProfile.verify();
+ Assert.equal(actualProfile, tc.cachedProfile);
+ } else if (tc.fetchAndCacheProfileResolves) {
+ mockProfile
+ .expects("_fetchAndCacheProfile")
+ .once()
+ .resolves(tc.fetchedProfile);
+
+ let actualProfile = await profile.ensureProfile(options);
+ let expectedProfile = tc.fetchedProfile
+ ? tc.fetchedProfile
+ : tc.cachedProfile;
+ mockProfile.verify();
+ Assert.equal(actualProfile, expectedProfile);
+ } else {
+ mockProfile.expects("_fetchAndCacheProfile").once().rejects();
+
+ let actualProfile = await profile.ensureProfile(options);
+ mockProfile.verify();
+ Assert.equal(actualProfile, tc.cachedProfile);
+ }
+ }
+});
+
+// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
+// last one *does* kick off a new request if ON_PROFILE_CHANGE_NOTIFICATION
+// is sent.
+add_task(async function fetchAndCacheProfileBeforeThresholdOnNotification() {
+ let numFetches = 0;
+ let client = mockClient(mockFxa());
+ client.fetchProfile = async function () {
+ numFetches += 1;
+ return {
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
+ };
+ };
+ let profile = CreateFxAccountsProfile(null, client);
+ profile.PROFILE_FRESHNESS_THRESHOLD = 1000;
+
+ // first fetch should return null as we don't have data.
+ let p = await profile.getProfile();
+ Assert.equal(p, null);
+ // ensure we kicked off a fetch.
+ Assert.notEqual(profile._currentFetchPromise, null);
+ // wait for that fetch to finish
+ await profile._currentFetchPromise;
+ Assert.equal(numFetches, 1);
+ Assert.equal(profile._currentFetchPromise, null);
+
+ Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
+
+ let origFetchAndCatch = profile._fetchAndCacheProfile;
+ let backgroundFetchDone = Promise.withResolvers();
+ profile._fetchAndCacheProfile = async () => {
+ await origFetchAndCatch.call(profile);
+ backgroundFetchDone.resolve();
+ };
+ await profile.getProfile();
+ await backgroundFetchDone.promise;
+ Assert.equal(numFetches, 2);
+});
+
+add_test(function tearDown_ok() {
+ let profile = CreateFxAccountsProfile();
+
+ Assert.ok(!!profile.client);
+ Assert.ok(!!profile.fxai);
+
+ profile.tearDown();
+ Assert.equal(null, profile.fxai);
+ Assert.equal(null, profile.client);
+
+ run_next_test();
+});
+
+add_task(async function getProfile_ok() {
+ let cachedUrl = "myurl";
+ let didFetch = false;
+
+ let fxa = mockFxa();
+ fxa._testProfileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl } };
+ let profile = CreateFxAccountsProfile(fxa);
+
+ profile._fetchAndCacheProfile = function () {
+ didFetch = true;
+ return Promise.resolve();
+ };
+
+ let result = await profile.getProfile();
+
+ Assert.equal(result.avatar, cachedUrl);
+ Assert.ok(didFetch);
+});
+
+add_task(async function getProfile_no_cache() {
+ let fetchedUrl = "newUrl";
+ let fxa = mockFxa();
+ let profile = CreateFxAccountsProfile(fxa);
+
+ profile._fetchAndCacheProfileInternal = function () {
+ return Promise.resolve({ uid: ACCOUNT_UID, avatar: fetchedUrl });
+ };
+
+ await profile.getProfile(); // returns null.
+ let result = await profile._currentFetchPromise;
+ Assert.equal(result.avatar, fetchedUrl);
+});
+
+add_test(function getProfile_has_cached_fetch_deleted() {
+ let cachedUrl = "myurl";
+
+ let fxa = mockFxa();
+ let client = mockClient(fxa);
+ client.fetchProfile = function () {
+ return Promise.resolve({
+ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: null },
+ });
+ };
+
+ let profile = CreateFxAccountsProfile(fxa, client);
+ fxa._testProfileCache = {
+ profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: cachedUrl },
+ };
+
+ // instead of checking this in a mocked "save" function, just check after the
+ // observer
+ makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
+ profile.getProfile().then(profileData => {
+ Assert.equal(null, profileData.avatar);
+ run_next_test();
+ });
+ });
+
+ return profile.getProfile().then(result => {
+ Assert.equal(result.avatar, "myurl");
+ });
+});
+
+add_test(function getProfile_fetchAndCacheProfile_throws() {
+ let fxa = mockFxa();
+ fxa._testProfileCache = {
+ profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" },
+ };
+ let profile = CreateFxAccountsProfile(fxa);
+
+ profile._fetchAndCacheProfile = () => Promise.reject(new Error());
+
+ return profile.getProfile().then(result => {
+ Assert.equal(result.avatar, "myimg");
+ run_next_test();
+ });
+});
+
+add_test(function getProfile_email_changed() {
+ let fxa = mockFxa();
+ let client = mockClient(fxa);
+ client.fetchProfile = function () {
+ return Promise.resolve({
+ body: { uid: ACCOUNT_UID, email: "newemail@bar.com" },
+ });
+ };
+ fxa._internal._handleEmailUpdated = email => {
+ Assert.equal(email, "newemail@bar.com");
+ run_next_test();
+ };
+
+ let profile = CreateFxAccountsProfile(fxa, client);
+ return profile._fetchAndCacheProfile();
+});
+
+function makeObserver(aObserveTopic, aObserveFunc) {
+ let callback = function (aSubject, aTopic, aData) {
+ log.debug("observed " + aTopic + " " + aData);
+ if (aTopic == aObserveTopic) {
+ removeMe();
+ aObserveFunc(aSubject, aTopic, aData);
+ }
+ };
+
+ function removeMe() {
+ log.debug("removing observer for " + aObserveTopic);
+ Services.obs.removeObserver(callback, aObserveTopic);
+ }
+
+ Services.obs.addObserver(callback, aObserveTopic);
+ return removeMe;
+}