зеркало из 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
|
||||
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
|
||||
exports.SCOPE_PROFILE = "profile";
|
||||
exports.SCOPE_PROFILE_WRITE = "profile:write";
|
||||
exports.SCOPE_OLD_SYNC = "https://identity.mozilla.com/apps/oldsync";
|
||||
|
||||
// OAuth metadata for other Firefox-related services that we might need to know about
|
||||
|
|
|
@ -212,7 +212,7 @@ var FxAccountsConfig = {
|
|||
async ensureConfigured() {
|
||||
let isSignedIn = !!(await this.getSignedInUser());
|
||||
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.
|
||||
// This is only done before sign-in and sign-up, and even then only if the
|
||||
// `identity.fxaccounts.autoconfig.uri` preference is set.
|
||||
async fetchConfigURLs() {
|
||||
async updateConfigURLs() {
|
||||
let rootURL = this.getAutoConfigURL();
|
||||
if (!rootURL) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
const config = await this.fetchConfigDocument(rootURL);
|
||||
try {
|
||||
// Update the prefs directly specified by the config.
|
||||
let config = JSON.parse(resp.body);
|
||||
let authServerBase = config.auth_server_base_url;
|
||||
if (!authServerBase.endsWith("/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.
|
||||
getSignedInUser() {
|
||||
return fxAccounts.getSignedInUser();
|
||||
|
|
|
@ -23,6 +23,7 @@ const {
|
|||
ERROR_UNKNOWN,
|
||||
log,
|
||||
SCOPE_PROFILE,
|
||||
SCOPE_PROFILE_WRITE,
|
||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
const { fxAccounts } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccounts.jsm"
|
||||
|
@ -59,9 +60,6 @@ var FxAccountsProfileClient = function(options) {
|
|||
} catch (e) {
|
||||
throw new Error("Invalid 'serverURL'");
|
||||
}
|
||||
this.oauthOptions = {
|
||||
scope: SCOPE_PROFILE,
|
||||
};
|
||||
log.debug("FxAccountsProfileClient: Initialized");
|
||||
};
|
||||
|
||||
|
@ -83,19 +81,21 @@ FxAccountsProfileClient.prototype = {
|
|||
* @param {String} path
|
||||
* Profile server path, i.e "/profile".
|
||||
* @param {String} [method]
|
||||
* Type of request, i.e "GET".
|
||||
* Type of request, e.g. "GET".
|
||||
* @param {String} [etag]
|
||||
* Optional ETag used for caching purposes.
|
||||
* @param {Object} [body]
|
||||
* Optional request body, to be sent as application/json.
|
||||
* @return Promise
|
||||
* Resolves: {body: Object, etag: Object} Successful response from the Profile server.
|
||||
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
||||
* @private
|
||||
*/
|
||||
async _createRequest(path, method = "GET", etag = null) {
|
||||
// tokens are cached, so getting them each request is cheap.
|
||||
let token = await this.fxai.getOAuthToken(this.oauthOptions);
|
||||
async _createRequest(path, method = "GET", etag = null, body = null) {
|
||||
method = method.toUpperCase();
|
||||
let token = await this._getTokenForRequest(method);
|
||||
try {
|
||||
return await this._rawRequest(path, method, token, etag);
|
||||
return await this._rawRequest(path, method, token, etag, body);
|
||||
} catch (ex) {
|
||||
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
||||
throw ex;
|
||||
|
@ -105,11 +105,11 @@ FxAccountsProfileClient.prototype = {
|
|||
"Fetching the profile returned a 401 - revoking our token and retrying"
|
||||
);
|
||||
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
|
||||
// revoking the token.
|
||||
try {
|
||||
return await this._rawRequest(path, method, token, etag);
|
||||
return await this._rawRequest(path, method, token, etag, body);
|
||||
} catch (ex) {
|
||||
if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
|
||||
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.
|
||||
*
|
||||
|
@ -132,16 +152,17 @@ FxAccountsProfileClient.prototype = {
|
|||
* Type of request, i.e "GET".
|
||||
* @param {String} token
|
||||
* @param {String} etag
|
||||
* @param {Object} payload
|
||||
* The payload of the request, if any.
|
||||
* @return Promise
|
||||
* Resolves: {body: Object, etag: Object} Successful response from the Profile server
|
||||
or null if 304 is hit (same ETag).
|
||||
* Rejects: {FxAccountsProfileClientError} Profile client error.
|
||||
* @private
|
||||
*/
|
||||
async _rawRequest(path, method, token, etag) {
|
||||
async _rawRequest(path, method, token, etag = null, payload = null) {
|
||||
let profileDataUrl = this.serverURL + path;
|
||||
let request = new this._Request(profileDataUrl);
|
||||
method = method.toUpperCase();
|
||||
|
||||
request.setHeader("Authorization", "Bearer " + token);
|
||||
request.setHeader("Accept", "application/json");
|
||||
|
@ -149,7 +170,7 @@ FxAccountsProfileClient.prototype = {
|
|||
request.setHeader("If-None-Match", etag);
|
||||
}
|
||||
|
||||
if (method != "GET") {
|
||||
if (method != "GET" && method != "POST") {
|
||||
// method not supported
|
||||
throw new FxAccountsProfileClientError({
|
||||
error: ERROR_NETWORK,
|
||||
|
@ -158,9 +179,8 @@ FxAccountsProfileClient.prototype = {
|
|||
message: ERROR_MSG_METHOD_NOT_ALLOWED,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await request.get();
|
||||
await request.dispatch(method, payload);
|
||||
} catch (error) {
|
||||
throw new FxAccountsProfileClientError({
|
||||
error: ERROR_NETWORK,
|
||||
|
@ -212,6 +232,27 @@ FxAccountsProfileClient.prototype = {
|
|||
log.debug("FxAccountsProfileClient: Requested profile");
|
||||
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.
|
||||
Observers: "resource://services-common/observers.js",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
CommonUtils: "resource://services-common/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(
|
||||
|
@ -47,8 +50,7 @@ class FxAccountsTelemetry {
|
|||
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.
|
||||
generateFlowID() {
|
||||
generateUUID() {
|
||||
return Cc["@mozilla.org/uuid-generator;1"]
|
||||
.getService(Ci.nsIUUIDGenerator)
|
||||
.generateUUID()
|
||||
|
@ -56,6 +58,11 @@ class FxAccountsTelemetry {
|
|||
.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".
|
||||
// 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).
|
||||
|
@ -95,14 +102,55 @@ class FxAccountsTelemetry {
|
|||
// 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
|
||||
// FxA server.
|
||||
async ensureEcosystemAnonId() {
|
||||
const profile = await this._internal.profile.ensureProfile();
|
||||
if (!profile.hasOwnProperty("ecosystemAnonId")) {
|
||||
// In a future iteration, we can synthesize a placeholder ecosystemAnonId and persist it
|
||||
// back to the FxA server.
|
||||
throw new Error("Profile data does not contain an 'ecosystemAnonId'");
|
||||
}
|
||||
return profile.ecosystemAnonId;
|
||||
async ensureEcosystemAnonId(generatePlaceholder = true) {
|
||||
const telemetry = this;
|
||||
return this._fxai.withCurrentAccountState(async function(state) {
|
||||
const profile = await telemetry._fxai.profile.ensureProfile();
|
||||
if (profile.hasOwnProperty("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
|
||||
|
|
|
@ -37,7 +37,7 @@ let mockResponse = function(response) {
|
|||
Request.ifNoneMatchSet = true;
|
||||
}
|
||||
},
|
||||
async get() {
|
||||
async dispatch(method, payload) {
|
||||
this.response = response;
|
||||
return this.response;
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ let mockResponseError = function(error) {
|
|||
return function() {
|
||||
return {
|
||||
setHeader() {},
|
||||
async get() {
|
||||
async dispatch(method, payload) {
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
|
@ -232,7 +232,7 @@ add_test(function server401ResponseThenSuccess() {
|
|||
Assert.equal(value, "Bearer " + lastToken);
|
||||
}
|
||||
},
|
||||
async get() {
|
||||
async dispatch(method, payload) {
|
||||
this.response = responses[numRequests];
|
||||
++numRequests;
|
||||
return this.response;
|
||||
|
@ -295,7 +295,7 @@ add_test(function server401ResponsePersists() {
|
|||
Assert.equal(value, "Bearer " + lastToken);
|
||||
}
|
||||
},
|
||||
async get() {
|
||||
async dispatch(method, payload) {
|
||||
this.response = response;
|
||||
++numRequests;
|
||||
return this.response;
|
||||
|
|
|
@ -15,10 +15,19 @@ const { FxAccountsProfile } = ChromeUtils.import(
|
|||
"resource://gre/modules/FxAccountsProfile.jsm"
|
||||
);
|
||||
|
||||
const { FxAccountsProfileClient } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsProfileClient.jsm"
|
||||
);
|
||||
|
||||
const { FxAccountsTelemetry } = ChromeUtils.import(
|
||||
"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");
|
||||
|
||||
const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff";
|
||||
|
@ -63,8 +72,8 @@ add_task(function test_sanitize_device_id() {
|
|||
});
|
||||
|
||||
add_task(async function test_getEcosystemAnonId() {
|
||||
let ecosystemAnonId = "aaaaaaaaaaaaaaa";
|
||||
let testCases = [
|
||||
const ecosystemAnonId = "aaaaaaaaaaaaaaa";
|
||||
const testCases = [
|
||||
{
|
||||
// testing retrieving the ecosystemAnonId when the profile contains it
|
||||
throw: false,
|
||||
|
@ -86,8 +95,10 @@ add_task(async function test_getEcosystemAnonId() {
|
|||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
let profile = new FxAccountsProfile({ profileServerUrl: "http://testURL" });
|
||||
let telemetry = new FxAccountsTelemetry({});
|
||||
const profile = new FxAccountsProfile({
|
||||
profileServerUrl: "http://testURL",
|
||||
});
|
||||
const telemetry = new FxAccountsTelemetry({});
|
||||
telemetry._internal = { profile };
|
||||
const mockProfile = sinon.mock(profile);
|
||||
const mockTelemetry = sinon.mock(telemetry);
|
||||
|
@ -113,49 +124,238 @@ add_task(async function test_getEcosystemAnonId() {
|
|||
.resolves("dddddddddd");
|
||||
}
|
||||
|
||||
let actualEcoSystemAnonId = await telemetry.getEcosystemAnonId();
|
||||
const actualEcoSystemAnonId = await telemetry.getEcosystemAnonId();
|
||||
mockProfile.verify();
|
||||
mockTelemetry.verify();
|
||||
Assert.equal(actualEcoSystemAnonId, tc.expectedEcosystemAnonId);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId() {
|
||||
let ecosystemAnonId = "bbbbbbbbbbbbbb";
|
||||
let testCases = [
|
||||
add_task(async function test_ensureEcosystemAnonId_fromProfile() {
|
||||
const expecteErrorMessage =
|
||||
"Profile data does not contain an 'ecosystemAnonId'";
|
||||
const expectedEcosystemAnonId = "aaaaaaaaaaa";
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
profile: {
|
||||
ecosystemAnonId,
|
||||
ecosystemAnonId: expectedEcosystemAnonId,
|
||||
},
|
||||
willErr: false,
|
||||
generatePlaceholder: true,
|
||||
},
|
||||
{
|
||||
profile: {},
|
||||
willErr: true,
|
||||
generatePlaceholder: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
let profile = new FxAccountsProfile({ profileServerUrl: "http://testURL" });
|
||||
let telemetry = new FxAccountsTelemetry({});
|
||||
telemetry._internal = { profile };
|
||||
let mockProfile = sinon.mock(profile);
|
||||
const profile = new FxAccountsProfile({
|
||||
profileServerUrl: "http://testURL",
|
||||
});
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
profile,
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({});
|
||||
},
|
||||
});
|
||||
const mockProfile = sinon.mock(profile);
|
||||
|
||||
mockProfile
|
||||
.expects("ensureProfile")
|
||||
.once()
|
||||
.returns(tc.profile);
|
||||
|
||||
if (tc.willErr) {
|
||||
const expectedError = `Profile data does not contain an 'ecosystemAnonId'`;
|
||||
try {
|
||||
await telemetry.ensureEcosystemAnonId();
|
||||
} catch (e) {
|
||||
Assert.equal(e.message, expectedError);
|
||||
}
|
||||
if (tc.profile.ecosystemAnonId) {
|
||||
const actualEcoSystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||
Assert.equal(actualEcoSystemAnonId, expectedEcosystemAnonId);
|
||||
} else {
|
||||
let actualEcoSystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||
Assert.equal(actualEcoSystemAnonId, ecosystemAnonId);
|
||||
try {
|
||||
await telemetry.ensureEcosystemAnonId(tc.generatePlaceholder);
|
||||
} catch (e) {
|
||||
Assert.equal(expecteErrorMessage, e.message);
|
||||
}
|
||||
}
|
||||
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();
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче