зеркало из https://github.com/mozilla/gecko-dev.git
564 строки
16 KiB
JavaScript
564 строки
16 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { fxAccounts, FxAccounts } = ChromeUtils.import(
|
|
"resource://gre/modules/FxAccounts.jsm"
|
|
);
|
|
|
|
const { PREF_ACCOUNT_ROOT } = ChromeUtils.import(
|
|
"resource://gre/modules/FxAccountsCommon.js"
|
|
);
|
|
|
|
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",
|
|
CryptoUtils: "resource://services-crypto/utils.js",
|
|
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
|
|
});
|
|
|
|
_("Misc tests for FxAccounts.telemetry");
|
|
|
|
const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff";
|
|
const MOCK_DEVICE_ID = "ffeeddccbbaa99887766554433221100";
|
|
|
|
add_task(function test_sanitized_uid() {
|
|
Services.prefs.deleteBranch(
|
|
"identity.fxaccounts.account.telemetry.sanitized_uid"
|
|
);
|
|
|
|
// Returns `null` by default.
|
|
Assert.equal(fxAccounts.telemetry.getSanitizedUID(), null);
|
|
|
|
// Returns provided value if set.
|
|
fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID);
|
|
Assert.equal(fxAccounts.telemetry.getSanitizedUID(), MOCK_HASHED_UID);
|
|
|
|
// Reverts to unset for falsey values.
|
|
fxAccounts.telemetry._setHashedUID("");
|
|
Assert.equal(fxAccounts.telemetry.getSanitizedUID(), null);
|
|
});
|
|
|
|
add_task(function test_sanitize_device_id() {
|
|
Services.prefs.deleteBranch(
|
|
"identity.fxaccounts.account.telemetry.sanitized_uid"
|
|
);
|
|
|
|
// Returns `null` by default.
|
|
Assert.equal(fxAccounts.telemetry.sanitizeDeviceId(MOCK_DEVICE_ID), null);
|
|
|
|
// Hashes with the sanitized UID if set.
|
|
// (test value here is SHA256(MOCK_DEVICE_ID + MOCK_HASHED_UID))
|
|
fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID);
|
|
Assert.equal(
|
|
fxAccounts.telemetry.sanitizeDeviceId(MOCK_DEVICE_ID),
|
|
"dd7c845006df9baa1c6d756926519c8ce12f91230e11b6057bf8ec65f9b55c1a"
|
|
);
|
|
|
|
// Reverts to unset for falsey values.
|
|
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();
|
|
}
|
|
});
|