diff --git a/browser/base/content/test/general/browser_fxa_profile_channel.html b/browser/base/content/test/general/browser_fxa_profile_channel.html index 2382eaad5e6..49a899d8075 100644 --- a/browser/base/content/test/general/browser_fxa_profile_channel.html +++ b/browser/base/content/test/general/browser_fxa_profile_channel.html @@ -11,7 +11,7 @@ detail: { id: "account_updates", message: { - command: "profile:image:change", + command: "profile:change", data: { uid: "abc123", }, diff --git a/browser/base/content/test/general/browser_fxa_profile_channel.js b/browser/base/content/test/general/browser_fxa_profile_channel.js index 0e974b074d5..40edb5575dc 100644 --- a/browser/base/content/test/general/browser_fxa_profile_channel.js +++ b/browser/base/content/test/general/browser_fxa_profile_channel.js @@ -34,7 +34,7 @@ let gTests = [ content_uri: HTTP_PATH, }); - makeObserver(FxAccountsCommon.ONPROFILE_IMAGE_CHANGE_NOTIFICATION, function (subject, topic, data) { + makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { Assert.ok(tabOpened); Assert.equal(data, "abc123"); resolve(); diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm index 58cbf1189a2..21ab33c2058 100644 --- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -26,6 +26,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthGrantClient", "resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfile", + "resource://gre/modules/FxAccountsProfile.jsm"); + // All properties exposed by the public FxAccounts API. let publicProperties = [ "accountStatus", @@ -36,6 +39,7 @@ let publicProperties = [ "getKeys", "getSignedInUser", "getOAuthToken", + "getSignedInUserProfile", "loadAndPoll", "localtimeOffsetMsec", "now", @@ -75,6 +79,7 @@ AccountState.prototype = { signedInUser: null, whenVerifiedDeferred: null, whenKeysReadyDeferred: null, + profile: null, get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this, @@ -90,10 +95,17 @@ AccountState.prototype = { new Error("Verification aborted; Another user signing in")); this.whenKeysReadyDeferred = null; } + this.cert = null; this.keyPair = null; this.signedInUser = null; this.fxaInternal = null; + this.initProfilePromise = null; + + if (this.profile) { + this.profile.tearDown(); + this.profile = null; + } }, getUserAccountData: function() { @@ -204,6 +216,41 @@ AccountState.prototype = { return d.promise.then(result => this.resolve(result)); }, + // Get the account's profile image URL from the profile server + getProfile: function () { + return this.initProfile() + .then(() => this.profile.getProfile()); + }, + + // Instantiate a FxAccountsProfile with a fresh OAuth token if needed + initProfile: function () { + + let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri"); + + let oAuthOptions = { + scope: "profile" + }; + + if (this.initProfilePromise) { + return this.initProfilePromise; + } + + this.initProfilePromise = this.fxaInternal.getOAuthToken(oAuthOptions) + .then(token => { + this.profile = new FxAccountsProfile(this, { + profileServerUrl: profileServerUrl, + token: token + }); + this.initProfilePromise = null; + }) + .then(null, err => { + this.initProfilePromise = null; + throw err; + }); + + return this.initProfilePromise; + }, + resolve: function(result) { if (!this.isCurrent) { log.info("An accountState promise was resolved, but was actually rejected" + @@ -228,7 +275,7 @@ AccountState.prototype = { return Promise.reject(error); }, -} +}; /** * Copies properties from a given object to another object. @@ -989,9 +1036,9 @@ FxAccountsInternal.prototype = { let error = SERVER_ERRNO_TO_ERROR[aError.errno]; return this._error(ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN, aError); } else if (aError.message && - aError.message === "INVALID_PARAMETER" || + (aError.message === "INVALID_PARAMETER" || aError.message === "NO_ACCOUNT" || - aError.message === "UNVERIFIED_ACCOUNT") { + aError.message === "UNVERIFIED_ACCOUNT")) { return Promise.reject(aError); } return this._error(ERROR_UNKNOWN, aError); @@ -1004,7 +1051,60 @@ FxAccountsInternal.prototype = { reason.details = aDetails; } return Promise.reject(reason); - } + }, + + /** + * Get the user's account and profile data + * + * @param options + * { + * contentUrl: (string) Used by the FxAccountsProfileChannel. + * Defaults to pref identity.fxaccounts.settings.uri + * profileServerUrl: (string) Used by the FxAccountsProfileChannel. + * Defaults to pref identity.fxaccounts.remote.profile.uri + * } + * + * @return Promise. + * The promise resolves to an accountData object with extra profile + * information such as profileImageUrl, or rejects with + * an error object ({error: ERROR, details: {}}) of the following: + * INVALID_PARAMETER + * NO_ACCOUNT + * UNVERIFIED_ACCOUNT + * NETWORK_ERROR + * AUTH_ERROR + * UNKNOWN_ERROR + */ + getSignedInUserProfile: function () { + let accountState = this.currentAccountState; + return accountState.getProfile() + .then( + (profileData) => { + let profile = JSON.parse(JSON.stringify(profileData)); + // profileData doesn't include "verified", but it must be true + // if we've gotten this far. + profile.verified = true; + return accountState.resolve(profile); + }, + (error) => { + log.error("Could not retrieve profile data", error); + + return this.getSignedInUser().then(data => { + let profile = null; + if (data) { + // If we fail to fetch the profile and have no profile cached + // we resort to using the account data for basic profile data. + profile = { + email: data.email, + uid: data.uid, + verified: data.verified + }; + } + return accountState.resolve(profile); + }); + }) + .then(null, err => this._errorToErrorClass(err)); + }, }; /** diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js index 9f518eea0ec..1c940912285 100644 --- a/services/fxaccounts/FxAccountsCommon.js +++ b/services/fxaccounts/FxAccountsCommon.js @@ -90,7 +90,7 @@ exports.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout"; // Internal to services/fxaccounts only exports.ON_FXA_UPDATE_NOTIFICATION = "fxaccounts:update"; -exports.ONPROFILE_IMAGE_CHANGE_NOTIFICATION = "fxaccounts:profileimagechange"; +exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange"; // UI Requests. exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow"; diff --git a/services/fxaccounts/FxAccountsProfile.jsm b/services/fxaccounts/FxAccountsProfile.jsm new file mode 100644 index 00000000000..42d0f751156 --- /dev/null +++ b/services/fxaccounts/FxAccountsProfile.jsm @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Firefox Accounts Profile helper. + * + * This class abstracts interaction with the profile server for an account. + * It will handle things like fetching profile data, listening for updates to + * the user's profile in open browser tabs, and cacheing/invalidating profile data. + */ + +this.EXPORTED_SYMBOLS = ["FxAccountsProfile"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient", + "resource://gre/modules/FxAccountsProfileClient.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileChannel", + "resource://gre/modules/FxAccountsProfileChannel.jsm"); + +let fxAccountProfileChannel = null; + +// Based off of deepEqual from Assert.jsm +function deepEqual(actual, expected) { + if (actual === expected) { + return true; + } else if (typeof actual != "object" && typeof expected != "object") { + return actual == expected; + } else { + return objEquiv(actual, expected); + } +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { + return false; + } + if (a.prototype !== b.prototype) { + return false; + } + let ka, kb, key, i; + try { + ka = Object.keys(a); + kb = Object.keys(b); + } catch (e) { + return false; + } + if (ka.length != kb.length) { + return false; + } + ka.sort(); + kb.sort(); + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +function hasChanged(oldData, newData) { + return !deepEqual(oldData, newData); +} + +this.FxAccountsProfile = function (accountState, options = {}) { + this.currentAccountState = accountState; + this.client = options.profileClient || new FxAccountsProfileClient({ + serverURL: options.profileServerUrl, + token: options.token + }); + + // for testing + if (options.channel) { + this.channel = options.channel; + } +} + +this.FxAccountsProfile.prototype = { + + tearDown: function () { + this.currentAccountState = null; + this.client = null; + }, + + _getCachedProfile: function () { + let currentState = this.currentAccountState; + return currentState.getUserAccountData() + .then(cachedData => cachedData.profile); + }, + + _notifyProfileChange: function (uid) { + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid); + }, + + // Cache fetched data if it is different from what's in the cache. + // Send out a notification if it has changed so that UI can update. + _cacheProfile: function (profileData) { + let currentState = this.currentAccountState; + if (!currentState) { + return; + } + return currentState.getUserAccountData() + .then(data => { + if (!hasChanged(data.profile, profileData)) { + return; + } + data.profile = profileData; + return currentState.setUserAccountData(data) + .then(() => this._notifyProfileChange(data.uid)); + }); + }, + + _fetchAndCacheProfile: function () { + return this.client.fetchProfile() + .then(profile => { + return this._cacheProfile(profile).then(() => profile); + }); + }, + + // Initialize a profile channel to listen for account changes. + _listenForProfileChanges: function () { + if (! fxAccountProfileChannel) { + let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri"); + + fxAccountProfileChannel = new FxAccountsProfileChannel({ + content_uri: contentUri + }); + } + + return fxAccountProfileChannel; + }, + + // Returns cached data right away if available, then fetches the latest profile + // data in the background. After data is fetched a notification will be sent + // out if the profile has changed. + getProfile: function () { + this._listenForProfileChanges(); + + return this._getCachedProfile() + .then(cachedProfile => { + if (cachedProfile) { + this._fetchAndCacheProfile(); + return cachedProfile; + } + return this._fetchAndCacheProfile(); + }) + .then(profile => { + return profile; + }); + }, +}; diff --git a/services/fxaccounts/FxAccountsProfileChannel.jsm b/services/fxaccounts/FxAccountsProfileChannel.jsm index 797729da857..124bb42b0df 100644 --- a/services/fxaccounts/FxAccountsProfileChannel.jsm +++ b/services/fxaccounts/FxAccountsProfileChannel.jsm @@ -19,7 +19,7 @@ Cu.import("resource://gre/modules/FxAccountsCommon.js"); XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", "resource://gre/modules/WebChannel.jsm"); -const PROFILE_IMAGE_CHANGE_COMMAND = "profile:image:change"; +const PROFILE_CHANGE_COMMAND = "profile:change"; /** * Create a new FxAccountsProfileChannel to listen to profile updates @@ -102,8 +102,8 @@ this.FxAccountsProfileChannel.prototype = { let command = message.command; let data = message.data; switch (command) { - case PROFILE_IMAGE_CHANGE_COMMAND: - Services.obs.notifyObservers(null, ONPROFILE_IMAGE_CHANGE_NOTIFICATION, data.uid); + case PROFILE_CHANGE_COMMAND: + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid); break; } } diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build index 5ee8f22ac6e..a7b955b37f8 100644 --- a/services/fxaccounts/moz.build +++ b/services/fxaccounts/moz.build @@ -16,6 +16,7 @@ EXTRA_JS_MODULES += [ 'FxAccountsCommon.js', 'FxAccountsOAuthClient.jsm', 'FxAccountsOAuthGrantClient.jsm', + 'FxAccountsProfile.jsm', 'FxAccountsProfileChannel.jsm', 'FxAccountsProfileClient.jsm', ] diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js index bc3dbe3080f..324a4caa932 100644 --- a/services/fxaccounts/tests/xpcshell/test_accounts.js +++ b/services/fxaccounts/tests/xpcshell/test_accounts.js @@ -32,6 +32,13 @@ Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://exam Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123"); +const PROFILE_SERVER_URL = "http://example.com/v1"; +const CONTENT_URL = "http://accounts.example.com/"; + +Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", PROFILE_SERVER_URL); +Services.prefs.setCharPref("identity.fxaccounts.settings.uri", CONTENT_URL); + + function run_test() { run_next_test(); } @@ -855,6 +862,123 @@ add_test(function test_getOAuthToken_unknown_error() { }); }); +add_test(function test_accountState_initProfile() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.internal.getOAuthToken = function (opts) { + return Promise.resolve("token"); + }; + + fxa.setSignedInUser(alice).then(() => { + let accountState = fxa.internal.currentAccountState; + + accountState.initProfile(options) + .then(result => { + do_check_true(!!accountState.profile); + run_next_test(); + }); + }); + +}); + +add_test(function test_accountState_getProfile() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + let mockProfile = { + getProfile: function () { + return Promise.resolve({ avatar: "image" }); + } + }; + + fxa.setSignedInUser(alice).then(() => { + let accountState = fxa.internal.currentAccountState; + accountState.profile = mockProfile; + accountState.initProfilePromise = new Promise((resolve, reject) => resolve(mockProfile)); + + accountState.getProfile() + .then(result => { + do_check_true(!!result); + do_check_eq(result.avatar, "image"); + run_next_test(); + }); + }); + +}); + +add_test(function test_getSignedInUserProfile_ok() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.setSignedInUser(alice).then(() => { + let accountState = fxa.internal.currentAccountState; + accountState.getProfile = function () { + return Promise.resolve({ avatar: "image" }); + }; + + fxa.getSignedInUserProfile() + .then(result => { + do_check_eq(result.avatar, "image"); + do_check_true(result.verified); + run_next_test(); + }); + }); + +}); + +add_test(function test_getSignedInUserProfile_error_uses_account_data() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.internal.getSignedInUser = function () { + return Promise.resolve({ email: "foo@bar.com" }); + }; + + fxa.setSignedInUser(alice).then(() => { + let accountState = fxa.internal.currentAccountState; + accountState.getProfile = function () { + return Promise.reject("boom"); + }; + + fxa.getSignedInUserProfile() + .then(result => { + do_check_eq(typeof result.avatar, "undefined"); + do_check_eq(result.email, "foo@bar.com"); + run_next_test(); + }); + }); + +}); + +add_test(function test_getSignedInUserProfile_no_account_data() { + let fxa = new MockFxAccounts(); + + fxa.internal.getSignedInUser = function () { + return Promise.resolve({ email: "foo@bar.com" }); + }; + + let accountState = fxa.internal.currentAccountState; + accountState.getProfile = function () { + return Promise.reject("boom"); + }; + + fxa.internal.getSignedInUser = function () { + return Promise.resolve(null); + }; + + fxa.getSignedInUserProfile() + .then(result => { + do_check_eq(result, null); + run_next_test(); + }); + +}); + /* * End of tests. * Utility functions follow. diff --git a/services/fxaccounts/tests/xpcshell/test_profile.js b/services/fxaccounts/tests/xpcshell/test_profile.js new file mode 100644 index 00000000000..2aef10d55b8 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_profile.js @@ -0,0 +1,286 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm"); +Cu.import("resource://gre/modules/FxAccountsProfile.jsm"); + +const URL_STRING = "https://example.com"; +Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "https://example.com/settings"); + +const PROFILE_CLIENT_OPTIONS = { + token: "123ABC", + serverURL: "http://127.0.0.1:1111/v1", + profileServerUrl: "http://127.0.0.1:1111/v1" +}; + +const STATUS_SUCCESS = 200; + +/** + * Mock request responder + * @param {String} response + * Mocked raw response from the server + * @returns {Function} + */ +let mockResponse = function (response) { + let Request = function (requestUri) { + // Store the request uri so tests can inspect it + Request._requestUri = requestUri; + return { + setHeader: function () {}, + head: function () { + this.response = response; + this.onComplete(); + } + }; + }; + + return Request; +}; + +/** + * Mock request error responder + * @param {Error} error + * Error object + * @returns {Function} + */ +let mockResponseError = function (error) { + return function () { + return { + setHeader: function () {}, + head: function () { + this.onComplete(error); + } + }; + }; +}; + +let mockClient = function () { + let client = new FxAccountsProfileClient(PROFILE_CLIENT_OPTIONS); + return client; +}; + +const ACCOUNT_DATA = { + uid: "abc123" +}; + +function AccountData () { +} +AccountData.prototype = { + getUserAccountData: function () { + return Promise.resolve(ACCOUNT_DATA); + } +}; + +let mockAccountData = function () { + return new AccountData(); +}; + +add_test(function getCachedProfile() { + let accountData = mockAccountData(); + accountData.getUserAccountData = function () { + return Promise.resolve({ + profile: { avatar: "myurl" } + }); + }; + let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS); + + return profile._getCachedProfile() + .then(function (cached) { + do_check_eq(cached.avatar, "myurl"); + run_next_test(); + }); +}); + +add_test(function cacheProfile_change() { + let accountData = mockAccountData(); + let setUserAccountDataCalled = false; + accountData.setUserAccountData = function (data) { + setUserAccountDataCalled = true; + do_check_eq(data.profile.avatar, "myurl"); + return Promise.resolve(); + }; + let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS); + + makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { + do_check_eq(data, ACCOUNT_DATA.uid); + do_check_true(setUserAccountDataCalled); + run_next_test(); + }); + + return profile._cacheProfile({ avatar: "myurl" }); +}); + +add_test(function cacheProfile_no_change() { + let accountData = mockAccountData(); + accountData.getUserAccountData = function () { + return Promise.resolve({ + profile: { avatar: "myurl" } + }); + }; + accountData.setUserAccountData = function (data) { + throw new Error("should not update account data"); + }; + let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS); + + return profile._cacheProfile({ avatar: "myurl" }) + .then((result) => { + do_check_false(!!result); + run_next_test(); + }); +}); + +add_test(function fetchAndCacheProfile_ok() { + let client = mockClient(); + client.fetchProfile = function () { + return Promise.resolve({ avatar: "myimg"}); + }; + let profile = new FxAccountsProfile(mockAccountData(), { + profileClient: client + }); + + profile._cacheProfile = function (toCache) { + do_check_eq(toCache.avatar, "myimg"); + return Promise.resolve(); + }; + + return profile._fetchAndCacheProfile() + .then(result => { + do_check_eq(result.avatar, "myimg"); + run_next_test(); + }); +}); + + +add_test(function profile_channel() { + let profile = new FxAccountsProfile(mockAccountData(), PROFILE_CLIENT_OPTIONS); + + let channel = profile._listenForProfileChanges(); + do_check_true(!!channel); + + let channel2 = profile._listenForProfileChanges(); + + do_check_eq(channel, channel2); + + run_next_test(); +}); + +add_test(function tearDown_ok() { + let profile = new FxAccountsProfile(mockAccountData(), PROFILE_CLIENT_OPTIONS); + + do_check_true(!!profile.client); + do_check_true(!!profile.currentAccountState); + + profile.tearDown(); + do_check_null(profile.currentAccountState); + do_check_null(profile.client); + + run_next_test(); +}); + +add_test(function getProfile_ok() { + let cachedUrl = "myurl"; + let accountData = mockAccountData(); + let didFetch = false; + let didListen = false; + + let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS); + profile._getCachedProfile = function () { + return Promise.resolve({ avatar: cachedUrl }); + }; + + profile._fetchAndCacheProfile = function () { + didFetch = true; + }; + profile._listenForProfileChanges = function () { + didListen = true; + }; + + return profile.getProfile() + .then(result => { + do_check_eq(result.avatar, cachedUrl); + do_check_true(didFetch); + do_check_true(didListen); + run_next_test(); + }); +}); + +add_test(function getProfile_no_cache() { + let fetchedUrl = "newUrl"; + let accountData = mockAccountData(); + let didListen = false; + + let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS); + profile._getCachedProfile = function () { + return Promise.resolve(); + }; + + profile._fetchAndCacheProfile = function () { + return Promise.resolve({ avatar: fetchedUrl }); + }; + profile._listenForProfileChanges = function () { + didListen = true; + }; + + return profile.getProfile() + .then(result => { + do_check_eq(result.avatar, fetchedUrl); + do_check_true(didListen); + run_next_test(); + }); +}); + +add_test(function getProfile_has_cached_fetch_deleted() { + let cachedUrl = "myurl"; + let didFetch = false; + let didListen = false; + + let client = mockClient(); + client.fetchProfile = function () { + return Promise.resolve({ avatar: null }); + }; + + let accountData = mockAccountData(); + accountData.getUserAccountData = function () { + return Promise.resolve({ profile: { avatar: cachedUrl } }); + }; + accountData.setUserAccountData = function (data) { + do_check_null(data.profile.avatar); + run_next_test(); + return Promise.resolve(); + }; + + let profile = new FxAccountsProfile(accountData, { + profileClient: client + }); + + return profile.getProfile() + .then(result => { + do_check_eq(result.avatar, "myurl"); + }); +}); + +function run_test() { + run_next_test(); +} + +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, false); + return removeMe; +} diff --git a/services/fxaccounts/tests/xpcshell/test_profile_channel.js b/services/fxaccounts/tests/xpcshell/test_profile_channel.js index 604b896768a..cbe18562639 100644 --- a/services/fxaccounts/tests/xpcshell/test_profile_channel.js +++ b/services/fxaccounts/tests/xpcshell/test_profile_channel.js @@ -23,11 +23,11 @@ add_test(function () { add_test(function () { var mockMessage = { - command: "profile:image:change", + command: "profile:change", data: { uid: "foo" } }; - makeObserver(ONPROFILE_IMAGE_CHANGE_NOTIFICATION, function (subject, topic, data) { + makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { do_check_eq(data, "foo"); run_next_test(); }); diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini index d49ac152f46..c01b9902336 100644 --- a/services/fxaccounts/tests/xpcshell/xpcshell.ini +++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini @@ -16,3 +16,4 @@ reason = FxAccountsManager is only available for B2G for now [test_oauth_grant_client.js] [test_profile_client.js] [test_profile_channel.js] +[test_profile.js]