Bug 1171253 - enable FxA profile image in Sync preferences pane. r=zaach

This commit is contained in:
Mark Hammond 2015-06-18 19:28:11 +10:00
parent 771a015604
commit 6a05686b86
7 changed files with 370 additions and 216 deletions

View File

@ -1829,6 +1829,9 @@ pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox
// The remote URL of the FxA OAuth Server
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
// Whether we display profile images in the UI or not.
pref("identity.fxaccounts.profile_image.enabled", true);
// Migrate any existing Firefox Account data from the default profile to the
// Developer Edition profile.
#ifdef MOZ_DEV_EDITION

View File

@ -111,12 +111,6 @@ AccountState.prototype = {
this.signedInUser = null;
this.uid = null;
this.fxaInternal = null;
this.initProfilePromise = null;
if (this.profile) {
this.profile.tearDown();
this.profile = null;
}
},
// Clobber all cached data and write that empty data to storage.
@ -294,41 +288,6 @@ 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" +
@ -594,6 +553,19 @@ FxAccountsInternal.prototype = {
return this._fxAccountsClient;
},
// The profile object used to fetch the actual user profile.
_profile: null,
get profile() {
if (!this._profile) {
let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri");
this._profile = new FxAccountsProfile({
fxa: this,
profileServerUrl: profileServerUrl,
});
}
return this._profile;
},
/**
* Return the current time in milliseconds as an integer. Allows tests to
* manipulate the date to simulate certificate expiration.
@ -849,6 +821,10 @@ FxAccountsInternal.prototype = {
*/
_signOutLocal: function signOutLocal() {
let currentAccountState = this.currentAccountState;
if (this._profile) {
this._profile.tearDown();
this._profile = null;
}
return currentAccountState.signOut().then(() => {
this.abortExistingFlow(); // this resets this.currentAccountState.
});
@ -1430,17 +1406,17 @@ FxAccountsInternal.prototype = {
* UNKNOWN_ERROR
*/
getSignedInUserProfile: function () {
let accountState = this.currentAccountState;
return accountState.getProfile()
.then((profileData) => {
let currentState = this.currentAccountState;
return this.profile.getProfile().then(
profileData => {
let profile = JSON.parse(JSON.stringify(profileData));
return accountState.resolve(profile);
return currentState.resolve(profile);
},
(error) => {
error => {
log.error("Could not retrieve profile data", error);
return accountState.reject(error);
})
.then(null, err => Promise.reject(this._errorToErrorClass(err)));
return currentState.reject(error);
}
).catch(err => Promise.reject(this._errorToErrorClass(err)));
},
};

View File

@ -17,9 +17,9 @@ 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");
Cu.import("resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
"resource://gre/modules/FxAccountsProfileClient.jsm");
@ -71,11 +71,12 @@ function hasChanged(oldData, newData) {
return !deepEqual(oldData, newData);
}
this.FxAccountsProfile = function (accountState, options = {}) {
this.currentAccountState = accountState;
this.FxAccountsProfile = function (options = {}) {
this._cachedProfile = null;
this.fxa = options.fxa || fxAccounts;
this.client = options.profileClient || new FxAccountsProfileClient({
fxa: this.fxa,
serverURL: options.profileServerUrl,
token: options.token
});
// for testing
@ -87,14 +88,15 @@ this.FxAccountsProfile = function (accountState, options = {}) {
this.FxAccountsProfile.prototype = {
tearDown: function () {
this.currentAccountState = null;
this.fxa = null;
this.client = null;
this._cachedProfile = null;
},
_getCachedProfile: function () {
let currentState = this.currentAccountState;
return currentState.getUserAccountData()
.then(cachedData => cachedData.profile);
// The cached profile will end up back in the generic accountData
// once bug 1157529 is fixed.
return Promise.resolve(this._cachedProfile);
},
_notifyProfileChange: function (uid) {
@ -104,18 +106,16 @@ this.FxAccountsProfile.prototype = {
// 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;
if (!hasChanged(this._cachedProfile, profileData)) {
log.debug("fetched profile matches cached copy");
return Promise.resolve(null); // indicates no change (but only tests care)
}
return currentState.getUserAccountData()
.then(data => {
if (!hasChanged(data.profile, profileData)) {
return;
}
data.profile = profileData;
return currentState.setUserAccountData(data)
.then(() => this._notifyProfileChange(data.uid));
this._cachedProfile = profileData;
return this.fxa.getSignedInUser()
.then(userData => {
log.debug("notifying profile changed for user ${uid}", userData);
this._notifyProfileChange(userData.uid);
return profileData;
});
},
@ -133,7 +133,11 @@ this.FxAccountsProfile.prototype = {
return this._getCachedProfile()
.then(cachedProfile => {
if (cachedProfile) {
this._fetchAndCacheProfile();
// Note that _fetchAndCacheProfile isn't returned, so continues
// in the background.
this._fetchAndCacheProfile().catch(err => {
log.error("Background refresh of profile failed", err);
});
return cachedProfile;
}
return this._fetchAndCacheProfile();

View File

@ -5,6 +5,7 @@
/**
* A client to fetch profile information for a Firefox Account.
*/
"use strict;"
this.EXPORTED_SYMBOLS = ["FxAccountsProfileClient", "FxAccountsProfileClientError"];
@ -13,6 +14,8 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-common/rest.js");
Cu.importGlobalProperties(["URL"]);
@ -29,16 +32,27 @@ Cu.importGlobalProperties(["URL"]);
* @constructor
*/
this.FxAccountsProfileClient = function(options) {
if (!options || !options.serverURL || !options.token) {
throw new Error("Missing 'serverURL' or 'token' configuration option");
if (!options || !options.serverURL) {
throw new Error("Missing 'serverURL' configuration option");
}
this.fxa = options.fxa || fxAccounts;
// This is a work-around for loop that manages its own oauth tokens.
// * If |token| is in options we use it and don't attempt any token refresh
// on 401. This is for loop.
// * If |token| doesn't exist we will fetch our own token. This is for the
// normal FxAccounts methods for obtaining the profile.
// We should nuke all |this.token| support once loop moves closer to FxAccounts.
this.token = options.token;
try {
this.serverURL = new URL(options.serverURL);
} catch (e) {
throw new Error("Invalid 'serverURL'");
}
this.token = options.token;
this.oauthOptions = {
scope: "profile",
};
log.debug("FxAccountsProfileClient: Initialized");
};
@ -49,19 +63,13 @@ this.FxAccountsProfileClient.prototype = {
*/
serverURL: null,
/**
* {String}
* Profile server bearer OAuth token.
*/
token: null,
/**
* Interface for making remote requests.
*/
_Request: RESTRequest,
/**
* Remote request helper
* Remote request helper which abstracts authentication away.
*
* @param {String} path
* Profile server path, i.e "/profile".
@ -72,13 +80,55 @@ this.FxAccountsProfileClient.prototype = {
* Rejects: {FxAccountsProfileClientError} Profile client error.
* @private
*/
_createRequest: function(path, method = "GET") {
_createRequest: Task.async(function* (path, method = "GET") {
let token = this.token;
if (!token) {
// tokens are cached, so getting them each request is cheap.
token = yield this.fxa.getOAuthToken(this.oauthOptions);
}
try {
return (yield this._rawRequest(path, method, token));
} catch (ex if ex instanceof FxAccountsProfileClientError && ex.code == 401) {
// If this object was instantiated with a token then we don't refresh it.
if (this.token) {
throw ex;
}
// it's an auth error - assume our token expired and retry.
log.info("Fetching the profile returned a 401 - revoking our token and retrying");
yield this.fxa.removeCachedOAuthToken({token});
token = yield this.fxa.getOAuthToken(this.oauthOptions);
// and try with the new token - if that also fails then we fail after
// revoking the token.
try {
return (yield this._rawRequest(path, method, token));
} catch (ex if ex instanceof FxAccountsProfileClientError && ex.code == 401) {
log.info("Retry fetching the profile still returned a 401 - revoking our token and failing");
yield this.fxa.removeCachedOAuthToken({token});
throw ex;
}
}
}),
/**
* Remote "raw" request helper - doesn't handle auth errors and tokens.
*
* @param {String} path
* Profile server path, i.e "/profile".
* @param {String} method
* Type of request, i.e "GET".
* @param {String} token
* @return Promise
* Resolves: {Object} Successful response from the Profile server.
* Rejects: {FxAccountsProfileClientError} Profile client error.
* @private
*/
_rawRequest: function(path, method, token) {
return new Promise((resolve, reject) => {
let profileDataUrl = this.serverURL + path;
let request = new this._Request(profileDataUrl);
method = method.toUpperCase();
request.setHeader("Authorization", "Bearer " + this.token);
request.setHeader("Authorization", "Bearer " + token);
request.setHeader("Accept", "application/json");
request.onComplete = function (error) {
@ -106,7 +156,12 @@ this.FxAccountsProfileClient.prototype = {
if (request.response.success) {
return resolve(body);
} else {
return reject(new FxAccountsProfileClientError(body));
return reject(new FxAccountsProfileClientError({
error: body.error || ERROR_UNKNOWN,
errno: body.errno || ERRNO_UNKNOWN_ERROR,
code: request.response.status,
message: body.message || body,
}));
}
};

View File

@ -960,29 +960,7 @@ 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();
add_test(function test_getSignedInUserProfile() {
let alice = getTestUser("alice");
alice.verified = true;
@ -991,40 +969,18 @@ add_test(function test_accountState_getProfile() {
return Promise.resolve({ avatar: "image" });
}
};
let fxa = new FxAccounts({
_profile: mockProfile,
});
fxa.setSignedInUser(alice).then(() => {
let accountState = fxa.internal.currentAccountState;
accountState.profile = mockProfile;
accountState.initProfilePromise = new Promise((resolve, reject) => resolve(mockProfile));
accountState.getProfile()
fxa.getSignedInUserProfile()
.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");
run_next_test();
});
});
});
add_test(function test_getSignedInUserProfile_error_uses_account_data() {
@ -1036,19 +992,26 @@ add_test(function test_getSignedInUserProfile_error_uses_account_data() {
return Promise.resolve({ email: "foo@bar.com" });
};
let teardownCalled = false;
fxa.setSignedInUser(alice).then(() => {
let accountState = fxa.internal.currentAccountState;
accountState.getProfile = function () {
return Promise.reject("boom");
fxa.internal._profile = {
getProfile: function () {
return Promise.reject("boom");
},
tearDown: function() {
teardownCalled = true;
}
};
fxa.getSignedInUserProfile()
.catch(error => {
do_check_eq(error.message, "UNKNOWN_ERROR");
fxa.signOut().then(run_next_test);
do_check_eq(error.message, "UNKNOWN_ERROR");
fxa.signOut().then(() => {
do_check_true(teardownCalled);
run_next_test();
});
});
});
});
add_test(function test_getSignedInUserProfile_unverified_account() {
@ -1056,8 +1019,6 @@ add_test(function test_getSignedInUserProfile_unverified_account() {
let alice = getTestUser("alice");
fxa.setSignedInUser(alice).then(() => {
let accountState = fxa.internal.currentAccountState;
fxa.getSignedInUserProfile()
.catch(error => {
do_check_eq(error.message, "UNVERIFIED_ACCOUNT");

View File

@ -11,12 +11,6 @@ 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;
/**
@ -58,35 +52,53 @@ let mockResponseError = function (error) {
};
};
let mockClient = function () {
let client = new FxAccountsProfileClient(PROFILE_CLIENT_OPTIONS);
return client;
let mockClient = function (fxa) {
let options = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: fxa,
}
return new FxAccountsProfileClient(options);
};
const ACCOUNT_DATA = {
uid: "abc123"
};
function AccountData () {
function FxaMock() {
}
AccountData.prototype = {
getUserAccountData: function () {
FxaMock.prototype = {
currentAccountState: {
profile: null,
get isCurrent() true,
},
getSignedInUser: function () {
return Promise.resolve(ACCOUNT_DATA);
}
};
let mockAccountData = function () {
return new AccountData();
let mockFxa = function() {
return new FxaMock();
};
function CreateFxAccountsProfile(fxa = null, client = null) {
if (!fxa) {
fxa = mockFxa();
}
let options = {
fxa: fxa,
profileServerUrl: "http://127.0.0.1:1111/v1"
}
if (client) {
options.profileClient = client;
}
return new FxAccountsProfile(options);
}
add_test(function getCachedProfile() {
let accountData = mockAccountData();
accountData.getUserAccountData = function () {
return Promise.resolve({
profile: { avatar: "myurl" }
});
};
let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
let profile = CreateFxAccountsProfile();
// a little pointless until bug 1157529 is fixed...
profile._cachedProfile = { avatar: "myurl" };
return profile._getCachedProfile()
.then(function (cached) {
@ -96,18 +108,20 @@ add_test(function getCachedProfile() {
});
add_test(function cacheProfile_change() {
let accountData = mockAccountData();
let fxa = mockFxa();
/* Saving profile data disabled - bug 1157529
let setUserAccountDataCalled = false;
accountData.setUserAccountData = function (data) {
fxa.setUserAccountData = function (data) {
setUserAccountDataCalled = true;
do_check_eq(data.profile.avatar, "myurl");
return Promise.resolve();
};
let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
*/
let profile = CreateFxAccountsProfile(fxa);
makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
do_check_eq(data, ACCOUNT_DATA.uid);
do_check_true(setUserAccountDataCalled);
// do_check_true(setUserAccountDataCalled); - bug 1157529
run_next_test();
});
@ -115,16 +129,14 @@ add_test(function cacheProfile_change() {
});
add_test(function cacheProfile_no_change() {
let accountData = mockAccountData();
accountData.getUserAccountData = function () {
return Promise.resolve({
profile: { avatar: "myurl" }
});
};
accountData.setUserAccountData = function (data) {
let fxa = mockFxa();
let profile = CreateFxAccountsProfile(fxa)
profile._cachedProfile = { avatar: "myurl" };
// XXX - saving is disabled (but we can leave that in for now as we are
// just checking it is *not* called)
fxa.setSignedInUser = 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) => {
@ -134,13 +146,11 @@ add_test(function cacheProfile_no_change() {
});
add_test(function fetchAndCacheProfile_ok() {
let client = mockClient();
let client = mockClient(mockFxa());
client.fetchProfile = function () {
return Promise.resolve({ avatar: "myimg"});
};
let profile = new FxAccountsProfile(mockAccountData(), {
profileClient: client
});
let profile = CreateFxAccountsProfile(null, client);
profile._cacheProfile = function (toCache) {
do_check_eq(toCache.avatar, "myimg");
@ -155,13 +165,13 @@ add_test(function fetchAndCacheProfile_ok() {
});
add_test(function tearDown_ok() {
let profile = new FxAccountsProfile(mockAccountData(), PROFILE_CLIENT_OPTIONS);
let profile = CreateFxAccountsProfile();
do_check_true(!!profile.client);
do_check_true(!!profile.currentAccountState);
do_check_true(!!profile.fxa);
profile.tearDown();
do_check_null(profile.currentAccountState);
do_check_null(profile.fxa);
do_check_null(profile.client);
run_next_test();
@ -169,16 +179,16 @@ add_test(function tearDown_ok() {
add_test(function getProfile_ok() {
let cachedUrl = "myurl";
let accountData = mockAccountData();
let didFetch = false;
let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
let profile = CreateFxAccountsProfile();
profile._getCachedProfile = function () {
return Promise.resolve({ avatar: cachedUrl });
};
profile._fetchAndCacheProfile = function () {
didFetch = true;
return Promise.resolve();
};
return profile.getProfile()
@ -191,9 +201,7 @@ add_test(function getProfile_ok() {
add_test(function getProfile_no_cache() {
let fetchedUrl = "newUrl";
let accountData = mockAccountData();
let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
let profile = CreateFxAccountsProfile();
profile._getCachedProfile = function () {
return Promise.resolve();
};
@ -212,23 +220,23 @@ add_test(function getProfile_no_cache() {
add_test(function getProfile_has_cached_fetch_deleted() {
let cachedUrl = "myurl";
let client = mockClient();
let fxa = mockFxa();
let client = mockClient(fxa);
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 = CreateFxAccountsProfile(fxa, client);
profile._cachedProfile = { avatar: cachedUrl };
let profile = new FxAccountsProfile(accountData, {
profileClient: client
// 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 => {
do_check_null(profileData.avatar);
run_next_test();
});
});
return profile.getProfile()

View File

@ -6,11 +6,6 @@
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
const PROFILE_OPTIONS = {
token: "123ABC",
serverURL: "http://127.0.0.1:1111/v1",
};
const STATUS_SUCCESS = 200;
/**
@ -35,6 +30,21 @@ let mockResponse = function (response) {
return Request;
};
// A simple mock FxA that hands out tokens without checking them and doesn't
// expect tokens to be revoked. We have specific token tests further down that
// has more checks here.
let mockFxa = {
getOAuthToken(options) {
do_check_eq(options.scope, "profile");
return "token";
}
}
const PROFILE_OPTIONS = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: mockFxa,
};
/**
* Mock request error responder
* @param {Error} error
@ -98,8 +108,8 @@ add_test(function parseErrorResponse () {
add_test(function serverErrorResponse () {
let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
let response = {
status: 401,
body: "{ \"code\": 401, \"errno\": 100, \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Bearer token not provided\" }",
status: 500,
body: "{ \"code\": 500, \"errno\": 100, \"error\": \"Bad Request\", \"message\": \"Something went wrong\", \"reason\": \"Because the internet\" }",
};
client._Request = new mockResponse(response);
@ -108,10 +118,149 @@ add_test(function serverErrorResponse () {
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, 401);
do_check_eq(e.code, 500);
do_check_eq(e.errno, 100);
do_check_eq(e.error, "Bad Request");
do_check_eq(e.message, "Unauthorized");
do_check_eq(e.message, "Something went wrong");
run_next_test();
}
);
});
// Test that we get a token, then if we get a 401 we revoke it, get a new one
// and retry.
add_test(function server401ResponseThenSuccess () {
// The last token we handed out.
let lastToken = -1;
// The number of times our removeCachedOAuthToken function was called.
let numTokensRemoved = 0;
let mockFxa = {
getOAuthToken(options) {
do_check_eq(options.scope, "profile");
return "" + ++lastToken; // tokens are strings.
},
removeCachedOAuthToken(options) {
// This test never has more than 1 token alive at once, so the token
// being revoked must always be the last token we handed out.
do_check_eq(parseInt(options.token), lastToken);
++numTokensRemoved;
}
}
let profileOptions = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: mockFxa,
};
let client = new FxAccountsProfileClient(profileOptions);
// 2 responses - first one implying the token has expired, second works.
let responses = [
{
status: 401,
body: "{ \"code\": 401, \"errno\": 100, \"error\": \"Token expired\", \"message\": \"That token is too old\", \"reason\": \"Because security\" }",
},
{
success: true,
status: STATUS_SUCCESS,
body: "{\"avatar\":\"http://example.com/image.jpg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}",
},
];
let numRequests = 0;
let numAuthHeaders = 0;
// Like mockResponse but we want access to headers etc.
client._Request = function(requestUri) {
return {
setHeader: function (name, value) {
if (name == "Authorization") {
numAuthHeaders++;
do_check_eq(value, "Bearer " + lastToken);
}
},
get: function () {
this.response = responses[numRequests];
++numRequests;
this.onComplete();
}
};
}
client.fetchProfile()
.then(result => {
do_check_eq(result.avatar, "http://example.com/image.jpg");
do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a");
// should have been exactly 2 requests and exactly 2 auth headers.
do_check_eq(numRequests, 2);
do_check_eq(numAuthHeaders, 2);
// and we should have seen one token revoked.
do_check_eq(numTokensRemoved, 1);
run_next_test();
}
);
});
// Test that we get a token, then if we get a 401 we revoke it, get a new one
// and retry - but we *still* get a 401 on the retry, so the caller sees that.
add_test(function server401ResponsePersists () {
// The last token we handed out.
let lastToken = -1;
// The number of times our removeCachedOAuthToken function was called.
let numTokensRemoved = 0;
let mockFxa = {
getOAuthToken(options) {
do_check_eq(options.scope, "profile");
return "" + ++lastToken; // tokens are strings.
},
removeCachedOAuthToken(options) {
// This test never has more than 1 token alive at once, so the token
// being revoked must always be the last token we handed out.
do_check_eq(parseInt(options.token), lastToken);
++numTokensRemoved;
}
}
let profileOptions = {
serverURL: "http://127.0.0.1:1111/v1",
fxa: mockFxa,
};
let client = new FxAccountsProfileClient(profileOptions);
let response = {
status: 401,
body: "{ \"code\": 401, \"errno\": 100, \"error\": \"It's not your token, it's you!\", \"message\": \"I don't like you\", \"reason\": \"Because security\" }",
};
let numRequests = 0;
let numAuthHeaders = 0;
client._Request = function(requestUri) {
return {
setHeader: function (name, value) {
if (name == "Authorization") {
numAuthHeaders++;
do_check_eq(value, "Bearer " + lastToken);
}
},
get: function () {
this.response = response;
++numRequests;
this.onComplete();
}
};
}
client.fetchProfile().then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsProfileClientError");
do_check_eq(e.code, 401);
do_check_eq(e.errno, 100);
do_check_eq(e.error, "It's not your token, it's you!");
// should have been exactly 2 requests and exactly 2 auth headers.
do_check_eq(numRequests, 2);
do_check_eq(numAuthHeaders, 2);
// and we should have seen both tokens revoked.
do_check_eq(numTokensRemoved, 2);
run_next_test();
}
);
@ -119,8 +268,8 @@ add_test(function serverErrorResponse () {
add_test(function networkErrorResponse () {
let client = new FxAccountsProfileClient({
token: "123ABC",
serverURL: "http://"
serverURL: "http://",
fxa: mockFxa,
});
client.fetchProfile()
.then(
@ -191,18 +340,12 @@ add_test(function fetchProfileImage_successfulResponse () {
add_test(function constructorTests() {
validationHelper(undefined,
"Error: Missing 'serverURL' or 'token' configuration option");
"Error: Missing 'serverURL' configuration option");
validationHelper({},
"Error: Missing 'serverURL' or 'token' configuration option");
"Error: Missing 'serverURL' configuration option");
validationHelper({ serverURL: "http://example.com" },
"Error: Missing 'serverURL' or 'token' configuration option");
validationHelper({ token: "123ABC" },
"Error: Missing 'serverURL' or 'token' configuration option");
validationHelper({ token: "123ABC", serverURL: "badUrl" },
validationHelper({ serverURL: "badUrl" },
"Error: Invalid 'serverURL'");
run_next_test();
@ -255,6 +398,10 @@ function run_test() {
* @returns {*}
*/
function validationHelper(options, expected) {
// add fxa to options - that missing isn't what we are testing here.
if (options) {
options.fxa = mockFxa;
}
try {
new FxAccountsProfileClient(options);
} catch (e) {