/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { AddonManager, AddonManagerPrivate } = ChromeUtils.import( "resource://gre/modules/AddonManager.jsm" ); ChromeUtils.import("resource://gre/modules/TelemetryEnvironment.jsm", this); ChromeUtils.import("resource://gre/modules/Preferences.jsm", this); ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm", this); ChromeUtils.import("resource://gre/modules/Timer.jsm", this); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", this); const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this); const { FileUtils } = ChromeUtils.import( "resource://gre/modules/FileUtils.jsm" ); const { CommonUtils } = ChromeUtils.import( "resource://services-common/utils.js" ); const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); const { SearchTestUtils } = ChromeUtils.import( "resource://testing-common/SearchTestUtils.jsm" ); if (AppConstants.MOZ_GLEAN) { Cu.importGlobalProperties(["Glean"]); } // AttributionCode is only needed for Firefox ChromeUtils.defineModuleGetter( this, "AttributionCode", "resource:///modules/AttributionCode.jsm" ); ChromeUtils.defineModuleGetter( this, "ExtensionTestUtils", "resource://testing-common/ExtensionXPCShellUtils.jsm" ); SearchTestUtils.init(this); async function installXPIFromURL(url) { let install = await AddonManager.getInstallForURL(url); return install.install(); } function promiseNextTick() { return new Promise(resolve => executeSoon(resolve)); } // The webserver hosting the addons. var gHttpServer = null; // The URL of the webserver root. var gHttpRoot = null; // The URL of the data directory, on the webserver. var gDataRoot = null; const PLATFORM_VERSION = "1.9.2"; const APP_VERSION = "1"; const APP_ID = "xpcshell@tests.mozilla.org"; const APP_NAME = "XPCShell"; const DISTRIBUTION_ID = "distributor-id"; const DISTRIBUTION_VERSION = "4.5.6b"; const DISTRIBUTOR_NAME = "Some Distributor"; const DISTRIBUTOR_CHANNEL = "A Channel"; const PARTNER_NAME = "test"; const PARTNER_ID = "NicePartner-ID-3785"; const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete"; const GFX_VENDOR_ID = "0xabcd"; const GFX_DEVICE_ID = "0x1234"; // The profile reset date, in milliseconds (Today) const PROFILE_RESET_DATE_MS = Date.now(); // The profile creation date, in milliseconds (Yesterday). const PROFILE_FIRST_USE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY; const PROFILE_CREATION_DATE_MS = PROFILE_FIRST_USE_MS - MILLISECONDS_PER_DAY; const FLASH_PLUGIN_NAME = "Shockwave Flash"; const FLASH_PLUGIN_DESC = "A mock flash plugin"; const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d"; const PLUGIN_MIME_TYPE1 = "application/x-shockwave-flash"; const PLUGIN_MIME_TYPE2 = "text/plain"; const PLUGIN2_NAME = "Quicktime"; const PLUGIN2_DESC = "A mock Quicktime plugin"; const PLUGIN2_VERSION = "2.3"; const PLUGIN_UPDATED_TOPIC = "plugins-list-updated"; // system add-ons are enabled at startup, so record date when the test starts const SYSTEM_ADDON_INSTALL_DATE = Date.now(); const EXPECTED_HDD_FIELDS = ["profile", "binary", "system"]; // Valid attribution code to write so that settings.attribution can be tested. const ATTRIBUTION_CODE = "source%3Dgoogle.com"; const pluginHost = Cc["@mozilla.org/plugin/host;1"].getService( Ci.nsIPluginHost ); /** * Used to mock plugin tags in our fake plugin host. */ function PluginTag(aName, aDescription, aVersion, aEnabled) { this.pluginTag = pluginHost.createFakePlugin({ handlerURI: "resource://fake-plugin/${Math.random()}.xhtml", mimeEntries: this.mimeTypes.map(type => ({ type })), name: aName, description: aDescription, fileName: `${aName}.so`, version: aVersion, }); this.name = aName; this.description = aDescription; this.version = aVersion; this.disabled = !aEnabled; } PluginTag.prototype = { name: null, description: null, version: null, filename: null, fullpath: null, blocklisted: false, clicktoplay: true, get disabled() { return this.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED; }, set disabled(val) { this.pluginTag.enabledState = Ci.nsIPluginTag[val ? "STATE_DISABLED" : "STATE_CLICKTOPLAY"]; }, mimeTypes: [PLUGIN_MIME_TYPE1, PLUGIN_MIME_TYPE2], getMimeTypes() { return this.mimeTypes; }, }; // A container for the plugins handled by the fake plugin host. var gInstalledPlugins = [ new PluginTag("Java", "A mock Java plugin", "1.0", false /* Disabled */), new PluginTag( FLASH_PLUGIN_NAME, FLASH_PLUGIN_DESC, FLASH_PLUGIN_VERSION, true ), ]; // A fake plugin host for testing plugin telemetry environment. var PluginHost = { getPluginTags() { return gInstalledPlugins.map(plugin => plugin.pluginTag); }, QueryInterface: ChromeUtils.generateQI(["nsIPluginHost"]), }; function registerFakePluginHost() { MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost); } var SysInfo = { overrides: {}, getProperty(name) { // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides)); if (name in this.overrides) { return this.overrides[name]; } return this._genuine.QueryInterface(Ci.nsIPropertyBag).getProperty(name); }, getPropertyAsACString(name) { return this.get(name); }, getPropertyAsUint32(name) { return this.get(name); }, get(name) { return this._genuine.QueryInterface(Ci.nsIPropertyBag2).get(name); }, get diskInfo() { return this._genuine.QueryInterface(Ci.nsISystemInfo).diskInfo; }, get osInfo() { return this._genuine.QueryInterface(Ci.nsISystemInfo).osInfo; }, get processInfo() { return this._genuine.QueryInterface(Ci.nsISystemInfo).processInfo; }, QueryInterface: ChromeUtils.generateQI(["nsIPropertyBag2", "nsISystemInfo"]), }; function registerFakeSysInfo() { MockRegistrar.register("@mozilla.org/system-info;1", SysInfo); } function MockAddonWrapper(aAddon) { this.addon = aAddon; } MockAddonWrapper.prototype = { get id() { return this.addon.id; }, get type() { return "service"; }, get appDisabled() { return false; }, get isCompatible() { return true; }, get isPlatformCompatible() { return true; }, get scope() { return AddonManager.SCOPE_PROFILE; }, get foreignInstall() { return false; }, get providesUpdatesSecurely() { return true; }, get blocklistState() { return 0; // Not blocked. }, get pendingOperations() { return AddonManager.PENDING_NONE; }, get permissions() { return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE; }, get isActive() { return true; }, get name() { return this.addon.name; }, get version() { return this.addon.version; }, get creator() { return new AddonManagerPrivate.AddonAuthor(this.addon.author); }, get userDisabled() { return this.appDisabled; }, }; function createMockAddonProvider(aName) { let mockProvider = { _addons: [], get name() { return aName; }, addAddon(aAddon) { this._addons.push(aAddon); AddonManagerPrivate.callAddonListeners( "onInstalled", new MockAddonWrapper(aAddon) ); }, async getAddonsByTypes(aTypes) { return this._addons .filter(a => !aTypes || aTypes.includes(a.type)) .map(a => new MockAddonWrapper(a)); }, shutdown() { return Promise.resolve(); }, }; return mockProvider; } function spoofGfxAdapter() { try { let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); gfxInfo.fireTestProcess(); gfxInfo.spoofVendorID(GFX_VENDOR_ID); gfxInfo.spoofDeviceID(GFX_DEVICE_ID); } catch (x) { // If we can't test gfxInfo, that's fine, we'll note it later. } } function spoofProfileReset() { return CommonUtils.writeJSON( { created: PROFILE_CREATION_DATE_MS, reset: PROFILE_RESET_DATE_MS, firstUse: PROFILE_FIRST_USE_MS, }, OS.Path.join(OS.Constants.Path.profileDir, "times.json") ); } function spoofPartnerInfo() { let prefsToSpoof = {}; prefsToSpoof["distribution.id"] = DISTRIBUTION_ID; prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION; prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME; prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL; prefsToSpoof["app.partner.test"] = PARTNER_NAME; prefsToSpoof["mozilla.partner.id"] = PARTNER_ID; // Spoof the preferences. for (let pref in prefsToSpoof) { Preferences.set(pref, prefsToSpoof[pref]); } } async function spoofAttributionData() { if (gIsWindows || gIsMac) { AttributionCode._clearCache(); await AttributionCode.writeAttributionFile(ATTRIBUTION_CODE); } } function cleanupAttributionData() { if (gIsWindows || gIsMac) { AttributionCode.attributionFile.remove(false); AttributionCode._clearCache(); } } /** * Check that a value is a string and not empty. * * @param aValue The variable to check. * @return True if |aValue| has type "string" and is not empty, False otherwise. */ function checkString(aValue) { return typeof aValue == "string" && aValue != ""; } /** * If value is non-null, check if it's a valid string. * * @param aValue The variable to check. * @return True if it's null or a valid string, false if it's non-null and an invalid * string. */ function checkNullOrString(aValue) { if (aValue) { return checkString(aValue); } else if (aValue === null) { return true; } return false; } /** * If value is non-null, check if it's a boolean. * * @param aValue The variable to check. * @return True if it's null or a valid boolean, false if it's non-null and an invalid * boolean. */ function checkNullOrBool(aValue) { return aValue === null || typeof aValue == "boolean"; } function checkBuildSection(data) { const expectedInfo = { applicationId: APP_ID, applicationName: APP_NAME, buildId: gAppInfo.appBuildID, version: APP_VERSION, vendor: "Mozilla", platformVersion: PLATFORM_VERSION, xpcomAbi: "noarch-spidermonkey", }; Assert.ok("build" in data, "There must be a build section in Environment."); for (let f in expectedInfo) { Assert.ok(checkString(data.build[f]), f + " must be a valid string."); Assert.equal( data.build[f], expectedInfo[f], f + " must have the correct value." ); } // Make sure architecture is in the environment. Assert.ok(checkString(data.build.architecture)); Assert.equal( data.build.updaterAvailable, AppConstants.MOZ_UPDATER, "build.updaterAvailable must equal AppConstants.MOZ_UPDATER" ); } function checkSettingsSection(data) { const EXPECTED_FIELDS_TYPES = { blocklistEnabled: "boolean", e10sEnabled: "boolean", e10sMultiProcesses: "number", fissionEnabled: "boolean", intl: "object", locale: "string", telemetryEnabled: "boolean", update: "object", userPrefs: "object", }; Assert.ok( "settings" in data, "There must be a settings section in Environment." ); for (let f in EXPECTED_FIELDS_TYPES) { Assert.equal( typeof data.settings[f], EXPECTED_FIELDS_TYPES[f], f + " must have the correct type." ); } // This property is not always present, but when it is, it must be a number. if ("launcherProcessState" in data.settings) { Assert.equal(typeof data.settings.launcherProcessState, "number"); } // Check "addonCompatibilityCheckEnabled" separately. Assert.equal( data.settings.addonCompatibilityCheckEnabled, AddonManager.checkCompatibility ); // Check "isDefaultBrowser" separately, as it is not available on Android an can either be // null or boolean on other platforms. if (gIsAndroid) { Assert.ok( !("isDefaultBrowser" in data.settings), "Must not be available on Android." ); } else if ("isDefaultBrowser" in data.settings) { // isDefaultBrowser might not be available in the payload, since it's // gathered after the session was restored. Assert.ok(checkNullOrBool(data.settings.isDefaultBrowser)); } // Check "channel" separately, as it can either be null or string. let update = data.settings.update; Assert.ok(checkNullOrString(update.channel)); Assert.equal(typeof update.enabled, "boolean"); Assert.equal(typeof update.autoDownload, "boolean"); // Check "defaultSearchEngine" separately, as it can either be undefined or string. if ("defaultSearchEngine" in data.settings) { checkString(data.settings.defaultSearchEngine); Assert.equal(typeof data.settings.defaultSearchEngineData, "object"); } if ("defaultPrivateSearchEngineData" in data.settings) { Assert.equal(typeof data.settings.defaultPrivateSearchEngineData, "object"); } if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") { Assert.equal(typeof data.settings.attribution, "object"); Assert.equal(data.settings.attribution.source, "google.com"); } checkIntlSettings(data.settings); } function checkIntlSettings({ intl }) { let fields = [ "requestedLocales", "availableLocales", "appLocales", "acceptLanguages", ]; for (let field of fields) { Assert.ok(Array.isArray(intl[field]), `${field} is an array`); } // These fields may be null if they aren't ready yet. This is mostly to deal // with test failures on Android, but they aren't guaranteed to exist. let optionalFields = ["systemLocales", "regionalPrefsLocales"]; for (let field of optionalFields) { let isArray = Array.isArray(intl[field]); let isNull = intl[field] === null; Assert.ok(isArray || isNull, `${field} is an array or null`); } } function checkProfileSection(data) { Assert.ok( "profile" in data, "There must be a profile section in Environment." ); Assert.equal( data.profile.creationDate, truncateToDays(PROFILE_CREATION_DATE_MS) ); Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS)); Assert.equal(data.profile.firstUseDate, truncateToDays(PROFILE_FIRST_USE_MS)); } function checkPartnerSection(data, isInitial) { const EXPECTED_FIELDS = { distributionId: DISTRIBUTION_ID, distributionVersion: DISTRIBUTION_VERSION, partnerId: PARTNER_ID, distributor: DISTRIBUTOR_NAME, distributorChannel: DISTRIBUTOR_CHANNEL, }; Assert.ok( "partner" in data, "There must be a partner section in Environment." ); for (let f in EXPECTED_FIELDS) { let expected = isInitial ? null : EXPECTED_FIELDS[f]; Assert.strictEqual( data.partner[f], expected, f + " must have the correct value." ); } // Check that "partnerNames" exists and contains the correct element. Assert.ok(Array.isArray(data.partner.partnerNames)); if (isInitial) { Assert.equal(data.partner.partnerNames.length, 0); } else { Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME)); } } function checkGfxAdapter(data) { const EXPECTED_ADAPTER_FIELDS_TYPES = { description: "string", vendorID: "string", deviceID: "string", subsysID: "string", RAM: "number", driver: "string", driverVendor: "string", driverVersion: "string", driverDate: "string", GPUActive: "boolean", }; for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) { Assert.ok(f in data, f + " must be available."); if (data[f]) { // Since we have a non-null value, check if it has the correct type. Assert.equal( typeof data[f], EXPECTED_ADAPTER_FIELDS_TYPES[f], f + " must have the correct type." ); } } } function checkSystemSection(data, assertProcessData) { const EXPECTED_FIELDS = [ "memoryMB", "cpu", "os", "hdd", "gfx", "appleModelId", ]; Assert.ok("system" in data, "There must be a system section in Environment."); // Make sure we have all the top level sections and fields. for (let f of EXPECTED_FIELDS) { Assert.ok(f in data.system, f + " must be available."); } Assert.ok( Number.isFinite(data.system.memoryMB), "MemoryMB must be a number." ); if (assertProcessData) { if (gIsWindows || gIsMac || gIsLinux) { let EXTRA_CPU_FIELDS = [ "cores", "model", "family", "stepping", "l2cacheKB", "l3cacheKB", "speedMHz", "vendor", ]; for (let f of EXTRA_CPU_FIELDS) { // Note this is testing TelemetryEnvironment.js only, not that the // values are valid - null is the fallback. Assert.ok(f in data.system.cpu, f + " must be available under cpu."); } if (gIsWindows) { Assert.equal( typeof data.system.isWow64, "boolean", "isWow64 must be available on Windows and have the correct type." ); Assert.equal( typeof data.system.isWowARM64, "boolean", "isWowARM64 must be available on Windows and have the correct type." ); Assert.ok( "virtualMaxMB" in data.system, "virtualMaxMB must be available." ); Assert.ok( Number.isFinite(data.system.virtualMaxMB), "virtualMaxMB must be a number." ); for (let f of [ "count", "model", "family", "stepping", "l2cacheKB", "l3cacheKB", "speedMHz", ]) { Assert.ok( Number.isFinite(data.system.cpu[f]), f + " must be a number if non null." ); } } // These should be numbers if they are not null for (let f of [ "count", "model", "family", "stepping", "l2cacheKB", "l3cacheKB", "speedMHz", ]) { Assert.ok( !(f in data.system.cpu) || data.system.cpu[f] === null || Number.isFinite(data.system.cpu[f]), f + " must be a number if non null." ); } // We insist these are available for (let f of ["cores"]) { Assert.ok( !(f in data.system.cpu) || Number.isFinite(data.system.cpu[f]), f + " must be a number if non null." ); } } } let cpuData = data.system.cpu; Assert.ok( Array.isArray(cpuData.extensions), "CPU extensions must be available." ); let osData = data.system.os; Assert.ok(checkNullOrString(osData.name)); Assert.ok(checkNullOrString(osData.version)); Assert.ok(checkNullOrString(osData.locale)); // Service pack is only available on Windows. if (gIsWindows) { Assert.ok( Number.isFinite(osData.servicePackMajor), "ServicePackMajor must be a number." ); Assert.ok( Number.isFinite(osData.servicePackMinor), "ServicePackMinor must be a number." ); if ("windowsBuildNumber" in osData) { // This might not be available on all Windows platforms. Assert.ok( Number.isFinite(osData.windowsBuildNumber), "windowsBuildNumber must be a number." ); } if ("windowsUBR" in osData) { // This might not be available on all Windows platforms. Assert.ok( osData.windowsUBR === null || Number.isFinite(osData.windowsUBR), "windowsUBR must be null or a number." ); } } else if (gIsAndroid) { Assert.ok(checkNullOrString(osData.kernelVersion)); } for (let disk of EXPECTED_HDD_FIELDS) { Assert.ok(checkNullOrString(data.system.hdd[disk].model)); Assert.ok(checkNullOrString(data.system.hdd[disk].revision)); Assert.ok(checkNullOrString(data.system.hdd[disk].type)); } let gfxData = data.system.gfx; Assert.ok("D2DEnabled" in gfxData); Assert.ok("DWriteEnabled" in gfxData); Assert.ok("Headless" in gfxData); Assert.ok("EmbeddedInFirefoxReality" in gfxData); // DWriteVersion is disabled due to main thread jank and will be enabled // again as part of bug 1154500. // Assert.ok("DWriteVersion" in gfxData); if (gIsWindows) { Assert.equal(typeof gfxData.D2DEnabled, "boolean"); Assert.equal(typeof gfxData.DWriteEnabled, "boolean"); Assert.equal(typeof gfxData.EmbeddedInFirefoxReality, "boolean"); // As above, will be enabled again as part of bug 1154500. // Assert.ok(checkString(gfxData.DWriteVersion)); } Assert.ok("adapters" in gfxData); Assert.ok( !!gfxData.adapters.length, "There must be at least one GFX adapter." ); for (let adapter of gfxData.adapters) { checkGfxAdapter(adapter); } Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean"); Assert.ok( gfxData.adapters[0].GPUActive, "The first GFX adapter must be active." ); Assert.ok(Array.isArray(gfxData.monitors)); if (gIsWindows || gIsMac || gIsLinux) { Assert.ok(gfxData.monitors.length >= 1, "There is at least one monitor."); Assert.equal(typeof gfxData.monitors[0].screenWidth, "number"); Assert.equal(typeof gfxData.monitors[0].screenHeight, "number"); if (gIsWindows) { Assert.equal(typeof gfxData.monitors[0].refreshRate, "number"); Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean"); } if (gIsMac) { Assert.equal(typeof gfxData.monitors[0].scale, "number"); } } Assert.equal(typeof gfxData.features, "object"); Assert.equal(typeof gfxData.features.compositor, "string"); Assert.equal(typeof gfxData.features.gpuProcess, "object"); Assert.equal(typeof gfxData.features.gpuProcess.status, "string"); try { // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing // this test. let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); if (gIsWindows || gIsMac) { Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID); Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID); } let features = gfxInfo.getFeatures(); Assert.equal(features.compositor, gfxData.features.compositor); Assert.equal( features.gpuProcess.status, gfxData.features.gpuProcess.status ); Assert.equal(features.opengl, gfxData.features.opengl); Assert.equal(features.webgl, gfxData.features.webgl); } catch (e) {} if (gIsMac) { Assert.ok(checkString(data.system.appleModelId)); } else { Assert.ok(checkNullOrString(data.system.appleModelId)); } // This feature is only available on Windows 8+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { Assert.ok("sec" in data.system, "sec must be available under data.system"); let SEC_FIELDS = ["antivirus", "antispyware", "firewall"]; for (let f of SEC_FIELDS) { Assert.ok( f in data.system.sec, f + " must be available under data.system.sec" ); let value = data.system.sec[f]; // value is null on Windows Server Assert.ok( value === null || Array.isArray(value), f + " must be either null or an array" ); if (Array.isArray(value)) { for (let product of value) { Assert.equal( typeof product, "string", "Each element of " + f + " must be a string" ); } } } } } function checkActiveAddon(data, partialRecord) { let signedState = "number"; // system add-ons have an undefined signState if (data.isSystem) { signedState = "undefined"; } const EXPECTED_ADDON_FIELDS_TYPES = { version: "string", scope: "number", type: "string", updateDay: "number", isSystem: "boolean", isWebExtension: "boolean", multiprocessCompatible: "boolean", }; const FULL_ADDON_FIELD_TYPES = { blocklisted: "boolean", name: "string", userDisabled: "boolean", appDisabled: "boolean", foreignInstall: "boolean", hasBinaryComponents: "boolean", installDay: "number", signedState, }; let fields = EXPECTED_ADDON_FIELDS_TYPES; if (!partialRecord) { fields = Object.assign({}, fields, FULL_ADDON_FIELD_TYPES); } for (let [name, type] of Object.entries(fields)) { Assert.ok(name in data, name + " must be available."); Assert.equal( typeof data[name], type, name + " must have the correct type." ); } if (!partialRecord) { // We check "description" separately, as it can be null. Assert.ok(checkNullOrString(data.description)); } } function checkPlugin(data) { const EXPECTED_PLUGIN_FIELDS_TYPES = { name: "string", version: "string", description: "string", blocklisted: "boolean", disabled: "boolean", clicktoplay: "boolean", updateDay: "number", }; for (let f in EXPECTED_PLUGIN_FIELDS_TYPES) { Assert.ok(f in data, f + " must be available."); Assert.equal( typeof data[f], EXPECTED_PLUGIN_FIELDS_TYPES[f], f + " must have the correct type." ); } Assert.ok(Array.isArray(data.mimeTypes)); for (let type of data.mimeTypes) { Assert.ok(checkString(type)); } } function checkTheme(data) { const EXPECTED_THEME_FIELDS_TYPES = { id: "string", blocklisted: "boolean", name: "string", userDisabled: "boolean", appDisabled: "boolean", version: "string", scope: "number", foreignInstall: "boolean", installDay: "number", updateDay: "number", }; for (let f in EXPECTED_THEME_FIELDS_TYPES) { Assert.ok(f in data, f + " must be available."); Assert.equal( typeof data[f], EXPECTED_THEME_FIELDS_TYPES[f], f + " must have the correct type." ); } // We check "description" separately, as it can be null. Assert.ok(checkNullOrString(data.description)); } function checkActiveGMPlugin(data) { // GMP plugin version defaults to null until GMPDownloader runs to update it. if (data.version) { Assert.equal(typeof data.version, "string"); } Assert.equal(typeof data.userDisabled, "boolean"); Assert.equal(typeof data.applyBackgroundUpdates, "number"); } function checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords) { const EXPECTED_FIELDS = [ "activeAddons", "theme", "activePlugins", "activeGMPlugins", ]; Assert.ok( "addons" in data, "There must be an addons section in Environment." ); for (let f of EXPECTED_FIELDS) { Assert.ok(f in data.addons, f + " must be available."); } // Check the active addons, if available. if (!expectBrokenAddons) { let activeAddons = data.addons.activeAddons; for (let addon in activeAddons) { checkActiveAddon(activeAddons[addon], partialAddonsRecords); } } // Check "theme" structure. if (Object.keys(data.addons.theme).length !== 0) { checkTheme(data.addons.theme); } // Check the active plugins. Assert.ok(Array.isArray(data.addons.activePlugins)); for (let plugin of data.addons.activePlugins) { checkPlugin(plugin); } // Check active GMPlugins let activeGMPlugins = data.addons.activeGMPlugins; for (let gmPlugin in activeGMPlugins) { checkActiveGMPlugin(activeGMPlugins[gmPlugin]); } } function checkExperimentsSection(data) { // We don't expect the experiments section to be always available. let experiments = data.experiments || {}; if (!Object.keys(experiments).length) { return; } for (let id in experiments) { Assert.ok(checkString(id), id + " must be a valid string."); // Check that we have valid experiment info. let experimentData = experiments[id]; Assert.ok( "branch" in experimentData, "The experiment must have branch data." ); Assert.ok( checkString(experimentData.branch), "The experiment data must be valid." ); if ("type" in experimentData) { Assert.ok(checkString(experimentData.type)); } } } function checkEnvironmentData(data, options = {}) { const { isInitial = false, expectBrokenAddons = false, assertProcessData = false, } = options; checkBuildSection(data); checkSettingsSection(data); checkProfileSection(data); checkPartnerSection(data, isInitial); checkSystemSection(data, assertProcessData); checkAddonsSection(data, expectBrokenAddons); } add_task(async function setup() { registerFakeSysInfo(); spoofGfxAdapter(); do_get_profile(); if (AppConstants.MOZ_GLEAN) { // We need to ensure FOG is initialized, otherwise we will panic trying to get test values. let FOG = Cc["@mozilla.org/toolkit/glean;1"].createInstance(Ci.nsIFOG); FOG.initializeFOG(); } // The system add-on must be installed before AddonManager is started. const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true); do_get_file("system.xpi").copyTo( distroDir, "tel-system-xpi@tests.mozilla.org.xpi" ); let system_addon = FileUtils.File(distroDir.path); system_addon.append("tel-system-xpi@tests.mozilla.org.xpi"); system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE; loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); // The test runs in a fresh profile so starting the AddonManager causes // the addons database to be created (as does setting new theme). // For test_addonsStartup below, we want to test a "warm" startup where // there is already a database on disk. Simulate that here by just // restarting the AddonManager. await AddonTestUtils.promiseShutdownManager(); await AddonTestUtils.overrideBuiltIns({ system: [] }); AddonTestUtils.addonStartup.remove(true); await AddonTestUtils.promiseStartupManager(); // Override ExtensionXPCShellUtils.jsm's overriding of the pref as the // search service needs it. Services.prefs.clearUserPref("services.settings.default_bucket"); // Register a fake plugin host for consistent flash version data. registerFakePluginHost(); // Setup a webserver to serve Addons, Plugins, etc. gHttpServer = new HttpServer(); gHttpServer.start(-1); let port = gHttpServer.identity.primaryPort; gHttpRoot = "http://localhost:" + port + "/"; gDataRoot = gHttpRoot + "data/"; gHttpServer.registerDirectory("/data/", do_get_cwd()); registerCleanupFunction(() => gHttpServer.stop(() => {})); // Create the attribution data file, so that settings.attribution will exist. // The attribution functionality only exists in Firefox. if (AppConstants.MOZ_BUILD_APP == "browser") { spoofAttributionData(); registerCleanupFunction(cleanupAttributionData); } await spoofProfileReset(); await TelemetryEnvironment.delayedInit(); await SearchTestUtils.useTestEngines("data", "search-extensions"); }); add_task(async function test_checkEnvironment() { // During startup we have partial addon records. // First make sure we haven't yet read the addons DB, then test that // we have some partial addons data. Assert.equal( AddonManagerPrivate.isDBLoaded(), false, "addons database is not loaded" ); let data = TelemetryEnvironment.currentEnvironment; checkAddonsSection(data, false, true); // Check that settings.intl is lazily loaded. Assert.equal( typeof data.settings.intl, "object", "intl is initially an object" ); Assert.equal( Object.keys(data.settings.intl).length, 0, "intl is initially empty" ); // Now continue with startup. let initPromise = TelemetryEnvironment.onInitialized(); finishAddonManagerStartup(); // Fake the delayed startup event for intl data to load. fakeIntlReady(); let environmentData = await initPromise; checkEnvironmentData(environmentData, { isInitial: true }); spoofPartnerInfo(); Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC); environmentData = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(environmentData, { assertProcessData: true }); }); add_task(async function test_prefWatchPolicies() { const PREF_TEST_1 = "toolkit.telemetry.test.pref_new"; const PREF_TEST_2 = "toolkit.telemetry.test.pref1"; const PREF_TEST_3 = "toolkit.telemetry.test.pref2"; const PREF_TEST_4 = "toolkit.telemetry.test.pref_old"; const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart"; const expectedValue = "some-test-value"; const unexpectedValue = "unexpected-test-value"; const PREFS_TO_WATCH = new Map([ [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_PREF_VALUE }], [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_PREF_STATE }], [PREF_TEST_3, { what: TelemetryEnvironment.RECORD_PREF_STATE }], [PREF_TEST_4, { what: TelemetryEnvironment.RECORD_PREF_VALUE }], [ PREF_TEST_5, { what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true }, ], ]); Preferences.set(PREF_TEST_4, expectedValue); Preferences.set(PREF_TEST_5, expectedValue); // Set the Environment preferences to watch. await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); let deferred = PromiseUtils.defer(); // Check that the pref values are missing or present as expected Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined ); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], expectedValue ); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5], expectedValue ); TelemetryEnvironment.registerChangeListener( "testWatchPrefs", (reason, data) => deferred.resolve(data) ); let oldEnvironmentData = TelemetryEnvironment.currentEnvironment; // Trigger a change in the watched preferences. Preferences.set(PREF_TEST_1, expectedValue); Preferences.set(PREF_TEST_2, false); Preferences.set(PREF_TEST_5, unexpectedValue); let eventEnvironmentData = await deferred.promise; // Unregister the listener. TelemetryEnvironment.unregisterChangeListener("testWatchPrefs"); // Check environment contains the correct data. Assert.deepEqual(oldEnvironmentData, eventEnvironmentData); let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs; Assert.equal( userPrefs[PREF_TEST_1], expectedValue, "Environment contains the correct preference value." ); Assert.equal( userPrefs[PREF_TEST_2], "", "Report that the pref was user set but the value is not shown." ); Assert.ok( !(PREF_TEST_3 in userPrefs), "Do not report if preference not user set." ); Assert.equal( userPrefs[PREF_TEST_5], expectedValue, "The pref value in the environment data should still be the same" ); }); add_task(async function test_prefWatch_prefReset() { const PREF_TEST = "toolkit.telemetry.test.pref1"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], ]); // Set the preference to a non-default value. Preferences.set(PREF_TEST, false); // Set the Environment preferences to watch. await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "testWatchPrefs_reset", deferred.resolve ); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "" ); // Trigger a change in the watched preferences. Preferences.reset(PREF_TEST); await deferred.promise; Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], undefined ); // Unregister the listener. TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset"); }); add_task(async function test_prefDefault() { const PREF_TEST = "toolkit.telemetry.test.defaultpref1"; const expectedValue = "some-test-value"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }], ]); // Set the preference to a default value. Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue); // Set the Environment preferences to watch. // We're not watching, but this function does the setup we need. await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], expectedValue ); }); add_task(async function test_prefDefaultState() { const PREF_TEST = "toolkit.telemetry.test.defaultpref2"; const expectedValue = "some-test-value"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }], ]); await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); Assert.equal( PREF_TEST in TelemetryEnvironment.currentEnvironment.settings.userPrefs, false ); // Set the preference to a default value. Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "" ); }); add_task(async function test_prefInvalid() { const PREF_TEST_1 = "toolkit.telemetry.test.invalid1"; const PREF_TEST_2 = "toolkit.telemetry.test.invalid2"; const PREFS_TO_WATCH = new Map([ [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }], [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }], ]); await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined ); Assert.strictEqual( TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_2], undefined ); }); add_task(async function test_addonsWatch_InterestingChange() { const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; const ADDON_ID = "tel-restartless-webext@tests.mozilla.org"; // We only expect a single notification for each install, uninstall, enable, disable. const EXPECTED_NOTIFICATIONS = 4; let receivedNotifications = 0; let registerCheckpointPromise = aExpected => { return new Promise(resolve => TelemetryEnvironment.registerChangeListener( "testWatchAddons_Changes" + aExpected, (reason, data) => { Assert.equal(reason, "addons-changed"); receivedNotifications++; resolve(); } ) ); }; let assertCheckpoint = aExpected => { Assert.equal(receivedNotifications, aExpected); TelemetryEnvironment.unregisterChangeListener( "testWatchAddons_Changes" + aExpected ); }; // Test for receiving one notification after each change. let checkpointPromise = registerCheckpointPromise(1); await installXPIFromURL(ADDON_INSTALL_URL); await checkpointPromise; assertCheckpoint(1); Assert.ok( ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons ); checkpointPromise = registerCheckpointPromise(2); let addon = await AddonManager.getAddonByID(ADDON_ID); await addon.disable(); await checkpointPromise; assertCheckpoint(2); Assert.ok( !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons) ); checkpointPromise = registerCheckpointPromise(3); let startupPromise = AddonTestUtils.promiseWebExtensionStartup(ADDON_ID); await addon.enable(); await checkpointPromise; assertCheckpoint(3); Assert.ok( ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons ); await startupPromise; checkpointPromise = registerCheckpointPromise(4); (await AddonManager.getAddonByID(ADDON_ID)).uninstall(); await checkpointPromise; assertCheckpoint(4); Assert.ok( !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons) ); Assert.equal( receivedNotifications, EXPECTED_NOTIFICATIONS, "We must only receive the notifications we expect." ); }); add_task(async function test_pluginsWatch_Add() { if (!gIsFirefox) { Assert.ok(true, "Skipping: there is no Plugin Manager on Android."); return; } Assert.equal( TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 1 ); let newPlugin = new PluginTag( PLUGIN2_NAME, PLUGIN2_DESC, PLUGIN2_VERSION, true ); gInstalledPlugins.push(newPlugin); let receivedNotifications = 0; let callback = (reason, data) => { receivedNotifications++; }; TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback); Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC); await ContentTaskUtils.waitForCondition(() => { return ( TelemetryEnvironment.currentEnvironment.addons.activePlugins.length == 2 ); }); TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add"); Assert.equal( receivedNotifications, 0, "We must not receive any notifications." ); }); add_task(async function test_pluginsWatch_Remove() { if (!gIsFirefox) { Assert.ok(true, "Skipping: there is no Plugin Manager on Android."); return; } Assert.equal( TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 2 ); // Find the test plugin. let plugin = gInstalledPlugins.find(p => p.name == PLUGIN2_NAME); Assert.ok(plugin, "The test plugin must exist."); // Remove it from the PluginHost. gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin); let receivedNotifications = 0; let callback = () => { receivedNotifications++; }; TelemetryEnvironment.registerChangeListener( "testWatchPlugins_Remove", callback ); Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC); await ContentTaskUtils.waitForCondition(() => { return ( TelemetryEnvironment.currentEnvironment.addons.activePlugins.length == 1 ); }); TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove"); Assert.equal( receivedNotifications, 0, "We must not receive any notifications." ); }); add_task(async function test_addonsWatch_NotInterestingChange() { // We are not interested to dictionary addons changes. const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi"; const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; let receivedNotification = false; let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener("testNotInteresting", () => { Assert.ok( !receivedNotification, "Should not receive multiple notifications" ); receivedNotification = true; deferred.resolve(); }); let dictionaryAddon = await installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL); let interestingAddon = await installXPIFromURL(INTERESTING_ADDON_INSTALL_URL); await deferred.promise; Assert.ok( !( "telemetry-dictionary@tests.mozilla.org" in TelemetryEnvironment.currentEnvironment.addons.activeAddons ), "Dictionaries should not appear in active addons." ); TelemetryEnvironment.unregisterChangeListener("testNotInteresting"); dictionaryAddon.uninstall(); await interestingAddon.startupPromise; interestingAddon.uninstall(); }); add_task(async function test_addonsAndPlugins() { const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; const ADDON_ID = "tel-restartless-webext@tests.mozilla.org"; const ADDON_INSTALL_DATE = truncateToDays(Date.now()); const EXPECTED_ADDON_DATA = { blocklisted: false, description: "A restartless addon which gets enabled without a reboot.", name: "XPI Telemetry Restartless Test", userDisabled: false, appDisabled: false, version: "1.0", scope: 1, type: "extension", foreignInstall: false, hasBinaryComponents: false, installDay: ADDON_INSTALL_DATE, updateDay: ADDON_INSTALL_DATE, signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, isSystem: false, isWebExtension: true, multiprocessCompatible: true, }; const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org"; const EXPECTED_SYSTEM_ADDON_DATA = { blocklisted: false, description: "A system addon which is shipped with Firefox.", name: "XPI Telemetry System Add-on Test", userDisabled: false, appDisabled: false, version: "1.0", scope: 1, type: "extension", foreignInstall: false, hasBinaryComponents: false, installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE), updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE), signedState: undefined, isSystem: true, isWebExtension: true, multiprocessCompatible: true, }; const WEBEXTENSION_ADDON_ID = "tel-webextension-xpi@tests.mozilla.org"; const WEBEXTENSION_ADDON_INSTALL_DATE = truncateToDays(Date.now()); const EXPECTED_WEBEXTENSION_ADDON_DATA = { blocklisted: false, description: "A webextension addon.", name: "XPI Telemetry WebExtension Add-on Test", userDisabled: false, appDisabled: false, version: "1.0", scope: 1, type: "extension", foreignInstall: false, hasBinaryComponents: false, installDay: WEBEXTENSION_ADDON_INSTALL_DATE, updateDay: WEBEXTENSION_ADDON_INSTALL_DATE, signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, isSystem: false, isWebExtension: true, multiprocessCompatible: true, }; const EXPECTED_PLUGIN_DATA = { name: FLASH_PLUGIN_NAME, version: FLASH_PLUGIN_VERSION, description: FLASH_PLUGIN_DESC, blocklisted: false, disabled: false, clicktoplay: true, }; let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "test_WebExtension", (reason, data) => { Assert.equal(reason, "addons-changed"); deferred.resolve(); } ); // Install an add-on so we have some data. let addon = await installXPIFromURL(ADDON_INSTALL_URL); // Install a webextension as well. ExtensionTestUtils.init(this); let webextension = ExtensionTestUtils.loadExtension({ useAddonManager: "permanent", manifest: { name: "XPI Telemetry WebExtension Add-on Test", description: "A webextension addon.", version: "1.0", applications: { gecko: { id: WEBEXTENSION_ADDON_ID, }, }, }, }); await webextension.startup(); await deferred.promise; TelemetryEnvironment.unregisterChangeListener("test_WebExtension"); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); // Check addon data. Assert.ok( ADDON_ID in data.addons.activeAddons, "We must have one active addon." ); let targetAddon = data.addons.activeAddons[ADDON_ID]; for (let f in EXPECTED_ADDON_DATA) { Assert.equal( targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value." ); } // Check system add-on data. Assert.ok( SYSTEM_ADDON_ID in data.addons.activeAddons, "We must have one active system addon." ); let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID]; for (let f in EXPECTED_SYSTEM_ADDON_DATA) { Assert.equal( targetSystemAddon[f], EXPECTED_SYSTEM_ADDON_DATA[f], f + " must have the correct value." ); } // Check webextension add-on data. Assert.ok( WEBEXTENSION_ADDON_ID in data.addons.activeAddons, "We must have one active webextension addon." ); let targetWebExtensionAddon = data.addons.activeAddons[WEBEXTENSION_ADDON_ID]; for (let f in EXPECTED_WEBEXTENSION_ADDON_DATA) { Assert.equal( targetWebExtensionAddon[f], EXPECTED_WEBEXTENSION_ADDON_DATA[f], f + " must have the correct value." ); } await webextension.unload(); // Check plugin data. Assert.equal( data.addons.activePlugins.length, 1, "We must have only one active plugin." ); let targetPlugin = data.addons.activePlugins[0]; for (let f in EXPECTED_PLUGIN_DATA) { Assert.equal( targetPlugin[f], EXPECTED_PLUGIN_DATA[f], f + " must have the correct value." ); } // Check plugin mime types. Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1)); Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2)); Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There.")); // Uninstall the addon. await addon.startupPromise; await addon.uninstall(); }); add_task(async function test_signedAddon() { AddonTestUtils.useRealCertChecks = true; const ADDON_INSTALL_URL = gDataRoot + "signed-webext.xpi"; const ADDON_ID = "tel-signed-webext@tests.mozilla.org"; const ADDON_INSTALL_DATE = truncateToDays(Date.now()); const EXPECTED_ADDON_DATA = { blocklisted: false, description: "A signed webextension", name: "XPI Telemetry Signed Test", userDisabled: false, appDisabled: false, version: "1.0", scope: 1, type: "extension", foreignInstall: false, hasBinaryComponents: false, installDay: ADDON_INSTALL_DATE, updateDay: ADDON_INSTALL_DATE, signedState: AddonManager.SIGNEDSTATE_SIGNED, }; let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "test_signedAddon", deferred.resolve ); // Install the addon. let addon = await installXPIFromURL(ADDON_INSTALL_URL); await deferred.promise; // Unregister the listener. TelemetryEnvironment.unregisterChangeListener("test_signedAddon"); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); // Check addon data. Assert.ok( ADDON_ID in data.addons.activeAddons, "Add-on should be in the environment." ); let targetAddon = data.addons.activeAddons[ADDON_ID]; for (let f in EXPECTED_ADDON_DATA) { Assert.equal( targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value." ); } AddonTestUtils.useRealCertChecks = false; await addon.startupPromise; await addon.uninstall(); }); add_task(async function test_addonsFieldsLimit() { const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi"; const ADDON_ID = "tel-longfields-webext@tests.mozilla.org"; // Install the addon and wait for the TelemetryEnvironment to pick it up. let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "test_longFieldsAddon", deferred.resolve ); let addon = await installXPIFromURL(ADDON_INSTALL_URL); await deferred.promise; TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon"); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); // Check that the addon is available and that the string fields are limited. Assert.ok( ADDON_ID in data.addons.activeAddons, "Add-on should be in the environment." ); let targetAddon = data.addons.activeAddons[ADDON_ID]; // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars, // to mitigate misbehaving addons. Assert.lessOrEqual( targetAddon.version.length, 100, "The version string must have been limited" ); Assert.lessOrEqual( targetAddon.name.length, 100, "The name string must have been limited" ); Assert.lessOrEqual( targetAddon.description.length, 100, "The description string must have been limited" ); await addon.startupPromise; await addon.uninstall(); }); add_task(async function test_collectionWithbrokenAddonData() { const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org"; const BROKEN_MANIFEST = { id: "telemetry-test2.example.com@services.mozilla.org", name: "telemetry broken addon", origin: "https://telemetry-test2.example.com", version: 1, // This is intentionally not a string. signedState: AddonManager.SIGNEDSTATE_SIGNED, type: "extension", }; const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; const ADDON_ID = "tel-restartless-webext@tests.mozilla.org"; const ADDON_INSTALL_DATE = truncateToDays(Date.now()); const EXPECTED_ADDON_DATA = { blocklisted: false, description: "A restartless addon which gets enabled without a reboot.", name: "XPI Telemetry Restartless Test", userDisabled: false, appDisabled: false, version: "1.0", scope: 1, type: "extension", foreignInstall: false, hasBinaryComponents: false, installDay: ADDON_INSTALL_DATE, updateDay: ADDON_INSTALL_DATE, signedState: AddonManager.SIGNEDSTATE_MISSING, }; let receivedNotifications = 0; let registerCheckpointPromise = aExpected => { return new Promise(resolve => TelemetryEnvironment.registerChangeListener( "testBrokenAddon_collection" + aExpected, (reason, data) => { Assert.equal(reason, "addons-changed"); receivedNotifications++; resolve(); } ) ); }; let assertCheckpoint = aExpected => { Assert.equal(receivedNotifications, aExpected); TelemetryEnvironment.unregisterChangeListener( "testBrokenAddon_collection" + aExpected ); }; // Register the broken provider and install the broken addon. let checkpointPromise = registerCheckpointPromise(1); let brokenAddonProvider = createMockAddonProvider( "Broken Extensions Provider" ); AddonManagerPrivate.registerProvider(brokenAddonProvider); brokenAddonProvider.addAddon(BROKEN_MANIFEST); await checkpointPromise; assertCheckpoint(1); // Now install an addon which returns the correct information. checkpointPromise = registerCheckpointPromise(2); let addon = await installXPIFromURL(ADDON_INSTALL_URL); await checkpointPromise; assertCheckpoint(2); // Check that the new environment contains the Social addon installed with the broken // manifest and the rest of the data. let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data, { expectBrokenAddons: true }); let activeAddons = data.addons.activeAddons; Assert.ok( BROKEN_ADDON_ID in activeAddons, "The addon with the broken manifest must be reported." ); Assert.equal( activeAddons[BROKEN_ADDON_ID].version, null, "null should be reported for invalid data." ); Assert.ok(ADDON_ID in activeAddons, "The valid addon must be reported."); Assert.equal( activeAddons[ADDON_ID].description, EXPECTED_ADDON_DATA.description, "The description for the valid addon should be correct." ); // Unregister the broken provider so we don't mess with other tests. AddonManagerPrivate.unregisterProvider(brokenAddonProvider); // Uninstall the valid addon. await addon.startupPromise; await addon.uninstall(); }); async function checkDefaultSearch(privateOn, reInitSearchService) { // Start off with separate default engine for private browsing turned off. Preferences.set( "browser.search.separatePrivateDefault.ui.enabled", privateOn ); Preferences.set("browser.search.separatePrivateDefault", privateOn); let data = await TelemetryEnvironment.testCleanRestart().onInitialized(); checkEnvironmentData(data); Assert.ok(!("defaultSearchEngine" in data.settings)); Assert.ok(!("defaultSearchEngineData" in data.settings)); Assert.ok(!("defaultPrivateSearchEngine" in data.settings)); Assert.ok(!("defaultPrivateSearchEngineData" in data.settings)); // Load the engines definitions from a xpcshell data: that's needed so that // the search provider reports an engine identifier. // Initialize the search service. if (reInitSearchService) { Services.search.wrappedJSObject.reset(); } await Services.search.init(); await promiseNextTick(); // Our default engine from the JAR file has an identifier. Check if it is correctly // reported. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier"); let expectedSearchEngineData = { name: "telemetrySearchIdentifier", loadPath: "[other]addEngineWithDetails:telemetrySearchIdentifier@search.mozilla.org", origin: "default", submissionURL: "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceId=Mozilla-search", }; Assert.deepEqual( data.settings.defaultSearchEngineData, expectedSearchEngineData ); if (privateOn) { Assert.equal( data.settings.defaultPrivateSearchEngine, "telemetrySearchIdentifier" ); Assert.deepEqual( data.settings.defaultPrivateSearchEngineData, expectedSearchEngineData, "Should have the correct data for the private search engine" ); } else { Assert.ok( !("defaultPrivateSearchEngine" in data.settings), "Should not have private name recorded as the pref for separate is off" ); Assert.ok( !("defaultPrivateSearchEngineData" in data.settings), "Should not have private data recorded as the pref for separate is off" ); } // Add a new search engine (this will have no engine identifier). const SEARCH_ENGINE_ID = "telemetry_default"; const SEARCH_ENGINE_URL = `http://www.example.org/${ privateOn ? "private" : "" }?search={searchTerms}`; await Services.search.addEngineWithDetails(SEARCH_ENGINE_ID, { method: "get", template: SEARCH_ENGINE_URL, }); // Register a new change listener and then wait for the search engine change to be notified. let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "testWatch_SearchDefault", deferred.resolve ); if (privateOn) { // As we had no default and no search engines, the normal mode engine will // assume the same as the added engine. To ensure the telemetry is different // we enforce a different default here. const engine = await Services.search.getEngineByName( "telemetrySearchIdentifier" ); engine.hidden = false; await Services.search.setDefault(engine); await Services.search.setDefaultPrivate( Services.search.getEngineByName(SEARCH_ENGINE_ID) ); } else { await Services.search.setDefault( Services.search.getEngineByName(SEARCH_ENGINE_ID) ); } await deferred.promise; data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID; const EXPECTED_SEARCH_ENGINE_DATA = { name: "telemetry_default", loadPath: "[other]addEngineWithDetails:telemetry_default@test.engine", origin: "verified", }; if (privateOn) { Assert.equal( data.settings.defaultSearchEngine, "telemetrySearchIdentifier" ); Assert.deepEqual( data.settings.defaultSearchEngineData, expectedSearchEngineData ); Assert.equal( data.settings.defaultPrivateSearchEngine, EXPECTED_SEARCH_ENGINE ); Assert.deepEqual( data.settings.defaultPrivateSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA ); } else { Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); Assert.deepEqual( data.settings.defaultSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA ); } TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); } add_task(async function test_defaultSearchEngine() { await checkDefaultSearch(false); // Cleanly install an engine from an xml file, and check if origin is // recorded as "verified". let promise = new Promise(resolve => { TelemetryEnvironment.registerChangeListener( "testWatch_SearchDefault", resolve ); }); let engine = await new Promise((resolve, reject) => { Services.obs.addObserver(function obs(obsSubject, obsTopic, obsData) { try { let searchEngine = obsSubject.QueryInterface(Ci.nsISearchEngine); info("Observed " + obsData + " for " + searchEngine.name); if ( obsData != "engine-added" || searchEngine.name != "engine-telemetry" ) { return; } Services.obs.removeObserver(obs, "browser-search-engine-modified"); resolve(searchEngine); } catch (ex) { reject(ex); } }, "browser-search-engine-modified"); Services.search.addOpenSearchEngine( "file://" + do_get_cwd().path + "/engine.xml", null ); }); await Services.search.setDefault(engine); await promise; TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.deepEqual(data.settings.defaultSearchEngineData, { name: "engine-telemetry", loadPath: "[other]/engine.xml", origin: "verified", }); // Now break this engine's load path hash. promise = new Promise(resolve => { TelemetryEnvironment.registerChangeListener( "testWatch_SearchDefault", resolve ); }); engine.wrappedJSObject.setAttr("loadPathHash", "broken"); Services.obs.notifyObservers( null, "browser-search-engine-modified", "engine-default" ); await promise; TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); data = TelemetryEnvironment.currentEnvironment; Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid"); await Services.search.removeEngine(engine); const SEARCH_ENGINE_ID = "telemetry_default"; const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID; // Work around bug 1165341: Intentionally set the default engine. await Services.search.setDefault( Services.search.getEngineByName(SEARCH_ENGINE_ID) ); // Double-check the default for the next part of the test. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); // Define and reset the test preference. const PREF_TEST = "toolkit.telemetry.test.pref1"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], ]); Preferences.reset(PREF_TEST); // Watch the test preference. await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "testSearchEngine_pref", deferred.resolve ); // Trigger an environment change. Preferences.set(PREF_TEST, 1); await deferred.promise; TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref"); // Check that the search engine information is correctly retained when prefs change. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); }); add_task(async function test_defaultPrivateSearchEngine() { await checkDefaultSearch(true, true); }); add_task(async function test_defaultSearchEngine_paramsChanged() { let extension = await SearchTestUtils.installSearchExtension({ name: "TestEngine", search_url: "https://www.google.com/fake1", }); let promise = new Promise(resolve => { TelemetryEnvironment.registerChangeListener( "testWatch_SearchDefault", resolve ); }); let engine = Services.search.getEngineByName("TestEngine"); await Services.search.setDefault(engine); await promise; let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.deepEqual(data.settings.defaultSearchEngineData, { name: "TestEngine", loadPath: "[other]addEngineWithDetails:example@tests.mozilla.org", origin: "verified", submissionURL: "https://www.google.com/fake1?q=", }); promise = new Promise(resolve => { TelemetryEnvironment.registerChangeListener( "testWatch_SearchDefault", resolve ); }); engine.wrappedJSObject._updateFromManifest( extension.id, extension.baseURI, SearchTestUtils.createEngineManifest({ name: "TestEngine", version: "1.2", search_url: "https://www.google.com/fake2", }) ); await promise; data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.deepEqual(data.settings.defaultSearchEngineData, { name: "TestEngine", loadPath: "[other]addEngineWithDetails:example@tests.mozilla.org", origin: "verified", submissionURL: "https://www.google.com/fake2?q=", }); await extension.unload(); }); add_task( { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, async function test_delayed_defaultBrowser() { // Skip this test on Thunderbird since it is not a browser, so it cannot // be the default browser. // Make sure we don't have anything already cached for this test. await TelemetryEnvironment.testCleanRestart().onInitialized(); let environmentData = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(environmentData); Assert.equal( environmentData.settings.isDefaultBrowser, null, "isDefaultBrowser must be null before the session is restored." ); Services.obs.notifyObservers(null, "sessionstore-windows-restored"); environmentData = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(environmentData); Assert.ok( "isDefaultBrowser" in environmentData.settings, "isDefaultBrowser must be available after the session is restored." ); Assert.equal( typeof environmentData.settings.isDefaultBrowser, "boolean", "isDefaultBrowser must be of the right type." ); // Make sure pref-flipping doesn't overwrite the browser default state. const PREF_TEST = "toolkit.telemetry.test.pref1"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], ]); Preferences.reset(PREF_TEST); // Watch the test preference. await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "testDefaultBrowser_pref", deferred.resolve ); // Trigger an environment change. Preferences.set(PREF_TEST, 1); await deferred.promise; TelemetryEnvironment.unregisterChangeListener("testDefaultBrowser_pref"); // Check that the data is still available. environmentData = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(environmentData); Assert.ok( "isDefaultBrowser" in environmentData.settings, "isDefaultBrowser must still be available after a pref is flipped." ); } ); add_task(async function test_osstrings() { // First test that numbers in sysinfo properties are converted to string fields // in system.os. SysInfo.overrides = { version: 1, name: 2, kernel_version: 3, }; await TelemetryEnvironment.testCleanRestart().onInitialized(); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.system.os.version, "1"); Assert.equal(data.system.os.name, "2"); if (AppConstants.platform == "android") { Assert.equal(data.system.os.kernelVersion, "3"); } // Check that null values are also handled. SysInfo.overrides = { version: null, name: null, kernel_version: null, }; await TelemetryEnvironment.testCleanRestart().onInitialized(); data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.equal(data.system.os.version, null); Assert.equal(data.system.os.name, null); if (AppConstants.platform == "android") { Assert.equal(data.system.os.kernelVersion, null); } // Clean up. SysInfo.overrides = {}; await TelemetryEnvironment.testCleanRestart().onInitialized(); }); add_task(async function test_experimentsAPI() { const EXPERIMENT1 = "experiment-1"; const EXPERIMENT1_BRANCH = "nice-branch"; const EXPERIMENT2 = "experiment-2"; const EXPERIMENT2_BRANCH = "other-branch"; let checkExperiment = (environmentData, id, branch, type = null) => { Assert.ok( "experiments" in environmentData, "The current environment must report the experiment annotations." ); Assert.ok( id in environmentData.experiments, "The experiments section must contain the expected experiment id." ); Assert.equal( environmentData.experiments[id].branch, branch, "The experiment branch must be correct." ); }; // Clean the environment and check that it's reporting the correct info. await TelemetryEnvironment.testCleanRestart().onInitialized(); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); // We don't expect the experiments section to be there if no annotation // happened. Assert.ok( !("experiments" in data), "No experiments section must be reported if nothing was annotated." ); // Add a change listener and add an experiment annotation. let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "test_experimentsAPI", (reason, env) => { deferred.resolve(env); } ); TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH); let eventEnvironmentData = await deferred.promise; // Check that the old environment does not contain the experiments. checkEnvironmentData(eventEnvironmentData); Assert.ok( !("experiments" in eventEnvironmentData), "No experiments section must be reported in the old environment." ); // Check that the current environment contains the right experiment. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH); TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI"); // Add a second annotation and check that both experiments are there. deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "test_experimentsAPI2", (reason, env) => { deferred.resolve(env); } ); TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH); eventEnvironmentData = await deferred.promise; // Check that the current environment contains both the experiment. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH); checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH); // The previous environment should only contain the first experiment. checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH); Assert.ok( !(EXPERIMENT2 in eventEnvironmentData), "The old environment must not contain the new experiment annotation." ); TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI2"); // Check that removing an unknown experiment annotation does not trigger // a notification. TelemetryEnvironment.registerChangeListener("test_experimentsAPI3", () => { Assert.ok( false, "Removing an unknown experiment annotation must not trigger a change." ); }); TelemetryEnvironment.setExperimentInactive("unknown-experiment-id"); // Also make sure that passing non-string parameters arguments doesn't throw nor // trigger a notification. TelemetryEnvironment.setExperimentActive({}, "some-branch"); TelemetryEnvironment.setExperimentActive("some-id", {}); TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI3"); // Check that removing a known experiment leaves the other in place and triggers // a change. deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener( "test_experimentsAPI4", (reason, env) => { deferred.resolve(env); } ); TelemetryEnvironment.setExperimentInactive(EXPERIMENT1); eventEnvironmentData = await deferred.promise; // Check that the current environment contains just the second experiment. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.ok( !(EXPERIMENT1 in data), "The current environment must not contain the removed experiment annotation." ); checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH); // The previous environment should contain both annotations. checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH); checkExperiment(eventEnvironmentData, EXPERIMENT2, EXPERIMENT2_BRANCH); // Set an experiment with a type and check that it correctly shows up. TelemetryEnvironment.setExperimentActive( "typed-experiment", "random-branch", { type: "ab-test" } ); data = TelemetryEnvironment.currentEnvironment; checkExperiment(data, "typed-experiment", "random-branch", "ab-test"); }); add_task(async function test_experimentsAPI_limits() { const EXPERIMENT = "experiment-2-experiment-2-experiment-2-experiment-2-experiment-2" + "-experiment-2-experiment-2-experiment-2-experiment-2"; const EXPERIMENT_BRANCH = "other-branch-other-branch-other-branch-other-branch-other" + "-branch-other-branch-other-branch-other-branch-other-branch"; const EXPERIMENT_TRUNCATED = EXPERIMENT.substring(0, 100); const EXPERIMENT_BRANCH_TRUNCATED = EXPERIMENT_BRANCH.substring(0, 100); // Clean the environment and check that it's reporting the correct info. await TelemetryEnvironment.testCleanRestart().onInitialized(); let data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); // We don't expect the experiments section to be there if no annotation // happened. Assert.ok( !("experiments" in data), "No experiments section must be reported if nothing was annotated." ); // Add a change listener and wait for the annotation to happen. let deferred = PromiseUtils.defer(); TelemetryEnvironment.registerChangeListener("test_experimentsAPI", () => deferred.resolve() ); TelemetryEnvironment.setExperimentActive(EXPERIMENT, EXPERIMENT_BRANCH); await deferred.promise; // Check that the current environment contains the truncated values // for the experiment data. data = TelemetryEnvironment.currentEnvironment; checkEnvironmentData(data); Assert.ok( "experiments" in data, "The environment must contain an experiments section." ); Assert.ok( EXPERIMENT_TRUNCATED in data.experiments, "The experiments must be reporting the truncated id." ); Assert.ok( !(EXPERIMENT in data.experiments), "The experiments must not be reporting the full id." ); Assert.equal( EXPERIMENT_BRANCH_TRUNCATED, data.experiments[EXPERIMENT_TRUNCATED].branch, "The experiments must be reporting the truncated branch." ); TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI"); // Check that an overly long type is truncated. const longType = "a0123456678901234567890123456789"; TelemetryEnvironment.setExperimentActive("exp", "some-branch", { type: longType, }); data = TelemetryEnvironment.currentEnvironment; Assert.equal(data.experiments.exp.type, longType.substring(0, 20)); }); if (gIsWindows) { add_task(async function test_environmentHDDInfo() { await TelemetryEnvironment.testCleanRestart().onInitialized(); let data = TelemetryEnvironment.currentEnvironment; let empty = { model: null, revision: null, type: null }; Assert.deepEqual( data.system.hdd, { binary: empty, profile: empty, system: empty }, "Should have no data yet." ); await TelemetryEnvironment.delayedInit(); data = TelemetryEnvironment.currentEnvironment; for (let k of EXPECTED_HDD_FIELDS) { checkString(data.system.hdd[k].model); checkString(data.system.hdd[k].revision); checkString(data.system.hdd[k].type); } if (AppConstants.MOZ_GLEAN) { if (data.system.hdd.profile.type == "SSD") { Assert.equal( true, Glean.fogValidation.profileDiskIsSsd.testGetValue(), "SSDness should be recorded in Glean" ); } else { Assert.equal( false, Glean.fogValidation.profileDiskIsSsd.testGetValue(), "nonSSDness should be recorded in Glean" ); } } }); add_task(async function test_environmentProcessInfo() { await TelemetryEnvironment.testCleanRestart().onInitialized(); let data = TelemetryEnvironment.currentEnvironment; Assert.deepEqual(data.system.isWow64, null, "Should have no data yet."); await TelemetryEnvironment.delayedInit(); data = TelemetryEnvironment.currentEnvironment; Assert.equal( typeof data.system.isWow64, "boolean", "isWow64 must be a boolean." ); Assert.equal( typeof data.system.isWowARM64, "boolean", "isWowARM64 must be a boolean." ); // These should be numbers if they are not null for (let f of [ "count", "model", "family", "stepping", "l2cacheKB", "l3cacheKB", "speedMHz", "cores", ]) { Assert.ok( !(f in data.system.cpu) || data.system.cpu[f] === null || Number.isFinite(data.system.cpu[f]), f + " must be a number if non null." ); } Assert.ok( checkString(data.system.cpu.vendor), "vendor must be a valid string." ); }); add_task(async function test_environmentOSInfo() { await TelemetryEnvironment.testCleanRestart().onInitialized(); let data = TelemetryEnvironment.currentEnvironment; Assert.deepEqual( data.system.os.installYear, null, "Should have no data yet." ); await TelemetryEnvironment.delayedInit(); data = TelemetryEnvironment.currentEnvironment; Assert.ok( Number.isFinite(data.system.os.installYear), "Install year must be a number." ); }); } add_task( { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, async function test_environmentServicesInfo() { let cache = TelemetryEnvironment.testCleanRestart(); await cache.onInitialized(); let oldGetFxaSignedInUser = cache._getFxaSignedInUser; try { // Test the 'yes to both' case. // This makes the weave service return that the usere is definitely a sync user Preferences.set("services.sync.username", "c00lperson123@example.com"); let calledFxa = false; cache._getFxaSignedInUser = () => { calledFxa = true; return null; }; await cache._updateServicesInfo(); ok( !calledFxa, "Shouldn't need to ask FxA if they're definitely signed in" ); deepEqual(cache.currentEnvironment.services, { accountEnabled: true, syncEnabled: true, }); // Test the fxa-but-not-sync case. Preferences.reset("services.sync.username"); // We don't actually inspect the returned object, just t cache._getFxaSignedInUser = async () => { return {}; }; await cache._updateServicesInfo(); deepEqual(cache.currentEnvironment.services, { accountEnabled: true, syncEnabled: false, }); // Test the "no to both" case. cache._getFxaSignedInUser = async () => { return null; }; await cache._updateServicesInfo(); deepEqual(cache.currentEnvironment.services, { accountEnabled: false, syncEnabled: false, }); // And finally, the 'fxa is in an error state' case. cache._getFxaSignedInUser = () => { throw new Error("You'll never know"); }; await cache._updateServicesInfo(); equal(cache.currentEnvironment.services, null); } finally { cache._getFxaSignedInUser = oldGetFxaSignedInUser; Preferences.reset("services.sync.username"); } } ); add_task(async function test_normandyTestPrefsGoneAfter91() { const testPrefBool = "app.normandy.test-prefs.bool"; const testPrefInteger = "app.normandy.test-prefs.integer"; const testPrefString = "app.normandy.test-prefs.string"; Services.prefs.setBoolPref(testPrefBool, true); Services.prefs.setIntPref(testPrefInteger, 10); Services.prefs.setCharPref(testPrefString, "test-string"); const data = TelemetryEnvironment.currentEnvironment; if (Services.vc.compare(data.build.version, "91") > 0) { Assert.equal( data.settings.userPrefs["app.normandy.test-prefs.bool"], null, "This probe should expire in FX91. bug 1686105 " ); Assert.equal( data.settings.userPrefs["app.normandy.test-prefs.integer"], null, "This probe should expire in FX91. bug 1686105 " ); Assert.equal( data.settings.userPrefs["app.normandy.test-prefs.string"], null, "This probe should expire in FX91. bug 1686105 " ); } }); add_task(async function test_environmentShutdown() { // Define and reset the test preference. const PREF_TEST = "toolkit.telemetry.test.pref1"; const PREFS_TO_WATCH = new Map([ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], ]); Preferences.reset(PREF_TEST); // Set up the preferences and listener, then the trigger shutdown await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); TelemetryEnvironment.registerChangeListener( "test_environmentShutdownChange", () => { // Register a new change listener that asserts if change is propogated Assert.ok(false, "No change should be propagated after shutdown."); } ); TelemetryEnvironment.shutdown(); // Flipping the test preference after shutdown should not trigger the listener Preferences.set(PREF_TEST, 1); // Unregister the listener. TelemetryEnvironment.unregisterChangeListener( "test_environmentShutdownChange" ); });