Bug 1436113 - Part 2: Refactor "shield-recipe-client" to "normandy" r=Gijs

MozReview-Commit-ID: 8i9Jrq8rj3W

--HG--
extra : rebase_source : 9fb6772a231d214659d024348a52997c74dd5523
extra : amend_source : 312164e67feb3ac43b6b760cad73a2ff6e1f601a
extra : source : 8ccf1c3f156f19293c8a692585a663c5f685d195
This commit is contained in:
Mike Cooper 2018-03-02 11:18:59 -08:00
parent 3c101aa22a
commit 466bc9466e
29 changed files with 333 additions and 286 deletions

View File

@ -1767,3 +1767,16 @@ pref("browser.chrome.errorReporter.infoURL",
#ifdef EARLY_BETA_OR_EARLIER
pref("browser.policies.enabled", true);
#endif
// Normandy client preferences
pref("app.normandy.api_url", "https://normandy.cdn.mozilla.net/api/v1");
pref("app.normandy.dev_mode", false);
pref("app.normandy.enabled", true);
pref("app.normandy.logging.level", 50); // Warn
pref("app.normandy.run_interval_seconds", 86400); // 24 hours
pref("app.normandy.shieldLearnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield");
#ifdef MOZ_DATA_REPORTING
pref("app.shield.optoutstudies.enabled", true);
#else
pref("app.shield.optoutstudies.enabled", false);
#endif

View File

@ -6,8 +6,8 @@
add_task(async function test_policy_disable_shield() {
const { RecipeRunner } = ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", {});
await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.api_url",
"https://localhost/selfsupport-dummy/"],
await SpecialPowers.pushPrefEnv({ set: [["app.normandy.api_url",
"https://localhost/selfsupport-dummy/"],
["datareporting.healthreport.uploadEnabled",
true]]});

View File

@ -64,7 +64,7 @@ user_pref("browser.search.countryCode", "US");
user_pref("browser.search.geoSpecificDefaults", false);
// Make sure Shield doesn't hit the network.
user_pref("extensions.shield-recipe-client.api_url", "https://localhost/selfsupport-dummy/");
user_pref("app.normandy.api_url", "https://localhost/selfsupport-dummy/");
// Make sure Ping Centre doesn't hit the network.
user_pref("browser.ping-centre.staging.endpoint", "https://localhost");

View File

@ -9,11 +9,11 @@ Unreleased
### Added
- New `--jsdebugger` flag to open the Browser Toolbox when Firefox
launches. This is useful for debugging Marionette internals
launches. This is useful for debugging Marionette internals.
- Introduced the temporary, boolean capability
`moz:useNonSpecCompliantPointerOrigin` to disable the WebDriver
conforming behavior of calculating the Pointer Origin
conforming behavior of calculating the Pointer Origin.
### Changed
@ -26,6 +26,8 @@ Unreleased
- `Delete Session` now allows Firefox to safely shutdown within 70s before
force-killing the process
- Changed preference used to disable shield studies to `app.normandy.api_url`.
### Fixed
- Improved error messages for malformed capabilities

View File

@ -152,7 +152,7 @@ lazy_static! {
("extensions.installDistroAddons", Pref::new(false)),
// Make sure Shield doesn't hit the network.
("extensions.shield-recipe-client.api_url", Pref::new("")),
("app.normandy.api_url", Pref::new("")),
("extensions.showMismatchUI", Pref::new(false)),

View File

@ -53,7 +53,7 @@ class GeckoInstance(object):
# Disable intalling any distribution add-ons
"extensions.installDistroAddons": False,
# Make sure Shield doesn't hit the network.
"extensions.shield-recipe-client.api_url": "",
"app.normandy.api_url": "",
"extensions.showMismatchUI": False,
# Turn off extension updates so they don't bother tests
"extensions.update.enabled": False,

View File

@ -313,7 +313,7 @@ user_pref("browser.search.countryCode", "US");
user_pref("browser.search.geoSpecificDefaults", false);
// Make sure Shield doesn't hit the network.
user_pref("extensions.shield-recipe-client.api_url", "");
user_pref("app.normandy.api_url", "");
// Make sure PingCentre doesn't hit the network.
user_pref("browser.ping-centre.staging.endpoint", "");

View File

@ -166,7 +166,7 @@ DEFAULTS = dict(
'media.gmp-manager.updateEnabled': False,
'extensions.systemAddon.update.url':
'http://127.0.0.1/dummy-system-addons.xml',
'extensions.shield-recipe-client.api_url':
'app.normandy.api_url':
'https://127.0.0.1/selfsupport-dummy/',
'browser.ping-centre.staging.endpoint':
'https://127.0.0.1/pingcentre/dummy/',

View File

@ -1473,8 +1473,7 @@ try {
_Services.prefs.setCharPref("media.gmp-manager.url.override", "http://%(server)s/dummy-gmp-manager.xml");
_Services.prefs.setCharPref("media.gmp-manager.updateEnabled", false);
_Services.prefs.setCharPref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
_Services.prefs.setCharPref("extensions.shield-recipe-client.api_url",
"https://%(server)s/selfsupport-dummy/");
_Services.prefs.setCharPref("app.normandy.api_url", "https://%(server)s/selfsupport-dummy/");
_Services.prefs.setCharPref("toolkit.telemetry.server", "https://%(server)s/telemetry-dummy");
_Services.prefs.setCharPref("browser.search.geoip.url", "https://%(server)s/geoip-dummy");
_Services.prefs.setCharPref("browser.safebrowsing.downloads.remote.url", "https://%(server)s/safebrowsing-dummy");

View File

@ -8,33 +8,27 @@ ChromeUtils.import("resource://gre/modules/Log.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "LogManager",
"resource://normandy/lib/LogManager.jsm");
ChromeUtils.defineModuleGetter(this, "ShieldRecipeClient",
"resource://normandy/lib/ShieldRecipeClient.jsm");
ChromeUtils.defineModuleGetter(this, "PreferenceExperiments",
"resource://normandy/lib/PreferenceExperiments.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AboutPages: "resource://normandy-content/AboutPages.jsm",
AddonStudies: "resource://normandy/lib/AddonStudies.jsm",
CleanupManager: "resource://normandy/lib/CleanupManager.jsm",
LogManager: "resource://normandy/lib/LogManager.jsm",
PreferenceExperiments: "resource://normandy/lib/PreferenceExperiments.jsm",
RecipeRunner: "resource://normandy/lib/RecipeRunner.jsm",
ShieldPreferences: "resource://normandy/lib/ShieldPreferences.jsm",
TelemetryEvents: "resource://normandy/lib/TelemetryEvents.jsm",
});
var EXPORTED_SYMBOLS = ["Normandy"];
const UI_AVAILABLE_NOTIFICATION = "sessionstore-windows-restored";
const STARTUP_EXPERIMENT_PREFS_BRANCH = "extensions.shield-recipe-client.startupExperimentPrefs.";
const PREF_LOGGING_LEVEL = "extensions.shield-recipe-client.logging.level";
const BOOTSTRAP_LOGGER_NAME = "extensions.shield-recipe-client.bootstrap";
const DEFAULT_PREFS = {
"extensions.shield-recipe-client.api_url": "https://normandy.cdn.mozilla.net/api/v1",
"extensions.shield-recipe-client.dev_mode": false,
"extensions.shield-recipe-client.enabled": true,
"extensions.shield-recipe-client.startup_delay_seconds": 300,
"extensions.shield-recipe-client.logging.level": Log.Level.Warn,
"extensions.shield-recipe-client.user_id": "",
"extensions.shield-recipe-client.run_interval_seconds": 86400, // 24 hours
"extensions.shield-recipe-client.first_run": true,
"extensions.shield-recipe-client.shieldLearnMoreUrl": (
"https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield"
),
"app.shield.optoutstudies.enabled": AppConstants.MOZ_DATA_REPORTING,
};
const BOOTSTRAP_LOGGER_NAME = "app.normandy.bootstrap";
const SHIELD_INIT_NOTIFICATION = "shield-init-complete";
const PREF_PREFIX = "app.normandy";
const LEGACY_PREF_PREFIX = "extensions.shield-recipe-client";
const STARTUP_EXPERIMENT_PREFS_BRANCH = `${PREF_PREFIX}.startupExperimentPrefs.`;
const PREF_LOGGING_LEVEL = `${PREF_PREFIX}.logging.level`;
// Logging
const log = Log.repository.getLogger(BOOTSTRAP_LOGGER_NAME);
@ -46,7 +40,7 @@ let studyPrefsChanged = {};
var Normandy = {
init() {
// Initialization that needs to happen before the first paint on startup.
this.initShieldPrefs(DEFAULT_PREFS);
this.migrateShieldPrefs();
this.initExperimentPrefs();
// Wait until the UI is available before finishing initialization.
@ -62,12 +56,51 @@ var Normandy = {
async finishInit() {
await PreferenceExperiments.recordOriginalValues(studyPrefsChanged);
ShieldRecipeClient.startup();
// Setup logging and listen for changes to logging prefs
LogManager.configure(Services.prefs.getIntPref(PREF_LOGGING_LEVEL, Log.Level.Warn));
Services.prefs.addObserver(PREF_LOGGING_LEVEL, LogManager.configure);
CleanupManager.addCleanupHandler(
() => Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure),
);
try {
TelemetryEvents.init();
} catch (err) {
log.error("Failed to initialize telemetry events:", err);
}
try {
await AboutPages.init();
} catch (err) {
log.error("Failed to initialize about pages:", err);
}
try {
await AddonStudies.init();
} catch (err) {
log.error("Failed to initialize addon studies:", err);
}
try {
await PreferenceExperiments.init();
} catch (err) {
log.error("Failed to initialize preference experiments:", err);
}
try {
ShieldPreferences.init();
} catch (err) {
log.error("Failed to initialize preferences UI:", err);
}
await RecipeRunner.init();
Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION);
},
async uninit() {
// Wait for async write operations during shutdown before unloading modules.
await ShieldRecipeClient.shutdown();
await CleanupManager.cleanup();
Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure);
// In case the observer didn't run, clean it up.
try {
@ -77,22 +110,45 @@ var Normandy = {
}
},
initShieldPrefs(defaultPrefs) {
const prefBranch = Services.prefs.getDefaultBranch("");
for (const [name, value] of Object.entries(defaultPrefs)) {
switch (typeof value) {
case "string":
prefBranch.setCharPref(name, value);
break;
case "number":
prefBranch.setIntPref(name, value);
break;
case "boolean":
prefBranch.setBoolPref(name, value);
break;
default:
throw new Error(`Invalid default preference type ${typeof value}`);
migrateShieldPrefs() {
const legacyBranch = Services.prefs.getBranch(LEGACY_PREF_PREFIX + ".");
const newBranch = Services.prefs.getBranch(PREF_PREFIX + ".");
for (const prefName of legacyBranch.getChildList("")) {
const legacyPrefType = legacyBranch.getPrefType(prefName);
const newPrefType = newBranch.getPrefType(prefName);
// If new preference exists and is not the same as the legacy pref, skip it
if (newPrefType !== Services.prefs.PREF_INVALID && newPrefType !== legacyPrefType) {
log.error(`Error migrating normandy pref ${prefName}; pref type does not match.`);
continue;
}
// Now move the value over. If it matches the default, this will be a no-op
switch (legacyPrefType) {
case Services.prefs.PREF_STRING:
newBranch.setCharPref(prefName, legacyBranch.getCharPref(prefName));
break;
case Services.prefs.PREF_INT:
newBranch.setIntPref(prefName, legacyBranch.getIntPref(prefName));
break;
case Services.prefs.PREF_BOOL:
newBranch.setBoolPref(prefName, legacyBranch.getBoolPref(prefName));
break;
case Services.prefs.PREF_INVALID:
// This should never happen.
log.error(`Error migrating pref ${prefName}; pref type is invalid (${legacyPrefType}).`);
break;
default:
// This should never happen either.
log.error(`Error getting startup pref ${prefName}; unknown value type ${legacyPrefType}.`);
}
legacyBranch.clearUserPref(prefName);
}
},

View File

@ -19,7 +19,7 @@ ChromeUtils.defineModuleGetter(
var EXPORTED_SYMBOLS = ["AboutPages"];
const SHIELD_LEARN_MORE_URL_PREF = "extensions.shield-recipe-client.shieldLearnMoreUrl";
const SHIELD_LEARN_MORE_URL_PREF = "app.normandy.shieldLearnMoreUrl";
// Due to bug 1051238 frame scripts are cached forever, so we can't update them
// as a restartless add-on. The Math.random() is the work around for this.

View File

@ -1,22 +1,22 @@
Data Collection
===============
This document describes the types of data that Shield collects.
This document describes the types of data that Normandy collects.
Uptake
------
Shield monitors the execution of recipes and reports to
Normandy monitors the execution of recipes and reports to
:ref:`telemetry` the amount of successful and failed runs. This data
is reported using :ref:`telemetry/collection/uptake` under the
``shield-recipe-client`` namespace.
``normandy`` namespace.
Runner Status
^^^^^^^^^^^^^
Once per-fetch and execution of recipes, one of the following statuses is
reported under the key ``shield-recipe-client/runner``:
reported under the key ``normandy/runner``:
.. data:: RUNNER_INVALID_SIGNATURE
Shield failed to verify the signature of the fetched recipes.
Normandy failed to verify the signature of the fetched recipes.
.. data:: RUNNER_NETWORK_ERROR
@ -34,9 +34,9 @@ reported under the key ``shield-recipe-client/runner``:
Action Status
^^^^^^^^^^^^^
For each action available from the Shield service, one of the
For each action available from the Normandy service, one of the
following statuses is reported under the key
``shield-recipe-client/action/<action name>``:
``normandy/action/<action name>``:
.. data:: ACTION_NETWORK_ERROR
@ -63,7 +63,7 @@ following statuses is reported under the key
Recipe Status
^^^^^^^^^^^^^
For each recipe that is fetched and executed, one of the following statuses is
reported under the key ``shield-recipe-client/recipe/<recipe id>``:
reported under the key ``normandy/recipe/<recipe id>``:
.. data:: RECIPE_ACTION_DISABLED
@ -85,7 +85,7 @@ reported under the key ``shield-recipe-client/recipe/<recipe id>``:
Enrollment
-----------
Shield records enrollment and unenrollment of users into studies, and
Normandy records enrollment and unenrollment of users into studies, and
records that data using `Telemetry Events`_. All data is stored in the
``normandy`` category.
@ -134,7 +134,7 @@ Unenrollment
changed the preference, or that some other mechanism set a
non-default value for the preference.
* ``"user-preference-changed-sideload"``: The study
preference was changed on the user branch while Shield was
preference was changed on the user branch while Normandy was
inactive. This could mean that the value was manually
changed in a profile while Firefox was not running.
* ``"unknown"``: A reason was not specificied. This should be
@ -196,7 +196,7 @@ Unenrollment
mechanism. For example, this could be a user action or the
add-on self-uninstalling.
* ``"uninstalled-sideload"``: The study's add-on was
uninstalled while Shield was inactive. This could be that
uninstalled while Normandy was inactive. This could be that
the add-on is no longer compatible, or was manually removed
from a profile.
* ``"unknown"``: A reason was not specified. This should be

View File

@ -342,6 +342,8 @@ var AddonStudies = {
throw new Error(`No study found for recipe ${recipeId}.`);
}
if (!study.active) {
dump(`@@@ Cannot stop study for recipe ${recipeId}; it is already inactive.\n`);
dump(`@@@\n${new Error().stack}\n@@@\n`);
throw new Error(`Cannot stop study for recipe ${recipeId}; it is already inactive.`);
}

View File

@ -77,11 +77,11 @@ var ClientEnvironment = {
const environment = {};
XPCOMUtils.defineLazyGetter(environment, "userId", () => {
let id = Preferences.get("extensions.shield-recipe-client.user_id", "");
let id = Preferences.get("app.normandy.user_id", "");
if (!id) {
// generateUUID adds leading and trailing "{" and "}". strip them off.
id = generateUUID().toString().slice(1, -1);
Preferences.set("extensions.shield-recipe-client.user_id", id);
Preferences.set("app.normandy.user_id", id);
}
return id;
});
@ -204,7 +204,7 @@ var ClientEnvironment = {
});
XPCOMUtils.defineLazyGetter(environment, "isFirstRun", () => {
return Preferences.get("extensions.shield-recipe-client.first_run");
return Preferences.get("app.normandy.first_run");
});
return environment;

View File

@ -8,7 +8,7 @@ ChromeUtils.import("resource://gre/modules/Log.jsm");
var EXPORTED_SYMBOLS = ["LogManager"];
const ROOT_LOGGER_NAME = "extensions.shield-recipe-client";
const ROOT_LOGGER_NAME = "app.normandy";
let rootLogger = null;
var LogManager = {

View File

@ -16,7 +16,7 @@ Cu.importGlobalProperties(["fetch", "URL"]); /* globals fetch, URL */
var EXPORTED_SYMBOLS = ["NormandyApi"];
const log = LogManager.getLogger("normandy-api");
const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
const prefs = Services.prefs.getBranch("app.normandy.");
let indexPromise = null;

View File

@ -65,7 +65,7 @@ ChromeUtils.defineModuleGetter(this, "TelemetryEvents", "resource://normandy/lib
var EXPORTED_SYMBOLS = ["PreferenceExperiments"];
const EXPERIMENT_FILE = "shield-preference-experiments.json";
const STARTUP_EXPERIMENT_PREFS_BRANCH = "extensions.shield-recipe-client.startupExperimentPrefs.";
const STARTUP_EXPERIMENT_PREFS_BRANCH = "app.normandy.startupExperimentPrefs.";
const MAX_EXPERIMENT_TYPE_LENGTH = 20; // enforced by TelemetryEnvironment
const EXPERIMENT_TYPE_PREFIX = "normandy-";

View File

@ -43,13 +43,13 @@ const PREF_CHANGED_TOPIC = "nsPref:changed";
const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
const SHIELD_PREF_PREFIX = "extensions.shield-recipe-client";
const RUN_INTERVAL_PREF = `${SHIELD_PREF_PREFIX}.run_interval_seconds`;
const FIRST_RUN_PREF = `${SHIELD_PREF_PREFIX}.first_run`;
const SHIELD_ENABLED_PREF = `${SHIELD_PREF_PREFIX}.enabled`;
const DEV_MODE_PREF = `${SHIELD_PREF_PREFIX}.dev_mode`;
const API_URL_PREF = `${SHIELD_PREF_PREFIX}.api_url`;
const LAZY_CLASSIFY_PREF = `${SHIELD_PREF_PREFIX}.experiments.lazy_classify`;
const PREF_PREFIX = "app.normandy";
const RUN_INTERVAL_PREF = `${PREF_PREFIX}.run_interval_seconds`;
const FIRST_RUN_PREF = `${PREF_PREFIX}.first_run`;
const SHIELD_ENABLED_PREF = `${PREF_PREFIX}.enabled`;
const DEV_MODE_PREF = `${PREF_PREFIX}.dev_mode`;
const API_URL_PREF = `${PREF_PREFIX}.api_url`;
const LAZY_CLASSIFY_PREF = `${PREF_PREFIX}.experiments.lazy_classify`;
const PREFS_TO_WATCH = [
RUN_INTERVAL_PREF,
@ -65,8 +65,8 @@ var RecipeRunner = {
this.watchPrefs();
// Run if enabled immediately on first run, or if dev mode is enabled.
const firstRun = Services.prefs.getBoolPref(FIRST_RUN_PREF);
const devMode = Services.prefs.getBoolPref(DEV_MODE_PREF);
const firstRun = Services.prefs.getBoolPref(FIRST_RUN_PREF, true);
const devMode = Services.prefs.getBoolPref(DEV_MODE_PREF, false);
if (this.enabled && (devMode || firstRun)) {
await this.run();

View File

@ -11,7 +11,7 @@ ChromeUtils.defineModuleGetter(
var EXPORTED_SYMBOLS = ["Uptake"];
const SOURCE_PREFIX = "shield-recipe-client";
const SOURCE_PREFIX = "normandy";
var Uptake = {
// Action uptake

View File

@ -7,6 +7,7 @@ head = head.js
# Skip this test when FHR/Telemetry aren't available.
skip-if = !healthreport || !telemetry
[browser_about_studies.js]
skip-if = true # bug 1442712
[browser_ActionSandboxManager.js]
[browser_Addons.js]
[browser_AddonStudies.js]
@ -21,5 +22,4 @@ skip-if = !healthreport || !telemetry
[browser_PreferenceExperiments.js]
[browser_RecipeRunner.js]
[browser_ShieldPreferences.js]
[browser_ShieldRecipeClient.js]
[browser_Storage.js]

View File

@ -347,7 +347,7 @@ decorate_task(
);
// Only activeUninstalledStudy should have generated any events
ok(sendEventStub.calledOnce);
ok(sendEventStub.calledOnce, "no extra events should be generated");
}
);

View File

@ -33,7 +33,7 @@ add_task(async function testUserId() {
ok(UUID_REGEX.test(environment.userId), "userId available");
// test that it pulls from the right preference
await SpecialPowers.pushPrefEnv({set: [["extensions.shield-recipe-client.user_id", "fake id"]]});
await SpecialPowers.pushPrefEnv({set: [["app.normandy.user_id", "fake id"]]});
environment = ClientEnvironment.getEnvironment();
is(environment.userId, "fake id", "userId is pulled from preferences");
});
@ -138,7 +138,7 @@ add_task(withDriver(Assert, async function testAddonsInContext(driver) {
}));
add_task(async function isFirstRun() {
await SpecialPowers.pushPrefEnv({set: [["extensions.shield-recipe-client.first_run", true]]});
await SpecialPowers.pushPrefEnv({set: [["app.normandy.first_run", true]]});
const environment = ClientEnvironment.getEnvironment();
ok(environment.isFirstRun, "isFirstRun is read from preferences");
});

View File

@ -1,81 +1,35 @@
"use strict";
ChromeUtils.import("resource://normandy/Normandy.jsm", this);
ChromeUtils.import("resource://normandy/lib/ShieldRecipeClient.jsm", this);
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
const initPref1 = "test.initShieldPrefs1";
const initPref2 = "test.initShieldPrefs2";
const initPref3 = "test.initShieldPrefs3";
ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
const experimentPref1 = "test.initExperimentPrefs1";
const experimentPref2 = "test.initExperimentPrefs2";
const experimentPref3 = "test.initExperimentPrefs3";
const experimentPref4 = "test.initExperimentPrefs4";
decorate_task(
async function testInitShieldPrefs() {
const defaultBranch = Services.prefs.getDefaultBranch("");
const prefDefaults = {
[initPref1]: true,
[initPref2]: 2,
[initPref3]: "string",
};
for (const pref of Object.keys(prefDefaults)) {
is(
defaultBranch.getPrefType(pref),
defaultBranch.PREF_INVALID,
`Pref ${pref} don't exist before being initialized.`,
);
}
Normandy.initShieldPrefs(prefDefaults);
ok(
defaultBranch.getBoolPref(initPref1),
`Pref ${initPref1} has a default value after being initialized.`,
);
is(
defaultBranch.getIntPref(initPref2),
2,
`Pref ${initPref2} has a default value after being initialized.`,
);
is(
defaultBranch.getCharPref(initPref3),
"string",
`Pref ${initPref3} has a default value after being initialized.`,
);
for (const pref of Object.keys(prefDefaults)) {
ok(
!defaultBranch.prefHasUserValue(pref),
`Pref ${pref} doesn't have a user value after being initialized.`,
);
}
defaultBranch.deleteBranch("test.");
},
);
decorate_task(
async function testInitShieldPrefsError() {
Assert.throws(
() => Normandy.initShieldPrefs({"test.prefTypeError": new Date()}),
"initShieldPrefs throws when given an invalid type for the pref value.",
);
},
);
function withStubInits(testFunction) {
return decorate(
withStub(AboutPages, "init"),
withStub(AddonStudies, "init"),
withStub(PreferenceExperiments, "init"),
withStub(RecipeRunner, "init"),
withStub(TelemetryEvents, "init"),
testFunction
);
}
decorate_task(
withPrefEnv({
set: [
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref1}`, true],
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref2}`, 2],
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref3}`, "string"],
[`app.normandy.startupExperimentPrefs.${experimentPref1}`, true],
[`app.normandy.startupExperimentPrefs.${experimentPref2}`, 2],
[`app.normandy.startupExperimentPrefs.${experimentPref3}`, "string"],
],
clear: [[experimentPref1], [experimentPref2], [experimentPref3]],
}),
async function testInitExperimentPrefs() {
const defaultBranch = Services.prefs.getDefaultBranch("");
@ -109,6 +63,8 @@ decorate_task(
!defaultBranch.prefHasUserValue(pref),
`Pref ${pref} doesn't have a user value after being initialized.`,
);
Services.prefs.clearUserPref(pref);
defaultBranch.deleteBranch(pref);
}
},
);
@ -116,7 +72,7 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [
["extensions.shield-recipe-client.startupExperimentPrefs.test.existingPref", "experiment"],
["app.normandy.startupExperimentPrefs.test.existingPref", "experiment"],
],
}),
async function testInitExperimentPrefsExisting() {
@ -134,7 +90,7 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [
["extensions.shield-recipe-client.startupExperimentPrefs.test.mismatchPref", "experiment"],
["app.normandy.startupExperimentPrefs.test.mismatchPref", "experiment"],
],
}),
async function testInitExperimentPrefsMismatch() {
@ -171,17 +127,10 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref1}`, true],
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref2}`, 2],
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref3}`, "string"],
[`extensions.shield-recipe-client.startupExperimentPrefs.${experimentPref4}`, "another string"],
],
clear: [
[experimentPref1],
[experimentPref2],
[experimentPref3],
[experimentPref4],
["extensions.shield-recipe-client.startupExperimentPrefs.existingPref"],
[`app.normandy.startupExperimentPrefs.${experimentPref1}`, true],
[`app.normandy.startupExperimentPrefs.${experimentPref2}`, 2],
[`app.normandy.startupExperimentPrefs.${experimentPref3}`, "string"],
[`app.normandy.startupExperimentPrefs.${experimentPref4}`, "another string"],
],
}),
withStub(PreferenceExperiments, "recordOriginalValues"),
@ -206,6 +155,11 @@ decorate_task(
}],
"finishInit should record original values of the prefs initExperimentPrefs changed",
);
for (const pref of [experimentPref1, experimentPref2, experimentPref3, experimentPref4]) {
Services.prefs.clearUserPref(pref);
defaultBranch.deleteBranch(pref);
}
},
);
@ -213,7 +167,7 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [
["extensions.shield-recipe-client.startupExperimentPrefs.testing.does-not-exist", "foo"],
["app.normandy.startupExperimentPrefs.testing.does-not-exist", "foo"],
["testing.does-not-exist", "foo"],
],
}),
@ -223,3 +177,104 @@ decorate_task(
ok(true, "initExperimentPrefs should not throw for non-existant prefs");
},
);
decorate_task(
withStubInits,
async function testStartup() {
const initObserved = TestUtils.topicObserved("shield-init-complete");
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
await initObserved;
}
);
decorate_task(
withStubInits,
async function testStartupPrefInitFail() {
PreferenceExperiments.init.returns(Promise.reject(new Error("oh no")));
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withStubInits,
async function testStartupAboutPagesInitFail() {
AboutPages.init.returns(Promise.reject(new Error("oh no")));
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withStubInits,
async function testStartupAddonStudiesInitFail() {
AddonStudies.init.returns(Promise.reject(new Error("oh no")));
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withStubInits,
async function testStartupTelemetryEventsInitFail() {
TelemetryEvents.init.throws();
await Normandy.finishInit();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withMockPreferences,
async function testPrefMigration(mockPreferences) {
const legacyPref = "extensions.shield-recipe-client.test";
const migratedPref = "app.normandy.test";
mockPreferences.set(legacyPref, 1);
ok(
Services.prefs.prefHasUserValue(legacyPref),
"Legacy pref should have a user value before running migration",
);
ok(
!Services.prefs.prefHasUserValue(migratedPref),
"Migrated pref should not have a user value before running migration",
);
Normandy.migrateShieldPrefs();
ok(
!Services.prefs.prefHasUserValue(legacyPref),
"Legacy pref should not have a user value after running migration",
);
ok(
Services.prefs.prefHasUserValue(migratedPref),
"Migrated pref should have a user value after running migration",
);
is(Services.prefs.getIntPref(migratedPref), 1, "Value should have been migrated");
Services.prefs.clearUserPref(migratedPref);
},
);

View File

@ -9,7 +9,7 @@ ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
// Save ourselves some typing
const {withMockExperiments} = PreferenceExperiments;
const DefaultPreferences = new Preferences({defaultBranch: true});
const startupPrefs = "extensions.shield-recipe-client.startupExperimentPrefs";
const startupPrefs = "app.normandy.startupExperimentPrefs";
function experimentFactory(attrs) {
return Object.assign({
@ -1049,17 +1049,20 @@ decorate_task(
decorate_task(
withMockPreferences,
withStub(TelemetryEvents, "sendEvent"),
async function testPrefChangeEventTelemetry(mockPreferences, sendEventStub) {
withMockExperiments,
async function testPrefChangeEventTelemetry(mockPreferences, sendEventStub, mockExperiments) {
is(Preferences.get("fake.preference"), null, "preference should start unset");
await PreferenceExperiments.start({
mockPreferences.set("fake.preference", "oldvalue", "default");
mockExperiments.test = experimentFactory({
name: "test",
branch: "branch",
expired: false,
preferenceName: "fake.preference",
preferenceValue: "experimentvalue",
preferenceBranchType: "default",
preferenceType: "string",
previousPreferenceValue: "oldvalue",
preferenceBranchType: "default",
});
PreferenceExperiments.startObserver("test", "fake.preference", "string", "experimentvalue");
// setting the preference on the user branch should trigger the observer to stop the experiment
mockPreferences.set("fake.preference", "uservalue", "user");
@ -1067,9 +1070,8 @@ decorate_task(
// let the event loop tick to run the observer
await Promise.resolve();
is(sendEventStub.getCall(0).args[0], "enroll", "There is an enrollment event from start()");
Assert.deepEqual(
sendEventStub.getCall(1).args,
sendEventStub.getCall(0).args,
["unenroll", "preference_study", "test", {
didResetValue: "false",
reason: "user-preference-changed",

View File

@ -73,13 +73,13 @@ decorate_task(
getStub.returns(Promise.resolve(false));
await SpecialPowers.pushPrefEnv({set: [
["extensions.shield-recipe-client.api_url",
["app.normandy.api_url",
"https://example.com/selfsupport-dummy"],
]});
// When the experiment pref is false, eagerly call getClientClassification.
await SpecialPowers.pushPrefEnv({set: [
["extensions.shield-recipe-client.experiments.lazy_classify", false],
["app.normandy.experiments.lazy_classify", false],
]});
ok(!getStub.called, "getClientClassification hasn't been called");
await RecipeRunner.run();
@ -87,7 +87,7 @@ decorate_task(
// When the experiment pref is true, do not eagerly call getClientClassification.
await SpecialPowers.pushPrefEnv({set: [
["extensions.shield-recipe-client.experiments.lazy_classify", true],
["app.normandy.experiments.lazy_classify", true],
]});
getStub.reset();
ok(!getStub.called, "getClientClassification hasn't been called");
@ -342,8 +342,8 @@ decorate_task(
withPrefEnv({
set: [
["datareporting.healthreport.uploadEnabled", true], // telemetry enabled
["extensions.shield-recipe-client.dev_mode", true],
["extensions.shield-recipe-client.first_run", false],
["app.normandy.dev_mode", true],
["app.normandy.first_run", false],
],
}),
withStub(RecipeRunner, "run"),
@ -360,8 +360,8 @@ decorate_task(
withPrefEnv({
set: [
["datareporting.healthreport.uploadEnabled", true], // telemetry enabled
["extensions.shield-recipe-client.dev_mode", false],
["extensions.shield-recipe-client.first_run", false],
["app.normandy.dev_mode", false],
["app.normandy.first_run", false],
],
}),
withStub(RecipeRunner, "run"),
@ -378,9 +378,9 @@ decorate_task(
withPrefEnv({
set: [
["datareporting.healthreport.uploadEnabled", true], // telemetry enabled
["extensions.shield-recipe-client.dev_mode", false],
["extensions.shield-recipe-client.first_run", true],
["extensions.shield-recipe-client.api_url", "https://example.com"],
["app.normandy.dev_mode", false],
["app.normandy.first_run", true],
["app.normandy.api_url", "https://example.com"],
],
}),
withStub(RecipeRunner, "run"),
@ -390,7 +390,7 @@ decorate_task(
await RecipeRunner.init();
ok(runStub.called, "RecipeRunner.run is called immediately on first run");
ok(
!Services.prefs.getBoolPref("extensions.shield-recipe-client.first_run"),
!Services.prefs.getBoolPref("app.normandy.first_run"),
"On first run, the first run pref is set to false"
);
ok(registerTimerStub.called, "RecipeRunner.registerTimer registers a timer");
@ -399,7 +399,7 @@ decorate_task(
// relies on the preferences it manages to actually change when it
// tries to change them. Settings this back to true here allows
// that to happen. Not doing this causes popPrefEnv to hang forever.
Services.prefs.setBoolPref("extensions.shield-recipe-client.first_run", true);
Services.prefs.setBoolPref("app.normandy.first_run", true);
}
);
@ -408,10 +408,10 @@ decorate_task(
withPrefEnv({
set: [
["datareporting.healthreport.uploadEnabled", true], // telemetry enabled
["extensions.shield-recipe-client.dev_mode", false],
["extensions.shield-recipe-client.first_run", false],
["extensions.shield-recipe-client.enabled", true],
["extensions.shield-recipe-client.api_url", "https://example.com"], // starts with "https://"
["app.normandy.dev_mode", false],
["app.normandy.first_run", false],
["app.normandy.enabled", true],
["app.normandy.api_url", "https://example.com"], // starts with "https://"
],
}),
withStub(RecipeRunner, "run"),
@ -425,19 +425,19 @@ decorate_task(
is(enableStub.callCount, 1, "Enable should be called initially");
is(disableStub.callCount, 0, "Disable should not be called initially");
await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.enabled", false]] });
await SpecialPowers.pushPrefEnv({ set: [["app.normandy.enabled", false]] });
is(enableStub.callCount, 1, "Enable should not be called again");
is(disableStub.callCount, 1, "RecipeRunner should disable when Shield is disabled");
await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.enabled", true]] });
await SpecialPowers.pushPrefEnv({ set: [["app.normandy.enabled", true]] });
is(enableStub.callCount, 2, "RecipeRunner should re-enable when Shield is enabled");
is(disableStub.callCount, 1, "Disable should not be called again");
await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.api_url", "http://example.com"]] }); // does not start with https://
await SpecialPowers.pushPrefEnv({ set: [["app.normandy.api_url", "http://example.com"]] }); // does not start with https://
is(enableStub.callCount, 2, "Enable should not be called again");
is(disableStub.callCount, 2, "RecipeRunner should disable when an invalid api url is given");
await SpecialPowers.pushPrefEnv({ set: [["extensions.shield-recipe-client.api_url", "https://example.com"]] }); // ends with https://
await SpecialPowers.pushPrefEnv({ set: [["app.normandy.api_url", "https://example.com"]] }); // ends with https://
is(enableStub.callCount, 3, "RecipeRunner should re-enable when a valid api url is given");
is(disableStub.callCount, 2, "Disable should not be called again");

View File

@ -1,88 +0,0 @@
"use strict";
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
ChromeUtils.import("resource://normandy/lib/ShieldRecipeClient.jsm", this);
ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
function withStubInits(testFunction) {
return decorate(
withStub(AboutPages, "init"),
withStub(AddonStudies, "init"),
withStub(PreferenceExperiments, "init"),
withStub(RecipeRunner, "init"),
withStub(TelemetryEvents, "init"),
testFunction
);
}
decorate_task(
withStubInits,
async function testStartup() {
const initObserved = TestUtils.topicObserved("shield-init-complete");
await ShieldRecipeClient.startup();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
await initObserved;
}
);
decorate_task(
withStubInits,
async function testStartupPrefInitFail() {
PreferenceExperiments.init.returns(Promise.reject(new Error("oh no")));
await ShieldRecipeClient.startup();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withStubInits,
async function testStartupAboutPagesInitFail() {
AboutPages.init.returns(Promise.reject(new Error("oh no")));
await ShieldRecipeClient.startup();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withStubInits,
async function testStartupAddonStudiesInitFail() {
AddonStudies.init.returns(Promise.reject(new Error("oh no")));
await ShieldRecipeClient.startup();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);
decorate_task(
withStubInits,
async function testStartupTelemetryEventsInitFail() {
TelemetryEvents.init.throws();
await ShieldRecipeClient.startup();
ok(AboutPages.init.called, "startup calls AboutPages.init");
ok(AddonStudies.init.called, "startup calls AddonStudies.init");
ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
}
);

View File

@ -22,7 +22,7 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [["extensions.shield-recipe-client.shieldLearnMoreUrl", "http://test/%OS%/"]],
set: [["app.normandy.shieldLearnMoreUrl", "http://test/%OS%/"]],
}),
withAboutStudies,
async function testLearnMore(browser) {

View File

@ -174,10 +174,16 @@ class MockPreferences {
preserve(name, branch) {
if (!(name in this.oldValues[branch])) {
const preferenceBranch = preferenceBranches[branch];
this.oldValues[branch][name] = {
oldValue: preferenceBranch.get(name),
existed: preferenceBranch.has(name),
};
let oldValue;
let existed;
try {
oldValue = preferenceBranch.get(name);
existed = preferenceBranch.has(name);
} catch (e) {
oldValue = null;
existed = false;
}
this.oldValues[branch][name] = {oldValue, existed};
}
}

View File

@ -26,7 +26,7 @@ class MockResponse {
function withServer(server, task) {
return withMockPreferences(async function inner(preferences) {
const serverUrl = `http://localhost:${server.identity.primaryPort}`;
preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`);
preferences.set("app.normandy.api_url", `${serverUrl}/api/v1`);
preferences.set(
"security.content.signature.root_hash",
// Hash of the key that signs the normandy dev certificates
@ -111,7 +111,7 @@ add_task(withMockApiServer(async function test_getApiUrlSlashes(serverUrl, prefe
// without slash
{
NormandyApi.clearIndexCache();
preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`);
preferences.set("app.normandy.api_url", `${serverUrl}/api/v1`);
const endpoint = await NormandyApi.getApiUrl("test-endpoint");
equal(endpoint, `${serverUrl}/test/`);
ok(mockGet.calledWithExactly(`${serverUrl}/api/v1/`), "trailing slash was added");
@ -121,7 +121,7 @@ add_task(withMockApiServer(async function test_getApiUrlSlashes(serverUrl, prefe
// with slash
{
NormandyApi.clearIndexCache();
preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1/`);
preferences.set("app.normandy.api_url", `${serverUrl}/api/v1/`);
const endpoint = await NormandyApi.getApiUrl("test-endpoint");
equal(endpoint, `${serverUrl}/test/`);
ok(mockGet.calledWithExactly(`${serverUrl}/api/v1/`), "existing trailing slash was preserved");