зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1697596 - Remove ecosystem_anon_id from FxA client code. r=markh
Differential Revision: https://phabricator.services.mozilla.com/D108389
This commit is contained in:
Родитель
ec7f538e0c
Коммит
b7f213243a
|
@ -763,14 +763,6 @@ FxAccountsInternal.prototype = {
|
|||
ChromeUtils.import("resource://services-sync/main.js", scope);
|
||||
return scope.Weave.Service.promiseInitialized;
|
||||
},
|
||||
// Telemetry, so ecosystem telemetry doesn't miss logouts.
|
||||
async () => {
|
||||
const { EcosystemTelemetry } = ChromeUtils.import(
|
||||
"resource://gre/modules/EcosystemTelemetry.jsm",
|
||||
{}
|
||||
);
|
||||
await EcosystemTelemetry.prepareForFxANotification();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -113,12 +113,17 @@ 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";
|
||||
exports.SCOPE_ECOSYSTEM_TELEMETRY =
|
||||
"https://identity.mozilla.com/ids/ecosystem_telemetry";
|
||||
// This scope and its associated key material are used by the old Kinto webextension
|
||||
// storage backend. We plan to remove that at some point (ref Bug 1637465) and when
|
||||
// we do, all uses of this legacy scope can be removed.
|
||||
exports.LEGACY_SCOPE_WEBEXT_SYNC = "sync:addon_storage";
|
||||
// This scope was previously used to calculate a telemetry tracking identifier for
|
||||
// the account, but that system has since been decommissioned. It's here entirely
|
||||
// so that we can remove the corresponding key from storage if present. We should
|
||||
// be safe to remove it after some sensible period of time has elapsed to allow
|
||||
// most clients to update; ref Bug 1697596.
|
||||
exports.DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY =
|
||||
"https://identity.mozilla.com/ids/ecosystem_telemetry";
|
||||
|
||||
// OAuth metadata for other Firefox-related services that we might need to know about
|
||||
// in order to provide an enhanced user experience.
|
||||
|
@ -293,8 +298,6 @@ exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set([
|
|||
"authAt",
|
||||
"sessionToken",
|
||||
"uid",
|
||||
"ecosystemAnonId",
|
||||
"ecosystemUserId",
|
||||
"oauthTokens",
|
||||
"profile",
|
||||
"device",
|
||||
|
|
|
@ -17,8 +17,8 @@ const { CryptoUtils } = ChromeUtils.import(
|
|||
const {
|
||||
LEGACY_DERIVED_KEYS_NAMES,
|
||||
SCOPE_OLD_SYNC,
|
||||
SCOPE_ECOSYSTEM_TELEMETRY,
|
||||
LEGACY_SCOPE_WEBEXT_SYNC,
|
||||
DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
log,
|
||||
logPII,
|
||||
|
@ -29,6 +29,10 @@ const {
|
|||
// these scopes.
|
||||
const LEGACY_DERIVED_KEY_SCOPES = [SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC];
|
||||
|
||||
// These are scopes that we used to store, but are no longer using,
|
||||
// and hence should be deleted from storage if present.
|
||||
const DEPRECATED_KEY_SCOPES = [DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY];
|
||||
|
||||
/**
|
||||
* Utilities for working with key material linked to the user's account.
|
||||
*
|
||||
|
@ -184,7 +188,6 @@ class FxAccountsKeys {
|
|||
* kXCS: A key hash of kB for the X-Client-State header
|
||||
* kExtSync: An encryption key for WebExtensions syncing
|
||||
* kExtKbHash: A key hash of kB for WebExtensions syncing
|
||||
* ecosystemUserId: A derived key used for Account EcosystemTelemetry
|
||||
* verified: email verification status
|
||||
* }
|
||||
* @throws If there is no user signed in.
|
||||
|
@ -201,6 +204,9 @@ class FxAccountsKeys {
|
|||
if (
|
||||
LEGACY_DERIVED_KEY_SCOPES.every(scope =>
|
||||
userData.scopedKeys.hasOwnProperty(scope)
|
||||
) &&
|
||||
!DEPRECATED_KEY_SCOPES.some(scope =>
|
||||
userData.scopedKeys.hasOwnProperty(scope)
|
||||
)
|
||||
) {
|
||||
return userData;
|
||||
|
@ -237,6 +243,30 @@ class FxAccountsKeys {
|
|||
*
|
||||
*/
|
||||
async _migrateOrFetchKeys(currentState, userData) {
|
||||
// Bug 1697596 - delete any deprecated scoped keys from storage.
|
||||
// If any of the deprecated keys are present, then we know that we've
|
||||
// previously applied all the other migrations below, otherwise there
|
||||
// would not be any `scopedKeys` field.
|
||||
if (userData.scopedKeys) {
|
||||
const toRemove = DEPRECATED_KEY_SCOPES.filter(scope =>
|
||||
userData.scopedKeys.hasOwnProperty(scope)
|
||||
);
|
||||
if (toRemove.length > 0) {
|
||||
for (const scope of toRemove) {
|
||||
delete userData.scopedKeys[scope];
|
||||
}
|
||||
await currentState.updateUserAccountData({
|
||||
scopedKeys: userData.scopedKeys,
|
||||
// Prior to deprecating SCOPE_ECOSYSTEM_TELEMETRY, this file had some
|
||||
// special code to store it as a top-level user data field. So, this
|
||||
// file also gets to delete it as part of the deprecation.
|
||||
ecosystemUserId: null,
|
||||
ecosystemAnonId: null,
|
||||
});
|
||||
userData = await currentState.getUserAccountData();
|
||||
return userData;
|
||||
}
|
||||
}
|
||||
// Bug 1661407 - migrate from legacy storage of keys as top-level account
|
||||
// data fields, to storing them as scoped keys in the `scopedKeys` object.
|
||||
if (
|
||||
|
@ -290,7 +320,7 @@ class FxAccountsKeys {
|
|||
/**
|
||||
* Fetch keys from the server, unwrap them, and derive required sub-keys.
|
||||
*
|
||||
* Once the user's email is verified, we can request the root key `kB` from the
|
||||
* Once the user's email is verified, we can resquest the root key `kB` from the
|
||||
* FxA server, unwrap it using the client-side secret `unwrapBKey`, and then
|
||||
* derive all the sub-keys required for operation of the browser.
|
||||
*/
|
||||
|
@ -394,7 +424,7 @@ class FxAccountsKeys {
|
|||
// Hard-coded list of scopes that we know about.
|
||||
// This list will probably grow in future.
|
||||
// Note that LEGACY_SCOPE_WEBEXT_SYNC is not in this list, it gets special-case handling below.
|
||||
const scopes = [SCOPE_OLD_SYNC, SCOPE_ECOSYSTEM_TELEMETRY].join(" ");
|
||||
const scopes = [SCOPE_OLD_SYNC].join(" ");
|
||||
const scopedKeysMetadata = await this._fxai.fxAccountsClient.getScopedKeyData(
|
||||
sessionToken,
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
|
@ -455,12 +485,6 @@ class FxAccountsKeys {
|
|||
kExtKbHash: scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC]
|
||||
? this.kidAsHex(scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC])
|
||||
: CommonUtils.bytesAsHex(await this._deriveWebExtKbHash(uid, kBbytes)),
|
||||
// The `ecosystemUserId` is derived from `kB` but is used more like an identifier
|
||||
// than a key. We unpack it into the top-level user account data so that it can be
|
||||
// stored in plaintext storage, and used for telemetry even when passwords are locked.
|
||||
ecosystemUserId: scopedKeys[SCOPE_ECOSYSTEM_TELEMETRY]
|
||||
? CommonUtils.base64urlToHex(scopedKeys[SCOPE_ECOSYSTEM_TELEMETRY].k)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -232,27 +232,6 @@ 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,10 +18,7 @@ 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(
|
||||
|
@ -40,7 +37,6 @@ class FxAccountsTelemetry {
|
|||
constructor(fxai) {
|
||||
this._fxai = fxai;
|
||||
Services.telemetry.setEventRecordingEnabled("fxa", true);
|
||||
this._promiseEnsureEcosystemAnonId = null;
|
||||
}
|
||||
|
||||
// Records an event *in the Fxa/Sync ping*.
|
||||
|
@ -64,184 +60,9 @@ class FxAccountsTelemetry {
|
|||
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).
|
||||
//
|
||||
// Instead, AET-related telemetry pings can identify the user by their "ecosystemAnonId",
|
||||
// an encrypted bundle that can communicate the "ecosystemUserId" through to the telemetry
|
||||
// backend without allowing it to be snooped on in transit.
|
||||
//
|
||||
// Thanks to the way this encryption works, it's possible for each signed-in client to have the same
|
||||
// userid but a *different* value for ecosystemAnonId. This may offer some incremental privacy benefits
|
||||
// for the un-processed data, and we can rely on the values all decrypting back to the same ecosystemUserId
|
||||
// value during processing.
|
||||
//
|
||||
// Thus, the code below will try to generate its own unique ecosystemAnonId value if possible, but
|
||||
// will fall back to using a shared value provided by the FxA server if not.
|
||||
|
||||
// Get the user's ecosystemAnonId, or null if it's not available.
|
||||
//
|
||||
// This method is asynchronous because it may need to load data from storage, but it will not
|
||||
// block on network access and will return null rather than throwing an error on failure. This is
|
||||
// designed to simplify usage from telemetry-sending code, which may want to avoid making expensive
|
||||
// network requests.
|
||||
//
|
||||
// If you want to ensure that a value is present then use `ensureEcosystemAnonId()` instead.
|
||||
async getEcosystemAnonId() {
|
||||
return this._fxai.withCurrentAccountState(async state => {
|
||||
// If we know the ecosystemUserId, we generate and store our own unique ecosystemAnonId value.
|
||||
// Otherwise, we may be able to use a shared value from the user's profile data.
|
||||
let {
|
||||
ecosystemAnonId,
|
||||
ecosystemUserId,
|
||||
} = await state.getUserAccountData([
|
||||
"ecosystemAnonId",
|
||||
"ecosystemUserId",
|
||||
]);
|
||||
// N.B. We should never have `ecosystemAnonId` without `ecosystemUserId`.
|
||||
if (!ecosystemUserId) {
|
||||
try {
|
||||
// N.B. `getProfile()` may kick off a silent background update but won't await network requests.
|
||||
const profile = await this._fxai.profile.getProfile();
|
||||
if (profile && profile.hasOwnProperty("ecosystemAnonId")) {
|
||||
ecosystemAnonId = profile.ecosystemAnonId;
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("Getting ecosystemAnonId from profile failed", err);
|
||||
}
|
||||
}
|
||||
// If we don't have ecosystemAnonId, call `ensureEcosystemAnonId()` to fetch or generate it in
|
||||
// the background, so the calling code doesn't have to do this for itself.
|
||||
// (ie, so that the next call to `getEcosystemAnonId() will return it)
|
||||
if (!ecosystemAnonId) {
|
||||
// N.B. deliberately not awaiting the promise here.
|
||||
this.ensureEcosystemAnonId().catch(err => {
|
||||
log.error(
|
||||
"Failed ensuring we have an anon-id in the background ",
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
return ecosystemAnonId || null;
|
||||
});
|
||||
}
|
||||
|
||||
// Get the user's ecosystemAnonId, fetching it from the server if necessary.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// If a call to this is already in-flight, the promise from that original
|
||||
// call is returned.
|
||||
async ensureEcosystemAnonId() {
|
||||
if (!this._promiseEnsureEcosystemAnonId) {
|
||||
this._promiseEnsureEcosystemAnonId = this._ensureEcosystemAnonId().finally(
|
||||
() => {
|
||||
this._promiseEnsureEcosystemAnonId = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
return this._promiseEnsureEcosystemAnonId;
|
||||
}
|
||||
|
||||
async _ensureEcosystemAnonId() {
|
||||
return this._fxai.withCurrentAccountState(async state => {
|
||||
// If we know the ecosystemUserId, we generate and store our own unique ecosystemAnonId value.
|
||||
// Otherwise, we need to work with a shared value stored in the user's profile.
|
||||
let {
|
||||
ecosystemAnonId,
|
||||
ecosystemUserId,
|
||||
} = await state.getUserAccountData([
|
||||
"ecosystemAnonId",
|
||||
"ecosystemUserId",
|
||||
]);
|
||||
if (ecosystemUserId) {
|
||||
if (!ecosystemAnonId) {
|
||||
ecosystemAnonId = await this._generateAnonIdFromUserId(
|
||||
ecosystemUserId
|
||||
);
|
||||
await state.updateUserAccountData({ ecosystemAnonId });
|
||||
}
|
||||
} else {
|
||||
ecosystemAnonId = await this._ensureEcosystemAnonIdInProfile();
|
||||
}
|
||||
return ecosystemAnonId;
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that we have an ecosystemAnonId obtained from account profile data.
|
||||
//
|
||||
// This is a bootstrapping mechanism for clients that are already connected to
|
||||
// the user's account, to obtain ecosystemAnonId from shared profile data rather
|
||||
// than from derived key material.
|
||||
//
|
||||
async _ensureEcosystemAnonIdInProfile(generatePlaceholder = true) {
|
||||
// Fetching a fresh profile should never *change* the ID, but it might
|
||||
// fetch the first value we see, and saving a network request matters for
|
||||
// telemetry, so:
|
||||
// * first time around we are fine with a slightly stale profile - if it
|
||||
// has an ID, it's a stable ID we can be sure is good.
|
||||
// * But if we didn't have one, so generated a new one, but then raced
|
||||
// with another client to update it, we *must* fetch a new profile, even
|
||||
// if our current version is fresh.
|
||||
let options = generatePlaceholder
|
||||
? { staleOk: true }
|
||||
: { forceFresh: true };
|
||||
const profile = await this._fxai.profile.ensureProfile(options);
|
||||
if (profile && 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 then we can fill it in
|
||||
// with a randomly-generated placeholder.
|
||||
const ecosystemUserId = CommonUtils.bufferToHex(
|
||||
CryptoUtils.generateRandomBytes(32)
|
||||
);
|
||||
const ecosystemAnonId = await this._generateAnonIdFromUserId(
|
||||
ecosystemUserId
|
||||
);
|
||||
// Persist the encrypted value to the server so other clients can find it.
|
||||
try {
|
||||
await this._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 this._ensureEcosystemAnonIdInProfile(false);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return ecosystemAnonId;
|
||||
}
|
||||
|
||||
// Generate an ecosystemAnonId value from the given ecosystemUserId.
|
||||
//
|
||||
// To do so, we must fetch the AET public keys from the server, and encrypt
|
||||
// ecosystemUserId into a JWE using one of those keys.
|
||||
//
|
||||
async _generateAnonIdFromUserId(ecosystemUserId) {
|
||||
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)
|
||||
);
|
||||
return jwcrypto.generateJWE(
|
||||
ecosystemKeys[randomKey],
|
||||
new TextEncoder().encode(ecosystemUserId)
|
||||
);
|
||||
}
|
||||
|
||||
// Prior to Account Ecosystem Telemetry, FxA- and Sync-related metrics were submitted in
|
||||
// a special-purpose "sync ping". This ping identified the user by a version of their FxA
|
||||
// uid that was HMAC-ed with a server-side secret key, but this approach provides weaker
|
||||
// privacy than "ecosystemAnonId" above. New metrics should prefer to use AET rather than
|
||||
// the sync ping.
|
||||
// FxA- and Sync-related metrics are submitted in a special-purpose "sync ping". This ping
|
||||
// identifies the user by a version of their FxA uid that is HMAC-ed with a server-side secret
|
||||
// key, in an attempt to provide a bit of anonymity.
|
||||
|
||||
// Secret back-channel by which tokenserver client code can set the hashed UID.
|
||||
// This value conceptually belongs to FxA, but we currently get it from tokenserver,
|
||||
|
|
|
@ -11,11 +11,9 @@ const { XPCOMUtils } = ChromeUtils.import(
|
|||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
const {
|
||||
SCOPE_OLD_SYNC,
|
||||
SCOPE_ECOSYSTEM_TELEMETRY,
|
||||
LEGACY_SCOPE_WEBEXT_SYNC,
|
||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
const { SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsCommon.js"
|
||||
);
|
||||
|
||||
// Some mock key data, in both scoped-key and legacy field formats.
|
||||
const MOCK_ACCOUNT_KEYS = {
|
||||
|
@ -26,11 +24,6 @@ const MOCK_ACCOUNT_KEYS = {
|
|||
"qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg",
|
||||
kty: "oct",
|
||||
},
|
||||
[SCOPE_ECOSYSTEM_TELEMETRY]: {
|
||||
kid: "1234567890123-7u7u7u7u7u7u7u7u7u7u7g",
|
||||
k: "7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u4",
|
||||
kty: "oct",
|
||||
},
|
||||
[LEGACY_SCOPE_WEBEXT_SYNC]: {
|
||||
kid: "1234567890123-3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0",
|
||||
k:
|
||||
|
@ -45,8 +38,6 @@ const MOCK_ACCOUNT_KEYS = {
|
|||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
kExtKbHash:
|
||||
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd ",
|
||||
ecosystemUserId:
|
||||
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
};
|
||||
|
||||
(function initFxAccountsTestingInfrastructure() {
|
||||
|
|
|
@ -20,6 +20,7 @@ const {
|
|||
ONLOGIN_NOTIFICATION,
|
||||
ONLOGOUT_NOTIFICATION,
|
||||
ONVERIFIED_NOTIFICATION,
|
||||
DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
|
||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
const { PromiseUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/PromiseUtils.jsm"
|
||||
|
@ -156,7 +157,7 @@ function MockFxAccountsClient() {
|
|||
this.getScopedKeyData = function(sessionToken, client_id, scopes) {
|
||||
Assert.ok(sessionToken);
|
||||
Assert.equal(client_id, FX_OAUTH_CLIENT_ID);
|
||||
Assert.equal(scopes, SCOPE_OLD_SYNC + " " + SCOPE_ECOSYSTEM_TELEMETRY);
|
||||
Assert.equal(scopes, SCOPE_OLD_SYNC);
|
||||
return new Promise(resolve => {
|
||||
do_timeout(50, () => {
|
||||
resolve({
|
||||
|
@ -166,12 +167,6 @@ function MockFxAccountsClient() {
|
|||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1234567890123,
|
||||
},
|
||||
"https://identity.mozilla.com/ids/ecosystem_telemetry": {
|
||||
identifier: "https://identity.mozilla.com/ids/ecosystem_telemetry",
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1234567890123,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -828,7 +823,6 @@ add_test(function test_getKeyForScope() {
|
|||
Assert.equal(!!user2.kXCS, false);
|
||||
Assert.equal(!!user2.kExtSync, false);
|
||||
Assert.equal(!!user2.kExtKbHash, false);
|
||||
Assert.equal(!!user2.ecosystemUserId, false);
|
||||
// And we still have a key-fetch token and unwrapBKey to use
|
||||
Assert.equal(!!user2.keyFetchToken, true);
|
||||
Assert.equal(!!user2.unwrapBKey, true);
|
||||
|
@ -843,7 +837,6 @@ add_test(function test_getKeyForScope() {
|
|||
Assert.notEqual(null, user3.kXCS);
|
||||
Assert.notEqual(null, user3.kExtSync);
|
||||
Assert.notEqual(null, user3.kExtKbHash);
|
||||
Assert.notEqual(null, user3.ecosystemUserId);
|
||||
Assert.equal(user3.keyFetchToken, undefined);
|
||||
Assert.equal(user3.unwrapBKey, undefined);
|
||||
run_next_test();
|
||||
|
@ -874,11 +867,6 @@ add_task(async function test_getKeyForScope_kb_migration() {
|
|||
"DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
|
||||
kty: "oct",
|
||||
},
|
||||
"https://identity.mozilla.com/ids/ecosystem_telemetry": {
|
||||
kid: "1234567890-ruhbB-qilFS-9bwxlCe4Qw",
|
||||
k: "niMTzlPWb01A2nkO4SkEAUalO7FiQ61yq69X6b8V08Y",
|
||||
kty: "oct",
|
||||
},
|
||||
"sync:addon_storage": {
|
||||
kid: "1234567890123-pBOR6B6JulbJr3BxKVOqIU4Cq_WAjFp4ApLn5NRVARE",
|
||||
k:
|
||||
|
@ -902,10 +890,6 @@ add_task(async function test_getKeyForScope_kb_migration() {
|
|||
newUser.kExtKbHash,
|
||||
"a41391e81e89ba56c9af70712953aa214e02abf5808c5a780292e7e4d4550111"
|
||||
);
|
||||
Assert.equal(
|
||||
newUser.ecosystemUserId,
|
||||
"9e2313ce53d66f4d40da790ee129040146a53bb16243ad72abaf57e9bf15d3c6"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getKeyForScope_scopedKeys_migration() {
|
||||
|
@ -918,7 +902,6 @@ add_task(async function test_getKeyForScope_scopedKeys_migration() {
|
|||
user.kXCS = MOCK_ACCOUNT_KEYS.kXCS;
|
||||
user.kExtSync = MOCK_ACCOUNT_KEYS.kExtSync;
|
||||
user.kExtKbHash = MOCK_ACCOUNT_KEYS.kExtKbHash;
|
||||
Assert.equal(user.ecosystemUserId, null);
|
||||
Assert.equal(user.scopedKeys, null);
|
||||
|
||||
await fxa.setSignedInUser(user);
|
||||
|
@ -926,19 +909,47 @@ add_task(async function test_getKeyForScope_scopedKeys_migration() {
|
|||
let newUser = await fxa._internal.getUserAccountData();
|
||||
Assert.equal(newUser.kA, null);
|
||||
Assert.equal(newUser.kB, null);
|
||||
// It should have correctly formatted the corresponding scoped keys,
|
||||
// but failed to magic the ecosystem-telemetry key out of nowhere.
|
||||
// It should have correctly formatted the corresponding scoped keys.
|
||||
const expectedScopedKeys = { ...MOCK_ACCOUNT_KEYS.scopedKeys };
|
||||
delete expectedScopedKeys[SCOPE_ECOSYSTEM_TELEMETRY];
|
||||
Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys);
|
||||
// And left the existing key fields unchanged.
|
||||
Assert.equal(newUser.kSync, user.kSync);
|
||||
Assert.equal(newUser.kXCS, user.kXCS);
|
||||
Assert.equal(newUser.kExtSync, user.kExtSync);
|
||||
Assert.equal(newUser.kExtKbHash, user.kExtKbHash);
|
||||
Assert.equal(user.ecosystemUserId, null);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_scoped_keys() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = getTestUser("eusebius");
|
||||
|
||||
const EXTRA_SCOPE = "an unknown, but non-deprecated scope";
|
||||
user.verified = true;
|
||||
user.ecosystemUserId = "ecoUserId";
|
||||
user.ecosystemAnonId = "ecoAnonId";
|
||||
user.scopedKeys = {
|
||||
...MOCK_ACCOUNT_KEYS.scopedKeys,
|
||||
[DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY]:
|
||||
MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC],
|
||||
[EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC],
|
||||
};
|
||||
|
||||
await fxa.setSignedInUser(user);
|
||||
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
let newUser = await fxa._internal.getUserAccountData();
|
||||
// It should have removed the deprecated ecosystem_telemetry key,
|
||||
// but left the other keys intact.
|
||||
const expectedScopedKeys = {
|
||||
...MOCK_ACCOUNT_KEYS.scopedKeys,
|
||||
[EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_OLD_SYNC],
|
||||
};
|
||||
Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys);
|
||||
Assert.equal(newUser.ecosystemUserId, null);
|
||||
Assert.equal(newUser.ecosystemAnonId, null);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_getKeyForScope_nonexistent_account() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let bismarck = getTestUser("bismarck");
|
||||
|
|
|
@ -185,68 +185,3 @@ add_task(async function test_rejects_bad_scoped_key_data() {
|
|||
/keyRotationSecret must be a 64-character hex string/
|
||||
);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_graceful_handling_of_missing_ecosystem_telemetry_scope() {
|
||||
const mockClient = {
|
||||
getScopedKeyData: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withExactArgs(
|
||||
"session-token",
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
SCOPE_OLD_SYNC + " " + SCOPE_ECOSYSTEM_TELEMETRY
|
||||
)
|
||||
.returns(
|
||||
Promise.resolve({
|
||||
// N.B. no SCOPE_ECOSYSTEM_TELEMETRY entry.
|
||||
[SCOPE_OLD_SYNC]: {
|
||||
identifier: SCOPE_OLD_SYNC,
|
||||
keyRotationTimestamp: 1234567890000,
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
},
|
||||
})
|
||||
),
|
||||
accountKeys: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withExactArgs("key-fetch-token")
|
||||
.returns({
|
||||
wrapKB: "00000000000000000000000000000000",
|
||||
}),
|
||||
};
|
||||
const mockState = {
|
||||
getUserAccountData: async () => {
|
||||
return {
|
||||
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
||||
sessionToken: "session-token",
|
||||
keyFetchToken: "key-fetch-token",
|
||||
unwrapBKey:
|
||||
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||
};
|
||||
},
|
||||
updateUserAccountData: sinon.spy(async data => {
|
||||
return data;
|
||||
}),
|
||||
};
|
||||
|
||||
const keys = new FxAccountsKeys({ fxAccountsClient: mockClient });
|
||||
await keys._fetchAndUnwrapAndDeriveKeys(
|
||||
mockState,
|
||||
"session-token",
|
||||
"key-fetch-token"
|
||||
);
|
||||
|
||||
Assert.ok(mockState.updateUserAccountData.calledOnce);
|
||||
const userData = mockState.updateUserAccountData.firstCall.args[0];
|
||||
|
||||
Assert.ok(userData.kSync);
|
||||
Assert.ok(userData.scopedKeys[SCOPE_OLD_SYNC]);
|
||||
Assert.equal(userData.ecosystemUserId, null);
|
||||
Assert.equal(userData.scopedKeys[SCOPE_ECOSYSTEM_TELEMETRY], null);
|
||||
|
||||
mockClient.getScopedKeyData.verify();
|
||||
mockClient.accountKeys.verify();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -72,492 +72,3 @@ add_task(function test_sanitize_device_id() {
|
|||
fxAccounts.telemetry._setHashedUID("");
|
||||
Assert.equal(fxAccounts.telemetry.sanitizeDeviceId(MOCK_DEVICE_ID), null);
|
||||
});
|
||||
|
||||
add_task(async function test_getEcosystemAnonId() {
|
||||
const ecosystemAnonId = "aaaaaaaaaaaaaaa";
|
||||
const testCases = [
|
||||
{
|
||||
// testing retrieving the ecosystemAnonId from account state
|
||||
throw: false,
|
||||
accountStateObj: { ecosystemAnonId, ecosystemUserId: "eco-uid" },
|
||||
profileObj: { ecosystemAnonId: "bbbbbbbbbbbbbb" },
|
||||
expectedEcosystemAnonId: ecosystemAnonId,
|
||||
},
|
||||
{
|
||||
// testing retrieving the ecosystemAnonId when the profile contains it
|
||||
throw: false,
|
||||
accountStateObj: {},
|
||||
profileObj: { ecosystemAnonId },
|
||||
expectedEcosystemAnonId: ecosystemAnonId,
|
||||
},
|
||||
{
|
||||
// testing retrieving the ecosystemAnonId when the profile doesn't contain it
|
||||
throw: false,
|
||||
accountStateObj: {},
|
||||
profileObj: {},
|
||||
expectedEcosystemAnonId: null,
|
||||
},
|
||||
{
|
||||
// testing retrieving the ecosystemAnonId when the profile is null
|
||||
throw: true,
|
||||
accountStateObj: {},
|
||||
profileObj: null,
|
||||
expectedEcosystemAnonId: null,
|
||||
},
|
||||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
const profile = new FxAccountsProfile({
|
||||
profileServerUrl: "http://testURL",
|
||||
});
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
profile,
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({
|
||||
getUserAccountData: async () => {
|
||||
return { ...tc.accountStateObj };
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const mockProfile = sinon.mock(profile);
|
||||
const mockTelemetry = sinon.mock(telemetry);
|
||||
|
||||
if (!tc.accountStateObj.ecosystemUserId) {
|
||||
if (tc.throw) {
|
||||
mockProfile
|
||||
.expects("getProfile")
|
||||
.once()
|
||||
.throws(Error);
|
||||
} else {
|
||||
mockProfile
|
||||
.expects("getProfile")
|
||||
.once()
|
||||
.returns(tc.profileObj);
|
||||
}
|
||||
}
|
||||
|
||||
if (tc.expectedEcosystemAnonId) {
|
||||
mockTelemetry.expects("ensureEcosystemAnonId").never();
|
||||
} else {
|
||||
mockTelemetry
|
||||
.expects("ensureEcosystemAnonId")
|
||||
.once()
|
||||
.resolves("dddddddddd");
|
||||
}
|
||||
|
||||
const actualEcoSystemAnonId = await telemetry.getEcosystemAnonId();
|
||||
mockProfile.verify();
|
||||
mockTelemetry.verify();
|
||||
Assert.equal(actualEcoSystemAnonId, tc.expectedEcosystemAnonId);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId_useAnonIdFromAccountState() {
|
||||
// If there's an eco-uid and anon-id in the account state,
|
||||
// we should use them without attempting any other updates.
|
||||
const expectedEcosystemAnonId = "account-state-anon-id";
|
||||
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({
|
||||
getUserAccountData: async () => {
|
||||
return {
|
||||
ecosystemAnonId: expectedEcosystemAnonId,
|
||||
ecosystemUserId: "account-state-eco-uid",
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const actualEcoSystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||
|
||||
Assert.equal(actualEcoSystemAnonId, expectedEcosystemAnonId);
|
||||
});
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId_useUserIdFromAccountState() {
|
||||
// If there's an eco-uid in the account state but not anon-id,
|
||||
// we should generate and save our own unique anon-id.
|
||||
const expectedEcosystemUserId = "02".repeat(32);
|
||||
const expectedEcosystemAnonId = "bbbbbbbbbbbb";
|
||||
|
||||
const mockedUpdate = sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withExactArgs({
|
||||
ecosystemAnonId: expectedEcosystemAnonId,
|
||||
});
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({
|
||||
getUserAccountData: async () => {
|
||||
return {
|
||||
// Note: no ecosystemAnonId field here.
|
||||
ecosystemUserId: expectedEcosystemUserId,
|
||||
};
|
||||
},
|
||||
updateUserAccountData: mockedUpdate,
|
||||
});
|
||||
},
|
||||
});
|
||||
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||
const mockJwcrypto = sinon.mock(jwcrypto);
|
||||
|
||||
mockFxAccountsConfig
|
||||
.expects("fetchConfigDocument")
|
||||
.once()
|
||||
.returns({
|
||||
ecosystem_anon_id_keys: ["testKey"],
|
||||
});
|
||||
|
||||
mockJwcrypto
|
||||
.expects("generateJWE")
|
||||
.once()
|
||||
.withExactArgs("testKey", new TextEncoder().encode(expectedEcosystemUserId))
|
||||
.returns(expectedEcosystemAnonId);
|
||||
|
||||
const actualEcosystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||
Assert.equal(expectedEcosystemAnonId, actualEcosystemAnonId);
|
||||
|
||||
mockFxAccountsConfig.verify();
|
||||
mockJwcrypto.verify();
|
||||
mockedUpdate.verify();
|
||||
});
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId_useValueFromProfile() {
|
||||
// If there's no eco-uid in the account state,
|
||||
// we should use the anon-id value present in the user's profile data.
|
||||
const expectedEcosystemAnonId = "bbbbbbbbbbbb";
|
||||
|
||||
const profileClient = new FxAccountsProfileClient({
|
||||
serverURL: "http://testURL",
|
||||
});
|
||||
const profile = new FxAccountsProfile({ profileClient });
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
profile,
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({
|
||||
getUserAccountData: async () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const mockProfile = sinon.mock(profile);
|
||||
|
||||
mockProfile
|
||||
.expects("ensureProfile")
|
||||
.withArgs(sinon.match({ staleOk: true }))
|
||||
.once()
|
||||
.returns({
|
||||
ecosystemAnonId: expectedEcosystemAnonId,
|
||||
});
|
||||
|
||||
const actualEcosystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||
Assert.equal(expectedEcosystemAnonId, actualEcosystemAnonId);
|
||||
|
||||
mockProfile.verify();
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_ensureEcosystemAnonId_generatePlaceholderInProfile() {
|
||||
// If there's no eco-uid in the account state, and no anon-id in the profile data,
|
||||
// we should generate a placeholder value and persist it to the profile data.
|
||||
const expectedEcosystemUserIdBytes = new Uint8Array(32);
|
||||
const expectedEcosystemUserId = "0".repeat(64);
|
||||
const expectedEcosystemAnonId = "bbbbbbbbbbbb";
|
||||
const profileClient = new FxAccountsProfileClient({
|
||||
serverURL: "http://testURL",
|
||||
});
|
||||
const profile = new FxAccountsProfile({ profileClient });
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
profile,
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({
|
||||
getUserAccountData: async () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const mockProfile = sinon.mock(profile);
|
||||
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||
const mockJwcrypto = sinon.mock(jwcrypto);
|
||||
const mockCryptoUtils = sinon.mock(CryptoUtils);
|
||||
const mockProfileClient = sinon.mock(profileClient);
|
||||
|
||||
mockProfile
|
||||
.expects("ensureProfile")
|
||||
.once()
|
||||
.returns({});
|
||||
|
||||
mockCryptoUtils
|
||||
.expects("generateRandomBytes")
|
||||
.once()
|
||||
.withExactArgs(32)
|
||||
.returns(expectedEcosystemUserIdBytes);
|
||||
|
||||
mockFxAccountsConfig
|
||||
.expects("fetchConfigDocument")
|
||||
.once()
|
||||
.returns({
|
||||
ecosystem_anon_id_keys: ["testKey"],
|
||||
});
|
||||
|
||||
mockJwcrypto
|
||||
.expects("generateJWE")
|
||||
.once()
|
||||
.withExactArgs(
|
||||
"testKey",
|
||||
new TextEncoder().encode(expectedEcosystemUserId)
|
||||
)
|
||||
.returns(expectedEcosystemAnonId);
|
||||
|
||||
mockProfileClient
|
||||
.expects("setEcosystemAnonId")
|
||||
.once()
|
||||
.withExactArgs(expectedEcosystemAnonId)
|
||||
.returns(null);
|
||||
|
||||
const actualEcosystemAnonId = await telemetry.ensureEcosystemAnonId(true);
|
||||
Assert.equal(expectedEcosystemAnonId, actualEcosystemAnonId);
|
||||
|
||||
mockProfile.verify();
|
||||
mockCryptoUtils.verify();
|
||||
mockFxAccountsConfig.verify();
|
||||
mockJwcrypto.verify();
|
||||
mockProfileClient.verify();
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId_failToGenerateKeys() {
|
||||
// If we attempt to generate an anon-id but can't get the right keys,
|
||||
// we should fail with a sensible error.
|
||||
const expectedErrorMessage =
|
||||
"Unable to fetch ecosystem_anon_id_keys from FxA server";
|
||||
const testCases = [
|
||||
{
|
||||
accountStateObj: {},
|
||||
serverConfig: {},
|
||||
},
|
||||
{
|
||||
accountStateObj: {},
|
||||
serverConfig: {
|
||||
ecosystem_anon_id_keys: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
accountStateObj: { ecosystemUserId: "bbbbbbbbbb" },
|
||||
serverConfig: {},
|
||||
},
|
||||
{
|
||||
accountStateObj: { ecosystemUserId: "bbbbbbbbbb" },
|
||||
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({
|
||||
getUserAccountData: async () => {
|
||||
return { ...tc.accountStateObj };
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const mockProfile = sinon.mock(profile);
|
||||
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||
|
||||
if (!tc.accountStateObj.ecosystemUserId) {
|
||||
mockProfile
|
||||
.expects("ensureProfile")
|
||||
.once()
|
||||
.returns({});
|
||||
} else {
|
||||
mockProfile.expects("ensureProfile").never();
|
||||
}
|
||||
|
||||
mockFxAccountsConfig
|
||||
.expects("fetchConfigDocument")
|
||||
.once()
|
||||
.returns(tc.serverConfig);
|
||||
|
||||
try {
|
||||
await telemetry.ensureEcosystemAnonId();
|
||||
} catch (e) {
|
||||
Assert.equal(expectedErrorMessage, e.message);
|
||||
mockProfile.verify();
|
||||
mockFxAccountsConfig.verify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId_selfRace() {
|
||||
// If we somehow end up calling `ensureEcosystemAnonId` twice,
|
||||
// we should serialize the requests rather than generting two
|
||||
// different placeholder ids.
|
||||
const expectedEcosystemAnonId = "self-race-id";
|
||||
|
||||
const profileClient = new FxAccountsProfileClient({
|
||||
serverURL: "http://testURL",
|
||||
});
|
||||
const profile = new FxAccountsProfile({ profileClient });
|
||||
const telemetry = new FxAccountsTelemetry({
|
||||
profile,
|
||||
withCurrentAccountState: async cb => {
|
||||
return cb({
|
||||
getUserAccountData: async () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
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({});
|
||||
|
||||
mockProfileClient
|
||||
.expects("setEcosystemAnonId")
|
||||
.once()
|
||||
.returns(null);
|
||||
|
||||
// We are going to "block" the config document promise and make 2 calls
|
||||
// to ensureEcosystemAnonId() while blocked, just to ensure we don't
|
||||
// actually enter the ensureEcosystemAnonId() impl twice.
|
||||
const deferInConfigDocument = PromiseUtils.defer();
|
||||
const deferConfigDocument = PromiseUtils.defer();
|
||||
mockFxAccountsConfig
|
||||
.expects("fetchConfigDocument")
|
||||
.once()
|
||||
.callsFake(() => {
|
||||
deferInConfigDocument.resolve();
|
||||
return deferConfigDocument.promise;
|
||||
});
|
||||
|
||||
mockJwcrypto
|
||||
.expects("generateJWE")
|
||||
.once()
|
||||
.returns(expectedEcosystemAnonId);
|
||||
|
||||
let p1 = telemetry.ensureEcosystemAnonId();
|
||||
let p2 = telemetry.ensureEcosystemAnonId();
|
||||
|
||||
// Make sure we've entered fetchConfigDocument
|
||||
await deferInConfigDocument.promise;
|
||||
// Let it go.
|
||||
deferConfigDocument.resolve({ ecosystem_anon_id_keys: ["testKey"] });
|
||||
|
||||
Assert.equal(await p1, expectedEcosystemAnonId);
|
||||
Assert.equal(await p2, expectedEcosystemAnonId);
|
||||
|
||||
// And all the `.once()` calls on the mocks are checking we only did the
|
||||
// work once.
|
||||
mockProfile.verify();
|
||||
mockFxAccountsConfig.verify();
|
||||
mockJwcrypto.verify();
|
||||
mockProfileClient.verify();
|
||||
});
|
||||
|
||||
add_task(async function test_ensureEcosystemAnonId_clientRace() {
|
||||
// If we attempt to upload a placeholder anon-id to the user's profile,
|
||||
// and our write conflicts with another client doing a similar upload,
|
||||
// then we should recover and accept the server version.
|
||||
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({
|
||||
getUserAccountData: async () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const mockProfile = sinon.mock(profile);
|
||||
const mockFxAccountsConfig = sinon.mock(FxAccountsConfig);
|
||||
const mockJwcrypto = sinon.mock(jwcrypto);
|
||||
const mockProfileClient = sinon.mock(profileClient);
|
||||
|
||||
mockProfile
|
||||
.expects("ensureProfile")
|
||||
.withArgs(sinon.match({ staleOk: true }))
|
||||
.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")
|
||||
.withArgs(sinon.match({ forceFresh: true }))
|
||||
.once()
|
||||
.returns({
|
||||
ecosystemAnonId: expectedEcosystemAnonId,
|
||||
});
|
||||
|
||||
const actualEcosystemAnonId = await telemetry.ensureEcosystemAnonId();
|
||||
Assert.equal(expectedEcosystemAnonId, actualEcosystemAnonId);
|
||||
} else {
|
||||
try {
|
||||
await telemetry.ensureEcosystemAnonId();
|
||||
} catch (e) {
|
||||
Assert.equal(expectedErrrorMessage, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
mockProfile.verify();
|
||||
mockFxAccountsConfig.verify();
|
||||
mockJwcrypto.verify();
|
||||
mockProfileClient.verify();
|
||||
}
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче