зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1635657 - synthesize placeholder ecosystemAnonId when not present on FxA server r=markh,rfkelly
Differential Revision: https://phabricator.services.mozilla.com/D84617
This commit is contained in:
Родитель
3ab28dbdec
Коммит
7456bd946c
|
@ -110,6 +110,7 @@ exports.COMMAND_SENDTAB = exports.COMMAND_PREFIX + exports.COMMAND_SENDTAB_TAIL;
|
||||||
// OAuth
|
// OAuth
|
||||||
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
|
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
|
||||||
exports.SCOPE_PROFILE = "profile";
|
exports.SCOPE_PROFILE = "profile";
|
||||||
|
exports.SCOPE_PROFILE_WRITE = "profile:write";
|
||||||
exports.SCOPE_OLD_SYNC = "https://identity.mozilla.com/apps/oldsync";
|
exports.SCOPE_OLD_SYNC = "https://identity.mozilla.com/apps/oldsync";
|
||||||
|
|
||||||
// OAuth metadata for other Firefox-related services that we might need to know about
|
// OAuth metadata for other Firefox-related services that we might need to know about
|
||||||
|
|
|
@ -212,7 +212,7 @@ var FxAccountsConfig = {
|
||||||
async ensureConfigured() {
|
async ensureConfigured() {
|
||||||
let isSignedIn = !!(await this.getSignedInUser());
|
let isSignedIn = !!(await this.getSignedInUser());
|
||||||
if (!isSignedIn) {
|
if (!isSignedIn) {
|
||||||
await this.fetchConfigURLs();
|
await this.updateConfigURLs();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -221,36 +221,14 @@ var FxAccountsConfig = {
|
||||||
// and replace all the relevant our prefs with the information found there.
|
// and replace all the relevant our prefs with the information found there.
|
||||||
// This is only done before sign-in and sign-up, and even then only if the
|
// This is only done before sign-in and sign-up, and even then only if the
|
||||||
// `identity.fxaccounts.autoconfig.uri` preference is set.
|
// `identity.fxaccounts.autoconfig.uri` preference is set.
|
||||||
async fetchConfigURLs() {
|
async updateConfigURLs() {
|
||||||
let rootURL = this.getAutoConfigURL();
|
let rootURL = this.getAutoConfigURL();
|
||||||
if (!rootURL) {
|
if (!rootURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let configURL = rootURL + "/.well-known/fxa-client-configuration";
|
const config = await this.fetchConfigDocument(rootURL);
|
||||||
let request = new RESTRequest(configURL);
|
|
||||||
request.setHeader("Accept", "application/json");
|
|
||||||
|
|
||||||
// Catch and rethrow the error inline.
|
|
||||||
let resp = await request.get().catch(e => {
|
|
||||||
log.error(`Failed to get configuration object from "${configURL}"`, e);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
if (!resp.success) {
|
|
||||||
log.error(
|
|
||||||
`Received HTTP response code ${resp.status} from configuration object request`
|
|
||||||
);
|
|
||||||
if (resp.body) {
|
|
||||||
log.debug("Got error response", resp.body);
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`HTTP status ${resp.status} from configuration object request`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Got successful configuration response", resp.body);
|
|
||||||
try {
|
try {
|
||||||
// Update the prefs directly specified by the config.
|
// Update the prefs directly specified by the config.
|
||||||
let config = JSON.parse(resp.body);
|
|
||||||
let authServerBase = config.auth_server_base_url;
|
let authServerBase = config.auth_server_base_url;
|
||||||
if (!authServerBase.endsWith("/v1")) {
|
if (!authServerBase.endsWith("/v1")) {
|
||||||
authServerBase += "/v1";
|
authServerBase += "/v1";
|
||||||
|
@ -292,6 +270,44 @@ var FxAccountsConfig = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Read expected client configuration from the fxa auth server
|
||||||
|
// (or from the provided rootURL, if present) and return it as an object.
|
||||||
|
async fetchConfigDocument(rootURL = null) {
|
||||||
|
if (!rootURL) {
|
||||||
|
rootURL = ROOT_URL;
|
||||||
|
}
|
||||||
|
let configURL = rootURL + "/.well-known/fxa-client-configuration";
|
||||||
|
let request = new RESTRequest(configURL);
|
||||||
|
request.setHeader("Accept", "application/json");
|
||||||
|
|
||||||
|
// Catch and rethrow the error inline.
|
||||||
|
let resp = await request.get().catch(e => {
|
||||||
|
log.error(`Failed to get configuration object from "${configURL}"`, e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
if (!resp.success) {
|
||||||
|
// Note: 'resp.body' is included with the error log below as we are not concerned
|
||||||
|
// that the body will contain PII, but if that changes it should be excluded.
|
||||||
|
log.error(
|
||||||
|
`Received HTTP response code ${resp.status} from configuration object request:
|
||||||
|
${resp.body}`
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`HTTP status ${resp.status} from configuration object request`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.debug("Got successful configuration response", resp.body);
|
||||||
|
try {
|
||||||
|
return JSON.parse(resp.body);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(
|
||||||
|
`Failed to parse configuration preferences from ${configURL}`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// For test purposes, returns a Promise.
|
// For test purposes, returns a Promise.
|
||||||
getSignedInUser() {
|
getSignedInUser() {
|
||||||
return fxAccounts.getSignedInUser();
|
return fxAccounts.getSignedInUser();
|
||||||
|
|
|
@ -23,6 +23,7 @@ const {
|
||||||
ERROR_UNKNOWN,
|
ERROR_UNKNOWN,
|
||||||
log,
|
log,
|
||||||
SCOPE_PROFILE,
|
SCOPE_PROFILE,
|
||||||
|
SCOPE_PROFILE_WRITE,
|
||||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||||
const { fxAccounts } = ChromeUtils.import(
|
const { fxAccounts } = ChromeUtils.import(
|
||||||
"resource://gre/modules/FxAccounts.jsm"
|
"resource://gre/modules/FxAccounts.jsm"
|
||||||
|
@ -59,9 +60,6 @@ var FxAccountsProfileClient = function(options) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("Invalid 'serverURL'");
|
throw new Error("Invalid 'serverURL'");
|
||||||
}
|
}
|
||||||
this.oauthOptions = {
|
|
||||||
scope: SCOPE_PROFILE,
|
|
||||||
};
|
|
||||||
log.debug("FxAccountsProfileClient: Initialized");
|
log.debug("FxAccountsProfileClient: Initialized");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,19 +81,21 @@ FxAccountsProfileClient.prototype = {
|
||||||
* @param {String} path
|
* @param {String} path
|
||||||
* Profile server path, i.e "/profile".
|
* Profile server path, i.e "/profile".
|
||||||
* @param {String} [method]
|
* @param {String} [method]
|
||||||
* Type of request, i.e "GET".
|
* Type of request, e.g. "GET".
|
||||||
* @param {String} [etag]
|
* @param {String} [etag]
|
||||||
* Optional ETag used for caching purposes.
|
* Optional ETag used for caching purposes.
|
||||||
|
* @param {Object} [body]
|
||||||
|
* Optional request body, to be sent as application/json.
|
||||||
* @return Promise
|
* @return Promise
|
||||||
* Resolves: {body: Object, etag: Object} Successful response from the Profile server.
|
* Resolves: {body: Object, etag: Object} Successful response from the Profile server.
|
||||||
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _createRequest(path, method = "GET", etag = null) {
|
async _createRequest(path, method = "GET", etag = null, body = null) {
|
||||||
// tokens are cached, so getting them each request is cheap.
|
method = method.toUpperCase();
|
||||||
let token = await this.fxai.getOAuthToken(this.oauthOptions);
|
let token = await this._getTokenForRequest(method);
|
||||||
try {
|
try {
|
||||||
return await this._rawRequest(path, method, token, etag);
|
return await this._rawRequest(path, method, token, etag, body);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
||||||
throw ex;
|
throw ex;
|
||||||
|
@ -105,11 +105,11 @@ FxAccountsProfileClient.prototype = {
|
||||||
"Fetching the profile returned a 401 - revoking our token and retrying"
|
"Fetching the profile returned a 401 - revoking our token and retrying"
|
||||||
);
|
);
|
||||||
await this.fxai.removeCachedOAuthToken({ token });
|
await this.fxai.removeCachedOAuthToken({ token });
|
||||||
token = await this.fxai.getOAuthToken(this.oauthOptions);
|
token = await this._getTokenForRequest(method);
|
||||||
// and try with the new token - if that also fails then we fail after
|
// and try with the new token - if that also fails then we fail after
|
||||||
// revoking the token.
|
// revoking the token.
|
||||||
try {
|
try {
|
||||||
return await this._rawRequest(path, method, token, etag);
|
return await this._rawRequest(path, method, token, etag, body);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
||||||
throw ex;
|
throw ex;
|
||||||
|
@ -123,6 +123,26 @@ FxAccountsProfileClient.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get an OAuth token for a request.
|
||||||
|
*
|
||||||
|
* OAuth tokens are cached, so it's fine to call this for each request.
|
||||||
|
*
|
||||||
|
* @param {String} [method]
|
||||||
|
* Type of request, i.e "GET".
|
||||||
|
* @return Promise
|
||||||
|
* Resolves: Object containing "scope", "token" and "key" properties
|
||||||
|
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _getTokenForRequest(method) {
|
||||||
|
let scope = SCOPE_PROFILE;
|
||||||
|
if (method === "POST") {
|
||||||
|
scope = SCOPE_PROFILE_WRITE;
|
||||||
|
}
|
||||||
|
return this.fxai.getOAuthToken({ scope });
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remote "raw" request helper - doesn't handle auth errors and tokens.
|
* Remote "raw" request helper - doesn't handle auth errors and tokens.
|
||||||
*
|
*
|
||||||
|
@ -132,16 +152,17 @@ FxAccountsProfileClient.prototype = {
|
||||||
* Type of request, i.e "GET".
|
* Type of request, i.e "GET".
|
||||||
* @param {String} token
|
* @param {String} token
|
||||||
* @param {String} etag
|
* @param {String} etag
|
||||||
|
* @param {Object} payload
|
||||||
|
* The payload of the request, if any.
|
||||||
* @return Promise
|
* @return Promise
|
||||||
* Resolves: {body: Object, etag: Object} Successful response from the Profile server
|
* Resolves: {body: Object, etag: Object} Successful response from the Profile server
|
||||||
or null if 304 is hit (same ETag).
|
or null if 304 is hit (same ETag).
|
||||||
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _rawRequest(path, method, token, etag) {
|
async _rawRequest(path, method, token, etag = null, payload = null) {
|
||||||
let profileDataUrl = this.serverURL + path;
|
let profileDataUrl = this.serverURL + path;
|
||||||
let request = new this._Request(profileDataUrl);
|
let request = new this._Request(profileDataUrl);
|
||||||
method = method.toUpperCase();
|
|
||||||
|
|
||||||
request.setHeader("Authorization", "Bearer " + token);
|
request.setHeader("Authorization", "Bearer " + token);
|
||||||
request.setHeader("Accept", "application/json");
|
request.setHeader("Accept", "application/json");
|
||||||
|
@ -149,7 +170,7 @@ FxAccountsProfileClient.prototype = {
|
||||||
request.setHeader("If-None-Match", etag);
|
request.setHeader("If-None-Match", etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method != "GET") {
|
if (method != "GET" && method != "POST") {
|
||||||
// method not supported
|
// method not supported
|
||||||
throw new FxAccountsProfileClientError({
|
throw new FxAccountsProfileClientError({
|
||||||
error: ERROR_NETWORK,
|
error: ERROR_NETWORK,
|
||||||
|
@ -158,9 +179,8 @@ FxAccountsProfileClient.prototype = {
|
||||||
message: ERROR_MSG_METHOD_NOT_ALLOWED,
|
message: ERROR_MSG_METHOD_NOT_ALLOWED,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await request.get();
|
await request.dispatch(method, payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new FxAccountsProfileClientError({
|
throw new FxAccountsProfileClientError({
|
||||||
error: ERROR_NETWORK,
|
error: ERROR_NETWORK,
|
||||||
|
@ -212,6 +232,27 @@ FxAccountsProfileClient.prototype = {
|
||||||
log.debug("FxAccountsProfileClient: Requested profile");
|
log.debug("FxAccountsProfileClient: Requested profile");
|
||||||
return this._createRequest("/profile", "GET", etag);
|
return this._createRequest("/profile", "GET", etag);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an ecosystemAnonId value to the user's profile data on the server.
|
||||||
|
*
|
||||||
|
* This should be used only if the user's profile data does not already contain an
|
||||||
|
* ecosytemAnonId field, and it will reject with a "412 Precondition Failed" if there
|
||||||
|
* is one already present on the server.
|
||||||
|
*
|
||||||
|
* @param {String} [ecosystemAnonId]
|
||||||
|
* The generated ecosystemAnonId value to store on the server.
|
||||||
|
* @return Promise
|
||||||
|
* Resolves: {body: Object} Successful response from the '/ecosystem_anon_id' endpoint.
|
||||||
|
* Rejects: {FxAccountsProfileClientError} profile client error.
|
||||||
|
*/
|
||||||
|
setEcosystemAnonId(ecosystemAnonId) {
|
||||||
|
log.debug("FxAccountsProfileClient: Setting ecosystemAnonId");
|
||||||
|
// This uses `If-None-Match: "*"` to prevent two concurrent clients from setting a value.
|
||||||
|
return this._createRequest("/ecosystem_anon_id", "POST", "*", {
|
||||||
|
ecosystemAnonId,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,7 +18,10 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||||
// "subject" data.
|
// "subject" data.
|
||||||
Observers: "resource://services-common/observers.js",
|
Observers: "resource://services-common/observers.js",
|
||||||
Services: "resource://gre/modules/Services.jsm",
|
Services: "resource://gre/modules/Services.jsm",
|
||||||
|
CommonUtils: "resource://services-common/utils.js",
|
||||||
CryptoUtils: "resource://services-crypto/utils.js",
|
CryptoUtils: "resource://services-crypto/utils.js",
|
||||||
|
FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.jsm",
|
||||||
|
jwcrypto: "resource://services-crypto/jwcrypto.jsm",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { PREF_ACCOUNT_ROOT, log } = ChromeUtils.import(
|
const { PREF_ACCOUNT_ROOT, log } = ChromeUtils.import(
|
||||||
|
@ -47,8 +50,7 @@ class FxAccountsTelemetry {
|
||||||
Observers.notify("fxa:telemetry:event", { object, method, value, extra });
|
Observers.notify("fxa:telemetry:event", { object, method, value, extra });
|
||||||
}
|
}
|
||||||
|
|
||||||
// A flow ID can be anything that's "probably" unique, so for now use a UUID.
|
generateUUID() {
|
||||||
generateFlowID() {
|
|
||||||
return Cc["@mozilla.org/uuid-generator;1"]
|
return Cc["@mozilla.org/uuid-generator;1"]
|
||||||
.getService(Ci.nsIUUIDGenerator)
|
.getService(Ci.nsIUUIDGenerator)
|
||||||
.generateUUID()
|
.generateUUID()
|
||||||
|
@ -56,6 +58,11 @@ class FxAccountsTelemetry {
|
||||||
.slice(1, -1);
|
.slice(1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A flow ID can be anything that's "probably" unique, so for now use a UUID.
|
||||||
|
generateFlowID() {
|
||||||
|
return this.generateUUID();
|
||||||
|
}
|
||||||
|
|
||||||
// Account Ecosystem Telemetry identifies the user by a secret id called their "ecosystemUserId".
|
// Account Ecosystem Telemetry identifies the user by a secret id called their "ecosystemUserId".
|
||||||
// To maintain user privacy this value must never be shared with Mozilla servers in plaintext
|
// To maintain user privacy this value must never be shared with Mozilla servers in plaintext
|
||||||
// (although there may be some client-side-only features that use it in future).
|
// (although there may be some client-side-only features that use it in future).
|
||||||
|
@ -95,14 +102,55 @@ class FxAccountsTelemetry {
|
||||||
// This asynchronous method resolves with the "ecosystemAnonId" value on success, and rejects
|
// This asynchronous method resolves with the "ecosystemAnonId" value on success, and rejects
|
||||||
// with an error if no user is signed in or if the value could not be obtained from the
|
// with an error if no user is signed in or if the value could not be obtained from the
|
||||||
// FxA server.
|
// FxA server.
|
||||||
async ensureEcosystemAnonId() {
|
async ensureEcosystemAnonId(generatePlaceholder = true) {
|
||||||
const profile = await this._internal.profile.ensureProfile();
|
const telemetry = this;
|
||||||
if (!profile.hasOwnProperty("ecosystemAnonId")) {
|
return this._fxai.withCurrentAccountState(async function(state) {
|
||||||
// In a future iteration, we can synthesize a placeholder ecosystemAnonId and persist it
|
const profile = await telemetry._fxai.profile.ensureProfile();
|
||||||
// back to the FxA server.
|
if (profile.hasOwnProperty("ecosystemAnonId")) {
|
||||||
throw new Error("Profile data does not contain an 'ecosystemAnonId'");
|
return profile.ecosystemAnonId;
|
||||||
}
|
}
|
||||||
return profile.ecosystemAnonId;
|
if (!generatePlaceholder) {
|
||||||
|
throw new Error("Profile data does not contain an 'ecosystemAnonId'");
|
||||||
|
}
|
||||||
|
// If the server doesn't have ecosystemAnonId yet, we can create a placeholder value.
|
||||||
|
// First, generate a random ecosystemUserId.
|
||||||
|
let ecosystemUserId = CommonUtils.bufferToHex(
|
||||||
|
CryptoUtils.generateRandomBytes(32)
|
||||||
|
);
|
||||||
|
// Now encrypt it to the AET public key, as advertized by the FxA server.
|
||||||
|
const serverConfig = await FxAccountsConfig.fetchConfigDocument();
|
||||||
|
const ecosystemKeys = serverConfig.ecosystem_anon_id_keys;
|
||||||
|
if (!ecosystemKeys || !ecosystemKeys.length) {
|
||||||
|
throw new Error(
|
||||||
|
"Unable to fetch ecosystem_anon_id_keys from FxA server"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const randomKey = Math.floor(
|
||||||
|
Math.random() * Math.floor(ecosystemKeys.length)
|
||||||
|
);
|
||||||
|
const ecosystemAnonId = await jwcrypto.generateJWE(
|
||||||
|
ecosystemKeys[randomKey],
|
||||||
|
new TextEncoder().encode(ecosystemUserId)
|
||||||
|
);
|
||||||
|
// Persist the encrypted value to the server so other clients can find it.
|
||||||
|
try {
|
||||||
|
await telemetry._fxai.profile.client.setEcosystemAnonId(
|
||||||
|
ecosystemAnonId
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.code && err.code === 412) {
|
||||||
|
// Another client raced us to upload the placeholder, fetch it.
|
||||||
|
return telemetry.ensureEcosystemAnonId(false);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// Locally persist the unencrypted ecosystemUserId for possible future use.
|
||||||
|
ecosystemUserId = state.ecosystemUserId;
|
||||||
|
await state.updateUserAccountData({
|
||||||
|
ecosystemUserId,
|
||||||
|
});
|
||||||
|
return ecosystemAnonId;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prior to Account Ecosystem Telemetry, FxA- and Sync-related metrics were submitted in
|
// Prior to Account Ecosystem Telemetry, FxA- and Sync-related metrics were submitted in
|
||||||
|
|
|
@ -37,7 +37,7 @@ let mockResponse = function(response) {
|
||||||
Request.ifNoneMatchSet = true;
|
Request.ifNoneMatchSet = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async get() {
|
async dispatch(method, payload) {
|
||||||
this.response = response;
|
this.response = response;
|
||||||
return this.response;
|
return this.response;
|
||||||
},
|
},
|
||||||
|
@ -72,7 +72,7 @@ let mockResponseError = function(error) {
|
||||||
return function() {
|
return function() {
|
||||||
return {
|
return {
|
||||||
setHeader() {},
|
setHeader() {},
|
||||||
async get() {
|
async dispatch(method, payload) {
|
||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -232,7 +232,7 @@ add_test(function server401ResponseThenSuccess() {
|
||||||
Assert.equal(value, "Bearer " + lastToken);
|
Assert.equal(value, "Bearer " + lastToken);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async get() {
|
async dispatch(method, payload) {
|
||||||
this.response = responses[numRequests];
|
this.response = responses[numRequests];
|
||||||
++numRequests;
|
++numRequests;
|
||||||
return this.response;
|
return this.response;
|
||||||
|
@ -295,7 +295,7 @@ add_test(function server401ResponsePersists() {
|
||||||
Assert.equal(value, "Bearer " + lastToken);
|
Assert.equal(value, "Bearer " + lastToken);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async get() {
|
async dispatch(method, payload) {
|
||||||
this.response = response;
|
this.response = response;
|
||||||
++numRequests;
|
++numRequests;
|
||||||
return this.response;
|
return this.response;
|
||||||
|
|
|
@ -15,10 +15,19 @@ const { FxAccountsProfile } = ChromeUtils.import(
|
||||||
"resource://gre/modules/FxAccountsProfile.jsm"
|
"resource://gre/modules/FxAccountsProfile.jsm"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { FxAccountsProfileClient } = ChromeUtils.import(
|
||||||
|
"resource://gre/modules/FxAccountsProfileClient.jsm"
|
||||||
|
);
|
||||||
|
|
||||||
const { FxAccountsTelemetry } = ChromeUtils.import(
|
const { FxAccountsTelemetry } = ChromeUtils.import(
|
||||||
"resource://gre/modules/FxAccountsTelemetry.jsm"
|
"resource://gre/modules/FxAccountsTelemetry.jsm"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||||
|
FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.jsm",
|
||||||
|
jwcrypto: "resource://services-crypto/jwcrypto.jsm",
|
||||||
|
});
|
||||||
|
|
||||||
_("Misc tests for FxAccounts.telemetry");
|
_("Misc tests for FxAccounts.telemetry");
|
||||||
|
|
||||||
const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff";
|
const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff";
|
||||||
|
@ -63,8 +72,8 @@ add_task(function test_sanitize_device_id() {
|
||||||
});
|
});
|
||||||
|
|
||||||
add_task(async function test_getEcosystemAnonId() {
|
add_task(async function test_getEcosystemAnonId() {
|
||||||
let ecosystemAnonId = "aaaaaaaaaaaaaaa";
|
const ecosystemAnonId = "aaaaaaaaaaaaaaa";
|
||||||
let testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
// testing retrieving the ecosystemAnonId when the profile contains it
|
// testing retrieving the ecosystemAnonId when the profile contains it
|
||||||
throw: false,
|
throw: false,
|
||||||
|
@ -86,8 +95,10 @@ add_task(async function test_getEcosystemAnonId() {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const tc of testCases) {
|
for (const tc of testCases) {
|
||||||
let profile = new FxAccountsProfile({ profileServerUrl: "http://testURL" });
|
const profile = new FxAccountsProfile({
|
||||||
let telemetry = new FxAccountsTelemetry({});
|
profileServerUrl: "http://testURL",
|
||||||
|
});
|
||||||
|
const telemetry = new FxAccountsTelemetry({});
|
||||||
telemetry._internal = { profile };
|
telemetry._internal = { profile };
|
||||||
const mockProfile = sinon.mock(profile);
|
const mockProfile = sinon.mock(profile);
|
||||||
const mockTelemetry = sinon.mock(telemetry);
|
const mockTelemetry = sinon.mock(telemetry);
|
||||||
|
@ -113,49 +124,238 @@ add_task(async function test_getEcosystemAnonId() {
|
||||||
.resolves("dddddddddd");
|
.resolves("dddddddddd");
|
||||||
}
|
}
|
||||||
|
|
||||||
let actualEcoSystemAnonId = await telemetry.getEcosystemAnonId();
|
const actualEcoSystemAnonId = await telemetry.getEcosystemAnonId();
|
||||||
mockProfile.verify();
|
mockProfile.verify();
|
||||||
mockTelemetry.verify();
|
mockTelemetry.verify();
|
||||||
Assert.equal(actualEcoSystemAnonId, tc.expectedEcosystemAnonId);
|
Assert.equal(actualEcoSystemAnonId, tc.expectedEcosystemAnonId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
add_task(async function test_ensureEcosystemAnonId() {
|
add_task(async function test_ensureEcosystemAnonId_fromProfile() {
|
||||||
let ecosystemAnonId = "bbbbbbbbbbbbbb";
|
const expecteErrorMessage =
|
||||||
let testCases = [
|
"Profile data does not contain an 'ecosystemAnonId'";
|
||||||
|
const expectedEcosystemAnonId = "aaaaaaaaaaa";
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
{
|
{
|
||||||
profile: {
|
profile: {
|
||||||
ecosystemAnonId,
|
ecosystemAnonId: expectedEcosystemAnonId,
|
||||||
},
|
},
|
||||||
willErr: false,
|
generatePlaceholder: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
profile: {},
|
profile: {},
|
||||||
willErr: true,
|
generatePlaceholder: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const tc of testCases) {
|
for (const tc of testCases) {
|
||||||
let profile = new FxAccountsProfile({ profileServerUrl: "http://testURL" });
|
const profile = new FxAccountsProfile({
|
||||||
let telemetry = new FxAccountsTelemetry({});
|
profileServerUrl: "http://testURL",
|
||||||
telemetry._internal = { profile };
|
});
|
||||||
let mockProfile = sinon.mock(profile);
|
const telemetry = new FxAccountsTelemetry({
|
||||||
|
profile,
|
||||||
|
withCurrentAccountState: async cb => {
|
||||||
|
return cb({});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mockProfile = sinon.mock(profile);
|
||||||
|
|
||||||
mockProfile
|
mockProfile
|
||||||
.expects("ensureProfile")
|
.expects("ensureProfile")
|
||||||
.once()
|
.once()
|
||||||
.returns(tc.profile);
|
.returns(tc.profile);
|
||||||
|
|
||||||
if (tc.willErr) {
|
if (tc.profile.ecosystemAnonId) {
|
||||||
const expectedError = `Profile data does not contain an 'ecosystemAnonId'`;
|
const actualEcoSystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||||
try {
|
Assert.equal(actualEcoSystemAnonId, expectedEcosystemAnonId);
|
||||||
await telemetry.ensureEcosystemAnonId();
|
|
||||||
} catch (e) {
|
|
||||||
Assert.equal(e.message, expectedError);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let actualEcoSystemAnonId = await telemetry.ensureEcosystemAnonId();
|
try {
|
||||||
Assert.equal(actualEcoSystemAnonId, ecosystemAnonId);
|
await telemetry.ensureEcosystemAnonId(tc.generatePlaceholder);
|
||||||
|
} catch (e) {
|
||||||
|
Assert.equal(expecteErrorMessage, e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mockProfile.verify();
|
mockProfile.verify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ensureEcosystemAnonId_failToGenerateKeys() {
|
||||||
|
const expectedErrorMessage =
|
||||||
|
"Unable to fetch ecosystem_anon_id_keys from FxA server";
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
serverConfig: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverConfig: {
|
||||||
|
ecosystem_anon_id_keys: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const tc of testCases) {
|
||||||
|
const profile = new FxAccountsProfile({
|
||||||
|
profileServerUrl: "http://testURL",
|
||||||
|
});
|
||||||
|
const telemetry = new FxAccountsTelemetry({
|
||||||
|
profile,
|
||||||
|
withCurrentAccountState: async cb => {
|
||||||
|
return cb({});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mockProfile = sinon.mock(profile);
|
||||||
|
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||||
|
|
||||||
|
mockProfile
|
||||||
|
.expects("ensureProfile")
|
||||||
|
.once()
|
||||||
|
.returns({});
|
||||||
|
|
||||||
|
mockFxAccountsConfig
|
||||||
|
.expects("fetchConfigDocument")
|
||||||
|
.once()
|
||||||
|
.returns(tc.serverConfig);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await telemetry.ensureEcosystemAnonId(true);
|
||||||
|
} catch (e) {
|
||||||
|
Assert.equal(expectedErrorMessage, e.message);
|
||||||
|
mockProfile.verify();
|
||||||
|
mockFxAccountsConfig.verify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ensureEcosystemAnonId_clientRace() {
|
||||||
|
const expectedEcosystemAnonId = "bbbbbbbbbbbb";
|
||||||
|
const expectedErrrorMessage = "test error at 'setEcosystemAnonId'";
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
errorCode: 412,
|
||||||
|
errorMessage: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errorCode: 405,
|
||||||
|
errorMessage: expectedErrrorMessage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const tc of testCases) {
|
||||||
|
const profileClient = new FxAccountsProfileClient({
|
||||||
|
serverURL: "http://testURL",
|
||||||
|
});
|
||||||
|
const profile = new FxAccountsProfile({ profileClient });
|
||||||
|
const telemetry = new FxAccountsTelemetry({
|
||||||
|
profile,
|
||||||
|
withCurrentAccountState: async cb => {
|
||||||
|
return cb({});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mockProfile = sinon.mock(profile);
|
||||||
|
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||||
|
const mockJwcrypto = sinon.mock(jwcrypto);
|
||||||
|
const mockProfileClient = sinon.mock(profileClient);
|
||||||
|
|
||||||
|
mockProfile
|
||||||
|
.expects("ensureProfile")
|
||||||
|
.once()
|
||||||
|
.returns({});
|
||||||
|
|
||||||
|
mockFxAccountsConfig
|
||||||
|
.expects("fetchConfigDocument")
|
||||||
|
.once()
|
||||||
|
.returns({
|
||||||
|
ecosystem_anon_id_keys: ["testKey"],
|
||||||
|
});
|
||||||
|
|
||||||
|
mockJwcrypto
|
||||||
|
.expects("generateJWE")
|
||||||
|
.once()
|
||||||
|
.returns(expectedEcosystemAnonId);
|
||||||
|
|
||||||
|
mockProfileClient
|
||||||
|
.expects("setEcosystemAnonId")
|
||||||
|
.once()
|
||||||
|
.throws({
|
||||||
|
code: tc.errorCode,
|
||||||
|
message: tc.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tc.errorCode === 412) {
|
||||||
|
mockProfile
|
||||||
|
.expects("ensureProfile")
|
||||||
|
.once()
|
||||||
|
.returns({
|
||||||
|
ecosystemAnonId: expectedEcosystemAnonId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualEcosystemAnonId = await telemetry.ensureEcosystemAnonId(true);
|
||||||
|
Assert.equal(expectedEcosystemAnonId, actualEcosystemAnonId);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await telemetry.ensureEcosystemAnonId(true);
|
||||||
|
} catch (e) {
|
||||||
|
Assert.equal(expectedErrrorMessage, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockProfile.verify();
|
||||||
|
mockFxAccountsConfig.verify();
|
||||||
|
mockJwcrypto.verify();
|
||||||
|
mockProfileClient.verify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ensureEcosystemAnonId_updateAccountData() {
|
||||||
|
const expectedEcosystemUserId = "aaaaaaaaaa";
|
||||||
|
const expectedEcosystemAnonId = "bbbbbbbbbbbb";
|
||||||
|
const profileClient = new FxAccountsProfileClient({
|
||||||
|
serverURL: "http://testURL",
|
||||||
|
});
|
||||||
|
const profile = new FxAccountsProfile({ profileClient });
|
||||||
|
const fxa = new FxAccounts();
|
||||||
|
fxa.withCurrentAccountState = async cb =>
|
||||||
|
cb({
|
||||||
|
ecosystemUserId: expectedEcosystemUserId,
|
||||||
|
updateUserAccountData: fields => {
|
||||||
|
Assert.equal(expectedEcosystemUserId, fields.ecosystemUserId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fxa.profile = profile;
|
||||||
|
const telemetry = new FxAccountsTelemetry(fxa);
|
||||||
|
const mockProfile = sinon.mock(profile);
|
||||||
|
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||||
|
const mockJwcrypto = sinon.mock(jwcrypto);
|
||||||
|
const mockProfileClient = sinon.mock(profileClient);
|
||||||
|
|
||||||
|
mockProfile
|
||||||
|
.expects("ensureProfile")
|
||||||
|
.once()
|
||||||
|
.returns({});
|
||||||
|
|
||||||
|
mockFxAccountsConfig
|
||||||
|
.expects("fetchConfigDocument")
|
||||||
|
.once()
|
||||||
|
.returns({
|
||||||
|
ecosystem_anon_id_keys: ["testKey"],
|
||||||
|
});
|
||||||
|
|
||||||
|
mockJwcrypto
|
||||||
|
.expects("generateJWE")
|
||||||
|
.once()
|
||||||
|
.returns(expectedEcosystemAnonId);
|
||||||
|
|
||||||
|
mockProfileClient
|
||||||
|
.expects("setEcosystemAnonId")
|
||||||
|
.once()
|
||||||
|
.returns(null);
|
||||||
|
|
||||||
|
const actualEcosystemAnonId = await telemetry.ensureEcosystemAnonId(true);
|
||||||
|
Assert.equal(expectedEcosystemAnonId, actualEcosystemAnonId);
|
||||||
|
|
||||||
|
mockProfile.verify();
|
||||||
|
mockFxAccountsConfig.verify();
|
||||||
|
mockJwcrypto.verify();
|
||||||
|
mockProfileClient.verify();
|
||||||
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче