diff options
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_telemetry.js')
-rw-r--r-- | services/fxaccounts/tests/xpcshell/test_telemetry.js | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_telemetry.js b/services/fxaccounts/tests/xpcshell/test_telemetry.js new file mode 100644 index 0000000000..1f09769246 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_telemetry.js @@ -0,0 +1,563 @@ +/* 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(); + } +}); |